feat(canvas): 画布同步 - 新增操作事务、预演保存与并发恢复能力

This commit is contained in:
ddcat666
2026-10-11 22:19:20 +08:00
parent 6dedc5973c
commit 5f359f2b1d
63 changed files with 3235 additions and 6193 deletions

View File

@@ -31,9 +31,7 @@ Schema 45 新增 `upload_reservations`:`id`、`user_id`、`size`、`day`、`ex
## 结构迁移与版本
当前结构版本为 49。版本 49 `banner_sort_order` 为 `banner_announcements` 增加可配置的 `sort_order` 展示顺序;版本 48 `channel_proxy_url` 为 `model_channels` 增加渠道级出站代理地址 `proxy_url`,支持 `socks5://`、`http://`、`https://`;留空时沿用全局代理。代理地址只返回管理员视图,普通用户目录不暴露渠道代理或其中的认证信息。版本 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`,不替历史流程补造同意记录。
当前结构版本为 50。版本 50 新增 `canvas_mutation_receipts`,按 `(user_id, canvas_id, mutation_id)` 唯一记录画布 operations 请求摘要、确定性结果和提交 revision,供响应丢失后的幂等重放使用;receipt 与画布内容、revision 在同一事务提交。版本 49 `banner_sort_order` 为 `banner_announcements` 增加可配置的 `sort_order` 展示顺序;版本 48 `channel_proxy_url` 为 `model_channels` 增加渠道级出站代理地址 `proxy_url`,支持 `socks5://`、`http://`、`https://`;留空时沿用全局代理。代理地址只返回管理员视图,普通用户目录不暴露渠道代理或其中的认证信息。版本 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 迁移完成后也校准序列。 `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` 的版本与校验和记录。
@@ -68,19 +66,20 @@ Schema 45 新增 `upload_reservations`:`id`、`user_id`、`size`、`day`、`ex
版本 3 会将 `wardrobe`、`weapon`、`accessory` 迁移为 `prop`,将 `style` 迁移为 `material`,并把原先归入 `other` 的图片、视频、音频和 3D 模型迁移为 `material`。迁移只合并重复的待确认候选:保留最早记录,其余标记为 `ignored`;已经确认的资产不会被自动删除或归并。
## 画布版本与快照
## 画布 operations 与幂等回执
| 表 | 用途 | 关键边界 |
| --- | --- | --- |
| `canvas_projects` | 当前画布内容及递增 `revision`(非空,默认 1) | 客户端新建传 0;后续写入按用户、ID、原版本原子校验,成功才加 1 |
| `canvas_snapshots` | 结构 JSON、版本、标题、节点/连线数、字节数、内容时间和备份时间 | `(canvas_id, revision)` 唯一;列表只读摘要,正文按需加载;不复制媒体文件 |
| `canvas_snapshot_resources` | 历史快照引用的资源 | `(snapshot_id, resource_id)` 联合主键;资源外键禁止删除被引用记录 |
| `canvas_projects` | 当前画布内容及递增 `revision` | 所有正常画布写入都通过 operations 服务更新;对象删除后迟到更新不能隐式重建 |
| `canvas_mutation_receipts` | 保存 `(user_id, canvas_id, mutation_id)`、请求摘要、确定性结果和提交 revision | 唯一请求身份与画布内容在同一事务提交;同 ID 不同摘要返回冲突 |
浏览器、云端 Agent 和创作任务共用保存事务。内容变化时备份旧内容,普通备份最短间隔 5 分钟,每画布最多保留 20 份;恢复前强制备份当前内容。版本号每次成功写入递增,快照按间隔采样,两者不是一一对应。画布关联项目或解除关联同样推进版本,使旧页面的全量保存失效。
`canvas_mutation_receipts` 不保存整批历史版本,只保存一次请求的恢复结果。服务端在事务内锁定画布、创建或读取 receipt、校验字段/对象基线、应用 operations、递增 revision、写入结果并完成 receipt;事务失败时不留下可重放的半成品。客户端可在响应丢失、断网或刷新后用原始 `mutationId` 和原始 payload 重放。
恢复历史保留当前画布 ID、归属和项目关联,按当前版本做 CAS 后形成新版本;不会把 revision 改回历史值。快照写入、引用保护、恢复和保留数量清理在同一事务内完成。资源删除检查与异步物理删除任务均检查历史引用(包括同对象的资源别名),仍被历史引用的媒体延后回收;删除画布时清理该画布快照及引用。
画布操作支持 `create_canvas`、`create_node`、`patch_node`、`remove_node`、`create_connection`、`update_connection`、`remove_connection`、`set_field`。创建操作显式创建对象;更新和删除要求对象存在,并以 `before`、`expected` 或 `beforeValue` 做字段/对象级并发校验。删除节点同时删除其连接,迟到的媒体回写返回对象缺失冲突。`viewport` 是本地视图状态,不进入远端 operations。
升级不改变已有画布正文,也不会凭空产生升级前历史。前后端应一起升级:旧客户端缺少 revision 的保存返回 428,需刷新客户端。
旧的画布版本历史、恢复和整批 PUT 不属于当前用户写入合同;数据库迁移保留既有历史表以兼容已部署数据,但新写入不依赖它们。Agent、媒体回写、预演台、创作提交及导入路径都复用同一 operations 服务。
画布/素材结构化配额使用 `UserStructuredStorageUsage`,仅聚合当前用户的 `assets` 和 `canvas_projects` 数量及 `payload_json` 字节数。SQLite 使用 `length(CAST(... AS BLOB))`,PostgreSQL 使用 `octet_length(...)`,避免中文按字符计数;空值按零字节处理。operations 通过事务 repository 读取用量后再写入,不增加缓存或配额表,不变更 schema。完整 `UserStorageUsage` 仍用于需要任务及日志统计的路径,画布保存不再为未使用的任务/日志指标扫描大字段。
## 已部署 PostgreSQL 的素材 bytes 修复

View File

@@ -0,0 +1,69 @@
---
title: 画布 operations 同步协议
description: 画布对象级写入、并发校验、幂等重放与删除边界
---
# 画布 operations 同步协议
## 请求合同
画布写入使用 `PATCH /api/canvas-projects/:id/operations`。请求体为:
```json
{
"mutationId": "客户端生成的稳定请求 ID",
"baseRevision": 12,
"operations": [
{
"type": "patch_node",
"id": "node-1",
"patch": { "metadata.prompt": "新的提示词" },
"before": { "metadata.prompt": "旧的提示词" }
}
]
}
```
`mutationId` 是重试身份,不是版本号。请求超时、响应丢失、断网、刷新或后台自动重试时,必须使用相同的 `mutationId`、`baseRevision` 和原始 `operations`。修改原始 payload 后复用 ID 会返回 `canvas_mutation_id_reused`。
`baseRevision` 是生成请求时的已确认版本,用于记录和诊断。真正的并发校验在操作本身:字段补丁使用 `before`,对象更新或删除使用 `expected`,顶层字段使用 `beforeValue`。不同字段可以合并;同一字段或同一对象的基线不匹配时返回冲突。可选顶层字段不存在时,客户端必须在 JSON 中将 `value` 或 `beforeValue` 编码为 `null`,不能让 `undefined` 被序列化为缺失属性。服务端仅兼容早期客户端对这些可选字段省略属性的请求,并按 `null` 参与同样的基线校验。
## 操作类型
- `create_canvas`:只在画布不存在时创建,第一项必须是该操作。
- `create_node`:显式新增节点,已有 ID 不会被覆盖。
- `patch_node`:对已有节点按字段路径更新,节点缺失或字段基线不匹配都会失败。
- `remove_node`:按完整对象基线删除节点,并删除关联连接。
- `create_connection`:显式新增连接,两个端点必须存在。
- `update_connection`:按连接对象基线更新,连接缺失或端点缺失都会失败。
- `remove_connection`:按连接对象基线删除。
- `set_field`:更新白名单中的画布字段,并校验 `beforeValue`;字段不存在时用 JSON `null` 表示删除或缺失基线。
只有 create 操作可以创建实体。删除后的迟到编辑、媒体回写或 Agent 更新得到 `canvas_object_missing`,不会隐式重建节点、连接或画布。画布删除后的迟到请求同样不会重建画布;只有新的 `create_canvas` 才能显式重新创建。
## 事务与 receipt
服务端事务按以下顺序处理请求:锁定画布写入、创建或读取 receipt、核对请求摘要、读取当前画布、应用 operations、校验媒体引用、递增 revision、写入画布和完成 receipt。画布内容、revision、receipt 的结果 JSON 在同一事务提交,任一步失败都会回滚。
`canvas_mutation_receipts` 使用 `(user_id, canvas_id, mutation_id)` 唯一约束。相同 ID 和相同摘要返回之前保存的确定性结果,不再次应用操作;相同 ID 和不同摘要返回冲突。receipt 不替代当前画布,也不保存整批版本历史。
结构化容量校验仍在同一事务内使用事务 repository 读取实时用量,但只统计当前账号的素材/画布数量和 JSON 字节数。此路径使用 `UserStructuredStorageUsage`,不扫描任务正文、结果或 API 请求/响应日志;任务与日志配额继续使用原有完整统计。容量上限、创建数量限制、存储锁、CAS 和 receipt 原子提交不变,也不引入可能过期的配额缓存。
## 浏览器 journal 与刷新
浏览器在发送请求前把以下内容写入账号隔离的本地 journal:
- 已确认的画布基线;
- 原始 operations 请求及 mutationId;
- 生成请求时的本地投影;
- 创建时间。
响应成功后删除对应未确认请求,并把服务端返回的完整画布保存为新基线。响应丢失时保留 journal,刷新后使用原始请求重放;请求发出后产生的新编辑不会改写原请求,而是在确认后从新基线生成下一批 operations。服务端完整读取或 Agent 刷新只能更新已确认基线,不能覆盖仍未确认的本地请求之后的修改。
`viewport` 是本地视图状态,不进入 operations。媒体文件先完成资源登记,再提交引用画布的 operations,避免产生只有节点没有资源记录的远端状态。
## Agent 和媒体回写
普通编辑、Agent、媒体生成终态、预演台、创作提交、LibTV/TapNow 导入都必须经过同一 operations 服务。Agent 事件里的 `canvasPatch` 只是客户端通知:小变更携带有界对象增量;删除、重排、顶层字段变化或事件过大时使用 `requiresRefresh: true`,客户端重新读取后继续处理未确认 journal。
媒体终态只更新仍存在且任务绑定未变化的节点。目标节点已删除、任务绑定已改变或资源不可用时,任务记录保留,画布不创建幽灵节点。

View File

@@ -65,8 +65,8 @@ description: 从产品功能快速定位主要代码入口
| 页面装配和跨域动作协调 | `web/src/pages/canvas/project.tsx` |
| 画布领域规则和生成协议 | `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` |
| 画布 operations 同步与本地 journal | `web/src/services/user-data-sync.ts`、`web/src/services/api/user-data.ts`、`web/src/lib/canvas/canvas-content.ts`、`web/src/pages/canvas/canvas-sync-status.tsx`、`web/src/stores/use-sync-progress-store.ts` |
| 内部资源保护与维护快照 | `backend/internal/canvas/canvas_history.go`、`repair_resources.go`、`backend/internal/repository/canvas_history.go`;仅供资源完整性维护和审计,不提供用户历史树、预览或恢复入口 |
| Patch 历史、生成任务和重试控制 | `web/src/pages/canvas/use-canvas-history.ts`、`use-canvas-generation.ts`、`use-canvas-generation-executor.ts`、`use-canvas-generation-retry.ts` |
| 文本、图片、视频和音频生成执行 | `web/src/pages/canvas/canvas-*-generation-executor*.ts` |
| 云端 Agent、助手面板、导演台和全局快捷键控制 | `web/src/components/canvas/canvas-cloud-agent-panel.tsx`、`web/src/pages/canvas/use-canvas-assistant-visibility.ts`、`use-canvas-director.ts`、`use-canvas-keyboard.ts`;画布操作契约在 `web/src/lib/canvas/canvas-operation-contract.ts` |

View File

@@ -71,19 +71,19 @@ handle 放错端点、格式错误或行 ID 不存在时,工具错误回执提
文本生成任务的 `metadata.edit = "layer-planning"` 使用图片参考时,服务端从当前用户归属的就绪资源读取实际像素并转换为内嵌图片,清除附带的旧 URL。图片数量与字节数沿用文本模型能力及资源读取上限;不允许其他用户资源或客户端内嵌内容替换资源文件。普通生成的签名 URL 策略保持不变,任务端点、账务与数据库结构沿用已有合同。
## 画布资源完整性与 409
## 画布资源完整性与 operations 写入
画布写入在保存历史快照前校验当前文档中的资源引用;引用须属于当前用户且处于 `ready`。引用收集器递归遍历受支持的资源字段,因此 `nodes[节点 ID].metadata.videoPreview.storageKey` 等嵌套派生封面也会参与检查。历史快照保存会一并校验新版本引用;若资源在保存期间缺失或状态变化,事务回滚,画布 revision 不递增。
画布 operations 应用前校验当前文档中的资源引用;引用须属于当前用户且处于 `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。
HTTP 409 不能一概解释为并发冲突:对象或字段基线冲突使用 `reason: conflict`;资源完整性失败使用 `reason: canvas_history_resources_missing`,并在 `details` 中返回 `resourceIds` 和 `missingResources`(含资源 ID、JSON 路径、节点 ID、引用类型及 `source`)。客户端应按 `reason` 分流,不解析 `msg` 猜测原因。普通 operations 不会绕过资源校验或 revision 语义,维护修复也不能把缺失主媒体写回为有效画布。
运维扫描/修复仅处理当前画布中可安全清除的失效 `videoPreview`;不删除主媒体、不扫描并改写所有历史快照,也不替代备份。使用目标数据库对应的新版本二进制,先在维护窗口备份并执行只读扫描:
运维扫描/修复仅处理当前画布中可安全清除的失效 `videoPreview`;不删除主媒体、不扫描并改写所有维护记录,也不替代备份。使用目标数据库对应的新版本二进制,先在维护窗口备份并执行只读扫描:
```sh
CANVAS_BACKEND_DATA_DIR=/明确的数据目录 ./migrate-schema repair-canvas-resources
```
复核 JSON 报告后,只有 `repairable` 项可由明确授权的操作员再次运行 `repair-canvas-resources --apply`。命令要求显式配置 `CANVAS_BACKEND_DATA_DIR` 或 `DATABASE_URL`,逐画布按版本 CAS 保存并保留原始历史;有未解决主媒体/素材绑定问题时以非零退出,不会自动恢复或重传。发生并发编辑时停止并重新扫描,不能用强制覆盖代替恢复有效历史或重新上传。
复核 JSON 报告后,只有 `repairable` 项可由明确授权的操作员再次运行 `repair-canvas-resources --apply`。命令要求显式配置 `CANVAS_BACKEND_DATA_DIR` 或 `DATABASE_URL`,逐画布按当前 revision CAS 保存并保留维护审计记录;有未解决主媒体/素材绑定问题时以非零退出,不会自动重传或重建节点。发生并发编辑时停止并重新扫描,不能用整批快照替换或绕过 operations 校验。
## 生成成功后的重试保存
@@ -213,24 +213,25 @@ Token 报价的 `estimated` 为 `true`,`quantity` 是预留 Token 数。参考
上传、工作流结构查询、取消查询和取消请求也不参与财务归属,即使日志被标记为可计费;它们不能代替生成请求承载订单金额。订单是否已结算以账单状态为准,保留人工核账后结算订单的统计能力。
## 画布保存、历史与恢复
## 画布保存与 operations 同步
画布内容使用数据库递增的 `revision` 做乐观并发校验;`updatedAt` 仅用于展示,不作为版本依据。新建 `PUT /canvas-projects/:id` 的 `project.revision` 为 0,已有画布必须带上读取到的版本。数据库在同一次条件更新中匹配 ID、用户与版本,成功后返回 `data.project` 摘要及递增后的版本;缺失版本返回 428,陈旧版本返回 409,非法版本返回 400。客户端不能获取新版号后直接重发旧内容。
画布写入统一使用 `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`。 |
| `GET /canvas-projects/:id/history` | 返回 `snapshots` 摘要与 `currentRevision`,不批量读取历史正文 |
| `GET /canvas-projects/:id/history/:snapshotId` | 按需返回 `snapshot` 摘要和 `project` 历史正文 |
| `POST /canvas-projects/:id/history/:snapshotId/restore` | 请求 `{ revision }` 为当前画布版本,成功返回新版本摘要;恢复前强制备份当前内容 |
| `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 画布。 |
历史接口先验证画布归属;无权访问、已删除画布或不存在的快照返回 404。恢复沿用画布写入限流和素材校验,在同一事务完成版本校验、备份、内容写入及历史资源保护。恢复保留当前画布 ID 和项目归属,生成新版本,不倒退版本号。升级前的内容无法补建历史。
`create_node`、`create_connection` 和 `create_canvas` 是唯一的创建语义;更新、删除和连接操作都要求目标存在并通过对象基线校验。节点删除会同步移除关联连接,迟到媒体回写或编辑更新返回结构化冲突,不会复活已删除节点。请求摘要与确定性结果写入 `canvas_mutation_receipts`,画布内容、版本递增和 receipt 在同一数据库事务完成;同一 `mutationId` 和相同摘要返回原结果,摘要不同返回 `canvas_mutation_id_reused`。
平移和缩放只持久化浏览器视口,内容 PUT 忽略客户端 `viewport` 和 `remoteContentHash`。前端遇到版本冲突(409/428)会先核对云端并尝试合并;无法合并时保留账号隔离的本地草稿、暂停该画布自动提交,并提供加载最新版和版本记录查看入口。载入最新版前草稿写入失败时停止替换,当前编辑内容保持不变;其他画布仍可保存。
前端先把未确认请求和请求基线写入账号隔离的本地 journal,再发 HTTP 请求。journal 保存已确认基线、原始请求、请求生成时的本地投影和创建时间;刷新后只重放原请求,随后把请求之后的新编辑按新的基线生成下一批 operations。服务端返回完整画布时,客户端只合并仍未确认的本地 mutations,视口仍属于本地视图状态,不进入远端 operations。
单画布读取会先查询归属用户的版本元数据,再判断 `If-None-Match`;命中时不会读取大型画布正文。未命中时返回完整项目 JSON。正文达到 1 KiB 且客户端声明接受 gzip 时响应使用 gzip 压缩,并带 `Vary: Accept-Encoding`;因此客户端应以 HTTP 状态码区分 `200` 正文和 `304` 未变化,不要把 `304` 当作业务错误。
云端 Agent、媒体生成终态回写、预演台、创作提交以及 LibTV/TapNow 导入都复用同一 operations 服务。Agent 事件中的 `canvasPatch` 只是一种有界通知;过大、删除或无法完整表达的变更使用 `requiresRefresh: true`,客户端重新读取后按未确认请求继续重放。用户界面不提供版本记录、加载云端版、覆盖云端或手动重试入口,失败请求由后台自动重放并在同步状态中显示当前状态。
云端 Agent 与创作任务的写入共用版本和历史事务;Agent `canvasPatch` 携带 `baseRevision`、`revision`,客户端只接受连续增量,跳过重复事件,缺口或同字段冲突时保留草稿并重新核对。删除、重排或顶层内容变化无法由增量完整表示时,事件携带 `requiresRefresh: true` 且不附 `canvasPatch`,客户端完整核对后才推进基线;撤销事件同样触发核对。保存日志仅记录请求/链路 ID、画布/用户 ID、版本、节点数量变化及 HTTP 状态,不记录正文、提示词或媒体 URL。
平移和缩放只持久化浏览器视口,不生成远端 operations。保存日志仅记录请求/链路 ID、画布/用户 ID、版本、节点数量变化及 HTTP 状态,不记录正文、提示词或媒体 URL。
operations 的结构化配额在提交事务内实时统计账号的素材/画布数量和 JSON 字节数,不为画布保存扫描任务正文及 API 请求/响应日志。接口请求、响应、容量上限及错误语义均不变;性能排查应分别记录成功写入、同请求重放和冲突请求的耗时,冲突请求提前退出不能代表完整提交耗时。
## 欢迎页开放配置

View File

@@ -13,6 +13,7 @@
"newapi-channel-2",
"system-update",
"protocol-plugins",
"canvas-data-structure"
"canvas-data-structure",
"canvas-operations-sync"
]
}

View File

@@ -129,15 +129,15 @@ AutoDL 视频模型在“能力与参数 → 画面规格”直接多选当前
技能库保留原有头部和技能广场共享分类;在“我的技能 / 我创建的”视图增加左侧个人技能库分类及技能数量。用户可为已加入或自建技能创建、分配个人分类;管理员也可创建对所有用户可见、可选、可用于筛选的分类。个人分类仅影响当前用户的技能库和画布 Agent,不改动技能广场共享分类。删除分类只解除相关归类,技能仍保留。画布 Agent 的“我的技能 / 已启用”列表可按个人或平台技能库分类快速筛选,市场页仍按共享分类浏览。
## 画布保存保护与版本记录
## 画布 operations 同步与本地恢复
旧页面或旧缓存不能覆盖云端新内容。每次内容保存核对服务端版本,发生冲突会停止该画布自动提交并保留本地草稿;平移、缩放和打开画布不触发整份内容保存。画布顶部显示保存状态,可保留草稿后加载云端最新版。
画布内容按字段和对象生成 operations,节点、连线和顶层业务字段分别提交;平移、缩放等 viewport 只保存在本地。每次请求携带唯一 `mutationId` 和可选基线版本,服务端在同一事务中校验对象存在性、字段基线、资源归属并保存回执。可选顶层字段不存在时用 JSON `null` 表示,避免 `undefined` 被序列化为缺失属性。相同请求安全重放,响应丢失、断网、刷新或服务端暂时不可用时,浏览器保留已确认基线与未确认请求,后台使用原 ID 和原 payload 自动继续处理。结构化配额只聚合当前用户素材与画布数据,不扫描任务和 API 日志大字段,容量上限与事务语义不变。
「版本记录」在桌面右侧按日期展开历史树,小屏使用抽屉。点击版本进入独立只读预览,可以缩放查看或下载;返回当前画布后继续编辑。恢复历史会先保存本地草稿、备份当前云端内容,再创建一个新版本。历史只保存画布结构和媒体引用,不复制视频文件;普通快照最短间隔 5 分钟,每个画布保留最近 20 份,因此每次递增的版本号不一定都有快照。历史引用的媒体受到删除保护。本地绘图笔画不在云端历史中,草稿和历史导出不混入当前笔画。
不同字段的并发修改可以合并;同一字段或同一对象的基线不一致会返回冲突,当前本地修改继续保留。删除后的迟到节点、连线或媒体更新不会重新创建对象,只有显式新增 operation 可以创建实体。界面只显示同步状态,不提供版本记录、加载云端版、覆盖云端或手动恢复入口。
此功能不提供多人实时协同或自动合并两个浏览器的完整画布;版本保护用于阻止静默覆盖。历史从升级后开始积累。
此合同不提供多人实时协同;它保证局部变更、幂等重放和删除保护,避免旧缓存静默覆盖新内容。内部历史表仍用于资源完整性维护和审计,不作为用户编辑入口。
![右侧版本记录与独立只读画布预览(示例数据)](/images/canvas-version-history.png)
相关实现见后端 [画布 operations 同步](/docs/backend/canvas-operations-sync)、`web/src/services/user-data-sync.ts`、`web/src/services/api/user-data.ts` 和 `web/src/pages/canvas/canvas-sync-status.tsx`。
## 画布智能引用
@@ -365,18 +365,18 @@ SEO 标题、描述、可选关键词会投影到浏览器 metadata、Open Graph
- `web/src/lib/canvas/canvas-starter.ts`:空画布起点状态决策。
- `web/src/stores/canvas/use-canvas-store.ts`:按画布保存自由起点选择。
## 画布手动保存与强制覆盖
## 画布同步与离线恢复
画布左上角菜单提供「保存」与「强制覆盖保存」两个手动入口。「保存」与 ⌘/Ctrl+S 等价:先写入浏览器本地缓存,再触发云端同步;云端失败时保留本地结果并排队自动重试,适合平板等无键盘设备手动落盘。
画布编辑会先写入浏览器本地持久化,再由统一同步队列提交 operations。浏览器在线时自动保存;断网、响应丢失或服务端暂时不可用时,未确认请求会保留在本地 journal,恢复连接后按原 `mutationId` 和原操作内容自动重放。同步状态会区分保存中、已完成、待处理和错误,用户无需选择本地版或云端版,也无需手动强制覆盖。
「强制覆盖保存」用于云端同步反复失败(如提示“画布媒体与素材库记录不一致”)时的显式恢复,操作前有确认弹窗。执行时以本地画布为准覆盖云端版本:先按服务端素材记录校验所有画布媒体节点与时间线片段声明的素材绑定,不一致时重绑到引用同一资源的素材(缺失则按节点新建),再按“素材先于画布”的顺序整体推送;素材远端版本冲突时采纳远端为基线继续用本地覆盖。后端“画布资源必须被其素材记录引用”的不变式保持不变,强制覆盖通过前端对齐绑定完成,不绕过校验。
媒体素材先完成资源归属和就绪校验,再提交引用画布的 operations。服务端返回并发或资源冲突时,本地修改继续保留,系统等待下一次基线校验;删除后的迟到更新会被拒绝,不会凭空创建节点。相关同步细节见 [画布 operations 同步](/docs/backend/canvas-operations-sync)。
### 相关实现
- `web/src/services/canvas-asset-repair.ts`:媒体与素材的资源身份归一化、增量自动修复和强制重绑。
- `web/src/services/user-data-sync.ts`:强制覆盖同步入口与素材冲突基线策略。
- `web/src/pages/canvas/use-canvas-project-lifecycle.ts`:手动保存与强制保存流程。
- `web/src/pages/canvas/canvas-project-top-bar.tsx`:菜单入口。
- `web/src/stores/canvas/use-canvas-store.ts`:本地画布持久化与 journal。
- `web/src/services/user-data-sync.ts`:operations diff、自动保存、回执重放和远端删除保护。
- `web/src/services/canvas-asset-repair.ts`:提交前媒体引用与素材记录校验。
- `web/src/pages/canvas/canvas-sync-status.tsx`:同步状态显示。
## 画布外观

View File

@@ -15,9 +15,19 @@ description: 当前项目已实现功能的手工验证清单与未确认边界
## 画布素材缺失时的本地强制覆盖
- 已实现:云端保存返回 `canvas_history_resources_missing` 时,画布同步面板提供“用本地版强制覆盖云端”。操作会先保存本地草稿,探测资源状态,清理确认已删除或不存在的资源定位,保留节点布局、提示词和连线后再提交;普通自动保存仍执行完整素材校验。
- [x] 专项测试覆盖嵌套资源定位收集、失效媒体字段清理、素材绑定移除以及画布性能回归;前端构建、后端画布/处理器/仓储测试通过。
- [ ] 使用已登录浏览器打开具体画布,点击强制覆盖并刷新确认:失效图片节点仍保留为可重新上传的空节点,本地草稿可从版本记录恢复。
## 画布 operations 自动恢复验收
- 已实现:云端画布写入统一使用字段/对象 operations;本地 journal 保存未确认请求,响应丢失、断网、刷新和重试都复用原 `mutationId` 与 payload。服务端 receipt 与画布变更同事务提交,相同请求安全重放,摘要不同返回冲突。
- [x] 专项测试覆盖字段合并、同字段冲突、显式创建、对象不存在、幂等重放以及删除后迟到更新不重新创建节点;前端类型检查、生产构建和后端全量测试通过。
- [ ] 使用已登录浏览器打开同一画布的两个标签页:分别修改不同字段应自动合并;修改同一字段应显示同步错误并保留本地输入,恢复后自动重放。
- [ ] 在浏览器开发者工具中模拟断网、刷新和响应丢失,确认本地 journal 保留原请求并在恢复连接后完成;检查同步状态图标的 pending/error/done 颜色。
- [ ] 让 Agent 或媒体任务在节点删除后返回迟到更新,确认任务回执保留但不会重新创建幽灵节点。
- 已实现:云端画布写入统一使用字段/对象 operations;本地 journal 保存未确认请求,响应丢失、断网、刷新和重试都复用原 `mutationId` 与 payload。服务端 receipt 与画布变更同事务提交,相同请求安全重放,摘要不同返回冲突。
- [x] 专项测试覆盖字段合并、同字段冲突、显式创建、对象不存在、幂等重放以及删除后迟到更新不重新创建节点;前端类型检查、生产构建和后端全量测试通过。
- [ ] 使用已登录浏览器打开同一画布的两个标签页:分别修改不同字段应自动合并;修改同一字段应显示同步错误并保留本地输入,恢复后自动重放。
- [ ] 在浏览器开发者工具中模拟断网、刷新和响应丢失,确认本地 journal 保留原请求并在恢复连接后完成;检查同步状态图标的 pending/error/done 颜色。
- [ ] 让 Agent 或媒体任务在节点删除后返回迟到更新,确认任务回执保留但不会重新创建幽灵节点。
## Agent 只读循环软收敛
@@ -198,14 +208,14 @@ description: 当前项目已实现功能的手工验证清单与未确认边界
- 画布编辑器左侧图标菜单与展开内容改用一块低圆角、半透明磨砂表面;删除双重标题、硬分隔线和搜索框边框。侧栏现在浮在真实画布上方,打开/折叠不再改变画布视口宽度;顶栏和左下缩放控件避开侧栏。工作区下拉菜单及共享单选下拉去掉高亮描边,用背景和阴影建立层次,键盘焦点保留低对比提示。仍待登录态真实页面验收明暗主题、画布节点透视、窄屏抽屉、菜单键盘导航和弹层层级。
- [ ] 在已登录的真实画布和用户工作区以浏览器 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 后人工恢复历史或重传。未扫描或改写生产数据库。
- 已实现:统一递归识别持久资源字段(含嵌套 `videoPreview`);operations 提交前校验资源归属和 ready 状态,资源缺失返回带路径和 ID 的结构化 409。未确认请求保留在 journal,不能通过覆盖或历史恢复绕过资源合同;主媒体问题继续报告为 unresolved。
- 已实现:哈希 chunk 采用长缓存,缺失资源不缓存;HTML 显式 `no-cache, must-revalidate`。静态容器发布保留当前与上一发布 chunk,并在保留窗口内清理更旧资源;Vite preload 失败只自动恢复一次,之后交由路由错误页。已有数据卷不会凭空恢复首次部署前已删的 chunk。
- 已实现:媒体元数据请求按账号 scope/generation 隔离迟到响应;画布内容 hash 对 viewport-only 副本复用;视口更新走轻量路径;loading 节点共享画布级 ticker,页面隐藏时暂停该 ticker。
- [ ] 先备份隔离/测试数据库,验证资源缺失返回结构化冲突、同一 mutationId 重放幂等、字段基线冲突和删除后迟到更新拒绝;确认事务失败不会留下半条 receipt。未扫描或改写生产数据库。
- [ ] 使用发布测试卷连续发布三版以上:旧 tab 延迟触发旧 chunk 后仍可加载;验证 HTML 与哈希资源 Cache-Control、旧 chunk 404 不被 SPA fallback 改写,以及浏览器一次性恢复不循环。当前没有 CDN/Ingress 线上响应证据,Firefox MIME 具体响应类型未确认。
- [ ] Firefox/Chrome/Safari 实际页面走关键 lazy route;检查离线草稿、登录切换和错误重试行为。尚未做真实浏览器/CDN 验收。
- [ ] Firefox/Chrome/Safari 实际页面走关键 lazy route;检查离线 journal、登录切换和自动重放行为。尚未做真实浏览器/CDN 验收。
- [ ] 使用 React Profiler 与长时间运行的大画布记录任务更新、节点数/可见数、主线程长任务和页面隐藏行为。当前只做结构性降热点,不推断整画布必然全量重渲染或已达到某个帧率。
## 完整 AI 拆图
@@ -618,14 +628,13 @@ MiniMax Hailuo Video V2 / H3 插件更新至 `2.0.1`,创建和查询均优先
- [ ] 使用 Artbox 的 NewAPI 通道 2 保存“视频 Token”,完成无用量视频任务后确认公式结算、10% 余量退回、来源标注;重复轮询/回调不重复扣费。
- [ ] PostgreSQL 部署验证 schema v25;新代码未启动或未迁移时不要用旧后端保存非 Ark Token 价格。历史无公式快照的收费订单缺少用量仍进入待核对。
## Agent 历史事件重放导致画布反复冲突
## Agent 事件校准与 operations journal 恢复
旧会话的画布增量可能缺少版本号。页面刷新重放事件时,原实现先标记冲突;后续完整快照即使确认内容一致,也提前返回而不清除冲突。现在增量拒绝只触发快照校准,由完整快照确认真实冲突;内容一致时更新同步基线并恢复已同步状态。云端仍为已核对的同一基线时,保留待保存的本地编辑,不误判为并发修改。真实冲突继续保留草稿并阻止覆盖,不重放无版本增量,不修改历史数据。
旧会话的画布增量可能缺少版本号或发生断流。现在增量缺口只触发完整画布校准;客户端保留已确认基线和未确认 operations,请求恢复时复用原 `mutationId` 与原始 payload。不同字段可合并,同字段或已删除对象不会被静默覆盖或重建。
- [x] 47 项画布版本、Agent 增量合并、事件调度与刷新测试通过,覆盖加载云端、保存、重建会话后三次重放旧事件,以及版本缺口、未核对离线草稿与真实冲突保护。
- [x] 前端 `bun run build` 通过;同时补齐 Agent 会话预览缺少的 `markdownPlainText` 导入。
- [x] 隔离浏览器使用实际前端同步模块与 IndexedDB、模拟云端 API,执行保存后三次整页刷新和旧事件重放:均保持已同步,未增加草稿或重复提交。不访问真实账号数据。
- [ ] 登录真实账号验证问题画布:加载云端、保存、刷新后不再因旧 Agent 事件回到冲突;已有草稿仍可读取。自动化浏览器当前未登录,尚未完成该账号的页面验收。
- [x] 画布 operations、Agent 增量合并、事件调度与刷新测试通过,覆盖版本缺口、响应丢失、未确认请求重放、删除后迟到更新和幂等回执。
- [x] 前端类型检查与生产构建通过;隔离浏览器使用实际同步模块与 IndexedDB 模拟刷新恢复,未产生重复提交。
- [ ] 登录真实账号验证断网、刷新和 Agent 事件缺口:同步面板自动恢复,未确认操作只提交一次,删除节点后迟到结果不复活节点。
## 画布 Agent 读取参数纠错
@@ -716,18 +725,13 @@ PR #507 核心能力已按主分支实现保留,并补齐小视口尺寸约束
- [ ] 同一张已录入方舟素材 ID 的参考图用于 Agent Plan / 方舟 Seedance 视频生成时,仍可走 `asset://` 并跳过重复上传。
- [ ] 官方 `volcengine-ark-image` / `volcengine-ark-video` 行为不变。
## 画布保存保护与版本记录
## 画布 operations 持久化与恢复验收
已增加版本 CAS、冲突草稿、视口与内容分离、打开时核对云端、历史快照和恢复,以及右侧历史树和独立只读预览。上游云端 Agent 与创作任务使用同一版本和历史事务。专项测试覆盖并发旧写入、断档增量、草稿写入失败、恢复冲突、历史权限、保留数量和历史媒体删除保护。Schema 顺延为 v23,不占用已发布的 v16 Agent 记忆版本。
画布写入已统一为字段级、对象级 operations。客户端先把未确认请求写入账号隔离 journal,服务端在同一事务中提交画布 mutation 与 `canvas_mutation_receipts`;响应丢失、断网、刷新和后台重试都复用原 `mutationId` 与原始 payload。内部维护快照只用于资源保护和审计,不作为用户可见的历史树、预览或恢复入口。
隔离 SQLite/PostgreSQL 的迁移与版本/快照测试已通过;前端构建、类型检查与 lint 通过。真实 Chrome 的组件测试(合成数据与模拟接口)通过了明暗主题、桌面预览缩放、草稿查看、窄屏抽屉、恢复确认与无内容写请求检查;完整登录态和真实生成渠道仍按下列清单验收。
- [ ] 双标签页编辑同一画布:旧页平移/缩放不发内容 PUT,旧页内容保存收到冲突后保留草稿,另一画布仍可正常保存。
- [ ] 出现「版本冲突 · 未同步」后点「加载云端最新版本」:画布不整页卸掉,状态变为已保存或等待保存;平移/缩放不能再次卡死加载。
- [ ] 在有未同步编辑时加载最新版或恢复历史,确认本地草稿可下载,恢复后版本递增且恢复前内容仍可找到。
- [ ] 明暗主题、桌面和窄屏下检查右侧历史树、草稿页、预览缩放和恢复确认;预览期间快捷键和拖拽不能修改当前编辑器,返回当前画布后可继续生成和编辑。
- [ ] 使用真实受控模型验收 Agent 生成任务完成与本地编辑同时发生时的冲突提示;自动化测试不调用收费渠道。
- [ ] 既有部署执行 schema 23 迁移并刷新前端,确认旧画布正文保留;历史只从升级后积累,不能恢复升级前已被覆盖的内容。
- [x] 后端覆盖显式创建、字段合并、对象基线、幂等回执、原子事务以及删除后迟到更新不重建节点。
- [x] 前端覆盖未确认请求 journal、刷新恢复、自动重放、远端完整快照与本地未确认 mutation 合并。
- [ ] 双标签页、断网、响应丢失、刷新和 Agent 媒体回写的真实浏览器验收:状态自动恢复,同字段冲突不覆盖远端修改,删除节点后迟到结果不复活节点。
## 首页常驻滚动通知(2026-09-18)
@@ -958,12 +962,11 @@ PR #507 核心能力已按主分支实现保留,并补齐小视口尺寸约束
- 已补充后端回归用例,覆盖任务状态、输入引用、画布引用、删除事务 Outbox、历史记录保留和自动清理边界;测试包编译通过,本地数据库测试因 `CGO_ENABLED=0` 无法初始化 SQLite,尚未验证通过。
- 待部署验收:未绑定画布的生成图片可单个及批量彻底删除;被画布或其他任务输入引用时仍提示来源。删除后任务历史中的原文件不再可用,共享素材仍保留其资源。
## 画布手动保存与强制覆盖保存
## 画布统一 operations 写入与媒体保护
- 画布左上角菜单新增「保存」(与 ⌘/Ctrl+S 等价,本地落盘后同步云端,适配平板无键盘场景)与「强制覆盖保存」(确认弹窗后以本地内容覆盖云端,并自动修复画布媒体与素材库的绑定)。
- 根因修复:同节点原地重生成(如视频节点改提示词后重新生成)走兜底写回路径时,节点保留旧 assetId 却指向新资源,后端画布媒体校验拒绝后,自动修复被节点自身声明掩盖,形成“自动重试但永远失败”。现节点声明不再覆盖素材库真实绑定,错误配对会在下一次自动同步时自愈。
- 强制覆盖语义:按服务端素材记录(与本地缓存合并)校验所有画布媒体节点与时间线片段的素材绑定,不一致时重绑到引用同一资源的素材或按节点新建,再按“素材先于画布”整体推送;素材远端版本冲突时采纳远端为基线继续用本地覆盖。后端不变式与接口未改动。
- 已通过 `web/test/canvas-asset-repair.test.ts`(8 例)与前端生产构建。待浏览器实测:改提示词原地重生成视频后不再出现“画布媒体与素材库记录不一致”的持续报错;出现历史遗留报错时「强制覆盖保存」能立即恢复且后续自动同步不再报错;平板设备上「保存」菜单可用。
- 画布手工编辑、Agent、媒体终态回写、预演台、创作提交和导入均通过统一 operations 服务提交;不存在整批快照覆盖入口。
- 素材和画布按“资源先确认、画布后提交”顺序同步。资源缺失或绑定不一致时保留本地内容并进入自动恢复队列,不通过用户选择云端覆盖本地,也不重建来源不明的媒体或插件节点。
- 已通过 operations、资源引用、幂等重放和删除顺序专项测试;真实浏览器仍需验证断网恢复、媒体回写和刷新后的状态投影。
## 欢迎页开放开关
@@ -1852,14 +1855,12 @@ PR #507 核心能力已按主分支实现保留,并补齐小视口尺寸约束
- [ ] 在存储资源页尝试删除当前浅色 Logo、深色 Logo、视频和封面,确认显示“外观”引用并保留文件;解除绑定后可正常删除。
- [ ] 将公开配置接口模拟为超时或 5xx,确认 4 秒内结束等待并完整回退内置外观,应用仍可进入。
## 画布导入同步、素材回收站与历史
## 画布导入同步、素材回收站与删除保护
- [x] 画布压缩包导入会先校验媒体完整性,并统一通过远端同步入口上传资源;失败时保持脏状态和自动重试,不再把浏览器本地 storage key 误报为已同步。
- [x] 画布压缩包导入先校验媒体完整性,资源上传和画布写入统一进入 operations journal;失败时保留未确认请求并自动重放,不把浏览器本地 storage key 标记为云端事实。
- [x] 素材回收站使用独立归档状态;彻底删除复用服务端引用校验与资源删除 Outbox,自动过期清理不会旁路业务引用。
- [x] 已删除画布历史改为按用户 scope 存入 IndexedDB;删除失败不记录历史,共享素材不会因删除单个画布被错误归档。
- [ ] 使用两个账号分别删除画布并切换登录,确认历史记录和回收站素材不串账号;刷新后记录仍在,清理历史只影响当前账号。
- [ ] 删除仍被其他画布或项目引用的回收站素材,确认服务端拒绝并显示引用来源;解除引用后彻底删除,确认资源记录进入 Outbox 并最终清理物理对象。
- [ ] 导入包含图片、视频、音频的真实画布压缩包,分别模拟上传中断和服务端保存失败,确认进度遮罩最终关闭、页面可离开且恢复网络后自动重试。
- [x] 画布删除后迟到的媒体或编辑更新不会重新创建画布或节点;只有显式 create operation 可以创建实体。
- [ ] 使用两个账号、两个标签页和断网恢复验收导入、删除、刷新与同步状态;确认来源未知的插件包和媒体修改不被清理。
## 邮箱找回密码