chore: bootstrap repository

This commit is contained in:
phamnazage-jpg
2026-05-12 21:46:19 +08:00
commit 1c02fcdaa7
9 changed files with 1723 additions and 0 deletions

66
README.md Normal file
View File

@@ -0,0 +1,66 @@
# sub2api-cn-relay-manager
`sub2api-cn-relay-manager` 是一个独立于 `sub2api` 宿主仓库的外部伴生安装器 / 控制面项目。
目标不是修改 `sub2api`,也不是把国产模型能力硬塞进宿主源码,而是通过 `sub2api` 已有的管理 API把“国产模型 OpenAI 兼容中转能力”以独立交付物的形式安装到任意一台已部署的 `sub2api` 实例上。
## 项目定位
- 宿主:第三方开源系统 `sub2api`
- 约束:不修改宿主代码,不 fork 宿主,不要求宿主内置原生插件运行时
- 交付:一个独立控制面项目 + 一个或多个 `model_pack`
- 结果:管理员可一键导入多个国产模型 key普通用户继续只用 `sub2api` 标准 API
## 这个项目解决的问题
- 不同机器部署了 `sub2api` 后,如何用同一套交付物补齐国产模型中转能力
- 如何把多个国产模型 key 批量导入为可用的宿主资源
- 如何在不改宿主的前提下实现热生效、探测、对账和漂移发现
- 如何让普通用户完全无感,继续走 `sub2api` 标准接口
## 非目标
- 不做宿主原生插件系统
- 不做任意第三方 Go 代码动态加载
- 不要求宿主识别“插件”概念
- 不接管宿主网关流量
## 目录结构
```text
sub2api-cn-relay-manager/
cmd/ # 未来 CLI / server 入口
internal/ # 未来控制面后端实现
web/ # 未来管理端前端
docs/
2026-05-12-sub2api-cn-relay-manager-solution.md
packs/
openai-cn-pack/
README.md
pack.json.example
providers/
deepseek.json.example
```
## 交付模型
本项目最终会拆成两个可独立发布的产物:
1. `sub2api-cn-relay-manager`
- 外部控制面 / 安装器
- 连接已有 `sub2api`
- 安装模型包
- 调用宿主管理 API 创建资源
- 做 smoke test、对账和状态展示
2. `model_pack`
- 只包含 provider 定义、默认模板、导入规则和校验信息
- 不携带宿主执行代码
- 可以跨机器复用
## 当前文档
完整方案见:
- [docs/2026-05-12-sub2api-cn-relay-manager-solution.md](./docs/2026-05-12-sub2api-cn-relay-manager-solution.md)

0
cmd/.gitkeep Normal file
View File

View File

@@ -0,0 +1,619 @@
# sub2api-cn-relay-manager 方案文档
日期2026-05-12
## 1. 背景
宿主 `sub2api` 是一个第三方开源系统,用户较多,迭代很快,且每周都会发布新版本。
当前目标不是修改宿主源码,也不是等待宿主内置原生插件运行时,而是建立一个完全独立交付的外部伴生项目,让任意一台已部署的 `sub2api` 都能通过安装该项目和导入模型包,快速具备国产模型 OpenAI 兼容中转能力。
## 2. 目标
本方案必须满足以下业务目标:
1. 独立交付,不修改宿主 `sub2api` 源码
2. 可跨机器复用,在任意一台兼容版本的 `sub2api` 上重复安装
3. 可一键导入多个国产模型 key
4. 导入后让普通用户继续通过 `sub2api` 标准 API 使用国产模型
5. 尽量热生效,不依赖宿主重编译
6. 能主动发现“内容未真正生效”“生效后漂移”“部分账号失效”
## 3. 核心结论
在“不改宿主代码”的前提下,不能把该能力定义为“宿主原生插件”。
正确做法是:
- 把这套能力定义为一个独立的外部伴生控制面项目
- 把国产模型接入定义为可安装的 `model_pack`
- 由控制面调用宿主已有管理 API 自动创建 `group / channel / account / plan`
- 由控制面维护安装状态、导入批次、探测结果和对账结果
因此,业务上它表现得像“独立安装插件”,但技术上它是“外部伴生安装器 + 模型资源包”。
## 3.1 宿主零改动硬约束
为了确保方案始终满足“绝不改宿主代码”,补充以下硬约束:
1. 不修改宿主源码
2. 不 fork 宿主并运行自定义二进制
3. 不直接写宿主数据库
4. 不向宿主容器或宿主目录写入运行时代码或配置补丁
5. 不依赖宿主未公开的动态加载、初始化钩子或内部运行时对象
6. 只通过宿主现有 HTTP 管理 API 和宿主公开标准 API 工作
这意味着本方案的本质是:
- 技术上:宿主外部自动化编排
- 业务上:像独立插件一样交付和安装
## 3.2 审核后的最终结论
经过对宿主现有能力复核后,可以确认:
- 该方案可以在零宿主代码改动前提下完成国产模型中转能力增加
- 该方案不能伪装成宿主原生插件中心
- 该方案必须把“访问闭环”和“对账闭环”都纳入首版
## 4. 为什么这条路线成立
虽然宿主没有原生插件运行时,但它已有足够的管理 API
- 创建 group`POST /api/v1/admin/groups`
- 创建 account`POST /api/v1/admin/accounts`
- 批量创建 account`POST /api/v1/admin/accounts/batch`
- 测试 account`POST /api/v1/admin/accounts/:id/test`
- 查询 account 模型:`GET /api/v1/admin/accounts/:id/models`
- 创建 channel`POST /api/v1/admin/channels`
- 创建 plan`POST /api/v1/admin/plans`
这意味着控制面完全可以把 `sub2api` 当作一个可编排宿主,而不需要侵入其源码。
但这里有一个经过审核后必须写明的宿主约束:
- 宿主标准 API 网关要求请求最终落到“已分组 API key”或“有效 subscription”
- 仅仅创建 `group / channel / account / plan`,还不足以保证普通用户已经可以调用国产模型
因此,本方案不能只做资源创建,还必须补齐用户访问闭环。
## 5. 总体架构
整体拆成两个发布物:
### 5.1 控制面 / 安装器
项目名:
`sub2api-cn-relay-manager`
职责:
- 连接宿主 `sub2api`
- 安装 `model_pack`
- 预检宿主版本与 API 能力
- 一键导入多个 key
- 通过宿主管理 API 创建和维护资源
- 做 smoke test
- 做持续对账
- 对外展示“可用 / 降级 / 漂移 / 失败”状态
### 5.2 模型包
项目内置样例:
`packs/openai-cn-pack/`
职责:
- 提供 provider 定义
- 提供默认 group / channel / plan / account 模板
- 提供 model mapping 和 smoke test model
- 提供导入策略和校验约束
模型包不携带宿主执行逻辑。
## 6. 运行时组件
控制面内部建议拆成以下模块:
### 6.1 Host Adapter
宿主适配器,只负责把控制面操作翻译成 `sub2api` admin API 调用。
接口建议:
- `GetHostVersion()`
- `ProbeCapabilities()`
- `CreateGroup()`
- `CreateChannel()`
- `CreatePlan()`
- `CreateAccount()`
- `BatchCreateAccounts()`
- `TestAccount()`
- `GetAccountModels()`
- `AssignSubscription()`
- `CheckAccessPath()`
- `DeleteGroup()`
- `DeleteChannel()`
- `DeletePlan()`
- `DeleteAccount()`
- `ListManagedResources()`
### 6.2 Pack Runtime
负责读取和校验 `model_pack`
- `pack.json`
- `providers/*.json`
- `checksums.txt`
### 6.3 Provision Engine
负责把一个 provider + 一批 key 变成宿主真实资源:
- 创建 group
- 创建 channel
- 创建 plan
- 创建 account
- 绑定 group / model mapping / pricing
- 调用测试接口验证账号
### 6.4 Reconciler
负责持续对账,主动发现:
- 资源是否还存在
- key 是否仍可用
- 模型是否仍可列出
- 测试是否通过
- 宿主资源是否被人工改坏
### 6.5 State Store
负责维护控制面自己的状态,因为宿主不知道“插件”概念。
## 7. 模型包结构
建议结构:
```text
openai-cn-pack/
pack.json
providers/
deepseek.json
kimi.json
qwen.json
glm.json
minimax.json
checksums.txt
docs/
```
## 8. 模型包协议
### 8.1 pack.json
```json
{
"pack_id": "openai-cn-pack",
"version": "1.0.0",
"vendor": "YourTeam",
"target_host": "sub2api",
"min_host_version": "0.1.126",
"max_host_version": "0.2.x",
"providers_dir": "providers",
"checksum_file": "checksums.txt"
}
```
### 8.2 provider 定义
每个 provider 文件至少包含:
- `provider_id`
- `display_name`
- `base_url`
- `platform`
- `account_type`
- `default_models`
- `smoke_test_model`
- `group_template`
- `channel_template`
- `plan_template`
- `import`
示例字段:
```json
{
"provider_id": "deepseek",
"display_name": "DeepSeek OpenAI Compatible",
"base_url": "https://api.deepseek.com",
"platform": "openai",
"account_type": "api",
"default_models": ["deepseek-chat", "deepseek-reasoner"],
"smoke_test_model": "deepseek-chat",
"group_template": {
"name": "DeepSeek 默认分组",
"subscription_type": "subscription",
"rate_multiplier": 1.0
},
"channel_template": {
"name": "DeepSeek 默认渠道",
"billing_model_source": "channel_mapped",
"restrict_models": true,
"model_mapping": {
"deepseek-chat": "deepseek-chat",
"deepseek-reasoner": "deepseek-reasoner"
}
},
"plan_template": {
"name": "DeepSeek 默认套餐",
"price": 19.9,
"validity_days": 30,
"for_sale": true
},
"import": {
"supports_multi_key": true,
"supports_strict": true,
"supports_partial": true
}
}
```
## 9. 一键导入流程
### 9.1 安装模型包
1. 上传 `openai-cn-pack.zip`
2. 控制面解压并校验
3. 读取 `pack.json`
4. 校验宿主版本兼容
5. 执行宿主 API 能力探测
6. 注册 provider 定义到控制面状态库
### 9.2 导入多个 key
1. 管理员选择 provider
2. 粘贴多个 key 或上传 `txt / csv`
3. 选择导入模式:
- `strict`
- `partial`
4. 运行预检
5. 创建或复用 group
6. 创建或复用 channel
7. 创建或复用 plan
8. 批量创建 accounts
9. 逐个运行账号测试
10. 汇总结果并写入批次记录
### 9.3 普通用户使用
导入完成后,普通用户继续使用宿主标准 API
- `/v1/chat/completions`
- `/v1/responses`
- 宿主已支持的其他 OpenAI 标准接口
控制面不接管用户流量,只负责把宿主配置成“可中转国产模型”。
### 9.4 用户访问闭环
这是方案审核后新增的强制章节。
宿主网关不是只要存在上游 account 就能调用。
实际还需要满足以下至少一条:
1. 用户持有的 API key 已绑定到目标 group
2. 用户在目标 group 上拥有有效 subscription
否则,普通用户即使拿到宿主标准 API 地址,也不能真正使用国产模型。
因此控制面必须明确支持两种访问模式:
#### 模式 ASubscription 模式
适用场景:
- SaaS 运营
- 后台给用户开通套餐
- 管理员按用户分配访问能力
控制面职责:
- 创建 subscription 类型 group
- 创建默认可售或可分配 plan
- 调用宿主订阅分配接口,把 group 分配给目标用户
#### 模式 BUser Self-Service API Key 模式
适用场景:
- 用户已经登录宿主后台
- 用户自己创建 API key
- 用户自己把 key 绑定到控制面准备好的 group
控制面职责:
- 负责准备好 group / channel / account / plan
- 确保目标 group 对用户可见、可分配、可购买或可授权
#### 首版限制
在零宿主代码改动前提下,控制面不能把“管理员代用户签发最终 API key”作为首版硬依赖。
因此首版应优先保证:
- subscription 模式闭环可用
- user self-service API key 模式可用
## 10. 导入模式
### 10.1 strict
- 任一 key 创建失败或测试失败
- 整个批次回滚
- 适合正式生产导入
### 10.2 partial
- 成功的保留
- 失败的单独记录
- 适合大批量导入
### 10.3 导入完成判定
经过审核,单纯“资源创建成功”不再视为导入完成。
导入完成至少需要满足:
1. 目标 group 存在
2. 目标 channel 存在并已绑定 group
3. 目标 plan 存在,若当前访问模式依赖 plan
4. 至少一个 account 创建成功
5. 至少一个 account smoke test 或模型探测通过
6. 至少一种用户访问模式已经被验证可用
不满足上述条件时,只能标记为:
- `degraded`
- `failed`
## 11. 数据模型
控制面至少需要以下表:
### 11.1 hosts
- 宿主实例信息
- base_url
- 宿主版本
- 最近能力探测结果
### 11.2 packs
- 模型包版本
- checksum
- 安装时间
### 11.3 providers
- provider 元信息
- pack 归属
### 11.4 provider_installs
- 某台宿主安装了哪些 provider
- 当前状态
### 11.5 import_batches
- 一次导入批次
- provider
- 模式
- 请求条数
- 成功条数
- 失败条数
### 11.6 managed_resources
- 宿主侧 group/channel/plan/account 映射
- 宿主资源 ID
- 控制面资源键
### 11.7 reconcile_runs
- 每次对账执行结果
### 11.8 probe_results
- 账号测试与模型探测结果
## 12. 状态机
### 12.1 provider 安装状态
- `discovered`
- `validated`
- `installed`
- `active`
- `degraded`
- `drifted`
- `failed`
- `disabled`
### 12.2 导入批次状态
- `pending`
- `running`
- `succeeded`
- `partially_succeeded`
- `rolled_back`
- `failed`
## 13. 热生效与重启建议
在本方案下,大多数操作应视为热生效:
- 创建 group
- 创建 channel
- 创建 plan
- 创建 account
- 更新 model mapping
- 导入多个 key
因为这些动作都是通过宿主已有 admin API 写入运行时资源。
因此默认策略是:
- 成功写入宿主并通过测试 = 热生效
- 如果出现宿主缓存、运行时异常、版本兼容问题,控制面只给出 `restart_recommended`
- 控制面可选支持宿主重启适配器,但不作为首版必需能力
审核补充:
- `restart_recommended` 只是运维建议,不应成为主成功路径
- 只要核心能力依赖“必须重启宿主后才能导入成功”,就视为不满足首版目标
- 因此首版所有核心链路必须默认按热生效设计
## 14. 主动发现未生效
控制面必须周期性对账,而不是只看“导入成功”。
检查项至少包括:
1. 记录中的 group 是否还存在
2. channel 是否还存在
3. plan 是否还存在
4. account 是否还存在
5. account 测试是否通过
6. account 模型列表是否仍可读取
7. provider 的关键模型是否能通过一次标准测试请求
8. 控制面记录与宿主实际资源是否一致
9. 至少一种用户访问模式仍然成立
对于第 9 条,控制面至少要检查一种访问路径:
- subscription 模式下:目标用户对该 group 是否仍有有效 subscription
- user self-service API key 模式下:是否存在已绑定目标 group 的有效用户 API key 样本
只要任一项不一致,就应标记:
- `degraded`
- `drifted`
- `failed`
## 15. 外部 API 设计
控制面对外至少提供:
### 15.1 宿主管理
- `POST /api/hosts`
- `GET /api/hosts`
- `POST /api/hosts/:id/probe`
### 15.2 模型包管理
- `POST /api/packs/install`
- `GET /api/packs`
- `GET /api/packs/:id/providers`
### 15.3 provider 导入
- `POST /api/providers/:provider_id/preview-import`
- `POST /api/providers/:provider_id/import`
- `GET /api/providers/:provider_id/import-batches`
- `POST /api/import-batches/:id/rollback`
### 15.4 访问闭环管理
- `POST /api/providers/:provider_id/access/preview`
- `POST /api/providers/:provider_id/access/assign-subscriptions`
- `GET /api/providers/:provider_id/access/status`
说明:
- 首版优先支持 subscription 访问闭环
- 不把“管理员代用户创建最终 API key”作为首版必需能力
### 15.5 对账与探测
- `POST /api/providers/:provider_id/reconcile`
- `GET /api/providers/:provider_id/status`
- `GET /api/providers/:provider_id/resources`
## 16. 首版 CLI
建议首版同时提供 CLI降低自动化接入门槛
```bash
cnrelay host add --base-url https://your-sub2api --admin-token xxx
cnrelay pack install ./openai-cn-pack.zip
cnrelay provider import deepseek --keys-file deepseek.txt --mode partial --smoke-test
cnrelay provider status deepseek
cnrelay reconcile run
```
## 17. 与宿主升级的兼容策略
由于宿主 `sub2api` 每周发布新版本,控制面必须内置兼容矩阵:
1. 安装前先探测宿主版本
2. 再跑一组能力探针:
- groups create/list
- accounts create/test/models
- channels create/list
- plans create/list
- subscriptions assign/list
- 至少一种用户访问闭环探测
3. 任何一项不满足,即阻断安装或阻断导入
4. 对宿主版本维护 `supported / warning / unsupported`
这样可以避免宿主快速迭代导致导入流程静默失效。
## 18. MVP 范围
首版只支持:
- 一个宿主适配器:`sub2api`
- 一种包类型:`openai-cn-pack`
- 5 个 provider 模板以内
- 多 key 导入
- strict / partial 两种模式
- account 测试与模型探测
- subscription 访问闭环
- 对账与漂移发现
首版不做:
- 多宿主类型
- 在线插件市场
- 远程代码执行
- 宿主内嵌 UI
- 宿主数据库直写
- 管理员代用户签发最终 API key
## 19. 验收标准
满足以下标准即视为方案落地成功:
1. 任意一台兼容版本的 `sub2api`,只通过控制面和模型包即可接入至少一个国产模型 provider
2. 支持一次导入多个 key并得到逐 key 成功/失败结果
3. 导入成功后,至少一种用户访问模式已经被控制面验证可用
4. 普通用户继续通过 `sub2api` 标准 API 成功调用目标模型
5. 控制面可检测删除、失效、模型缺失、测试失败、访问闭环断裂等漂移
6. 宿主升级后,控制面能在导入前给出兼容性结论,而不是静默失败
## 20. 下一步
建议下一步直接进入实现计划拆解,顺序如下:
1. 定义控制面状态库 schema
2. 实现 `sub2api` 宿主适配器
3. 实现 `model_pack` schema 校验器
4. 实现 provider 导入引擎
5. 实现对账器
6. 再补 CLI 和最小 Web UI

View File

@@ -0,0 +1,973 @@
# sub2api-cn-relay-manager Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 构建一个完全独立于宿主 `sub2api` 的外部伴生控制面,在不修改宿主代码的前提下,通过安装 `model_pack`、批量导入国产模型 key、自动创建宿主资源并完成用户访问闭环让普通用户直接通过 `sub2api` 标准 API 使用国产模型。
**Architecture:** 采用“外部控制面 + `model_pack` + 宿主 HTTP API 适配器”架构。控制面只通过 `sub2api` 公开管理 API 和标准 API 工作,负责模型包安装、宿主能力探测、资源编排、导入回滚、访问闭环验证和持续对账;宿主继续承担标准 API 网关职责,不做任何源码、数据库或运行时注入修改。
**Tech Stack:** Go 1.24、Chi、`database/sql` + SQLite 驱动、PostgreSQL 兼容预留、OpenAPI 3.1、Go `testing` + `httptest`、前端首版后置MVP 先提供 HTTP API、CLI 和 Docker 交付物。
---
## 1. 实施边界
### 1.1 必须满足
- 不修改宿主 `sub2api` 源码
- 不 fork 宿主并运行自定义二进制
- 不直接写宿主数据库
- 不向宿主目录写入运行时代码、插件文件或配置补丁
- 只通过宿主现有管理 API 和标准 API 工作
- 首版必须完成从“导入 key”到“普通用户可实际调用”的闭环
### 1.2 首版不做
- 宿主原生插件中心
- 任意后端代码插件加载
- 代替用户签发宿主最终 API key
- 多宿主编排联邦
- 自定义计费、结算或审计系统
## 2. 目标目录结构
```text
sub2api-cn-relay-manager/
cmd/
server/main.go
cli/main.go
internal/
app/
app.go
bootstrap.go
config/
config.go
domain/
host.go
pack.go
provider.go
resource.go
import_batch.go
reconcile.go
access_closure.go
host/
sub2api/
client.go
capability_probe.go
groups.go
channels.go
plans.go
accounts.go
subscriptions.go
gateway_probe.go
pack/
manifest.go
provider_manifest.go
checksum.go
validator.go
loader.go
provision/
preview_service.go
import_service.go
rollback_service.go
naming.go
access/
planner.go
subscription_service.go
self_service_checker.go
reconcile/
runner.go
drift_checker.go
probe_runner.go
store/
migrations/
sqlite/
db.go
hosts_repo.go
host_capability_snapshots_repo.go
packs_repo.go
providers_repo.go
installs_repo.go
import_batches_repo.go
resources_repo.go
reconcile_runs_repo.go
probe_results_repo.go
access_closure_records_repo.go
api/
http/
router.go
middleware.go
hosts_handler.go
packs_handler.go
providers_handler.go
imports_handler.go
access_handler.go
reconcile_handler.go
dto/
hosts.go
packs.go
providers.go
imports.go
access.go
reconcile.go
worker/
scheduler.go
jobs.go
docs/
api/openapi.yaml
plans/
2026-05-12-sub2api-cn-relay-manager-implementation-plan.md
packs/
openai-cn-pack/
pack.json.example
providers/
deepseek.json.example
tests/
integration/
host_stub_test.go
install_pack_test.go
import_keys_test.go
access_closure_test.go
reconcile_test.go
```
## 3. 核心接口与契约
### 3.1 控制面对外 API
#### 宿主管理
- `POST /api/hosts`
- `GET /api/hosts`
- `GET /api/hosts/{host_id}`
- `POST /api/hosts/{host_id}/probe`
#### 模型包管理
- `POST /api/packs/install`
- `GET /api/packs`
- `GET /api/packs/{pack_id}`
- `GET /api/packs/{pack_id}/providers`
#### Provider 导入
- `POST /api/providers/{provider_id}/preview-import`
- `POST /api/providers/{provider_id}/import`
- `GET /api/providers/{provider_id}/import-batches`
- `GET /api/import-batches/{batch_id}`
- `POST /api/import-batches/{batch_id}/rollback`
#### 访问闭环
- `POST /api/providers/{provider_id}/access/preview`
- `POST /api/providers/{provider_id}/access/assign-subscriptions`
- `GET /api/providers/{provider_id}/access/status`
#### 对账与状态
- `POST /api/providers/{provider_id}/reconcile`
- `GET /api/providers/{provider_id}/status`
- `GET /api/providers/{provider_id}/resources`
### 3.2 宿主适配器接口
`internal/host/sub2api/client.go` 中定义单一适配接口:
```go
type HostAdapter interface {
GetHostVersion(ctx context.Context) (string, error)
ProbeCapabilities(ctx context.Context) (HostCapabilities, error)
CreateGroup(ctx context.Context, req CreateGroupRequest) (GroupRef, error)
CreateChannel(ctx context.Context, req CreateChannelRequest) (ChannelRef, error)
CreatePlan(ctx context.Context, req CreatePlanRequest) (PlanRef, error)
CreateAccount(ctx context.Context, req CreateAccountRequest) (AccountRef, error)
BatchCreateAccounts(ctx context.Context, req BatchCreateAccountsRequest) ([]AccountRef, error)
TestAccount(ctx context.Context, accountID string) (ProbeResult, error)
GetAccountModels(ctx context.Context, accountID string) ([]string, error)
AssignSubscription(ctx context.Context, req AssignSubscriptionRequest) (SubscriptionRef, error)
CheckGatewayAccess(ctx context.Context, req GatewayAccessCheckRequest) (GatewayAccessResult, error)
DeleteGroup(ctx context.Context, groupID string) error
DeleteChannel(ctx context.Context, channelID string) error
DeletePlan(ctx context.Context, planID string) error
DeleteAccount(ctx context.Context, accountID string) error
ListManagedResources(ctx context.Context, req ListManagedResourcesRequest) (ManagedResourceSnapshot, error)
}
```
约束:
- 所有宿主调用都必须通过该接口进入
- 任何绕过该接口的 HTTP 调用都视为实现缺陷
- 所有宿主 API 路径、请求体和响应体在 `internal/host/sub2api/` 内部封装,不向业务层泄漏
## 4. 状态机
### 4.1 Provider 安装状态
```text
discovered -> validated -> installed -> active
| |
v v
failed <-> degraded <-> drifted
|
v
disabled
```
状态定义:
- `discovered`:模型包已上传,尚未完成结构校验
- `validated`:包结构、校验和、版本兼容、宿主能力探测通过
- `installed`provider 元数据和模板已写入控制面状态库
- `active`:宿主资源存在,账号探测成功,至少一种用户访问模式已验证可用
- `degraded`:核心资源存在但 smoke test、模型探测或访问闭环部分失败
- `drifted`:宿主资源与控制面记录不一致,例如资源被人工改动或删除
- `failed`:安装、导入、回滚、对账任一关键步骤失败
- `disabled`provider 被显式停用,不再调度对账任务
### 4.2 导入批次状态
```text
pending -> previewed -> running -> succeeded
| | |
| | v
| -> partially_succeeded
v
failed -> rollback_running -> rolled_back
```
### 4.3 访问闭环状态
- `not_configured`
- `subscription_ready`
- `self_service_ready`
- `fully_ready`
- `broken`
判定规则:
-`subscription` 模式可用:`subscription_ready`
-`self-service` 模式可用:`self_service_ready`
- 两者都可用:`fully_ready`
- 两者都不可用:`broken`
## 5. 校验流程
### 5.1 宿主接入校验
请求:`POST /api/hosts`
流程:
1. 校验 `base_url`、认证信息格式
2. 拉取宿主版本
3. 探测宿主管理 API 能力:
- group 创建
- channel 创建
- plan 创建
- account 创建 / 测试 / 模型列举
- subscription 分配
4. 生成能力矩阵并落库
5. 若任一关键能力缺失,宿主状态标记为 `unsupported`
通过条件:
- 宿主版本在 `pack.json` 兼容区间内
- 所有首版硬依赖 API 均可调用
### 5.2 模型包装载校验
请求:`POST /api/packs/install`
流程:
1. 解压上传包到临时目录
2. 校验 `pack.json` 存在且字段完整
3. 校验 `providers/*.json` 至少存在一个 provider
4. 校验 `checksums.txt`
5. 校验 provider schema
6. 校验模型包声明的宿主版本兼容性
7. 将包元数据、provider 元数据写入状态库
拒绝条件:
- 缺少 `pack.json`
- provider 文件重复或 `provider_id` 冲突
- `base_url` 非 HTTPS
- `default_models` 为空
- `smoke_test_model` 不在 `default_models`
### 5.3 导入预检
请求:`POST /api/providers/{provider_id}/preview-import`
流程:
1. 规范化 key 列表:去空白、去重、去 BOM、过滤空行
2. 校验导入模式是否允许:`strict``partial`
3. 生成建议资源名
4. 检查宿主是否已有同名 group / channel / plan
5. 检查当前 provider 是否已有历史导入资源
6. 计算执行计划:
- create
- reuse
- conflict
7. 输出预检报告,不落宿主资源
### 5.4 导入执行
请求:`POST /api/providers/{provider_id}/import`
流程:
1. 创建批次记录,状态 `pending`
2. 执行预检,成功后切换为 `previewed`
3. 创建或复用 group
4. 创建或复用 channel
5. 按访问模式决定是否创建或复用 plan
6. 批量创建 accounts
7. 对每个 account 执行:
- 宿主 `/test`
- 宿主 `/models`
- 关键模型 smoke probe
8. 若访问模式为 `subscription`
- 预览待分配用户列表
- 调用宿主分配接口
9. 执行网关访问探测
10. 写入资源映射、探测结果、访问闭环状态
11. 根据模式决定成功、部分成功或回滚
### 5.5 回滚校验
触发条件:
- `strict` 模式下任一关键步骤失败
- 管理员主动回滚批次
流程:
1. 读取批次关联资源
2. 按依赖逆序删除:
- subscriptions
- accounts
- channel
- plan
- group
3. 删除成功后标记 `rolled_back`
4. 若部分删除失败,批次状态标记 `failed`provider 状态标记 `drifted`
### 5.6 对账校验
请求:`POST /api/providers/{provider_id}/reconcile`
流程:
1. 读取控制面记录的宿主资源快照
2. 拉取宿主实际资源
3. 核对 group / channel / plan / account 是否仍存在
4. 对 active account 重新执行 `/test``/models`
5. 重新执行至少一种用户访问模式探测
6. 汇总为:
- `active`
- `degraded`
- `drifted`
- `failed`
## 6. API 请求与响应最小契约
### 6.1 `POST /api/hosts`
请求体:
```json
{
"name": "prod-sub2api",
"base_url": "https://sub2api.example.com",
"auth": {
"type": "bearer",
"token": "xxxx"
}
}
```
成功响应:
```json
{
"host_id": "host_01",
"version": "0.1.126",
"status": "supported",
"capabilities": {
"groups": true,
"channels": true,
"plans": true,
"accounts": true,
"account_test": true,
"account_models": true,
"subscriptions": true
}
}
```
### 6.2 `POST /api/providers/{provider_id}/import`
请求体:
```json
{
"host_id": "host_01",
"mode": "partial",
"access_mode": "subscription",
"keys": [
"sk-1",
"sk-2"
],
"subscription_targets": [
{
"user_id": "user_01",
"duration_days": 30
}
]
}
```
成功响应:
```json
{
"batch_id": "batch_01",
"status": "succeeded",
"provider_status": "active",
"access_closure_status": "subscription_ready",
"created": {
"group_id": "g_01",
"channel_id": "c_01",
"plan_id": "p_01",
"account_ids": ["a_01", "a_02"]
}
}
```
## 7. 数据库与迁移计划
### 7.1 必建表
- `hosts`
- `host_capability_snapshots`
- `packs`
- `providers`
- `provider_installs`
- `import_batches`
- `import_batch_items`
- `managed_resources`
- `probe_results`
- `access_closure_records`
- `reconcile_runs`
### 7.2 约束
- `providers.provider_id` 在同一 `pack_id` 下唯一
- `managed_resources``host_id + provider_id + resource_type + logical_key` 唯一
- `import_batch_items.raw_key_hash` 唯一约束只用于同一批次内去重
- 密钥明文不落库,只存掩码、哈希和宿主侧 account ID
## 8. 测试策略
### 8.1 单元测试
- `internal/pack/validator.go`
- `internal/provision/naming.go`
- `internal/access/planner.go`
- `internal/reconcile/drift_checker.go`
### 8.2 宿主适配器契约测试
- 使用 `httptest.Server` 模拟 `sub2api` 管理 API
- 覆盖成功、鉴权失败、字段缺失、版本不兼容、接口漂移
### 8.3 集成测试
- 安装模型包
- 多 key 导入
- `strict` 失败回滚
- `partial` 部分成功
- `subscription` 访问闭环
- 对账发现漂移
### 8.4 验收测试
- 真实连接一套测试版 `sub2api`
- 导入 `deepseek` 两个 key
- 为测试用户分配 subscription
- 使用该用户实际标准 API key 发起一次 `/v1/chat/completions`
- 验证请求成功进入目标 provider
## 9. 分阶段里程碑
### Milestone 1项目骨架与状态库
验收标准:
- 控制面可启动
- SQLite 可自动迁移
- `POST /api/hosts``GET /api/hosts` 可用
- 宿主能力探测结果可持久化
### Milestone 2模型包安装与校验
验收标准:
- 可安装 `openai-cn-pack`
- 可列出 provider
- 版本兼容和 checksum 校验有效
- 非法 provider 包会被拒绝
### Milestone 3多 key 导入与资源编排
验收标准:
- 可对单个 provider 执行预检
- 可批量导入多个 key
- 可创建 group / channel / plan / account
- `strict` / `partial` 两种模式行为正确
### Milestone 4访问闭环
验收标准:
- `subscription` 模式打通
- 至少一个测试用户可通过宿主标准 API 调用目标国产模型
- 控制面可返回 `subscription_ready``fully_ready`
### Milestone 5对账、漂移与回滚
验收标准:
- 定时对账可运行
- 宿主资源被人工删除时provider 状态变为 `drifted`
- 可对批次执行回滚
- 回滚后状态库与宿主资源一致
### Milestone 6独立交付与部署文档
验收标准:
- 可生成单二进制和 Docker 镜像
- 提供 `.env.example` 和最小部署说明
- 可通过 CLI 完成宿主接入、模型包装载和导入
- 文档明确宿主零改动边界和支持矩阵
## 10. 逐任务实施清单
### Task 1: 建立项目骨架与配置加载
**Files:**
- Create: `cmd/server/main.go`
- Create: `cmd/cli/main.go`
- Create: `internal/app/app.go`
- Create: `internal/app/bootstrap.go`
- Create: `internal/config/config.go`
**Step 1: 写配置加载单元测试**
- Test: `tests/integration/config_bootstrap_test.go`
- 覆盖环境变量、默认值、缺失必填项
**Step 2: 运行测试验证失败**
Run: `go test ./tests/integration -run TestConfigBootstrap -v`
Expected: FAIL提示配置加载尚未实现
**Step 3: 写最小实现**
- 完成配置结构、启动参数、默认 SQLite DSN
**Step 4: 运行测试验证通过**
Run: `go test ./tests/integration -run TestConfigBootstrap -v`
Expected: PASS
**Step 5: Commit**
```bash
git add cmd internal tests
git commit -m "feat: bootstrap control plane app skeleton"
```
### Task 2: 建立状态库与迁移
**Files:**
- Create: `internal/store/migrations/0001_init.sql`
- Create: `internal/store/sqlite/db.go`
- Create: `internal/store/sqlite/hosts_repo.go`
- Create: `internal/store/sqlite/packs_repo.go`
- Create: `internal/store/sqlite/providers_repo.go`
**Step 1: 写迁移与仓储测试**
- Test: `tests/integration/store_init_test.go`
- 覆盖建表、唯一约束、事务回滚
**Step 2: 跑失败测试**
Run: `go test ./tests/integration -run TestStoreInit -v`
Expected: FAIL表不存在
**Step 3: 实现最小迁移与仓储**
- 先实现 `hosts``packs``providers`
**Step 4: 跑测试**
Run: `go test ./tests/integration -run TestStoreInit -v`
Expected: PASS
**Step 5: Commit**
```bash
git add internal/store tests
git commit -m "feat: add state store migrations and repositories"
```
### Task 3: 实现宿主适配器与能力探测
**Files:**
- Create: `internal/host/sub2api/client.go`
- Create: `internal/host/sub2api/capability_probe.go`
- Create: `internal/host/sub2api/groups.go`
- Create: `internal/host/sub2api/channels.go`
- Create: `internal/host/sub2api/plans.go`
- Create: `internal/host/sub2api/accounts.go`
- Create: `internal/host/sub2api/subscriptions.go`
**Step 1: 写 `httptest` 宿主桩和适配器测试**
- Test: `tests/integration/host_stub_test.go`
- 覆盖版本获取、能力探测、错误映射
**Step 2: 先跑失败**
Run: `go test ./tests/integration -run TestSub2APIHostAdapter -v`
Expected: FAIL适配器未实现
**Step 3: 写最小适配器实现**
- 只支持首版必需 API
**Step 4: 跑测试**
Run: `go test ./tests/integration -run TestSub2APIHostAdapter -v`
Expected: PASS
**Step 5: Commit**
```bash
git add internal/host tests
git commit -m "feat: add sub2api host adapter and capability probe"
```
### Task 4: 实现模型包协议与安装校验
**Files:**
- Create: `internal/pack/manifest.go`
- Create: `internal/pack/provider_manifest.go`
- Create: `internal/pack/checksum.go`
- Create: `internal/pack/validator.go`
- Create: `internal/pack/loader.go`
**Step 1: 写模型包校验测试**
- Test: `tests/integration/install_pack_test.go`
- 覆盖合法包、缺失字段、坏 checksum、版本不兼容
**Step 2: 跑失败**
Run: `go test ./tests/integration -run TestInstallPack -v`
Expected: FAIL包解析和校验未实现
**Step 3: 实现最小装载器**
- 支持从 zip 和目录读取
**Step 4: 跑测试**
Run: `go test ./tests/integration -run TestInstallPack -v`
Expected: PASS
**Step 5: Commit**
```bash
git add internal/pack tests
git commit -m "feat: add model pack loader and validator"
```
### Task 5: 实现导入预检与命名策略
**Files:**
- Create: `internal/provision/preview_service.go`
- Create: `internal/provision/naming.go`
- Create: `internal/domain/import_batch.go`
**Step 1: 写预检测试**
- Test: `tests/integration/import_preview_test.go`
- 覆盖 key 规范化、冲突判定、create/reuse/conflict 输出
**Step 2: 跑失败**
Run: `go test ./tests/integration -run TestImportPreview -v`
Expected: FAIL
**Step 3: 实现预检逻辑**
- 只做只读分析,不写宿主
**Step 4: 跑测试**
Run: `go test ./tests/integration -run TestImportPreview -v`
Expected: PASS
**Step 5: Commit**
```bash
git add internal/provision internal/domain tests
git commit -m "feat: add provider import preview flow"
```
### Task 6: 实现多 key 导入、探测与回滚
**Files:**
- Create: `internal/provision/import_service.go`
- Create: `internal/provision/rollback_service.go`
- Create: `internal/store/sqlite/import_batches_repo.go`
- Create: `internal/store/sqlite/resources_repo.go`
- Create: `internal/store/sqlite/probe_results_repo.go`
**Step 1: 写导入测试**
- Test: `tests/integration/import_keys_test.go`
- 覆盖 `strict` 成功、`strict` 回滚、`partial` 部分成功
**Step 2: 跑失败**
Run: `go test ./tests/integration -run TestImportKeys -v`
Expected: FAIL
**Step 3: 实现导入与回滚服务**
- 创建宿主资源
- 记录资源映射
- 运行账号测试和模型探测
**Step 4: 跑测试**
Run: `go test ./tests/integration -run TestImportKeys -v`
Expected: PASS
**Step 5: Commit**
```bash
git add internal/provision internal/store tests
git commit -m "feat: add multi-key import and rollback flow"
```
### Task 7: 实现访问闭环
**Files:**
- Create: `internal/access/planner.go`
- Create: `internal/access/subscription_service.go`
- Create: `internal/access/self_service_checker.go`
- Create: `internal/domain/access_closure.go`
**Step 1: 写访问闭环测试**
- Test: `tests/integration/access_closure_test.go`
- 覆盖 `subscription_ready``self_service_ready``broken`
**Step 2: 跑失败**
Run: `go test ./tests/integration -run TestAccessClosure -v`
Expected: FAIL
**Step 3: 实现最小闭环服务**
- 首版先保证 `subscription` 模式打通
- `self-service` 只做绑定状态检查,不代用户签发 key
**Step 4: 跑测试**
Run: `go test ./tests/integration -run TestAccessClosure -v`
Expected: PASS
**Step 5: Commit**
```bash
git add internal/access internal/domain tests
git commit -m "feat: add access closure services"
```
### Task 8: 实现对账与漂移检测
**Files:**
- Create: `internal/reconcile/runner.go`
- Create: `internal/reconcile/drift_checker.go`
- Create: `internal/reconcile/probe_runner.go`
- Create: `internal/store/sqlite/reconcile_runs_repo.go`
**Step 1: 写对账测试**
- Test: `tests/integration/reconcile_test.go`
- 覆盖正常、资源丢失、账号失效、访问闭环破坏
**Step 2: 跑失败**
Run: `go test ./tests/integration -run TestReconcile -v`
Expected: FAIL
**Step 3: 实现对账器**
- 拉取宿主资源快照
- 比对状态库
- 重新执行探测
**Step 4: 跑测试**
Run: `go test ./tests/integration -run TestReconcile -v`
Expected: PASS
**Step 5: Commit**
```bash
git add internal/reconcile internal/store tests
git commit -m "feat: add provider reconcile and drift detection"
```
### Task 9: 暴露 HTTP API 与 OpenAPI 文档
**Files:**
- Create: `internal/api/http/router.go`
- Create: `internal/api/http/middleware.go`
- Create: `internal/api/http/hosts_handler.go`
- Create: `internal/api/http/packs_handler.go`
- Create: `internal/api/http/providers_handler.go`
- Create: `internal/api/http/imports_handler.go`
- Create: `internal/api/http/access_handler.go`
- Create: `internal/api/http/reconcile_handler.go`
- Create: `docs/api/openapi.yaml`
**Step 1: 写 handler 测试**
- Test: `tests/integration/http_api_test.go`
- 覆盖主要成功与失败路径
**Step 2: 跑失败**
Run: `go test ./tests/integration -run TestHTTPAPI -v`
Expected: FAIL
**Step 3: 实现路由和 DTO**
- 保持 handler 只做参数解析和错误映射
**Step 4: 跑测试**
Run: `go test ./tests/integration -run TestHTTPAPI -v`
Expected: PASS
**Step 5: Commit**
```bash
git add internal/api docs/api tests
git commit -m "feat: add control plane HTTP API"
```
### Task 10: 增加调度器、CLI 和端到端验收脚本
**Files:**
- Create: `internal/worker/scheduler.go`
- Create: `internal/worker/jobs.go`
- Modify: `cmd/cli/main.go`
- Create: `scripts/e2e/verify_with_host_stub.sh`
**Step 1: 写调度与 CLI 测试**
- Test: `tests/integration/cli_scheduler_test.go`
- 覆盖手动 reconcile 和定时调度
**Step 2: 跑失败**
Run: `go test ./tests/integration -run TestCLIScheduler -v`
Expected: FAIL
**Step 3: 实现最小调度与 CLI**
- 支持 `host add``pack install``provider import``reconcile run`
**Step 4: 跑全量测试**
Run: `go test ./...`
Expected: PASS
**Step 5: Commit**
```bash
git add cmd internal scripts tests
git commit -m "feat: add scheduler cli and e2e verification script"
```
### Task 11: 补齐独立交付物与部署文档
**Files:**
- Create: `Dockerfile`
- Create: `.env.example`
- Create: `deploy/docker-compose.yml`
- Create: `docs/deployment.md`
- Modify: `README.md`
**Step 1: 写交付物检查测试**
- Test: `tests/integration/distribution_smoke_test.go`
- 覆盖配置样例、镜像启动参数、CLI 帮助输出
**Step 2: 跑失败**
Run: `go test ./tests/integration -run TestDistributionSmoke -v`
Expected: FAIL
**Step 3: 实现最小交付物**
- 提供可运行镜像
- 提供本地 SQLite 默认部署
- 文档明确对接宿主所需环境变量
**Step 4: 跑测试**
Run: `go test ./tests/integration -run TestDistributionSmoke -v`
Expected: PASS
**Step 5: Commit**
```bash
git add Dockerfile .env.example deploy docs README.md tests
git commit -m "docs: add distribution artifacts and deployment guide"
```
## 11. 最终验收清单
- 从空状态启动控制面
- 连接一套兼容版本的 `sub2api`
- 安装 `openai-cn-pack`
- 导入 `deepseek` 两个 key
- 成功创建宿主资源并记录映射
- 至少一个账号通过测试与模型探测
- 至少一种用户访问模式验证成功
- 使用宿主标准 API 成功调用国产模型
- 人工删除一个宿主 account 后,对账将 provider 状态标记为 `drifted`
- 执行批次回滚后,宿主残留资源清理完成
## 12. 风险与收敛策略
- 宿主管理 API 漂移风险:所有 HTTP 路径和字段通过 `HostAdapter` 封装,并为版本差异保留适配层
- 宿主权限模型不稳定:在 `POST /api/hosts` 阶段强制能力探测,不满足即拒绝接入
- 不同 provider 规则差异:统一约束到 `providers/*.json`,首版只支持 OpenAI-compatible provider
- 访问闭环误判:首版必须落真实标准 API 探测,不能只以资源存在判定成功

0
internal/.gitkeep Normal file
View File

View File

@@ -0,0 +1,18 @@
# openai-cn-pack
这是 `sub2api-cn-relay-manager` 的最小模型包样例。
它不是宿主原生插件,而是一个可被控制面读取的 `model_pack`,用于描述国产模型 provider 的默认接入模板、默认模型映射、默认套餐和导入约束。
当前目录仅提供协议样例:
- `pack.json.example`
- `providers/deepseek.json.example`
后续真实交付时,可以扩展更多 provider
- `kimi.json`
- `qwen.json`
- `glm.json`
- `minimax.json`

View File

@@ -0,0 +1,10 @@
{
"pack_id": "openai-cn-pack",
"version": "1.0.0",
"vendor": "YourTeam",
"target_host": "sub2api",
"min_host_version": "0.1.126",
"max_host_version": "0.2.x",
"providers_dir": "providers",
"checksum_file": "checksums.txt"
}

View File

@@ -0,0 +1,37 @@
{
"provider_id": "deepseek",
"display_name": "DeepSeek OpenAI Compatible",
"base_url": "https://api.deepseek.com",
"platform": "openai",
"account_type": "api",
"default_models": [
"deepseek-chat",
"deepseek-reasoner"
],
"smoke_test_model": "deepseek-chat",
"group_template": {
"name": "DeepSeek 默认分组",
"subscription_type": "subscription",
"rate_multiplier": 1.0
},
"channel_template": {
"name": "DeepSeek 默认渠道",
"billing_model_source": "channel_mapped",
"restrict_models": true,
"model_mapping": {
"deepseek-chat": "deepseek-chat",
"deepseek-reasoner": "deepseek-reasoner"
}
},
"plan_template": {
"name": "DeepSeek 默认套餐",
"price": 19.9,
"validity_days": 30,
"for_sale": true
},
"import": {
"supports_multi_key": true,
"supports_strict": true,
"supports_partial": true
}
}

0
web/.gitkeep Normal file
View File