合并主分支

合并远端主线,保留钱包快照竞态修复。
This commit is contained in:
2026-08-06 11:01:00 +08:00
143 changed files with 27087 additions and 1567 deletions
File diff suppressed because it is too large Load Diff
+95 -2
View File
@@ -14,6 +14,60 @@
- 关联:相关文件、文档、提交或 Issue
```
## manifest 被旧快照写回 Pending 时不能让父 Run 抛下真实运行中的 child
- 现象:`code-prototype` 已有确定性 child run、running journal 和工具事件,父 Supervisor 却在几秒后以 fixed graph stalled 失败;child 随后完成代码与静态检查,但 completion gate 持续报告 `task=code-prototype status=pending`,最后 `loop-budget-exhausted`
- 原因:并发 hydration 或其它旧 manifest 快照把 scheduler 已写的 running 覆盖为 pending;父 Run 把“当前不宜调度新 child”错误等同于“没有 child 需要等待”,并只以 manifest 状态判断 DAG 活性。父先终态后,真实 child 也失去正常投影窗口。
- 处理:调度与等待分离。新调度可以被派生视觉修复等门禁阻止,但当前最新活跃根 Run 下,只要 durable child 具有确定性 runId、scheduler source、正确父链接且 journal 仍处于 queued/running,父 Run 就继续等待;同一 child 的完成门可容忍 manifest pending,但仍执行正式产物、revision、静态检查与试玩证据门禁。更新根 Run、GUI/CLI、终态/确认/reconciliation child 或绑定冲突全部失败关闭。
- 验证:人工把当前 child 的 manifest 状态回写 pending,断言父 DAG 仍 in progress、父上下文可持久化为 `waiting-for-manifest-tasks`、child 可投影 completed;随后创建更新根 Run,断言旧 child 不再保持 DAG 活性且 completion blocker 恢复 `status=pending`。不要靠增加 loop 次数或伪造 completed 掩盖竞态。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs``apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop.rs``apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs`
## 既有正式产物不能同时被快车道视为已完成、被本轮 baseline 门视为未变化
- 现象:增量任务已有完整美术图集,`art-asset-plan` 每轮都返回零 action 和“已验证交付”,但 completion gate 每轮都报告 `assets/manifest.art.json(unchanged-from-run-baseline)`;最终 child `loop-budget-exhausted`,随后 Graph 和父 Run 失败。日志中没有本轮 Provider request、tool plan 或 action receipt。
- 原因:Graph reset 无差别重新打开稳定的美术 owner 节点;快车道按“当前产物有效”判断完成,owner 完成合同则按“本轮必须修改 baseline 产物”判断完成,两套语义互相冲突。增加 loop 预算、伪造版本号或机械改写 manifest 都不能消除冲突,还会引入 verification loop、字段丢失或错误复用旧主题。
- 处理:先在 Graph 层区分“复用已验收产物”和“需要重新生成”。仅对明确的既有美术接入意图、非占位游戏和整套严格有效的视觉合同保留美术节点 completed;根完成门只忽略这一路径的 art manifest baseline 相同,所有结构、Canvas、私有回执、切片和可见使用验收继续失败关闭。普通新游戏、全新美术或证据损坏必须重新打开 owner 节点。
- 验证:不要只断言 child 未被调度;还要直接经过父完成门,证明合法复用不再出现 art baseline gap,并证明删除切片后 baseline gap 与视觉合同 gap 重新出现。另覆盖普通新目标和全新美术请求不复用旧美术。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/game_chat_fast_path.rs``apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs`
## 执行锁移交给未确认启动的异步 future 会制造永久 queued
- 现象:父 Supervisor 与 Runner 一直显示运行中、heartbeat 正常,专业 Agent 已有 `background_task.queued``autonomous_ready_task.scheduled`,对应执行锁也被 Runner 持有,但该 child 永远没有 running journal、`turn.started` 或后续 Runtime event;其它同批 Agent 可能已经完成。
- 原因:ready scheduler 把 per-Agent 执行锁直接 move 进 fire-and-forget Tokio future,并在 future 首次 poll 前返回成功。锁移交不是启动确认;future 未进入 start transition 时,常规 wake/recovery 又拿不到同一把锁,queued task 因而没有任何接管者。预先占用 child locks 的测试会绕过真实 spawn 路径,无法发现该缺口。
- 处理:实际 Runner 必须在项目写锁外同步完成 pending -> running 与 started journal,成功且 execution worker 已开始轮询后才移交执行锁;启动/接管失败必须持锁完成 child failed 与 manifest Graph failed 投影,再释放锁并让 parent 收到错误。scheduled 诊断审计失败不能阻断 child 启动,external client 只负责 wake Runner。UI 另以 durable `startedAt` 和父/子最大活动时间显示运行态时长及疑似停滞;task-record fallback 不得把最新 record 时间写成 startedAt,父 Run terminal 后也不得被 child 晚到事件继续增加时长。
- 验证:使用真实空闲 child lanes 一次调度 `design-director / art-director / code-director`,在有界时间内逐一断言 running/`turn.started`,并验证幂等重调度不新增逻辑 Run;禁止只断言 scheduled 记录、锁文件或 Runner heartbeat。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/task_start.rs``apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/task_queue.rs``apps/ai-game-creator-shell/src/features/project-workspace/SupervisorChatOnlyView.tsx`
## Canvas 视觉门不能在没有资产词法候选时运行整套 JavaScript 语义分析
- 现象:Supervisor 已写入 `turn.started`,但第一条 planning 进度长期不出现;Runner 无 Provider 连接,单个 Tokio worker 持续占满一核。对项目现场复现时,15 KiB 的经典脚本在检查一个根本未被引用的切片文件时,超过 60 秒仍未返回。
- 原因:视觉门先为每个候选切片无条件执行模块依赖分析和函数可达性分析,最后才判断图片 `.src` 或已绑定 DOM 元素是否能指向目标资产。没有 `import` 关键字、没有目标文件名且没有已绑定图片元素时,这些全程序分析不可能产生有效视觉证据,属于纯浪费;复杂闭包与 alias 图会把浪费放大成看似 Runtime 停滞。
- 处理:仅做单向安全短路:JavaScript 原文同时没有大小写精确的 `import``export` 字节序列时,跳过模块依赖语义分析;纯 `export ... from` / `export * from` 仍是模块图依赖,不能误跳过。当前脚本不含目标文件名或任一已绑定 DOM 图片元素 ID 时,先低成本解码 `\\xNN``\\uNNNN``\\u{...}`、简单转义和续行;解码后仍无候选才直接判定没有绘制证据,解码不确定则保守进入 Oxc。这样必须保留 computed `s\\x72c`、转义资产 URL 与转义 `getElementById/querySelector`,同时不能因 HTML 中存在某个绑定元素让所有无关 JavaScript 单元进入重分析。
- 验证:无候选现场的同一切片检查必须有界返回且保持 `missing-visible-art-slice-use` 结论;同时覆盖相对路径、转义 URL、computed/转义属性与 DOM 方法、绑定 DOM 图片元素、含无关正则转义的脚本单元、路径大小写、纯重导出模块图、动态 import namespace 写入、未调用函数、恒假分支和真实可达 `drawImage`,证明短路只拒绝不可能命中的输入,不扩大验收权限。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs`
## ready-task 对账取消后不能让 successor 永久继承 failed Graph
- 现象:未知工具结果按安全边界进入 `needs-reconciliation`,人工核对后取消原 child;manifest 随即把该节点投影为 failed,父 game-chat 固定任务图明确失败。随后重试父 Run 虽创建同源 successor,却原样继承 failed manifest,几秒内再次失败,原项目无法继续。
- 原因:取消原 reconciliation Run 只负责安全释放 Agent 队列屏障,并不等于 manifest 任务完成;continuation 完成合同保留既有 Graph 进度,却没有区分“普通失败”和“已经人工核对、保留 cancel tombstone 的 reconciliation 取消”。
- 处理:旧 action 继续禁止重放或伪造 observation;旧 child 与父 Run 先真实终态。新 Supervisor continuation 仅扫描同 Session、同 source、同有效任务合同的历史根 Run,并要求对应 ready-task 同时存在 `failed / needs-reconciliation` 记录、最终 `cancelled` 记录和 durable cancel tombstone,才把当前 manifest 的同一 failed 节点恢复为 pending,让 scheduler 创建新 child Run。manifest 的读取、筛选、child 证据重验和写回放在同一项目写锁内;每个 task journal 只读取一次并按 parent Run 建索引。较新的无 child Run 默认阻断旧凭证,只有其 root journal 精确证明为旧 failed Graph 在进入 scheduler 前即失败时才允许向前查找;scheduler 自身失败不得被当成该兼容场景。
- 验证:构造 reconciliation child、人工 cancel tombstone、failed manifest 和终态父 Run,证明同源 continuation 只重排该节点;并列普通 failed 节点保持 failed,完成合同继续继承原任务 SHA 与项目 baseline,旧 pending action 不恢复。追加覆盖“旧 failed Graph 未调度”的中间 Run 可以跨过,而较新的 scheduler failure 即使没有 child journal 也会阻断更老 tombstone。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs``apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion_contract_tests.rs`
## `timeout_at` 不能替代显式的预算耗尽预检
- 现象:给完美像素加端点级并发闸后,预算已经耗尽的请求仍然能拿到许可,白占一个名额继续去打几轮全账号 SpacetimeDB 扫描,直到下载那步才失败。
- 原因:`tokio::time::timeout_at` 会先 poll 一次内层 future 再判超时。信号量有空闲许可时 `acquire_owned()` 首次 poll 就绪,于是即使 deadline 早已过去,返回的仍是 `Ok(Ok(permit))` 而不是超时。既有 `acquire_editor_pixel_art_cpu_permit` 里那句 `if Instant::now() >= processing_deadline` 正是为此存在,新写的许可函数漏掉后被单测抓出。
- 处理:所有「先判预算、再等资源」的获取函数都必须在 `timeout_at` 之前显式判一次 `Instant::now() >= deadline` 并直接返回超时错误;这句不是冗余防御。同理,进入排队计数之前也要先做这个预检,避免为注定失败的请求占用队列名额。
- 验证:在有空闲许可时用已过期的 deadline 调用获取函数,只断言返回 `504` 而不是许可;`504` 已足以证明显式预检没有被 `timeout_at` 的首次 poll 绕过。禁止在该用例里读取进程级队列 Atomic 的 before/after;相对断言同样会被并行测试插入。仅靠「信号量占满时超时」的用例发现不了这个问题。
- 关联:`server-rs/crates/api-server/src/editor_project.rs``acquire_editor_pixel_art_snap_permit``acquire_editor_pixel_art_cpu_permit`)。
## 有界等待队列的计数递减必须写在 Drop 里
- 现象:给同步端点加「最多 N 个等待者」的保险丝时,若把计数递减写在正常返回路径上,客户端断连或超时触发会让等待中的 future 被丢弃而跳过递减;计数只增不减,最终队列永久判定为满,接口对所有人返回 `503` 且不会自愈。
- 原因:Rust 的 async future 可以在任意 await 点被取消,取消时只保证 `Drop` 会跑,不保证后续代码会执行。有界队列的入场与离场天然不对称。
- 处理:把递增封进一个 guard 结构体,递减放在它的 `Drop` 实现里;递增本身用 `fetch_update` 的 CAS,不能用「先读后加」——两个线程同时读到 `max - 1` 各自加一就会越界。拿到资源后立即 `drop(guard)` 让出队列名额,不要让它跟着许可一起活到请求结束。
- 验证:单测覆盖 CAS 边界(满了返回失败且计数不越界、上限为 0 时任何进入都失败),并由独立用例覆盖 guard 离开作用域后的计数归还。预算耗尽路径只断言 `504`,不得通过另一个测试也会修改的进程级 static before/after 来推断“未入队”,也不得用串行锁或 `--test-threads=1` 掩盖隔离问题。
- 关联:`server-rs/crates/api-server/src/editor_project.rs``try_enter_bounded_queue``EditorPixelArtSnapQueueGuard`)。
## Linux 生产脚本门禁不能假设本地也是 GNU userland
- 现象:macOS 本地运行维护页、生产 API 部署和 Rust 产物门禁时,依次出现 `mv: illegal option -- T``mapfile: command not found``/usr/bin/cp` / `/usr/bin/chmod` 不存在,以及 `.rlib` 明明含有 `.o` 却报告“没有可扫描成员”;安全修复计划还会把 `/var/folders``/private/var/folders` 的系统别名误判为用户符号链接。
@@ -3236,9 +3290,9 @@
- 现象:release 上 api-server 周期性出现全量 `spacetime_stage="pool_acquire" elapsed_ms=45000` 业务超时,`/readyz` 503`reason=spacetime_unhealthy, stage=pool_acquire`),`/healthz` 仍 200,只有重启能恢复,过若干小时复发。
- 原因:旧 `PooledConnectionLease` 只能显式 `release_connection` 归还;HTTP 请求方在等待 StDB 回包期间断开时 handler future 被取消,permit 自动归还但槽位 `in_use` 永不复位。后续 acquire 在拿到 permit 后进入无界 `loop + yield_now` 扫描空闲槽位,泄漏积累到 pool_size 后整池挂死。
- 处理:租约持有 `Arc<SpacetimeConnectionPool>` 并实现 `Drop` 统一复位槽位/归还连接;槽位改 `AtomicBool` CAS 抢占,删除自旋循环(持有 permit 必然命中空闲槽位)。任何新的"显式归还"资源在 async 取消语义下都要先想 Drop 兜底。
- 处理:租约持有 `Arc<SpacetimeConnectionPool>` 并实现 `Drop` 统一复位槽位/归还连接;槽位改 `AtomicBool` CAS 抢占,删除自旋循环(持有 permit 必然命中空闲槽位)。任何新的"显式归还"资源在 async 取消语义下都要先想 Drop 兜底。该保证只覆盖本地 lease / slot / permit 回收;RPC 已发出后,handler timeout/drop 不会取消或回滚远端 procedure,结果仍须按 unknown 读取权威事实。
- 验证:`cargo test -p spacetime-client --manifest-path server-rs/Cargo.toml --lib``dropped_lease_releases_slot_and_permit``acquire_times_out_at_pool_acquire_when_pool_is_busy`)。
- 关联:`server-rs/crates/spacetime-client/src/lib.rs``docs/【后端架构】SpacetimeDB连接池租约Drop兜底与取消安全-2026-06-11.md`
- 关联:`server-rs/crates/spacetime-client/src/active.rs``docs/【后端架构】SpacetimeDB连接池租约Drop兜底与取消安全-2026-06-11.md`
## 后台灰度配置不能从 SpacetimeDB 本地表缓存读取
@@ -4145,6 +4199,38 @@
- 处理:使用完整 JSON Schema validator 校验原始 catalog schema,不手写 required/type 子集;native parser、fingerprint enrichment 与实际 MCP 调用边界复用同一校验器。enrichment 错误必须映射回 classified `arguments-schema` repair,不能以普通字符串直接终止 run;执行点重验用于阻断升级前已经落盘的 schema 外 pending。关闭网络和文件 `$ref` 解析,schema 无法安全编译时不广告或不执行。`serde` 类型错误会包含实际字符串值,catalog miss 也会包含模型提交的 server/tool,因此这两类错误同样只能返回稳定类别,不能拼接原始错误、参数值或 schema 内容。
- 验证:覆盖 required、additionalProperties、type、enum、本地 `$defs/$ref`、HTTP/file 外部引用、无效 schema、错误脱敏,证明 legacy wrapper 在注入 fingerprint 前进入 repair,并证明带旧有效 fingerprint 的历史 pending 在实际调用前仍被 schema 拒绝。
## 2026-08-05 不要把 static smoke 当作完整专业交付
- 现象:code-prototype 已通过 `game.static_smoke`,但完成门明确报告 `missing-visible-art-slice-use`;随后每轮 thinking summary 都是“已取得验证证据”,没有新 action,最终 loop-budget-exhausted。
- 根因:game-chat 快车道只看 verification gate 就返回确定性交付,完整 autonomous completion blocker 直到空 action 的最终收束阶段才被发现;Provider 因而永远拿不到下一轮修复机会。失败的 `file.patch` 也会因验证凭证保守失效而推进 revision,若快车道只比较 `mutationRevision`,会把“文件未修改”误认成本 Run 已修改。已有未完成计划收到 steer 后若再追加一整套新步骤,还会与 retained completed 步骤合并成超过 8 步,随后稳定重复 `runtime.plan_update blocked`
- 处理:确定性交付与自动 plan completion 都必须先通过完整 completion gate,并要求当前 Run 最后一条同 mutation 工具调用与 Agent DB 中严格绑定当前身份的 `status=ok` receipt 一致;pending action 的 steer cursor fingerprint 也必须一致,失败 patch 或旧 Run receipt 不能取得交付资格。新 blocker 不回退旧 completed 步骤:已有非终态步骤时用明确 repair step 替换首个非终态步骤,其余保持 pending;只有全 completed 且仍有容量时才追加。8 步已满时进入外部 repair lane;计划已有 failed 步骤时立即失败关闭。回归同时覆盖失败 patch、跨 Run receipt、非零 steer cursor、8 个 completed 与 blocker,以及 failed plan 在 ownership/blocker 不同组合下都不会继续空转。
## 2026-08-05 Runtime 时间戳必须验证 Date 范围并保持来源身份
- 现象:极大但有限的持久时间值会让 `toISOString()``RangeError`,或让界面显示 `Invalid Date`;实时回复又借用其它 Runtime 的最近活动时间,文字继续流入时仍显示几分钟前,缺失时还随前端定时器漂移。
- 处理:秒/毫秒归一化后必须再检查 `Date#getTime()`;不可表示的值统一显示“时间未知”并省略 `datetime`。实时回复只使用 response stream 自己的 `updatedAt`,不能借父/子 Runtime 活动时间或 `Date.now()`
## 2026-08-05 Pending manifest 容错必须覆盖真实终态时序
- 现象:手工把内存 state 改为 Completed 的测试通过,但真实 finalization 先写 durable Completed、再投影 manifest 时仍被 Pending 状态门拒绝;或 stale manifest 全 Completed 后,父 Run 忽略仍在运行的真实 child。
- 根因:测试没有写 durable terminal recordPending 容错只验证了非终态 journalDAG 又把 manifest `completed=true` 放在 active child 之前。终态投影和收束前检查使用了不同事实时序。
- 处理:测试必须按真实顺序分别写 durable Running 和 durable Completed。Pending 漂移只允许 state/journal 的 Running-Running 或 Completed-Completed 对;queued/waiting/failed/reconciliation 一律拒绝。active child 在无 Failed 时独立保持 DAG 活跃,项目 mutation gate 在写锁内核对当前 root,防止旧 child 污染新根 Run。
## 2026-08-05 Canvas 可达性不能在扇入调用图中回退 visited
- 现象:game-chat 的 code-director 长期显示 queuedRunner 单核持续高 CPUdurable cancel 也无法被事件循环处理;manifest 已提前显示 running,用户看起来像“稳定卡死”。
- 根因:大 classic script 虽使用了 bounded direct-call graph,但 `javascript_named_function_is_reachable` 在递归返回时删除 visited,只阻止当前环,不记忆已经遍历的祖先。render/update 图的大量重复调用让同一节点指数重算;父完成门又在 code-prototype 未完成时提前深验四个 Canvas 切片,使第一次 wake 就同步阻塞,200 次外层重试预算完全没有机会推进。
- 处理:单次可达性查询每个 function node 最多访问一次;全 `None` alias 历史直接返回,稳定外层初始化使用有调用前置证明的快路。父完成门只深验 Completed seed taskwake 预算耗尽写入 reconciliation。格子游戏的符号坐标只在唯一数值 `COLS / ROWS / CELL` 与画布范围能共同证明时接受,普通无界动态坐标继续拒绝。
- 验证:永久 fixture 至少包含 48 层重复扇入调用、IIFE 外层素材初始化、格子常量绘制、无界坐标反例和整画布尺寸引用;真实项目的全部四个切片还要在同一轮秒级返回 true。禁止用延长 queued timeout、Tokio timeout 或 synthetic 小脚本通过来替代真实大脚本复验。
## 2026-08-05 Canvas clamp 与 parent wake 不能走字符串或易失兜底
- 现象:通用 game-chat fallback 明明把玩家坐标限制在当前 Canvas 内,完成门仍报 `missing-visible-art-slice-use`;反向放开任意动态坐标又会让离屏绘制或错误 Canvas 假通过。
- 根因:Canvas owner 收紧后正确禁用了含尺寸成员的字符串兜底,但 AST 数值区间器尚不认识嵌套 `Math.min / Math.max` clamp。若只查源码包含 `canvas.width`,无法证明该 Canvas 创建了当前 context,也无法排除局部伪造 `Math`
- 处理:只在 semantic 证明未遮蔽全局 `Math`、上界读取当前 context 所属 Canvas、下界为 `0` 时生成有限区间;加入错误 Canvas、遮蔽 Math 和无界坐标负向回归。不要用字符串包含、变量名白名单或把未知动态值当 `0`
- 现象:parent wake 的 200 次瞬态预算耗尽后 Runtime 仍长期显示 running,或 lane 忙、取消、child 前进、manifest 损坏时 reconciliation 被静默丢弃或覆盖新状态。
- 处理:预算耗尽错误必须向上传递;lane 忙先持久化 deferred signal,再在 lane + 项目锁内重检最新事实。结构损坏路径使用不依赖 manifest hydration 的专用 journal/state 写入,CAS 失败转为继续对账,绝不覆写并发取消或 DAG 进展。可解析的空对象/空 runId 仍是损坏身份,只有完整有效的新 Run 才能阻止旧 markerevent/audit 的同键记录必须完整比对并拒绝冲突或重复。旧 task 已终态、Runtime 非 waiting 或新 Run 接管时,deferred signal 必须写 resolved/superseded,不能留给后续 wake 永久重复 settle。
- 测试注意:autonomous child fixture 先 linked Pending、后正式 Running;终态 runId 必须拒绝复用。判断 Completed-only 诊断时按每个 seed task 的实际状态分析,不能因为 `code-prototype` Pending 就忽略已经 Completed 的 `art-asset-plan` 深验。
## macOS 安全路径测试必须使用规范化临时目录(2026-08-05)
- 现象:调用仓库上下文、Runtime context bundle 或 pending recovery 的 Rust 测试在 macOS 报“Repository root and its ancestors must not be symbolic links”,Linux CI 却可能通过;本地 HTTP 恢复夹具在完整串行测试中还可能偶发 `WouldBlock`
@@ -4180,3 +4266,10 @@
- 原因:两个跨模块测试读写同一进程全局状态,却没有共用隔离边界;只给 accept 后取得的 stream 设置 read timeout 无法约束 accept 本身,payload 读取也缺少总 deadline。
- 处理:全部全局 sink 测试共用一把 test-only 串行锁,并由 RAII guard 在 `Drop` 中无条件清空;测试统一使用 `manifest_invalidation_sink_isolation_` 前缀。relay fixture 对 accept 和 payload 分别使用非阻塞轮询与总 deadline,不使用固定 sleep;生产 loopback、token、连接 / 写入超时和 payload 大小校验保持不变。
- 验证:用 `--test-threads=2` 重复运行统一 filter,覆盖正常 relay、无事件 accept 超时、不完整 payload 超时、panic 展开清理,以及 GUI owner attach 配置与 guard 清理。
## 编辑器生成不能把传输重试、参考图截断和客户端 provenance 当成独立小问题(2026-08-05
- 现象:生成 POST 首次已经入队但响应丢失时,客户端自动重试产生第二个任务;第 6 张或更多参考图仍显示在 UI / 元数据里,却没有送给 provider;直接构造请求还能把任意资源 ID 写成最终素材引用。
- 原因:客户端虽在重试中复用 `x-request-id`,队列入口却用随机 job id 生成 dedupe key;前端允许无限追加,api-server 和 provider 用 `.take(...)` 静默截断;`generationInputs.references` 被当成可信持久 provenance。
- 处理:主站生成 POST 禁止自动重试,把显式复用的稳定 request id 接到队列唯一键并校验 replay payload;所有边界显式拒绝超限,前端还要预留主图槽位、统计在途上传,并在上传完成前拒绝模型切换、画布选图、提交生成、关联源图删除 / 剪切 / 素材删除和面板切换 / 关闭;reservation 必须绑定原面板上下文,批量部分失败时不能丢弃已经持久化的成功项。入队、完美像素及直接创建资源 / 素材时删除客户端 references,执行时按真实参考源和 owner 资源记录重建权威引用。历史任务比较必须兼容仅差已删除 references 的旧 payload,不能只保留旧 hash 却让 payload 比较误报冲突。
- 验证:覆盖同键同 payload / 不同 payload、普通图片第 6 张、带主图的 GPT-image-2 第 5 张额外引用、provider 6 / 15 张边界、伪造引用删除和 owned 资源 / 素材重建。