Files
ttstd-infinite-canvas/docs/content/docs/backend/http-api.mdx
ddCat 6892732b06 feat(agent): Agent 工作台 - 计划插话记忆、统一模型选择器并修复摘要与视频入库
将计划清单、运行中插话和个人记忆接入云端 Agent;全站模型选择器统一为左右二级菜单。画布摘要按预算截断不再整轮拒绝,首页视频未知宽高可入库。README 补充贡献者 morrow。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-17 20:21:25 +08:00

123 lines
24 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: HTTP API 合同
description: 信封、错误码与 OpenAPI
---
# HTTP API 合同
路径前缀是 `/api`。前后端一起发布,不按 `/api/v1`、`/api/v2` 做路径版本。不兼容变更随版本一起改接口和调用方。
机器可读规范:运行中的 `GET /api/openapi.yaml`,源文件 `backend/internal/handler/openapi.yaml`。前端 TypeScript 仍走 `web/src/services/api` 的 `http`,不从 OpenAPI 生成客户端。
## 欢迎页开放配置
`GET /public/welcome` 无需登录,信封 `data` 仅包含布尔值 `welcomeEnabled`,响应禁用缓存。读取失败按标准错误信封返回。
管理员通过 `GET/PATCH /admin/settings/features` 读取和保存 `welcomeEnabled`,默认 `true`,与其他功能开关一起存入 `system_settings` 的 `feature_availability` JSON,并记录管理员审计。关闭后新访问 `/welcome` 的浏览器跳转到 `/`。
## 云端 Agent
所有 `/agent` 路径都需要登录 Cookie。`GET /agent/capabilities` 返回当前版本(2)、权限模式、上下文范围、技能和工具清单;`maxHistoryPairs: 10` 表示跨轮对话只把最近 10 轮用户目标带进模型,`maxSteps: 0` 表示不设固定模型调用轮数,历史仍受 `maxHistoryBytes: 64000` 约束(超限先丢掉更早轮次,单轮仍超限才要求新建对话)。Agent 运行由服务端任务 Worker 驱动,浏览器断开不会停止运行。
- `POST /agent/runs` 创建一轮;`POST /agent/runs/:id/messages` 以上一轮为父节点创建新的不可变轮次,两者返回 `{ run }`,追加后必须使用新的 `run.id` 订阅。
- `POST /agent/runs/:id/interjections` 向**正在运行**的轮次插入一句话,不新建轮次、不产生新的扣费身份。请求 `{ text, messageId }`,`text` 最多 4000 字符;同一 `messageId` 幂等。插话在下一次模型调用前注入;运行已结束返回 409,前端应回落为新一轮发送。步数耗尽或运行失败时发出 `user_interjection_dropped`,不会静默吞掉,也不会自动替用户开新一轮。
- 请求字段:`canvasId`、`prompt`、受管 `logicalModelId` 或 `channelId` + `channelModelKey`、`permissionMode: "read_only" | "request_approval" | "auto"`、`reasoningMode: "off" | "auto" | "deep"`、`profileRevision`、`skillIds`、`contextScope: [] | ["canvas"]`、`budget: { maxCredits, maxGenerationTasks, maxVideoSeconds }`、`idempotencyKey`。不接受密钥、自定义 URL、客户端历史或未知字段。
- 同用户同幂等键及相同请求返回同一运行;不同请求复用键返回 409。失败后重试同一提交应保留键。上一轮未完成时禁止追加;已完成、失败、取消或已拒绝的终态轮次作为不可变历史读取,即使其冻结的策略或节点能力版本早于当前服务,也可用于创建采用当前执行合同的新一轮,父运行不会被改写。旧版合同下仍活动或待审批的运行不得恢复执行。历史只保留最近 10 轮用户目标,JSON 最大 64000 字节,超限先丢掉更早轮次,单轮仍超限才提示新建对话;历史结构损坏返回明确 409,不投影为通用 500。
- `POST /agent/runs/:id/approvals/:approvalId/decision` 的 `{ decision, reason? }` 可在批准图片生成时附带 `mediaSettings: { logicalModelId? | channelId? + channelModelKey?, size, quality }`。仅替换本次模型选择、画幅和质量,不允许修改目标、提示词、引用、模式或快照。服务端先无收费预检,失败保留待审批状态;成功原子更新审批调用、哈希及预览,提交任务时再次校验内容、资源归属、模型能力、价格与累计预算。相同参数重复批准幂等,不同参数复用已决定审批返回 409。
- 审批拒绝是独立终态 `rejected`,表示本次操作未执行;不会生成 `tool_failed`、继续调用模型、写入画布、提交生成任务或扣费。拒绝后的草稿按既有草稿策略保留,用户可在下一轮说明修改方向。
- 创建和追加消息可选择 `reasoningMode`,默认 `off`;`auto`/`deep` 为服务端行为策略与模型推理配置的选择,不等于允许暴露内部思维链。供应商能力由后端校验;声明式 Agent 渠道不支持时明确报错。推理可能增加耗时和费用。
- Cloud Agent 与首页创造的规划请求使用自动工具选择。OpenAI Chat Completion 兼容协议会把显式 `auto` 归一化为省略 `tool_choice`,沿用协议默认的自动选择语义;显式开启思考模式时还会在首个上游请求前移除 `required` 或指定函数对象。非思考模式仍保留艺术点评、提示词优化等结构化任务的强制函数选择。若上游隐式开启思考而本地未声明,仍按 `required` → `auto` → 省略字段做兼容重试。
- `GET /agent/profile?scope=user|project|canvas&projectId=...&canvasId=...` 按作用域读取生效的用户 → 项目 → 画布偏好;`PATCH /agent/profile` 用 `{ scope, projectId?, canvasId?, content, revision }` 更新单层(首次 revision 为 0,冲突返回 409)。读取返回各层独立数字 revision,以及合并文档的 `revision`/`hash`;新轮次携带合并 `profileRevision`,发生变化拒绝创建,已运行轮次仍用提交时固定的快照。system prompt 只包含层级、revision、hash 和长度清单,正文保存在运行快照中并由 `agent_profile_read` 按 user → project → canvas 顺序读取,不整本注入系统策略。偏好仅是非授权数据,不能扩展工具、节点或预算,不能绕过审批;最多 12000 字符/层。
- 系统行为策略与媒体默认策略是版本化、编译进后端发布物的资源,修改它们仍需发布后端,不是后台热更新;工具、校验、扣费和审批在代码侧强制执行。画布节点能力由服务端 registry 定义,工具 schema、创建、连线与画布摘要读取共同消费;新生成模式还必须接入媒体任务适配器,不能仅凭 md 或注册节点启用收费操作。
- 已安装且属于当前用户的技能在创建时只保存版本、哈希和文本文件路径,不预存全部正文,也不按所有选中技能正文合计 128KB 拒绝创建。`skill_read_file` 按需读取 `SKILL.md` 或必要引用,支持字符偏移 `offset`(默认 0),每页最多 12000 字符,返回 `hasMore` / `nextOffset`。读取时重新验证用户权限、启用状态和版本;技能更新后拒绝混读,需新开一轮。技能是不可信数据,不能创造工具、改变权限或覆盖系统规则。
- 模型上下文达到 96KB 或超过 24 条消息时,尝试移除较早工具结果中可重新读取的正文,保留工具调用/结果配对、用户原始要求、生成回执及最近完整工具轮次,并允许重新读取。此机制是确定性正文卸载,不是 LLM 语义摘要;不可压缩内容仍受 192KB 请求和 512KB 运行状态保护。技能读取事件只保存回执,不复制正文,正文页只进入模型对话。
- 平台支持工具全集为 `agent_profile_read`、`plan_update`、`ask_user`、`recall_lessons`、`remember_lesson`、`canvas_list_node_types`、`canvas_get_state`、`skill_read_file`、`task_get`、`model_list`、`canvas_apply_ops`、`generate_media`;单轮实际工具仍按偏好快照、技能、权限、画布上下文和生成预算裁剪。画布写入工具要求本轮授权 `contextScope: ["canvas"]`。禁止 shell、任意网络请求和外部 MCP。`remember_lesson` 在 `read_only` 下不可用。
- `plan_update` 维护本轮结构化待办清单(整表替换,最多 20 项),跨轮继承未完成项,并在界面以 PlanBar 展示。权限为 `request_approval` 时,首次建立至少 2 项的清单会先进入审批;`auto` 直接写入清单。新一轮的第一次模型请求就会带上继承的清单。模型在没有工具调用且清单仍有未完成项时,服务端会催办并要求继续调用工具;这是软约束,不验证工作是否真的完成,也不会在没有清单时强行催建。
- `ask_user` 给出一个问题与 2–6 个候选项后结束本轮,界面弹出选项面板;用户点选或输入后作为新一轮继续。运行中也可以用插话直接回答。
- `recall_lessons` / `remember_lesson` 是当前用户的个人记忆:已批准条目在系统提示里只出现索引(标题 + 适用场景,最多 20 条,与当前目标可能相关的排在前面),完整做法由模型用 `recall_lessons(topic/keyword/category)` 取;Agent 写入先入待审,用户在 `GET/POST/PATCH/DELETE /agent/memories` 与 `POST /agent/memories/:id/decide` 自己批准、添加、编辑、删除,也可用 `GET /agent/memories/export`、`POST /agent/memories/import` 导出导入,并用 `GET/PATCH /agent/memories/settings`、`POST /agent/memories/compact` 选择文本模型后手动或按日/周/月压缩。管理员只在 `GET/DELETE /admin/agent-lessons` 按 `userId` / `keyword` / `status` 查看或删除,不能代批。写入会机械脱敏(拒绝链接、资源键、节点/任务 ID;@引用允许),只读模式不能写,本轮最多 3 条、每位用户最多 50 条待审、300 条已批准,一次导入最多 200 条。召回接口不返回作者。记忆只作参考,不能覆盖工具契约、权限、计费或审批。
- `canvas_list_node_types` 从服务端节点能力 registry 返回当前可用类型;可更新节点的 `updateFields` 同时包含字段类型、最大字符数和字段语义。模型不能从前端渲染器、技能或偏好文档推断额外能力。
- `canvas_apply_ops` 必须带 `snapshotHash`,每批最多 20 项;支持新增文本、Markdown、图片、视频、音频、分组与分镜表节点,以及 `connect_nodes` 引用连线。媒体占位节点的 `content` 是提示词,不是媒体 URL;只建节点不提交收费生成。`update_node` 的字段映射完全由能力 registry 决定:文本 / Markdown 可改标题和正文,图片 / 视频 / 音频可改标题和下一次生成使用的提示词草稿;媒体更新参数中的 `content` 写入 `metadata.composerContent`,不会覆盖媒体结果 `metadata.content`、已提交提示词 `metadata.prompt`、任务状态或引用状态。锁定节点、删除、任意元数据写入、自连接、重复边与不存在的端点仍被服务端拒绝。
- `model_list` 复用生成面板的当前模型目录。生成前传 `mode` 和实际 `referenceNodeIds`(无参考传空数组),服务端读取当前用户画布内的就绪资源,按真实素材类型、数量及生成操作构建意图并调用统一能力匹配,不让多图生视频落入仅文生视频候选;返回 `intent` 供核对。空参数仍可浏览完整目录;空候选不降级到不匹配模型。每项的 `selection` 是 `logicalModelId` 或 `channelId` + `channelModelKey`,工具调用必须原样选择一种,不猜 ID 或混用;时长、画幅、音频、价格仍需核对返回配置,任务准入再次强校验。
- `generate_media` 支持图片 / 视频 / 音频:必填 `mode`、`prompt`、`snapshotHash`、`nodeId`(可续用草稿,无草稿时才新建)、`title`、有序 `referenceNodeIds`(无参考传空数组),再加一种模型选择;图片 / 视频要求 `size`,视频要求正数 `durationSeconds`,图片 / 音频时长为 0。可传来源节点 `sourceNodeId`、`quality`、视频 `videoGenerateAudio`。`sourceNodeId` 仅接文本 / 镜头提示词节点;参考图片、视频、音频只放 `referenceNodeIds`,无文本来源时留空且不重复传入。引用从当前画布节点的账号资源解析,校验归属、类型、就绪状态和模型能力,不接受任意 URL。图片生成仅接图片参考;视频可按模型能力接图片、视频、音频参考;音频生成当前仅支持文本输入。
- 参数选择与收费授权分开:用户明确说“随便 / 你决定 / 按默认”时,Agent 可从当前目录选择能力及预算匹配的模型,优先用户最近明确使用的可用模型,其次价格清楚的低成本选项;支持时沿用参考图原始比例,否则选目录默认,时长用目录默认。未要求音频且模型不支持时用默片。用户接受已推荐方案后不再要求特定确认口令;仅真正缺失或冲突的阻塞项一次性询问。本轮已有目录结果应复用。以上是 Agent 的参数选择策略,服务端仍强校验具体规格,不提供绕过预算 / 能力校验的默认兜底,也不绕过独立收费审批。
- 媒体参数先进行无任务、无预留账单的准入校验,再在同一事务创建草稿节点、来源 / 资产连线及待审批检查点;`approval_requested` 和运行中的 `approval` 携带目录真实 `modelName`。用户批准后才在事务中绑定任务、创建预留账单并更新检查点;拒绝或取消保留未生成草稿,不提交媒体任务。`nodeId` 可续用同类型、未锁定、idle、无任务且无媒体结果的空白占位节点或当前运行草稿;跨轮 Agent 草稿还要求原运行属于同一用户和画布、已结束且清理完成。续用必须重新读取快照并重新审批,不能复用旧审批、覆盖已有任务或成品,不能通过循环新建节点绕过限制。画布读取的 `generationDraft` 提示未提交、原运行状态、是否仍在清理及仍需审批;写入仍重新强校验。已明确审批的规格不得被模型默认值静默替换。媒体终态回写原节点;已删除或已换任务的节点不自动复活,结果仍可从任务中心查看。
- Agent 的 `snapshotHash` 忽略顶层 `viewport`、`updatedAt`。`canvas_get_state` 另返回 `mediaSnapshotHash`,供 `generate_media.snapshotHash` 使用,并额外忽略节点 `position`;创建生成草稿后的审批也固定该内容哈希。因此移动图片或草稿不会使生成审批失效,执行时保留最新坐标;节点内容、素材、锁定状态和连线变化仍需重新读取并审批。画布修改与撤销继续使用包含位置的 `snapshotHash`,数据库写入仍使用完整快照 CAS。客户端刷新保留本地视口。
- `read_only` 只允许读取;`request_approval` 对写画布和媒体生成暂停为 `waiting_approval`;`auto` 可在预算内执行已授权画布操作,但图片 / 视频生成始终独立审批,不能绕过。`maxGenerationTasks` / `maxVideoSeconds` 为非负整数,0 表示该项不设业务上限,正数按本轮累计执行;不再固定 8 次模型调用、8 个生成任务或 120 秒视频。积分上限仍必填且大于 0,实际报价在任务创建与预留积分前校验,时长必须符合所选模型的实际能力。失败事件保留具体错误和已有任务的 `taskId`、`nodeId`、`status`;媒体结果的 `phase` 区分 `admission` 与 `completion`,`taskSubmitted` 表达是否已提交。模型应报告错误,不能自行删减提示词或自动重试收费生成。
- 必须有当前用户已保存的画布。首轮摘要最多取前 80 个节点,并按约 60KB 预算截断;装不下的节点记入 `omittedNodes`,不因摘要过大整轮拒绝。`canvas_get_state` 可按 `offset` 每页读取 40 个节点、资产可引用状态及相关连线,按 `nodeIds` 精读最多 8 个节点(正文每字段最多 16000 字符),通过 `hasMore` / `nextOffset` 和字段 `Truncated` 判断是否完整。分镜表包含镜头、角色节点与资产绑定;摘要每页最多 20 行,精读每次 1 行,可用 `storyboardOffset` 翻页;单行叙事文本累计最多 24000 字符,角色 / 绑定各最多 16 项。上下文不含媒体 URL、任意元数据或未同步修改;节点名称不等于图像视觉识别。普通任务重试接口不能重试 Agent 任务。
- `GET /agent/runs/:id` 返回运行信息;`POST /agent/runs/:id/cancel` 取消当前运行及活动子任务;`POST /agent/runs/:id/approvals/:approvalId/decision` 接受 `{"decision":"approve"|"reject","reason":"..."}`,审批决定幂等且服务端校验归属。
- `GET /agent/runs/:id/events` 返回 `agent_event` 和首次 / 状态版本变化时的紧凑 `run_snapshot` SSE;无变化时只发送 heartbeat。事件 `Seq` 在运行内递增;支持 `?after=` / `Last-Event-ID`,游标不是运行 ID。`cleanupPending: true` 时即使运行已进入终态也会继续推送,直到后台收尾完成。前端应以快照恢复状态,代理只对这一精确路径关闭缓冲。
- `generation_task_created` 携带 `canvasId`、`nodeId`、`taskId`、`title`、`mode`、`referenceNodeIds`。草稿、提交、画布工具写入,以及媒体成功 / 失败 / 取消的真实节点变更,均在画布写入与运行检查点的同一事务持久化 `canvas_updated`。节点已删除或换任务时不伪造更新;`run_failed` 本身不代表节点媒体任务失败。
- 未注册 Agent 能力的节点不阻断整张画布读取:摘要和 `canvas_get_state` 仅返回基础信息,并标记 `agentSupported: false` 与原因;不暴露任意 metadata,不授予该类型的创建、修改、连线或媒体引用能力。
- `approval.preview` 和 `canvas_updated.actions` 是服务端根据同一解析、快照与 Capability 规划流程生成的解释信息,不是授权凭证。审批卡片明确列出新增/修改节点、节点类型、修改字段、名称变化和连线两端;执行事件保存同样的可读信息,供历史消息显示和点击定位。实际写入仍重新校验用户归属、`snapshotHash`、Capability、节点锁、预算和审批 `callHash`。
- `canvas_updated` 包含 `canvasId`、`operation`、`callId`、`actions`、可选的 `preview`/`text` 和 `canvasPatch`。`canvasPatch` 为 `{ canvasId, updatedAt, nodes: [{ before, after }], connections: [{ before, after }] }`;新增对象的 `before` 为 `null`,仅包含有变化的节点和连线。当前工具不开放删除。节点 action 保存操作、节点 ID、执行前标题、类型、修改字段和可选的修改后标题;引用 action 同时保存来源与目标节点 ID、标题和类型,不要求前端从内部 ID 猜测对象。`callId` 用于合并同一画布操作的工具完成消息。
- 前端对增量按 40ms 窗口合批,三方合并远端基线、当前 Store 和编辑器,正常完成 / 失败无需全量 GET;保留无关节点编辑、拖动与视口,同字段冲突、删除或新任务绑定不强行覆盖。只有事件缺口、断流、增量冲突或没有增量的事件源需要全量校准,同一画布协调器最多每秒发起一次,单请求在途,期间重复通知合并为一次尾随校准。已识别增量协议后,相邻 `tool_failed` / `run_failed` 不重复触发整图刷新。
- Agent 创建 / 续聊请求在客户端先持久化完整正文、幂等键、父运行和消息 ID,再提交;网络超时或响应丢失只允许用同一正文和 key 有界重放,不能换 key 生成新收费轮次。客户端区分“服务端已明确返回错误”和“未收到服务端确认”,不再把 HTTP 409/500 一律描述成请求未确认。待确认记录损坏时对话暂停发送,不把它当成“从未提交”。
- 生成按钮与提交入口共用节点任务状态:排队 / 运行中不可重复生成,终态解除任务锁,历史 `taskId` 不永久禁用。聊天留痕保留节点名、引用与失败任务 ID,点击使用现有画布聚焦;已删除节点只提示,不自动重建。聊天缓存不重复保存增量中的完整节点正文。
Agent 的聊天列表缓存只负责 UI 恢复;运行、技能快照、审批、工具调用、子任务和事件均以服务端持久化状态为准。提交响应丢失时,客户端应使用同一幂等键重试,不生成新的提交。
Skills 的 `tool_completed` / `tool_failed` 事件附带 `skillId`、`skillName`(已启用技能)和 `path`;空路径表示列目录,不表示已读取文件。目录结果中的 `entryPath` 固定为 `SKILL.md`,`files` 为本轮固定快照的可读路径;Agent 先读取入口,再按入口引用读取必要文件。同一运行内同技能同路径只允许请求一次(包括不存在的路径),重复请求返回工具失败和继续执行指引,状态随运行持久化。模型初始上下文只包含技能版本、哈希、入口和可读文件清单,不包含技能正文。
模型任务失败的 `run_failed` 事件保留 `taskId`,并提供安全分类 `reason`:`model_connection_reset`、`model_request_timeout`、`model_connection_refused` 或 `model_task_failed`。`text` 不透传原始上游错误,避免泄露地址和凭证;前端用事件类型而非摘要关键词判断工具成功 / 失败。失败不会触发自动付费重试。
## 管理员支付订单
`GET /api/admin/payments/orders` 需要管理员权限,支持 `status`、`keyword`、`page`、`pageSize`。关键词匹配订单号、渠道交易号、用户名称、用户名或邮箱,也支持完整用户 ID 精确匹配。用户名和邮箱搜索忽略大小写;状态过滤同时生效,返回匹配总数和当前页订单。
每条管理员列表订单在原有字段之外包含 `user: { id, username, displayName, email }`,由当前页用户 ID 批量读取;用户不存在时返回 `user: null`,保留原 `userId` 与历史订单。此身份摘要仅用于管理员订单列表,不添加到普通用户支付接口或支付状态同步响应。
后台支付订单列表展示用户身份,点击名称打开用户详情;订单详情提供完整订单号、渠道交易号、用户 ID 和各阶段时间。同步支付状态与关闭订单位于更多菜单,关闭订单仍需确认。
## 用户 Agent 记忆
`GET/POST /api/agent/memories`、`PATCH/DELETE /api/agent/memories/:id`、`POST /api/agent/memories/:id/decide`、`GET /api/agent/memories/export`、`POST /api/agent/memories/import`、`GET/PATCH /api/agent/memories/settings`、`POST /api/agent/memories/compact` 需要登录。记忆按 `author_user_id` 隔离;手动添加直接批准,Agent `remember_lesson` 写入待审,由本人 `decide`。导出 `kind` 为 `agent-memories`。压缩只处理该用户已批准记忆,创建 `canvas_text` 任务 `operation=agent_memory_compact`;worker 仅在 `agent_memory_settings.compact_task_id` 与任务 ID 一致时应用 JSON 计划(merge 保留第一条并删除其余,rewrite 原地更新)。周期为 `off` / `daily` / `weekly` / `monthly`,调度每 5 分钟扫描,每拍最多 3 个用户。
## 管理员查看 Agent 记忆
`GET /api/admin/agent-lessons`、`DELETE /api/admin/agent-lessons/:id` 需要管理员权限。列表支持 `status`、`userId`、`keyword`(主题/内容/用户名)与 `limit`;条目含解析后的 `steps`、`authorUserId`、`authorUsername`、`authorDisplayName`。管理员不能批准或拒绝,批准入口在用户设置页。
## 管理员系统渠道
`POST /api/admin/channels/:id/duplicate` 需要管理员权限,会复制指定系统渠道的连接配置、加密凭证、请求头、模型和价格档,生成新的渠道与模型 ID;复制渠道名称自动追加“ - 副本”。原渠道及其前台供应线路不受影响。
## 信封
成功:
```json
{ "code": 0, "data": {}, "msg": "ok" }
```
失败:HTTP status 与 `code` 都不是成功;`reason` 是稳定机器可读原因。
```json
{ "code": 40301, "data": null, "msg": "账号素材数量已达到 100 个上限", "reason": "quota_exceeded" }
```
`code === 0` 才是业务成功。HTTP 200 但 `code !== 0` 必须当失败。前端判断类型用 `ApiError.reason` / `ApiError.code`,不要解析 `msg`。
## 错误码
`code` 默认等于 HTTP 状态。需要区分同一状态的不同原因时,使用 `status * 100 + n`。没有单独的 1001/2001 号段,以免和 HTTP 状态、现有 42901 以及重试判断打架。
| code | HTTP | reason | 含义 |
| --- | --- | --- | --- |
| 0 | 200 | — | 成功 |
| 400 | 400 | `invalid_argument` | 请求不合法 |
| 401 | 401 | `unauthorized` | 未登录 |
| 403 | 403 | `forbidden` | 无权限 |
| 404 | 404 | `not_found` | 资源不存在 |
| 409 | 409 | `conflict` | 冲突 |
| 429 | 429 | `rate_limited` | 限流(未细分) |
| 500 | 500 | `internal` | 内部错误 |
| 502 | 502 | `bad_gateway` | 上游失败 |
| 503 | 503 | `unavailable` | 暂时不可用 |
| 504 | 504 | `timeout` | 超时 |
| 40301 | 403 | `quota_exceeded` | 账号配额用尽(不自动重试) |
| 40901 | 409 | `conflict` | 幂等冲突 |
| 42901 | 429 | `rate_limited` | 请求限流(可带 Retry-After) |
模型错误把已有字符串码放进 `reason`(如 `model_capability_not_supported`),`code` 仍为 400。
常量:后端 `service/error_codes.go`,前端 `web/src/services/api/error-codes.ts`。