Files
ttstd-infinite-canvas/docs/content/docs/backend/http-api.mdx
2026-10-13 05:31:30 +08:00

463 lines
89 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 合同
## 用户登录日志
菜单“用户与运营 → 用户登录日志”,页面 `/admin/login-logs`。查询在 service 校验管理员权限:匿名 401、普通用户 403。
- `GET /api/admin/login-logs`:支持 `keyword`(用户名、昵称、脱敏账号或用户 ID)、`event=login|logout`、`method=password|email|sms|linuxdo|verification|unknown`、`result=success|failed`、精确 `ip`、RFC3339 `from/to`(含边界)、`page/pageSize`。开始时间不能晚于结束时间。默认每页 20 条、时间倒序;成功 `data={logs,total,page,pageSize}`,空结果 `logs=[]`。
- `GET /api/admin/login-logs/:id`:成功 `data={log}`;不存在返回 404。包含用户快照、时间、事件、来源、方式、IP、UA、安全原因码/文案和非凭据的关联会话标识。
后端在密码、邮箱/短信验证码、Linux.do 登录、注册后自动登录及主动退出的真实认证路径记录,不提供客户端任意新增日志接口。`verification` 表示凭据/请求无效导致方式无法识别;`unknown` 表示上线前会话未保存方式。`source=registration` 标记注册自动登录。退出失败不清 Cookie,不再忽略撤销错误。刷新状态和会话自然过期不新增事件。浏览器/系统由页面根据 UA 展示,无法识别时明确标注;UA 是客户端自报信息。
## 个人信息
所有 `/auth/profile` 路径均要求当前活动用户的登录 Cookie,不能通过用户 ID 修改他人资料。
| 路径 | 入参与结果 |
| --- | --- |
| `GET /auth/profile` | 返回 `user`、`hasPassword`、`emailEnabled`、`linuxdoEnabled` 和可选 `reauthenticatedUntil`;密码散列、头像图片正文、内部缓存版本均不在用户 JSON 中 |
| `PUT /auth/profile/avatar` | multipart `file`,只接受真实 PNG/JPEG、最大 5 MiB;返回 `{ user }`,包含带版本的头像地址 |
| `DELETE /auth/profile/avatar` | 清除个人头像并明确选择系统默认头像;第三方重新登录不会覆盖该选择;返回 `{ user }` |
| `POST /auth/profile/avatar/default` | `{ preset }`,只接受 `person/director/camera/cat/rabbit/robot/rocket/sparkles`;保存选择并清除个人图片,返回 `{ user }` |
| `GET /auth/profile/avatar/:userId` | 只读取本账号的 PNG,`Cache-Control: private, no-store`;账号之间拒绝读取 |
| `POST /auth/profile/password` | `{ currentPassword, newPassword }`;新密码至少 8 个 Unicode 字符、UTF-8 不超过 72 字节;返回 `{ changed: true }` 并清除 Cookie、撤销全部会话 |
| `POST /auth/profile/email-code` | `{ email, currentPassword }`;向未占用的新邮箱发送验证码,返回 `{ ticket, expiresIn, retryAfter }` |
| `POST /auth/profile/email` | `{ ticket, emailCode, currentPassword }`;验证后更新邮箱及验证时间、撤销全部会话并清除 Cookie;返回 `{ changed: true, notificationStatus }` |
| `GET /auth/profile/linuxdo/start` | 纯第三方账号重新核验身份的入口;绑定原用户和原会话,成功回到 `/settings?section=profile&reauth=success`,失败回到该页的 `reauth=error` 状态 |
已有本地密码的账号必须验证当前密码。纯 Linux.do 账号设置密码或更换邮箱时,必须先完成同一会话内的重新授权,核验必须匹配原第三方用户标识;有效期为 5 分钟。邮箱验证码用途固定为 `profile_email`,绑定当前会话与凭据状态,6 位数字、10 分钟有效、最多 5 次核验;收件地址跨场景共享 60 秒发送冷却、每小时 5 次和每天 20 次额度。发送失败明确报错;邮件服务未启用时禁止更换邮箱。
新邮箱验证之前保留原绑定。更新及验证码消费、登录会话撤销一起提交,唯一性检查在事务内重复执行。更新后向原邮箱发通知,`notificationStatus` 为 `sent`、`failed` 或 `not_needed`;`failed` 表示邮箱已更新、原邮箱通知失败,不能把已成功更新显示成未保存。接口使用现有业务错误信封和限流规则,密码、验证码和 ticket 不进入 URL。
上传头像先通过共享裁剪弹窗选择 1:1 区域,支持拖动、缩放裁剪框、键盘调整与重置;确认后导出最大 512×512 的 PNG 到待保存预览,点击“保存头像”才上传。取消裁剪保留原头像;待保存图片可重新调整裁剪。选择系统头像使用现有 AppModal、Avatar 与主题 token,支持 8 个预设,确认后才写入服务端。
## Agent 画布权限与大事件
自由画布和项目关联画布均使用 `canvas_projects`。Agent 运行时按登录账号和画布 ID 查询归属,不能把所属项目 ID 当作画布 ID;画布不存在、属于其他账号或数据库读取失败时,不声明读写及导出权限。
`canvas_updated` 的画布增量超过单事件 128 KiB 上限时,复用 `requiresRefresh: true` 通知客户端重新读取完整画布,优先保留操作摘要;摘要仍超限则仅发送刷新通知。画布正文、历史和操作回执仍完整保存,普通大小事件继续携带 `canvasPatch`。不提高事件上限、不截断正文、不修改审批暂停或模型调度判断。
## Agent 模型输出限制
Pi 模型桥在保存待执行调用之前,复用单步输出限制校验:一次回复最多 8 个工具调用,正文最多 32000 字节。超限时整批未执行,释放本步模型任务并附带 `invalid_output` 及具体上限要求模型分批纠正;与既有异常重试共用首次失败后最多 3 次的上限,不截取部分调用执行。纠正请求仍是正常计费的文本模型任务,不创建媒体任务。
系统行为策略版本升至 14,明确工具调用数量与单个 `canvas_apply_ops` 操作数量的区别。已有会话保留其冻结策略快照;部署后用新会话验证新提示词,不修改历史检查点。
## Agent 分镜连线参数反馈
素材绑定使用“素材→分镜”和 `toHandleId: row:<rowId>`;镜头输出使用“分镜→图片/视频”和 `fromHandleId: row:<rowId>`。`rowId` 必须是读取工具返回的真实行 ID,不能用镜头编号代替。普通节点连线与镜头行连线按 handle 区分,已有整表连线不代表该行已经绑定。
handle 放错端点、格式错误或行 ID 不存在时,工具错误回执提供具体 `field`(如 `ops[0].fromHandleId`)、`issue`、方向说明及连线参数模板,不再使用无关的新增节点示例。模板不会被自动执行,不自动交换端点或修改原调用。权限、快照和重复连线校验保持不变;工具契约版本为 v4,重启后端后新会话使用更新后的说明。
## KM 图片尺寸能力
图片能力 `size.presets[].tier` 在已有档位之外接受 `1.5k`;前端档位选择、请求归一化与服务端预设校验保持一致。`km-kemei-seedream` 默认 `2048x2048`、单次一张、最多十张参考图,提供二十四个精确尺寸。透明输出使用插件扩展字段,不开启宿主通用透明背景开关。模型能力仍保存在现有 JSON 字段,无数据库迁移或新端点。
## 注册邮箱等价检查
公开注册与邮箱绑定的唯一性检查将 `gmail.com` 和 `googlemail.com` 视为同一域,移除本地部分的点及 `+` 后缀。例如 `a.b+x@googlemail.com` 与 `ab@gmail.com` 不允许创建两个账号。其他域保留点和 `+`;现有域名白名单规则保持不变。
邮箱原值仍按原有小写、去首尾空格规则保存。登录、密码重置、验证码收件地址保持精确邮箱匹配,避免既有重复别名账号被合并或错误授权。已存在的重复账号不迁移、不删除、不重新发放注册奖励;它们会阻止相同规范信箱的新注册。写入前在事务内重新检查,以覆盖不同实例使用不同验证码的并发注册。无数据库结构迁移;唯一性由公开注册与绑定的事务入口保证,直接 SQL 写入不属于该约束入口。
## 分片上传配额
`POST /resources/uploads` 在接收分片前预留整个文件的日额度和账号容量。文件大小必须严格小于 `resourceUploadMB`;同一账号最多 32 个未到期会话,跨后端实例共同计数。未满足配额的请求不会创建分片目录。分片和完成请求按会话串行执行;会话过期或已完成返回 404,重新创建会话后可整传重试。
完成上传时以数据库事务提交容量并消费预留,使用既有资源幂等标识。失败释放预留;跨 UTC 日期完成需要通过当天日额度检查。请求与响应字段不变。
## AutoDL 视频画面规格配置
渠道模型的 `capabilityConfig.video.resolutions` 与 `defaultResolution` 直接表示所选完整分辨率与默认值,始终生效;不再使用或返回 `customSizeEnabled`、`customScreenSpec`。AutoDL v2.3.0 未声明独立比例参数,因此 `ratios` 为空、`defaultRatio` 为空字符串,横竖与方形由完整分辨率标签表达。
服务端返回 `fixedScreenSpec` 供前端展示插件预设;写入时重新读取官方插件的当前工作流规格,不信任客户端传入的固定值。所选列表必须非空且属于插件预设,默认值必须属于所选列表;大小写归一化为插件原始标签。任务创建在价格选择前清空无效的 `size`、归一化 `vquality`;自动选择使用配置的默认值,不支持的显式选择被拒绝。价格档可保留全部插件预设,但任务和模型目录只采用所选列表。数据库继续使用现有能力 JSON 字段,无新增表或迁移。
路径前缀是 `/api`。前后端一起发布,不按 `/api/v1`、`/api/v2` 做路径版本。不兼容变更随版本一起改接口和调用方。
机器可读规范:运行中的 `GET /api/openapi.yaml`,源文件 `backend/internal/handler/openapi.yaml`。前端 TypeScript 仍走 `web/src/services/api` 的 `http`,不从 OpenAPI 生成客户端。
出站模型服务域名解析失败返回 HTTP / `code: 502`、`reason: upstream_dns_failed`,不归类为用户参数错误。公开消息不包含上游域名、DNS 服务器或请求 URL;原始解析错误仅保留在内部错误链。此分类不会自动重试、切换渠道或放宽 SSRF 校验;上下文取消仍按取消处理。
模型 HTTP 失败及经过 Provider 错误转换的业务失败,在通用提示后追加经过过滤的 `;上游:具体原因`,供任务错误和画布卡片展示内容安全、提示词、尺寸等可操作信息。只读取上游 `error` / `message` / `msg` / `detail` 消息字段,不回传整个 JSON;消息中的 HTTP(S) URL 隐藏,包含余额、账务、凭据、请求诊断或 HTML 的消息不追加原文,保留通用分类提示。后台响应拦截规则仍对最终用户文案生效;不会修改历史任务错误或改变重试、退款策略。
## 资源访问与 OSS/CDN 分发
普通上传、分片合并及 URL 导入按内容签名识别 MIME,不以客户端声明或文件扩展名决定媒体类型;无法识别的内容仍可作为附件上传。上传标识对应的 ready/failed 重试均先读取内容,并核对检测后的 MIME、资源类型与大小;不一致返回 409,不覆盖原对象。这是内容判型与元数据一致性校验,不是完整格式解码或内容摘要去重。
非媒体文件(包括可执行脚本的 SVG)通过平台本地读取或代理交付,强制 `Content-Disposition: attachment` 与 `Content-Security-Policy: sandbox; default-src 'none'`,不向浏览器或访问描述返回其 OSS/CDN 直链。普通图片、音频及视频保持原有媒体分发规则。历史资源不在本次变更中批量重新判型。
资源展示、播放、下载、复制地址和模型输入统一消费资源访问合同,不在客户端通过 `direct=1`、`proxy=1` 或存储厂商规则选择出口。
- `POST /api/resources/access` 要求登录,请求体为 1–100 项 `{ resourceId, purpose?, variant? }`。`purpose` 支持 `display`、`copy`、`download`、`browser-process`、`provider-input`;`variant` 支持 `original`、`playback`、`thumbnail`(缩略图未生成前回落原件,`fallbackReason` 为 `thumbnail_not_ready`)。每项独立返回 `access` 或结构化 `error`。
- `access.delivery` 为 `cdn`、`origin`、`platform-local` 或 `platform-proxy`。媒体配置了可用 CDN 时返回 CDN 地址;OSS 源站签名是源站直连,不等于 CDN。媒体的 `requireCDN` 开启且 CDN 鉴权不完整时失败,不静默回源。浏览器媒体的展示、复制、下载和本地处理不使用 `platform-proxy`,正文由 OSS/CDN 直接发送;`allowPrivateProxy` 允许服务端在 `provider-input` 场景读取私有媒体源站。非媒体附件始终通过平台控制响应头,不受上述媒体直连规则影响。
- `expiresAt` / `refreshAt` 只描述当前授权有效期。有效期来自后台「资源与请求策略」(`provider-input` 与其余签名访问分别配置,默认均为 4 小时);公开 CDN 可没有 `expiresAt`。签名地址不写入画布或服务端持久化内容。
- `display`、`copy`、`browser-process` 的源站签名把签名起始时间对齐到有效期 1/4 的时间窗(按 Unix 纪元对齐),过期时间为窗口起点加有效期:同一窗口内重复签发得到逐字节相同的 URL,刷新画布或多实例签发都能命中浏览器 HTTP 缓存;签名同时携带 `response-cache-control=private, max-age=<有效期>, immutable`,由对象存储回写缓存头。`download` 与 `provider-input` 仍按当前时间签发,不带缓存覆盖。
- `GET /api/resources/:id/file` 仍是稳定平台入口,负责本地/平台代理字节流,可直连的云媒体返回 307 到 CDN 或对象存储,非媒体附件走平台流;`?access=1` 只返回访问描述,不打开正文。`Range` 只影响字节流路径。
- 公共资源、画布分享、外观、公告、管理员资源和日志媒体复用同一个 HTTP 执行器。公共签名链接受自身到期时间和变体绑定限制,但已签发的外部 URL 不承诺立即撤销。
当前自动签名能力:公开 CDN、七牛私有下载签名和对象存储源站签名。阿里云/腾讯云私有 CDN 签名尚未自动支持,不能仅填写 CDN 域名就宣称已命中 CDN。真实 CORS、Range、CDN 命中率及账单收益必须在实际云环境通过浏览器网络记录和服务端流量观测验收。
## AI 拆图视觉规划输入
文本生成任务的 `metadata.edit = "layer-planning"` 使用图片参考时,服务端从当前用户归属的就绪资源读取实际像素并转换为内嵌图片,清除附带的旧 URL。图片数量与字节数沿用文本模型能力及资源读取上限;不允许其他用户资源或客户端内嵌内容替换资源文件。普通生成的签名 URL 策略保持不变,任务端点、账务与数据库结构沿用已有合同。
## 画布资源完整性与 operations 写入
画布 operations 应用前校验当前文档中的资源引用;引用须属于当前用户且处于 `ready`。引用收集器递归遍历受支持的资源字段,因此 `nodes[节点 ID].metadata.videoPreview.storageKey` 等嵌套派生封面也会参与检查。维护写入会保留必要的损坏文档审计信息;若资源在事务期间缺失或状态变化,事务回滚,画布 revision 不递增。
HTTP 409 不能一概解释为并发冲突:对象或字段基线冲突使用 `reason: conflict`;资源完整性失败使用 `reason: canvas_history_resources_missing`,并在 `details` 中返回 `resourceIds` 和 `missingResources`(含资源 ID、JSON 路径、节点 ID、引用类型及 `source`)。客户端应按 `reason` 分流,不解析 `msg` 猜测原因。普通 operations 不会绕过资源校验或 revision 语义,维护修复也不能把缺失主媒体写回为有效画布。
运维扫描/修复仅处理当前画布中可安全清除的失效 `videoPreview`;不删除主媒体、不扫描并改写所有维护记录,也不替代备份。使用目标数据库对应的新版本二进制,先在维护窗口备份并执行只读扫描:
```sh
CANVAS_BACKEND_DATA_DIR=/明确的数据目录 ./migrate-schema repair-canvas-resources
```
复核 JSON 报告后,只有 `repairable` 项可由明确授权的操作员再次运行 `repair-canvas-resources --apply`。命令要求显式配置 `CANVAS_BACKEND_DATA_DIR` 或 `DATABASE_URL`,逐画布按当前 revision CAS 保存并保留维护审计记录;有未解决主媒体/素材绑定问题时以非零退出,不会自动重传或重建节点。发生并发编辑时停止并重新扫描,不能用整批快照替换或绕过 operations 校验。
## 生成成功后的重试保存
`POST /tasks/:id/recover-media` 无需请求体。当前用户只能恢复自己有保存恢复记录的失败任务;重入队仍返回原任务 ID,不请求重新生成、不新建计费订单。已排队、运行或完成时幂等返回当前任务。取消任务、已退款订单、任务/账单归属不一致、项目已删除或归档时拒绝恢复。
任务详情和摘要新增 `mediaStage`、`canRecoverMedia`。后者表示失败任务存在保存恢复记录,实际操作仍校验权限、账单、项目及重试额度。每任务最多手动恢复三次,两次之间至少一分钟,每次手动操作只执行一次保存尝试;接口另受用户维度任务请求限流约束。普通 `/tasks/:id/retry` 不接受带保存恢复记录的任务,避免误走重新生成。
保存网络故障在原 worker 中最多自动重试四次(15 秒、1 分钟、3 分钟、10 分钟并加抖动;超过 20 分钟不再安排自动重试),使用 `next_poll_at` 释放 worker,而不是在请求内睡眠。不可重试错误直接保留为保存失败。画布和任务中心显示“作品已生成,正在保存”或“作品保存未完成”,失败按钮为“重试保存”。完成后客户端消费原任务的结果。
请求明细保留上游生成与结果下载的独立状态,新增 `upload`、`local_save`、`register` 内部非计费明细;列表和详情通过 `mediaStage` 展示交付摘要。`recordType=request` 排除这些保存明细和下载,`all` 查看全部阶段,`download` 仍仅查看下载。视频下载失败不会再覆盖生成请求的成功记录。
此恢复能力接入声明式媒体协议返回 URL/data URL 的任务路径;旧协议自行下载、协议二进制下载端点以及无恢复记录的历史失败任务不自动补录。历史任务需先核实上游结果可用性、资源归属及原账单,再单独制定修复动作。
## 素材彻底删除
`DELETE /assets/:id` 表示用户确认后的彻底删除,不依赖服务端当前记录是否已经同步为 `archived`。接口仍校验登录用户和素材归属,但不会因画布、任务、项目或历史快照仍有引用而拒绝;事务会删除素材及对应历史资源索引,并通过删除 Outbox 清理未被其他素材复用的物理对象。其他素材共享的同一物理对象继续保留。移入回收站仍通过素材写入接口把状态更新为 `archived`,不能用 DELETE 代替可恢复归档。
素材页及素材选择器的批量彻底删除、清空回收站统一调用 `POST /assets/batch-delete`,请求体直接为 JSON 数组(不是 `{ ids: [...] }`):
```json
["asset-1", "asset-2"]
```
- 每次 1–1000 项,单个请求体最多 256 KiB;ID 去除首尾空白并去重,空 ID / 非数组返回 400,任一 ID 不存在或不属于当前用户返回 404,整批不删除。
- 成功信封 `data.ids` 为请求传入的数组。前端等待服务端成功后一次更新本地集合并冲刷持久化;失败不回退为逐条 DELETE,不先移除本地记录。
- 整批素材、版本、表现、业务关联、待删资源及对应历史资源索引,与物理删除 Outbox 在同一数据库事务中提交,数据库失败整批回滚。共享检查排除整批被删素材,避免两条互相共享的选中素材残留文件。
- 物理空间由后台删除队列异步释放,失败保留任务重试;接口成功不代表对象存储已经清理完成。未选中素材使用的资源,以及其他 Resource 行(包括其他用户)复用的物理对象继续保留。
- 旧画布 / 任务 / 历史快照记录本身保留,但被删媒体链接将失效。此操作不可恢复;没有数据库结构迁移。
## 密码登录限流
`POST /auth/login` 保留 IP 总请求限制与十分钟账号请求限制。同一 IP 下,用户名和邮箱解析为同一用户时共享账号计数;不存在的账号仍按去除首尾空白、转小写后的输入计次。错误密码与不存在的账号使用相同的 401 响应,达到限额后均返回 429,不返回限流键或用户身份。计数仍涵盖所有登录请求,成功不清零。
## 注册服务协议
`POST /auth/register` 的 JSON 请求必须携带布尔值 `acceptedTerms: true`,首次创建管理员也适用。缺省、`false` 或 `null` 均按未同意处理,在创建用户、消费验证码和创建会话前返回 HTTP 400,提示“请先同意<协议名称>”。原有注册开放、邮箱白名单、验证码与限流校验继续生效。
`GET /auth/linuxdo/start` 可携带 `acceptedTerms=true`,只有该精确值表示同意。服务端将标志写入一次性的 OAuth state;回调仅在新建 Linux.do 用户时要求该 state 已同意,不从回调参数重新取值。已有 Linux.do 用户可从登录页继续登录,无需传此参数;同意协议不绕过注册关闭策略。
协议名称与正文由管理员在 `PATCH /admin/settings/registration` 配置,公开读取面是 `GET /auth/settings` 的 `agreementTitle`、`agreementContent`。`agreementTitle` 未配置时按当前外观品牌名生成“<品牌名>服务协议”,因此外观改名后注册页文案同步;接口返回的标题不含书名号,由注册页补《》。`agreementContent` 支持多段文本,按空行分段渲染,留空时注册页提示“服务协议内容待补充”。
读取注册配置失败(如 JSON 损坏)时,`RegistrationAgreement` 返回空标题而不是品牌名兜底标题,注册页据此隐藏协议入口并提示可重新读取,避免展示后台并不存在的协议名称;此时错误提示统一回落为“请先同意服务协议”。
`PATCH /admin/settings/registration` 的两个协议字段为可选指针语义:不提交表示保留已保存内容,提交空串表示明确清空。注册开关与协议正文可以分开保存,单独切换 `enabled` 不会覆盖协议。当前只校验本次注册的同意标志,不保存长期协议版本或签署凭证。前后端需一起更新,旧客户端缺少同意标志时无法注册。
## 站点皮肤主按钮
### 画布配置、更新公告与 Live2D(外观合同版本 10)
外观设置新增 `updates: { enabled, currentVersion, releases }`,由管理员通过 `PATCH /admin/settings/appearance` 保存;关闭开关保留草稿,但 `GET /public/appearance` 返回关闭状态与空公告列表,前端恢复内置更新日志。开启时公开版本与内容块,当前版本须对应公告版本。媒体上传、管理员预览、公开引用读取及限制详见 [自定义更新公告](/docs/backend/update-announcements)。JSON 请求上限为 512KB;无数据表迁移。
`GET /public/appearance` 和管理员 `GET /admin/settings/appearance` 返回 `canvas`;管理员通过 `PATCH /admin/settings/appearance` 随外观配置统一保存。字段为 `agentName`(1–24 字)、`launcherLabel`(0–24 字)、`panelTitle`(1–40 字)、`welcomeTitle`(1–100 字)、`welcomeDescription`(0–200 字)、`inputPlaceholder`(1–160 字)、`avatarType`(`orb|live2d`)、`avatarHeight`(120–360 整数)、`live2dResourceId`、`live2dEntry`。入口由服务端核验覆盖;Live2D 无有效资源时拒绝启用。名称以数据形式投影到每轮 Agent 执行上下文,不提供自由系统提示词配置,也不改变工具权限。
- `POST /admin/settings/appearance/live2d`:管理员 multipart `file` 上传,受上传频率和配额限制。成功信封的 `data.model` 为 `{ resourceId, entry }`,上传不会发布。ZIP 限 128 MiB、256 条目、解压合计 384 MiB、单文件 128 MiB;只接收 model3/MOC3/PNG/JSON,拒绝路径穿越、脚本和外部资源。
- `GET /admin/settings/appearance/live2d/:id/*file`:管理员预览,校验资源归属;其他管理员仅能查看当前外观已引用模型。
- `GET /public/appearance/live2d/:id/*file`:仅提供当前启用模型的数据文件。停用或更换后旧模型拒绝公开访问。成功为文件内容,失败为错误信封;响应带 `nosniff` 和私有、需重新验证的缓存策略。
管理员资源列表增加 `kind=live2d` 筛选;引用中的模型禁止删除,清除外观引用后按既有资源删除流程清理。
部署要求与更详细的文件限制见 [画布 Agent 外观与 Live2D](/docs/backend/canvas-agent-appearance)。
`GET /public/appearance` 返回当前 `activeSkin`;管理员通过 `GET/PUT /admin/settings/appearance` 读取、保存完整主题库。外观合同版本 8 新增 `tokens.buttons.light/dark`,字段为 `mode`(`solid|gradient`)、整数 `angle`(0–360)、HEX 色值 `start/end`、`hoverStart/hoverEnd`、`activeStart/activeEnd`、`foreground`。纯色模式使用既有主操作颜色,渐变仅用于主按钮。非法模式、角度、色值或修改锁定经典主题均拒绝保存;整块缺失的旧配置按主题身份补齐。此变更不增加数据表迁移。
## 模型规格报价
### 管理员批量调价
`POST /admin/channels/:id/models/batch-reprice` 接收最终售价及原始价格版本,返回 `{ code: 0, data: { updated: number }, msg }`。仅管理员可调用;单批 1–100 个模型,空 ID、重复 ID 或不属于当前系统渠道的记录整批拒绝。请求结构如下(价格为整数微积分):
```json
{
"models": [{
"modelId": "MODEL001",
"priceVersion": 4,
"priceTiers": [{
"id": "TIER001",
"priceVersion": 8,
"prices": { "unitPriceMicrocredits": 1370000 }
}]
}]
}
```
弹窗一行一个模型,列为模型展示名、渠道展示名(上游模型 ID)、规格、成本价和销售价输入框;多个规格在模型行内纵向对齐。利润率沿用财务口径:`(销售价 − 成本价) / 销售价 × 100%`,并非成本加价率。目标利润率为 0–99.99%,最多两位小数;售价按 `成本价 / (1 − 目标利润率)` 计算,按所选 0–6 位积分小数四舍五入(默认 2 位)。成本 1 积分、目标 20%、保留 2 位时售价为 1.25 积分。修改比例或精度实时计算,也可点击「重新计算」按当前参数覆盖全部手动售价;随后允许逐规格手动修改,保存以最终输入值为准。舍入和手动修改可能使实际利润率偏离目标;售价为零时利润率显示 `—`。
每个模型必须提交全部已启用规格(模型本身可停用),且每个规格只提交当前计费方式需要的完整 `prices` 字段:按次/秒为 `unitPriceMicrocredits`;文本 Token 为 `inputTokenPriceMicrocredits`、`outputTokenPriceMicrocredits`、`cachedTokenPriceMicrocredits`;视频 Token 仅为 `outputTokenPriceMicrocredits`。缺失、多余、重复、停用规格、null、负数或非整数价格均拒绝。成本和售价配置必须已保存;成本及 Token 售价上限为 1,000,000 积分,按次/秒售价不得超出 JavaScript 安全整数微积分范围。
服务端校验最终售价和模型/规格版本,不重新计算覆盖手工价格。价格快照过期必须刷新后重试。所有模型与价格档在单事务中更新,增加价格版本、失效目录缓存;无效配置、归属错误、超限或并发版本冲突均不部分提交。不改变成本、规格、计费方式和启停状态;未启用规格保持原值。此接口不新增数据表或迁移,不追溯修改已下单的计费快照。
### 用户模型目录
`GET /model-catalog` 固定返回 `source=system`,直接查询所有启用系统渠道下的渠道模型,目录数据在 `channels[].models[]`,`models` 固定为空数组。不受逻辑模型开关影响,不反查逻辑模型或供应线路。每项的 `modelKey` 是调用标识,`displayName` 是模型展示名,`icon` 是模型图标键,`channelLabel` 是可选的渠道展示名(默认空字符串)。后台渠道模型创建与更新请求使用相同的 `channelLabel` 字段;服务端去除首尾空白并限制为 80 字,空字符串表示使用所属渠道名称。
渠道模型创建、更新、管理列表、公开目录和会话模型价格信息均使用 `description` 表达模型描述。服务端去除首尾空白后限制为 500 字,允许空字符串清除描述;无管理员权限或超长内容拒绝保存。创作端二级选项常驻展示该字段,不依赖悬浮 Tooltip;未填写的系统模型不按名称生成宣传文案。
以上模型接口同时使用 `tags: [{ text, color }]` 传递有序展示标签,例如 `[{"text":"限时特价","color":"purple"},{"text":"官方1折","color":"gold"}]`。最多 5 个,文字去除首尾空白后为 1–12 字且不可重复;颜色仅接受 `purple`、`blue`、`green`、`gold`、`orange`、`pink`。创建/更新由管理员操作,非法标签拒绝整个保存;提交 `tags: []`(或省略字段)清空标签。用户模型选择器在对应二级渠道项中常驻展示,标签不参与路由、价格计算或权限判断。
创作端跨渠道按模型 `displayName` 分组为一级菜单,二级菜单使用每条模型的 `channelLabel`,例如 `MiniMax H3 → 秘塔(满血渠道)`。选中值继续使用 `channelId::modelKey`,报价和生成请求保留该渠道身份。展示字段不参与路由、权限及计费。系统渠道的“前台名称/前台显示别名”已移除,管理 API 不再接受或返回 `publicAlias`。
同一产品在不同渠道中使用相同的模型展示名即可归组,`modelKey` 可以不同;相同 `modelKey` 但展示名不同会分为不同组。一级顺序与图标按后台渠道和模型排序首次出现的位置确定,二级保持后台渠道与模型顺序。即使二级展示名相同,也保留独立选项及各自能力、价格,不自动改写历史配置或交换两个展示字段。
`GET /admin/references` 的 `channels[]` 同时返回 `modelDisplayNames`,包含系统渠道已创建模型的一级分组名称(含停用模型,空展示名使用产品模型标识)。编辑器“模型展示名(一级目录)”汇总去重后提供可搜索下拉,并允许输入新分组;保存后刷新引用数据。
以下接口需要登录 Cookie,返回 `data.quote`,仅校验和报价,不创建任务、账单或冻结余额,不调用供应商生成接口。
| 接口 | 请求 |
| --- | --- |
| `POST /model-catalog/quote` | `{ channelId, modelKey, intent }`,仅允许启用的系统渠道与可用模型,复用任务准入、默认参数和价格档选择 |
| `POST /models/:id/quote` | 直接提交 `intent`,按前台逻辑模型当前可用线路和价格策略报价 |
`intent` 包含 `capability`(`text/image/video/audio`)、`operation`、`inputs`(素材类型到数量的映射)和 `options`(请求规格)。素材数量必须为 0–128 的整数,再按模型能力限制校验。视频规格包括 `vquality`、`size`、`videoSeconds`、布尔值 `videoGenerateAudio`,显式 `false` 必须保留。
报价包含 `logicalModelId`(系统渠道直连为空)、`billingMode`、`quantity`、`amountMicrocredits`、`estimated`。视频 Token 报价另有 `videoTokenEstimate`:`formulaTokens`、`reservedTokens`、`outputWidth`、`outputHeight`、`framesPerSecond`、`outputSeconds`、`referenceSeconds`、`referenceDurationEstimated`、`dimensionsEstimated`、`reservationMarginPercent`。
Token 报价的 `estimated` 为 `true`,`quantity` 是预留 Token 数。参考素材数量不包含时长信息,带参考视频时统一按 15 秒估算并返回 `referenceDurationEstimated: true`。所有视频协议均支持 Token 定价;10% 预留是平台策略,不是费用上限。成功后优先有效上游用量,无有效用量时按提交的公式快照结算,余量不计入公式费用。账单新增 `videoFormulaTokens` 及 `usageSource`(`provider` / `video_formula`),公式结算的 `usageAvailable` 为 `false`,`outputTokens` 表示公式数量。算法、配置与异常用量处理见[视频 Token 计费](/docs/backend/video-token-billing)。
## 管理员积分成本
`GET/POST /admin/channels/:id/models` 与 `PATCH /admin/channels/:id/models/:modelId` 的 `priceTiers[]` 增加 `costPricing`:`configured`、`unitPriceMicrocredits`、`inputTokenPriceMicrocredits`、`outputTokenPriceMicrocredits`、`cachedTokenPriceMicrocredits`。成本沿用该价格档的计费方式;按次/秒使用单位价,文本 Token 按输入/输出/缓存分别定价,视频 Token 只使用输出价。价格为整数微积分,范围为 0 到 1000000000000;`configured=false` 表示未配置,`true` 且价格为 0 表示显式零成本。
成本保存到下单时的内部账单快照,不受售价倍率或扣费上限影响,后续改价不重算旧订单。历史逻辑模型任务切换备用线路时,成本更新为实际执行线路的快照,独立于用户售价策略。管理员请求列表、详情与 CSV 同时展示销售金额及成本金额;JSON 增加 `creditCostConfigured` 和可选 `creditCostMicrocredits`。成功结算后按实际计费数量、Token 用量或视频公式快照核算;未结算、已退回、历史无快照或缺少有效用量时不编造成本金额。
成本字段仅通过经过管理员权限校验的接口显式返回;普通用户的模型目录、会话模型信息、报价、任务与账单 JSON 均不包含成本配置、成本快照或成本金额。底层价格档和账单成本字段禁止 JSON 序列化,不以页面隐藏代替接口隔离。
数据概览不再提供独立的“模型价格配置”入口。`GET /admin/analytics/overview` 的 `kpi`、`models[]` 使用 `finance` 返回 `settledOrders`、`costedOrders`、`revenueMicrocredits`、`costMicrocredits`、可空的 `profitMicrocredits`、`profitMargin`,取代旧的费用/币种字段。按当前请求日志时间及筛选范围关联已结算订单,以订单 ID 去重并核对用户与实际结算渠道;轮询、下载、未结算、退款和无关联账单请求不计入财务汇总。收入为实际结算积分,成本复用系统模型价格档的订单快照与计费用量,利润为收入减成本,利润率为利润占收入百分比。已知成本可显示部分合计;只要有已结算订单缺失成本,利润和利润率返回 `null`,不把未知成本视为零。无订单时利润为 `null`;零收入时利润率为 `null`。历史无成本快照的订单不按当前价回填。此处为已结算生成订单的积分毛利,不含充值收入或失败请求的上游损耗。
`GET /admin/analytics/export.csv` 沿用相同口径,CSV 输出收入、成本、利润(整数微积分);重复请求只在该筛选范围内选定的一行记录金额,未知值留空。
上传、工作流结构查询、取消查询和取消请求也不参与财务归属,即使日志被标记为可计费;它们不能代替生成请求承载订单金额。订单是否已结算以账单状态为准,保留人工核账后结算订单的统计能力。
## 画布保存与 operations 同步
画布写入统一使用 `PATCH /canvas-projects/:id/operations`。请求只携带对象级或字段级变更,不提交整批画布快照;`mutationId` 是一次写入的幂等身份,客户端在响应丢失、断网、刷新或后台重试时必须复用同一个 ID 和原始 `operations`。`baseRevision` 记录生成请求时的已确认基线版本,用于诊断和响应;字段/对象的 `before`、`expected`、`beforeValue` 是实际并发校验,允许不同字段合并,拒绝同字段覆盖和缺失对象更新。可选顶层字段缺失时必须显式编码为 JSON `null`;服务端仅对早期客户端省略这些可选字段的 `value` 或 `beforeValue` 按 `null` 兼容处理,并继续执行基线校验。
| 接口 | 行为 |
| --- | --- |
| `GET /canvas-projects/:id` | 返回当前完整画布及权威 `revision`;响应包含基于数据库版本的弱 `ETag` 和 `Cache-Control: private, no-cache`。请求可用 `If-None-Match` 做条件读取,版本未变化时返回 `304`。 |
| `PATCH /canvas-projects/:id/operations` | 接收 `{ mutationId, baseRevision?, operations[] }`,返回当前项目摘要和 `mutationId`。支持 `create_canvas`、`create_node`、`patch_node`、`remove_node`、`create_connection`、`update_connection`、`remove_connection`、`set_field`。 |
| `DELETE /canvas-projects/:id` | 显式删除画布。删除后迟到的更新不会重新创建画布;只有新的 `create_canvas` 操作可以显式创建同 ID 画布。 |
`create_node`、`create_connection` 和 `create_canvas` 是唯一的创建语义;更新、删除和连接操作都要求目标存在并通过对象基线校验。节点删除会同步移除关联连接,迟到媒体回写或编辑更新返回结构化冲突,不会复活已删除节点。请求摘要与确定性结果写入 `canvas_mutation_receipts`,画布内容、版本递增和 receipt 在同一数据库事务完成;同一 `mutationId` 和相同摘要返回原结果,摘要不同返回 `canvas_mutation_id_reused`。
前端先把未确认请求和请求基线写入账号隔离的本地 journal,再发 HTTP 请求。journal 保存已确认基线、原始请求、请求生成时的本地投影和创建时间;刷新后只重放原请求,随后把请求之后的新编辑按新的基线生成下一批 operations。服务端返回完整画布时,客户端只合并仍未确认的本地 mutations,视口仍属于本地视图状态,不进入远端 operations。
云端 Agent、媒体生成终态回写、预演台、创作提交以及 LibTV/TapNow 导入都复用同一 operations 服务。Agent 事件中的 `canvasPatch` 只是一种有界通知;过大、删除或无法完整表达的变更使用 `requiresRefresh: true`,客户端重新读取后按未确认请求继续重放。用户界面不提供版本记录、加载云端版、覆盖云端或手动重试入口,失败请求由后台自动重放并在同步状态中显示当前状态。
平移和缩放只持久化浏览器视口,不生成远端 operations。保存日志仅记录请求/链路 ID、画布/用户 ID、版本、节点数量变化及 HTTP 状态,不记录正文、提示词或媒体 URL。
operations 的结构化配额在提交事务内实时统计账号的素材/画布数量和 JSON 字节数,不为画布保存扫描任务正文及 API 请求/响应日志。接口请求、响应、容量上限及错误语义均不变;性能排查应分别记录成功写入、同请求重放和冲突请求的耗时,冲突请求提前退出不能代表完整提交耗时。
## 欢迎页开放配置
`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)、权限模式、上下文范围、技能和工具清单;跨轮历史仍按可恢复事实和发送预算整理,不把某个固定字节数当作模型窗口。Agent 运行由服务端任务 Worker 驱动,浏览器断开不会停止运行。
文本模型能力合同包含 `contextWindowTokens`(输入加输出的总上下文窗口)和 `maxOutputTokens`(输出/推理上限)。Agent 每轮根据实际执行的渠道模型计算输入预算,并预留工具 schema、协议包装和恢复空间;逻辑模型按候选文本线路取安全交集,旧配置或能力解析失败回退 128K / 16K。1M 模型可通过后台能力配置使用更大输入预算。前后端的 Token 估算用于提前压缩和拒绝病态请求,最终仍以上游 tokenizer 和响应为准。运行 checkpoint 的 512KB 持久化上限只约束恢复状态,不限制模型上下文窗口。
- `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 保护,超限先丢掉更早的可重读正文并保留结构化事实,单轮仍超出当前模型能力预算时才提示缩小范围或新建对话;历史结构损坏返回明确 409,不投影为通用 500。
- 续轮交接是一条**有界结构化事实帧**(`{ source: "server", kind: "run_handoff", parentRunId, status, failureReason?, submittedTaskIds?, canvasChanges?, omittedCanvasChanges?, authority }`):画布改动取自上一轮真实的 `canvas_updated` 事件,最多 12 组(operation、去重后的 nodeId/title、actionCount),超出的组数只记 `omittedCanvasChanges`,不静默丢弃;帧还带 `authority` 明写"执行事实交接,不是重放写入、重复提交任务或扩大权限的授权"。帧按字节封顶(单个 nodeId 截 64 字符、整帧 8 KiB,超限按最近改动优先裁剪),失败原因沿用 `cloudAgentSafeUserMessage` 的脱敏口径,不合格退回固定说法。身份由服务端字段 `agentContextSource=continuation` 承载并要求"来源元数据 + `【运行状态】` 前缀 + JSON `source=server`/`kind=run_handoff`"三者同时成立,因此用户逐字复制帧正文也拿不到该身份;升级前落库、没有来源字段的旧散文交接只在历史裁剪与轮次计数上按"紧跟上一轮 assistant 回复 + 旧版文案前缀"识别,该判据不影响 canonical 身份。
- `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`,只读清单中存在的层。没有偏好层时工具清单不含该工具。偏好仅是非授权数据,不能扩展工具、节点或预算,不能绕过审批;最多 12000 字符/层。
- 系统行为策略与媒体默认策略是版本化、编译进后端发布物的资源,修改它们仍需发布后端,不是后台热更新;工具、校验、扣费和审批在代码侧强制执行。系统行为策略当前版本为 10:`canvasSummary` 被定义为节点目录,模型不得把它当成完整画布。画布节点能力由服务端 registry 定义,工具 schema、创建、连线与画布目录读取共同消费;新生成模式还必须接入媒体任务适配器,不能仅凭 md 或注册节点启用收费操作。
- 已安装且属于当前用户的技能在创建时只保存版本、哈希和文本文件路径,不预存全部正文,也不按所有选中技能正文合计 128KB 拒绝创建。`skill_read_file` 按需读取 `SKILL.md` 或必要引用,支持字符偏移 `offset`(默认 0),每页最多 12000 字符,返回 `hasMore` / `nextOffset`。读取时重新验证用户权限、启用状态和版本;技能更新后拒绝混读,需新开一轮。技能是不可信数据,不能创造工具、改变权限或覆盖系统规则。
- Agent 按实际文本模型的 `contextWindowTokens` 与 `maxOutputTokens` 计算输入预算,并在达到该模型预算的压缩阈值时移除较早工具结果中可重新读取的正文,保留工具调用/结果配对、用户原始要求、生成回执及最近完整工具轮次,并允许重新读取。此机制是确定性正文卸载,不是 LLM 语义摘要;运行 checkpoint 的持久化上限与模型上下文窗口独立。技能读取事件只保存回执,不复制正文,正文页只进入模型对话。
- 达到同一条压缩线时整轮不再直接判死:Agent 先暂停步进循环,用一次独立模型调用(任务操作名 `cloud_agent_context_compaction`,按单步墙钟计时但不占本轮的模型调用步数)把历史压成结构化检查点,再以 `Resume` 用压缩后的上下文继续本轮;同一轮最多压缩 4 次,压完仍超过输入预算才回到原有的预算失败路径。检查点是固定字段的 JSON(`version` 为 1,字段为 `historySummary`、`scriptDesign`、`operationHistory`、`pendingTasks`、`currentWork`、`nextStep`、`decisions`、`constraints`、`userPreferences`、`compactedTurnCount`),未知字段一律拒绝,超过 64KiB 判为不合格。压缩后的上下文是「检查点帧 + 确认回执 + 最近 2 对完整对话」:不以 tool 回执或带工具调用的 assistant 开头,检查点帧不计为用户轮次,续轮交接不会再重复追加本轮的用户要求与回复。渠道没有配置模型上下文窗口时,判据退回既有的跨轮历史闸门(`cloudAgentHistoryKeepRounds` 轮与 `cloudAgentHistoryMaxBytes` 字节)。
- 压缩过程对外只有两个事件:`context_compaction_requested` 带触发读数(`basis` 为 `tokens`/`bytes`、`projectedTokens`、`usableInputTokens`、`compactAtTokens`、`pressureRatio`、`budgetSource`、`sourceBytes`、`turnCount`),`context_compacted` 带 `mode`(`model` 为压缩模型输出,`fallback` 为服务端保底检查点)、`resume`、`historyMessages`、`compactedTurnCount`、`droppedTurns`,降级时附 `reason`。压缩模型失败、输出不合规、或本轮在压缩期间被取消/失败,服务端都落保底检查点并写 `mode=fallback`;终态轮次的收尾只落检查点与事件,不改写已经失败或已取消的状态。
- 每次普通 Agent 步进模型调用发出之前都会落一条 `context_pressure` 事件(`schemaVersion: 2`、`phase: "before_request"`),把"这次请求装了多少、离装不下还有多远"作为可观测读数下发,`requestId` 是本次请求的任务 ID。主读数是 `projectedNextInputTokens`:有可采信的上游实测锚点时等于"锚点实测 + 本地估算的有符号增量"(`tokenSource` / `measurementSource` 为 `provider`,`tokenScale` 给出实测÷估算的换算比例);没有实测时退回纯本地估算(`tokenSource` 为 `estimate`)。两把尺子各有自己的名字、谁也**不覆盖**谁:`estimatedInputTokens`(`readingScope: "next_request"`、`estimateMethod: "local_v1"`)永远是本地下一次请求的估算,`pressureTokens` / `providerUsage`(`providerMeasurementScope: "previous_request"`)永远是上游对**上一次**请求的实测,消费方不得把两者画成一条曲线。本读数只增加可观测性:请求准入与正文裁剪的既有判据不变。
- 上游实测锚点的采信与治理:上游没有回 usage 的调用(`usage_available=false`)不定锚、不猜用量,读数继续用估算;只有"模型调用"这一步能配锚(媒体任务发的是另一份信封,`operation` 不匹配即跳过)。上游实测与本地估算差出一个量级时不采信(比值低于 0.5× 或高于 2×,通常是换了模型或计量口径),事件给 `anchorRejected` 说明原因并继续用估算。锚点可以跨步沿用,但**换模型、换线路、换系统提示或工具 schema、换模型窗口,以及超过 3 步未刷新**(`anchor.ageSteps`)都会作废:真机上出现过锚点被冻结、估算已经涨了 50% 而读数还停在老值的情况。
- 事件还带 `breakdown` 占用分布:三个桶(`system` 系统提示(含画布摘要)、`tools` 工具 schema、`messages` 会话消息(含工具结果))各自的字节与估算 Token,协议外壳余量 `envelopeBytes`(tool choice、缓存键这类不属于任何桶的部分,单独报而不摊进桶里),按锚点比例换算的 `tokenScale` 与每桶/每分段的 `scaledTokens`,以及 `systemSegments` 系统提示内部分段(`policy` / `mediaPolicy` / `capabilities` / `execution` / `memory` 个人记忆 / `other` 零头)。分段按编译顺序逐块量出,合计与 `system` 桶严格相等:编译之后才拼接的个人记忆由 `memory` 登记,零头归入 `other`,不出现"分段加不到桶"的错觉。
- 窗口口径由 `contextWindowTokens` / `reservedOutputTokens` / `overheadTokens` / `inputBudgetTokens` / `usableInputTokens` / `budgetSource` 给出:输入预算 = 上下文窗口 − 输出预留 − overhead,其中 overhead 是工具 schema、协议包装与兜底轮的比例预留(窗口的 4%,夹在 4096–32768),`compactAtTokens` 是既有的"可重读正文就地裁剪"触发线(输入预算的 85%),`budgetSource` 说明这份窗口来自渠道模型还是逻辑模型的线路交集。**没有解析到模型自己声明的窗口时不下发这些字段、也不给占用率**(`modelLimitConfigured: false`):窗口未知就说未知,不拿兜底默认窗口冒充真实能力编一个百分比。
- `context_transition` 事件说明"读数为什么变":`window_resolved`(窗口从未确认变为已确认,读数改用窗口口径,**不是**上下文变少)、`model_changed` / `route_changed` / `window_changed`(锚点因换模型、换线路或换窗口而作废;窗口变化另带 `before` / `after` 的窗口读数)。它只解释口径与锚点状态,不携带任何授权语义,也不改变执行边界。
- 画布 Agent 的看图能力:当且仅当本轮渠道模型的能力合同声明了图片输入(`text.references.maxImages > 0`)时,额外暴露 `canvas_inspect_image(nodeId, refresh?)`。工具校验图片节点、资源归属、就绪状态、媒体类型和模型文件大小限制;检查点与任务输入只保存 `resource:ID` 占位及授权参考清单。worker 复用资源读取链路,在内存中将占位替换为真实图片数据后展开模型协议,不持久化 base64 或签名 URL,不依赖 `CANVAS_PUBLIC_BASE_URL`,不接受任意外部地址。多图在本批所有 tool 回执后统一附送;图片保留最近 3 个工具轮次,发给模型时还按数量上限保留最新图片,本批超限则明确要求分批查看。同一张图一轮内附送两次后只回执文字,确需重看时传 `refresh=true`。工具成功不等于识别成功,普通模型正文不再自动写入 `visualNote`,旧 `inspected` 标记及观察不跨轮继承;`visualIdentity=unknown` 表示没有已确认缓存,不表示模型不能查看随附图片。画面内文字仍按不可信数据处理;资源读取失败则真实失败,不凭标题推断画面。其他需要公网 URL 的生成协议仍遵循各自存储配置合同。
- 平台支持工具全集为 `agent_profile_read`、`plan_update`、`ask_user`、`recall_lessons`、`remember_lesson`、`canvas_list_node_types`、`canvas_get_state`、`canvas_inspect_image`、`canvas_read_batch_table`、`canvas_read_storyboard`、`skill_read_file`、`skill_search`、`task_get`、`model_list`、`canvas_create_storyboard`、`canvas_edit_storyboard`、`canvas_edit_batch_table`、`canvas_apply_ops`、`canvas_arrange_nodes`、`generate_media`、`image_layer_split`、`image_text_detect`、`image_annotation_render`;单轮实际工具仍按偏好快照、技能、权限、画布上下文和生成预算裁剪。画布写入工具要求本轮授权 `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 项;`add_node` 可省略坐标:服务端按画布内容落位(优先贴合本批连线的对端,否则追加到同泳道之后并避让已有节点),不再落到原点重叠;`update_node` 的 patch 额外支持 `x`/`y`(坐标绝对值上限 1e6)用于微调单个节点。支持新增文本、Markdown、图片、视频、音频、分组与分镜表节点,以及 `connect_nodes` 引用连线。媒体占位节点的 `content` 是提示词,不是媒体 URL;只建节点不提交收费生成。`update_node` 的字段映射完全由能力 registry 决定:文本 / Markdown 可改标题和正文,图片 / 视频 / 音频可改标题和下一次生成使用的提示词草稿;媒体更新参数中的 `content` 写入 `metadata.composerContent`,不会覆盖媒体结果 `metadata.content`、已提交提示词 `metadata.prompt`、任务状态或引用状态。锁定节点、删除、任意元数据写入、自连接、重复边与不存在的端点仍被服务端拒绝。
- `canvas_arrange_nodes` 只改节点坐标:`snapshotHash` 必需,`mode` 取 `auto`(有引用连线按依赖分层、否则按媒体类型分区)/`flow`/`byType`/`row`/`column`/`grid`,`groups` 把节点按给定顺序排成横向位置带(仅位置分带,不创建容器、不改层级),`align` 做对齐或等距分布,`gap` 调整组间距,`dryRun` 只预演。`nodeIds` 省略时整理全部可整理节点,锁定节点、背板等容器节点、隐藏批次子节点与已归属背板的子节点保持不动并在回执中说明;一次最多 50 个节点。整理与 `add_node` 的自动落位共用服务端布局实现(`backend/internal/canvas/layout`),与前端“自动整理节点”使用同一套常量与算法。
- `model_list` 复用生成面板的当前模型目录。生成前传 `mode` 和实际 `referenceNodeIds`(无参考传空数组),服务端读取当前用户画布内的就绪资源,按真实素材类型、数量及生成操作构建意图并调用统一能力匹配,不让多图生视频落入仅文生视频候选;返回 `intent` 供核对。空参数仍可浏览完整目录;空候选不降级到不匹配模型。每项的 `selection` 是 `logicalModelId` 或 `channelId` + `channelModelKey`,工具调用必须原样选择一种,不猜 ID 或混用;时长、画幅、音频、价格仍需核对返回配置,任务准入再次强校验。
- `generate_media` 支持图片 / 视频 / 音频:必填 `mode`、`prompt`、`nodeId`(可续用草稿,无草稿时才新建)、`title`、有序 `referenceNodeIds`(无参考传空数组),再加一种模型选择;`snapshotHash` 可省略,省略时服务端使用当前画布内容快照。图片 / 视频要求 `size`,视频要求正数 `durationSeconds`,图片 / 音频时长为 0。可传来源节点 `sourceNodeId`、`quality`、视频 `videoGenerateAudio`。`sourceNodeId` 仅接文本 / 镜头提示词节点;参考图片、视频、音频只放 `referenceNodeIds`,无文本来源时留空且不重复传入。引用从当前画布节点的账号资源解析,校验归属、类型、就绪状态和模型能力,不接受任意 URL。图片生成仅接图片参考;视频可按模型能力接图片、视频、音频参考;音频生成当前仅支持文本输入。
- 参数选择与收费授权分开:用户明确说“随便 / 你决定 / 按默认”时,Agent 可从当前目录选择能力及预算匹配的模型,优先用户最近明确使用的可用模型,其次价格清楚的低成本选项;支持时沿用参考图原始比例,否则选目录默认,时长用目录默认。未要求音频且模型不支持时用默片。用户接受已推荐方案后不再要求特定确认口令;仅真正缺失或冲突的阻塞项一次性询问。本轮已有目录结果应复用。以上是 Agent 的参数选择策略,服务端仍强校验具体规格,不提供绕过预算 / 能力校验的默认兜底,也不绕过独立收费审批。
- 媒体参数先进行无任务、无预留账单的准入校验,再在同一事务创建草稿节点、来源 / 资产连线及待审批检查点;`approval_requested` 和运行中的 `approval` 携带目录真实 `modelName`。用户批准后才在事务中绑定任务、创建预留账单并更新检查点;拒绝或取消保留未生成草稿,不提交媒体任务。`nodeId` 可续用同类型、未锁定、idle、无任务且无媒体结果的空白占位节点或当前运行草稿;跨轮 Agent 草稿还要求原运行属于同一用户和画布、已结束且清理完成。续用必须重新读取快照并重新审批,不能复用旧审批、覆盖已有任务或成品,不能通过循环新建节点绕过限制。画布读取的 `generationDraft` 提示未提交、原运行状态、是否仍在清理及仍需审批;写入仍重新强校验。已明确审批的规格不得被模型默认值静默替换。媒体终态回写原节点;已删除或已换任务的节点不自动复活,结果仍可从任务中心查看。
- `generate_media.prompt` 使用 `@图片1`、`@视频1`、`@音频1` 引用素材,各类型按 `referenceNodeIds` 的出现顺序独立编号,文本来源不占媒体编号。角色主图属于图片序列,与普通图片共享编号;编辑器显示时保留 `@角色N` 前缀,但 N 仍对应实际图片位置,例如角色、场景图、角色依次显示为 `@角色1`、`@图片2`、`@角色3`,任务使用 `@图片1`、`@图片2`、`@图片3`。已有标签保留;遗漏的画布素材以“素材名称:@标签”补入 `【资产参考】`,不推断素材用途。节点入边同步排列,草稿、审批和实际任务的标签对应同一组参考资源,编辑器与任务仅转换显示前缀。未绑定的媒体编号和补齐后超过16000字符的提示词会在提交前被拒绝,模型自身的提示词长度限制仍由任务准入校验。临时标注图没有持久画布节点,不自动添加无法解析的编辑器标签;历史节点不自动改写。
- Agent 的 `snapshotHash` 忽略顶层 `viewport`、`updatedAt`。`canvas_get_state` 另返回 `mediaSnapshotHash`,供 `generate_media.snapshotHash` 使用:仅比较节点与连线,节点忽略 `position`、`width`、`height`、`createdAt`、`updatedAt`,保留全部业务字段及未知 metadata。生成草稿审批也固定该哈希,移动、调整显示尺寸、自动补时间戳和保存聊天记录不会使生成审批失效,执行保留最新展示信息;提示词、素材、锁定状态、任务绑定和连线变化仍需重新读取并审批。画布修改与撤销继续使用完整内容哈希,数据库写入保持完整快照 CAS。发布前已按旧算法生成的待审批内容可能要求重新读取和审批,不自动替换旧快照重提任务。
- `canvas_get_state.nodes[].outputReference` 的 `ready` / `issue` 仅表示该节点产物能否作为其他生成的参考,不诊断本节点的生成输入。`generation` 返回当前画布、当前用户所属任务的 `taskId`、`taskStatus` 和安全的失败 / 取消 `error`;无任务绑定为 `not_submitted`,无法验证归属或读取失败为 `unavailable`。节点无法作为草稿提交时返回 `submitBlockedReason` / `submitBlockedIssue`,与写路径共用节点准入规则;未被阻止不等于已获授权,草稿所属运行和审批仍在提交时校验。`task_get` 同样返回安全错误详情。`generate_media` 准入失败可返回 `snapshot_conflict`、`task_bound`、`target_locked` 等结构化 `reason` 和 `nodeId`,不自动选择新节点或重新生成。
- `task_get` / `generation` 的执行状态与回写状态分开:`succeeded` 不返回执行 `error`;回写失败仍通过 `writebackOutcome` / `writebackError` 暴露,不能因生成成功而隐藏。失败 / 取消任务保留脱敏错误,缺少详情不能伪装成成功。Agent 文本步骤的持久诊断为 `upstream_dns_failed` 时,`run_failed.reason` 同步该分类并明确提示检查渠道域名及后端 DNS,不自动重跑。
- `generate_media` / `image_layer_split` 必须将 `model_list.selection` 展开到顶层参数:提供非空 `logicalModelId`,或完整的非空 `channelId` + `channelModelKey`,不得混用;未使用的字段可省略或传空字符串,不能传 `null` 或仅含空白的字符串。后端在读取画布、创建草稿、审批与计费前校验选择结构;返回 `reason: invalid_tool_arguments`、`field`、`issue` 和本轮工具 `parameters`,区分 `required`、`mutually_exclusive`、`type_mismatch`、`invalid_value`。合法结构不代表模型可用,仍须通过目录、能力、权限和价格校验;不猜测模型、不补全选择、不切换渠道,也不吞掉未知字段。
- `canvas_apply_ops` 创建节点、编辑提示词或连线不会提交生成任务,也不会改变已提交任务的输入;收费生成独立使用 `generate_media`。操作参数错误按数组项返回 `field`(例如 `ops[0].snapshotHash`)和 `issue`(例如 `unexpected_field`、`type_mismatch`、`required`),并附本轮工具 schema;不把未知字段误报成缺少其他必填字段,不回显任意参数键和值。
- `read_only` 只允许读取;`request_approval` 对写画布和媒体生成暂停为 `waiting_approval`;`auto` 可在预算内执行已授权画布操作,但图片 / 视频生成始终独立审批,不能绕过。`maxGenerationTasks` / `maxVideoSeconds` 为非负整数,0 表示该项不设业务上限,正数按本轮累计执行;不再固定 8 次模型调用、8 个生成任务或 120 秒视频。积分上限仍必填且大于 0,实际报价在任务创建与预留积分前校验,时长必须符合所选模型的实际能力。失败事件保留具体错误和已有任务的 `taskId`、`nodeId`、`status`;媒体结果的 `phase` 区分 `admission` 与 `completion`,`taskSubmitted` 表达是否已提交。已提交的收费生成失败作为工具错误交回模型,整轮保持可继续,不得自动重试收费生成;只有生成结果无法回写画布时才把整轮标为 `failed`。模型应报告错误,不能自行删减提示词。
- 必须有当前用户已保存的画布。系统提示里的 `canvasSummary` 是节点目录,不是整张画布的 JSON:每条只含 `id`、`type`、截断后的 `title`,以及不支持操作的类型标记;最多列入 120 个节点,并按约 24KB 兜底截断,装不下的节点记入 `omittedNodes` 与 `totalNodes`。正文、提示词、媒体和结构化表不进目录,由 `canvas_get_state` 按 `offset` 每页读取 40 个节点、资产可引用状态及相关连线,按 `nodeIds` 精读最多 8 个节点(正文每字段最多 16000 字符),通过 `hasMore` / `nextOffset` 和字段 `Truncated` 判断是否完整。工具结果留在当轮消息里,历史压缩会卸掉可重读正文,不会在后续每一步把整页结果再打包一次。分镜表包含镜头、角色节点与资产绑定;摘要每页最多 20 行,精读每次 1 行,可用 `storyboardOffset` 翻页;单行叙事文本累计最多 24000 字符,角色 / 绑定各最多 16 项。上下文不含媒体 URL、任意元数据或未同步修改;节点名称不等于图像视觉识别。普通任务重试接口不能重试 Agent 任务。
- `canvas_get_state` 首次调用传 `{}`,仅接受可选的 `offset`、`storyboardOffset`(非负整数)和 `nodeIds`(最多 8 个字符串组成的数组);当前画布由运行绑定,不接受 `canvasId`、`limit` 或 `page`。读取工具分别反馈非对象 / 多对象、JSON 格式错误、未知字段和字段类型错误。可纠正的工具参数失败在 `tool_failed.result` 及模型工具结果中返回 `reason: invalid_tool_arguments`、本轮已声明的 `parameters` 和 `guidance`;画布读取额外给出 `exampleArguments: {}`。失败调用仍如实记录,由模型修正参数后继续,不静默丢弃字段或自动重放写操作;资源归属、审批、快照和收费校验仍按原规则执行。
- `GET /agent/runs/:id` 返回运行信息;`POST /agent/runs/:id/cancel` 取消当前运行及活动子任务;`POST /agent/runs/:id/approvals/:approvalId/decision` 接受 `{"decision":"approve"|"reject","reason":"..."}`,审批决定幂等且服务端校验归属。
- `GET /agent/skills/usage` 需要登录,只聚合当前用户最近最多 200 次 Agent 运行的日志;返回技能启用次数、检索命中、成功/失败读取、总纲/卡片读取和最多 5 条最常读卡片路径,不返回技能正文。损坏的日志会返回错误而非静默给出不完整统计。
- 运行详情的事件**分页**:事件全量在 `cloud_agent_event_records`(append-only,主键 `run_id` + `sequence`),运行详情默认只返回**尾部一窗**(最新 40 条),并同时给出四个位置字段:`eventSeqBase`(本次返回的首条事件之前已入库的条数,不变量是 `events[i].seq == eventSeqBase + i + 1`)、`eventCount`(该轮次累计事件条数)、`latestSeq`(本次返回的最后一条序号,可直接作为下次 `sinceSeq`)、`eventsTruncated`(还有更早的记录没随本次返回)。不返回事件时 `eventsTruncated` 为 false,`eventSeqBase` 退回内存窗口在整条日志里的偏移。
- 运行详情查询参数:`?sinceSeq=<seq>` 只返回该序号**之后**的事件(增量 / 断线重连,单次最多 500 条,按 seq 升序);`?eventLimit=<n>` 覆盖默认页大小,取值 1–500,大于窗口时由服务端从事件表往前补页。两个参数都必须是整数,越界或非法返回 400;游标追平或越界时返回空 `events` 而不是报错。读取一律走 `(run_id, sequence)` 的 keyset 分页(`sequence > ? ORDER BY sequence LIMIT ?`),不用 offset,也不会为这些字段载入整条日志。升级前把事件放在 `state_json` 里的旧轮次在事件表中没有行,读取回退到内存窗口,行为与升级前一致。
- 首次通过取消接口取消运行时记录一次 `run_cancelled` 事件,`source: user_request` 表示来自用户取消 API,并携带当时的 `activeTaskId` / `mediaTaskId`;此事件表示取消请求已接收,关联任务最终状态以任务查询及收尾状态为准。运行记录损坏或超限时优先保证取消和后台收尾,事件无法保存会记录服务端告警;旧导出日志不能据此追溯取消发起来源。
- `GET /agent/runs/:id/events` 返回 `agent_event` 和首次 / 状态版本变化时的紧凑 `run_snapshot` SSE;无变化时只发送 heartbeat。事件 `Seq` 在运行内递增;支持 `?after=` / `Last-Event-ID`,游标不是运行 ID,服务端把它作为运行详情的 `sinceSeq`,因此断线重连只读该游标之后的增量。`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`。
- 可纠正且尚未产生副作用的工具参数错误会在 `tool_failed` 的顶层及 `result.retry` 返回 `{ groupId, attempt, maxAttempts, status }`。`status=retrying` 表示 Agent 可按字段、schema 和示例自动纠正;同一工具累计三次仍失败时返回 `status=exhausted`,随后才产生 `run_failed(reason=tool_retry_exhausted)`。纠正成功的 `tool_completed` 返回同一 `groupId` 和 `status=recovered`。前端应按 `groupId` 折叠中间失败,不能将其显示为独立红色错误。权限、安全、存储错误以及已提交的收费生成任务不进入该重试流程。
- `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 恢复;运行、技能快照、审批、工具调用、子任务和事件均以服务端持久化状态为准。提交响应丢失时,客户端应使用同一幂等键重试,不生成新的提交。
媒体完成事件分别报告生成与回写结果:`tool_failed.result.status` 始终是媒体任务真实状态,`generationError` 是脱敏后的生成错误;`writebackError` / `writebackReason` 记录回写失败,不能覆盖生成错误。`targetNodeId` 保留原调用目标,`nodeId` 只在匹配到真实任务绑定时返回。回写原因包含 `canvas_unavailable`、`target_node_missing`、`task_binding_changed`、`result_resource_unavailable`,缺少可验证目标时为 `target_node_unknown`。终态 `run_failed` 同步提供目标节点、`generationStatus`、两层错误及回写 `reason`,并持久化 `failureMessage`。回写必须同时匹配原目标节点和任务 ID,不能仅凭另一个节点复制的 taskId 改写目标;恢复收尾同样使用任务持久化的目标信息。失败任务只承诺保留任务记录,不宣称已经生成可用媒体。
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`、`providerId`、`timeField`、`from`、`to`、`page`、`pageSize`。关键词匹配订单号、渠道交易号、用户名称、用户名或邮箱,也支持完整用户 ID 精确匹配。用户名和邮箱搜索忽略大小写;所有筛选同时生效,返回匹配总数和当前页订单。`timeField` 为 `created`(默认创建时间)、`paid`(渠道支付时间)、`credited`(入账时间)。`from`、`to` 为 `YYYY-MM-DD`,包含起止日期,按北京时间转换为左闭右开的时间区间;未填写的边界不限制,倒置日期或无效状态/时间口径返回 400。
每条管理员列表订单在原有字段之外包含 `user: { id, username, displayName, email }`,由当前页用户 ID 批量读取;用户不存在时返回 `user: null`,保留原 `userId` 与历史订单。此身份摘要仅用于管理员订单列表,不添加到普通用户支付接口或支付状态同步响应。
后台支付订单列表展示用户身份,点击名称打开用户详情;订单详情提供完整订单号、渠道交易号、用户 ID 和各阶段时间。同步支付状态与关闭订单位于更多菜单,关闭订单仍需确认。
官方易支付插件 `official-payment-epay` 通过既有支付接口接入,不新增业务路由。管理员必须显式填写可信支付网关;V1 支持 MD5 下单、查单与通知验签,未付款订单的关单仅关闭本地记录。V2 目前只支持 RSA 下单和通知验签,不支持查单、刷新收银台及自动关单;相应操作返回明确失败,不能按“订单已关闭”处理。启用前需在实际网关验收回调和到账,不能用插件安装成功代替支付链路验收。
### 支付对账筛选及 CSV 导出
`GET /api/admin/payments/reconciliations` 支持 `providerId`、`status`(`running` / `completed` / `failed`)、`from`、`to`(账单日期,包含起止日)、`page`、`pageSize`。`GET /api/admin/payments/reconciliations/:id/items` 支持 `result`、`page`、`pageSize`;`result=abnormal` 同时选出所有非 `matched`、非 `recovered` 明细,未指定或 `all` 表示全部。具体结果还支持 `matched`、`recovered`、`local_order_not_found`、`provider_record_missing`、`amount_mismatch`、`trade_no_mismatch`、`credit_failed`。
三个只读导出接口均在 service 校验管理员权限,返回 `text/csv; charset=utf-8` 附件,设置 `Cache-Control: no-store`。导出复用列表筛选条件,不受 `page`、`pageSize` 影响:
| 接口 | 参数与内容 |
| --- | --- |
| `GET /api/admin/payments/orders/export.csv` | 与订单列表相同的筛选;完整订单号、渠道交易号、用户身份、商品、渠道、金额、币种、积分、订单/渠道状态及各阶段时间 |
| `GET /api/admin/payments/reconciliations/export.csv` | 与对账列表相同的筛选;每日执行状态、总笔数、一致/补发/异常笔数、失败原因、发起人 ID 或系统、开始/完成时间 |
| `GET /api/admin/payments/reconciliations/:id/items/export.csv` | `result`;单次对账的完整交易明细及对账开始/完成时间,便于区分同日重跑结果 |
最多导出 10000 行数据(不含表头);超限返回 400 业务错误,提示缩小筛选范围,不截断文件。无匹配数据时接口返回仅含表头的 CSV;前端无数据时禁用导出。导出不查单、不重跑对账、不补发积分。
对账完成不代表无异常,汇总保留异常笔数;失败记录仍可导出汇总及失败原因。明细仅允许已完成的对账,执行中或失败返回 409。对账重跑复用记录并替换明细,明细导出使用可重复读快照,保证汇总与明细来自同一次数据库快照。订单及关联用户身份也在同一快照内读取。
明细的“记录金额”附带“金额来源”:通常为渠道账单,`provider_record_missing` 为本地订单。该字段不声称同时保存渠道和本地金额,不根据订单当前值推算历史差额。订单金额和积分为订单数据,未支付订单的积分不代表已经发放。
CSV 使用中文表头、UTF-8 BOM、CRLF;金额按整数分精确转换为两位小数,积分从微积分转换并保留最多六位小数,时间表头注明北京时间。空时间留空。文本转义逗号/引号/换行,并在公式触发字符前加单引号;可被解析为数字或科学计数法的 ID/订单号也加单引号防止表格软件丢失精度,不生成 `="..."` 公式。通用 CSV 消费者可能看见该保护前缀,按文本导入并按需移除;仅 CSV 引号不能声明列类型。
前端使用已经应用到列表的筛选条件导出;更改条件后,先查询/筛选再启用导出,分页和刷新沿用已应用条件。明细抽屉支持“仅异常”和单独刷新,切换记录/筛选时取消旧请求,避免旧响应覆盖新记录。下载接口的 JSON 错误 Blob 会解析为业务提示,超量或重跑冲突不会被保存为 CSV。
## 用户 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` | 冲突 |
| 409 | 409 | `project_name_conflict` | `POST /projects` 同一用户下已有同名项目;客户端可有限更换名称重试 |
| 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。
常量:后端标准原因定义在 `backend/internal/kernel/error_codes.go`,由 `app` 和 `service` 兼容导出;前端业务模块按 `ApiError.reason` 分支。
## 技能库分类 API
以下接口需要登录。技能库分类与技能广场共享分类分离:`skills.tag` 仍表示共享市场分类;用户技能库分类记录在用户自己的技能状态中,只影响该用户技能库和画布 Agent。
| 方法 | 路径 | 行为 |
| --- | --- | --- |
| GET | `/api/skills/library-categories?scope=mine\|created` | 返回当前用户可见的个人分类和平台分类,以及所选视图下每类数量、全部技能数和未分类数量。`mine` 包含自己创建及已加入的公开技能;`created` 只统计自己创建的技能。 |
| POST | `/api/skills/library-categories` | 请求 `{ "name": "分镜", "scope": "personal" }`。`scope` 可省略并默认为 `personal`;管理员可指定 `platform`,该分类对所有用户可见、可分配和筛选。同一用户可见范围内分类名唯一。 |
| DELETE | `/api/skills/library-categories/:id` | 删除个人分类(仅分类所有者)或平台分类(仅管理员),只解除该分类与技能的关联,不删除技能。 |
| PATCH | `/api/skills/:id/library-category` | 请求 `{ "categoryId": "分类 ID" }` 设置分类;空字符串清除分类。仅允许对自己创建或已加入的技能操作,并且只能使用当前用户的个人分类或平台分类。 |
`GET /api/skills` 可带 `libraryCategoryId` 或 `libraryUncategorized=true`,仅用于 `scope=mine` / `scope=created`,且两者不能同时使用。它们可与共享分类筛选 `tag` 同时提供,结果需同时满足两个筛选条件。分类列表的数量不受搜索词影响,表示对应技能库范围内的完整数量。
技能列表响应中的 `categories` 是技能广场共享分类,包含 `value`、`label` 和 `count`;`count` 统计已启用且公开的技能,不受当前搜索词、分页和视图筛选影响。它与技能库个人/平台分类是两套独立分类。
## 画布工具 API
以下接口均要求登录,使用标准业务信封:
| 方法 | 路径 | 行为 |
| --- | --- | --- |
| GET | `/api/tools` | 分页列表;`scope=public/favorites/custom`,默认 public;可按 `type=style/motion/nine_grid/effect`、tag、search 筛选。page 从 1 开始,pageSize 默认 20、最大 80;返回 tools、totalCount、page、pageSize、hasMore |
| GET | `/api/tools/:id` | 可见且启用的工具详情,包含 prompt 和 extraInfo;列表不含这两个大字段 |
| POST | `/api/tools` | 新建自己的工具;type、label、prompt 必填,visibility 默认 private;预览仅支持本人已上传完成的资源,公开工具暂不接受私人预览 |
| POST / DELETE | `/api/tools/:id/favorite` | 幂等收藏 / 取消收藏,仅允许操作可见且启用工具 |
| DELETE | `/api/tools/:id` | 仅删除自己的用户工具,不能删除内置或他人工具 |
生成提示词工具引用格式为 `@[tool:type:ID:label:icon]`,label 使用 URI 编码。名称和图标只用于展示,不作为授权依据。服务端在任务入队及执行前检查用户可见性、启用状态、工具类型和生成模式;无效、越权、嵌套工具或数据库错误必须失败,不能把未解析 token 发送给模型。风格和九宫格仅用于图片,运镜和特效仅用于视频。入队校验不冻结工具快照:排队期间删除工具将导致执行失败。
## 平台技能策展
新增登录读取 `/api/skills/curation` 与管理员 GET/PUT `/api/admin/skill-curation`;原技能列表仅在策展开启时应用新增分类筛选,响应结构不变。见 [接口与行为](./skill-curation)。
一级分类扩展:管理写入增加 root 命令,assignment.rootId 与子分类完整集合一起保存;列表增加 platformRootId 筛选。关闭模式继续忽略新增筛选。