# 真实宿主验收经验与已调通细节

日期：2026-05-21

## 目的

这份文档不替代 `docs/REAL_HOST_ACCEPTANCE_RUNBOOK.md`，而是把已经在线下真实打通过、以及多次踩坑后确认的细节沉淀下来，避免后续重复误判。

建议阅读顺序：
1. `docs/EXECUTION_BOARD.md` —— 看当前 gate 与最新真相
2. `docs/REAL_HOST_ACCEPTANCE_RUNBOOK.md` —— 看标准执行步骤
3. 本文 —— 看调试经验、误判点、诊断顺序

## 已经确认打通的事实

1. account 视角模型暴露可以正确落库
   - CRM 在 account 创建/导入时写入 `credentials.model_mapping`
   - 宿主 `GET /api/v1/admin/accounts/:id/models` 已能返回目标 provider 模型，而不是一律回退到 GPT 默认集合
   - DeepSeek / MiniMax 都已在 live 验收中确认

2. channel 视角模型映射与定价可以正确落库
   - channel 创建时需要同时下发：
     - `model_mapping`
     - `model_pricing`
     - `restrict_models=true`
     - `billing_model_source=channel_mapped`
   - 对既有 channel，CRM 需要走 `UpdateChannel` 做纠偏；这一点已在 latest-head fresh rerun 上确认生效
   - 旧现象“MiniMax channel 有 model_mapping 但没有 pricing”已经被 `ca1d448` 修复并完成 live 验证

3. subscription 场景的真实 probe key 语义已经确认
   - closure 最终用于宿主 `/v1/models` 探测的，不是外部传入的原始 `access_api_key`
   - 真正使用的是 CRM 在宿主侧创建/查找出来的 managed key（`sk-relay-*` 风格）
   - 因此 subscription 验收如果直接拿调用方原始 probe key 去打 `/v1/models`，出现 `403 not assigned to any group` 并不代表 CRM 主链路失败，而是 probe key 用错了
   - latest-head 当前实现已把 artifact 语义拆开：
     - `requested_probe_api_key` 记录调用方传入原始 key
     - `effective_probe_key_source=managed_subscription` 记录实际 gateway probe 来源
     - `effective_probe_key_fingerprint` 记录实际 probe key 指纹
     - `probe_api_key` 只继续保留给 `self_service`，不再在 `subscription` closure 里复用
   - 2026-05-23 的干净本地 fresh-host 验收 `artifacts/real-host-acceptance/20260523_local_clean_minimax_subscription_probe_semantics` 已再次证明这层语义修复生效：
     - closure 里 `requested_probe_api_key=sk-raw-probe-20260523b`
     - `effective_probe_key_source=managed_subscription`
     - 不再出现 legacy `probe_api_key`
     - 同一轮 raw key 直打宿主 `/v1/models` 与 `/v1/chat/completions` 仍都是 `403 permission_error`
   - 这轮 provider 最终仍是 `completion_status=429`，说明剩余阻断是 MiniMax 官方 upstream rate limit，不是 probe key 语义再次混淆
   - 继续在同一 fresh-host 上补的 MiniMax `M2.5` 缩圈验证，已经把 `429 -> 503` 的因果链单独坐实：
     - 单独只打一条 `MiniMax-M2.5-highspeed` 时，真实结果是 upstream `429`，见 `artifacts/real-host-acceptance/20260523_local_clean_minimax_m25_only_probe`
     - 连续第 1 次打 `M2.5` 时仍是 `429`
     - 紧接着第 2 次、第 3 次再打同一模型，会变成宿主 `503 Service temporarily unavailable`
     - 对应宿主日志显示：第一次有 `account_id=1` 和 `upstream_status=429`，后两次只剩 `account_select_failed error=\"no available accounts\"`
     - 因此 `M2.5` 的 `503` 不是模型自身固定返回 `503`，而是唯一账号被前一次 `429` 打进临时不可调度窗口后的宿主侧结果，见 `artifacts/real-host-acceptance/20260523_local_clean_minimax_m25_repeated_probe`

4. self_service 场景的 gateway probe 认证语义已经确认
   - 真实宿主的普通用户 gateway key 访问 `/v1/models` / `/v1/chat/completions` 时，使用的是 `Authorization: Bearer <gateway-key>`
   - 不能把这条普通用户 gateway key 当成宿主管理 API key 再塞进 `x-api-key`
   - latest-head 最后一个真实阻断就是这里：CRM 的 `CheckGatewayAccess` / `CheckGatewayCompletion` 之前错误地把 self_service 的普通用户 key 放进了 `x-api-key`
   - 修复后，latest-head `self_service` 标准 fresh-host 验收 `artifacts/real-host-acceptance/20260521_210403` 已真实收口到 `self_service_ready`

4. group 聚合视角与 account 单体视角必须分开看
   - `GET /api/v1/admin/accounts/:id/models` 是 account 单体视角
   - `GET /v1/models` 是普通用户 key + group 聚合视角
   - 二者语义不同，不能互相替代
   - 正确诊断顺序应该是：
     1) 先看 account models 是否正确
     2) 再看 managed key 视角 `/v1/models` 是否正确
     3) 最后才看 completion smoke 是否通过

## 宿主源码再次确认的设计逻辑

这部分不是基于 artifact 推断，而是直接对照 `sub2api-official-fresh` 宿主源码确认：

1. channel admin handler 的真实入参契约
   - `backend/internal/handler/admin/channel_handler.go`
   - `model_mapping` 的真实结构是 `map[string]map[string]string`
   - `model_pricing` 是独立数组字段，不会从 `model_mapping` 自动推导
   - `billing_model_source` 合法值包括 `channel_mapped`
   - `restrict_models` 是独立布尔开关

2. channel pricing platform 为空时，宿主会回退到 `anthropic`
   - create/update handler 都会在入参 platform 为空时补默认值
   - repository `createModelPricingExec` 也会把空 platform 写成 `anthropic`
   - 这意味着 CRM 若不给 OpenAI-compatible provider 显式写 platform，宿主会按 anthropic 语义处理，不能接受
   - 因此 CRM 当前策略必须是：
     1) 先用 provider platform
     2) 若调用侧仍为空，再回退 `openai`

3. gateway `/v1/models` 与 completion 共享同一套 API key middleware 前置校验
   - `backend/internal/server/middleware/api_key_auth.go`
   - 它先校验：
     - key 有效
     - user active
     - IP 限制
     - group / subscription / balance 前置
   - 所以 `/v1/models` 的 403/429 通常首先反映的是 key/group/subscription/balance 约束，而不等同于 account/channel 落库失败

4. subscription group 的 key 绑定条件与 standard group 不同
   - `backend/internal/service/api_key_service.go`
   - standard group：走 `user.CanBindGroup(...)`
   - subscription group：走 `GetActiveByUserIDAndGroupID(...)`
   - 也就是说，subscription 场景里“group 已存在”或“allowed_groups 已写入”都不够，必须有 active subscription

5. user 自助创建 key 与 admin 绑定 group 是两步
   - `backend/internal/handler/api_key_handler.go` + `api_key_service.go`
   - user 侧 `POST /api/v1/api-keys` 可创建带 `custom_key` 的 key
   - CRM managed key 流程里，先以普通用户身份创建 key，再用 admin `PUT /api/v1/admin/api-keys/:id` 绑定 group
   - 这与我们当前 `EnsureSubscriptionAccess` 的两阶段实现一致

## 已调通的宿主侧前置动作

### self_service

至少满足：
1. 普通用户已真实创建
2. 普通用户 key 已生成且可用
3. 该 key 已绑定目标标准 group
4. 用户具备可用余额

经验结论：
- 若只做了 key/group 绑定但没余额，`/v1/models` 可能从 403 进入 `INSUFFICIENT_BALANCE`
- 这不是 CRM 导入逻辑失败，而是宿主运营前置未完成
- 若普通用户 key 直打宿主 `/v1/models` / `/v1/chat/completions` 已经 `200`，但 CRM 的 self_service closure 仍显示 `401/403 broken`，优先检查 CRM 的 gateway probe 是否错误地复用了 `x-api-key` 语义，而不是先怀疑宿主前置

### subscription

至少满足：
1. 普通用户已真实创建
2. 普通用户 key 已生成且可用
3. 目标 group 是 `subscription` 类型
4. 普通用户已完成 subscription 分配
5. 普通用户 key 已绑定该 subscription group

经验结论：
- 只有管理员主体、只有 group、或者只有 subscription 记录都不够
- 必须把“普通用户 + key + group + subscription”整条链补齐，`/v1/models` 才会稳定通过

## 已确认的高频误判点

1. 把 `/accounts/:id/models` 和 `/v1/models` 混为一谈
   - 前者对，后者错，不代表 account 落库失败；往往是 key/group/subscription 问题

2. 用错 probe key
   - subscription 场景拿原始 `access_api_key` 直打宿主 `/v1/models`，很容易得到 403
   - 这时应先回到 closure 结果或 managed access 证据，而不是先否定产品链路

3. 旧 CRM 进程误导当前结论
   - live 行为必须先确认运行中的 CRM 进程是否真的包含最新提交
   - 之前 MiniMax existing channel 没自动补 `model_pricing`，最终确认根因就是在线 CRM 进程早于修复提交 `ca1d448`
   - 如果看到“有 `model_mapping` 但 `model_pricing=[]`”，不要立刻判定 current-code 仍未执行 `UpdateChannel`；先核对该 artifact 是否本来就是旧进程产物

### MiniMax `model_pricing=[]` 误判的已确认根因时间线

1. 旧进程先创建了半成品 channel
   - 证据：`artifacts/real-host-acceptance/20260520_222713_crm18100_live_model_mapping_validation/summary.json`
   - 其中 MiniMax `host_channel.data.id=5`
   - `model_mapping` 已有值，但 `model_pricing=[]`
   - 且 `created_at=updated_at=2026-05-20T20:39:23Z`
   - 这说明当时只是旧逻辑创建了 channel，没有发生后续 `UpdateChannel` 纠偏

2. 新代码已经具备纠偏能力，但必须由新进程实际执行
   - `ca1d448` 之后，代码路径已改为：
     - 新建 channel 时直接携带完整 `model_pricing`
     - 命中既有 channel 时执行 `UpdateChannel`
   - 所以判断“修复是否生效”时，不能只看仓库 HEAD，必须看 18100 监听进程的真实启动时间与实际 DB

3. 当前 18100 新进程已在 live host 上完成纠偏
   - 18100 新进程启动时间：`2026-05-21 01:08`
   - 当前真实 DB：`/tmp/sub2api-relay-manager-realhost-18100.db`
   - 当前 host admin 直查 `GET /api/v1/admin/channels/5` 可见：
     - `model_pricing` 非空
     - `model_mapping` 仍正确
     - `updated_at=2026-05-21T06:45:00Z`
   - 这证明新进程已经真正执行过 `UpdateChannel`，MiniMax 既有 channel 已被纠偏

4. 最终结论
   - “MiniMax channel 有 mapping 但无 pricing”不是 current-code 仍缺失 `UpdateChannel`
   - 真相是：旧 artifact 反映的是旧 CRM 进程产物；切到新进程并 fresh rerun 后，该问题已被 live 修复

4. `PACK_PATH` 使用了 operator 机器的概念路径，而不是 CRM 进程本机可读路径
   - 当 CRM 改在本机运行时，继续传远端 `/home/ubuntu/...` 会直接触发 `stat pack path ... no such file or directory`
   - 这个报错属于验收 harness / 环境参数问题，不是 import 业务逻辑问题

5. remote43/fresh-host 的 Postgres/Redis 容器目标写错
   - 若脚本仍打到旧 relaymgr 宿主，会看到 managed user / key / subscription 状态为空或串台
   - 需要确保脚本明确指向 fresh host 对应的 `{postgres,redis}` 容器

7. fresh-host bearer token 过期时，最前面的 host 注册/探测也会伪装成 CRM 侧 502
   - latest-head `self_service` 收尾时，脚本最前面的 `POST /api/hosts` 曾直接返回 `502`
   - 继续往里看，upstream detail 才显示 `TOKEN_EXPIRED`
   - 这类现象不要先误判成 CRM 新代码挂了；应先刷新 fresh-host 管理员 bearer token，再继续验收

6. 把 `/v1/models` 已通误认为 completion 也一定通
   - 这不成立
   - 当前最新真相就是：DeepSeek / MiniMax 的 `/v1/models` 可以 200，但 `/v1/chat/completions` 仍可能因为 host 兼容性或上游 quota 问题失败

## 推荐诊断顺序

### 一、先确认是不是环境/脚本问题

1. 确认当前运行 CRM 的提交版本与启动时间
2. 确认 `PACK_PATH` 是 CRM 本机可读路径
3. 确认 `CRM_HOST_BASE` 是否与实际 CRM 到 host 的可达地址一致
4. 确认脚本命中的 Postgres/Redis 容器属于目标 fresh host，而不是旧环境

### 二、再确认导入数据是否正确写入宿主

1. account
   - `GET /api/v1/admin/accounts/:id`
   - 看 `credentials.model_mapping`
2. account models
   - `GET /api/v1/admin/accounts/:id/models`
3. channel
   - `GET /api/v1/admin/channels/:id`
   - 看：
     - `model_mapping`
     - `model_pricing`
     - `restrict_models`
     - `billing_model_source`

### 三、最后再确认普通用户访问链路

1. self_service：看普通用户 key/group/balance
2. subscription：看 managed key / allowed_groups / user_subscriptions
3. 宿主 `/v1/models`
4. 宿主 `/v1/chat/completions`

## 如何解释常见现象

### 现象 A：`/accounts/:id/models` 正确，但 `/v1/models` 返回 403
优先判断：
- 普通用户 key 没绑定 group
- subscription 场景用错了原始 probe key
- subscription 分配或 allowed_groups 未完成

### 现象 B：`/v1/models` 返回 GPT 系模型，而不是目标国产模型
优先判断：
- account `credentials.model_mapping` 是否落库
- channel 是否同时具备 `model_mapping + model_pricing + restrict_models + billing_model_source=channel_mapped`
- 是否误打到了旧 CRM 进程

### 现象 C：`/v1/models` 已 200，但 `/v1/chat/completions` 失败
优先判断：
- host provider 兼容性
- 上游 key/quota
- 不要先回退归因为 CRM 导入失败

### 现象 D：普通用户 key 直打宿主 `/v1/models` 与 `/v1/chat/completions` 都是 200，但 CRM 的 `self_service` access/status 仍是 broken
优先判断：
- CRM 的 gateway probe 是否错误使用了 `x-api-key` 而不是 `Authorization: Bearer`
- 当前 online CRM 进程是否真的已经切到包含该修复的新二进制

## 2026-05-22 ~ 2026-05-23 多次反复出错后的最终收敛记录

这一节专门记录“不是一次性修掉，而是经过多轮误判、切环境、换宿主版本、补控制面自愈后才真正收口”的问题。后续再遇到相似现象，优先回看这里，不要重复从零推理。

### 1. Kimi A7M `/v1/models` 正常但 `/v1/chat/completions` 长期失败

最终确认这不是单一问题，而是两层问题叠加。

1. 第一层是宿主把 OpenAI-compatible API key account 默认判成可走 `/responses`
   - 表象：
     - upstream 直打 `/v1/chat/completions` = `200`
     - 经宿主转发后 `host /v1/chat/completions` = `502`
     - body 常见为 `Upstream access forbidden` 或 `service temporarily unavailable`
   - 真正根因：
     - 宿主把 `openai_responses_supported=true` 错写到 account capability
     - managed chat 请求被错误走成 Responses 兼容分支
     - 对 A7M 这类只稳定支持 raw chat-completions 的链路，会直接被上游拒绝

2. 第二层是宿主升级后 capability 误判会再次出现
   - 即使手工在宿主里把 `openai_responses_supported=false` 调对，后续异步 probe 或宿主升级仍可能覆写回错误值
   - 所以“只修宿主代码”不够稳，控制面必须有自愈

3. CRM 侧最终收口策略
   - access closure 首次确认时，如果看到：
     - account probe = `API returned 403: Forbidden`
     - host completion = `502 upstream_error`
     - body 含 `service temporarily unavailable` 或 `no available accounts`
   - CRM 会自动把对应 account 的 `openai_responses_supported=false` 写回宿主，然后立即重试一次 completion
   - 后台 reconcile 也复用同一逻辑，所以宿主升级后再次漂移，下一轮 confirm/reconcile 还能拉回正确状态

4. 已固化的回归层
   - `internal/access`：capability repair 判定与重试
   - `internal/provision`：首次安装后确认自愈
   - `internal/reconcile`：宿主升级后的后台持续自愈
   - 因此以后若再看到 “A7M `/models` 200 但 completion 502”，应先确认自愈逻辑是否触发，而不是先怀疑 pack 或 subscription 链路

### 2. `host_base_url` / stale CRM 进程 / fresh-host 容器串台 导致的假回归

这类问题在这轮里反复出现多次，而且表面上都像“代码修了但线上还是老问题”，实际是环境指向错了。

1. stale CRM 进程
   - 典型表象：
     - 仓库代码已经包含修复，但 live artifact 仍表现为旧逻辑
     - 最典型的是 “MiniMax channel 有 `model_mapping` 但 `model_pricing=[]`”
   - 真相：
     - 旧 artifact 反映的是旧进程创建的 channel
     - 新代码只有在新进程真的启动并重新跑过 import/update 后，宿主数据才会被纠偏

2. `host_base_url` 用成 operator 侧概念地址
   - 典型表象：
     - `stat pack path ... no such file or directory`
     - host 注册/导入看似失败，但实际上是 CRM 进程所在机器根本读不到该路径或访问不到该宿主地址
   - 真相：
     - `host_base_url` 和 `PACK_PATH` 都必须以 CRM 进程本机视角解释
     - 不能混用 operator 机器、remote43 主机、fresh-host 容器内部这三种地址空间

3. fresh-host Postgres/Redis 指到了旧容器
   - 典型表象：
     - managed user / subscription / key 状态看起来全部缺失
     - 或者 reconcile / group state 结果和当前验收宿主不一致
   - 真相：
     - harness 查的不是目标 fresh-host 的数据库，而是旧 relaymgr 或别的 fresh-host

4. 最终经验
   - 在判定“当前代码是否失效”前，必须先确认：
     1) CRM 在线进程启动时间
     2) CRM 实际提交版本
     3) `PACK_PATH` 是否对 CRM 本机可读
     4) `CRM_HOST_BASE` 是否真的是 CRM 到宿主的地址
     5) Postgres/Redis 容器是否属于目标 fresh-host

### 3. `models-only ready` 假阳性已经关闭，后续不能再按旧经验验收

这条误判在前几轮里也反复出现，必须明确写死。

1. 旧误判方式
   - 只要宿主 `/v1/models` 命中 `smoke_test_model`，就把 access 状态记成 ready
   - 这会把“普通用户 key / group / subscription 前置已完成”与“真实 completion 可用”混为一谈

2. 真实问题
   - `/v1/models = 200` 只能证明访问链路和宿主前置成立
   - 不证明上游 completion 一定可用
   - 在 DeepSeek、Kimi、MiniMax 的真实验收里，这一点都出现过

3. 当前收口后的真相
   - access ready 必须同时满足：
     - `/v1/models` 命中 `smoke_test_model`
     - 最小 `POST /v1/chat/completions` smoke 成功
   - access closure、import runtime artifact、reconcile rerun payload 现在都会持久化 completion 结果
   - 因此后续任何人看到 `latest_access_status=ready`，都可以默认它已经经过 completion 层验证

4. 回归建议
   - 若以后再改宿主 gateway 适配或第三方 provider capability，不要只验 `/v1/models`
   - 至少要一起看：
     - host `/v1/models`
     - host `/v1/chat/completions`
     - access closure details 里的 `completion_*` 字段

### 进一步缩圈：DeepSeek `chat/completions` 当前更像宿主兼容层问题，而不是 key 失效

2026-05-21 新增的直接证据链：

1. managed key 直打 fresh host 仍稳定失败
   - `http://127.0.0.1:18097/v1/models` = `HTTP 200`
   - `http://127.0.0.1:18097/v1/chat/completions` = `HTTP 502`
   - 说明普通用户 / subscription / key / group 绑定链路不是这一步的主阻断

2. 同一台 remote43 主机直打 upstream 反而成功
   - 对 `https://aitoken.quanfuli.cn/v1/chat/completions`
   - 使用同一 upstream key、同一 `deepseek-v4-flash` payload
   - 返回 `HTTP 200`
   - 但响应 `Content-Type` 是 `text/event-stream`

3. fresh-host app 日志显示 host chat 会在一组重复 DeepSeek accounts 间 failover，全部记成 `account_upstream_error 500/502`
   - 当前 group `5` 里有 10 个 active DeepSeek accounts：`14,15,16,17,19,20,23,25,26,28`
   - 它们 `credentials.api_key/base_url/model_mapping` 相同
   - 请求并不是命中一个固定坏 account，而是在重复 account 集合中轮流失败

当前最合理的解释：
- DeepSeek 这条 completion 阻断已经缩到“宿主 chat 上游兼容/解析层”
- 不是 CRM 没把模型、channel、subscription、managed key 准备好
- 重复 account 不是唯一根因，但会把一次失败放大成整组 failover 噪音，增加生产不稳定性

### 进一步缩圈：MiniMax 当前是 quota 阻断，不是 CRM 路由阻断

1. managed key 视角 `/v1/models` 已 200
2. upstream 直探 `/chat/completions` = `403 insufficient_user_quota`
3. fresh-host group `6` 内 6 个 active MiniMax accounts 的 `temp_unschedulable_reason` 都明确记录了 `insufficient_user_quota`

因此：
- MiniMax 当前要解的是“换可用 key / 补额度”
- 不应继续把它归因为 CRM import/access 逻辑失败
- 而且要区分两层失败：
  - 第一次 completion 失败是真实 upstream `429 insufficient_user_quota / rate_limit`
  - 同一账号冷却窗口内的后续 completion 失败，可能退化成宿主 `503 no available accounts`
- `20260523_local_clean_minimax_m25_only_probe` 与 `20260523_local_clean_minimax_m25_repeated_probe` 已证明：`429` 和后续 `503` 不是两个独立故障，而是同一条账号冷却链上的前后态

## 当前建议固化到后续文档/脚本的规则

1. 所有真实宿主验收结论都要同时记录：
   - account 视角结果
   - managed key 视角 `/v1/models` 结果
   - completion smoke 结果

2. 任何“MiniMax/DeepSeek 没生效”的结论前，必须先检查在线 CRM 是否为最新提交

3. 任何 subscription 验收脚本都不应默认把外部 `access_api_key` 当最终 probe key

4. 任何 fresh-host 验收脚本都必须参数化：
   - `PACK_PATH`
   - `CRM_HOST_BASE`
   - 目标 Postgres 容器
   - 目标 Redis 容器

5. latest-head `self_service` 验收通过后，如果 `reconcile` 仍是 `drifted`，应优先把它解释为 shared fresh-host 的历史残留资源噪音，而不是主链路未打通；判断时先看 `05-import.json` / `07-access-status.json` 的 ready 结果，再看 `09-reconcile.json` 的 `summary.access_status`

## 凭据与可用性判断矩阵

先记住：本项目里最容易混淆的不是 API 本身，而是“看起来都像 key，但其实职责完全不同”的几类凭据。

| 凭据/身份 | 属于谁 | 主要用途 | 正确验证方式 | 不能直接证明什么 | 最常见误判 |
|---|---|---|---|---|---|
| 供应链 key / 上游 key | 供应商账号 / 中转账号 | 写入 host account `credentials.api_key`，供宿主向上游 provider 发请求 | 1. account 创建成功 2. `POST /api/v1/admin/accounts/:id/test` 成功 3. `GET /api/v1/admin/accounts/:id/models` 返回目标模型 | 不能直接证明普通用户一定能看到模型，也不能直接证明 `/v1/chat/completions` 一定可用 | 把 account `/test` 成功误说成“普通用户已可用” |
| 普通用户 key | 宿主普通用户 | 走宿主网关访问 `/v1/models`、`/v1/chat/completions` | 1. key/group 绑定正确 2. `GET /v1/models` 返回目标模型 3. 推荐继续测 `/v1/chat/completions` | 不能直接证明供应链 account 本身健康 | 把普通用户 403 直接归因为供应链 key 无效 |
| subscription 原始外部 `ACCESS_API_KEY` | 调用方传入的外部 probe key | subscription 请求输入，可能只用于触发流程，不一定是最终探测 key | 不能单独用它判断最终 gateway closure；必须先确认是否被 managed key 覆盖 | 不能直接代表最终 subscription 场景的普通用户访问结果 | 拿它直打 `/v1/models` 收到 403，就误判 CRM 主链路失败 |
| managed key (`sk-relay-*`) | CRM 在宿主侧创建/查找的托管普通用户 key | subscription 场景最终 gateway probe / managed 普通用户访问 | 1. managed user / key 已创建 2. group/subscription 已绑定 3. `GET /v1/models` 返回目标模型 4. 推荐继续测 `/v1/chat/completions` | 不能直接证明上游 provider 一定有 quota 或 host completion 一定兼容 | 把它和外部原始 `ACCESS_API_KEY` 混为一谈 |
| host admin token / bearer | 宿主管理员 | 创建 group/channel/plan/account、分配 subscription、读取 admin API | 看 admin API 是否能成功执行管理动作 | 不能直接证明普通用户访问已可用 | 以为“管理接口全成功 = 普通用户链路也成功” |

### 一眼区分规则

1. 供应链 key
   - 验证的是“上游供应商 account 是否健康”
   - 不直接验证普通用户访问
2. 普通用户 key
   - 验证的是“宿主网关路径是否对普通用户开放”
   - 不直接验证上游供应链是否健康
3. subscription 原始外部 key
   - 只是一种流程输入
   - 不一定等于最终探测 key
4. managed key
   - 才是 subscription 场景里更接近“最终真实普通用户访问”的 key
5. admin token
   - 只证明管理面可用
   - 不证明用户面可用

### 两套最小判断口径

#### 口径 A：供应链账号是否成功
看：
1. account 是否创建成功
2. `/api/v1/admin/accounts/:id/test` 是否成功
3. `/api/v1/admin/accounts/:id/models` 是否返回目标模型

#### 口径 B：普通用户是否真的可用
看：
1. key/group/subscription/balance 前置是否到位
2. `/v1/models` 是否返回目标模型
3. `/v1/chat/completions` 是否成功

### 明确禁止的混用

- ❌ 用 account `/test` 成功替代普通用户 `/v1/models`
- ❌ 用 `/v1/models` 成功替代 `/v1/chat/completions`
- ❌ 用外部原始 `ACCESS_API_KEY` 替代 subscription managed key
- ❌ 用 admin API 成功替代普通用户链路成功
- ❌ 看到普通用户 403 就直接判定供应链 key 不可用

## FAQ：新增模型 / 新增供应链账号 / 普通用户访问

### 1. “新增模型参数”到底指什么？

这里至少分四层，不能混成一句“模型加上了”：

1. pack/provider 定义层
   - `base_url`
   - `default_models`
   - `smoke_test_model`
   - `channel_template.model_mapping`
2. host 落库层
   - account `credentials.model_mapping`
   - channel `model_mapping`
   - channel `model_pricing`
   - `restrict_models`
   - `billing_model_source`
3. 模型暴露层
   - `GET /api/v1/admin/accounts/:id/models`
   - `GET /v1/models`
4. completion 层
   - `POST /v1/chat/completions`

经验结论：
- 前三层正确，不等于第四层一定正确
- 当前项目最新真相就是：模型暴露层大体已经打通，但 completion 层仍可能受 host 兼容性或上游 quota 影响

### 2. “新增供应链账号成功”到底以什么为准？

建议区分三档成功标准：

1. 窄口径成功（只看供应链 account）
   - account 创建成功
   - `POST /api/v1/admin/accounts/:id/test` 成功
   - `GET /api/v1/admin/accounts/:id/models` 返回目标模型
2. 完整接入成功（看普通用户是否能看到模型）
   - 上述 account 条件成立
   - 普通用户或 managed key 的 `GET /v1/models` 返回目标模型
3. 业务可用成功（看真实调用）
   - 上述条件都成立
   - `POST /v1/chat/completions` 成功

经验结论：
- 如果你只说“供应链账号成功”，默认最多只能代表前两档
- 如果要说“模型完全可用”，必须把 completion smoke 也过掉

### 3. 新增供应链账号时，系统会不会自动补 group / channel / plan？

会，但要分资源类型看：

1. group
   - 一定会先 ensure
   - 不存在就创建，存在就复用
2. channel
   - 一定会先 ensure
   - 不存在就创建，存在就 `UpdateChannel` 纠偏
3. plan
   - 只在 `subscription` 模式下需要
   - 不存在就创建，存在就复用
4. account
   - 最后创建，并绑定到目标 `group_ids`

经验结论：
- 新增供应链账号不是“只加 account”
- 它本质上是“确保资源面完整后再挂 account”

### 4. 新增模型时，哪些字段必须同时对齐？

至少要对齐：
- provider `base_url`
- `default_models`
- `smoke_test_model`
- `channel_template.model_mapping`
- account `credentials.model_mapping`
- channel `model_mapping`
- channel `model_pricing`
- `restrict_models=true`
- `billing_model_source=channel_mapped`

经验结论：
- 少了 `model_mapping`，模型列表可能回退到默认集合
- 少了 `model_pricing`，`/v1/models` 可能看起来没问题，但实际聊天流量可能仍失败
- 只修 account 不修 channel，或者只修 channel 不修 account，都会留下半通不通的假阳性

### 5. 如果是“中转 URL / relay URL”，而且不在宿主官方已知库里，标准会不会不一样？

本项目的标准本质不变，但前提是：
- 这个 provider 必须先在本项目的 pack/provider manifest 中被正确定义

也就是说：
- “不在宿主官方库里”没关系
- “没有在本项目 pack 中定义”才不行

只要 manifest 正确提供了：
- `base_url`
- `default_models`
- `smoke_test_model`
- `channel_template.model_mapping`

系统就仍然可以自动做：
- account 创建
- account `/test`
- account `/models`
- 普通用户 `/v1/models`
- completion smoke（如果你把这一步也纳入验收）

经验结论：
- 系统不是“任意 URL 自动猜测器”
- 系统是“pack/provider 驱动的导入与验证器”

### 6. 只要 `/v1/models` 成功，是不是就说明新模型已经完全可用了？

不是。

`/v1/models` 成功，只能证明：
- 普通用户或 managed key 访问路径至少已经看到了模型列表

它不能自动证明：
- provider 上游有 quota
- host 对该 provider 的 completion 兼容性没问题
- `POST /v1/chat/completions` 一定能成功

经验结论：
- `/v1/models = 200` 是“模型暴露通过”
- `/v1/chat/completions = 200` 才更接近“模型可用通过”

### 7. 新增供应链账号后，普通用户 key 的 group 信息要不要更新？

通常要，取决于 access mode。

#### self_service
- 普通用户 key 必须绑定目标标准 group
- 如果新模型落在新的 group，而 key 没绑定过去，普通用户就看不到或用不到它
- 若目标 group 是标准计费组，通常还需要余额

#### subscription
- 目标 group 必须是 `subscription` 类型
- 普通用户必须完成 subscription 分配
- 普通用户 key 必须绑定该 group
- 当前 closure 最终优先使用宿主 managed key，而不是外部原始 `access_api_key`

经验结论：
- “新增了供应链模型”不等于“所有普通用户 key 自动获得访问权”
- 最终是否能访问，取决于 key/group/subscription 这条链是否同步完成

### 8. 新增供应链账号后，如果普通用户看不到模型，优先查哪里？

建议按这个顺序查：

1. account 视角
   - `GET /api/v1/admin/accounts/:id`
   - `GET /api/v1/admin/accounts/:id/models`
2. channel 视角
   - `GET /api/v1/admin/channels/:id`
   - 看 `model_mapping/model_pricing/restrict_models/billing_model_source`
3. 普通用户视角
   - `GET /v1/models`
4. completion 视角
   - `POST /v1/chat/completions`
5. 环境与运行时
   - CRM 是否是最新提交
   - `PACK_PATH` 是否正确
   - `CRM_HOST_BASE` 是否正确

经验结论：
- 不要一上来就看普通用户 403/502
- 先查 account 和 channel 落库，更容易快速定位根因

### 9. 什么时候应该判定是“运营前置没做”，而不是“导入代码失败”？

常见场景：

1. `self_service`
   - key 没绑 group
   - 用户没余额
2. `subscription`
   - group 不是 subscription 类型
   - user subscription 没写入
   - key 没绑 group
3. probe key 用错
   - subscription 场景拿外部原始 key 去打 `/v1/models`
4. 脚本参数错
   - `PACK_PATH` 错
   - 命中错的 Postgres/Redis 容器
   - probe auth 用错

经验结论：
- 如果 account `/models` 已对、channel 落库也对，但普通用户流量不对，优先怀疑运营前置或 harness 参数
- 不要立即重开“导入代码失效”的结论

### 13. remote43 如果同时有“本地 CRM + 远端宿主 + 远端 DB/Redis”，最容易错哪三件事？

2026-05-23 这一轮 remote43 `kimi-a7m` 复验把最容易反复出错的 3 个点彻底暴露出来了。

#### 1) 把 `CRM_HOST_BASE` 和 `REMOTE_HOST_BASE` 混成一个地址

- 本地运行的 CRM 访问宿主时，应该走本地 SSH 隧道，例如 `http://127.0.0.1:18089`
- 远端 SSH 内部执行 `curl` 或 `docker exec` 时，才应该走远端机器自己能看到的地址，例如 `http://127.0.0.1:18097`
- 如果把两者都写成 `18097`，本地 CRM 会尝试访问自己机器上的 `127.0.0.1:18097`，结果在 `POST /api/hosts` 阶段直接掉进 `500 internal_error`

这类错误的现象通常是：
- `01a-create-host.json` 为空
- `03-import.body.json` 直接是 `batch_id=0`
- message 落在 `get host version` 或 `probe host capabilities`

经验结论：
- **本地 CRM 到宿主的地址** 和 **远端 SSH 侧到宿主的地址** 必须分开记录
- 以后若脚本同时涉及 `curl CRM API` 和 `ssh remote curl host API`，必须显式区分 `CRM_HOST_BASE` 与 `REMOTE_HOST_BASE`

#### 2) 远端 DB/Redis 误指到 relaymgr 数据面

之前 remote43 统一 `401 INVALID_API_KEY` 的主因不是 provider key 坏，而是：
- 脚本错误地从 `sub2api-relaymgr-pg` 里找普通用户 key
- 但实际宿主是另一套 fresh-host app + postgres + redis

修正后脚本已经改为：
- 先按目标宿主端口解析远端 `app` 容器
- 再自动推导同栈的 `postgres/redis`

2026-05-23 的 `20260523_144937_remote43_kimi-a7m_key_import` 已证明这条修正生效：
- `subscription_user_key_prefix`、`managed_user_id`、`managed_probe_key_prefix` 都来自目标 fresh-host 数据面
- 不再复现统一 `401`

经验结论：
- 远端若同时存在 `relaymgr` 和 `fresh-host` 两套栈，**任何 subscription user / api key / group state / redis invalidation 都必须落到目标宿主自己的数据面**
- 不要再靠固定容器名假设

#### 3) provider status / access status 忘了带 `host_id`

当本地 CRM 状态库里同一个 provider 已经跑过多个 host 样本时：
- `GET /api/providers/{provider}/status`
- `GET /api/providers/{provider}/access/status`
- `POST /api/providers/{provider}/access/preview`

如果不显式带 `host_id`，很容易直接返回：
- `provider exists on multiple hosts; host_id is required`
- 外部看起来像验收在最后一步莫名其妙 `400`

经验结论：
- 这不是导入失败，也不是宿主坏了
- 这是 **状态查询维度不完整**
- 对带历史样本的 live CRM，所有 provider 尾部查询都应该带 `host_id`

### 14. `20260523_144937_remote43_kimi-a7m_key_import` 到底证明了什么？

这份 artifact 很关键，因为它把“脚本问题”和“宿主问题”拆开了。

它证明了：
- `POST /api/hosts` 已成功
- import 已成功返回 `HTTP 200`
- `gateway.models=["kimi-k2.6"]`
- `has_expected_model=true`
- upstream `/models=200`
- upstream `/chat/completions=200`

同时它也证明：
- 未改宿主的 host `/v1/chat/completions` 仍然返回 `503 Service temporarily unavailable`
- account probe 仍是 `403 Forbidden`，但已经只是 advisory / warning，不再阻断 import 主链

经验结论：
- 这份样本可以用来证明：**插件脚本的数据面/地址问题已经修掉**
- 它不能用来证明“宿主已经通过”
- 它应该被归类为：**插件侧修复完成，未改宿主 completion 路径仍异常**

### 15. 如果 create-host 阶段突然又回到 `500`，先查什么？

`20260523_145531_remote43_kimi-a7m_key_import` 提供了另一类重要样本：

- `01a-create-host.json` 仍成功
- 但 `03-import.body.json` 直接写明：
  - `get host version: perform GET /api/v1/admin/system/version request`
  - `context deadline exceeded`

这说明当时不是 provider key 坏，不是脚本回退，而是：
- 本地 `18089` 隧道虽然监听着端口
- 但到远端宿主的链路已经不再返回字节

经验结论：
- 如果 `host tunnel` 端口还在监听，但 `curl -I --max-time 5 $CRM_HOST_BASE/healthz` 无法返回任何 header
- 那就先把它当成 **隧道失活 / 运行时链路问题**
- 不要先把结论写成“导入逻辑回退”或“provider 又坏了”

### 10. 新增供应链账号或模型后，哪些结果可以算“已确认”，哪些只能算“部分确认”？

#### 可算“已确认”
- account 已创建
- account `/test` 成功
- account `/models` 返回目标模型
- channel 落库包含完整 routing/pricing 字段
- 普通用户 `/v1/models` 返回目标模型

#### 只能算“部分确认”
- 只有 import API 返回成功
- 只有 batch status 成功
- 只有 account 创建成功但还没测 `/models`
- 只有 `/v1/models` 成功但没测 `/v1/chat/completions`

经验结论：
- “新增模型成功”这句话必须说明你指的是哪一层成功
- 最容易误导人的说法，就是把“导入成功”直接说成“模型可用成功”

### 11. 如果新增供应链账号是复用旧 channel，而不是新建 channel，需要特别注意什么？

特别注意两件事：

1. 旧 channel 不能只复用名字
   - 必须做配置纠偏
   - 至少要补齐 `model_mapping + model_pricing + restrict_models + billing_model_source`
2. 不能默认“有 model_mapping 就够了”
   - 这正是之前 MiniMax live 问题踩过的坑

经验结论：
- 旧 channel 复用比新建更危险
- 因为它最容易留下“看起来有模型，实际上定价/路由没补齐”的半漂移状态

### 12. 如果我要把“新增模型/新增供应链账号”做成标准验收 checklist，最小应包含哪些项？

最小 checklist：

1. provider manifest 已更新
   - `base_url`
   - `default_models`
   - `smoke_test_model`
   - `channel_template.model_mapping`
2. import 成功
   - group/channel/(subscription: plan)/accounts 已生成或被正确复用
3. account 验证成功
   - `/test`
   - `/models`
4. channel 回读成功
   - `model_mapping`
   - `model_pricing`
   - `restrict_models`
   - `billing_model_source`
5. 普通用户路径验证成功
   - `/v1/models`
6. 业务路径验证成功（推荐）
   - `/v1/chat/completions`
7. 若失败，明确归类
   - provider definition drift
   - host compatibility
   - upstream quota/key 问题
   - key/group/subscription/balance 前置问题
   - harness 参数问题

## 相关证据入口

- 当前执行真相：`docs/EXECUTION_BOARD.md`
- 当前收口真相：`docs/PRODUCTION_CLOSURE_BOARD.md`
- 标准操作步骤：`docs/REAL_HOST_ACCEPTANCE_RUNBOOK.md`
- 关键 live 证据：
  - `artifacts/real-host-acceptance/20260520_222713_crm18100_live_model_mapping_validation`
  - `artifacts/real-host-acceptance/20260521_011544_remote43_minimax_key_import`
  - `artifacts/real-host-acceptance/20260521_011717_remote43_deepseek_key_import`
  - `artifacts/real-host-acceptance/20260521_064910_completion_smoke_calibration.md`
