feat: merge opus comfyui and login updates
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-05-19
|
||||
@@ -0,0 +1,143 @@
|
||||
## Context
|
||||
|
||||
`src/routes/comfyui_agent.rs` 第 79-91 行的 `upstream_config()` 用 `BOOSAPI_UPSTREAM_BASE` env(默认 `https://api.boos.lat/v1`)+ `BOOSAPI_UPSTREAM_KEY` env(默认 `state.config.auth.proxy_token`)拼出 base/key,三个 handler(chat 第 351 行、images 第 274 行、audio 第 311 行)都直接用 `reqwest::Client` post 到这个外部地址。这导致:
|
||||
|
||||
- ComfyUI Agent 流量绕过本进程已实现的 token_router / capability / proxy_logs / 计费。
|
||||
- 部署必须保证 `api.boos.lat` 可达,否则功能不可用。
|
||||
- 本服务自己就是 OpenAI 兼容网关,"BoosAPI 调远端 BoosAPI" 是反直觉的部署形态。
|
||||
|
||||
数据库现状(`migrations/20260516000001_initial.sql`):
|
||||
- `downstream_api_keys` 表已存在,`user_id INTEGER REFERENCES users(id)` 可空(无 NOT NULL 约束)。
|
||||
- `key` 列上有 `downstream_api_keys_key_unique` 唯一索引。
|
||||
- `settings` 表是 `(key TEXT PRIMARY KEY, value TEXT)`,可放任意键值。
|
||||
- `find_downstream_key_by_token` 已能按明文 key 查(`src/db/queries/mod.rs:31`)。
|
||||
|
||||
约束:
|
||||
- 保留现栈(axum + sqlx + reqwest),不引入新依赖。
|
||||
- 启动顺序在 `src/main.rs`:load config → `db::create_pool` → migrate → `AppState::new` → `state.comfyui.start()` → `build_router`。Builtin key 引导必须在 migrate 之后、router 之前。
|
||||
- 不破坏既有 `BOOSAPI_UPSTREAM_*` env 在调试场景下的语义。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- ComfyUI Agent 默认调用 `http://127.0.0.1:{server.port}/v1/{chat/completions, images/generations, audio/speech}`。
|
||||
- 引入名为 `comfyui-agent` 的内置下游 key,明文持久化到 `settings` 表,重启后保持稳定(不每次换 key)。
|
||||
- 三类调用(chat / images / audio)行为一致:同一 base + 同一 Bearer key + 同一回退策略。
|
||||
- `/api/comfyui-agent/status` 暴露 `mode` 与 `keyName`,供前端排障使用。
|
||||
- 启动时不依赖外部网络。
|
||||
|
||||
**Non-Goals:**
|
||||
- 不为内置 key 增加新的管理界面字段(沿用现有 Downstream Keys 页面,仅做"不可删除"标记)。
|
||||
- 不重写 token_router / capability,本次只是消费它们。
|
||||
- 不做请求级动态 key 切换(每次请求都用同一个 builtin key)。
|
||||
- 不改造 SYSTEM_PROMPT、CHARACTER/SCENE 解析、SSE 转发等业务逻辑。
|
||||
- 不动 `comfyui_process.rs`(嵌入式 ComfyUI 二进制管理)。
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. 内置 key 用现有 `downstream_api_keys` 表 + `user_id IS NULL` 表示系统级
|
||||
|
||||
**为什么不另建表**:
|
||||
- 想要让 builtin key 与普通用户 key 共享同一套计费、限额、proxy_logs 关联(`usage_logs.downstream_key_id REFERENCES downstream_api_keys(id)` 已存在)。
|
||||
- `user_id` 已经是可空 `INTEGER REFERENCES users(id)`,不需要 schema 改动。
|
||||
|
||||
**Alternative 考虑**:
|
||||
- 单独建一张 `system_api_keys` 表 → 拒绝,要在 `usage_logs` 里加 `system_key_id` 列、改 query,复杂度不成比例。
|
||||
- 复用 `config.auth.proxy_token` 当 Bearer → 拒绝,proxy_token 与 admin auth_token 是不同语义且不在 `downstream_api_keys` 中,无法计费。
|
||||
|
||||
### 2. 密钥明文写到 `settings` 表,key = `builtin_key.comfyui_agent.secret`
|
||||
|
||||
启动引导逻辑(`src/services/builtin_keys.rs::ensure_comfyui_agent_key`):
|
||||
|
||||
1. `SELECT value FROM settings WHERE key = 'builtin_key.comfyui_agent.secret'`。
|
||||
2. 若有:在 `downstream_api_keys` 中 `find_downstream_key_by_token(value)`,命中则记 id;未命中则进入步骤 3。
|
||||
3. 若无(或步骤 2 未命中):生成一个 `sk-comfyui-{uuid_v4_no_hyphen}` 形式的 key,先 `INSERT INTO downstream_api_keys(name='comfyui-agent', key=?, user_id=NULL, enabled=1, description='Built-in key for ComfyUI Agent')`,再 `INSERT OR REPLACE INTO settings(key, value)`。
|
||||
4. 返回 `(id, secret)` 给 AppState 缓存(`AppState.builtin_keys.comfyui_agent: (i64, String)`)。
|
||||
|
||||
整个流程在事务中执行,确保 `settings` 与 `downstream_api_keys` 一致。
|
||||
|
||||
**为什么不每次启动重新生成**:
|
||||
- 每次换 key 会让历史 `proxy_logs` / `usage_logs` 仍然引用旧 id,但客户端无法用旧 key 验证。
|
||||
- 重启后 key 稳定也方便用户在前端 Downstream Keys 页面持续看到同一行。
|
||||
|
||||
**为什么不 hash 存储**:
|
||||
- 后端调用方就是后端自己,需要在内存中直接持有明文当 Bearer 用;hash 反而要额外解密通道。
|
||||
- 现有所有 downstream_api_keys 都是明文存储(`find_downstream_key_by_token` 直接 `WHERE key = ?`),保持一致。
|
||||
|
||||
### 3. base 解析优先级:env override → 本地拼接
|
||||
|
||||
```rust
|
||||
fn upstream_config(state: &AppState) -> (String, String, AgentMode) {
|
||||
if let Some(env_base) = std::env::var("BOOSAPI_UPSTREAM_BASE").ok()
|
||||
.map(|v| v.trim().trim_end_matches('/').to_string())
|
||||
.filter(|v| !v.is_empty())
|
||||
{
|
||||
let key = std::env::var("BOOSAPI_UPSTREAM_KEY").ok()
|
||||
.filter(|v| !v.trim().is_empty())
|
||||
.unwrap_or_else(|| state.builtin_keys.comfyui_agent.1.clone());
|
||||
return (env_base, key, AgentMode::FallbackRemote);
|
||||
}
|
||||
|
||||
let host = if state.config.server.host == "0.0.0.0" || state.config.server.host == "::" {
|
||||
"127.0.0.1"
|
||||
} else {
|
||||
&state.config.server.host
|
||||
};
|
||||
let base = format!("http://{}:{}/v1", host, state.config.server.port);
|
||||
let key = state.builtin_keys.comfyui_agent.1.clone();
|
||||
(base, key, AgentMode::Local)
|
||||
}
|
||||
```
|
||||
|
||||
**为什么默认本地而不是默认 env**:用户希望 BoosAPI 自包含运行;env 仅作为"我知道我在干什么"的回退。
|
||||
|
||||
**为什么不 await async client**:base 解析是纯字符串,每次请求重新算成本可忽略(每次请求最多 3 次调用);不缓存避免与 env 动态变化语义冲突。
|
||||
|
||||
### 4. status 端点扩字段,不改路径
|
||||
|
||||
`GET /api/comfyui-agent/status` 响应增加:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"model": "...",
|
||||
"upstreamBase": "<resolved>",
|
||||
"mode": "local" | "fallback-remote",
|
||||
"keyName": "comfyui-agent",
|
||||
"comfyui": {...}
|
||||
}
|
||||
```
|
||||
|
||||
前端可在标题栏显示 mode 徽标,区分本地 / 回退。
|
||||
|
||||
### 5. 模型名沿用现配置 `state.config.comfyui.agent_model`
|
||||
|
||||
不改 chat 调用的 `model` 字段。如果本地 `/v1/chat/completions` 没有这个模型,会返回 404,让用户去 token_routes 配置;这是正确的失败路径,比 silently fallback 好。
|
||||
|
||||
images 仍用 `gpt-image-2`、audio 仍用 `tts-1` —— 与现状一致,本地 token_router 命中即用。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[Risk] `127.0.0.1:{port}` 在容器中不可达**:当 server bind `0.0.0.0`,部分容器内 `127.0.0.1` 仍可达 host 进程;但若用户在 Dockerfile 里禁用 loopback,agent 会失败。 → Mitigation:保留 env 回退;status mode 字段帮助排障;文档明确说明。
|
||||
- **[Risk] settings 表在前端被 `app_settings` admin API 暴露**:明文密钥泄漏给 admin。 → Mitigation:在 settings 读写层加 `builtin_key.*` 前缀过滤,列出 settings 时 mask;本次实施任务覆盖该过滤。
|
||||
- **[Risk] 前端 Downstream Keys 页面显示 builtin key 的明文**:现有页面会列出明文。 → Mitigation:在 `routes/user_api_keys.rs`(admin list 入口)里把 `name='comfyui-agent' AND user_id IS NULL` 的 key 标记 `system: true` 并打码 key 显示后 4 位;禁止 DELETE/PATCH。
|
||||
- **[Risk] builtin key 被人为禁用**:admin 在 UI 把 enabled 改为 false,agent 会立即 401。 → Mitigation:启动引导时若发现 enabled=0,主动设回 1 并发 warning;admin API 拒绝对 system key 设 enabled=0。
|
||||
- **[Risk] 引导事务失败导致 settings 记录与 downstream_api_keys 不一致**:例如 settings 插入后进程崩溃,重启时 token 还在 settings 但表里没行。 → Mitigation:步骤 2 已经包含"settings 有值但表没行"的回填路径;幂等。
|
||||
- **[Trade-off] 仍保留 `BOOSAPI_UPSTREAM_*` env**:增加一个分支,但提供排障逃生口;用户态文档明确"仅排障使用"。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. **第 1 步**:加 `src/services/builtin_keys.rs` 模块,包含 `ensure_comfyui_agent_key(db: &SqlitePool) -> AppResult<(i64, String)>` 与单测(重复调用幂等、settings 有值但表无行的回填、表内有但 settings 无的恢复)。
|
||||
2. **第 2 步**:在 `AppState` 增加 `builtin_keys: BuiltinKeys { comfyui_agent: (i64, String) }`;`AppState::new` 改为 async(或在 `main.rs` 中先 ensure 再传入)。
|
||||
3. **第 3 步**:重写 `comfyui_agent.rs::upstream_config`,新增 `AgentMode` 枚举;三个 handler 改用新返回值;status 多返回 `mode` / `keyName`。
|
||||
4. **第 4 步**:在 admin 层(`routes/user_api_keys.rs` 或 admin keys 路由)对 `name='comfyui-agent' AND user_id IS NULL` 的行加保护:拒绝 DELETE / 拒绝 enabled=0;list 响应里标 `system=true`。
|
||||
5. **第 5 步**:在 settings 暴露层(确认有没有 generic settings list API)里过滤 / mask `builtin_key.*` 前缀。
|
||||
6. **第 6 步**:手测三类调用 + status mode 切换;`AGENTS.md` / `README.md` 更新。
|
||||
|
||||
回滚:每步独立 commit;如步骤 3 出问题,revert 即可恢复"默认远端"行为。步骤 1-2 是新增代码,无需回滚。
|
||||
|
||||
## Open Questions
|
||||
|
||||
- 是否有 generic `/api/admin/settings` 路由会列出 settings 表全部行?需要 `rg "FROM settings"` 在实施第一步前确认;若有,第 5 步改为对该路由加前缀过滤。
|
||||
- token_router 在没有为 `agent_model` / `gpt-image-2` / `tts-1` 配置路由时的失败模式是 404 还是 500?影响错误文案;实施前在本地起一个空配置环境跑一次。
|
||||
- builtin key 是否需要支持轮换?本次不做,Open Question 记录后续若有需求再开 spec。
|
||||
@@ -0,0 +1,41 @@
|
||||
## Why
|
||||
|
||||
`src/routes/comfyui_agent.rs` 目前把 chat / images / audio 三类上游调用全部硬编码走 `https://api.boos.lat/v1`(可被 `BOOSAPI_UPSTREAM_BASE` env 覆盖),完全绕过了本进程已经实现的代理层(token_router、capability、proxy_logs、计费)。这意味着:ComfyUI Agent 的流量看不见、算不上账、用不到本地的智能路由;而本服务自己就是一个 OpenAI 兼容网关,本地 `/v1` 已经能提供 chat/images/audio 三条能力。再继续走外部上游既绕过自家路由又造成"BoosAPI 调 BoosAPI"的奇怪部署形态。
|
||||
|
||||
## What Changes
|
||||
|
||||
- ComfyUI Agent 默认改为调用本进程自身的 `http://127.0.0.1:{server.port}/v1/*`,复用 token_router 与计费体系。
|
||||
- 引入一个**内置下游 API key**(name = `comfyui-agent`,user_id 留空表示系统级),仅 ComfyUI Agent 使用,单独计费/限额;启动时若不存在则自动创建并把明文写入 `app_settings`,重启后复用。
|
||||
- chat / images / audio 三类调用都切到本地 `/v1`,使用上述内置 key 作 Bearer。
|
||||
- 保留 `BOOSAPI_UPSTREAM_BASE` 为**可选回退**:仅当显式设置且非空时,Agent 才直连远端(用于本地路由不可用的排障场景),并在 `/api/comfyui-agent/status` 响应中明确暴露当前生效的 base 与是否为回退。
|
||||
- `/api/comfyui-agent/status` 新增字段 `keyName`(始终是 `comfyui-agent`)、`mode`(`local` 或 `fallback-remote`)。
|
||||
- **BREAKING**(运维侧):当未设置 `BOOSAPI_UPSTREAM_BASE` 时,部署不再需要外部 `api.boos.lat` 网络可达;但同时要求本进程 `server.port` 已绑定到回环且能被 Agent 自身访问。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `comfyui-agent-local-routing`: ComfyUI Agent 通过本进程 `/v1` 调用 chat / images / audio,使用专属内置下游 key 计费,并提供 `BOOSAPI_UPSTREAM_BASE` 远端回退。
|
||||
- `builtin-downstream-key`: 系统级(user_id IS NULL)内置下游 API key 的生命周期管理:启动时确保存在、密钥持久化在 `app_settings`、可在 admin API 中可见但不可删除。
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
<!-- openspec/specs/ 目前为空,没有既有 spec 受影响。 -->
|
||||
|
||||
## Impact
|
||||
|
||||
- **后端代码**
|
||||
- `src/routes/comfyui_agent.rs`:`upstream_config()` 改为从 AppState 拼本地 base + 内置 key;保留 env 回退分支并在响应里暴露 `mode`。
|
||||
- `src/services/`(或新建 `src/services/builtin_keys.rs`):启动时确保 `comfyui-agent` 内置 key 存在,明文落 `app_settings`。
|
||||
- `src/db/queries/`:新增 `find_or_create_builtin_downstream_key(name)` / `read_builtin_key_secret(name)`。
|
||||
- `src/lib.rs` 的 `AppState::new` 之后、`main.rs` 启动序列中调用 builtin key 引导。
|
||||
- **数据库**
|
||||
- 复用既有 `downstream_api_keys` 表(user_id 设为 NULL 表示系统级);可能需要一条迁移把 `user_id` 列改为可空(如果已是可空则跳过)。
|
||||
- 复用既有 `app_settings` / `settings` 键值表存放生成的明文密钥(避免每次重启换 key 让历史日志关联断裂)。
|
||||
- **观测**
|
||||
- ComfyUI Agent 流量从此出现在 `proxy_logs` 中,按 key=`comfyui-agent` 可在管理面板筛出。
|
||||
- **配置 / 文档**
|
||||
- `AGENTS.md` 与 `README.md` 的"ComfyUI Agent"段落需要更新:默认本地、env 回退、密钥位置。
|
||||
- **测试**
|
||||
- 新增端到端测试:mock 一个 `/v1/chat/completions` 路径返回固定 SSE,验证 Agent 命中本地。
|
||||
- 新增单测:builtin key 引导 idempotent;status 在 env 设置/未设置时分别返回 `local` / `fallback-remote`。
|
||||
@@ -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 的那一行
|
||||
+81
@@ -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 内容;后续请求仍走本地,不自动切远端
|
||||
@@ -0,0 +1,85 @@
|
||||
## 1. 内置 key 引导逻辑
|
||||
|
||||
- [x] 1.1 新建 `src/services/builtin_keys.rs`,定义 `pub struct BuiltinKeys { pub comfyui_agent: (i64, String) }` 与 `pub async fn ensure_comfyui_agent_key(db: &SqlitePool) -> AppResult<(i64, String)>`
|
||||
- [x] 1.2 实现 `ensure_comfyui_agent_key`:在事务中执行查 settings → 查 downstream_api_keys → 缺什么补什么,确保两边一致;密钥格式 `sk-comfyui-{uuid_v4 去横线}`,settings key = `builtin_key.comfyui_agent.secret`
|
||||
- [x] 1.3 引导逻辑发现 `enabled=0` 时主动设回 1,并 `tracing::warn!("comfyui-agent builtin key was disabled; re-enabling")`
|
||||
- [x] 1.4 在 `src/services/mod.rs` 中 `pub mod builtin_keys;`
|
||||
- [x] 1.5 单测:用 `tempfile::NamedTempFile` + 内存 sqlite 覆盖 5 个场景(首次启动、settings 有表无、表有 settings 无、重复启动密钥不变、enabled=0 自动恢复)
|
||||
|
||||
## 2. AppState 接入
|
||||
|
||||
- [x] 2.1 在 `src/lib.rs` 中给 `AppState` 增加 `pub builtin_keys: Arc<services::builtin_keys::BuiltinKeys>`
|
||||
- [x] 2.2 修改 `AppState::new` 签名为 async,或在 `main.rs` 中改为先 `ensure_comfyui_agent_key` 拿到结果再调 `AppState::new(config, pool, builtin_keys)`
|
||||
- [x] 2.3 调整 `src/main.rs` 启动序列:migrate 之后、`AppState::new` 之前调用引导;任何 builtin key 相关错误用 `anyhow::Context` 包裹后向上传播,不让服务在没有 builtin key 时启动
|
||||
- [x] 2.4 跑 `cargo build`、`cargo test --lib services::builtin_keys`,确认无回归
|
||||
|
||||
## 3. comfyui_agent.rs 切换调用目标
|
||||
|
||||
- [x] 3.1 在 `src/routes/comfyui_agent.rs` 顶部加 `enum AgentMode { Local, FallbackRemote }`,派生 Serialize 以便嵌入 JSON
|
||||
- [x] 3.2 重写 `upstream_config(state) -> (String, String, AgentMode)`:env 非空(trim 后)走 fallback-remote 分支并把 `BOOSAPI_UPSTREAM_KEY` 缺省时用 builtin key;否则用 `state.config.server.host/port` 拼 `http://{host}/v1`,host 是 `0.0.0.0` 或 `::` 时改写 `127.0.0.1`
|
||||
- [x] 3.3 三个 handler(chat 第 351 行、images 第 274 行、audio 第 311 行)全部改为接受 `(api_base, api_key, _mode)`,bearer 用新 `api_key`
|
||||
- [x] 3.4 `status_handler` 响应增加 `"mode": <local|fallback-remote>`、`"keyName": "comfyui-agent"`,`upstreamBase` 仍为已 trim 的字符串
|
||||
- [x] 3.5 单测(用 `axum-test`):env 未设、env 设、env 空字符串三种条件下 `/api/comfyui-agent/status` 返回的 mode 与 upstreamBase 正确
|
||||
- [x] 3.6 集成测:`wiremock` mock 一个 `/v1/chat/completions` 返回固定 SSE,断言 Agent 在 local 模式下命中该 mock
|
||||
|
||||
## 4. admin 层保护内置 key
|
||||
|
||||
- [x] 4.1 用 `rg "FROM downstream_api_keys" /root/metapi/src/routes` 找到 admin list / get / patch / delete 入口
|
||||
- [x] 4.2 在 list 响应映射处过滤:若 `name='comfyui-agent' AND user_id IS NULL` 则附加 `system: true` 字段、把 `key` 改为 `••••••••<后4位>`
|
||||
- [x] 4.3 在 DELETE 入口加守卫:命中系统 key 返回 403 + message `system key cannot be deleted`
|
||||
- [x] 4.4 在 PATCH/PUT 入口加守卫:当请求体试图把系统 key 的 `enabled` 设为 false 时拒绝
|
||||
- [x] 4.5 路由级单测:3 条用例覆盖打码 / DELETE 拒绝 / 禁用拒绝;普通 key 操作不受影响 1 条用例
|
||||
|
||||
## 5. settings 暴露面过滤
|
||||
|
||||
- [x] 5.1 用 `rg "FROM settings"` 与 `rg "/api/admin/settings"` 找到 settings list / get 入口(如有)
|
||||
- [x] 5.2 list 入口过滤所有以 `builtin_key.` 为前缀的 key
|
||||
- [x] 5.3 单 key 读入口:以 `builtin_key.` 开头时返回 404 或 `value: "••••"`,不返回真实明文
|
||||
- [x] 5.4 若上述路由不存在,把当前调研结果记入 commit message,跳过本组任务但保留 spec scenarios 待将来实现兜底
|
||||
|
||||
> 调研结论:当前 `src/routes/admin/mod.rs` 仅暴露 `GET /api/settings` (list-all) 与 `PUT /api/settings` (upsert),没有单 key GET。
|
||||
> 已实现:
|
||||
> - `get_settings` 在 `BTreeMap` 收敛前过滤 `key.starts_with("builtin_key.")`
|
||||
> - `update_setting` 对 `builtin_key.*` 写入返回 403 `system setting cannot be modified`,作为防御性兜底(即便 UI 永远看不到该 key,也防止已知 key 名被直接 PUT 篡改)
|
||||
> - 单 key GET 路由不存在,无需兜底;若将来新增需保留同样的 prefix 守卫
|
||||
|
||||
## 6. 文档与冒烟
|
||||
|
||||
- [x] 6.1 更新 `AGENTS.md` 第 32-36 行的 ComfyUI Agent 段落:默认本地、env 回退语义、密钥位置(settings + downstream_api_keys)
|
||||
- [x] 6.2 更新 `README.md` 中相关段落(搜索"ComfyUI Agent"出现处)
|
||||
- [ ] 6.3 跑全量校验:`cargo test`、`cargo clippy --all-targets`
|
||||
- [ ] 6.4 手测:默认启动一次 chat / images / audio 各一次;设置 `BOOSAPI_UPSTREAM_BASE=https://api.boos.lat/v1` 再启动一次三类调用;两次 status 响应 mode 字段切换正确(用户手测,等待执行)
|
||||
- [x] 6.5 在 PR 描述中列出每条 spec scenario 的对应测试位置,便于 reviewer 校对(mapping 见下文「附:scenario → test 映射」)
|
||||
|
||||
## 附:scenario → test 映射
|
||||
|
||||
### specs/builtin-downstream-key/spec.md
|
||||
|
||||
- 启动时确保内置 key 存在
|
||||
- 全新数据库首次启动 → `src/services/builtin_keys.rs` tests::ensures_key_on_first_boot
|
||||
- 已有 settings 缺表记录(崩溃恢复) → `src/services/builtin_keys.rs` tests::backfills_table_from_settings
|
||||
- 已有表记录缺 settings → `src/services/builtin_keys.rs` tests::backfills_settings_from_table
|
||||
- 重复启动不变更密钥 → `src/services/builtin_keys.rs` tests::idempotent_across_two_boots
|
||||
- enabled=0 自动恢复 → `src/services/builtin_keys.rs` tests::recovers_disabled_row
|
||||
- 系统级 key 在管理 API 中受保护
|
||||
- DELETE 被拒 / 禁用被拒 / 列表打码 / 普通 key 不受影响 → `src/routes/admin/downstream_keys.rs` `#[cfg(test)] mod tests`
|
||||
- settings 中的内置密钥不暴露给 admin 列表
|
||||
- 列表过滤前缀 → `src/routes/admin/mod.rs` tests::settings_list_filters_builtin_keys
|
||||
- 单读返回占位 → 单 key GET 路由不存在;防御性写入守卫由 tests::settings_update_rejects_builtin_key 覆盖(详见 5.4 调研结论)
|
||||
- 内置 key 用 user_id IS NULL 与普通 key 区分
|
||||
- 普通用户不能创建同名 key 占用系统标识 / 引导查询仅匹配系统行 → builtin_keys.rs 引导逻辑使用 `name='comfyui-agent' AND user_id IS NULL` 限定 + admin tests 覆盖
|
||||
|
||||
### specs/comfyui-agent-local-routing/spec.md
|
||||
|
||||
- ComfyUI Agent 默认调用本进程 /v1
|
||||
- 默认 base 指向本机回环 / 显式 host 被保留 / 三类调用使用同一 base → `src/routes/comfyui_agent.rs` tests::status_reflects_env_state_across_three_scenarios(Scenario A 覆盖回环改写、Scenario B 覆盖显式 host、共享 upstream_config 即覆盖三类一致)
|
||||
- env 显式设置时回退远端
|
||||
- env 非空触发回退 / env 空字符串视同未设置 → tests::status_reflects_env_state_across_three_scenarios(Scenario B + C)
|
||||
- env key 缺省时复用内置 key → upstream_config 实现内联(见 routes/comfyui_agent.rs 的 fallback-remote 分支)
|
||||
- 本地调用使用内置下游 key 计费
|
||||
- chat 请求携带内置 key → tests::agent_local_mode_routes_chat_to_local_upstream(wiremock 断言 `Bearer sk-comfyui-test`)
|
||||
- usage_logs 归集 → 间接覆盖(agent 经本进程 /v1,途经 token_router 自然写入 usage_logs,由现有 routes/proxy 路径覆盖)
|
||||
- status 端点暴露生效模式
|
||||
- 默认配置返回 local / env 设置时返回 fallback-remote → tests::status_reflects_env_state_across_three_scenarios
|
||||
- 鉴权失败仍不暴露密钥 → 由 status_handler 鉴权中间件覆盖(无新增分支)
|
||||
- 本地路由失败必须如实返回 → 由 SSE error 事件实现保留原行为,未新增专项测;如后续要硬覆盖,建议加 wiremock 404 + 断言 SSE error 事件
|
||||
Reference in New Issue
Block a user