.agents/design/api/openapi-apikey-redesign.md
当前 OpenAPI APIKey 同时存在团队级 key 和带 appId 绑定的旧 key:
openapi 集合通过 appId 是否存在区分 key 类型。authOpenApiKey 通过 authApiKey / authAppApiKey 判断当前接口是否允许不同来源的 key。appId 推导应用上下文。appId 创建和列出 key,拥有应用管理权限的人可以看到该应用下所有 key。这使 APIKey 身份凭证和业务资源上下文耦合在一起。新方案将 APIKey 统一为团队成员身份凭证;应用上下文由 handler 显式传入,只有 v1/v2 completions 为兼容 OpenAI SDK 保留 appId 兜底来源。
appId。appId。authAppApiKey 完全移除,只保留 authApiKey。authAppByApiKeyTeam 删除,所有应用资源回到现有 tmbId 权限链路。/api/v1/chat/completions 和 /api/v2/chat/completions 外,所有需要应用上下文的开放接口必须显式传 appId。body.appId;apiKey-appId 只作为 OpenAI SDK 兼容格式。handlerAppId > parsedAppId > legacyAppId。openapi.appId 不做批量迁移,字段标记 deprecated。appId 字段。APIKey 只表示“某个团队成员的开放接口调用凭证”。鉴权成功后只提供:
teamIdtmbIdapikeyauthProxy 配置legacyAppId / parsedAppId后续资源鉴权沿用现有 token 路径的 tmbId 权限链路,不新增 APIKey 专属权限模型。
appId 表示本次请求操作哪个应用,由 handler 在 API 边界解析后显式传给鉴权服务。
appId 必填,handler 必须传入。body.appId 为空,handler 仍把解析结果传入;为空时鉴权服务再按兼容来源兜底。req.body、req.query 或 req.params 中的 appId,也不关心具体路由。POST /api/support/openapi/create
appId。appId。TeamApikeyCreatePermissionVal。authProxy 仅团队 owner 可开启。tmbId 统计。appId 时忽略该字段并创建系统 key;OpenAPI 文档标记该字段废弃或移除。GET /api/support/openapi/list
appId。tmbId 创建的 key。canCopy 对返回列表恒为 true。appId 时忽略该字段并返回本人系统 key;OpenAPI 文档标记该字段废弃或移除。PUT /api/support/openapi/update
openapi.tmbId === 当前登录 tmbId 的 key。authProxy 开关仍要求当前登录成员是 team owner。POST /api/support/openapi/copy
apiKey-appId 拼接值。appId 拼接。DELETE /api/support/openapi/delete
GET /api/support/openapi/health
-<24位ObjectId>,先拆出真实 key 和 parsedAppId,再用真实 key 查库。appId 的 key 继续返回 deprecated 的 legacyAppId。保留:
authApiKey移除:
authAppApiKey所有历史 authAppApiKey: true 调用点改为 authApiKey: true,并由 handler 显式传入 appId。
authOpenApiKey 只负责:
返回建议:
{
apikey: string; // 真实 APIKey,不包含 -appId 后缀
teamId: string;
tmbId: string;
legacyAppId: string; // 旧 DB appId,仅用于 completions 兜底
authProxy: boolean;
sourceName: string;
parsedAppId?: string; // 从 Bearer apiKey-appId 解析出来的 appId
}
不再返回或依赖:
keyType: 'team' | 'app'appId 作为常规鉴权结果为兼容 Bearer apiKey-appId,统一封装凭证解析:
resolveOpenApiCredential(authorization)
解析策略:
Authorization: Bearer xxx 取出 rawCredential。rawCredential 命中 ^(.+)-([a-fA-F0-9]{24})$,拆为真实 apiKey 和 parsedAppId。rawCredential 本身就是真实 apiKey,parsedAppId 为空。apiKey 查询记录。apiKey-appId 是传输层兼容格式,不会入库,因此命中兼容格式时不需要再用完整 rawCredential 查库。
对 v1/v2 completions,兼容期存在 3 个 appId 来源:
handlerAppId:handler 传入的 appId;对 completions 来自 body.appId。parsedAppId:从 Authorization: Bearer apiKey-appId 解析。legacyAppId:旧 DB 记录 openapi.appId。固定优先级:
handlerAppId > parsedAppId > legacyAppId
规则:
legacyAppId 兜底值;显式传入其他 appId 时仍按 key 所属 tmbId 校验权限。APIKey 鉴权本质上只拿到 key 记录上的 teamId、tmbId 和限额状态。后续资源鉴权沿用现有 token 路径的 tmbId 权限链路。
需要删除旧兼容捷径:
authAppByApiKeyTeam helper。authAppByApiKeyTeam 引用。tmbId。团队 owner 创建的系统 APIKey 可以开启 authProxy。该选项只影响 v1/v2 completions 中解析出的实际调用成员,不改变真实 APIKey 的校验、限额和开关判断。
authProxy 只能由 team owner 在本人系统 key 上开启或关闭。authProxy。tmbId、限额状态和是否允许 authProxy。authProxy.username 或 authProxy.tmbId 把 effective tmbId 切换为同 team 的另一个成员。tmbId 继续走 authApp、authChat 等现有权限链路。authProxy 不能绕过 app/chat 权限;代理成员无权访问目标应用或目标会话时必须拒绝。tmbId 用于应用鉴权、会话鉴权、对话归属、运行用户信息、用量来源等运行上下文。authProxy.username 和 authProxy.tmbId 同时传入时必须指向同一个成员。覆盖接口:
POST /api/v1/chat/completionsPOST /api/v2/chat/completions规则:
body.appId 指定应用。appId,但只用于兼容。Authorization: Bearer apiKey-appId。authProxy 只允许未绑定 legacy app 且开启 authProxy 的系统 key 使用。apiKey-appId 不应导致 authProxy 被禁用;判断依据是真实 key 是否为新系统 key 且开启 authProxy,不是本次请求是否解析出了 parsedAppId。authProxy 不跳过资源鉴权;解析 effective tmbId 后,必须用该 tmbId 校验目标 app 和 chat 权限。除 v1/v2 completions 外,凡是需要应用上下文的开放接口都必须显式传 appId。
重点关注:
/api/core/chat/init/api/core/chat/history/**/api/core/chat/record/**/api/core/chat/feedback/**/api/core/chat/file/**/api/core/app/logs/**改造原则:
appId 必填。parseApiInput。appId 调用鉴权。apiKeyAppId 作为 matchAppId 或查询条件兜底。删除应用时:
appId 字段 unset。body.appId 且不使用 apiKey-appId,应因无法解析 appId 而拒绝。appId 创建或筛选 key。appId,仅用于展示调用示例和兼容复制。Authorization: Bearer apiKey + body.appId。apiKey-appId。appId 写入 API 请求。authProxy 开关不再因为 defaultData.appId 隐藏;它只根据用户是否 team owner 和是否系统 key 判断。appId 必填。body.appId。apiKey-appId 只出现在 OpenAI SDK 兼容说明中。| 场景 | 是否允许 | appId 来源 | 说明 |
|---|---|---|---|
| 新系统 key + 普通应用接口 + 显式 appId | 允许 | body/query | 继续校验 tmbId 权限 |
| 新系统 key + 普通应用接口 + 不传 appId | 拒绝 | 无 | 除 completions 外必须显式传 |
| 新系统 key + completions + body.appId | 允许 | handlerAppId | 推荐方式 |
新系统 key + completions + apiKey-appId | 允许 | parsedAppId | OpenAI SDK 兼容 |
| 新系统 key + completions + 无 appId | 拒绝 | 无 | 无法确定应用 |
| 旧应用 key + completions + 无 appId | 允许 | legacyAppId | 历史兼容 |
| 旧应用 key + 普通应用接口 + 显式 appId | 允许 | body/query | 旧 key 按系统 key 处理,继续校验 tmbId 权限 |
| 旧应用 key + 普通应用接口 + 不传 appId | 拒绝 | 无 | 防止继续扩散隐式 appId |
| completions + body.appId 与 parsedAppId 不一致 | 允许 | handlerAppId | 高优先级覆盖低优先级 |
更新 packages/global/openapi/support/openapi/api.ts
CreateApiKeyBodySchema 移除或废弃 appIdGetApiKeyListQuerySchema 移除或废弃 appId更新 APIKey CRUD 路由
create 不再写入 appIdcreate 数量限制改为按 tmbIdlist 只查询当前 tmbIdupdate/copy/delete 只允许当前 tmbId 操作authProxy 保留 team owner 限制重构 OpenAPI APIKey 鉴权
apiKey-appId 解析 helperauthOpenApiKey 不再按 appId 区分 keyTypeparseHeaderCert 返回真实 key、legacyAppId、parsedAppIdauthAppApiKeyappIdhandlerAppId > parsedAppId > legacyAppId删除旧应用 key 特殊鉴权路径
authAppByApiKeyTeam helperauthAppByApiKeyTeam 引用tmbId 权限校验链路改造 v1/v2 completions
appId 并传给 auth servicehandlerAppId > parsedAppId > legacyAppIdauthProxy 判断改用 legacy key 状态,而不是解析出的本次 appIdauthProxy 解析 effective tmbId 后,用 effective tmbId 执行 authApp 和 authChat 鉴权/api/v1/chat/completions 和 /api/v2/chat/completions 保持同一套 appId 解析规则改造其他应用对话接口
authAppApiKeyappIdapiKeyAppId 兜底 appId更新删除应用逻辑
MongoOpenApi.deleteMany({ appId })appId 字段更新前端
ApiKeyTable 创建/list 不再传 appIdAuthorization: Bearer apiKey + body.appIdapiKey-appId更新 OpenAPI 文档
appId 必填body.appIdapiKey-appId本次改造必须同时覆盖以下层级:
packages/service 单元测试:覆盖 APIKey 解析、鉴权结果、限额调用、lastUsedTime 更新、用量更新 key 选择。projects/app API 路由测试或集成测试:覆盖 APIKey CRUD、v1/v2 completions、典型非 completions 应用接口。appId、apiKey-appId 兼容格式。除单元测试外,必须准备一套可重复执行的本地集成测试环境:
projects/app 本地服务,测试通过 HTTP 调用真实 Next.js API 路由。apiKey-appId、旧应用 key。建议新增独立集成测试目录,例如:
projects/app/test/integration/openapi-apikey/
如果仓库已有更合适的集成测试目录,应沿用现有目录,但测试名称必须能看出属于 OpenAPI APIKey 改造。
对话相关开放接口覆盖范围以生成后的 systemopenapi 为准,不能只手写挑选几个接口。测试应从 packages/global/openapi/provider/systemopenapi.ts 生成结果或对应 path/tag 源文件中提取以下 tag:
SystemOpenApiTagMap.chatHistorySystemOpenApiTagMap.chatSystemOpenApiTagMap.chatFeedbackSystemOpenApiTagMap.chatController当前必须覆盖的 systemopenapi 对话接口清单如下;后续新增 systemopenapi 对话接口时,测试清单必须同步更新。
POST /api/v1/chat/completionsPOST /api/v2/chat/completionsPOST /api/v2/chat/stopGET /api/core/chat/initPOST /api/core/chat/history/getHistoriesPOST /api/core/chat/history/getHistoryStatusPOST /api/core/chat/history/markReadPUT /api/core/chat/history/updateHistoryDELETE /api/core/chat/history/delHistoryDELETE /api/core/chat/history/clearHistoriesPOST /api/core/chat/history/batchDeletePOST /api/core/chat/record/getPaginationRecordsPOST /api/core/chat/record/getRecords_v2GET /api/core/chat/record/getResDataDELETE /api/core/chat/record/deletePOST /api/core/chat/feedback/updateUserFeedback最低集成覆盖标准:
appId 的非 completions 接口至少有 1 个缺少 appId 的拒绝用例。body.appId、apiKey-appId、旧应用 key 3 条兼容路径。systemopenapi 中有 tag 但暂时无法跑通的接口,必须在测试文件中显式 todo/skip 并写明阻塞原因,不能遗漏。建议增加一个覆盖校验测试:
systemopenapi paths。必须覆盖:
Authorization: Bearer <apiKey> 能按真实 key 鉴权成功,返回真实 apikey、teamId、tmbId。Authorization: Bearer <apiKey>-<appId> 能拆出真实 key 和 parsedAppId。apiKey-appId 拆分只匹配末尾 -<24位ObjectId>。apiKey-非ObjectId 不拆分,按真实 key 查询;不存在则拒绝。authOpenApiHandler 只在真实 key 鉴权成功后调用。lastUsedTime 只更新真实 key 记录。apiKey-appId 拼接值。keyType 作为业务判断依据。必须覆盖:
authAppApiKey 入参。parseHeaderCert、authCert、authOpenApiKey 不再接收或传递 authAppApiKey。authAppApiKey 的调用点全部改为 authApiKey。openapi.appId 自动获得应用上下文。rg "authAppApiKey" 只能出现在迁移说明或测试快照允许范围内;业务代码中应为 0。针对 /api/v1/chat/completions 和 /api/v2/chat/completions 都必须覆盖同一组用例:
body.appId:成功,使用 handlerAppId。apiKey-appId + 无 body.appId:成功,使用 parsedAppId。body.appId + 无 parsedAppId:拒绝。body.appId + 无 parsedAppId:成功,使用 legacyAppId。handlerAppId、parsedAppId、legacyAppId 同时存在且互不相同:成功使用 handlerAppId,不做冲突报错。parsedAppId 和 legacyAppId 不同且无 handlerAppId:成功使用 parsedAppId。req.body.appId;测试应通过 handler 传入的 appId 断言优先级。至少选择以下接口类别做代表测试:
/api/core/chat/init/api/core/chat/history/**/api/core/chat/record/**/api/core/chat/feedback/**/api/core/chat/file/**/api/core/app/logs/**必须覆盖:
appId:按 key 所属 tmbId 权限成功或拒绝。appId:拒绝。apiKey-appId + 缺少业务入参 appId:拒绝。appId:拒绝。appId:允许进入资源鉴权,但必须按 key 所属 tmbId 校验目标 app 权限。appId;必须由 handler schema 解析后显式传入。apiKeyAppId 不再作为 matchAppId 或查询条件兜底。必须覆盖:
appId。appId,也不写入 appId。tmbId 统计。tmbId 的 key。appId 获取其他成员 key。apiKey-appId。authProxy 只能 team owner 开启或关闭。authProxy 拒绝,但仍可更新本人 key 的普通字段。必须覆盖:
appId 的旧 APIKey 仍能用于 v1/v2 completions。body.appId 时使用 legacyAppId。body.appId 时使用 handlerAppId。apiKey-appId 且无 body.appId 时使用 parsedAppId。MongoOpenApi.deleteMany({ appId }) 应改为 unset 匹配记录的 appId 字段。body.appId / apiKey-appId 的 completions 调用应因无法解析 appId 而拒绝。必须覆盖:
authProxy。authProxy。authProxy + body.appId:允许代理团队成员。authProxy + apiKey-appId:允许代理团队成员。authProxy:拒绝代理。authProxy:拒绝代理。authProxy.username 和 authProxy.tmbId 同时传入且指向不同成员:拒绝。tmbId。必须覆盖:
authOpenApiHandler 限额失败时,不更新 lastUsedTime,不进入业务逻辑。必须覆盖:
packages/global/openapi/support/openapi/api.ts 不再把 appId 描述为新建应用 key 的字段。body.appId。apiKey-appId 只出现在兼容说明中。appId 必填。systemopenapi.json 不出现多种 APIKey 类型的旧描述。必须覆盖:
Authorization: Bearer apiKey + body.appId。apiKey-appId。开发中优先跑局部测试:
pnpm test packages/service/test/support/openapi/auth.test.ts
pnpm test projects/app/test/support/openapi
pnpm test projects/app/test/core/chat/completions
如果实际测试文件路径不同,应以最终新增或修改的测试文件为准。完成全部实现后再运行全量测试:
pnpm test