完善 Router 共享账号与订阅契约

Router 固定 Token 展示名改为 agc_auto_generate,并同步后台安全查询测试。

文档记录跨独立数据库的稳定账号恢复、default 分组、订阅续期和 best-effort 扣费规则。

调整共享官方 Router 的非生产启动告警,并保留错误 code 构造能力。
This commit is contained in:
2026-09-02 14:56:19 +08:00
parent 1c6c499a11
commit b414f2f5f8
7 changed files with 39 additions and 10 deletions
@@ -105,7 +105,7 @@ test('external_api_key 使用专用安全查询且详情不展示原始 JSON', a
{
keyId: 'external-api-key-1',
ownerUserId: 'user-1',
name: '陶泥儿 LLM Router 官方账号(服务端)',
name: 'agc_auto_generate',
keyPrefix: 'tnr_sk_fixture',
purpose: 'llm-router',
scopes: ['llm:responses'],
@@ -7901,6 +7901,23 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
- api-server 读取 `GENARRATIVE_ENV`;只有 `production` 才允许固定官方 Router 控制面,生产缺管理员 Token 在启动时告警并在新账号 provisioning 时拒绝。`development``test``container` 等非生产环境默认只允许 loopback Router,避免测试调用线上用户服务创建真实账号、冲突用户名或消耗额度。
- 删除进程级 Router fallback Key 语义。测试如需模拟已完成账号,只能注入显式 owner-scoped、loopback 的“已 provisioning”fixture;没有 fixture 必须走 `external_api_key` / `llm_router_account` 查询与正式 provisioning,不能直接访问上游。新生成的 pending 凭据不再被误判为可复用远端账号。
## 2026-09-01 LLM Router 公共实例与独立数据库的稳定账号恢复
- Router 为公共实例、各部署数据库独立时,Router 用户密码改为由固定版本 provisioning secret、Router 控制面 origin 和 owner 稳定推导;所有能操作同一 Router 的部署必须使用同一 secret。该 secret 当前按临时过渡方案固定在 api-server 服务端实现,客户端、数据库明文、日志和普通请求不接触;后续再迁移到部署密钥管理并保留 `credential_version`
- 数据库没有本地 `llm_router_account` / `external_api_key` 行时,先用稳定用户名/密码查询并登录远端账号;只有确认用户不存在才注册。登录成功后先按固定名称查询 Router token,存在则复用,不存在才创建,避免不同部署因本地数据库为空而重复创建远端账号或 token。注册返回冲突时必须重新查询,不得盲目重试。
## 2026-09-01 Router 账号可恢复标识与跨开发库复用
- Router 用户名固定为 `agc_user_` 加 11 位 URL-safe SHA-256 短码,短码由完整 owner `user_id` 稳定派生,以满足 New API `username` 20 字符上限;完整 owner `user_id` 同步写入 New API 用户 `remark`,Router 统计可据此直接回溯对应的 Genarrative 用户,即使某个部署的本地数据库被重置。
- Router 用户密码只由 owner user id 与固定版本 provisioning secret 稳定派生,不绑定 route origin;所有连接同一公共 Router 的开发/生产部署都能计算同一密码。
- Router 用户下用于 AGC 的 Token 固定标识为 `agc_auto_generate`。各部署登录后先按该标识查询并复用 Token,再通过 Token Key 接口取得同一把 API Key;不存在时才创建 Token,避免每次 provisioning 新建 Key。
## 2026-09-01 Router 订阅按账号准备续期
- 每次 api-server 准备或复用用户 Router API Key 时,在账号登录成功后、Token/API Key 流程继续前查询 New API 管理订阅接口。固定套餐为 `plan_id=1`:没有 active 订阅、订阅已过期或 `end_time - now <= 24h` 时创建一条管理员订阅;剩余超过 24 小时则复用现有订阅。`end_time` 按 New API 合同解释为 Unix 秒。
- 检查锚点固定为认证后的账号准备、显式 Router Key 准备和 LLM 请求解析凭据路径,不放入 Responses 流式 chunk;同一 api-server 进程继续复用 owner 级 provisioning mutex,跨实例重复订阅幂等性依赖 Router 端后续约束或部署侧串行化。
- 订阅查询/创建只使用 api-server 私有管理员 Token;管理员 Token 缺失时保留启动 Warning 并跳过续期检查,不能伪造客户端凭据或把 Router Key 暴露给客户端。
## 2026-08-29 DirectProject 受控联网搜索默认与边界
- 正式产品本次只覆盖 `DirectProject` 单 Codex Agent。`Provider``ToolHost``DirectHome` 不是 Agent,也不是本次联网主链路;不新增全路由联网或工具桥。唯一受控联网工具为 `agc_tools.agc_web_search`,链路固定为 Codex MCP 工具目录 -> 客户端 loopback `DirectToolBridge` -> 有界 Bing RSS HTTPS -> 过滤 / 脱敏 -> MCP 结果回传。
@@ -630,10 +630,12 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
- Rust 结构体:`ExternalApiKey`
- 源码:`server-rs/crates/spacetime-module/src/external_api_key_storage.rs`
- 说明:外部 OpenAPI 与 LLM Router 使用的账号级 API Key 凭据表,公开元数据只包含 key prefix、作用域、用途、撤销状态和使用时间;服务端内部还保存 Router Key 的 AES-256-GCM 密文、Router 账号标识和固定上游元数据,明文永不通过登录态接口、SpacetimeDB 表查询或客户端返回。普通 External Editor Key 的明文只在 `/api/profile/api-keys` 创建接口返回一次,后端只保存 `key_hash``key_prefix`;正式 Router 账号用途固定为 `purpose=llm-router`,每次认证成功后由 api-server 幂等确保并保存,已有合法记录直接复用。Router 账号由 New API 管理 Token 完成创建、分组、登录、无限额度 token 和 API Key 签发Router 凭据只来源于这条正式账号流程。Router 密文加密优先使用专用 `GENARRATIVE_LLM_ROUTER_API_KEY_ENCRYPTION_SECRET`;缺省时使用带域分离的 JWT secret 派生值。生产环境应显式配置专用 secret,轮换时先完成旧密文对账,不能静默创建第二个 active Key。`/api/profile/api-keys/llm-router` 只返回安全元数据,不返回 Router 明文 Key。API Key 管理接口不写入外部 OpenAPI JSON。`purpose=external-editor` 是默认用途,v1 默认作用域为 `editor:project``editor:canvas``editor:image-generate``editor:asset`;其中 `editor:project` 覆盖项目列表、最近项目、创建、读取、重命名和删除,`editor:canvas` 覆盖默认画布布局保存,`editor:image-generate` 覆盖编辑器现有图片生成、重绘 / 调整、去背景、规范图、宣发素材、图标 spritesheet 生成 / 拆分、UI 设计图素材拆分、角色动画、视频、音效和背景音乐生成,`editor:asset` 覆盖素材直传凭证、素材对象确认、签名读取、账号级素材库和项目画布资源记录操作。`purpose=llm-router` 是 LLM Router 正式账号 Key,作用域固定为 `llm:responses`
- 说明:外部 OpenAPI 与 LLM Router 使用的账号级 API Key 凭据表,公开元数据只包含 key prefix、作用域、用途、撤销状态和使用时间;服务端内部还保存 Router Key 的 AES-256-GCM 密文、Router 账号标识和固定上游元数据,明文永不通过登录态接口、SpacetimeDB 表查询或客户端返回。普通 External Editor Key 的明文只在 `/api/profile/api-keys` 创建接口返回一次,后端只保存 `key_hash``key_prefix`;正式 Router 账号用途固定为 `purpose=llm-router`,每次认证成功后由 api-server 幂等确保并保存,已有合法记录直接复用。Router 账号由 New API 管理 Token 完成创建、用户分组设置、登录、无限额度 token 和 API Key 签发:用户分组固定为 `taonier`AGC Token/API Key 固定为 `default` 分组;已有固定 Token 若分组不是 `default`,登录恢复时先通过 Token 更新接口纠正。Router 凭据只来源于这条正式账号流程。Router 密文加密优先使用专用 `GENARRATIVE_LLM_ROUTER_API_KEY_ENCRYPTION_SECRET`;缺省时使用带域分离的 JWT secret 派生值。生产环境应显式配置专用 secret,轮换时先完成旧密文对账,不能静默创建第二个 active Key。`/api/profile/api-keys/llm-router` 只返回安全元数据,不返回 Router 明文 Key。API Key 管理接口不写入外部 OpenAPI JSON。`purpose=external-editor` 是默认用途,v1 默认作用域为 `editor:project``editor:canvas``editor:image-generate``editor:asset`;其中 `editor:project` 覆盖项目列表、最近项目、创建、读取、重命名和删除,`editor:canvas` 覆盖默认画布布局保存,`editor:image-generate` 覆盖编辑器现有图片生成、重绘 / 调整、去背景、规范图、宣发素材、图标 spritesheet 生成 / 拆分、UI 设计图素材拆分、角色动画、视频、音效和背景音乐生成,`editor:asset` 覆盖素材直传凭证、素材对象确认、签名读取、账号级素材库和项目画布资源记录操作。`purpose=llm-router` 是 LLM Router 正式账号 Key,作用域固定为 `llm:responses`
- 索引:`by_external_api_key_owner_user_id` 用于登录态 API Key 列表;`key_hash` 唯一索引用于外部 API 鉴权。
- 2026-08-31 修订:LLM Router 行在上述字段末尾追加 `provider_account_json`Router 账号 JSON,普通 External Editor Key 为 `NULL`)与 `credential_version`(默认 `1`)。每次认证成功由 api-server 通过 New API 管理 Token 完成“创建用户 → 查询用户 ID → 设置 `taonier` 分组 → 登录 → 创建无限额度 token → 签发 API Key”,再把账号状态与加密凭据写入 `llm_router_account` / `external_api_key`;Router 凭据只来源于正式账号流程。Responses 请求成功后再按 usage 写入钱包扣费流水,失败请求不扣费。
- 2026-09-01 修订:Router provisioning `GENARRATIVE_ENV` 门禁保护。只有 `production` 允许固定官方 Router 控制面;`development``test``container` 默认只接受 loopback fixture,避免测试环境通过线上用户服务创建真实账号。api-server 不再提供任何 fallback Key 路径;没有已完成 provisioning 的账号行时,LLM 请求必须在本地解析阶段失败关闭。
- 2026-08-31 修订:LLM Router 行在上述字段末尾追加 `provider_account_json`Router 账号 JSON,普通 External Editor Key 为 `NULL`)与 `credential_version`(默认 `1`)。每次认证成功由 api-server 按稳定用户名/密码先查询并恢复远端用户;确认不存在时再通过 New API 管理 Token 完成“创建用户 → 查询用户 ID → 设置 `taonier` 分组 → 登录 → 查询并复用固定标识 `agc_auto_generate` 的 Token(不存在才创建)→ 签发 API Key”,再把账号状态与加密凭据写入 `llm_router_account` / `external_api_key`;Router 凭据只来源于正式账号流程。公共 Router、独立数据库的部署必须共享同一版本 provisioning secret,数据库缺行时才能恢复同一远端账号。Responses 请求成功后再按 usage 写入钱包扣费流水,失败请求不扣费。
- 2026-09-01 修订:Router provisioning 允许所有环境使用官方固定 Router 控制面,以便独立开发数据库通过完整 owner `user_id` 的稳定派生凭据和 `agc_auto_generate` Token 复用同一远端账号/Key。由于 New API 的 `username``password``display_name` 均限制 20 个字符,用户名固定为 `agc_user_` 加 11 位 URL-safe SHA-256 短码,密码为基于完整 owner `user_id` 与 provisioning secret 派生的 20 位 hex;完整 owner `user_id` 通过 New API 用户 `remark` 字段保存,并在本地 `llm_router_account.owner_user_id` 保留权威映射。非官方公网地址仍拒绝,loopback 仅用于本地 fixture。使用共享官方 Router 的非生产环境启动时告警,提醒会触及线上账号与额度。api-server 不再提供任何 fallback Key 路径;没有已完成 provisioning 的账号行时,LLM 请求必须在本地解析阶段失败关闭。
- 2026-09-01 修订:每次账号认证、Router Key 准备或 LLM 请求解析既有账号时,api-server 都会在同一 owner 级 provisioning 锁内检查固定套餐 `plan_id=1`。没有 active 订阅、订阅已过期或 `end_time` 距当前 Unix 秒不超过 24 小时时,使用管理员接口新建一条订阅;超过 24 小时则复用现有订阅。订阅检查不进入 Responses 流式 chunk,管理员 Token 缺失时仅保留启动告警并跳过续期检查。
- 2026-09-02 修订:LLM Router 成功返回后按“每开始 10,000 token 扣 1 点、至少 1 点”后置结算。余额足够时全额扣除;余额不足时在同一钱包事务内扣除当前可消费余额并记录 `waivedPoints` 差额,响应仍正常返回。该规则为过渡产品策略,待真实定价接入后替换。
- Windows 私有文件准备:AGC 自有 AppData、凭据目录和 `.agent` 运行态继续使用 managed 范围;用户通过原生选择器明确选中的项目根或文件,若 owner/DACL 仅因权限不足无法读取,则由一次性 UAC helper 在严格复核普通文件/目录、非 reparse/symlink、路径类型和目标 TokenUser 后接管并收紧为当前用户私有 DACL。项目放在当前 profile 之外(例如其他磁盘)不再因为路径位置被拒绝;未经过原生选择器或 AGC 项目根入口的内部路径仍不获得任意提权资格。
### `admin_account`
@@ -215,7 +215,7 @@ SpacetimeDB procedure
### LLM Router 账号 Key 与 Router 代理
普通 AGC 发行版不把 Router 当作客户端可配置 Provider,也不把 Router API Key 下发到桌面端。注册成功视为账号已有可用余额;每次密码、手机号或微信认证成功后,api-server 都必须幂等准备该用户对应的 Router 账号和 API Key,只有准备成功后才发放登录会话,正式记录使用 `purpose=llm-router` 并保存服务端加密凭据。当前按 New API 管理接口执行正式 provisioning管理员创建随机密码的普通用户,查询用户 ID,设置 `group=taonier`,再用该用户登录、创建无限额度 token 并签发 API Key。若任一步外部结果不确定,记录进入 reconciliation 状态,禁止重复注册;本地 API Key 写入失败则保留 `key_issued` 状态并用确定的 key id 重试落库。Router 密文加密优先使用 `GENARRATIVE_LLM_ROUTER_API_KEY_ENCRYPTION_SECRET`;缺省时使用带域分离的 `GENARRATIVE_JWT_SECRET` 派生密钥。Router Key 的明文只在 api-server 进程内短暂存在,`/api/profile/api-keys/llm-router` 只返回安全元数据;普通 External Editor Key 仍沿用创建接口明文只显示一次的正式链路。后续请求链路固定为:
普通 AGC 发行版不把 Router 当作客户端可配置 Provider,也不把 Router API Key 下发到桌面端。注册成功视为账号已有可用余额;每次密码、手机号或微信认证成功后,api-server 都必须幂等准备该用户对应的 Router 账号和 API Key,只有准备成功后才发放登录会话,正式记录使用 `purpose=llm-router` 并保存服务端加密凭据。当前按 New API 管理接口执行正式 provisioning由于 `username``password``display_name` 均限制 20 个字符,用户名固定为 `agc_user_` 加 11 位 URL-safe SHA-256 短码,密码为基于完整 owner `user_id` 与固定版本 provisioning secret 稳定派生的 20 位 hex,展示名与短用户名一致;完整 owner `user_id` 写入 New API 用户 `remark`,本地 `llm_router_account.owner_user_id` 仍是平台权威映射。服务端先查询远端用户:已存在则直接登录,不重复注册;确认不存在时才由管理员创建普通用户,查询用户 ID,设置 `remark=<完整 owner user_id>` 与用户分组 `taonier`,再登录、查询并复用固定标识 `agc_auto_generate` 的 TokenToken/API Key 固定使用 `default` 分组;已有固定 Token 若分组不是 `default`,登录恢复时先通过 Token 更新接口纠正),签发 API Key。每次新建或准备 API Key 时,服务端在签发前查询该 Router 用户的固定套餐 `plan_id=1`;无 active 订阅、订阅已过期或剩余时间不超过 24 小时时调用管理员订阅接口新建一条订阅,剩余超过 24 小时则复用现有订阅。订阅查询/创建只使用 api-server 私有管理员 Token,不进入客户端或 Router Key;检查锚点是账号认证成功、显式 Router Key 准备接口和新 Key provisioning,不放在 Responses 流式 chunk 中。由于 Router 公共而各部署数据库独立,所有能操作同一 Router 的部署必须使用相同的 provisioning secret。若任一步外部结果不确定,记录进入 reconciliation 状态,禁止重复注册;本地 API Key 写入失败则保留 `key_issued` 状态并用确定的 key id 重试落库。Router 密文加密优先使用 `GENARRATIVE_LLM_ROUTER_API_KEY_ENCRYPTION_SECRET`;缺省时使用带域分离的 `GENARRATIVE_JWT_SECRET` 派生密钥。Router Key 的明文只在 api-server 进程内短暂存在,`/api/profile/api-keys/llm-router` 只返回安全元数据;普通 External Editor Key 仍沿用创建接口明文只显示一次的正式链路。后续请求链路固定为:
```text
AGC loopback Provider ProxyBearer=平台 access token
@@ -230,7 +230,7 @@ Windows 私有路径由同一套正式准备入口复用:AGC 自有 AppData、
管理员 Token 仅由 api-server 私有配置 `GENARRATIVE_LLM_ROUTER_ADMIN_TOKEN` 或对应文件提供,不进入客户端、数据库、日志或请求 payload。当前仓库无法验证真实管理员 Token、真实账号 Key 与生产 Router `/v1/responses` 联通;这些仍需在受控部署 smoke 中完成,客户端不得退回手工 Key。
运行环境隔离是 provisioning 的前置门禁:`GENARRATIVE_ENV=production` 才允许使用固定官方 Router 地址并执行 New API 用户创建;`development``test``container` 等非生产环境只允许 loopback Router fixture,默认拒绝线上用户服务,避免测试创建真实账号、产生用户名冲突或消耗线上额度。测试代码也不再通过进程级 fallback Key 旁路数据库;只有显式、按 owner 绑定的“已完成 provisioning” fixture 才能模拟已有账号,未提供 fixture 时必须经过 `external_api_key`/`llm_router_account` 读取或正式 provisioning,不能访问 Router。刚生成的 pending 用户名/密码不会被当成既有远端账号只有确认的 Router account id 或历史合法凭据才允许复用登录
运行环境隔离是 provisioning 的前置门禁:所有环境都允许使用官方固定 Router 地址,开发环境因此可以通过完整 owner `user_id` 派生的稳定短用户名、稳定密码和 `agc_auto_generate` Token 复用共享 Router 账号;完整 owner `user_id` 同步写入 New API 用户 `remark`,便于 Router 侧统计回溯平台账号。非官方公网地址仍被拒绝,loopback 地址仅用于本地 fixture。使用共享官方 Router 的开发环境会实际创建/使用线上账号与额度,启动时必须告警。测试代码也不再通过进程级 fallback Key 旁路数据库;只有显式、按 owner 绑定的“已完成 provisioning” fixture 才能模拟已有账号,未提供 fixture 时必须经过 `external_api_key`/`llm_router_account` 读取或正式 provisioning,不能访问 Router。数据库记录缺失时,稳定短用户名/密码先尝试恢复远端账号只有确认远端不存在才注册,避免跨部署数据库为空时重复创建用户
## 素材生成与落库
+1 -1
View File
@@ -5489,7 +5489,7 @@ mod tests {
&json!([
"external-api-key-1",
"user-1",
"陶泥儿 LLM Router 官方账号(服务端)",
"agc_auto_generate",
"tnr_sk_fixture",
"[\"llm:responses\"]",
"2026-08-29T00:00:00Z",
@@ -63,6 +63,11 @@ impl AppError {
self
}
pub fn with_code(mut self, code: &'static str) -> Self {
self.code = code;
self
}
pub fn with_details(mut self, details: Value) -> Self {
self.details = Some(details);
self
+8 -3
View File
@@ -208,14 +208,19 @@ fn log_llm_router_startup_warnings(config: &AppConfig) {
);
}
if !config.is_production() && !is_loopback {
if !config.is_production() && base_url == OFFICIAL_LLM_ROUTER_BASE_URL {
warn!(
environment = %config.environment,
"非生产环境 LLM Router 目标不是 loopback;为避免测试污染线上用户,provisioning 和请求将被拒绝"
"非生产环境正在使用共享官方 LLM RouterRouter 账号、Token 和额度属于共享线上实例"
);
} else if !config.is_production() && !is_loopback {
warn!(
environment = %config.environment,
"非生产环境的 LLM Router 目标既不是官方固定路由也不是 loopbackprovisioning 和请求将被拒绝"
);
}
if config.is_test_environment() {
if config.is_test_environment() && is_loopback {
warn!("当前为 test 环境:必须使用 loopback Router fixture,不会连接线上用户服务");
}
}