Files
sub2api-cn-relay-manager/docs/REAL_HOST_ACCEPTANCE_LEARNINGS.md
2026-05-23 17:34:53 +08:00

812 lines
38 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.
# 真实宿主验收经验与已调通细节
日期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`
- 对既有 channelCRM 需要走 `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`