Merge remote-tracking branch 'upstream/main'

This commit is contained in:
2026-10-10 09:17:28 +08:00
1919 changed files with 239870 additions and 40699 deletions

View File

@@ -0,0 +1,11 @@
# ADR-0009: 媒体产出模型与选用模型分开记录
状态:已接受。日期:2026-09-22。
画布节点上的模型字段同时承担两件事:提示词面板下一次要用的选用模型,以及这份图片、视频或音频实际由哪个模型写成。改下拉并不重新生成时,后一件事会被覆盖,过一段时间无法从节点上认出资产的产出模型。
决定:产出模型在生成成功并写入节点时冻结,选用模型继续随下拉变化。角标、节点详情和版本对比只展示产出模型;没有产出模型时留空。展示名按当前模型目录解析,目录中没有则显示模型 ID。本地加工、上传和本次改动之前的旧节点不回填。
## 考虑过的方案
继续复用同一个模型字段。改动更少,但选用模型一变,产出事实就丢了。否决。

View File

@@ -26,6 +26,12 @@ description: 云端画布 Agent 的行为策略、上下文来源、运行提醒
## 用户目标与参考素材
### 视频首尾帧
`generate_media` 支持可选的 `videoStartFrameNodeId` 和 `videoEndFrameNodeId`。每个非空值必须是 `referenceNodeIds` 中已解析的图片引用,仅用于视频。参考数组决定素材编号,两个帧字段决定用途;在提示词中写“图片1作为首帧”不会自动选择首帧。
只编辑、不生成时,先用 `canvas_apply_ops` 建立图片到视频节点的引用连线,再通过 `update_node.patch` 设置同名字段;空字符串取消选择。节点读取会返回当前帧选择,生成审批也会显示首尾帧节点。生成工具仍需明确传入本次选择,不会根据历史提示词猜测用途;原有权限、素材归属、审批和计费流程不变。
- 新消息只覆盖与之冲突的旧要求。“继续”“可以”仍结合历史理解,不丢弃有效约束;已送达的运行中插话也进入后续可信历史。
- 当前用户请求保留在 user 消息及运行记录里,不复制进 system 提示锁定目标。系统上下文只包含可验证事实,素材标题、摘要和标签编码为 JSON 数据。
- 删除原先通过“参考图”“图生视频”等关键词推断强制引用的逻辑,否定句和概念解释不再被代码标记为引用任务。
@@ -46,6 +52,8 @@ description: 云端画布 Agent 的行为策略、上下文来源、运行提醒
自动回归覆盖新目标继承、插话保留、否定参考需求、消息来源、计划取消、有界催办、恢复事件及 OpenAI Chat/Responses、Claude、Gemini 协议转换。测试使用临时 SQLite 和模拟模型结果,不证明真实模型的创作质量或指令遵循率。
当前系统策略版本为 6、编译器为 `cloud-agent-policy-compiler/v4`。不涉及数据库表结构或 HTTP API 变更。既有冻结合同校验仍生效:旧版本正在执行或等待审批的运行不能由新版服务继续执行;终态历史仍可读取,新消息建立使用新合同的运行。发布前应处理在途运行;回滚同样受版本校验限制,不应改写历史快照来强行恢复。
当前系统策略版本为 13、编译器为 `cloud-agent-policy-compiler/v5`。助手身份由服务端从外观配置读取 `canvas.agentName`,通过执行上下文的 `agentIdentity.name` 提供;字段只表示名称,不能更改行为规则或工具权限。每个新回合重新读取配置,已完成对话的后续回合使用最新名称;身份变化改变规范系统提示及对应缓存键,不覆盖历史消息或冻结快照。
不涉及数据库表结构或 HTTP API 变更。既有冻结合同校验仍生效:旧版本正在执行或等待审批的运行不能由新版服务继续执行;终态历史仍可读取,新消息建立使用新合同的运行。发布前应处理在途运行;回滚同样受版本校验限制,不应改写历史快照来强行恢复。
本轮治理覆盖对话主链路。批量创作表的业务默认词、个人记忆压缩专用提示词属于独立生成入口,尚未迁移到版本化资源;后续应分别明确空提示词语义和结构化输出合同,并建立真实模型场景评估,不能把本轮回归称为全仓提示词治理完成。

View File

@@ -5,6 +5,18 @@ description: 影策后端核心持久化表与状态边界
# 后端数据库
## 分片上传预留
Schema 45 新增 `upload_reservations`:`id`、`user_id`、`size`、`day`、`expires_at`。创建会话时在数据库事务内检查并行会话数、现有资源容量、未到期预留和每日上传用量,更新既有 `user_daily_upload_usages` 后插入预留。SQLite 先取得写锁,PostgreSQL 使用账号级事务 advisory lock。
资源变为 ready 与删除对应预留在同一事务中提交;普通资源上传及生成结果的容量提交也使用同一锁,并计入分片预留。沿用 `UserStoredFileBytes` 的 ready 物理对象去重口径,不改变容量配置。跨 UTC 日期完成时重新预留当天日额度并释放原日额度,失败则整体回滚。
预留有效期 90 分钟。失败或会话删除时释放原日额度;再次创建该账号会话时清理已到期预留。进程重启不批量删除其他实例的预留。分片文件与路由会话仍在本机,需要粘性路由;崩溃后的残片需要部署侧临时目录清理策略,未增加跨实例文件恢复或启动时全局清扫。
## 更新公告配置
外观 JSON 合同版本 10 新增 `updates`:`enabled`、`currentVersion`、`releases`。每个版本保存 `id/version/title/date/blocks`,内容块保存 `id/type/text/resourceId/caption/layout`;公共媒体 URL 由服务端按当前引用生成,不保存客户端 URL。关闭开关保留公告和资源引用,只关闭公共展示。不增加数据库表或 schema 迁移。
## 画布 Agent 外观配置
`system_settings` 的 `appearance` JSON 合同版本 9 新增 `canvas`:助手名称、入口标签、面板标题、欢迎文案、输入提示、`avatarType`(`orb|live2d`)、`avatarHeight`(120–360)、`live2dResourceId` 和服务端确认的 `live2dEntry`。独立于站点品牌;缺失时读取内置画布默认值。不新增数据库结构迁移,不改变全局 schema 版本。
@@ -13,9 +25,17 @@ description: 影策后端核心持久化表与状态边界
后端通过 GORM 管理 SQLite 与 PostgreSQL 表结构。结构变化由 `migrate-schema` 按版本顺序执行并写入 `schema_migrations`;受管生产部署在 Backend 启动前运行独立迁移服务,Backend 只校验当前 Schema 是否与二进制兼容。业务写路径必须通过 service 和 repository 的状态约束,不能直接依赖前端状态。
## 素材资源引用边界
资源引用快照同时读取素材表现的 `resource_id` 和 `metadata_json`(包括嵌套资源字段);不能先按 `resource_id` 过滤表现记录,否则仅在 metadata 中引用的共享资源会漏检。素材批量删除保留其他素材仍引用的资源;管理员删除也复用这一引用边界。
## 结构迁移与版本
当前结构版本为 34。版本 34 `task_media_recovery` 为 `tasks` 增加 `media_recovery_json`(text)和 `media_stage`(最长 24 字)。版本 33 `oauth_state_accepted_terms` 为 `o_auth_states` 增加非空布尔列 `accepted_terms`,默认 `false`,不替历史流程补造同意记录。升级前备份,按既有流程执行 `migrate-schema up` 和 `verify` 后再启动新后端。旧版本二进制不支持版本 34,回退需恢复匹配的数据库备份。
当前结构版本为 42。版本 42 `cloud_agent_pi_sessions` 新增 `cloud_agent_pi_sessions` 表,按运行 ID 保存 Pi 原生 JSONL 会话快照、用户 ID、乐观并发 revision 和更新时间;会话正文由 Agent runtime 管理,Go 只将其作为不透明快照读写,不替代有界控制检查点、事件日志或任务记录。版本 41 `resource_thumbnail` 为 `resources` 增加缩略图元数据。版本 40 `builtin_skill_tombstones` 新增内置技能删除记录,管理员删除平台技能后,后续启动同步不会重新创建该技能;版本 39 `skill_library_categories` 新增 `skill_library_categories` 表,并为 `user_skill_states` 增加 `library_category_id`,保存个人技能库分类及每个用户对技能的归类;分类不改变 `skills.tag` 的技能广场共享分类。版本 38 `prefixed_id_sequence_reconcile` 创建(若缺失)并校准 `id_sequences`:`CHANNEL`、`MODEL`、`PTIER`、`ATTEMPT`、`LMODEL`、`REVISION`、`ROUTE` 按对应业务表的最大有效 ID 前移,包含软删除记录,不回退已有序列、不重写业务 ID。分配新 ID 时再次校准并检查冲突;SQLite → PostgreSQL 迁移完成后也校准序列。
版本 36 `cloud_agent_gemini_cache` 创建 `cloud_agent_gemini_caches`,保存用户范围内的官方 Gemini 缓存元数据;版本 37 `cloud_agent_gemini_cache_identity` 将缓存标识约束修正为 `(user_id, cache_key)`,移除旧的全局 cache_key 唯一索引。版本 35 `auth_notifications` 为认证通知与组合验证补齐 `auth_verifications`、`notification_quotas`、`sms_channels`、`sms_records` 四张表,并为 `users` 增加 `phone`、`email_verified_at`、`phone_verified_at`,为 `email_verification_codes` 增加 `attempts`;同时创建非空手机号唯一索引。版本 34 `task_media_recovery` 为 `tasks` 增加 `media_recovery_json`(text)和 `media_stage`(最长 24 字)。版本 33 `oauth_state_accepted_terms` 为 `o_auth_states` 增加非空布尔列 `accepted_terms`,默认 `false`,不替历史流程补造同意记录。
升级前备份,按既有流程执行 `migrate-schema up` 和 `verify` 后再启动新后端。旧版本二进制不支持版本 42,回退需恢复匹配的数据库备份,并评估备份之后的新写入;不能只切回旧镜像。关闭自动迁移的生产环境必须运行正式迁移;手工改表不能更新 `schema_migrations` 的版本与校验和记录。
### 生成成功后的作品保存恢复
@@ -31,7 +51,7 @@ description: 影策后端核心持久化表与状态边界
| 表 | 用途 | 关键边界 |
| --- | --- | --- |
| `schema_migrations` | 保存已成功应用的结构版本、名称、校验和与时间 | 已应用版本的校验和发生变化时拒绝启动;数据库版本高于当前二进制支持版本时拒绝旧程序连接 |
| `cloud_agent_executions` | 保存 Agent 每轮的持久化执行状态、修订号和运行状态 | `(id, user_id)` 归属受服务端校验;状态通过 revision CAS 更新;`state_json` 包含技能快照、工具调用、审批、活动子任务和事件,不保存浏览器密钥 |
| `cloud_agent_executions` | 保存 Agent 每轮的持久化执行状态、修订号和运行状态 | `(id, user_id)` 归属受服务端校验;状态通过 revision CAS 更新;`state_json` 包含技能快照、工具调用、审批、活动子任务、事件、最近一次语义压缩检查点与"正在压缩"的压缩态,不保存浏览器密钥 |
| `agent_profiles` | 保存用户、项目和画布层的 Agent 常驻偏好、哈希和独立 revision | `(user_id, scope, project_id, canvas_id)` 唯一;归属由 service 校验,写入使用 revision CAS;正文不能授权新能力 |
| `agent_lessons` | 用户私有的 Agent 记忆。Agent 写入待审,用户自己批准后才会注入该用户的会话 | 按 `author_user_id` 隔离;写入机械脱敏;`steps_json` 存路线;`hits` 是主动查阅、`injected` 是自动注入;管理员可按用户查看和删除,不能代批 |
| `agent_memory_settings` | 用户私有的记忆压缩周期、文本模型和最近一次压缩任务状态 | 主键 `user_id`;压缩结果只在 `compact_task_id` 匹配时应用到该用户已批准记忆 |
@@ -129,7 +149,10 @@ docker compose -f docker-compose.deploy.yml up -d backend web
| `skills` | 保存技能公开元数据、当前版本指针、来源和 GitHub 同步状态 | `instruction` 仅保留入口 `SKILL.md` 的兼容投影;目录文件不展开写入该字段 |
| `skill_versions` | 保存每次安装、编辑或同步形成的不可变技能版本 | 记录内容哈希、入口路径、规范化 ZIP 的存储键、文件数、总大小和 GitHub commit |
| `skill_files` | 保存某个版本的文件索引 | `(skill_version_id, path)` 唯一;只保存路径、类型、MIME、大小和 SHA-256,不保存正文 |
| `user_skill_states` | 保存用户的加入、收藏、安装版本和自动更新选择 | 技能正文读取仍经过公开性和归属校验,不能通过文件接口绕过私有技能权限 |
| `skill_library_categories` | 保存个人技能库分类和管理员创建的平台分类 | personal 分类按用户隔离;platform 分类对所有用户可见;分类名在用户可见范围内唯一,删除分类只清除归类引用 |
| `user_skill_states` | 保存用户的加入、收藏、安装版本、自动更新选择及该用户对技能的技能库分类 | `library_category_id` 属于用户技能库元数据,不修改 `skills.tag` 等技能广场共享分类;技能正文读取仍经过公开性和归属校验 |
技能库分类由 schema migration 39 创建/扩展;内置技能删除记录由 schema migration 40 创建。删除分类时在事务内清空对应 `user_skill_states.library_category_id` 后删除分类记录,不会级联删除任何技能。删除内置技能时写入 tombstone,后续启动同步会跳过该技能。
规范化技能包保存在后端数据目录的 `skill-packages/<skill-id>/<version-id>.zip`,数据库只保存索引和版本指针。上传 ZIP 时强制校验 `SKILL.md`、路径越界、软链接、重复路径、文件数量和解压大小;GitHub 技能保存公开仓库、分支、子目录和提交 SHA,后台每 6 小时检查一次开启自动更新的技能,并记录最近检查、同步时间及失败状态。
@@ -282,3 +305,9 @@ Schema 32 为 `channel_models` 新增 `tags`(TEXT、NOT NULL、默认 `'[]'`
- 启动幂等装载 87 个预设(风格 45、运镜 33、九宫格 9),不下载第三方媒体。显式 ID 与用户工具冲突时启动报错,不覆盖用户数据;PostgreSQL 同步自增序列。
- 工具预览纳入资源引用检查;自定义预览必须是当前用户已上传完成的资源。公开工具暂不接受私人预览资源。
- SQLite → PostgreSQL 迁移包含上述两表。首次启动前备份测试数据;回退旧代码不能视为数据库降级,应使用备份恢复,不手工重写迁移记录。
## 可选技能策展
迁移 46 新增 skill_curation_settings、skill_curation_categories、skill_curation_assignments;默认关闭,不导入技能内容。表结构与事务规则见 [平台技能策展](./skill-curation)。
迁移 47 依赖 46,新增 skill_curation_roots 与 skill_curation_root_assignments;保留原五个根 ID,根停用时使用派生默认桶,根表为空回落固定五类。

View File

@@ -7,7 +7,9 @@ description: 助手名称、文案、模型导入和运行库部署
管理员在「站点及外观 → 画布配置」设置助手名称、入口文字、面板标题、欢迎标题、欢迎说明和输入提示。助手名称独立于站点品牌,例如站点为“影策”、助手为“小鱼”。文案中的 `{agentName}` 会替换为助手名称,并作为普通文本渲染。
这些字段只改变展示,不改历史对话、模型提示词、AI 自我介绍、技能或工具权限。运行中插话提示保留原有操作说明。入口文字和欢迎说明允许留空。
助手名称同时用于界面文案和 AI 自我介绍。服务端在每个新回合从同一份外观配置读取名称,将其作为 JSON 名称数据加入内置身份规则;站点品牌、模型供应商、个人设定和历史回复中的旧名称不覆盖它,也不将名称解释为行为指令。
保存后当前页面立即更新,其他已打开页面在重新获得焦点、切回前台或刷新时读取最新配置;刷新失败保留已有名称。新对话和已有对话的后续回合使用新名称,正在执行的回合保留创建时的身份快照,历史消息不重写。其他文案字段仍只改变展示,技能和工具权限不变。运行中插话提示保留原有操作说明。入口文字和欢迎说明允许留空。
## 模型来源与授权

View File

@@ -5,6 +5,12 @@ description: 从产品功能快速定位主要代码入口
# 代码功能地图
## 自定义更新公告
- 后台编辑:`web/src/pages/admin/settings/components/update-announcement-editor.tsx`;版本、排序与草稿校验:`web/src/lib/update-announcement.ts`。
- 统一入口与弹窗:`web/src/components/layout/app-changelog-modal.tsx`、`app-changelog-dialog.tsx`;图文视频展示:`update-announcement-content.tsx`。
- 服务端持久化、校验、公开投影及媒体访问:`backend/internal/app/appearance_updates.go` 与 `appearance.go`;HTTP 边界:`backend/internal/handler/appearance.go`。
## 画布 Agent 配置与 Live2D
- 后台入口:`web/src/pages/admin/settings/appearance-settings-page.tsx` 与 `components/canvas-appearance-editor.tsx`。
@@ -16,7 +22,7 @@ description: 从产品功能快速定位主要代码入口
## 前端页面
云端 Agent:`web/src/components/canvas/canvas-cloud-agent-panel.tsx` 协调对话、Skills 和审批,`canvas-cloud-agent-settings.tsx` 编辑长期偏好与个人记忆,`services/api/agent.ts` 适配运行快照与事件流;画布手工编辑与引用同步共用 `web/src/lib/canvas/canvas-node-prompt.ts`,已有媒体结果只更新 `composerContent` 草稿,不覆盖生成结果和已提交提示词。后端 `handler/agent.go` 负责 HTTP 合同,`app/cloud_agent.go` 负责权限、幂等、可信历史,`app/cloud_agent_profile.go` / `repository/agent_profile.go` 管理用户、项目、画布偏好及 revision CAS,`app/cloud_agent_lessons.go` / `app/cloud_agent_memory_compact.go` / `repository/agent_lesson.go` / `handler/agent_lesson.go` 管理用户私有记忆的写入脱敏、召回注入、用户自审批与压缩优化,`web/src/pages/settings/agent-memory-pane.tsx` 是用户批准/添加/导入导出/压缩入口,`web/src/pages/admin/components/agent-lessons-panel.tsx` 是管理员按用户查看入口,`internal/prompts/agent-*-policy.md` 是随发布物编译、并在文档头声明 ID/版本的行为资源,`app/cloud_agent_policy.go` 编译本轮策略快照,`internal/canvas/capability/` 注册服务端节点能力和字段映射,`app/cloud_agent_canvas_state.go` 投影画布摘要和精读,`app/cloud_agent_runtime.go` 负责持久化模型循环,`app/cloud_agent_tools.go` 根据能力定义生成工具 schema,并从固定技能快照渐进读取 `SKILL.md` / reference,`app/cloud_agent_media.go` 负责收费媒体任务适配,`app/task_creation.go` 执行预算 admission,运行沿用任务 Worker。节点新增需要能力描述符和真实画布实现;新生成模式还需任务适配器,不由偏好或技能 md 授权。协议见 HTTP API 专题,未确认的真实登录态与上游联调见待测试清单。
云端 Agent:`web/src/components/canvas/canvas-cloud-agent-panel.tsx` 协调对话、Skills 和审批,`canvas-cloud-agent-settings.tsx` 编辑长期偏好与个人记忆,`services/api/agent.ts` 适配运行快照与事件流;画布手工编辑与引用同步共用 `web/src/lib/canvas/canvas-node-prompt.ts`,已有媒体结果只更新 `composerContent` 草稿,不覆盖生成结果和已提交提示词。后端 `handler/agent.go` 负责 HTTP 合同,`app/cloud_agent.go` 负责权限、幂等、可信历史,`app/cloud_agent_profile.go` / `repository/agent_profile.go` 管理用户、项目、画布偏好及 revision CAS,`app/cloud_agent_lessons.go` / `app/cloud_agent_memory_compact.go` / `repository/agent_lesson.go` / `handler/agent_lesson.go` 管理用户私有记忆的写入脱敏、召回注入、用户自审批与压缩优化,`web/src/pages/settings/agent-memory-pane.tsx` 是用户批准/添加/导入导出/压缩入口,`web/src/pages/admin/components/agent-lessons-panel.tsx` 是管理员按用户查看入口,`internal/prompts/agent-*-policy.md` 是随发布物编译、并在文档头声明 ID/版本的行为资源,`app/cloud_agent_policy.go` 编译本轮策略快照,`internal/canvas/capability/` 注册服务端节点能力和字段映射,`app/cloud_agent_canvas_state.go` 投影画布摘要和精读,`app/cloud_agent_runtime.go` / `app/cloud_agent_pi_coordinator.go` 负责可恢复编排,原生 Pi runtime 协议适配位于 `internal/agent/runtime/` 与 `agent-runtime/pi/agent-runtime.mjs`;`cloud_agent_pi_sessions` 独立保存原生 JSONL 会话,控制检查点仍由 Go 管理。工具根据能力定义生成 schema,并从固定技能快照渐进读取 `SKILL.md` / reference;`app/cloud_agent_media.go` 负责收费媒体任务适配,`app/task_creation.go` 执行预算 admission,运行沿用任务 Worker。节点新增需要能力描述符和真实画布实现;新生成模式还需任务适配器,不由偏好或技能 md 授权。协议见 HTTP API 专题,未确认的真实登录态与上游联调见待测试清单。
| 功能 | 页面入口 | 主要协作模块 |
| --- | --- | --- |
@@ -43,7 +49,7 @@ description: 从产品功能快速定位主要代码入口
| 职责 | 代码位置 |
| --- | --- |
| 页面装配和跨域动作协调 | `web/src/pages/canvas/project.tsx` |
| 画布领域规则和生成协议 | `web/src/lib/canvas/canvas-project-domain.ts`、`canvas-project-generation.ts` |
| 画布领域规则和生成协议 | `web/src/lib/canvas/canvas-project-domain.ts`、`canvas-project-generation.ts`、`produced-model.ts` |
| 项目恢复、持久化和项目级动作 | `web/src/pages/canvas/use-canvas-project-lifecycle.ts` |
| 云端版本校验与冲突草稿 | `web/src/services/user-data-sync.ts`、`canvas-sync-drafts.ts`、`web/src/lib/canvas/canvas-content.ts`、`web/src/pages/canvas/canvas-sync-status.tsx` |
| 历史树与独立只读预览 | `web/src/pages/canvas/canvas-version-history.tsx`、`canvas-version-preview.tsx`;后端 `internal/canvas/canvas_history.go` 与 `repository/canvas_history.go`,Agent 写入桥接在 `app/canvas_bridge.go` |
@@ -84,6 +90,8 @@ description: 从产品功能快速定位主要代码入口
## 后端调用链
AutoDL 画面规格由 `app/model_capability_screen_spec.go` 读取官方插件工作流合同,校验所选分辨率及默认值;`task_creation.go` 在价格选择之前归一化,任务校验与 `provider_video.go` 再次使用同一规则。前端 `lib/video-screen-specs.ts` 管理插件预设、所选规格与工作流切换,`ModelCapabilityEditor` 复用到系统及自定义渠道;无开关或独立草稿。
```text
HTTP 请求 -> handler -> service(稳定导入面 / 别名)-> app(组合根)-> 业务域包 -> repository/model
|
@@ -107,6 +115,7 @@ HTTP 请求 -> handler -> service(稳定导入面 / 别名)-> app(组合
| `auth` | 登录注册、OAuth、邮件验证码、LibTV/TapNow 导入 | **已抽出**:`internal/auth` |
| `canvas` | 画布公开分享、媒体引用守卫、用户画布/素材库同步 | **已抽出**:`internal/canvas` |
| `assets` | 资源 URL/引用解析、流式交付类型 | **部分抽出**:`internal/assets`;OSS/配额/转码仍在 `app` |
| `agentcontext` | Agent 上下文检查点的编码、解析与压缩提示词(provider 中立,不含触发与调度) | **已抽出**:`internal/agentcontext`;触发、暂停与恢复在 `app/cloud_agent_context_compaction.go` |
组合根原则:`service.New`(委托 `app.New`)组装各域服务;coordinator 不得再持有整个 `*Service` 回灌域包。
@@ -150,3 +159,9 @@ HTTP 请求 -> handler -> service(稳定导入面 / 别名)-> app(组合
- 任务入队 `internal/app/task_creation.go` 与生成执行 `internal/app/provider.go` 共用严格 token 解析。
- 前端 `canvas-tool-presentation.ts` 共享轻量展示配置,`canvas-resource-references.ts` 统一 token;节点提示词与工具面板共用同一持久状态,不复制到 metadata 风格字段。
- `canvas-workspace-panel.tsx` 提供常驻左侧菜单,按需挂载节点、资产、工具、任务和历史面板;资产面板复用素材分页服务与画布插入合同;主分支项目章节侧栏与工作区互斥。
## 技能策展
`internal/skills/skill_curation.go` 负责校验,`internal/repository/skill_curation.go` 负责分类持久化与 CAS 审计交易,handler/app bridge 接入原有技能域。
`skill_curation_roots.go` 扩展动态根维护、有效根查询和空表回落;数据库迁移 47 接续 46。

View File

@@ -5,6 +5,24 @@ description: 信封、错误码与 OpenAPI
# HTTP API 合同
## 注册邮箱等价检查
公开注册与邮箱绑定的唯一性检查将 `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 生成客户端。
@@ -13,6 +31,37 @@ description: 信封、错误码与 OpenAPI
模型 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 命中率及账单收益必须在实际云环境通过浏览器网络记录和服务端流量观测验收。
## 画布资源完整性与 409
画布写入在保存历史快照前校验当前文档中的资源引用;引用须属于当前用户且处于 `ready`。引用收集器递归遍历受支持的资源字段,因此 `nodes[节点 ID].metadata.videoPreview.storageKey` 等嵌套派生封面也会参与检查。历史快照保存会一并校验新版本引用;若资源在保存期间缺失或状态变化,事务回滚,画布 revision 不递增。
HTTP 409 不能一概解释为 revision 冲突:普通版本冲突使用 `reason: conflict`;资源完整性失败使用 `reason: canvas_history_resources_missing`,并在 `details` 中返回 `resourceIds` 和 `missingResources`(含资源 ID、JSON 路径、节点 ID、引用类型及 `source`)。客户端应按 `reason` 分流,不解析 `msg` 猜测原因。显式资源修复写入仅允许历史 preimage 存在缺失资源,不允许新画布仍引用缺失资源,也不绕过 revision CAS。
运维扫描/修复仅处理当前画布中可安全清除的失效 `videoPreview`;不删除主媒体、不扫描并改写所有历史快照,也不替代备份。使用目标数据库对应的新版本二进制,先在维护窗口备份并执行只读扫描:
```sh
CANVAS_BACKEND_DATA_DIR=/明确的数据目录 ./migrate-schema repair-canvas-resources
```
复核 JSON 报告后,只有 `repairable` 项可由明确授权的操作员再次运行 `repair-canvas-resources --apply`。命令要求显式配置 `CANVAS_BACKEND_DATA_DIR` 或 `DATABASE_URL`,逐画布按版本 CAS 保存并保留原始历史;有未解决主媒体/素材绑定问题时以非零退出,不会自动恢复或重传。发生并发编辑时停止并重新扫描,不能用强制覆盖代替恢复有效历史或重新上传。
## 生成成功后的重试保存
`POST /tasks/:id/recover-media` 无需请求体。当前用户只能恢复自己有保存恢复记录的失败任务;重入队仍返回原任务 ID,不请求重新生成、不新建计费订单。已排队、运行或完成时幂等返回当前任务。取消任务、已退款订单、任务/账单归属不一致、项目已删除或归档时拒绝恢复。
@@ -41,18 +90,28 @@ description: 信封、错误码与 OpenAPI
- 物理空间由后台删除队列异步释放,失败保留任务重试;接口成功不代表对象存储已经清理完成。未选中素材使用的资源,以及其他 Resource 行(包括其他用户)复用的物理对象继续保留。
- 旧画布 / 任务 / 历史快照记录本身保留,但被删媒体链接将失效。此操作不可恢复;没有数据库结构迁移。
## 密码登录限流
`POST /auth/login` 保留 IP 总请求限制与十分钟账号请求限制。同一 IP 下,用户名和邮箱解析为同一用户时共享账号计数;不存在的账号仍按去除首尾空白、转小写后的输入计次。错误密码与不存在的账号使用相同的 401 响应,达到限额后均返回 429,不返回限流键或用户身份。计数仍涵盖所有登录请求,成功不清零。
## 注册服务协议
`POST /auth/register` 的 JSON 请求必须携带布尔值 `acceptedTerms: true`,首次创建管理员也适用。缺省、`false` 或 `null` 均按未同意处理,在创建用户、消费验证码和创建会话前返回 HTTP 400,提示“请先同意影策服务协议”。原有注册开放、邮箱白名单、验证码与限流校验继续生效。
`POST /auth/register` 的 JSON 请求必须携带布尔值 `acceptedTerms: true`,首次创建管理员也适用。缺省、`false` 或 `null` 均按未同意处理,在创建用户、消费验证码和创建会话前返回 HTTP 400,提示“请先同意<协议名称>”。原有注册开放、邮箱白名单、验证码与限流校验继续生效。
`GET /auth/linuxdo/start` 可携带 `acceptedTerms=true`,只有该精确值表示同意。服务端将标志写入一次性的 OAuth state;回调仅在新建 Linux.do 用户时要求该 state 已同意,不从回调参数重新取值。已有 Linux.do 用户可从登录页继续登录,无需传此参数;同意协议不绕过注册关闭策略。
注册页的协议名称为《影策服务协议》。正文暂留空,由维护者在 `web/src/pages/auth/register.tsx` 的 `SERVICE_AGREEMENT` 中手动填写正式条款并审核后再发布;占位提示不是正式协议。当前只校验本次注册的同意标志,不保存长期协议版本或签署凭证。前后端需一起更新,旧客户端缺少同意标志时无法注册。
协议名称与正文由管理员在 `PATCH /admin/settings/registration` 配置,公开读取面是 `GET /auth/settings` 的 `agreementTitle`、`agreementContent`。`agreementTitle` 未配置时按当前外观品牌名生成“<品牌名>服务协议”,因此外观改名后注册页文案同步;接口返回的标题不含书名号,由注册页补《》。`agreementContent` 支持多段文本,按空行分段渲染,留空时注册页提示“服务协议内容待补充”。
读取注册配置失败(如 JSON 损坏)时,`RegistrationAgreement` 返回空标题而不是品牌名兜底标题,注册页据此隐藏协议入口并提示可重新读取,避免展示后台并不存在的协议名称;此时错误提示统一回落为“请先同意服务协议”。
`PATCH /admin/settings/registration` 的两个协议字段为可选指针语义:不提交表示保留已保存内容,提交空串表示明确清空。注册开关与协议正文可以分开保存,单独切换 `enabled` 不会覆盖协议。当前只校验本次注册的同意标志,不保存长期协议版本或签署凭证。前后端需一起更新,旧客户端缺少同意标志时无法注册。
## 站点皮肤主按钮
### 画布配置与 Live2D(外观合同版本 9)
### 画布配置、更新公告与 Live2D(外观合同版本 10)
`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 系统提示词。
外观设置新增 `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`:管理员预览,校验资源归属;其他管理员仅能查看当前外观已引用模型。
@@ -137,7 +196,7 @@ Token 报价的 `estimated` 为 `true`,`quantity` 是预留 Token 数。参考
| 接口 | 行为 |
| --- | --- |
| `GET /canvas-projects/:id` | 返回当前内容及权威版本号 |
| `GET /canvas-projects/:id` | 返回当前内容及权威版本号;响应包含基于数据库 `revision` 的弱 `ETag` 和 `Cache-Control: private, no-cache`。请求可用 `If-None-Match` 做条件读取,版本未变化时返回无正文的 `304`。 |
| `GET /canvas-projects/:id/history` | 返回 `snapshots` 摘要与 `currentRevision`,不批量读取历史正文 |
| `GET /canvas-projects/:id/history/:snapshotId` | 按需返回 `snapshot` 摘要和 `project` 历史正文 |
| `POST /canvas-projects/:id/history/:snapshotId/restore` | 请求 `{ revision }` 为当前画布版本,成功返回新版本摘要;恢复前强制备份当前内容 |
@@ -146,6 +205,8 @@ Token 报价的 `estimated` 为 `true`,`quantity` 是预留 Token 数。参考
平移和缩放只持久化浏览器视口,内容 PUT 忽略客户端 `viewport` 和 `remoteContentHash`。前端遇到 409/428 会保留账号隔离的本地草稿、暂停该画布自动提交,并提供加载最新版和下载草稿入口;载入最新版前草稿写入失败时停止替换。其他画布仍可保存。
单画布读取会先查询归属用户的版本元数据,再判断 `If-None-Match`;命中时不会读取大型画布正文。未命中时返回完整项目 JSON。正文达到 1 KiB 且客户端声明接受 gzip 时响应使用 gzip 压缩,并带 `Vary: Accept-Encoding`;因此客户端应以 HTTP 状态码区分 `200` 正文和 `304` 未变化,不要把 `304` 当作业务错误。
云端 Agent 与创作任务的写入共用版本和历史事务;Agent `canvasPatch` 携带 `baseRevision`、`revision`,客户端只接受连续增量,跳过重复事件,缺口或同字段冲突时保留草稿并重新核对。删除、重排或顶层内容变化无法由增量完整表示时,事件携带 `requiresRefresh: true` 且不附 `canvasPatch`,客户端完整核对后才推进基线;撤销事件同样触发核对。保存日志仅记录请求/链路 ID、画布/用户 ID、版本、节点数量变化及 HTTP 状态,不记录正文、提示词或媒体 URL。
## 欢迎页开放配置
@@ -170,9 +231,16 @@ Token 报价的 `estimated` 为 `true`,`quantity` 是预留 Token 数。参考
- 创建和追加消息可选择 `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 字符/层。
- 系统行为策略与媒体默认策略是版本化、编译进后端发布物的资源,修改它们仍需发布后端,不是后台热更新;工具、校验、扣费和审批在代码侧强制执行。画布节点能力由服务端 registry 定义,工具 schema、创建、连线与画布摘要读取共同消费;新生成模式还必须接入媒体任务适配器,不能仅凭 md 或注册节点启用收费操作。
- 系统行为策略与媒体默认策略是版本化、编译进后端发布物的资源,修改它们仍需发布后端,不是后台热更新;工具、校验、扣费和审批在代码侧强制执行。系统行为策略当前版本为 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` 直接写入清单。新一轮的第一次模型请求就会带上继承的清单。模型在没有工具调用且清单仍有未完成项时,服务端会催办并要求继续调用工具;这是软约束,不验证工作是否真的完成,也不会在没有清单时强行催建。
@@ -192,11 +260,14 @@ Token 报价的 `estimated` 为 `true`,`quantity` 是预留 Token 数。参考
- `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`。模型应报告错误,不能自行删减提示词。
- 必须有当前用户已保存的画布。首轮摘要最多取前 80 个节点,并按约 60KB 预算截断;装不下的节点记入 `omittedNodes`,不因摘要过大整轮拒绝。`canvas_get_state` 可按 `offset` 每页读取 40 个节点、资产可引用状态及相关连线,按 `nodeIds` 精读最多 8 个节点(正文每字段最多 16000 字符),通过 `hasMore` / `nextOffset` 和字段 `Truncated` 判断是否完整。分镜表包含镜头、角色节点与资产绑定;摘要每页最多 20 行,精读每次 1 行,可用 `storyboardOffset` 翻页;单行叙事文本累计最多 24000 字符,角色 / 绑定各最多 16 项。上下文不含媒体 URL、任意元数据或未同步修改;节点名称不等于图像视觉识别。普通任务重试接口不能重试 Agent 任务。
- 必须有当前用户已保存的画布。系统提示里的 `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。`cleanupPending: true` 时即使运行已进入终态也会继续推送,直到后台收尾完成。前端应以快照恢复状态,代理只对这一精确路径关闭缓冲。
- `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`。
@@ -286,6 +357,7 @@ CSV 使用中文表头、UTF-8 BOM、CRLF;金额按整数分精确转换为两
| 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` | 上游失败 |
@@ -297,7 +369,22 @@ CSV 使用中文表头、UTF-8 BOM、CRLF;金额按整数分精确转换为两
模型错误把已有字符串码放进 `reason`(如 `model_capability_not_supported`),`code` 仍为 400。
常量:后端 `service/error_codes.go`,前端 `web/src/services/api/error-codes.ts`。
常量:后端标准原因定义在 `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
@@ -312,3 +399,9 @@ CSV 使用中文表头、UTF-8 BOM、CRLF;金额按整数分精确转换为两
| 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 筛选。关闭模式继续忽略新增筛选。

View File

@@ -0,0 +1,43 @@
---
title: 平台技能策展
description: 可选的后台一级分类和子分类维护
---
平台策展模式在“后台 → 技能分类”中开启,默认关闭。关闭时原有 `/skills` JSON、固定五类、个人技能库分类保持不变,新增筛选参数被忽略。分类设置可以在开启前准备。
管理员可以新增、改名、排序、停用子分类,并给公开且启用的技能设置同根的多个子分类。停用不删除指派或个人加入状态,恢复后重新生效。技能 tag 改变时旧跨根指派不再生效;删除技能同时清理指派。无有效指派的技能进入“未细分”。
## API
- `GET /api/skills/curation`:登录后读取分类及公开有效技能指派;关闭时返回空集合。
- `GET /api/admin/skill-curation`:管理员读取设置、revision、含停用的子分类及公开技能指派。
- `PUT /api/admin/skill-curation`:管理员提交 `expectedRevision`,及 `enabled`、完整 `category`、完整 `assignment` 三者之一。category 含 id(新建为空)、rootTag、name、sortOrder、enabled。assignment 含 skillId、categoryIds;空数组清除,null 拒绝。
- `GET /api/skills` 新增 `platformCategoryId` 或 `platformUncategorized=true`,仅开启时生效;与原有 scope、搜索、排序、个人分类组合,分页前筛选,不改变响应字段。
写请求上限 64 KiB、单技能最多 32 类;拒绝未知字段、重复指派、跨根、私人技能、过期版本。409 后重新加载核对修改。管理员身份在业务层和事务内检查,revision CAS、修改和 AdminAuditEvent 同事务。审计只记录分类元数据,不记录技能正文。分类不是使用技能的授权。
## 数据库
迁移 46 `skill_curation`,checksum `sha256:skill-curation-v46-20261005`:
| 表 | 用途 |
| --- | --- |
| skill_curation_settings | 单例 id=1,enabled 默认 false,revision 用于并发写入 |
| skill_curation_categories | 稳定 id、root_tag、名称及规范化名称、sort_order、enabled;同根名称唯一 |
| skill_curation_assignments | skill_id + category_id 联合主键 |
迁移只建表及默认关闭的设置,不导入技能或分类内容。44、45 由独立变更保留;完整迁移程序支持缺号补跑。停用开关不是数据库降版;回退旧程序需要匹配的数据库备份。
## 待验证
SQLite 及 HTTP recorder 可在无网络环境验证。PostgreSQL、前端构建、浏览器中的明暗主题、键盘操作和分页交互需要单独验证。
## 一级分类维护
迁移 47 `skill_curation_roots`,checksum `sha256:skill-curation-roots-v47-20261005`,依赖迁移 46。新增 `skill_curation_roots`(id、name、normalized_name 唯一索引、icon_key、sort_order、enabled)及 `skill_curation_root_assignments`(skill_id 主键、root_id 索引)。仅创建原目录的五个中性根,保持 U39 的 rootTag 引用,不导入任何技能指派。
管理员在同一页面维护一级分类;PUT 增加 `root` 命令,含 id(新建为空)、name、iconKey、sortOrder、enabled。图示只接受空值或 shapes/film/shopping-bag/megaphone/landmark/palette/users/wrench,客户端未知值使用 Shapes。根表为空时回落原五类;第一次编辑时将回落值保存入表。读取或数据库错误不冒充空表。
`assignment` 可带 `rootId`,空字符串清除指定;改根必须同时提交完整 categoryIds,跨根则整笔回滚。原 tag、技能内容、版本和安装状态不变。未指定时按原 tag 对应稳定根 ID;指定优先,目标停用或不存在时进入派生桶 `__unassigned__`(未归入启用分类),此桶不可编辑或指派。根停用不删除子分类、指派或已挂载技能。
策展快照开启时增加 roots/rootAssignments;公开读取隐藏停用根及其子分类,不公开私人技能指派。`GET /api/skills?platformRootId=...` 在原有可见性和分页查询前筛选;关闭时忽略该参数,原响应结构和五类不变。后台界面和市场/技能库根导览、分组、图示均读取同一快照。场景预设来源及 scene 仍不变。

View File

@@ -19,7 +19,7 @@ description: Host Updater、Release 检查、数据库备份迁移、健康验
4. 拉取目标 backend/web 镜像并要求存在仓库 `sha256` 摘要。任何失败都在停服前中止。
5. 停止 web/backend,等待 Backend 的 SIGTERM drain 完成;PostgreSQL 和 Redis 保持运行。
6. 用目标 backend 镜像独立执行 `migrate-schema up`。整批迁移在事务和 PostgreSQL advisory lock 内完成,迁移名称与 checksum 必须匹配。
7. 原子更新 `CANVAS_IMAGE_TAG` 和 Compose 文件,启动目标版本。
7. 解析目标 backend/web 镜像的仓库 digest,原子更新 `CANVAS_IMAGE_TAG`、`CANVAS_BACKEND_IMAGE`、`CANVAS_WEB_IMAGE` 和 Compose 文件,启动目标版本;运行容器始终使用 `image@sha256:...`,不使用 `latest`。
8. 连续检查 `/api/health/ready` 的数据库、运行时、schema 和 build version,并经过稳定窗口后才标记成功。
更新状态写入 `/var/lib/open-ai-canvas-updater/state.json`。Backend 或浏览器在切换期间断线不会终止宿主机操作,恢复后可继续读取日志。
@@ -30,20 +30,27 @@ description: Host Updater、Release 检查、数据库备份迁移、健康验
1. 停止目标 web/backend;
2. 恢复更新前保存的 Compose 文件;
3. 再次校验备份 ZIP 和 SHA-256;
4. 使用 `pg_restore --clean --if-exists` 恢复 PostgreSQL;
5. 恢复旧 `CANVAS_IMAGE_TAG`,启动旧服务并重新进行稳定窗口验证。
3. 恢复更新前保存的 `.env`,确保 backend/web 镜像仍绑定到原 digest;
4. 再次校验备份 ZIP 和 SHA-256;
5. 使用 `pg_restore --clean --if-exists` 恢复 PostgreSQL;
6. 启动旧服务并重新进行稳定窗口验证。
回退任一步失败时状态变为 `manual_intervention`,不伪造成功。管理后台仅在存在旧版本和已校验备份时开放人工回退按钮。
## 安装与升级约束
GHCR 镜像部署必须把 `CANVAS_IMAGE_TAG` 固定到具体 Release,不可使用 `latest`。安装更新器:
GHCR 镜像部署必须把 `CANVAS_IMAGE_TAG` 固定到具体 Release,并把 `CANVAS_BACKEND_IMAGE`、`CANVAS_WEB_IMAGE` 固定到对应 GHCR digest,不可使用 `latest` 或仅使用可变 tag。首次安装时显式指定 Release:
```bash
curl -fsSL https://raw.githubusercontent.com/ddcat-ai/open-ai-canvas/main/scripts/install-server-image.sh \
| sudo env CANVAS_IMAGE_TAG=v1.5.7.1 bash
```
已有部署只安装更新器时,先确认 `.env` 已包含两个 `@sha256:...` 镜像引用,再执行:
```bash
cd /opt/open-ai-canvas
curl -fsSL https://raw.githubusercontent.com/ddcat-ai/open-ai-canvas/main/scripts/install-host-updater.sh | sudo bash
sudo docker compose --env-file .env -f docker-compose.deploy.yml up -d --force-recreate backend web --wait
```
脚本会下载与当前 Release 匹配的 Linux amd64/arm64 二进制和 `SHA256SUMS`,生成随机 `CANVAS_UPDATER_TOKEN`,创建 systemd 服务、Socket 目录、状态目录和 `backups/`。Token 只保存在服务器 `.env` 与 root-only 更新器环境文件中。
@@ -59,7 +66,7 @@ sudo docker compose --env-file .env -f docker-compose.deploy.yml up -d --force-r
3. 前端 Bun 构建阶段固定到 `BUILDPLATFORM`,静态资源只生成一次,再放入 AMD64/ARM64 nginx 镜像。Bun 与 `web/package.json`、质量检查保持 `1.3.9` 一致。
4. 先推送 `sha-<完整提交>-amd64/arm64` 后端镜像及 `sha-<完整提交>-multi` 前端镜像,以摘要 artifact 传递结果。两种后端架构和前端全部成功后,才从摘要合并发布 manifest,不用可变标签作为合并源。
5. 同一 SHA 的 main/tag 发布串行运行;后到的一轮仍经过质量门禁,然后复用已有 SHA 镜像,不重复编译。只有明确的镜像不存在可以转入构建,鉴权、限流、网络和异常摘要必须报错。
6. 发布保留完整 SHA 和短 SHA 标签;只有版本 tag 推送写入具体版本标签。`main` 只有在仍是远端最新提交时才更新 `latest`,旧提交的慢任务不能覆盖新版本。正式部署仍应固定版本标签,不依赖 `latest`。
6. 发布保留完整 SHA 和短 SHA 标签;只有版本 tag 推送写入具体版本标签。`main` 只有在仍是远端最新提交时才更新 `latest`,旧提交的慢任务不能覆盖新版本。正式部署由安装器和更新器解析并落盘 digest,不依赖 `latest`。
Go 检查显式使用 `backend/go.sum` 作为缓存依赖键,并上传 `go-test-timings`(JSONL),可按 `Test` / `Elapsed` 定位慢测试。Docker 的后端模块下载保留在普通镜像层,编译步骤可以直接读取;支付插件只按实际 import 下载依赖,不重复预取完整后端模块图。版本参数下移到编译入口。GHA 构建缓存按镜像和架构隔离,避免并行任务共用同一 scope。`go-build` cache mount 只用于同一个 builder 内的复用,当前没有额外的跨 Runner mount 导出方案;不能把它当作持久编译缓存。冷缓存首轮和缓存上传仍可能较慢,需要比较连续两轮的实际耗时。

View File

@@ -5,6 +5,10 @@ description: 热点回源保护、并发租约、故障边界和上线验收
# 任务缓存与队列稳定性
## 排队后的自定义渠道检查
worker 在准备模型路由前重新读取自定义渠道开关,覆盖首次排队和失败后重新入队。排队期间关闭开关的任务以“自定义渠道”不可用的明确错误进入失败终态,不调用渠道;未发出的预扣沿用已有失败退款路径。已接受、提交不明或原本待核对的订单仍进入既有待核对路径,不将可能已发生的费用自动退回。已生成作品的媒体保存恢复不重新生成,不受此入口阻断。
## 详情读取和日志筛选边界
任务中心打开详情只读取任务和日志,不在该路径调用画布结果同步。后台同步遇到短剧领域项目任务(`domainProjectId` 等于任务 `projectId`)或没有生成节点关联时跳过画布写回,避免把短剧项目 ID 当作画布 ID 查询。

View File

@@ -0,0 +1,18 @@
---
title: 自定义更新公告
description: 站点更新公告的编辑、开关、媒体与同步
---
# 自定义更新公告
在「站点及外观 → 更新公告」编辑公告,点击页面顶部「保存修改」统一保存。开关默认关闭,此时工作台与管理后台入口展示原有内置更新日志及真实构建版本;开启后,两处入口和公告弹窗采用自定义当前版本与公告内容。后台「系统更新」继续使用真实程序版本和官方 Release。
支持最多 30 个版本;每个版本有版本号、可选标题与发布日期,以及最多 50 个可排序的文字、图片、视频内容块。文字支持换行、列表和 Markdown 加粗,原始 HTML 不执行。内容块可选择整行或半行,相邻半行并排,窄屏自动单列。编辑区域提供即时预览,视频手动播放。
图片支持 PNG、JPEG、WebP,最大 10MB;视频支持 MP4、WebM,最大 256MB。媒体使用现有平台资源存储,管理员身份、文件真实 MIME 和新资源归属均由服务端校验。未保存媒体只能由上传管理员预览;已保存媒体可由其他管理员预览;公开读取仅允许当前已启用公告引用的媒体,支持视频 Range 请求。关闭开关保留全部自定义内容及素材引用保护,公共外观响应不返回关闭的公告正文。
当前版本必须对应已添加的版本,版本号不能重复,空文字或未上传媒体不能提交;正文总大小最多 200KB,外观 PATCH 请求上限 512KB。保存成功后同步本页公共状态,其他页面在激活、聚焦或打开更新日志时重新读取;读取失败保留已有状态,过期响应不能覆盖较新的保存结果。
配置保存在现有 `system_settings` 的 `appearance` JSON 的 `updates` 中,外观合同版本为 10;不增加数据表或数据库迁移。恢复默认外观会清空自定义公告并关闭开关,恢复原日志,已上传媒体仍保留在存储资源中。
媒体接口:`POST /admin/settings/appearance/assets/update-image|update-video`;管理员预览 `GET /admin/settings/appearance/updates/:id`;公开读取 `GET /public/appearance/updates/:id`。公开媒体不接受任意外部 URL,只从当前公告引用的资源生成访问地址。

View File

@@ -5,9 +5,41 @@ description: 当前项目已实现的核心工作台能力与使用入口
# 功能清单
## 卡藏 Midjourney
画布接收一次任务返回的全部图片,四张结果自动组成收起的图片堆叠,可展开、选择主图和分别使用。每张图保留独立资源及素材绑定,保存、刷新和任务重复同步不会将后续图片覆盖为第一张。已有仅显示首图的成功 Midjourney 节点在重新打开画布时从原任务恢复遗漏结果,无需再次生成。
`kacang-midjourney` 统一声明式插件提供 V6/V8 特惠、V7 特惠、稳定/高速三个生图协议。选择协议后自动填入供应商比例、默认比例、参考图上限和一次提交;禁用无合同的蒙版和通用输出参数。稳定版数值 quality 通过插件命名空间设置,不与宿主分辨率档混用。
特惠使用标准图片端点,稳定/高速使用受理和任务查询端点。结果支持顶层与 data 信封,全部单图及可选四宫格封面交给宿主立即保存;失败和空结果保留真实失败语义。完整模型和字段合同见插件包内 `docs/interface.md`。
## 沧元 Midjourney
`cangyuan-midjourney` 声明式插件分别提供 V7 与 1K/2K(供应商默认 V8.2)协议。V7 使用 `midjourney-v7`;1K/2K 使用 `midjourney-1k`、`midjourney-2k`,可在提示词显式写 `--v 8.2`。一次提交固定 `n=1`,全部结果交由宿主下载保存,参考图按供应商合同发送 HTTPS URL 字符串。
两条线路的参数、编辑端点和响应格式分别适配,不自动换模型。1K/2K 返回官网 CDN 原链,HTTP 403 会如实保留保存失败与原任务重试能力;插件不能消除供应商 CDN 的访问限制。完整字段与接口合同见插件包内 README 和 `docs/interface.md`。
创作页按整个任务一次消费返回图片,保留各图片的输出序号;一次请求返回多张时,素材与消息幂等去重不会再把后续结果当作第一张合并。
后台选择这两个协议时,“能力与参数”自动使用线路专属默认值:V7 为 4000 字符、五张参考图、不支持蒙版;V8.2 默认单张编辑源图、支持蒙版和十五种比例。两者默认自动尺寸、一次提交,关闭不支持的质量、透明背景、响应格式和输出格式选项。V8.2 在模型名未明确时暂用 1K,识别到上游 `midjourney-2k` 后改为 2K,切换档位保留管理员的参考限制、默认比例和价格。像素预设由宿主换算供页面展示,提交上游仍使用比例,实际尺寸由供应商决定。
## 自定义更新公告
「站点及外观 → 更新公告」提供开关、当前版本、多版本文字/图片/视频编辑、内容排序、整行/半行布局与即时预览。关闭展示原日志,开启展示自定义内容,保存后同步工作台和后台入口;后台系统更新保留真实构建版本。详见 [自定义更新公告](/docs/backend/update-announcements)。
## AutoDL 画面规格
AutoDL 视频模型在“能力与参数 → 画面规格”直接多选当前插件工作流声明的完整分辨率,并设置默认值;配置始终可编辑并生效。卡片沿用原有标题、副标题与字段样式,不再显示自定义开关或额外说明。AutoDL ComfyUI 视频与音频 v2.3.0 通过完整分辨率标签控制横、竖、方形,不显示未声明的独立比例控件。时长、音频与参考素材配置继续各自生效。
后台保存、用户模型目录、画布参数归一化、计费规格选择、服务端校验及插件请求使用同一套所选规格。仅允许选择插件预设,不能输入新标签;至少选一项,默认值必须属于所选列表。价格档可保留全部插件预设的价格,但不会扩大实际允许生成的分辨率范围。
## 画布媒体产出模型
图片、视频、音频在生成成功并写入节点后,会冻结当时提交的产出模型。提示词面板里的模型下拉仍是下一次要用的选用模型,改下拉不会改掉已经生成的内容。画布外观中的「媒体信息」打开后,有内容的媒体节点在左下角显示产出模型;图片右下角继续显示尺寸和体积。上传、粘贴、截取、合并、提取音频和本地加工不记录产出模型。本次改动之前的节点不回填。分享页和版本预览画布不显示这块信息。
## 画布 Agent 名称、文案与形象
「站点及外观 → 画布配置」提供独立助手名称、入口文字、面板标题和欢迎/输入文案。支持导入授权的 Live2D Cubism 3/4 运行时 ZIP,预览后保存启用、调整高度、切回默认动态球。加载失败保留可点击的默认入口,上传和资源访问具有管理员校验和引用保护。Cubism Core 由部署者提供,真实播放待验收;不含图片自动建模或 AI 自我介绍同步。详见 [画布 Agent 外观与 Live2D](/docs/backend/canvas-agent-appearance)。
「站点及外观 → 画布配置」提供独立助手名称、入口文字、面板标题和欢迎/输入文案。助手名称同步用于界面和后续对话的自我介绍;名称只是数据,不改变工具权限,历史消息保留原文。支持导入授权的 Live2D Cubism 3/4 运行时 ZIP,预览后保存启用、调整高度、切回默认动态球。加载失败保留可点击的默认入口,上传和资源访问具有管理员校验和引用保护。Cubism Core 由部署者提供,真实播放待验收;不含图片自动建模。详见 [画布 Agent 外观与 Live2D](/docs/backend/canvas-agent-appearance)。
## 素材库分类与画布媒体链路
@@ -83,6 +115,14 @@ description: 当前项目已实现的核心工作台能力与使用入口
云端画布 Agent 已分离行为策略、用户目标、参考事实和运行状态。续聊保留已送达插话,不再继承代码预设的创作自由;旧待办只作上下文,可移除取消项,催办不会强制工具调用或无限重复。提示词来源与发布约束见[画布 Agent 提示词与用户意图](/docs/backend/agent-prompt-policy),真实模型行为仍待场景验收。
## 导演台 3D-BOX 核心 MVP(待真实页面验收)
导演台默认进入“快速镜头”工作流:演员、白模姿态、景别、运镜、当前视图设机位和预演入口集中在同一条路径;完整摆场、骨骼、动画和时间轴能力保留在“高级工作台”。“生成预演”沿用真实 Three.js 视口录制白模视频,并把产物回写为画布视频节点。画布 Agent 新增 `director_scene_read` 安全摘要读取和 `director_preview` 预演请求,后端不返回模型 URL / storage key,浏览器导演台执行实际录制。该版本是自研的 3D-BOX 核心闭环,不复制第三方实现;复杂场景编辑、Agent 语义写入和多镜头剪辑仍在后续阶段。
## 技能库个人分类与画布 Agent 筛选
技能库保留原有头部和技能广场共享分类;在“我的技能 / 我创建的”视图增加左侧个人技能库分类及技能数量。用户可为已加入或自建技能创建、分配个人分类;管理员也可创建对所有用户可见、可选、可用于筛选的分类。个人分类仅影响当前用户的技能库和画布 Agent,不改动技能广场共享分类。删除分类只解除相关归类,技能仍保留。画布 Agent 的“我的技能 / 已启用”列表可按个人或平台技能库分类快速筛选,市场页仍按共享分类浏览。
## 画布保存保护与版本记录
旧页面或旧缓存不能覆盖云端新内容。每次内容保存核对服务端版本,发生冲突会停止该画布自动提交并保留本地草稿;平移、缩放和打开画布不触发整份内容保存。画布顶部显示保存状态,可保留草稿后加载云端最新版。
@@ -408,3 +448,9 @@ SEO 标题、描述、可选关键词会投影到浏览器 metadata、Open Graph
## 画布生成工具(待本地验收)
画布支持风格、运镜、九宫格提示词工具及自定义特效工具,提供收藏和私有自定义工具。工具仅加入提示词或创建待生成子节点,选择工具不自动发起生成。画布左侧提供主页、折叠、节点、资产、任务和历史菜单,不再提供独立工具菜单,展开面板沿用主分支主题。资产支持分类、搜索、分页和直接插入,复用现有素材服务及插入合同;任务和历史独立,历史明确展示最近 30 条。图片节点直接展示风格和预设,不再展示提示词模板入口;其他节点的提示词模板保持不变。
## 平台技能策展
后台可选开启子分类维护,市场和技能库支持子分类浏览;默认关闭,保留原行为。
开启时一级分类也可在后台新增、改名、排序、停用和选择图示,未归入技能仍有默认入口。

View File

@@ -5,6 +5,167 @@ description: 当前项目已实现功能的手工验证清单与未确认边界
# 待测试
## 复制生图节点后节点消失
- 已修复:副本不再继承原节点的 `generationEffectKeys`,避免普通保存把它误判为未确认的生成结果。图片内容、提示词和参考字段仍保留,原节点不受影响。
- [x] 回归测试在修改前复现副本无法保存;修改后覆盖复制、普通保存、新生成结果提交及重新读取。
- [ ] 在真实画布中通过复制粘贴和“创建副本”分别验证,生成完成并刷新后副本仍保留。已丢失的副本不会自动恢复,也不会自动重试生图。
## Agent 视频首尾帧选择
- 已实现:生成工具支持明确指定首尾帧,视频节点编辑支持设置和取消已连接的图片;画布读取和生成审批展示当前选择。
- [x] 专项测试覆盖工具字段、视频模式和图片引用校验、编辑与取消、禁止其他节点类型修改帧字段。
- [x] PostgreSQL 独立测试区验证帧选择进入任务输入和持久化草稿,协议按明确字段识别首尾帧,不依赖参考数组顺序;未调用外部模型,测试区已删除。
- [ ] 开启新的 Agent 对话,确认 Agent 能设置下方首尾帧选项,刷新后仍保留;用户确认后再批准真实视频生成。
## 上传内容与附件交付
- [ ] 运行 `TestUploadContentImportSocket` 验证 URL 导入与重试;在可解析外部存储域名的环境复测 `TestResolveAccessPolicyMatrix`。
- [ ] PostgreSQL 上传重试、真实 OSS 附件代理与浏览器附件下载验收。
## 排队后的自定义渠道检查
- [ ] PostgreSQL 环境验证排队期间关闭自定义渠道:首次与重试任务均停止调用,未发出订单退款,已提交或待核对订单保持待核对。
## Agent 失败消息合并与展示
- 已修复:同一运行的 `run_failed` / `error` 与失败状态快照共用一条错误消息;以公开失败原因补全内容,重放、乱序和空原因不覆盖已有详细说明。不同运行即使错误文本相同也分别保留,工具失败与连接错误不合并。
- 已调整:错误提示使用紧凑中性表面和状态图标,不重复渲染相同的标题与正文;诊断号放入默认折叠、支持键盘展开的「诊断信息」。旧对话中带相同运行 ID 的事件错误与终态提示只展示一次。
- [x] 自动专项测试通过:8 个 Agent 错误/对话相关测试文件共 77 项通过、0 失败(304 个断言);前端 `tsc --noEmit` 与本次文件 Prettier 检查通过。
- [x] 已在本地真实登录画布的历史失败对话中核对:同一失败只显示一张错误卡,模型调用失败原因仍可见;展开「诊断信息」可看到诊断号,未触发新的模型生成或写入操作。
- [ ] 明暗画布、窄面板与长错误文本的可读性,以及诊断展开、键盘焦点和历史滚动体验。
## 画布 Midjourney 四图堆叠
- 已修复:画布按一次任务的真实输出数组保留全部图片,以现有图片批次机制默认堆叠,每张图保留资源和素材绑定;单图仍为单节点。
- 已实现:稳定子节点 ID 和整批持久化,任务重放保留用户移动、改名及其他画布内容;成功的旧 Midjourney 首图节点可从原任务补齐。
- [ ] 用户验收四图默认堆叠、展开、切换主图、保存和刷新后的完整结果。
## 卡藏 Midjourney
- 已实现:三个声明式生图协议、公开模型与参考图限制校验、稳定版参数默认映射、受理和任务查询、顶层/data 信封解析、所有单图及可选四宫格保存。
- 已实现:选择协议自动填入默认能力;特惠 16:9、稳定/高速 9:16;参考图分别 1/5 张,固定一次提交,无蒙版或未声明的分辨率参数。
- [ ] 用户验收默认参数、真实特惠与稳定版任务、图片保存和刷新后的完整显示。
- [ ] 多参考图、Raw/数值画质与风格扩展参数的供应商实测,以及全部版本的真实生成;不以两条文生图验收替代这些边界。
## 沧元 Midjourney V7 与 V8.2
- 已实现:独立声明式插件及 V7、1K/2K Provider,固定一次提交、URL 响应、全部结果提取;参考图、编辑、蒙版、速度及模型名按各线路合同校验。
- [x] 协议专项与完整 `internal/protocol` 测试通过,覆盖多图响应、查询端点、错误、非法模型与媒体输入,以及包内文档合同一致性。
- [x] V7 实际任务完成,四张 1456×816 PNG 均已写入 OSS。一次请求返回多图不增加生成次数;积分价格由部署者配置,不代表供应商费用。
- 已修复:创作页按完整任务消费多图结果,保持真实 outputIndex,避免逐图投影为 outputIndex=0 后被素材及消息幂等去重合并。
- 已实现:选择 V7/V8.2 协议自动载入专属能力默认值;V8.2 空模型名暂用 1K,上游模型改为 `midjourney-2k` 自动切换预设档位并保留其他参数和价格。
- [ ] 用户验收协议选择、1K/2K 档位联动、保存并重新打开的能力参数,以及重新提交后的创作页四图显示。
- [ ] 用户验收原提示词的 V7 生成效果、全部图片显示和刷新后的保存结果。
- [ ] 1K/2K 实际生成与保存:官网 CDN HTTP 403 已确认,需供应商提供可下载地址或解决访问限制后验证;协议测试不替代该线路实际保存验收。
- [ ] 参考图、蒙版、style/moodboard 及 fast 档的上游执行验收;专项测试仅验证请求合同。
## 自定义更新公告
- 已实现:站点及外观新增更新公告页签,开关控制原日志/自定义公告;多版本文字、图片和视频可排序并选择整行/半行布局,保存同步公共外观状态。
- [ ] 用户验收实际发布内容、版本排序、图片比例和视频播放效果;关闭并重新启用确认保存的自定义内容仍保留。
## 画布助手身份与名称同步
- 已实现:后台助手名称与内置身份共用外观配置,新对话及已有对话的后续回合采用最新名称;历史消息和运行快照保留,工具权限不变。
- 专项覆盖:保存名称进入规范提示词、续聊改名、品牌独立、名称 JSON 编码、缓存键变化、历史保留、配置读取失败及前端刷新失败保留旧值。
- [ ] 用户验收在「站点及外观 → 画布配置」改名保存,切回画布后新开或继续对话询问身份,与界面名称一致。
## AutoDL 画面规格简化
- 已实现:去除画面规格开关与额外说明,保留原标题、副标题和字段样式;完整分辨率多选与默认值始终可编辑、生效,仅允许当前插件工作流预设,删除旧草稿和开关分支。
- 专项覆盖:所选列表与默认值保存恢复、画布过期选择、真实组件渲染;后端插件预设校验、保存与目录、价格范围、任务校验和声明式请求传参。
- [ ] 用户验收真实页面多选增删、默认值联动、保存与重新打开,确认仅显示六种插件预设及所选值生效。
- [ ] 用户验收实际 AutoDL GPU 生成,确认选定完整标签对应横、竖和方形输出;专项测试不替代付费上游执行。
## 素材彻底删除恢复不拦截、CI 提速与大文件拆分
- 已恢复:`DELETE /assets/:id`、批量删除、清空回收站、回收站素材删除与过期自动清理不再被画布、任务、项目或画布历史快照引用拦截(9 月 21 日 dc1ad680 的契约,9 月 24 日 5b382726 曾误收紧)。事务移除对应历史资源索引,物理对象经 Outbox 异步删除;其他素材复用的资源继续保留。与 `http-api.mdx`、`openapi.yaml` 及删除确认弹窗文案一致。
- 已修复:审批后的媒体等待在服务停机被取消时,不再继续回写与恢复运行时;`MediaTaskID` 保留,重启后重新挂上等待。
- CI 提速:每次构造 Service 都会完整解压 85MB 官方插件包(每次约 4 秒,1000 余个用例累计 7 分钟以上)。现启动只读清单、进程内缓存官方包读取结果、支付运行目录只解压本机平台的可执行文件;`internal/app` 本地由约 488 秒降到约 144 秒。发布流程的镜像构建与质量检查并行(可变标签推广仍等待检查通过),PR 连续推送取消旧检查。
- 拆分:`cloud_agent_runtime.go`、`cloud_agent_tools.go`、`cloud_agent_pi_coordinator.go`、`workflow_provider.go`、`repository.go`、Agent 面板与输入区按职责拆成多个文件,逐声明校验为纯移动;新文件带职责说明。
- [x] 后端 `go vet ./...`、`go test ./...` 全部通过(依赖 Postgres/Redis 的 7 个用例本地跳过,CI 会执行);新增取消等待、插件包按需解压与本机平台产物回归用例。
- [x] 前端 `tsc`、lint、改动文件格式检查与 2103 项测试通过。
- [ ] 真实页面:删除被画布引用的素材应直接成功;画布再次打开时断链由修复流程处理;共享给其他素材的文件仍可访问。
- [ ] 推送后确认 GitHub Actions 后端检查耗时下降,镜像构建与检查并行、检查失败时不会推广 latest / 版本标签。
## 后端统一日志与降噪
- 已实现:新增 `backend/internal/logging`,统一接管 slog、标准库 `log`、GORM 与 Gin 访问日志,级别与格式由 `CANVAS_LOG_LEVEL` / `CANVAS_LOG_FORMAT` 控制。`info` 下不再输出成功的查询类请求、成功 SQL、`[Agent] message_end` 等流式事件及常规画布自动保存;4xx/5xx、慢请求、慢 SQL、SQL 失败和 panic 仍会记录,SQL 不带参数值。
- [x] `go build ./...`、`go test ./...` 与 `internal/logging` 单元测试通过。
- [ ] 部署到服务器后观察一段时间的日志量,确认没有遗漏的高频输出;临时切换 `CANVAS_LOG_LEVEL=debug` 验证 Agent 事件与查询请求可以重新看到,再切回 `info`。
- [ ] 若接入日志采集,验证 `CANVAS_LOG_FORMAT=json` 的字段(`request_id`、`status`、`latency_ms`)可被正确解析。
## 模型选择弹窗长描述与视口边界
- 已调整:共用 `ModelPicker` 的宽度由共享样式统一控制,移除按内容撑宽及触发器最小宽度约束;渠道描述支持换行,字号统一使用说明文字 token(12px),行高为 1.5,移除旧 9px / 10px 覆盖;模型名称与价格在空间不足时分行显示,长价格文本可换行。
- 调用方排查:画布与创作、短剧新建/章节提取/分镜制作/项目默认模型、Agent 对话与设置、提示词优化、批量生成、图片编辑/蒙版/图层拆解及视频分段等入口复用同一组件,均继承修复。
- [x] 现有模型分组、价格展示、共享样式、Agent 和分镜选择器共 20 项专项测试通过;不代表浏览器布局验收通过。
- [ ] 真实页面验证 500 字中英文描述、连续无空格文本、长价格、禁用原因与无描述选项;检查 1440px / 768px / 390px 视口、明暗主题、靠近浏览器左右边缘的弹窗、键盘切换和关闭。本次未完成浏览器验收。
## 分镜视频预览画幅
- 已调整:分镜制作右侧的视频播放器使用媒体原始比例和完整画面适配,避免共享预览样式的固定 16:9 与 `object-fit: cover` 裁剪竖屏视频。
- [ ] 真实分镜页面播放已有 9:16、16:9 视频,确认画面完整、控制条可用;切换镜头与历史版本,检查明暗主题和窄屏下的预览区滚动。本次仅核对组件与样式,未完成浏览器验收。
## 角色卡引用与本地参考图交付
- 已修复:Agent 任务保留模型侧图片槽位,画布编辑器单独使用角色/图片编号;仅有角色卡输入时可解析图片别名,混合输入时不猜测、不交换素材。引用仍由真实角色节点和资源归属校验决定。
- 已修复:OpenAI Images 插件允许 JSON 图片编辑请求携带 Base64 data URL,本地资源经后端授权读取后在内存中交付,不再错误要求公网存储地址;其他仅支持公网 URL 的协议要求不变。
- [x] 用户已在本地真实画布测试,扶颦参考图生成完成并上传;未额外发起收费生成。此结果不代表最新主分支集成后的完整 Agent 流程或生产环境已验收。
## 导演台 3D-BOX 核心 MVP
- 已实现:默认快速镜头工作流、演员快速添加与落位、语义姿态/景别/运镜、当前视图设机位、真实 Three.js 白模预演视频及画布视频回写。
- 已实现:画布 Agent 的 `director_scene_read` 安全摘要与 `director_preview` 请求桥;当前预演由已打开的浏览器导演台执行,不在后端伪造 Three.js 渲染。
- [ ] 登录真实画布验收:快速/高级模式切换、添加演员、地面落位、姿态、景别、运镜、白模预演、视频节点回写。
- [ ] Agent 场景验收:先读取导演场景,再请求指定镜头预演;确认只对当前画布/场景生效,确认模型 URL、storage key 等敏感字段不出现在工具结果。
- [ ] 验收明暗主题、窄屏滚动、录制期间修改场景的快照保护、WebGL context loss 恢复;当前未启动开发服务器做真实浏览器验收。
## 技能库个人分类与画布 Agent 筛选
- 已实现:结构迁移 v39 增加用户技能库分类,v40 增加内置技能删除记录;个人分类与平台共享技能广场分类分离。管理员可创建平台分类,用户可为已加入或自建技能归类;删除分类只解除归类,删除内置技能后启动同步不会重新创建。技能库个人视图和画布 Agent 的“我的技能 / 已启用”列表均可按分类筛选。
- [ ] 使用真实登录页面验收技能库左侧分类及数量、创建/分配/删除、未分类筛选;确认删除分类后技能仍存在,管理员平台分类对其他账号可见、可选,技能广场共享分类不变。
- [ ] 在画布 Agent 技能选择弹窗分别验证“我的技能”和“已启用”的个人/平台分类筛选;市场页仍按共享分类筛选。
- [ ] 在隔离 SQLite 与 PostgreSQL 数据库备份后执行 schema v39 / v40 `up` / `verify`,确认 `user_skill_states.library_category_id`、分类表和 `builtin_skill_tombstones` 迁移正确;未在生产数据库执行迁移或改写数据。
## 发布回归与资源引用保护
- 已修复:素材表现只在 `metadata_json` 引用资源时,批量删除和管理员资源检查仍识别该引用,不误删共享资源。已有回归覆盖共享对象、跨用户别名、事务回滚和删除队列。
- 已更新:启动并行外观加载、主题联动、统一下拉框、画布首帧与 Agent 插话的回归断言;资源消费竞态测试通过模拟 HTTP 传输验证真实持久化链路,不依赖外部网络。
- [ ] 升级到 schema 40 前备份数据库,执行正式迁移及 verify;验证已有渠道和逻辑模型继续生成唯一可读 ID,并确认内置技能删除记录不会在启动同步时被重新创建。
- [ ] 登录真实页面验收素材删除、生成结果转画布、Agent 插话、SSE 断线续传及资源访问。健康检查不能代替这些路径。
## 注册服务协议后台配置
- 已实现:协议名称与条款正文由后台「登录与注册」第 3 区块配置,经 `PATCH /admin/settings/registration` 保存,公开下发在 `GET /auth/settings` 的 `agreementTitle` / `agreementContent`。标题未配置时跟随外观品牌名生成;正文按空行分段渲染,留空时注册页提示待补充。注册开关单独切换不会覆盖协议(后端按可选指针字段合并)。
- [ ] 在真实后台改品牌名与协议正文后,用未登录浏览器打开注册页,确认勾选框与弹窗标题、正文段落在明暗主题下正确显示;再确认协议留空时的占位提示与开关关闭时的注册拦截文案。
- [ ] 用管理员账号验证「注册开关」与「服务协议」两次保存互不覆盖,撤销与离开页面提醒不丢内容;未确认服务端并发写入与多管理员同时编辑的冲突表现。
## 工作区 100% 缩放下的界面密度
- 已收紧用户工作区侧栏、顶栏和导航行、画布桌面端 Dock、Agent 内边距及默认浮窗尺寸;Agent 旧的手动调整尺寸仍保留,可点标题栏「恢复 Agent 紧凑窗口」自行重置。技能组合标签维持透明底和彩色描边。
- 画布编辑器左侧图标菜单与展开内容改用一块低圆角、半透明磨砂表面;删除双重标题、硬分隔线和搜索框边框。侧栏现在浮在真实画布上方,打开/折叠不再改变画布视口宽度;顶栏和左下缩放控件避开侧栏。工作区下拉菜单及共享单选下拉去掉高亮描边,用背景和阴影建立层次,键盘焦点保留低对比提示。仍待登录态真实页面验收明暗主题、画布节点透视、窄屏抽屉、菜单键盘导航和弹层层级。
- [ ] 在已登录的真实画布和用户工作区以浏览器 100% 缩放验收明暗主题、侧栏展开/折叠、顶部/底部工具栏、Agent 历史与设置、输入区和标签横向滚动;再检查窄屏与触屏下是否截断或遮挡。当前本地独立浏览器会跳转登录页,未将静态检查视为视觉验收。
## 画布资源断链、前端发布兼容与画布热点
- 已实现:统一递归识别持久资源字段(含嵌套 `videoPreview`);保存前和历史快照保存时校验当前资源归属/ready 状态,资源缺失返回带路径和 ID 的结构化 409。显式修复保留坏版本历史但要求新版本完整且 revision CAS 成功。后台 `repair-canvas-resources` 默认只读,仅可清除确认缺失的派生视频预览;主媒体问题报告为 unresolved。
- 已实现:哈希 chunk 采用长缓存,缺失资源不缓存;HTML 显式 `no-cache, must-revalidate`。静态容器发布保留当前与上一发布 chunk,并在保留窗口内清理更旧资源;Vite preload 失败只自动恢复一次,之后交由路由错误页,用户手动重试仍可用。已有数据卷不会凭空恢复首次部署前已删的 chunk。
- 已实现:同步显式 repair/force 请求不会被正在运行的普通同步吞掉;素材元数据请求按账号 scope/generation 隔离迟到响应。画布内容 hash 对 viewport/revision-only 副本复用;视口更新走轻量路径;loading 节点共享画布级 ticker,页面隐藏时暂停该 ticker。
- [ ] 先备份隔离/测试数据库,验证 `repair-canvas-resources` dry-run、`--apply`、重复运行幂等、并发 revision 冲突、只清派生 preview、不改主媒体;检查 unresolved 后人工恢复历史或重传。未扫描或改写生产数据库。
- [ ] 使用发布测试卷连续发布三版以上:旧 tab 延迟触发旧 chunk 后仍可加载;验证 HTML 与哈希资源 Cache-Control、旧 chunk 404 不被 SPA fallback 改写,以及浏览器一次性恢复不循环。当前没有 CDN/Ingress 线上响应证据,Firefox MIME 具体响应类型未确认。
- [ ] Firefox/Chrome/Safari 实际页面走关键 lazy route;检查离线草稿、登录切换和错误重试行为。尚未做真实浏览器/CDN 验收。
- [ ] 使用 React Profiler 与长时间运行的大画布记录任务更新、节点数/可见数、主线程长任务和页面隐藏行为。当前只做结构性降热点,不推断整画布必然全量重渲染或已达到某个帧率。
## 画布媒体产出模型
- 产出模型与选用模型分开保存。生成成功才冻结产出模型;生成中、失败和只改模型下拉不改已有产出模型。
- [ ] 打开画布外观「媒体信息」后,新生成的图片、视频、音频左下角显示模型展示名或模型 ID。图片右下角仍显示尺寸和体积。关闭开关后这两处都不显示。视频标题旁的宽高保持可见;视频播放时角标不盖住控制条。
- [ ] 生成成功后把下拉改成另一个模型且不重新生成,角标仍是原来的产出模型。重新生成成功后角标换成新模型。
- [ ] 上传、粘贴、宫格切分、截取、合并、提取音频不显示产出模型。节点详情和版本对比在没有产出模型时留空,不显示「默认模型」。搜索能按产出模型名称和选用模型找到节点。
- [ ] 分享页和版本预览画布不显示产出模型。旧画布节点在重新生成成功之前不出现角标。
- [ ] 真实浏览器验收尚未执行。
## 原任务 ID 媒体保存恢复
- [x] 故障注入:下载 503 后服务实例重建继续原任务、OSS 上传失败保留暂存文件及资源 ID、素材登记异常使任务成功事务回滚,登记恢复无需再次下载。
@@ -1131,6 +1292,13 @@ PR #507 核心能力已按主分支实现保留,并补齐小视口尺寸约束
- [ ] 登录后用 DeepSeek 等 chat-completion 渠道跑「开始一部新短剧 → AI 生成章节」,确认章节正常导入。
- [ ] Claude、Gemini、OpenAI Responses 渠道各跑一次文本生成,确认系统指令生效且没有重复出现在正文里。
## AI 生成章节的同名项目处理
- 已实现:创建项目遇到同一用户下的重名时返回 HTTP/code 409、`reason=project_name_conflict`;前端只对该原因有限更换名称重试,其他冲突和服务错误直接显示,不调用模型。
- [x] 隔离 PostgreSQL 的仓储和服务测试、HTTP 响应及前端重试边界测试通过:同用户重名返回结构化冲突、不同用户可同名、项目 ID 冲突不被误判、原项目和工作流不被覆盖;更换名称后可正常创建。
- [ ] SQLite 同组用例待在启用 CGO 的环境运行。
- [ ] 登录真实页面,用已存在项目的同一故事再次执行「AI 生成章节」,确认自动创建带后缀项目、章节正常导入,且只有一次模型生成请求;确认其他 HTTP 409 不触发重试。
## 画布智能引用
- [ ] 在简洁、专业模式及展开提示词面板中打开智能引用,检查明暗主题、弹层宽度和开关状态。
@@ -2698,3 +2866,77 @@ SSE 现在按 revision 变化发送紧凑快照,无变化只 heartbeat;客
画布页本轮改动:区域选择(底部工具栏胶囊切到「区域选择」)下滚轮改为上下平移画布,缩放保留给 Command/Ctrl + 滚轮(含触控板捏合);带 `data-canvas-wheel-scroll` 的节点内部区域仍保持自身滚动。顶栏分享按钮右侧新增「账户与设置」按钮,点击复用画布内「去设置」的 `navigateToSettings({ section: "channels", continueCreation: true })`,进入 `/settings?section=channels&continue=1`(顶部有「返回创作 / 保存并返回」,与模型缺失时的跳转一致,不新增全局配置弹窗)。曾尝试在画布内用抽屉内嵌设置页,因嵌套 Router 报错已移除。类型检查通过;待登录态浏览器验收:区域选择的滚轮方向与实际滚动手感,设置按钮跳转后返回画布时画布内容与滚动位置是否保持,以及设置项保存后画布生成默认值立即生效。
节点面板快捷键:`[data-canvas-node-panel]` 面板内任意位置按 ⌘/Ctrl + Enter 触发面板主按钮(目前只有创作面板的生成按钮带 `data-canvas-panel-submit`),生成中或提示词为空时按钮禁用,快捷键不拦截按键。待验收:文本框内按下不插入换行、输入法组合中不触发、面板内其他输入框与配置面板不被误触发。
## 画布媒体交互与 Agent 循环止损复测(2026-09-23)
本轮针对画布媒体节点、生成任务状态及云端 Agent 重复读取问题完成实现和复测。代码仍未提交,以下结论基于当前工作区的专项测试与隔离本地浏览器环境,不代表已部署生产。
- [x] 第三方 API 已提交后,前后端均拒绝取消:任务存在 `providerRequestId`、取消状态、媒体阶段、未知运行阶段或已进入媒体保存阶段时不再显示取消按钮,也不接受取消请求。
- [x] 图片、视频、音频下载改为浏览器直接导航 OSS/CDN 签名地址;不再 `fetch → Blob`,平台只签发短时地址,不中转媒体正文。隔离浏览器下载的 Host 为 `cn-nb1.rains3.com`。
- [x] 生成节点初始状态统一显示“作品创作中”;“正在准备参考素材”和“正在连接上游”等技术阶段也统一映射为该文案。没有上游真实进度时不显示 `0%` 或素材准备假进度,只有上游返回真实进度才显示百分比。
- [x] 图片预览弹窗增加非纯黑加载占位、边框、虚线内框、Spinner、错误态和加载完成淡入;已用 2.5 秒图片响应延迟在浏览器中验证加载态可见。
- [x] 视频节点无持久化 poster 时主动从 OSS/CDN 视频取首帧;已验证首帧静态预览真实显示,视频资源请求返回 Range `206`,持久化 poster 仍优先使用对象存储地址。
- [x] 视频播放器不再因未知音轨默认静音;已验证视频解码音频字节持续增加,`muted=false`,播放期间 `paused=false`,音量滑块可操作并从 100% 调至 85%。
- [x] Agent 只读工具增加同参数缓存、同结果重复回放护栏、跨参数累计 32 次安全上限;识图按画布版本和资源限制重复调用,并受本轮总识图预算约束,超过上限直接结束本轮,避免继续消耗模型调用和 Token。附件事件序列已解析确认事故主要是只读工具循环(`canvas_get_state` 6 次、`canvas_read_storyboard` 2 次、`agent_profile_read` 4 次),不是单一图片识别失败。
- [x] Agent 媒体审批快照忽略节点位置和展示性变化;仅移动节点不会使已审批生成失效,提示词、资源、连接、锁定状态和任务状态等真实生成输入变化仍会使审批失效。
自动验证记录:
- 前端 `bun run test:canvas`:37 项通过;媒体与生成显示专项:36 项通过。
- 前端 `bun run typecheck`、`bun run lint`、`bun run build`:通过;构建仍有既有大 chunk 提示。
- 后端 Agent 读取、识图、审批、媒体依赖专项:通过;资源/OSS、取消、上游进度专项:通过。
- 后端全量 `go test ./...`:未通过,阻断点为既有 `TestPluginViewIncludesDocumentationForEveryOfficialProtocol` 插件视图数量断言(实际 99,对断言期望 97),与本轮媒体和 Agent 修复无直接关系;因此不能记录为后端全量通过。
仍待真实环境验收:
- [ ] 使用真实生产渠道重新执行 Agent 图片识别和“识别多张图片后命名节点”流程;本轮未再次调用付费生产模型。生产渠道仍建议将“最大参考图片数”设为 `0`,不需要识图的渠道不要暴露图片识别能力。
- [ ] 使用真实 Agent 会话完成审批后仅移动节点、再提交生成的端到端验收;后端回归已覆盖该语义,真实模型会话尚未执行。
- [ ] 对含音轨视频做人工听觉验收;当前隔离视频已由浏览器运行时确认音频解码和音量控制可用。
- [ ] 修复或单独核对插件视图数量断言后,再执行后端全量测试;本轮不修改与媒体/Agent 无关的插件注册逻辑。
## Pi Agent runtime 与存储边界预发布验证(2026-09-29)
- 已实现:在线画布 Agent 切换到 Pi 原生会话 runtime;Go 保留权限、工具审批、模型桥和控制检查点,Pi JSONL 会话按运行 ID 持久化;画布能力工具、错误映射、技能渐进读取、运行时镜像依赖和存储运行策略已有回归覆盖。
- 已修正验证门禁:旧的 2026-09-23 记录称 `TestPluginViewIncludesDocumentationForEveryOfficialProtocol` 固定期望 97、实际 99;当前断言已改为根据官方包和内置插件动态计算数量,该专项现已通过。不要把该历史记录当作当前未解决的失败。
- 本地自动验证:后端 `go test ./... -count=1 -timeout=15m` 全量通过;PostgreSQL 资产迁移、资源字节修复及画布版本历史依赖用例通过。前端类型检查、退场 lint、全量测试(2096 项)和生产构建通过;文档站 `bun run types:check` 通过。
- 本机没有 `redis-server`,因此 Redis 租约及缓存白名单两个真实 Redis 用例在全量本地运行中会跳过;质量工作流已安装 Redis、连接 PostgreSQL,并强制检查 5 个依赖型用例均真实执行并通过。需以 GitHub Actions 的结果作为合并/发布门禁。
仍待真实环境验收:
- [ ] GitHub Actions 确认 PostgreSQL / Redis 依赖测试、前后端检查、支付插件双架构 smoke 和 amd64/arm64 镜像构建全部通过后再部署。
- [ ] 部署后使用真实登录态验证 Agent 多轮会话、Skill 按需读取、工具审批/继续、进程重启后的会话恢复,以及用户/画布之间的会话隔离;不以健康检查替代该链路。
- [ ] 在隔离数据库备份上验收 schema v42 的 `cloud_agent_pi_sessions` 迁移与回滚方案;不得在生产服务器手工改数据库或代码。
## Agent 角色卡读取与图文参考待验收
画布角色卡仍为 `text` 节点,但角色设定由项目角色资产按当前版或固定版读取;Agent 的节点目录标注 `kind: character`,精读返回角色设定、形象/声音状态及图片参考就绪情况,不再把空 `content` 当作空角色卡。选择角色卡作为媒体参考时,后端从同一角色版本解析三视图图片,并把角色设定加入本次生成提示词;只选 `sourceNodeId` 则使用文字设定。审批期间角色版本变化会阻止旧输入提交。生成仍需用户确认或符合 auto 准入,不在读取时发起付费任务。
待登录态页面验收:对「扶颦」精读应返回实际外貌和服装;选择该卡生成时,审批/任务输入应同时包含该版设定与已绑定三视图;故意改动角色版本后旧审批不能提交。声音档案可精读,音频任务在未显式选择音色时采用角色绑定的 voiceKey 与声音指令;跨模型适配及真实音色效果仍需单独验收。此轮不代表生产已部署。
## 存储满额生成拦截与 Live2D 开关稳定性(2026-09-30)
- 已修复:图片、视频、音频生成任务在创建、积分预留和上游调用之前检查账号文件容量;容量已满时返回配额错误并提示先清理素材,文本任务不受文件容量限制。普通画布与 Agent 媒体生成共用该 admission 边界。
- 已修复:Agent 面板开合时保留 Live2D 入口组件和已加载模型,仅隐藏入口并暂停不可见渲染,不再因反复卸载、重建 PIXI/Live2D 上下文而短暂回退默认动态球。
- 自动验证:后端纯 admission 专项测试通过(CGO 关闭,3 项);前端 Live2D 生命周期专项测试通过;前端生产构建通过。既有 Agent 位置测试在当前 Windows 环境因 Bun 将磁盘路径解析为 `/E:/...` 而未进入测试逻辑。
仍待真实环境验收:
- [ ] 将测试账号文件容量填满后,从普通画布和 Agent 分别提交图片、视频、音频生成,确认立即显示容量提示、没有创建计费订单、没有调用上游;清理出容量后可恢复生成。
- [ ] 使用自定义 Live2D 模型连续开合 Agent 面板,确认不出现默认动态球闪回,拖动、窗口缩放和重新进入画布仍正常。
## 后台存储资源用户筛选(2026-09-30)
- 已修复:后台存储资源列表的用户筛选不再只接受完整用户 ID;现在支持按用户 ID、用户名、昵称和邮箱进行不区分大小写的模糊匹配,并可与资源类型、状态、存储位置、资源关键词组合使用。
- API 新增可读性更高的 `user` 查询参数,后端继续兼容旧 `userId` 参数;前端请求同时发送两者,避免滚动升级期间旧后端忽略新参数并错误返回全量资源。
- 自动验证:后台 UI 回归 17 项通过;前端生产构建通过;后端参数规范化专项通过。SQLite 集成用例已补充四种用户字段匹配,但当前 Windows 本机 CGO/GCC 无法解析生成的对象文件,未在本机实际执行该数据库用例。
仍待真实环境验收:
- [ ] 在管理员后台存储资源页分别输入完整/部分用户名、昵称、邮箱和用户 ID,确认返回同一用户资源;再叠加类型、状态和存储位置筛选,确认分页与清空筛选正常。
## 平台技能策展
SQLite 与 HTTP recorder 已验证;PostgreSQL 16/17、前端构建及浏览器中的开关、分类维护、市场/技能库筛选、分页与明暗主题待验证。
一级分类还需 PostgreSQL 验证 46→47 与并发改归/删除,以及浏览器图示、根停用默认桶与空根表回落。handler 全套涉及 socket,受限环境中未完成。

View File

@@ -1218,3 +1218,8 @@
- 具体改动:暗色回到 `#0f0f0f / #181818`,亮色回到 `#f5f5f5 / #ffffff`,仍写死在后台 token 里不读皮肤。分组标题取消圆点与渐隐短线;组内取消竖向连接线和缩进;当前页只保留底色,不再加描边和左侧指示条。
- 验证:回归测试更新隔离色值与“无树状线”断言。
- 逐项回滚:恢复本批上述文件即可。
## 自定义更新公告页签
- 在“画布配置”后新增“更新公告”,沿用统一保存、刷新、撤销和离开保护。
- 支持开关、多版本内容块、整行/半行布局和实时预览;后台颜色使用既有独立 token,窄屏单列。
- 沿用既有外观配置 JSON 存储,无新增数据库表;公告草稿与当前站点构建版本分别管理。

View File

@@ -0,0 +1,288 @@
# 画布性能与媒体分发最终改造方案
状态:基于当前 `main`(`7bf655e9`)和现有 OSS/CDN 重构重新整理。本文是当前仓库的最终目标和一次性改造边界,不把已经存在的能力重复当成待建设,也不把关键问题推到二期、三期。
## 1. 最终结论
当前仓库已经完成了画布优化的第一代基础设施:
- 资源访问已经收敛到 `assets.ResolveAccess` 和 `POST /api/resources/access`。
- 画布已经有 `canvas-live-viewport` 的合成层预览、Leafer underlay/overlay 图形层、空间索引、节点拖拽预览和选择预览。
- 图片、视频和音频的多数展示路径已经使用 `storageKey` 解析访问地址。
- 云资源 `/file` 路径已经能够重定向到 CDN/源站,平台不必转发云端媒体正文。
因此,最终改造方向不是重写成 WebGL,也不是复制 LibTV 的组件,而是把当前已经存在的能力合并成一个闭环:
```text
稳定资源身份
-> 统一访问描述
-> poster-first 媒体状态机
-> 展示与字节读取分离
-> 交互预览与持久化提交分离
-> 可见节点 / 轻量节点 / 完整节点三层 LOD
-> 同一帧节点、连接、对齐线更新
-> 以设备帧预算驱动调度和验收
```
用户手势帧永远不承担以下工作:整视频 Blob 下载、视频首帧编码上传、批量 React 复杂节点挂载、全量连线重算、逐 pointermove 持久化和签名地址同步写入画布。
## 2. 外部方案需要修正的地方
外部方案仍然正确的核心原则是视频默认图片化、旧画面不清空、交互期间不做重活、屏内节点不能被屏外预算截断,以及节点与连线使用同一帧预览。
以下内容不能直接照搬:
1. **OSS snapshot 尚未成为当前仓库的统一首帧供应链。** 当前 `web/src/services/canvas-video-preview.ts` 仍在缺少持久 poster 时使用浏览器截帧并后台上传;这只能是历史资源的降级路径,不能继续作为新资源的正常路径。
2. **当前没有完整的 `SelfVirtualizingNode` 和渐进挂载队列。** `use-canvas-render-model.ts` 主要负责空间查询和 DOM 节点挂载窗口,固定使用 280/420/720 节点预算;这不是完整 LOD,也不能宣称支持任意万级画布。
3. **Leafer 目前主要承载连接、选框、对齐线和草稿线。** 节点正文仍是 DOM。`canvas-live-viewport.ts` 已解决交互期合成变换,但不能替代节点 LOD 和挂载调度。
4. **小地图仍然为所有节点创建 DOM 和标签。** 只有图片预览数量限制为 24,节点和标签没有虚拟化;节点规模增大时,小地图本身会变成主线程负担。
5. **当前播放后整文件缓存会制造重复流量。** `resource-blob-cache.ts` 曾由播放器播放后延迟 4 秒拉取完整 Blob;播放 Range 和完整下载可能并存,且缓存预算最高可达 2 GiB。这不属于普通展示行为。
6. **`provider-input` 的浏览器边界与文档不一致。** 当前登录用户 `/resources/access` 直接以 `allowProvider=true` 处理该用途,前端可以申请长 TTL 模型输入地址。最终合同必须把模型输入迁移到服务端受控读取,浏览器公开 batch 只接受展示、复制、下载和浏览器字节处理。
7. **固定数字不是性能承诺。** 0.23/0.27、256px、30+4、280/420/720、5000 条连接、DPR 上限 3 都只能是初始参数。最终参数必须由真实设备和帧预算测量得到。
## 3. 最终分层
### 3.1 持久数据层
画布 JSON 只保存稳定身份和业务元数据:
```ts
type CanvasMediaRef = {
kind: "image" | "video" | "audio";
resourceId?: string;
storageKey?: string; // resource:<id>
originalUrl?: string; // 外部资源历史兼容字段
previewContent?: string; // 已经持久化的静态预览
videoPreview?: {
storageKey?: string;
content?: string;
width?: number;
height?: number;
mimeType?: string;
};
naturalWidth?: number;
naturalHeight?: number;
mimeType?: string;
};
```
临时签名 URL、`blob:` URL、当前播放器实例、缓存 lease 和访问描述不得写入画布 JSON、任务结果、localStorage 或日志。
上传、生成、导入三条路径必须在创建节点时统一写入同一 `resource:<id>`。只有外部直链未转存时才保留外部 URL,并明确标记为外部资源。
### 3.2 访问描述层
`assets.ResolveAccess` 是唯一分发策略入口。浏览器端的 `media-access` 只负责:
- 按 scope、资源、用途、变体合并请求。
- 使用服务端返回的 `expiresAt`、`refreshAt` 和 `revision`。
- 账号切换时取消在途请求并清理旧描述。
- 只把描述交给 `<img>`、`<video>`、原生下载或明确的字节读取器。
服务端用途矩阵固定为:
| 用途 | 浏览器公开 batch | 服务端内部 | 变体 |
| --- | --- | --- | --- |
| `display` | 允许 | 允许 | original/playback |
| `copy` | 允许 | 允许 | original |
| `download` | 允许 | 允许 | original |
| `browser-process` | 允许,必须满足 CORS/Range | 允许 | original/playback |
| `provider-input` | 禁止 | 仅模型请求组合根 | original |
模型输入由后端在真正发出上游请求前解析,前端只提交稳定资源身份或引用绑定。不得让浏览器先申请 4 小时地址再把它当作普通页面能力。
`ResourceAccess` 的缓存键至少包含:
```text
userScope + resourceId + purpose + requestedVariant + serverRevision
```
响应返回后必须再次校验发起时的 scope 和 access generation;账号切换后旧请求即使晚到,也不能写入新账号缓存。
### 3.3 媒体层
视频节点采用 poster-first 状态机:
```text
no-poster -> requesting-poster -> poster-visible
poster-visible -> activating -> player-created
player-created -> metadata-ready -> first-frame-ready -> player-visible
任何失败 -> poster-visible / 可重试状态
```
硬规则:
- 未选中视频不挂载 `<video>`。
- 激活时 poster 保持在底层,播放器真实出帧后才淡入。
- 播放 URL 不先置空,不能制造黑屏窗口。
- `canplay` 或 `requestVideoFrameCallback` 之前播放器保持透明。
- 播放失败、签名过期和 CORS 失败都回到 poster,并给出可见重试。
- 普通播放不调用完整 Blob 缓存。Blob 只服务裁剪、像素读取、导出、识别和明确离线场景。
- 播放器池只保留少量热实例,按设备内存和可见性淘汰。
### 3.4 首帧供应链
新上传和生成完成的视频必须优先得到服务端可复用的静态首帧:
1. OSS 资源使用对应对象存储的 `video/snapshot` 或同等处理能力生成首帧访问描述。
2. 处理参数固定由服务端白名单生成,宽度按 400/800 等档位选择,不接受任意客户端处理串。
3. 签名 URL 只在内存中使用,不写回画布。
4. 高频资源可以异步物化为独立 poster 对象,但不放进上传同步关键路径。
5. 历史没有 poster 的资源才使用浏览器截帧兜底,且该路径必须限流、可取消、可观测。
首帧接口需要验证资源归属、资源类型、ready 状态、实际存储位置和访问用途。非支持 OSS 资源返回结构化 `supported=false`,不能伪造一个视频 URL 当作图片。
### 3.5 画布渲染层
画布分成四个运行时状态:
```text
committedViewport 持久化视口
liveViewport 手势期间实时视口
committedNodes 持久化节点状态
mediaRuntime poster/player/cache 运行时状态
```
平移、缩放、小地图拖动和节点拖动期间只更新 live 状态、CSS 合成层和 Leafer preview;手势结束后一次性提交 React/store/服务端状态。媒体运行时状态不能因为 viewport 每帧变化而重置。
节点渲染采用三层 LOD:
| 层级 | 条件 | 内容 |
| --- | --- | --- |
| `shell` | 远景、屏外 retain 区、交互资源不足 | 尺寸、标题、连接入口、色块或低清 poster |
| `preview` | entry margin、可见但非焦点 | poster/图片、最少状态、静态文字 |
| `full` | 屏内焦点、选中、拖动、连接、编辑 | 完整业务组件和交互控件 |
进入和退出完整层必须有迟滞;选中、拖动、连接和编辑是强制升级条件。屏内节点和 entry margin 节点不能被总预算裁掉,预算只用于更远的 shell 和完整层数量。
渐进挂载调度器以帧预算为准:
- 每帧先测量 `performance.now()` 消耗。
- 剩余预算足够时才挂载下一个完整节点。
- pointerdown、wheel、拖动、框选期间暂停后台升级。
- 优先级顺序为当前交互节点、屏内节点、entry margin、按距离排序的 shell。
- 连续超预算时自动降低媒体效果和完整层并发,而不是硬编码一个全球固定数量。
### 3.6 连接和图形层
Leafer 保留为连接、选框、对齐线和草稿线的图形层。连接路径按连接 ID 缓存,并维护节点到连接的反向索引。
拖动的一帧必须按以下顺序完成:
```text
pointermove
-> 计算 dragOffset
-> DOM 节点临时 translate
-> Leafer 只更新受影响连接
-> 更新选择框和对齐线
-> 同一 RAF 提交图形脏区
```
只有一端移动时重算曲线,两端一起移动时可整体平移旧路径。屏内连接不能因为屏外连接预算截断。拖动期间暂停流光动画,避免动画和路径重算争抢帧预算。
### 3.7 小地图
小地图改为单一 Canvas/Leafer 绘制:
- 节点按颜色和尺寸画矩形。
- 只对当前焦点或极少数指定节点画 poster。
- 不创建全量标题 DOM。
- 世界范围使用空间索引或增量 bounds,不在每次 pointermove 全量计算。
- 视口框拖动保留抓取偏移;框内拖动不跳到鼠标中心。
- 小地图拖动持续发布 live viewport,render window 同步跟随。
## 4. 缓存最终规则
缓存分成四类,不能互相替代:
1. **请求合并**:同一 scope、资源、用途、变体、档位只允许一个在途请求。
2. **访问描述缓存**:只缓存短期签名描述,按服务端 `refreshAt` 和 `revision` 失效。
3. **展示对象缓存**:交给浏览器/CDN;普通展示不转 Blob。
4. **字节缓存**:仅给明确 Blob 消费者,按 scope、实际变体、revision 隔离,有单文件上限、总量上限、LRU 和可取消下载。
账号切换必须同时:
- 清理 access descriptor、in-flight descriptor 和 resource metadata cache。
- 撤销展示用途产生的 Object URL。
- 取消或使旧 Blob 下载代际失效。
- 清理播放器池和媒体 lease。
- 响应提交前核对 scope/generation。
Service Worker 只允许缓存公开、无 token、无签名敏感参数的缩略图。私有 OSS、原始视频、鉴权 API 和包含用户 token 的 URL 不进入公开 Service Worker。
## 5. 后端 OSS/CDN 合同
资源访问顺序固定为:
```text
业务授权
-> ready 校验
-> 变体解析
-> 实际存储位置解析
-> 用途能力与保护校验
-> CDN/源站签名或明确平台流计划
```
当前仓库必须保持以下边界:
- CDN 是分发能力,不等于 OSS 厂商名称。
- 历史资源按资源绑定的 provider、endpoint、bucket 和 storage setting 读取,不能把所有历史对象拼到当前 CDN 域名。
- `download` 使用对象存储响应的 Content-Disposition,不能因为改文件名而把媒体正文拉回平台。
- local/proxy 正文流必须带原因、字节量、Range 和失败指标。
- `/api/resources/access` 不得成为浏览器获取长效 provider-input 凭据的入口。
- `revision` 必须包含资源实际版本、播放副本状态和分发配置版本;配置变化后旧描述不能继续被前端长期使用。
- CORS、GET/HEAD、Range、206、416、签名过期、续期和 CDN 命中必须通过真实链路验证,不能用本地单测替代。
## 6. 一次性改造清单
这次改造以一个完整发布批次交付,下面是同一批次内的工作流,不是功能延期:
1. **访问合同收口**:公开 batch 拒绝 `provider-input`;模型输入迁移到后端组合根;访问描述缓存增加 scope/generation/revision 失效;补全账号切换清理。
2. **展示与字节读取分离**:移除播放后自动整 Blob 下载;保留 Blob 只给裁剪、导出、识别等真实字节消费者;补取消、去重、LRU 和单文件上限。
3. **poster-first 完整化**:新资源首帧走 OSS/CDN snapshot;浏览器截帧只做历史/不支持资源兜底;播放器保留 poster 到真实首帧。
4. **LOD 和渐进挂载**:在现有 `useCanvasRenderModel` 上增加 shell/preview/full 三层模型、迟滞和帧预算队列;当前固定节点预算降为初始策略,不作为产品承诺。
5. **小地图换成图形绘制**:去掉全量节点标签 DOM 和图片遍历,保留色块、焦点预览和视口框。
6. **连接增量更新**:保留现有 Leafer 路径缓存和拖动预览,补齐连接空间索引裁剪、屏内连接强制保留和动画暂停。
7. **运行时观测**:记录手势帧 P50/P95/max、50ms 长任务、可见/挂载节点、完整层数量、播放器数量、poster 请求、Blob 字节、平台正文 200/206 字节、CDN/origin 命中和降级原因。
8. **全入口清理**:静态扫描并删除页面自行拼接签名 URL、旧 `direct/proxy` 开关、展示路径隐式 Blob、重复的 provider 判断和旧直链 helper。
## 7. 验收门槛
### 画布交互
- 5%、10%、25% 缩放下四向连续平移,屏内节点不成片消失。
- 快速跳远、缩放和小地图持续拖动不出现成片黑边;新区域在手势进行中进入 render window。
- 单节点、多节点拖动时节点、连接端点、选择框、对齐线同一帧移动。
- pointercancel、Esc、失焦不会写入错误位置,也不会触发点击或工具栏。
- 连续框选不激活播放器、不显示单节点工具栏、不批量升级完整节点。
### 媒体
- 未选中视频 DOM 中 `<video>` 数量为 0。
- 选中视频在播放器真实出帧前 poster 持续可见。
- 播放失败、断网、签名过期和 CORS 失败不会黑屏。
- 普通播放不会触发整文件 Blob 下载;播放 Range 不与后台整文件下载重复。
- 相同资源/用途/变体的并发访问描述请求为 1。
- 账号切换后旧账号 URL、Blob、播放器和 in-flight 结果不会进入新账号。
### OSS/CDN
- CDN、源站、local、明确批准的 proxy、历史资源位置均有合同测试。
- 真实浏览器验证 CORS、Range、206、416、签名过期、续期、暂停后继续和下载文件名。
- 平台只对 local/approved-proxy 发送媒体正文;每次正文都有结构化原因。
- `provider-input` 不再由登录浏览器 batch 直接获取长效 URL。
### 规模与设备
- 至少覆盖 100、300、1000、3000 节点和 100、900、2000、5000 条连接。
- 覆盖 DPR 1/2、390px 窄屏、桌面、低端设备模拟、冷缓存和热缓存。
- 记录 P50、P95、最大帧间隔和长任务,不能只看平均 FPS。
- 所有固定数字都必须回填到设备/帧预算实测结果,不把某次机器上的 60fps 结果写成普遍承诺。
## 8. 风险控制
- 旧资源没有 poster 时允许降级为占位卡和可取消浏览器截帧,但不得挂载常驻被动播放器。
- OSS/CDN 配置无效时明确返回结构化错误,不把故障静默转成平台代理。
- 图形层异常时保留 DOM 节点和静态连接;LOD 调度异常时退回 shell/preview,不在手势帧内批量升级。
- 资源清理继续执行画布、项目、任务、素材和历史快照引用检查;性能改造不改变删除安全边界。
- 不以新增 Service Worker、`will-change`、WebGL 或 OffscreenCanvas 掩盖访问合同和渲染调度问题。

View File

@@ -0,0 +1,253 @@
# 资源访问与 OSS/CDN 分发架构重构
状态:已实施核心重构,待真实 OSS/CDN 链路与线上流量验收。
已落地:统一资源访问合同、存储分发策略、批量访问 API、共享 HTTP 执行器、用户/管理员/公开/分享/公告/外观/日志媒体迁移,以及前端 provider 输入、Blob 读取和画布复制地址迁移。未声称真实 CDN 命中率、CORS、Range 或账单收益已在本地验证。
## 1. 决策摘要与完成边界
这不是「把几个 URL 改成 OSS 地址」,而是收回散落在页面、API helper、handler、缓存和存储实现中的分发决策权。目标是:**资源身份稳定、授权按业务场景执行、分发只有一个决策入口、供应商差异封装在适配器内**。
- 用户展示、播放、下载、复制地址,以及分享、公开素材、后台预览和模型输入,必须使用同一资源访问合同;不同授权场景不能变成不同分发策略。
- 对具备直连条件的云资源,浏览器直接请求 CDN 或对象存储,平台只返回访问描述或重定向,不转发媒体正文。配置了合格 CDN 时优先 CDN;对象存储直连不能计作 CDN 命中。
- 本地资源、不可公网访问且没有可用 CDN 的资源,以及获准的特殊字节处理,是明确、有监控的例外;不能用静默代理掩盖配置错误。
- 画布右键复制地址签发原始资源的新鲜直链,不复制平台展示入口或浏览器 Blob URL。实际为本地资源时如实提示平台地址;没有可用地址时明确失败。
- 展示路径不默认下载完整 Blob;浏览器缓存成功不再是云端资源或生成任务成功的条件。
- **新增统一服务不是完成。全部调用方迁入、旧策略删除、权限及跨入口验收通过,才算完成整体重构。** 可分阶段验证,但不把阶段性补丁作为最终交付。
本轮不重写任务引擎、计费、模型协议或转码算法,不引入通用插件平台、事件总线或为每次签名建立数据库记录。
## 2. 已确认问题与证据边界
| 现状 | 代码依据(仓库相对位置) | 架构影响 |
| --- | --- | --- |
| 节点复制地址优先取 `metadata.content`,否则拼平台文件入口,没有使用已有直链接口 | `web/src/pages/canvas/project.tsx` 的 `copyNodeMediaUrlToClipboard` | 交互层在解释媒体身份并选择出口 |
| 前端文件地址带 `direct=1`,后端生成的稳定文件地址不带;另有 `proxy=1` | `web/src/services/api/resources.ts`;`backend/internal/assets/urls.go` | 相同资源可能因调用来源而采用不同策略 |
| S3 默认代理判断在 CDN 判断前;直链签发与文件分发又是两套分支 | `backend/internal/app/resource.go` 的 `prepareResourceDelivery`、`directResourceURL`、`signedOSSObjectURL` | 供应商、部署能力与业务用途混在一起 |
| `/file` 有重定向和流式读取两条路径 | `backend/internal/handler/user_data.go` 的资源文件路由 | 平台 URL 不等于平台转发,必须看最终请求与正文发送量 |
| 显式 Blob 消费者在部分直连失败后允许走代理 | `web/src/services/api/resources.ts` 的 `getResourceBlob`;`web/src/services/resource-blob-cache.ts` | CORS、过期或网络问题可能被代理流量掩盖;不是所有失败、所有缓存入口都会回退 |
| 视频生成同步等待完整 Blob;播放后还会计划缓存整文件,展示仍使用远程 URL | `web/src/lib/canvas/canvas-generation-task-sync.ts`;`web/src/services/project-asset-sync.ts`;`web/src/services/resource-blob-cache.ts`;`web/src/components/canvas/canvas-node-content.tsx` | 播放器读取与后台完整下载可能并存,缓存失败还会影响业务成功状态 |
| 管理预览、外观、公告、公开签名读取有直接流式路径;分享和日志媒体使用默认分发选项 | `backend/internal/handler/admin_storage.go`;`backend/internal/app/appearance.go`、`announcement.go`、`analytics.go`;`backend/internal/canvas/canvas_share.go` | 仅修改用户文件接口不能覆盖全站 |
| 普通资源写入存在 OSS 失败后落本地的路径,任务媒体新链路则强校验 | `backend/internal/app/resource.go` 的 `storeResourceObject`;`backend/internal/app/task_media_storage.go` | 当前配置不代表每个对象的实际位置,不能推断所有资源已经在 OSS |
| 对象保存实际 provider、endpoint、bucket 和历史存储关联;已有位置解析逻辑 | `backend/internal/model/models_project.go`;`backend/internal/app/resource.go` 的 `ossSettingForResource` | 不能把历史对象统一套到当前全局 CDN 域名 |
| 播放副本当前只针对本地原件生成,handler 自行选择副本和 ETag | `backend/internal/app/video_transcode.go`;`backend/internal/handler/user_data.go` | 变体选择必须进入合同,不能在重定向之后才选择;不能承诺云资源已经支持云端转码 |
| 存储连接测试会清空 CDN 配置,只验证源站;现有配置没有独立的 CDN 用户访问鉴权合同 | `backend/internal/app/storage_location.go` 的 `verifyOSSConnection`;`backend/internal/app/settings.go` | 「存储连接成功」不能作为 CDN、浏览器跨域和私有内容访问策略可用的证明 |
以上来自代码调查。尚未读取生产存储配置、实际请求日志、CDN 日志或账单,不能据此认定全部媒体都在代理,也不能量化已经多花了多少费用。线上归因需要同时记录最终域名、307/200/206、平台正文发送字节和 CDN 回源/命中情况;它用于确认收益,不替代架构重构。
## 3. 目标结构:身份、授权、分发、字节读取分离
```text
UI / 画布 / 素材 / 编辑器
-> media-access:资源引用解析、批量请求、有效期、请求去重、账号隔离
-> resources API(现有 http 入口)
-> handler -> service 别名 -> app 组合根
-> 用户 / 分享 / 管理员 / 公开槽位的授权规则
-> assets.ResourceAccessService(唯一访问决策入口)
-> 变体及实际存储位置解析
-> DeliveryPolicy(用途、能力、鉴权、期限)
-> OriginSigner / CDNSigner
-> Redirect 或有明确原因的 StreamPlan
需要字节的编辑 / 导出 / 缩略图计算
-> 浏览器 media-bytes:先取得访问描述,再按需读取与缓存
服务端转码 / 导出 / 导入
-> 授权与业务编排 -> OriginReader:直接读实际存储,不套浏览器 CDN 策略
```
### 3.1 后端职责
| 边界 | 负责 | 不负责 |
| --- | --- | --- |
| `handler` | 入参、会话上下文、业务响应、执行已决定的重定向或流式 HTTP 响应 | 查存储配置、选择 provider、解释 CDN、决定强制代理 |
| `app` | 场景授权编排、依赖注入、配置装配;向现有 `canvas.Host` 提供收敛后的能力 | 再保留一套直连与代理分支 |
| `assets` | `ResourceRef`、访问合同、变体解析、分发策略、结构化失败原因 | HTTP、React、第三方 SDK 细节、业务提示文案 |
| `storage`(从现有实现提取) | 按实际存储位置执行 Put/OpenRange/Delete、源站签名;封装 local、阿里云、腾讯云、七牛及 S3 差异 | 判断当前用户是否能看画布、选择显示还是复制 |
| 分发签名适配器 | CDN 路径映射与用户访问签名;与源站签名分开实现、分开测试 | 默认把私有内容当公开内容,或用对象存储签名算法猜测 CDN 算法 |
| `repository/model` | 资源、位置、引用、配置与状态持久化 | 签发临时 URL、控制浏览器缓存 |
复用现有 `assets` 域,不再造一个包罗上传、任务、权限和 UI 逻辑的大 service。`internal/service` 继续只做别名;域包不得反向 import `app/service`。存储适配器不依赖 `assets` 业务策略,所需接口由组合根装配,避免循环依赖。
### 3.2 前端职责
- `services/api/resources.ts`:只保留业务接口与类型,通过现有 `http` 调用,不负责选择媒体出口。
- `services/media-access.ts`(拟):统一访问描述生命周期,处理批量、去重、刷新、取消、账号切换;不依赖 JSX、路由或 AntD。
- UI hook:订阅访问状态,适配图片加载和视频恢复;组件只声明资源引用、用途和变体。
- `services/media-bytes.ts`(拟):承接真实需要 Blob/ArrayBuffer 的操作;原缓存模块收敛为其内部缓存能力,不能自行拼代理地址或签名地址。
- `lib` 中的引用解析为纯函数。页面、store、生成同步和编辑器不得自行拼 `/file`、判断云厂商或提取签名参数。
## 4. 统一访问合同
### 4.1 持久身份与临时访问描述
持久数据继续以资源 ID / `resource:<id>` storageKey 等稳定身份关联对象;临时访问 URL 只在内存中使用,不写回画布、任务结果、素材记录或 localStorage。文本节点的 `content` 语义不受影响,不全局删除同名字段。
现有稳定 `/file` 引用通过已有引用解析边界提取资源 ID;它可以作为稳定入口保留,但不能保留第二套分发策略。解析只识别受控站点的资源格式,不因外部 URL 包含相似路径就将其认作受管对象。不为替换域名批量重写所有历史画布,也不以整库迁移作为本次收敛的前提。
前端明确区分受管资源、外部 URL、浏览器本地内容。外部 URL 不冒充 OSS 对象;本地内容在上传成功前没有云端可复制地址。已有资源 ID 时不得优先采用过期的 `content` URL。
建议新增 `POST /api/resources/access`,单项和批量使用同一实现;批量上限初始为 100 项。分享等场景提供自己的授权适配入口,内部调用同一访问服务,不能让客户端自行声明管理员或分享授权上下文。
```ts
type ResourceAccessRequest = {
resourceId: string;
variant: 'original' | 'playback';
purpose: 'display' | 'download' | 'copy' | 'browser-process';
};
type ResourceAccess = {
resourceId: string;
requestedVariant: 'original' | 'playback';
actualVariant: 'original' | 'playback';
url: string;
delivery: 'cdn' | 'origin' | 'platform-local' | 'platform-proxy';
issuedAt: string;
expiresAt: string | null;
refreshAt: string;
revision: string;
fallbackReason?: string;
};
```
- 返回仍遵守 `{ code, data, msg, reason }`。批量成功受理后,`data.items` 按项返回 `access` 或机器可读错误;顶层认证失败返回真实 HTTP/业务错误。逐项校验权限,不因同一批次有一个合法资源而放行其他资源。
- `purpose` 是策略输入,不是授权。`provider-input` 只允许可信服务端调用,不开放一个供浏览器获取长效凭据的用途枚举。
- `copy/download` 默认原件;播放可请求兼容副本。副本不就绪时按集中规则返回原件并说明 `actualVariant/fallbackReason`,不能静默伪装副本就绪。源站与副本使用各自真实位置、MIME 和 ETag。
- 时间字段使用 UTC RFC 3339。`expiresAt` 为实际签名到期时间,只有明确公开资源可为 `null`;客户端结合 `issuedAt`、请求耗时和本地单调时钟计算剩余期限,不能假定设备时钟准确。`refreshAt` 同时受授权剩余时间、配置刷新周期和安全余量约束,不能固定缓存 4 分钟后猜测有效期。
- `revision` 表示资源变体与分发配置版本,不含密钥。访问描述响应默认 `no-store`,客户端仅做受控内存缓存;媒体正文的浏览器缓存与 CDN 缓存另行配置。
- 内部决策使用互斥类型 `Redirect` / `LocalStream` / `ApprovedProxy`,每种类型只携带必要字段;删除 `ForceDirect/ForceProxy` 和多个可空字段并存的隐式状态机。
- 返回平台 URL 时使用绑定资源、变体、期限及授权范围的受控读取入口;该入口执行已验证的流式授权,不再次解析成自己的重定向目标,防止循环重定向。
### 4.2 期限、复制与长视频
当前直链为 5 分钟、模型输入为 4 小时。重构后由单一 policy 按用途管理期限,复用这两个值作为已有行为基线,而不是让 UI 各自写常量;分享签名不得超过分享有效期,私有资源不得因用途不同扩大授权范围。
- 复制操作在用户点击时取得新鲜原件地址,并显示「CDN/对象存储临时地址及有效期」;浏览器剪贴板失败必须报错,不能吞掉异常再提示成功。
- 短期直链是可转交的访问凭据,不是永不过期的分享链接。需要长期分享时使用既有分享授权能力,不把 Bucket 改为公开以满足复制需求。
- 已签发地址在剩余有效期内的撤权限制必须明示;不能承诺撤销分享后所有外部签名立即失效。已经下载的内容也无法收回。
- 视频续期不能简单定时替换正在播放的 `src`。媒体控制器维护播放位置、暂停状态和当前请求版本;在需要续期或过期恢复时受控换源、恢复位置,最多刷新重试一次,失败可见。
- 验收必须覆盖播放、暂停后继续、跨过签名有效期后的拖动与新 Range 请求;不能以短视频能开始播放作为长视频通过的证明。
- 下载文件名、Content-Disposition 等由访问策略和适配器统一处理;不能仅为更改文件名而退回平台整文件转发。
## 5. 唯一分发策略与配置模型
分发顺序固定为:**校验业务授权 → 校验资源 ready → 解析变体 → 解析该对象实际位置 → 校验用途能力和访问保护 → 签发访问描述或返回结构化错误**。
| 实际条件 | 决策 | 限制 |
| --- | --- | --- |
| 本地对象 | `platform-local` | 如实呈现;不能生成看起来像 OSS 的假地址 |
| 对象位置绑定有效 CDN,路径映射与用户访问保护适用于该用途 | `cdn` | 先判断可用 CDN,再考虑源站是否私网或 S3 |
| 无 CDN,源站可公网安全访问且可签发读取凭据 | `origin` | 计作源站直连,不宣称使用 CDN |
| 私网源站、无可用 CDN,配置明确允许平台读取 | `platform-proxy` | 必须记录原因、流量、大小/并发限制;复制时不能称为 OSS 直链 |
| 私有内容的 CDN 用户鉴权缺失、配置损坏、签名失败或凭据失效 | 结构化失败 | 不发布无保护地址,不因捕获异常就自动转代理 |
| 字节处理需要 CORS,但分发端未满足要求 | 明确失败;仅在服务端策略授权时采用受控代理 | 浏览器不能用 `proxy=1` 自行扩大权限或绕过流量约束 |
CDN 是分发配置,不是存储厂商别名。源站权限、CDN 回源权限、用户访问 CDN 的权限分别建模;本项目不得把「能回源」当作「私有资源可以匿名分发」。
复用 `StorageLocation` 与现有历史位置解析,为位置增加结构化 `delivery` 配置,区分域名/路径映射、用户访问鉴权方式、代理例外、期限和浏览器能力。优先沿用现有配置存储与加密边界,不为了抽象额外新建一套存储中心;签名密钥不返回给浏览器。
原有 `cdnBaseUrl` 不足以推导私有 CDN 鉴权能力。实施时盘点实际 CDN 产品和鉴权模式,补对应签名适配器;未验证配置保持不可用,不默认公开或自动猜算法。选择「必须走 CDN」时配置失败应报错,不能自动降为源站;允许源站降级则必须由同一 policy 明确配置并记录原因。
连接测试拆成源站读写检查与分发检查。后者覆盖浏览器 GET/HEAD/Range、所需 CORS、响应头、签名过期和未授权访问,并关联配置版本。保存配置不等于已经验证可用;不擅自修改云端 ACL、CORS、DNS 或 CDN 配置。
历史资源按其绑定位置寻址;改默认厂商只影响新写入。相同位置的 CDN/密钥变更可由位置解析统一生效,并使访问缓存失效;不得把另一 Bucket 的对象拼到新域名。
## 6. 缓存与写入成功语义
### 6.1 展示缓存和字节缓存分离
- 普通图片、视频、音频展示直接使用访问描述,交给浏览器/CDN 按媒体协议读取。删除生成完成、进入画布、开始播放后默认整文件缓存的流程。
- Blob 只为图片编辑、像素读取、合成、导出、复制图片内容及明确离线操作等字节消费者产生。下载原件优先原生直链下载,不一律先在 JS 中抓取全量文件。
- `media-bytes` 统一检查 `response.ok`,传递取消信号;云直连不附带平台 Cookie。凭据过期可重新解析一次,不把所有网络/CORS 错误都判为过期,更不能无声启动代理。
- 字节缓存按用户 scope、资源、实际变体及内容版本隔离,有大小/总量限制、请求去重与 Blob URL 回收。清晰区分缓存成功、云端保存成功和生成成功。
- 访问描述缓存另外按 scope、授权场景、资源、变体、用途去重,按服务端时间信息留余量刷新;公开/分享会话不得复用登录用户缓存。
- 切换账号时取消在途请求、清理访问与字节缓存引用;响应提交前核对发起时的 scope,禁止旧请求写入新账号缓存。签名 URL 不落入 localforage/localStorage。
- 采用微批量解析避免一个画布逐节点产生 N+1 请求;无视图消费者的资源不提前签发或下载。
### 6.2 写入与变体
资源成功以服务端实际对象和资源记录持久化成功为准。任务同步、项目素材同步不再等待浏览器拿到完整视频 Blob;元信息来自服务端或按需探测,读取失败不得捏造宽高、时长或成功状态。
资源写入的本地降级从隐式 catch 改为明确存储策略:选择 OSS 且未允许本地降级时写入失败向上抛出;允许降级时记录实际 local 位置、返回可见状态并计入例外指标。任务媒体已有的强校验不被削弱。此处只收敛存储成功语义,不修改任务计费规则。
已有本地资源不会因修改分发策略自动变为云资源。若要迁移,另行执行带预检、复制、完整性校验、引用切换与延迟清理的可回滚作业;未经确认不搬迁或删除现有对象。
现有本地播放副本纳入统一变体解析,不改变转码范围。未来新增云端副本时必须记录副本的真实位置并复用同一存储与分发接口,不能重新在播放器/handler 写一个云端特例。
## 7. 全入口迁移与旧实现退场清单
| 入口/旧职责 | 目标归属 | 删除或收敛要求 |
| --- | --- | --- |
| 画布、素材库、项目封面、任务预览、图片预览、音视频播放器、编辑器媒体展示 | `media-access` + 展示 hook | 页面不得直接拼平台或 OSS 文件地址 |
| 节点「复制地址」、复制图片失败后的地址操作、下载入口 | 统一 copy/download 用途 | 不优先复制 `metadata.content`,不继续调用旧 `oss-url` helper |
| `resourceFileUrl`、`resourceProxyFileUrl`、`playbackVariantUrl`、`resolveResourceUrl` 等 | 引用解析与访问合同 | 删除出口选择;稳定文件路由仅作为同一 policy 的 HTTP 适配 |
| `getResourceOSSUrl`、独立直链缓存、`DirectResourceURL` | 唯一访问签发服务 | 调用方清零后删除 `/oss-url` 及独立分支,不长期并行 |
| `resolveImageUrl`、`resolveMediaUrl`、文件/图片存储服务、CachedResourceImage | 统一访问生命周期 | 存储与展示不再各自解析直链或续期 |
| 生成结果恢复、任务输出、素材同步 | 持久资源引用 + 访问描述 | 不保存签名 URL,不依赖完整 Blob 才标成功 |
| 缓存、图片编辑、导出、剪贴板内容操作 | `media-bytes` | 删除自动整文件预取和 `allowProxyFallback` 隐式回退策略 |
| 用户文件、公开签名文件、管理员存储预览、调用日志媒体 | 各自授权 + 统一访问服务 + HTTP 执行器 | 删除 handler 自选 provider、直接调用云读取实现的旁路 |
| 画布分享、站点外观、公告资源 | 现有业务授权后使用同一分发策略 | 公开入口只能访问业务实际绑定资源,不能成为任意 resourceId 下载器 |
| 模型参考输入、服务端导出/转码/导入 | 外部读取签发或内部 OriginReader | 输入 URL 只在出站前解析;内部字节处理不绕经浏览器 CDN 再代理 |
| `ForceDirect/ForceProxy`、`direct/proxy` 查询参数 | 服务端集中 policy | 不再接受调用方强制选择出口,OpenAPI 与测试同步删除 |
| 存储配置、历史位置解析、连通性测试 | 位置解析 + 分发配置/能力检查 | 一次改配置影响全部入口,不修改页面,不为每个路由新增开关 |
上述是必须覆盖的入口族,不声称静态列举已经穷尽所有调用点。实施第一阶段生成逐调用点清单,覆盖直接 import、封装转调、动态路由及外部 SDK 输入;清单中每项都要有迁入位置和验证用例,不能只搜索一个函数名就宣布完成。
## 8. 错误、安全与可观测性
- 权限仍在业务 service/域内校验,授权后的内部资源描述不得由客户端构造。管理员、分享、公开槽位各有授权规则,但只有一套分发规则。
- 非所属资源、不存在资源、未 ready、变体不可用、分发配置无效、签名失败、分发端不可读、代理不允许分别有机器可读 reason;沿用项目错误信封,不解析错误文案。
- 代理只读取已授权的受管对象,不接受任意 URL;保留出站 SSRF、私网精确放行、超时和取消约束,不为 S3 内网部署放开全网访问。
- Range、206、416、If-Range、条件缓存、MIME、Content-Disposition 在统一 HTTP 执行器及签名适配器中验证;读取失败不返回伪成功空文件。
- 每次访问决策记录场景、用途、实际位置类别、变体、delivery、结构化原因和配置版本。日志不记录签名完整 URL、查询参数、Cookie、密钥或分享凭据。
- 平台正文发送字节按 local/proxy 及原因分类;签发接口和重定向响应单独计量。资源 ID 可作受控日志关联,但不作高基数指标标签。
- 观察直连/代理占比、代理发送字节、重复整文件下载、签发失败、续期失败、CDN 命中与回源字节。账单比较按同等资源量/访问量归一化,不以平台请求数下降代替成本证明。
## 9. 实施阶段与落地顺序
这些是同一架构的实施阶段,不是「先只止血,后续再说」。阶段内允许未完成的代码演进,最终不保留新旧两套策略。
| 阶段 | 交付物 | 进入下一阶段的门槛 |
| --- | --- | --- |
| A:合同与回归基线 | 全调用点清单、资源引用/访问/错误合同、位置与授权矩阵、现有测试映射 | 能明确每个入口的授权方、策略方、字节读取方;生产未知项单列 |
| B:后端纵向收敛 | `assets` 访问服务、存储/分发适配器、批量 API、共享 HTTP 执行器、位置配置与 OpenAPI | local、各云厂商、S3 公网/私网、CDN 私有访问、变体和所有授权场景的契约测试通过 |
| C:全部消费者迁入 | 前端访问/字节层、生成同步解耦、分享/后台/公开/模型输入迁入 | 迁移清单清零;删除旧接口、布尔策略、旁路与自动整文件缓存;前后端版本配套 |
| D:架构与真实链路验收 | 依赖约束测试、最小专项回归、真实浏览器网络记录、经授权的云配置小流量验证 | 下节验收项完成;例外流量有明确来源;真实费用收益另用线上观察确认 |
实施同步维护 HTTP API、资源存储、代码地图及部署专题;如新增数据库字段,同步数据库说明。已实现但未获得用户确认的结果进入待验收记录,不把本设计登记为已完成功能。
本轮不更改数据库、云配置或生产路由。若实施涉及新增分发配置,先预检现有配置并说明变更影响;保留版本化配置/发布产物用于回滚。原则上不改变既有稳定引用编码;如确需改变持久合同,必须先给出备份、迁移与旧版本可读性方案,不能只回滚二进制。
## 10. 硬性验收门槛
### 架构约束
- [ ] 分发决策仅存在于 `DeliveryPolicy`;供应商签名只存在于对应适配器。
- [ ] 新增 CDN 或更换签名实现只改分发配置/适配器及其测试,不改画布、播放器、复制按钮和业务 handler。
- [ ] 增加一个媒体展示入口只消费统一合同,不引入新的地址拼装或 provider 判断。
- [ ] 通过依赖约束/静态扫描检查旧导出、旧查询参数及直接读取旁路;许可例外集中列出,禁止用模糊忽略规则绕过。
- [ ] 迁移清单无未处理调用方,没有长期保留的兼容分发链。
### 行为与安全
- [ ] CDN、源站直连、local、明确批准的私网代理覆盖原件/播放副本;配置变化后历史资源仍能按实际位置读取。
- [ ] 所有页面展示与右键复制均遵守同一策略;云资源复制为新鲜原件直链,过期提示准确;Blob/local 不被伪装为云资源。
- [ ] 本地播放副本、副本尚未就绪/失败、Range/拖动、签名过期、重新登录及账号切换均有针对真实实现的测试。
- [ ] 用户越权、管理员校验、分享期限/撤销边界、公开槽位越界、私有 CDN 未鉴权访问均验证拒绝语义。
- [ ] CORS/网络失败不会静默变平台代理;必需字节读取失败和剪贴板失败有可见错误,取消不被当成普通失败重试。
- [ ] 打开画布、生成成功、开始播放不再触发无业务必要的整视频下载;编辑/导出等真正需要字节的路径仍可用。
### 流量与上线
- [ ] 对已验证可直连的云资源样本,逐入口检查最终 CDN/源站域名;平台不发送该媒体的 200/206 正文,仅允许访问描述或重定向。
- [ ] 平台所有媒体正文发送都有可解释的 local/approved-proxy 原因;不能因路径叫 `/api` 就统计成代理,也不能把 CDN 回源当命中。
- [ ] 浏览器真实请求与服务端计量一致,未产生播放 Range 加后台完整下载的重复流量。
- [ ] 小流量上线、配置异常和发布回滚有记录;没有擅自公开私有存储、删除旧对象或修改生产 ACL。
## 11. 本次评估结论与经验约束
方案具备落地基础:仓库已有资源 ID、实际位置/历史配置、权限边界和部分直链能力;主要工作是重置职责与合同、迁移全部调用方、删除重复策略,而不是重新发明对象存储系统。改动横跨资源核心流程、权限、缓存和部署配置,应按 L2 完整实施,不适合将其承诺为几个文件的小修复。
尚需实际环境验证的是 CDN 产品及用户访问鉴权方式、CORS/Range、历史 local 占比与各路径流量成本。它们决定具体配置、签名适配与例外范围,不改变「单一策略入口」的架构选择;本轮不把未知项当作已验证事实。
上一版评估虽然识别了分散决策,但没有把策略的唯一归属、旧实现删除和未来变更影响面作为硬完成条件,容易让止血成为终点。本次已按 **症状 → 职责图 → 单一合同 → 全调用方迁移 → 旧路径删除 → 防回归约束** 推进;剩余工作只属于真实云链路验收、供应商能力补齐和调用方继续收敛,不再新增第二套资源访问策略。

View File

@@ -4,7 +4,9 @@
## 设计沉淀
- [短信渠道插件与登录注册策略](design/sms-channels-and-auth-policy.md):阿里云/腾讯云官方 SDK 候选基线、多渠道与模板路由、发送记录、验证码安全、短信和邮箱登录注册组合及分阶段验收(设计提案,未实施)。
- [资源访问与 OSS/CDN 分发架构重构](design/resource-delivery-architecture.md):资源身份、场景授权与分发策略分离,统一访问合同、全入口迁移、旧实现退场及流量/安全验收门槛(核心代码已实施,待真实 OSS/CDN 链路验收)。
- [短信渠道插件与登录注册策略](design/sms-channels-and-auth-policy.md):阿里云/腾讯云官方 SDK 候选基线、多渠道与模板路由、发送记录、验证码安全、短信和邮箱登录注册组合及分阶段验收(核心代码已实施,待真实 OSS/CDN 链路验收)。
- [画布批量生成一致性治理](design/canvas-consistency-repair.mdx):节点丢失、批次状态与引用解析的根因、已实施边界重构、回归证据和仍待验收的性能/同步场景。
@@ -43,6 +45,7 @@
- [ADR-0006:语音滤镜与项目模板](adr/0006-voice-filters-and-templates.md):补充评估 Concat 剩余两个缺口——语音滤镜走 Web Audio 预览近似 + ffmpeg 音频滤镜图导出,模板走项目快照服务化;均为预设插件且服从单一滤镜图计划。
- [ADR-0007:AI 编辑交互](adr/0007-ai-editing-interaction.md):对话式剪辑作为预设插件——AI 输出受约束命令 JSON(schema 校验 fail-closed),与手势命令同队列同撤销栈;≤3 条直接执行、批量改动 diff 预览待确认。
- [ADR-0008:自研 UI 组件层与 AntD 替换](adr/0008-ui-component-layer-antd-replacement.md):`components/ui` 重建为自研组件层(Celadon UI),吸收 dbx 契约形态(族目录+barrel)与 BoardUI 设计深度(语义状态矩阵/复合排版/RAC 原语/动效纪律/agentic 产品块),分阶段替换 AntD(依赖面 180 文件/41 导入)最终脱离;试点画布工具条/节点徽章,唯一实施计划见 [plans/ui-kit-rollout-plan.mdx](plans/ui-kit-rollout-plan.mdx),活规范见 [plans/ui-design-system.mdx](plans/ui-design-system.mdx)。
- [ADR-0009:媒体产出模型与选用模型分开记录](adr/0009-produced-model-separate-from-selection.md):生成成功时冻结产出模型,选用模型继续随下拉变化;角标按当前目录解析展示名。
## 本地协作文档(不随仓库分发)