# Real Host Acceptance Runbook

日期：2026-05-21

## 先读哪几份文档

1. `docs/EXECUTION_BOARD.md`
   - 当前 gate、最新阻断、最新 live 真相以它为准。
2. `docs/PRODUCTION_CLOSURE_BOARD.md`
   - 看是否已经达到可上线口径，以及哪些只是历史 PASS。
3. `docs/PROVIDER_ONBOARDING_PLAYBOOK.md`
   - 当你是在“新增 provider”或“宿主升级后重新适配”场景下工作时，先看这份。
   - 它定义稳定的 onboarding / rerun 顺序，而不是只定义一次性的验收动作。
4. `docs/REAL_HOST_ACCEPTANCE_CHECKLIST.md`
   - 每次 real-host 验收先走这一页。
   - 适合快速确认红线、三层证据和最短诊断顺序。
5. `docs/REAL_HOST_ACCEPTANCE_LEARNINGS.md`
   - 看已经调通的细节、典型误判点、推荐诊断顺序。
6. `docs/OPENCLAW_EXTERNAL_VALIDATION.md`
   - 当真实宿主、公网域名与最终用户 key 已经打通后，继续看这份。
   - 它定义的是 OpenClaw 最后一跳真实使用验证与升级后自检，不再局限于 relay-manager/host 导入层。

## 目标

把当前 `CONDITIONAL_APPROVED` 的剩余外部门禁收敛为一套可直接执行的真实宿主验收流程，覆盖：

1. 真实 sub2api 宿主接入探测
2. pack 安装
3. preview/import 验证
4. access preview / access status 验证
5. reconcile 验证
6. rollback smoke

## 前置条件

### 控制面
- `sub2api-cn-relay-manager` 已启动
- `CRM_BASE_URL` 可访问，例如 `http://127.0.0.1:8080`
- 已设置 `CRM_ADMIN_TOKEN`

### 真实宿主
- 已知真实宿主 `HOST_BASE_URL`
- 已知宿主管理认证：
  - `HOST_API_KEY` 或
  - `HOST_BEARER_TOKEN`
- 至少一个真实 provider key
- 已知 pack 路径，例如 `/app/packs/openai-cn-pack`

## 推荐执行方式

### 0. 先跑脚本回归自检（避免把 harness 漂移带进真实宿主结论）

```bash
cd /path/to/sub2api-cn-relay-manager
bash ./scripts/test/test_real_host_scripts.sh
```

说明：
- 当前推荐显式用 `bash` 调起，确保在不同机器上不会因为脚本执行位差异把 harness 回归误报成逻辑失败。
- 只有这一步通过后，再继续真实宿主验收。

### 1. 构建本地容器镜像（适用于代理/离线开发机）

```bash
cd /path/to/sub2api-cn-relay-manager
scripts/deploy/build_local_image.sh
```

默认输出：
- 二进制：`bin/sub2api-cn-relay-manager`
- 镜像：`sub2api-cn-relay-manager:local`

### 2. remote43 patched host / CRM 固定启动脚本

当目标是复现 2026-05-25 那条 `remote43 + patched overlay + remote CRM` 验收链路时，优先先跑固定脚本，不要再手工拼 `/tmp/*.sh`：

```bash
cd /path/to/sub2api-cn-relay-manager

HOST_BINARY=/path/to/sub2api-patched \
CRM_BINARY=./server \
bash ./scripts/deploy/setup_remote43_patched_stack.sh
```

脚本会：
- 把本地 pack 镜像到 `/tmp/openai-cn-pack-<stack>` 并同步到 remote43 同路径
- 把当前本地 `main` 分支打成 `git bundle`，并在 remote43 固定维护仓库工作副本：
  - `/home/ubuntu/sub2api-cn-relay-manager-git-current`
- 上传 patched 宿主二进制与当前 CRM server 二进制
- 在 remote43 拉起新的 Postgres / Redis / patched host
- 在 remote43 启动独立 SQLite 的临时 CRM
- 生成两个本地辅助文件：
  - `local operator env file`
  - `local tunnel script`

说明：
- 以后 provider 草稿发布链默认依赖这个固定 repo 根，不再依赖人工创建的时间戳 checkout 目录。

后续按脚本输出执行：

```bash
set -a; source /tmp/remote43-patched-stack-18139.env; set +a
bash /tmp/remote43-patched-stack-18139.tunnel.sh
```

然后再跑：

```bash
bash ./scripts/acceptance/import_remote43_provider.sh kimi-a7m kimi-k2.6 A7M_KIMI_API_KEY /path/to/keyfile
```

### 3. 先 dry-run 检查真实验收参数

```bash
CRM_BASE_URL=http://127.0.0.1:8080 \
CRM_ADMIN_TOKEN=replace-me \
HOST_NAME=prod-sub2api \
HOST_BASE_URL=https://sub2api.example.com \
HOST_API_KEY=host-admin-key \
PACK_PATH=/app/packs/openai-cn-pack \
PROVIDER_ID=deepseek \
KEYS=sk-live-1,sk-live-2 \
ACCESS_MODE=self_service \
ACCESS_API_KEY=user-gateway-key \
DRY_RUN=1 \
bash ./scripts/acceptance/real_host_acceptance.sh
```

### 4. 执行真实验收

```bash
CRM_BASE_URL=http://127.0.0.1:8080 \
CRM_ADMIN_TOKEN=replace-me \
HOST_NAME=prod-sub2api \
HOST_BASE_URL=https://sub2api.example.com \
HOST_API_KEY=host-admin-key \
PACK_PATH=/app/packs/openai-cn-pack \
PROVIDER_ID=deepseek \
KEYS=sk-live-1,sk-live-2 \
ACCESS_MODE=self_service \
ACCESS_API_KEY=user-gateway-key \
bash ./scripts/acceptance/real_host_acceptance.sh
```

### 5. 订阅模式示例

```bash
CRM_BASE_URL=http://127.0.0.1:8080 \
CRM_ADMIN_TOKEN=replace-me \
HOST_NAME=prod-sub2api \
HOST_BASE_URL=https://sub2api.example.com \
HOST_BEARER_TOKEN=host-bearer-token \
PACK_PATH=/app/packs/openai-cn-pack \
PROVIDER_ID=deepseek \
KEYS=sk-live-1 \
ACCESS_MODE=subscription \
SUBSCRIPTION_USERS=user-a,user-b \
SUBSCRIPTION_DAYS=30 \
bash ./scripts/acceptance/real_host_acceptance.sh
```

### 6. 导入后自动补 access 前置（可选）

当真实宿主需要额外完成“普通用户余额 / key-group 绑定 / 订阅写入 / 缓存失效”等宿主侧动作时，可在 import 完成后插入自定义 hook：

```bash
AFTER_IMPORT_HOOK_COMMAND='bash /path/to/host-access-hook.sh' \
... \
scripts/acceptance/real_host_acceptance.sh
```

hook 执行时会额外导出：
- `BATCH_ID`
- `BATCH_DETAIL_FILE`（若非 dry-run，会指向 `05a-batch-detail-pre-access.json`）
- `PROVIDER_ID`
- `HOST_BASE_URL`
- `CRM_BASE_URL`
- `ACCESS_MODE`
- `MODE`
- `ARTIFACT_DIR`

标准产物会新增：
- `05a-batch-detail-pre-access.json`
- `05b-after-import-hook.stdout.txt`
- `05b-after-import-hook.stderr.txt`

### 7. OpenClaw 最后一跳外部验证

当你已经确认：

- remote43 或本地 patched host 验收通过
- 公网根地址已暴露
- 真实用户可以自助注册并创建 key

不要在 `21-summary.json` 停止，继续追加一轮 OpenClaw 外部真实验证：

```bash
~/.openclaw/bin/apply-openclaw-minimax-proxy-fix.sh doctor
~/.openclaw/bin/openclaw-minimax-post-upgrade-check.sh

openclaw infer model run --local --model "tksea-gpt/gpt-5.4" --prompt "reply with pong only" --json
openclaw infer model run --local --model "tksea-gpt/gpt-5.4-mini" --prompt "reply with pong only" --json
openclaw infer model run --local --model "tksea-minimax/MiniMax-M2.5-highspeed" --prompt "reply with pong only" --json
openclaw infer model run --local --model "tksea-minimax/MiniMax-M2.7-highspeed" --prompt "reply with pong only" --json
```

当前推荐把这一步的口径记录到：

- `docs/OPENCLAW_EXTERNAL_VALIDATION.md`
- 或对应批次 artifact 的补充说明中

## 产物

脚本会把每一步 JSON 响应落到：

```text
artifacts/real-host-acceptance/<timestamp>/
```

### Artifact 安全模式

默认：

```bash
ARTIFACT_SECURITY_MODE=safe
ARTIFACT_INCLUDE_SECRETS=0
```

含义：
- `safe`：主 artifact 目录只允许落脱敏后的验收证据，可作为仓库内长期保留材料。
- `debug`：允许额外生成本地敏感调试材料；这类材料不得作为默认主证据提交或长期保留。
- `ARTIFACT_INCLUDE_SECRETS=1` 只允许用于本地短时调试；一旦开启，产物不再默认视为可入库证据。

`safe` 模式下的硬规则：
- 不落完整 upstream / managed / user API key
- 不落完整 bearer token
- 不落可直接复用的 SQL 明文 key 语句
- 不落 Redis cache key 原文
- header 文件必须去掉 `Authorization` / `Cookie` / `Set-Cookie` / `x-api-key`

默认文件顺序：
- `01-create-host.json`
- `02-probe-host.json`
- `03-install-pack.json`
- `04-preview-import.json`
- `05-import.json`
- `05a-batch-detail-pre-access.json`（若拿到了 `batch_id` 且非 dry-run）
- `05b-after-import-hook.stdout.txt` / `05b-after-import-hook.stderr.txt`（若配置了 hook）
- `06-access-preview.json`
- `07-access-status.json`
- `08-provider-status.json`
- `09-reconcile.json`
- `10-batch-detail.json`
- `11-rollback.json`（若未跳过）

remote43 / 本地缩圈脚本若需要额外证据，会在同目录追加：
- `00-local-key-source.json`（只保留 redacted key 指纹/前后缀）
- `01-runtime-context.json`（仅保留 hash 后的 user/admin/managed 身份）
- `05-subscription-access-prep.summary.json`（替代默认明文 SQL）
- `07-redis-targeted-invalidation.json`（只保留失效动作结果，不保留 cache key 原文）
- `08-subscription-group-state.json`（已裁剪 key 明文）
- `21-summary.json` / `99-semantic-summary.json`（推荐长期保留的摘要证据）
- 若你是在清理旧目录，而不是生成新验收产物，优先运行：
  ```bash
  python3 scripts/acceptance/migrate_historical_artifacts.py artifacts/real-host-acceptance
  ```
  它会把主目录中的历史敏感材料迁到 `artifacts/real-host-acceptance-sensitive/`，并在原目录生成安全摘要版。
- 历史目录迁移脚本当前已覆盖两层：
  1. 固定命名标准 artifact（runtime-context / key-source / redis invalidation / group-state / sql summary / headers）
  2. 复杂业务快照与 JSON-in-string 字段（`summary.json`、`99-summary.json`、`99-semantic-summary.json`、`05a-batch-detail-pre-access.json`、`07-access-status.json`、`10-batch-detail.json` 以及其中的 `DetailsJSON/details_json/probe_summary_json`）
- 若迁移后仍看到类似 `00-managed-key-corrected.txt` 的手工 probe 文本，它们属于非标准人工产物，当前仍建议迁到 `artifacts/real-host-acceptance-sensitive/` 或直接删除。 
## 通过标准

至少同时满足：

1. `probe-host` 返回宿主版本与 capability 快照
2. `install-pack` 成功
3. `import` 返回 `batch_id`，且 batch/provider 状态不为 `failed`
4. `access-preview` 返回 `available=true` 或 access status 进入：
   - `subscription_ready`
   - `self_service_ready`
   - `fully_ready`
5. `reconcile` 不返回关键失败
6. `rollback smoke` 成功（若本次需要验证回滚链路）

## 推荐额外落盘的三层证据

为了避免把不同语义混为一谈，真实宿主验收建议每次都额外记录下面三层证据：

1. account 单体视角
   - `GET /api/v1/admin/accounts/:id`
   - `GET /api/v1/admin/accounts/:id/models`
2. group/普通用户聚合视角
   - 用真实可用的普通用户 key 或 subscription managed key 请求 `GET /v1/models`
3. completion smoke
   - 用同一条普通用户访问链路请求 `POST /v1/chat/completions`

注意：
- `GET /api/v1/admin/accounts/:id/models` 正确，不等于普通用户 `/v1/models` 一定正确
- `/v1/models` 正确，也不等于 `/v1/chat/completions` 一定正确
- 这三层必须分开记证据、分开下结论

## 当前门禁解释

- 若以上脚本在真实宿主环境全部通过：
  - 可以把当前项目推进到 **真实环境放行**
- 若脚本未执行：
  - 仍然不能把最新代码直接视作真实宿主已放行
- 若脚本执行但失败：
  - 失败应被归类为真实宿主兼容性 / 凭据 / 网络 / pack 内容问题，而不是再泛化成“代码是否已完成”

## 注意事项

1. 默认会执行 rollback smoke；若当前环境不允许回滚，设置：

```bash
SKIP_ROLLBACK=1 scripts/acceptance/real_host_acceptance.sh
```

2. `PACK_PATH` 必须是控制面进程可读路径，不是用户本地概念路径。
3. 如果控制面部署在容器中，确保 pack 目录已经挂载进去。
4. `HOST_API_KEY` 与 `HOST_BEARER_TOKEN` 二选一即可；脚本会自动推导 `auth.type=apikey|bearer`。
5. `ACCESS_API_KEY` 必须使用真实未脱敏的普通用户 gateway key；不能直接复用数据库/列表接口中的展示值。
6. 真实宿主初始化只会准备管理员账号；普通用户账号/密码不会自动生成，验收前必须显式创建并留存可复用凭据。
7. `self_service` 验证除普通用户 key 外，还需要该 key 绑定目标 group；若目标 group 是标准计费组，还需要用户侧具备可用余额，否则 `/v1/models` 可能从“未授权”转为 `INSUFFICIENT_BALANCE`。
8. `subscription` 验证需要目标 group 本身是 `subscription` 类型，并且完成“普通用户订阅分配 + 普通用户 key 绑定该 group”；仅有管理员主体或未绑定 key 不足以通过 `/v1/models`。
9. 若需要验证 `reconcile` 收敛，优先在干净宿主场景或隔离 group 下执行，避免历史残留资源把结果污染成 `status=drifted` / `extra_count>0`。
10. `scripts/acceptance/import_remote43_provider.sh` 现已内置 remote43 的 subscription 验收补全动作：会根据 import batch 自动解析目标 group，执行“普通用户最低余额补齐 + key/group 绑定 + user_subscriptions upsert + 定向 Redis 缓存失效（auth / balance / subscription）”，并把 SQL / host state 证据写入 artifact 目录。
11. 当 CRM 进程与 operator 到 host 的访问地址不一致时，优先显式设置 `CRM_HOST_BASE`，避免把 CRM 侧探测地址和本地运维隧道地址混用。
12. 对 `Upstream service temporarily unavailable` 一类 502，不要先认定是上游聊天链路故障；先看脚本落盘的 `09-models.headers.txt` / `10-models.body.json`。若 `/v1/models` 已返回了别的 provider 模型集（例如 GPT 系列而不是预期的 DeepSeek/Minimax 模型），先检查普通用户 key/group 绑定，也要检查 CRM 导入时是否把 provider 的 `channel_template.model_mapping`、`restrict_models`、`billing_model_source` 一并下发到宿主 channel。
13. subscription 场景里，closure 最终用于 gateway probe 的 key 是宿主侧 managed key，不一定是请求体里外部传入的 `ACCESS_API_KEY`。如果你拿原始 key 直打 `/v1/models` 收到 `403 not assigned to any group`，先不要判定 CRM 主链路失败。
14. 对“既有 channel 没自动补 `model_pricing`”这类 live 现象，先核对在线 CRM 进程的启动时间与 git 提交时间；之前 MiniMax 的该现象最终被确认是 stale CRM 进程导致，而不是源码缺逻辑。
15. 当 CRM 切换到本机运行时，`PACK_PATH` 也必须切换到控制面本机可读路径；继续沿用远端 `/home/ubuntu/...` 会触发 `stat pack path ... no such file or directory`，这是验收 harness 参数问题，不是导入业务逻辑问题。
16. 若要把 DeepSeek 的“host `/v1/models`=200 但 host `/v1/chat/completions`=502，而 upstream 直探 `/chat/completions`=200”做成可提 issue 的最小复现，直接运行 `scripts/acceptance/check_deepseek_completion_split.sh`。它会同时落盘 host `/v1/models`、host `/v1/chat/completions`、upstream `/chat/completions` 三层证据，并在 `summary.json` 里给出 `host_compatibility_gap|upstream_key_quota_issue|unknown` 分类。
17. 若 reconcile 面对的是“非 latest batch 的同前缀旧账号”，最新代码会把它们记为 `stale_noise_count` / `stale_noise_accounts` 并保留 `raw_extra_count`，而不是继续把它们算进 `extra_count` 造成 drift 误报；因此应优先看 `extra_count` 是否归零，再看 `probe_failures`/`access_status` 是否仍有真实异常。
18. self_service 场景里，普通用户 gateway key 访问宿主 `/v1/models` / `/v1/chat/completions` 时，真实语义是 `Authorization: Bearer <gateway-key>`；若 CRM 的 self_service closure 仍显示 `401/403 broken`，优先排查 gateway probe 是否错误复用了 `x-api-key`。
19. fresh-host 管理员 bearer token 过期时，最前面的 `POST /api/hosts` / `probe-host` 可能直接表现成 CRM 侧 `502`。遇到这类现象，先刷新 host bearer token，再继续验收，不要先把它归因为最新代码故障。
20. shared fresh-host 上若 `05-import.json` / `07-access-status.json` 已经 ready，而 `09-reconcile.json` 仍是 `status=drifted`，优先把它解释为历史残留资源噪音；PRD 首版放行判断应以 import/access 闭环是否打通为主。
21. 如果 CRM 本身运行在本机，而宿主运行在远端 SSH 隧道后面，必须同时明确 2 个地址：
   - `CRM_HOST_BASE`：本地 CRM 实际访问宿主时使用的地址，例如 `http://127.0.0.1:18089`
   - `REMOTE_HOST_BASE`：远端 SSH 会话内部访问宿主时使用的地址，例如 `http://127.0.0.1:18097`
   - 两者不能混用；混用后 `POST /api/hosts` 往往会先在 `get host version` / `probe host capabilities` 处直接变成 `500`
22. 如果远端同时存在 `relaymgr` 和 `fresh-host` 两套容器栈，不要手填 `REMOTE_PG_CONTAINER` / `REMOTE_REDIS_CONTAINER` 到旧的 relaymgr 容器。优先按目标宿主端口自动解析同栈的 `postgres/redis`；否则最容易回到“错库取 key 导致统一 401”。
23. 若同一个 provider 已在本地 CRM 状态库里跑过多个宿主样本，尾部查询必须带 `host_id`：
   - `GET /api/providers/{provider}/status`
   - `GET /api/providers/{provider}/access/status`
   - `POST /api/providers/{provider}/access/preview`
   - 否则很容易在最后一步收到 `provider exists on multiple hosts; host_id is required`
24. 不要把“隧道端口还在 LISTEN”误判成“链路可用”。
   - 若 `curl -I --max-time 5 $CRM_HOST_BASE/healthz` 完全收不到 header
   - 就应先判定为 tunnel 失活或远端链路异常
   - 这类现象会在 `03-import.body.json` 中表现为 `get host version ... context deadline exceeded`
25. 当前 artifact 安全模式默认是 `safe`：
   - 主目录 `artifacts/real-host-acceptance/` 只能保留脱敏后证据
   - 若为了缩圈必须看原始 SQL / headers / token 相关细节，应显式切到 `ARTIFACT_SECURITY_MODE=debug` 并把产物视为本地敏感材料，不进入仓库主证据区

## 建议固定执行的快速诊断顺序

1. 先看环境
  - CRM 是否是最新提交对应的在线进程
  - `PACK_PATH` 是否是 CRM 本机可读路径
  - `CRM_HOST_BASE` 是否与 CRM 到 host 的实际访问地址一致
  - 如果走 SSH 隧道，`CRM_HOST_BASE` 是否真的可在本机 `curl -I --max-time 5` 读到响应
  - 若脚本还要在 SSH 会话里执行 host probe，`REMOTE_HOST_BASE` 是否与远端主机看到的地址一致
2. 再看宿主落库
  - account `credentials.model_mapping`
  - `GET /api/v1/admin/accounts/:id/models`
  - channel `model_mapping/model_pricing/restrict_models/billing_model_source`
3. 最后看普通用户流量
   - `/v1/models`
   - `/v1/chat/completions`

若需要背景解释、误判案例和已调通经验，直接看：`docs/REAL_HOST_ACCEPTANCE_LEARNINGS.md`
