# V2 技术架构 — Batch Auto-Import 日期:2026-05-22 状态:设计中 关联文档: - `docs/2026-05-21-BATCH_AUTO_IMPORT_SPEC.md` - `docs/2026-05-21-BATCH_AUTO_IMPORT_TDD_PLAN.md` - `docs/openapi.yaml` ## 1. 文档目标 这份文档只回答 4 个问题: 1. V2 的后端组件如何分层 2. 运行态状态如何持久化并保证稳定推进 3. 结果页到底展示什么、从哪张表读 4. 如何把 run / item / retry / advisory / validation 收口成单一真相 ## 2. 核心原则 V2 必须同时满足: 1. **可恢复**:控制面重启后 run/item 仍可查看,unfinished item 可继续推进 2. **可观测**:页面和 API 不依赖日志拼接,而依赖 canonical state store 3. **可解释**:warning/broken 必须给出可读原因 4. **可分层**:Probe、Provision、Confirm、Validate 各司其职 5. **可兼容**:针对第三方 OpenAI-compatible 上游,显式记录 transport + model capability 6. **可复用**:重复导入命中同 URL + 同模型家族时,优先复用已有 provider/account,而不是重复创建 ## 3. Canonical runtime model ### 3.1 单一真相 V2 的单一真相是控制面自己的三类表: - `import_runs` - `import_run_items` - `import_run_item_events` 现有表: - `import_batches` - `import_batch_items` - `probe_results` - `access_closure_records` - `managed_resources` 在 V2 中只承担两类职责: 1. 资源关联和 legacy 追溯 2. 对现有 v1 行为的兼容 它们不再是结果页与 V2 API 的主数据源。 ### 3.2 状态归属 - `run.state`:只属于 `import_runs` - `item.current_stage / confirmation_status / access_status`:只属于 `import_run_items` - `retry trail / advisory / stage transition`:只属于 `import_run_item_events` ## 4. 组件分层 ```text operator input ↓ Batch Import API / CLI ↓ BatchImportService ├── Reuse Preflight ├── Probe Layer ├── Capability Profiler ├── Provision Adapter ├── RunStateStore └── ConfirmationQueue ↓ ConfirmationWorker ↓ ValidationService ↓ ResultProjection ↓ HTTP API / Result Pages / CLI wait output ``` ### 4.1 Batch Import API / CLI 职责: - 接收 `BatchImportRunRequest` - 校验 `access_mode` 必填输入 - 创建 run 与 item - 触发 `BatchImportService` - 可选等待一个短暂窗口,但不负责长期 confirm ### 4.2 BatchImportService 职责: - 执行 Reuse Preflight - 执行 Stage 1 Probe - 执行 Stage 2 Provision - 把 item 推进到 `confirm` - 将确认任务交给后台 worker 不负责: - 把每个 item 长时间阻塞在请求线程里等待最终稳定 ### 4.3 ConfirmationWorker 职责: - 扫描需要确认的 item - 吸收 probe race / warmup 窗口 - 将 item 从 `pending` 推进到 `confirmed/advisory/failed` - 推进到 validate 阶段 ### 4.4 ValidationService 职责: - 唯一写入 `access_status` - 使用宿主 gateway 真实 `/v1/chat/completions` - 产生 `active/degraded/broken` ### 4.5 ResultProjection 职责: - 将底层运行态投影为页面/API 视图 - 统一状态 badge / warning 文案 / item 摘要 ## 5. State machine ### 5.1 Run state `run.state`: - `running` - `completed` - `completed_with_warnings` - `failed` - `cancelled` Run 级 projection 规则: - 只要存在 `broken` item,且未完成恢复,则 run 可能 `failed` - 无 broken 但存在 advisory/degraded item,则 run 为 `completed_with_warnings` - 全部 item 为 `confirmed/active`,则 run 为 `completed` ### 5.2 Item lifecycle `item.current_stage`: - `probe` - `provision` - `confirm` - `validate` - `done` `item.confirmation_status`: - `pending` - `confirmed` - `advisory` - `failed` `item.access_status`: - `unknown` - `active` - `degraded` - `broken` `item.matched_account_state`: - `none` - `active` - `disabled` - `deprecated` - `broken` `item.account_resolution`: - `created` - `reused` - `reactivated` - `replaced` ### 5.3 State ownership | 字段 | 写入者 | |---|---| | `current_stage` | service / worker / validation | | `confirmation_status` | confirmation worker | | `access_status` | validation service | | `run.state` | result projection / run aggregator | ## 6. Request architecture ### 6.1 Canonical request ```text BatchImportRunRequest - host_id - mode - access_mode - confirm_wait_timeout_sec - subscription_users - subscription_days - probe_api_key - entries[] ``` ### 6.2 Access-mode matrix | access_mode | 必填字段 | 用途 | |---|---|---| | `subscription` | `subscription_users`, `subscription_days` | 订阅绑定和闭环验证 | | `self_service` | `probe_api_key` | gateway key 验证 | 这一步必须在入口层就校验,不能把不完整请求放进 worker 后再失败。 ## 7. Capability architecture ### 7.1 两层 capability V2 不再把 capability 当成“一个 key 一个总画像”,而是拆成: 1. `transport_profile` 2. `model_profiles[]` ### 7.2 为什么必须按模型维度记录 目标是满足“快速匹配兼容模型”的运营需求。 如果只有 upstream 级总画像,无法表达: - 同 upstream 下模型 A 可 stream,模型 B 不可 - 同 upstream 下模型 A 支持 reasoning 字段,模型 B 不支持 - 同 upstream 下模型 A smoke 通过,模型 B 失败 ### 7.3 Canonical JSON ```json { "transport_profile": { "supports_openai_models": true, "supports_openai_chat_completions": true, "supports_openai_responses": false, "supports_anthropic_messages": false, "auth_style": "bearer", "model_id_style": "vendor_prefixed", "known_advisories": [ "responses_unsupported_but_chat_ok", "initial_probe_race_expected" ] }, "model_profiles": [ { "raw_model_id": "kimi-k2.6", "normalized_model_id": "kimi-k2.6", "canonical_model_family": "kimi-k2.6", "supports_stream": true, "supports_tools": "unknown", "supports_reasoning_fields": "unknown", "smoke_chat_ok": true } ] } ``` ### 7.4 Canonical model family 的作用 `normalized_model_id` 只能解决字符串归一化,不能稳定回答跨中转的“是否同一模型家族”。 V2 需要额外记录 `canonical_model_family`,用于识别这类情况: - `kimi 2.6` - `kimi-2.6` - `kimi-k2.6` 它们可能属于同一个模型家族,应支持: 1. 重复导入时快速匹配 2. 已存在 provider 只 patch 别名映射,不重复 provision 3. 结果页解释“为何这次被直接复用” ## 8. State store schema ### 8.1 `import_runs` ```text run_id TEXT PRIMARY KEY mode TEXT NOT NULL access_mode TEXT NOT NULL state TEXT NOT NULL total_items INTEGER NOT NULL completed_items INTEGER NOT NULL active_items INTEGER NOT NULL degraded_items INTEGER NOT NULL broken_items INTEGER NOT NULL warning_items INTEGER NOT NULL started_at DATETIME NOT NULL updated_at DATETIME NOT NULL finished_at DATETIME NULL ``` ### 8.2 `import_run_items` ```text item_id TEXT PRIMARY KEY run_id TEXT NOT NULL base_url TEXT NOT NULL provider_id TEXT NOT NULL api_key_fingerprint TEXT NOT NULL requested_models_json TEXT NOT NULL raw_models_json TEXT NOT NULL normalized_models_json TEXT NOT NULL canonical_model_families_json TEXT NOT NULL resolved_smoke_model TEXT NULL recommended_models_json TEXT NOT NULL capability_profile_json TEXT NOT NULL current_stage TEXT NOT NULL confirmation_status TEXT NOT NULL access_status TEXT NOT NULL matched_account_state TEXT NOT NULL account_resolution TEXT NOT NULL provision_reused INTEGER NOT NULL reused_from_provider_id TEXT NULL reused_from_account_id INTEGER NULL channel_id INTEGER NULL account_id INTEGER NULL retry_count INTEGER NOT NULL confirmation_attempts INTEGER NOT NULL last_retry_at DATETIME NULL next_retry_at DATETIME NULL lease_owner TEXT NULL lease_until DATETIME NULL advisory_messages_json TEXT NOT NULL last_error_stage TEXT NULL last_error TEXT NULL legacy_batch_id INTEGER NULL legacy_provider_id TEXT NULL created_at DATETIME NOT NULL updated_at DATETIME NOT NULL ``` 关键约束: - `resolved_smoke_model` 可为 `NULL`,因为 Stage 1 可能失败 - `channel_id/account_id` 可为 `NULL`,因为 Stage 2 可能未开始 - `access_status` 初始必须允许 `unknown` - `api_key_fingerprint` 只存指纹,不存明文 key - `canonical_model_families_json` 是 Reuse Preflight 的核心输入,而不是附带展示字段 - `matched_account_state` 用于结果页直接展示“重复已启用 / 已弃用待启用 / 已重新启用” - `account_resolution` 用于解释这次 item 最终是创建、复用、快速启用还是替换 ### 8.3 为什么必须有 key fingerprint 只靠 `provider_id` 不能稳定区分: - 同一个 URL 下是否是同一把 key - 是真正的重复导入,还是同 URL 的新账号 因此 V2 必须显式落: - `api_key_fingerprint` - `provision_reused` - `reused_from_provider_id` - `reused_from_account_id` 这样 Reuse Preflight 才能先按: 1. `host_id + provider_id` 2. `host_id + base_url + api_key_fingerprint` 3. `canonical_model_families` 做稳定判定。 ### 8.4 已存在账号状态的标准化 对命中的既有账号,V2 需要统一投影为: - `active` - 当前账号已启用,可直接使用 - `disabled` - 当前账号被停用,但可尝试快速启用 - `deprecated` - 当前账号不推荐继续调度,但仍可由 operator 重新启用 - `broken` - 当前账号不可直接复用 对应本次导入的处理结果: - `created` - `reused` - `reactivated` - `replaced` ### 8.5 `import_run_item_events` ```text event_id TEXT PRIMARY KEY run_id TEXT NOT NULL item_id TEXT NOT NULL event_type TEXT NOT NULL stage TEXT NOT NULL attempt INTEGER NOT NULL message TEXT NOT NULL payload_json TEXT NOT NULL created_at DATETIME NOT NULL ``` 事件类型示例: - `stage_transition` - `retry_scheduled` - `advisory_added` - `validation_result` ### 8.6 为什么必须有 event 表 仅靠 `retry_count` 不足以支撑结果页要求。 页面要能展示: - 第几次重试 - 在哪个阶段重试 - 为什么进入 advisory - warning/broken 最终解释 这必须依赖 event trail。 ## 9. Confirmation worker design ### 9.1 轮询条件 worker 每次 Tick 只捞: - `current_stage='confirm'` - `confirmation_status='pending'` - `next_retry_at IS NULL OR next_retry_at <= now` - `lease_until IS NULL OR lease_until < now` ### 9.2 Lease 机制 为避免多个 worker 重复确认,同一 item 需要: - `lease_owner` - `lease_until` worker 成功抢到 lease 后才能执行 confirm。 ### 9.3 Retry policy 建议默认: - probe race `403`:advisory,不立即失败 - `503 no available accounts`:短暂指数退避,最多 N 次 - definitive `401/403 unauthorized`:立即失败 每次 retry 都写 event。 ### 9.4 Restart safety V2 要求: - worker 重启后自动接管过期 lease 的 item - unfinished item 不需要人工恢复 - CLI 超时退出不影响后台继续推进 ## 10. Validation architecture ### 10.1 唯一职责 ValidationService 只做一件事: - 对已经完成 confirm 的 item 执行最终 gateway completion 验证 ### 10.2 Outcome mapping | confirmation_status | gateway result | access_status | |---|---|---| | `confirmed` | `200` | `active` | | `advisory` | `200` | `active` | | `confirmed`/`advisory` | transient but exhausted | `degraded` | | `failed` | any | `broken` | | any | definitive invalid path | `broken` | ## 11. Result projection ### 11.1 Projection fields 结果页/API 不再自己拼原始表字段,Projection 层统一输出: - run summary - item table row - item detail - warning explanation - badge color mapping ### 11.2 Warning 文案模板 建议至少固化: - `responses_unsupported_but_chat_ok` - `该上游不支持 /v1/responses,系统已自动回退到 /v1/chat/completions` - `initial_probe_race_expected` - `账号创建后宿主异步探测尚未稳定,首次 /test 已按 advisory 处理` - `gateway_warmup_retry_succeeded` - `初次调度出现 no available accounts,短暂重试后已恢复` - `provision_reused` - `已检测到同 URL + 同模型家族 + 健康账号,系统直接复用已有 provider` - `patch_only_new_aliases` - `模型属于已覆盖家族,仅补充别名映射与定价,不重复创建资源` - `duplicate_active_account` - `该账号已存在且处于启用状态,本次未重复创建,直接复用` - `deprecated_account_reactivated` - `该账号此前处于弃用/停用状态,本次已快速启用并重新确认` ## 12. Result pages ### 12.1 页面列表 - `/batch-import/runs` - `/batch-import/runs/{run_id}` ### 12.2 列表页字段 | 列 | 数据源 | |---|---| | `Run ID` | `import_runs.run_id` | | `State` | projection | | `Mode` | `import_runs.mode` | | `Access Mode` | `import_runs.access_mode` | | `Total/Active/Degraded/Broken/Warning` | `import_runs.*` | | `Started/Finished` | `import_runs.*` | 筛选值使用 canonical 枚举: - `running` - `completed` - `completed_with_warnings` - `failed` - `cancelled` 页面可把 `completed_with_warnings` 渲染成 badge 文案 `warning`。 ### 12.3 详情页字段 Item 行至少包含: - `item_id` - `base_url` - `provider_id` - `api_key_fingerprint` - `requested_models` - `canonical_model_families` - `resolved_smoke_model` - `current_stage` - `confirmation_status` - `access_status` - `matched_account_state` - `account_resolution` - `provision_reused` - `retry_count` - `last_error_stage` - `last_error` Item 详情至少包含: - `raw_models` - `normalized_models` - `canonical_model_families` - `recommended_models` - `transport_profile` - `model_profiles` - `matched_account_state` - `account_resolution` - `reused_from_provider_id` - `reused_from_account_id` - `channel_id` - `account_id` - `advisory_messages` - `event trail` ## 13. HTTP API ### 13.1 V2 canonical endpoints ```text POST /api/batch-import/runs GET /api/batch-import/runs GET /api/batch-import/runs/{run_id} GET /api/batch-import/runs/{run_id}/items GET /api/batch-import/runs/{run_id}/items/{item_id} ``` ### 13.2 Legacy endpoints 以下继续保留,但在 OpenAPI 中标为 `deprecated` 或 `legacy`: - `/api/import-batches/{batchID}` - `/api/import-batches/{batchID}/rollback` ## 14. Backend module mapping 建议模块边界如下: ### `internal/batch/run_state.go` - run/item/event 仓储 - lease 管理 - run 聚合基础能力 ### `internal/batch/status_projection.go` - 页面/API 视图 projection - state/badge/warning 文案映射 ### `internal/batch/service.go` - run 创建 - Stage 1 + Stage 2 - item 入队 confirm ### `internal/batch/confirmation.go` - worker polling - confirm logic - retry scheduling ### `internal/batch/validation.go` - final gateway validation - final access_status write ### `internal/app/http_batch_import.go` - create/list/detail APIs ### `internal/app/http_batch_runs.go` - 结果页渲染 ## 15. Implementation boundary 第一阶段实现可以暂不做: - WebSocket 实时刷新 - 多 worker 分布式协调 - 跨 host 汇总看板 但以下不能再降级为“后续再说”: - canonical runtime tables - confirmation worker - lease + retry scheduling - event trail - 结果 API ## 16. Architecture acceptance 该架构只有满足以下条件,才算真正达到 V2 目标: 1. 输入契约、状态枚举、API、页面字段完全一致 2. run/item/event 可持久化并支持重启恢复 3. 结果页只读 canonical state store 4. transport capability 与 model capability 都能表达 5. `confirmation_status` 与 `access_status` 责任边界清楚 6. 第三方兼容 upstream 的异步窗口不会直接把可用链路打成最终失败