feat: merge opus comfyui and login updates

This commit is contained in:
boos
2026-05-21 16:32:04 +02:00
parent c16479da7c
commit 9c84759498
105 changed files with 20160 additions and 1034 deletions
@@ -0,0 +1,82 @@
## ADDED Requirements
### Requirement: 启动时确保内置 ComfyUI Agent key 存在
服务启动序列 SHALL 在数据库迁移完成之后、HTTP 路由对外提供之前,调用 `ensure_comfyui_agent_key`,确保 `downstream_api_keys` 表中存在唯一一行 `name='comfyui-agent' AND user_id IS NULL`,且其明文 key 同步保存在 `settings` 表的 `builtin_key.comfyui_agent.secret` 键。该流程 MUST 幂等。
#### Scenario: 全新数据库首次启动
- **WHEN** 启动时数据库无任何 `name='comfyui-agent'` 行,且 settings 表无 `builtin_key.comfyui_agent.secret`
- **THEN** 启动结束后两处都存在;密钥形如 `sk-comfyui-<32 位十六进制或等价 uuid>`;`enabled=1`
#### Scenario: 已有 settings 但缺少表记录(崩溃恢复)
- **WHEN** settings 表存在 `builtin_key.comfyui_agent.secret = "sk-comfyui-xxx"`,但 `downstream_api_keys` 中没有对应行
- **THEN** 引导逻辑用 settings 中的明文回填一行 `downstream_api_keys`,不重新生成密钥
#### Scenario: 已有表记录但缺少 settings
- **WHEN** `downstream_api_keys` 中存在 `name='comfyui-agent' AND user_id IS NULL` 行 key=`sk-comfyui-yyy`,但 settings 中无 `builtin_key.comfyui_agent.secret`
- **THEN** 引导逻辑把表中的明文写回 settings;密钥保持原值
#### Scenario: 重复启动不变更密钥
- **WHEN** 同一数据库连续启动两次
- **THEN** 第二次启动后 settings 与表中的 key 与第一次完全一致
#### Scenario: 引导期间发现 enabled=0 自动恢复
- **WHEN** 启动时发现已有行 `enabled=0`
- **THEN** 引导逻辑把该行 `enabled` 设为 1,并通过 `tracing::warn!` 输出一条提示
### Requirement: 系统级 key 在管理 API 中受保护
任何 admin 接口 SHALL 拒绝对 `name='comfyui-agent' AND user_id IS NULL` 的下游 key 执行删除(`DELETE`)或将 `enabled` 设为 false。list / get 类响应 MUST 把这一行标记为 `system: true`,并 mask 明文 key(仅暴露后 4 位)。
#### Scenario: DELETE 被拒
- **WHEN** admin 调用 `DELETE` 删除该 key 对应 id
- **THEN** 返回 403 / 409 类错误(取实现选择),message 含 `system key` 关键字;数据库中的行不变
#### Scenario: 禁用被拒
- **WHEN** admin PATCH/PUT 设 `enabled=false`
- **THEN** 返回拒绝错误,行 `enabled` 保持 1
#### Scenario: 列表响应打码
- **WHEN** admin GET 列出下游 key
- **THEN** 该行字段 `key` 形如 `••••••••<后4位>`,并附 `system: true`
#### Scenario: 普通 key 不受影响
- **WHEN** admin 操作其他普通 user_id 非空的 key
- **THEN** DELETE / PATCH / list 行为不受系统 key 保护逻辑影响
### Requirement: settings 中的内置密钥不暴露给 admin 列表
如果存在 admin 接口列出 `settings` 表内容,该接口 SHALL 过滤所有以 `builtin_key.` 为前缀的 key;按 key 单独读取时,对该前缀返回 404 或带占位符 `"••••"`,不返回实际明文。
#### Scenario: 列表过滤前缀
- **WHEN** admin GET `/api/admin/settings`(若存在)
- **THEN** 响应中不出现任何 key 以 `builtin_key.` 开头的条目
#### Scenario: 单读返回占位
- **WHEN** admin GET `/api/admin/settings/builtin_key.comfyui_agent.secret`(若存在)
- **THEN** 返回 404 或 `value: "••••"`,不返回真实明文
### Requirement: 内置 key 用 user_id IS NULL 与普通 key 区分
内置下游 key 在 `downstream_api_keys` 表中 SHALL 满足 `user_id IS NULL`;任何普通用户创建的 key MUST 写入非空 `user_id`,确保 `name='comfyui-agent' AND user_id IS NULL` 的查询能稳定匹配仅一行。
#### Scenario: 普通用户不能创建同名 key 占用系统标识
- **WHEN** 普通用户调用创建 key 接口,name 设为 `comfyui-agent`
- **THEN** 该 key 仍写入非空 user_id;系统级查询不会返回该行
#### Scenario: 引导查询仅匹配系统行
- **WHEN** 数据库中同时存在 user_id=5 且 name='comfyui-agent' 的普通 key 与 user_id IS NULL 的系统 key
- **THEN** `ensure_comfyui_agent_key` 仅作用于 user_id IS NULL 的那一行
@@ -0,0 +1,81 @@
## ADDED Requirements
### Requirement: ComfyUI Agent 默认调用本进程 /v1
ComfyUI Agent 的三类上游调用(`/v1/chat/completions`、`/v1/images/generations`、`/v1/audio/speech`)SHALL 默认指向本进程自身,base 由 `state.config.server.host` 与 `state.config.server.port` 拼出(host 为 `0.0.0.0` 或 `::` 时改写为 `127.0.0.1`)。Agent MUST NOT 在默认配置下访问任何外部主机。
#### Scenario: 默认 base 指向本机回环
- **WHEN** `BOOSAPI_UPSTREAM_BASE` 未设置且 `server.port = 4000`、`server.host = 0.0.0.0`
- **THEN** chat / images / audio 三类调用的 base 解析为 `http://127.0.0.1:4000/v1`
#### Scenario: 显式 host 被保留
- **WHEN** `server.host = 10.0.0.5`、`server.port = 4000` 且未设置 env
- **THEN** base 解析为 `http://10.0.0.5:4000/v1`,不被改写为回环
#### Scenario: 三类调用使用同一 base
- **WHEN** Agent 在同一次会话中触发 chat、images、audio 三类调用
- **THEN** 三次请求的 base 完全一致(来自同一 `upstream_config` 调用结果)
### Requirement: env 显式设置时回退远端
当且仅当 `BOOSAPI_UPSTREAM_BASE` 设置为非空字符串时,Agent SHALL 改用该 env 值作为 base,并使用 `BOOSAPI_UPSTREAM_KEY`(若设置且非空)或内置 key 的明文作为 Bearer。该分支以下统称 `fallback-remote` 模式。
#### Scenario: env 非空触发回退
- **WHEN** `BOOSAPI_UPSTREAM_BASE = "https://api.boos.lat/v1"`
- **THEN** 三类调用的 base 解析为 `https://api.boos.lat/v1`,且 `mode = "fallback-remote"`
#### Scenario: env 为空字符串视同未设置
- **WHEN** `BOOSAPI_UPSTREAM_BASE = ""` 或 `BOOSAPI_UPSTREAM_BASE = " "`
- **THEN** base 解析回本机回环,`mode = "local"`
#### Scenario: env key 缺省时复用内置 key
- **WHEN** 设置了 `BOOSAPI_UPSTREAM_BASE` 但未设置 `BOOSAPI_UPSTREAM_KEY`
- **THEN** Bearer 使用 `comfyui-agent` 内置 key 的明文,不退化为 `proxy_token`
### Requirement: 本地调用使用内置下游 key 计费
`mode = "local"` 时,Agent SHALL 在请求头 `Authorization` 中携带 `Bearer <comfyui-agent 内置 key 明文>`。该 key MUST 与本进程 `downstream_api_keys` 表中 `name='comfyui-agent' AND user_id IS NULL` 的行一致,使所有 Agent 流量在 `usage_logs` 中以该 key 归集。
#### Scenario: chat 请求携带内置 key
- **WHEN** Agent 在本地模式下发起 `/v1/chat/completions`
- **THEN** 请求头 `Authorization` 形如 `Bearer sk-comfyui-...`,且该值等于 settings 表中 `builtin_key.comfyui_agent.secret` 的值
#### Scenario: usage_logs 归集到内置 key
- **WHEN** 一次本地模式下的 chat 请求成功
- **THEN** `usage_logs` 中存在一条记录,`downstream_key_id` 等于内置 key 在 `downstream_api_keys` 中的 id
### Requirement: status 端点暴露生效模式
`GET /api/comfyui-agent/status` 响应 SHALL 在原有字段基础上增加 `mode: "local" | "fallback-remote"` 与 `keyName: "comfyui-agent"` 两个字段;`upstreamBase` 字段 MUST 反映当前实际生效的 base(已 trim 末尾斜杠)。
#### Scenario: 默认配置返回 local
- **WHEN** 客户端在未设置 env 的情况下 GET `/api/comfyui-agent/status`
- **THEN** 响应包含 `"mode": "local"`、`"keyName": "comfyui-agent"`、`upstreamBase` 形如 `http://127.0.0.1:<port>/v1`
#### Scenario: env 设置时返回 fallback-remote
- **WHEN** `BOOSAPI_UPSTREAM_BASE = "https://api.boos.lat/v1"` 时 GET 该端点
- **THEN** 响应 `"mode": "fallback-remote"`、`upstreamBase = "https://api.boos.lat/v1"`
#### Scenario: 鉴权失败仍不暴露密钥
- **WHEN** 客户端用错误的 admin token 请求
- **THEN** 返回 401,且响应体不包含 `keyName` / `upstreamBase` 任一字段
### Requirement: 本地路由失败必须如实返回
当本地 `/v1` 没有配置对应模型路由(chat 的 `agent_model`、images 的 `gpt-image-2`、audio 的 `tts-1`),Agent SHALL 把上游返回的错误状态码 / 文案如实通过 SSE `error` 事件转发给前端,MUST NOT 在收到 4xx 时自动切到 `fallback-remote`。
#### Scenario: 本地缺少 agent_model 路由
- **WHEN** 本地模式下发起 chat,token_router 返回 404
- **THEN** SSE 流中出现 `error` 事件,message 包含上游 404 内容;后续请求仍走本地,不自动切远端