Files
sub2api-cn-relay-manager/docs/2026-05-21-BATCH_AUTO_IMPORT_SPEC.md
2026-05-22 13:38:56 +08:00

466 lines
12 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.
# SPEC: Batch Auto-Import by URL + Key (v2)
日期2026-05-21
技术架构:`docs/2026-05-22-BATCH_AUTO_IMPORT_V2_ARCHITECTURE.md`
## 1. Objective
V2 的目标不是“又一条导入命令”,而是把这件事做成**稳定、可恢复、可追踪**的控制面能力:
1. **上游发现**:基于 `(base_url, api_key)` 自动发现模型,而不是默认信任人工输入
2. **模型纠错**:自动归一化、别名匹配、推荐正确模型名
3. **兼容画像**:记录每个上游和每个模型的兼容能力,避免重复踩坑
4. **宿主演化**:自动创建/更新 channel、account、provider binding
5. **异步确认**:吸收宿主异步 probe、首次 `403/503` 的预热窗口
6. **闭环验证**:以宿主网关真实 `/v1/chat/completions` 结果作为最终可用性判断
7. **结果可视**:提供 run 列表、run 详情、item 详情,而不是只靠日志和 artifact
## 2. Scope
V2 在现有 v1 pack-based 路径旁边新增一条**URL + key auto-import** 路径。
### In Scope
- 批量输入 `(base_url, api_key, requested_models?)`
- 兼容 `subscription``self_service`
- 运行态状态持久化
- 后台异步确认与有限重试
- 最小结果 API 与结果页
- 模型纠错与 capability profile 持久化
### Out of Scope
- 多 key 自动负载均衡
- 宿主数据库直连
- 自动价格发现/自动调价
- 实时 WebSocket 推送
- 复杂工作台前端
## 3. Canonical Contract
V2 从这里开始只认一套 canonical contract后续所有文档、API、页面、状态库都必须遵循这套命名。
### 3.1 ID 规则
- `run_id`:一次批量导入任务 ID字符串
- `item_id`run 内单条导入记录 ID字符串
- `provider_id``{normalized_host}-{url_hash_last8}`
`provider_id` 生成规则:
1. 取完整 `base_url` 规范化后参与计算,不能只取 host
2. `normalized_host` 用于可读性
3. `url_hash_last8` 用于区分同 host 不同 path
示例:
- `https://api.deepseek.com/v1``api-deepseek-9f31c2ab`
- `https://api.deepseek.com/proxy/v1``api-deepseek-4a2d88f1`
### 3.2 Run 级状态
`run.state` 固定为:
- `running`
- `completed`
- `completed_with_warnings`
- `failed`
- `cancelled`
说明:
- `completed_with_warnings` 是 run 级总状态
- 页面可以显示成黄色 badge `warning`
- 但 API/状态库里一律写全量枚举值 `completed_with_warnings`
### 3.3 Item 级状态
`item.current_stage` 固定为:
- `probe`
- `provision`
- `confirm`
- `validate`
- `done`
`item.confirmation_status` 固定为:
- `pending`
- `confirmed`
- `advisory`
- `failed`
`item.access_status` 固定为:
- `unknown`
- `active`
- `degraded`
- `broken`
约束:
- `confirmation_status` 只描述“宿主异步窗口是否已确认稳定”
- `access_status` 只描述“最终网关真实可用性”
- `Validation Engine``access_status` 的唯一写入方
### 3.4 Legacy 兼容规则
V1 的 `import_batches` / `import_batch_items` / `managed_resources` 继续保留,但在 V2 中:
- 仅作为 legacy execution evidence 或资源关联来源
- 不再作为结果页主数据源
- V2 结果页/API 只读 `import_runs` / `import_run_items` / `import_run_item_events`
## 4. Request / Result Contract
### 4.1 Batch Import Request
```text
BatchImportRunRequest
- host_id: string
- mode: "strict" | "partial"
- access_mode: "subscription" | "self_service"
- confirm_wait_timeout_sec: int # CLI/HTTP 可选等待时间
- entries: []BatchImportEntry
- subscription_users: []string # access_mode=subscription 必填
- subscription_days: int # access_mode=subscription 必填
- probe_api_key: string # access_mode=self_service 必填
BatchImportEntry
- base_url: string
- api_key: string
- requested_models: []string # 可选,仅作为提示
```
### 4.2 Access Mode 必填规则
`subscription`
- 必填:`subscription_users`
- 必填:`subscription_days`
- 不接受只写 `access_mode=subscription` 但不带订阅目标
`self_service`
- 必填:`probe_api_key`
- `probe_api_key` 用于最终 gateway access validation
### 4.3 Batch Import Result
```text
BatchImportRunResult
- run_id: string
- state: string
- total_items: int
- active_items: int
- degraded_items: int
- broken_items: int
- warning_items: int
- result_page: string
```
### 4.4 Item Projection
```text
BatchImportRunItemView
- item_id: string
- base_url: string
- provider_id: string
- requested_models: []string
- raw_models: []string
- normalized_models: []string
- resolved_smoke_model: string | null
- recommended_models: []string
- current_stage: string
- confirmation_status: string
- access_status: string
- retry_count: int
- last_retry_at: string | null
- advisory_messages: []string
- last_error_stage: string | null
- last_error: string | null
- channel_id: int64 | null
- account_id: int64 | null
- capability_profile: object
```
## 5. Core Pipeline
### 5.1 Five-stage pipeline
```text
Stage 0: Run Setup
create import_run + import_run_items
persist operator input
Stage 1: Probe
/v1/models
capability probe
completion smoke
normalize aliases
Stage 2: Provision
find/create channel
patch model_mapping + model_pricing + restrict_models + billing_model_source
create/update account
persist managed resource link
Stage 3: Confirm
background confirmer absorbs async probe race / warmup window
writes confirmation_status
Stage 4: Validate
host gateway real /v1/chat/completions
writes final access_status
Stage 5: Project
update run summary
serve result API / pages
```
### 5.2 Ownership boundaries
- `Probe Layer` 负责发现和分类,不决定最终 `access_status`
- `Provision Adapter` 负责创建/更新宿主资源
- `Confirmation Engine` 负责把瞬时 `403/503` 吸收到 `pending/advisory/failed`
- `Validation Engine` 负责最终 `access_status`
- `Result Projection` 负责把状态库转换成页面/API 视图
## 6. Capability Profile
### 6.1 为什么要分两层
真实场景里兼容能力不是“一个 key 一个总画像”就能表达清楚的。必须拆成:
1. **transport profile**:这个 upstream 支不支持 `/models``/chat/completions``/responses``/messages`
2. **model profiles**:这个 upstream 下的具体模型,在 stream/tools/reasoning 字段上是否可用
### 6.2 Canonical schema
```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": "deepseek-ai/DeepSeek-V4-Pro",
"normalized_model_id": "deepseek-v4-pro",
"supports_stream": true,
"supports_tools": "unknown",
"supports_reasoning_fields": "unknown",
"smoke_chat_ok": true
}
]
}
```
### 6.3 用途
- 决定是否跳过 `/responses`
- 决定是否直接走 raw `/chat/completions`
- 决定 warning 文案
- 决定推荐 smoke model
- 决定后续快速匹配“哪个模型在哪种兼容层下靠谱”
## 7. Channel / Account Evolution Contract
V2 不再使用“薄 patch 接口”表达 channel 更新。宿主 patch 必须以完整 contract 表达:
```text
ChannelPatchContract
- model_mapping: map[string]string
- model_pricing: map[string]PriceSpec
- restrict_models: true
- billing_model_source: "channel_mapped"
```
约束:
- `model_mapping` 同时记录 raw → canonical
- `model_pricing` 默认可填零值,但字段必须完整存在
- patch 不得破坏旧模型
- `PatchChannel(addModels []string)` 这类接口不再作为 V2 canonical contract
## 8. Async Confirmation Mechanism
### 8.1 为什么 V2 必须有后台 confirmer
V2 的稳定性目标不能建立在“请求线程里顺序 sleep + retry”。必须有独立后台机制推进
- `confirming` item
- 因 probe race 暂时 advisory 的 item
-`503 no available accounts` 等待预热的 item
### 8.2 Canonical executor
V2 必须实现 `ConfirmationWorker`
```text
ConfirmationWorker
- poll import_run_items where current_stage='confirm'
- condition: next_retry_at <= now
- acquire lease
- run confirm logic
- update item state
- release lease
```
### 8.3 必需字段
`import_run_items` 至少要有:
- `confirmation_attempts`
- `retry_count`
- `last_retry_at`
- `next_retry_at`
- `lease_owner`
- `lease_until`
### 8.4 Restart safety
V2 第一版即要求:
- 进程重启后 unfinished confirm item 会被 worker 重新拾取
- 页面能看到 item 停在哪个阶段
- CLI `--confirm-wait-timeout` 只是“等待窗口”,不是确认机制本身
## 9. Single Source of Truth
### 9.1 Canonical runtime tables
V2 运行态只认三类表:
- `import_runs`
- `import_run_items`
- `import_run_item_events`
### 9.2 Legacy linkage
若某个 V2 item 调用了现有 v1 provision 流程,可在 item 上保留:
- `legacy_batch_id`
- `legacy_provider_id`
但这些字段仅作为追溯链接,不能替代 V2 状态源。
### 9.3 Result page data source
结果页/API 只读 V2 canonical tables不直接拼接
- `import_batches`
- `probe_results`
- `access_closure_records`
- 宿主数据库
## 10. Result API and Pages
### 10.1 API
V2 标准 API
```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}
```
Legacy API `/api/import-batches/*` 保留,但标为 v1/legacy。
### 10.2 Pages
```text
/batch-import/runs
/batch-import/runs/{run_id}
```
结果页必须能直接回答:
- 哪条 URL 导入成功
- 哪条卡在 `probe/provision/confirm/validate`
- 哪条发生模型纠错
- 哪条是 advisory 而不是 broken
- 重试过几次
- 当前 warning 的原因是什么
## 11. CLI Contract
```bash
go run ./cmd/cli batch-import \
--host-id "<host_id>" \
--entry "https://example.com/v1,sk-xxx" \
--batch-file "./keys.csv" \
--mode "strict|partial" \
--access-mode "subscription|self_service" \
--subscription-users "u1,u2" \
--subscription-days 30 \
--probe-api-key "<user_gateway_key>" \
--confirm-wait-timeout 15s
```
CLI 输出必须至少包含:
- `run_id`
- `result_page`
- 每个 entry 的 `resolved_smoke_model`
- capability 摘要
- `confirmation_status`
- `access_status`
- 推荐模型名(若发生纠错)
## 12. Error Policy
### Blocking
- `401/403 unauthorized` 且证据表明 key 无效
- `/v1/models` 完全不可用且无替代路径
- provision 明确失败
### Advisory
- 第三方 upstream `/responses=403``/chat/completions=200`
- 首次 `/accounts/:id/test=403`,但 probe race 已被识别
- 首次 `/v1/chat/completions=503 no available accounts`,且重试后恢复
- `429 rate_limit`
### Access status ownership
- `confirmation_status=advisory` 不自动等于 `access_status=degraded`
- 只有 Validation Engine 可以把 item 标成 `active/degraded/broken`
## 13. Success Criteria
1. `access_mode` 输入契约完整,`subscription` / `self_service` 都可单独落地
2. run / item 状态、重试、warning、错误阶段能持久化并在重启后恢复可见
3. 结果页和 API 只读 V2 canonical tables
4. 模型纠错结果、capability profile、推荐模型名可追溯
5. 第三方兼容 upstream 的 `/responses` 误判和宿主异步窗口不会把可用链路直接打成最终失败
6. 页面可以清楚地区分 `confirmed/advisory/failed``active/degraded/broken`
7. OpenAPI、SPEC、TDD、Architecture 对同一字段和同一状态枚举保持一致
## 14. Non-goals for first implementation
- 多 key 自动调度
- 实时推送
- 自动定价策略
- 自动负载均衡
## 15. Final decisions
1. `provider_id` 采用 `normalized_host + url_hash_last8`
2. `requested_models` 仅作提示,不作为事实源
3. `Validation Engine``access_status` 唯一写入方
4. V2 runtime canonical tables 为 `import_runs/import_run_items/import_run_item_events`
5. `ConfirmationWorker` 是 V2 必备组件,不是可选增强