Files
sub2api-cn-relay-manager/docs/REAL_HOST_ACCEPTANCE_RUNBOOK.md
2026-05-28 10:13:13 +08:00

374 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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. 固定命名标准 artifactruntime-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`