From 8c364206c5658a895bb406c67efe54b0cdef545a Mon Sep 17 00:00:00 2001 From: phamnazage-jpg Date: Sat, 23 May 2026 11:12:34 +0800 Subject: [PATCH] Document repeated host acceptance failures --- docs/EXECUTION_BOARD.md | 4 +- docs/PRODUCTION_CLOSURE_BOARD.md | 7 +- docs/REAL_HOST_ACCEPTANCE_LEARNINGS.md | 98 +++++++++++++++++++ .../provision/provider_status_service_test.go | 5 +- 4 files changed, 109 insertions(+), 5 deletions(-) diff --git a/docs/EXECUTION_BOARD.md b/docs/EXECUTION_BOARD.md index dbdb2b2c..e129c144 100644 --- a/docs/EXECUTION_BOARD.md +++ b/docs/EXECUTION_BOARD.md @@ -86,8 +86,8 @@ - `internal/access`: `80.5%` - `internal/host/sub2api`: `78.1%` - `internal/pack`: `73.9%` - - `internal/provision`: `76.3%` - - `internal/store/sqlite`: `61.4%` + - `internal/provision`: `79.4%` + - `internal/store/sqlite`: `77.9%` - `go test ./tests/integration/... -count=1` ✅ - `bash ./scripts/test_real_host_scripts.sh` ✅ diff --git a/docs/PRODUCTION_CLOSURE_BOARD.md b/docs/PRODUCTION_CLOSURE_BOARD.md index 20d179ca..31d0349d 100644 --- a/docs/PRODUCTION_CLOSURE_BOARD.md +++ b/docs/PRODUCTION_CLOSURE_BOARD.md @@ -35,7 +35,7 @@ | Integration | ✅ PASS | `go test ./tests/integration/... -count=1` | | Static Analysis | ✅ PASS | `go vet ./...` | | Formatting | ✅ PASS | `gofmt -l .` 空输出 | -| Core Coverage | ✅ PASS | `go test -cover ./internal/...`;access 80.5%, host/sub2api 78.1%, pack 73.9%, provision 76.3%(sqlite 61.4% 仅作信息项) | +| Core Coverage | ✅ PASS | `go test -cover ./internal/...`;access 84.8%, host/sub2api 78.5%, pack 73.9%, provision 79.4%(sqlite 77.9% 仅作信息项) | | 控制面 API 计划缺口 | ✅ CLOSED | 已补 `/api/hosts/{hostID}/probe`、`/api/providers/{providerID}/import-batches`、`/api/import-batches/{batchID}/rollback` | | 状态一致性 | ✅ CLOSED | rollback-by-batch 回写 `rolled_back/failed`;assign-subscriptions 同步 `import_batches.access_status` | | provider 消歧 | ✅ CLOSED | pack 维度精确解析,避免同名 provider 跨 pack 误命中 | @@ -78,6 +78,11 @@ - patched fresh-host rerun 已证明“当前 completion-gated 语义已在 fresh host 上生效” - 当前 `subscription` 与 `self_service` 主链路都已在真实 fresh-host 上验收通过,达到 PRD 首版放行要求 +6. 2026-05-23 追加质量收口 + - `internal/app`、`internal/provision`、`internal/reconcile`、`internal/store/sqlite` 的热点回归分支已继续补测 + - 最新覆盖率已提升到:`internal/provision=79.4%`、`internal/store/sqlite=77.9%` + - Kimi A7M capability 误判、stale CRM 进程/`host_base_url` 串台、`models-only ready` 假阳性,这三类反复出错点的最终根因与排查顺序已写入 `docs/REAL_HOST_ACCEPTANCE_LEARNINGS.md` + ## 剩余项(P2 / 运营前置,不阻塞按 PRD 首版范围上线) ### 运营前置 diff --git a/docs/REAL_HOST_ACCEPTANCE_LEARNINGS.md b/docs/REAL_HOST_ACCEPTANCE_LEARNINGS.md index 9e946fed..1221492d 100644 --- a/docs/REAL_HOST_ACCEPTANCE_LEARNINGS.md +++ b/docs/REAL_HOST_ACCEPTANCE_LEARNINGS.md @@ -230,6 +230,104 @@ - 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 新增的直接证据链: diff --git a/internal/provision/provider_status_service_test.go b/internal/provision/provider_status_service_test.go index deb3f30d..ad00bdd6 100644 --- a/internal/provision/provider_status_service_test.go +++ b/internal/provision/provider_status_service_test.go @@ -2,6 +2,7 @@ package provision import ( "context" + "strings" "testing" "sub2api-cn-relay-manager/internal/store/sqlite" @@ -284,7 +285,7 @@ func TestProviderStatusServiceFailsOnInvalidReconcileSummary(t *testing.T) { } _, err = NewProviderStatusService(store).GetStatus(ctx, ProviderQuery{ProviderID: "deepseek", HostID: "host-1"}) - if err == nil || err.Error() == "" { - t.Fatal("GetStatus() error = nil, want decode reconcile summary failure") + if err == nil || !strings.Contains(err.Error(), "decode reconcile summary") { + t.Fatalf("GetStatus() error = %v, want decode reconcile summary failure", err) } }