加入手动完美像素入口;像素艺术勾选时自动补提示词约束 #124

Merged
kdletters merged 84 commits from feat/pixel_art2 into master 2026-08-05 21:28:41 +08:00
Owner
No description provided.
lhk229 added 2 commits 2026-07-30 21:27:33 +08:00
在图片工具栏接入完美像素交互、占位流程与历史保护

新增严格像素网格检测和登录态同步处理接口

补齐素材类型校验、持久化边界、并发限制及技术文档

修复 Windows rlib 门禁长文件名解析
简化完美像素严格模式
Project CI / Backend tests (pull_request) Failing after 10s
Project CI / Repository checks (pull_request) Failing after 11s
Project CI / Native shell tests (pull_request) Successful in 1m56s
Project CI / Frontend tests (pull_request) Successful in 2m15s
044f278aa3
严格模式仅在两轴均未检测到时拒绝统一网格兜底
删除严格模式专用网格判定参数与算法
收敛单元测试和接口测试为 strict 与 legacy 契约测试
同步更新完美像素相关架构与决策文档
lhk229 added 3 commits 2026-07-31 13:58:11 +08:00
OSS 读取收口为进程级共享客户端并设置连接与整体超时
完美像素处理预算改从请求入口起算以覆盖下载阶段
补齐共享客户端、预算起点与带界下载的回归断言

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
OSS 共享客户端扩展为读写共用并按上传方向放宽整体超时
画板生成图片持久化的 PUT 与 HEAD 改走共享客户端
补齐持久化写路径的共享客户端回归断言

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
去除完美像素持久化的输出图片复制
Project CI / Repository checks (pull_request) Failing after 10s
Project CI / Backend tests (pull_request) Failing after 10s
Project CI / Frontend tests (pull_request) Successful in 1m39s
Project CI / Native shell tests (pull_request) Successful in 2m14s
95443da63c
改用按值移交的持久化入口避免复制整份输出图片
补齐禁止退回借用版持久化入口的回归断言

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lhk229 added 3 commits 2026-07-31 15:18:05 +08:00
新增端点级并发许可并在首次 IO 之前取得
以取消安全的有界队列限制等待者数量
补齐队列边界、释放与闸位顺序的回归测试

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
补齐端点级并发闸、等待队列与 503/504 的契约描述
修正 30 秒预算的覆盖范围为自请求入口起算
记录超时预检与有界队列计数的两条踩坑

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
避免完美像素输出的整图解码
Project CI / Repository checks (pull_request) Failing after 7s
Project CI / Backend tests (pull_request) Failing after 8s
Project CI / Frontend tests (pull_request) Successful in 1m46s
Project CI / Native shell tests (pull_request) Successful in 2m13s
af2a1bc26e
取输出尺寸改为只读 PNG 头
补齐禁止在该入口整图解码的回归断言

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lhk229 added 4 commits 2026-07-31 18:03:07 +08:00
# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
#	docs/project-memory/shared-memory/pitfalls.md
#	docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md
#	server-rs/crates/api-server/src/editor_project.rs
来源资源已随项目完成鉴权时直接取用其 objectKey
以显式归属断言替代跨记录扫描并保留存储类型点查
补齐免查解析的一致性与归属回归测试

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
补齐已鉴权来源资源直接取用 objectKey 的归属断言契约
说明跨记录扫描省略范围与保留的存储类型点查
提示前端带上来源资源 ID 以走免查路径

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
补记图集切片按需编码与批量持久化决策
Project CI / Repository checks (pull_request) Failing after 11s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / Frontend tests (pull_request) Failing after 21s
Project CI / Native shell tests (pull_request) Successful in 11m52s
6036c5664d
事后记录已落地的内存准入、延迟编码与累计裁剪像素上限
事后记录单事务批量确认、稳定切片标识与重放幂等边界
事后记录手动拆分改用已鉴权来源资源的三重归属断言

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lhk229 added 1 commit 2026-07-31 19:39:46 +08:00
Merge remote-tracking branch 'web/master' into feat/pixel_art2
Project CI / Repository checks (pull_request) Failing after 51s
Project CI / Backend tests (pull_request) Successful in 4m9s
Project CI / Frontend tests (pull_request) Failing after 22s
Project CI / Native shell tests (pull_request) Successful in 12m34s
ab7121449c
# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
lhk229 added 2 commits 2026-07-31 20:59:03 +08:00
移除画布生图反向提示词中的低清晰度
Project CI / Frontend tests (pull_request) Failing after 23s
Project CI / Repository checks (pull_request) Successful in 1m44s
Project CI / Backend tests (pull_request) Successful in 3m38s
Project CI / Native shell tests (pull_request) Successful in 11m23s
1c180f4a69
「低清晰度」按字面否定低分辨率,与像素艺术这类以低分辨率重采样为本质的
画风直接对冲:勾选 style=pixelArt 时,提示词端要求硬边像素块,反向提示词
却压制低清晰度,两条指令互相打架。

覆盖画布的四个生图入口:普通图片、角色形象(共用 false 分支)、UI 设计图
(true 分支)、修改图片(两个 provider 分支的内联字面量)。图标图集的
negative 本来就传 None,不受影响。

其余玩法(拼图、消除、跳跃、方洞、大鱼、吠叫、自定义世界场景图)的同名
词条保持不动,本次只收口画布项目。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lhk229 added 1 commit 2026-08-01 15:06:22 +08:00
生成风格 pixelArt 同时约束提示词
Project CI / Repository checks (pull_request) Successful in 1m32s
Project CI / Backend tests (pull_request) Successful in 3m42s
Project CI / Native shell tests (pull_request) Successful in 10m43s
Project CI / Frontend tests (pull_request) Successful in 2m25s
db6dbfaf94
style="pixelArt" 此前只驱动 provider 回图后的确定性像素规整,完全不参与
提示词。但 snapper 是几何对齐器——先检测网格步长再按格重采样;provider 交
一张柔和渐变图时横纵两轴都测不到步长,生成路径用的 legacy profile 会退到
min(width,height)/64 统一网格兜底,产出的是马赛克而不是像素画。原语义等于
「随便生成什么,然后强行网格化」。

注入点固定在 generate_editor_image_for_owner 与
generate_editor_icon_spritesheet_for_owner 构造提交提示词的位置,包住既有
builder 的返回值,builder 签名和输出契约不变。登录态路由、外部 API v1、
异步 job worker 三个入口都汇聚到这两个函数,一处覆盖。

约束句按链路分三条,追加在末尾并独立成行:普通图片没有抠像底色,用
「画面为像素风格」;角色形象和图标图集生成后都要按纯色抠像,绿幕底必须保持
平整,分别用「角色主体为像素风格」和「每个图标素材均为像素风格」,都不出现
「画面」级别的像素化要求,否则与同一段提示词里既有的「纯色背景必须平整无
纹理、无渐变」互相拆台。

实测只提「像素风格」效果已可接受,因此不注入网格密度、色板色数和抗锯齿等
约束。source-guard 钉住注入排在 create_openai_ 之前——被挪到之后时产物看起来
仍然「成功」,只是静默退化成统一网格马赛克,普通测试抓不到。

约束句随 submitted_prompt 写入 editor_project_resource 的 prompt 列;响应体
返回的仍是用户原文,前端显示不变;不新增 OSS PUT、项目资源、素材记录或画布
图层。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lhk229 marked the pull request as ready for review 2026-08-01 15:09:02 +08:00
Author
Owner
# 真实复现链路 当前分支引入 最终判断
1. 回包覆盖未保存编辑 POST 在途时移动图层;若回包前不足 450ms,防抖保存尚未执行,完成快照 会取消 pending save 并替换布局,perfect-pixel 又禁止 Undo。 是,但根因基础设施旧。 防抖与快照替换在 base 已有;ee2cad377 新增了这条无合并、不可 Undo 的调用路径。 成立,P2 数据丢失回归。
2. 未知结果直接失败并开放重试 服务端已写入但响应丢失,或客户端 120s abort;catch 直接标 failed、finally 解锁,再次点击会创建新 task/对象/素材。 是。 client、timeout、catch/finally 均由 ee2cad377 新增。 成立,P2,证据最强。 明确违反 decision-log:5897 的“unknown 先 GET 对账”。
3. 30 秒预算漏 SpacetimeDB 合法 assetId 且不传 sourceResourceId 时,下载前最多顺序执行 8 次 procedure;每次仅约 4 秒就已累计超过 30 秒。首次真正套 deadline 是 OSS 下载 是。 端点由 ee2cad377 新增;823df0f0f 宣称前移预算,但没有包住 DB;5d2588dbe 又让 permit 跨这些调用持有。 成立,P2 可用性及明确契约违反。 不是鉴权漏洞,但应阻断合并。
4. 刷新后 generating 无恢复 占位先持久化,inline POST 未终态时刷新;新页面原样 hydrate generating,没有 job、轮询或再次 GET。 是,但序列化设施旧。 本分支首次组合“持久化 generating + 无 durable job 的 inline 操作”。 成立,P2 恢复回归。 准确说法是“无自动收口”,直到删除、再次刷新或其它冲突偶然触发 GET。可与 #2 合并为 unknown-result reconciliation 缺失。
5. generationInputs 审计字段伪造 登录用户提交 screenColorHex/mattingProvider/mattingModel入口仅序列化,随后原样写入资源和素材,后台 raw mapper 可见。 是。 base 已有 sanitizer 且相邻入口均调用;新端点从加入时便绕过它。 成立,P2 审计完整性漏洞。 影响限于攻击者自己的记录,不是越权、泄密或计费漏洞。
6. assetKind 保存竞态 把已有非空类型改成另一个非空类型,并在异步 resource 保存完成前点击按钮;会发送“新 kind + 旧 resourceId”,后端返回 mismatch 400。 是。 新按钮遗漏已有 isPersistingAssetKind 与 handler ref guard。 成立,P3。 只造成可恢复失败和垃圾占位,无数据破坏。
7. 角色 PixelArt prompt 持久化 kind=character, style=pixelArt 时 provider 收到追加后的 prompt;但原图资源仍保存 role_setting透明结果 保存“去除纯色背景”。 部分。 持久化行为是旧的;db6dbfaf9 新增了像素子句和“会写入 project resource”的错误声明。 成立,P3 项目文档/审计契约问题。 生成行为和 OpenAPI schema 没有违约,只应点名角色链路。
8. “byte-for-byte what you sent” 普通 prompt 会先 trim;角色经过 builder;图标甚至没有原始 prompt,而是由 descriptions 组装。 是。 错误措辞由 db6dbfaf9 新增。 P3 次级 skill 文档错误,无运行时回归。 OpenAPI 使用“与同一请求省略 style 时一致”,该表述和代码是正确的。若审查只收运行时问题,可删除此条。
| # | 真实复现链路 | 当前分支引入 | 最终判断 | |---|---|---|---| | 1. 回包覆盖未保存编辑 | POST 在途时移动图层;若回包前不足 450ms,防抖保存尚未执行,[完成快照](C:/projects/narrative/Genarrative/src/components/image-editor/useImageCanvasGenerationWorkflow.ts:1842) 会取消 pending save 并替换布局,`perfect-pixel` 又禁止 Undo。 | **是,但根因基础设施旧。** 防抖与快照替换在 base 已有;`ee2cad377` 新增了这条无合并、不可 Undo 的调用路径。 | **成立,P2 数据丢失回归。** | | 2. 未知结果直接失败并开放重试 | 服务端已写入但响应丢失,或客户端 [120s abort](C:/projects/narrative/Genarrative/src/services/image-editor/editorProjectClient.ts:1165);catch 直接标 failed、finally 解锁,再次点击会创建新 task/对象/素材。 | **是。** client、timeout、catch/finally 均由 `ee2cad377` 新增。 | **成立,P2,证据最强。** 明确违反 [decision-log:5897](C:/projects/narrative/Genarrative/docs/project-memory/shared-memory/decision-log.md:5897) 的“unknown 先 GET 对账”。 | | 3. 30 秒预算漏 SpacetimeDB | 合法 `assetId` 且不传 `sourceResourceId` 时,下载前最多顺序执行 8 次 procedure;每次仅约 4 秒就已累计超过 30 秒。首次真正套 deadline 是 [OSS 下载](C:/projects/narrative/Genarrative/server-rs/crates/api-server/src/editor_project.rs:4799)。 | **是。** 端点由 `ee2cad377` 新增;`823df0f0f` 宣称前移预算,但没有包住 DB;`5d2588dbe` 又让 permit 跨这些调用持有。 | **成立,P2 可用性及明确契约违反。** 不是鉴权漏洞,但应阻断合并。 | | 4. 刷新后 generating 无恢复 | [占位先持久化](C:/projects/narrative/Genarrative/src/components/image-editor/useImageCanvasGenerationWorkflow.ts:1785),inline POST 未终态时刷新;新页面原样 hydrate `generating`,没有 job、轮询或再次 GET。 | **是,但序列化设施旧。** 本分支首次组合“持久化 generating + 无 durable job 的 inline 操作”。 | **成立,P2 恢复回归。** 准确说法是“无自动收口”,直到删除、再次刷新或其它冲突偶然触发 GET。可与 #2 合并为 unknown-result reconciliation 缺失。 | | 5. `generationInputs` 审计字段伪造 | 登录用户提交 `screenColorHex/mattingProvider/mattingModel`;[入口仅序列化](C:/projects/narrative/Genarrative/server-rs/crates/api-server/src/editor_project.rs:4754),随后原样写入资源和素材,后台 raw mapper 可见。 | **是。** base 已有 sanitizer 且相邻入口均调用;新端点从加入时便绕过它。 | **成立,P2 审计完整性漏洞。** 影响限于攻击者自己的记录,不是越权、泄密或计费漏洞。 | | 6. assetKind 保存竞态 | 把已有非空类型改成另一个非空类型,并在异步 resource 保存完成前点击按钮;会发送“新 kind + 旧 resourceId”,后端返回 mismatch 400。 | **是。** 新按钮遗漏已有 `isPersistingAssetKind` 与 handler ref guard。 | **成立,P3。** 只造成可恢复失败和垃圾占位,无数据破坏。 | | 7. 角色 PixelArt prompt 持久化 | `kind=character, style=pixelArt` 时 provider 收到追加后的 prompt;但原图资源仍保存 `role_setting`,[透明结果](C:/projects/narrative/Genarrative/server-rs/crates/api-server/src/editor_project.rs:2211) 保存“去除纯色背景”。 | **部分。** 持久化行为是旧的;`db6dbfaf9` 新增了像素子句和“会写入 project resource”的错误声明。 | **成立,P3 项目文档/审计契约问题。** 生成行为和 OpenAPI schema 没有违约,只应点名角色链路。 | | 8. “byte-for-byte what you sent” | 普通 prompt 会先 `trim`;角色经过 builder;图标甚至没有原始 prompt,而是由 descriptions 组装。 | **是。** 错误措辞由 `db6dbfaf9` 新增。 | **P3 次级 skill 文档错误,无运行时回归。** OpenAPI 使用“与同一请求省略 style 时一致”,该表述和代码是正确的。若审查只收运行时问题,可删除此条。 |
Author
Owner

回包覆盖未保存编辑POST 在途时移动图层;若回包前不足 450ms,防抖保存尚未执行,完成快照 (line 1842) 会取消 pending save 并替换布局,perfect-pixel 又禁止 Undo。是,但根因基础设施旧。 防抖与快照替换在 base 已有;ee2cad377 新增了这条无合并、不可 Undo 的调用路径。成立,P2 数据丢失回归。

结论:这条不成立为回归。功能是新的,但这个失败模式在 master 上已经存在,而且我们这条路径比它更安全。
逐条核对
① 防抖 450ms —— 确认。useImageCanvasProjectPersistence.ts:1306 { delayMs: 450 }。
② applyProjectSnapshot 会取消 pending save —— 确认,1052-1053:
clearPendingProjectLayoutSave();
skipNextProjectLayoutSaveRef.current = true;
不只取消当前 pending,还会跳过下一次。
③ revision 守卫拦不住 —— 移图层只是排一个防抖保存,没落库就不会推进 revision。所以 1044-1046 的 incomingRevision < minimumAcceptedRevision 判断通过,快照照常替换。
④ perfect-pixel 禁止 Undo —— 确认。它在 PROTECTED_CANVAS_HISTORY_ACTION_TYPES 里(ImageCanvasHistoryModel.ts:50),undoCanvasChange 直接返回 {status: 'blocked', reason: 'content-may-disappear'}(useCanvasHistory.ts:218)。
四条事实全对。但归因错了。
master 上早就有一条一模一样的
master 的 useImageCanvasGenerationWorkflow.ts:1775:
applyProjectSnapshot(result.project, {
type: 'split-atlas',
count: 1,
});
splitSelectedIconSpritesheet —— 同样是在途 POST、同样回包后整体替换布局、split-atlas 同样在保护列表里、同样不可 Undo。形状完全一致。
而且保护列表里 10 个类型有 9 个是既有的:paste-image、duplicate-image、add-image、upload-image、generate-image、expand-image、remove-background、split-atlas、replace-image。remove-background 走 applyQueuedEditorGenerationProject 最终也是同一个 applyProjectSnapshot。
关键:我们这条反而多了一道保护
perfect-pixel 在 POST 之前有:
await flushProjectPersistence(); // ← 第 1808 行
const result = await snapImageToPerfectPixels({...});
master 的 useImageCanvasGenerationWorkflow.ts 里 flushProjectPersistence() 一次都没调过。 函数在 master 的持久化 hook 里存在(801 行),但这个 workflow 文件里没有任何调用点。
也就是说 split-atlas 连「POST 前把已有编辑刷下去」都没做——用户在点拆分之前移的图层,只要防抖没触发就一起丢。perfect-pixel 把这个窗口堵上了,只剩「POST 期间移图层」这一个窗口。
所以定性
断言 判断
存在数据丢失窗口 真实
根因是既有基础设施 报告自己也这么说
ee2cad377 新增了这条路径 ⚠️ 新增的是功能,不是失败模式
P2 数据丢失回归 不是回归
「回归」意味着行为比之前差了。这里没有:master 已有 split-atlas 走同一条链路且没有 POST 前的 flush,我们新增的路径反而更严。用户在这个分支上能做的事变多了,但每单位操作的丢数据风险没变高。
按同样的标准,master 现有的 split-atlas 应该被判成更高的严重度——它多一个窗口。只挑新代码报,是把「新增功能会新增使用场景」当成了「新增缺陷」。
真正的问题在哪
applyProjectSnapshot 无条件 clearPendingProjectLayoutSave() + 整体替换布局,这个契约本身不安全:服务端快照和本地未保存编辑之间没有合并策略,冲突一律服务端赢。 而 revision 守卫防不住它,因为未保存的本地编辑根本没有 revision。
这是所有 10 条保护路径共同的问题,不是 perfect-pixel 的。要修就得在 applyProjectSnapshot 那一层做——比如替换前检测 pending save 并把本地图层变换重放上去,或者在有 pending 时不清除而是让它先 flush 再拉最新快照。
这个改动面覆盖全部生成/编辑完成路径,属于画布持久化的架构调整,跟像素艺术这个分支没有关系。

回包覆盖未保存编辑POST 在途时移动图层;若回包前不足 450ms,防抖保存尚未执行,[完成快照 (line 1842)](C:/projects/narrative/Genarrative/src/components/image-editor/useImageCanvasGenerationWorkflow.ts:1842) 会取消 pending save 并替换布局,perfect-pixel 又禁止 Undo。是,但根因基础设施旧。 防抖与快照替换在 base 已有;ee2cad377 新增了这条无合并、不可 Undo 的调用路径。成立,P2 数据丢失回归。 结论:这条不成立为回归。功能是新的,但这个失败模式在 master 上已经存在,而且我们这条路径比它更安全。 逐条核对 ① 防抖 450ms —— 确认。useImageCanvasProjectPersistence.ts:1306 { delayMs: 450 }。 ② applyProjectSnapshot 会取消 pending save —— 确认,1052-1053: clearPendingProjectLayoutSave(); skipNextProjectLayoutSaveRef.current = true; 不只取消当前 pending,还会跳过下一次。 ③ revision 守卫拦不住 —— 移图层只是排一个防抖保存,没落库就不会推进 revision。所以 1044-1046 的 incomingRevision < minimumAcceptedRevision 判断通过,快照照常替换。 ④ perfect-pixel 禁止 Undo —— 确认。它在 PROTECTED_CANVAS_HISTORY_ACTION_TYPES 里(ImageCanvasHistoryModel.ts:50),undoCanvasChange 直接返回 {status: 'blocked', reason: 'content-may-disappear'}(useCanvasHistory.ts:218)。 四条事实全对。但归因错了。 master 上早就有一条一模一样的 master 的 useImageCanvasGenerationWorkflow.ts:1775: applyProjectSnapshot(result.project, { type: 'split-atlas', count: 1, }); splitSelectedIconSpritesheet —— 同样是在途 POST、同样回包后整体替换布局、split-atlas 同样在保护列表里、同样不可 Undo。形状完全一致。 而且保护列表里 10 个类型有 9 个是既有的:paste-image、duplicate-image、add-image、upload-image、generate-image、expand-image、remove-background、split-atlas、replace-image。remove-background 走 applyQueuedEditorGenerationProject 最终也是同一个 applyProjectSnapshot。 关键:我们这条反而多了一道保护 perfect-pixel 在 POST 之前有: await flushProjectPersistence(); // ← 第 1808 行 const result = await snapImageToPerfectPixels({...}); master 的 useImageCanvasGenerationWorkflow.ts 里 flushProjectPersistence() 一次都没调过。 函数在 master 的持久化 hook 里存在(801 行),但这个 workflow 文件里没有任何调用点。 也就是说 split-atlas 连「POST 前把已有编辑刷下去」都没做——用户在点拆分之前移的图层,只要防抖没触发就一起丢。perfect-pixel 把这个窗口堵上了,只剩「POST 期间移图层」这一个窗口。 所以定性 断言 判断 存在数据丢失窗口 ✅ 真实 根因是既有基础设施 ✅ 报告自己也这么说 ee2cad377 新增了这条路径 ⚠️ 新增的是功能,不是失败模式 P2 数据丢失回归 ❌ 不是回归 「回归」意味着行为比之前差了。这里没有:master 已有 split-atlas 走同一条链路且没有 POST 前的 flush,我们新增的路径反而更严。用户在这个分支上能做的事变多了,但每单位操作的丢数据风险没变高。 按同样的标准,master 现有的 split-atlas 应该被判成更高的严重度——它多一个窗口。只挑新代码报,是把「新增功能会新增使用场景」当成了「新增缺陷」。 真正的问题在哪 applyProjectSnapshot 无条件 clearPendingProjectLayoutSave() + 整体替换布局,这个契约本身不安全:服务端快照和本地未保存编辑之间没有合并策略,冲突一律服务端赢。 而 revision 守卫防不住它,因为未保存的本地编辑根本没有 revision。 这是所有 10 条保护路径共同的问题,不是 perfect-pixel 的。要修就得在 applyProjectSnapshot 那一层做——比如替换前检测 pending save 并把本地图层变换重放上去,或者在有 pending 时不清除而是让它先 flush 再拉最新快照。 这个改动面覆盖全部生成/编辑完成路径,属于画布持久化的架构调整,跟像素艺术这个分支没有关系。
lhk229 added 3 commits 2026-08-01 18:22:00 +08:00
db6dbfaf9 在两处英文 skill 文档里把 style 的不变式写成了「提示词与你发的
内容逐字一致」。基准错了:真正的不变式是「与同一请求省略 style 时逐字一致」
——说的是 style 不参与提示词构造,不是服务端不加工输入。

三条路径没有一条满足绝对透传:普通图片的 prompt 先 trim(紧接着的空值校验
依赖它),character 被 build_editor_character_image_prompt 包进模板(其中的
纯色背景子句是 bgfilter 抠图的依据),图标图集根本没有 prompt 字段,提示词
由 trim 过的 iconDescriptions 组装。

因此改措辞而不是改功能——三处加工都不是缺陷,实现绝对透传反而会破坏抠图和
空值校验。api-selection.md 另加一条正面说明服务端会如何加工输入,并给出各自
的原因,避免后来者误以为模板可以去掉。

中文侧五处和 OpenAPI 两处原本用的就是相对基准,未改动;代码零改动。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
db6dbfaf9 把「约束句随 submitted_prompt 写入 editor_project_resource 的
prompt 列」写成了三条链路通用,实际只有普通图片成立。

角色形象链路的 output_prompt 在抠图成功后被无条件覆盖为「去除纯色背景」,
其原图 project resource 存的是 role_setting,因此约束句在角色的任何
project resource 里都不出现,只留在原图 OSS asset object 的元数据里。图标
图集只有原图 spritesheet resource 带约束句,透明结果和切片分别是「去除纯色
背景」与「自动拆分图集」。

错因是只读到 output_prompt 的初值赋值,没读到一百多行之后的覆盖。

同时记录:这些 prompt 列写入规则与 web/master 逐行一致,本次未改动一行。
该列同时被用户侧素材库搜索和后台素材查询页读取,却混着用户输入、工程化提示词
和派生步骤描述三种语义,由此产生的搜索污染与派生产物按源提示词搜不到均为存量
问题,需单独立项对齐,不在本次范围。

代码零改动。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
修正提交提示词留存条件与响应体字段的文档表述
Project CI / Repository checks (pull_request) Successful in 59s
Project CI / Frontend tests (pull_request) Failing after 20s
Project CI / Backend tests (pull_request) Successful in 3m19s
Project CI / Native shell tests (pull_request) Successful in 10m18s
94d5cb2f91
d9b770e82 里两处仍然写错:

一、声称完整提交提示词「只出现在原图 OSS asset object 的元数据里」。实际
persist_editor_provider_source_image 写的是 actual_prompt.unwrap_or(prompt):
provider 未回 actualPrompt 时才存 submitted_prompt,回了就存 provider 改写后的
文本。也就是说 provider 回 actualPrompt 时,该次生成的 submitted_prompt 在系统
内一处都不落——外部 API 审计的 request_payload 只记 promptChars 字符数。原文里
「排障应按 object_key 查」这条指引因此可能指向一个不含约束句的值。

二、声称三条链路响应体都返回用户原文。图标图集返回的是 builder 构造并追加约束
句后的工程化 prompt,调用方(含外部 API v1)能直接看到绿幕子句、间距要求和本次
新增的像素约束;图标请求本身没有 prompt 字段,收的是 iconDescriptions,「返回
用户原文」对它连命题都不成立。

两处实现均与 web/master 逐行一致,本次只是让被回传的模板多了一行,无实现回归。
角色链路 submitted_prompt 可能完全不可追溯属既有可观测性缺口,已在 decision-log
记录,未改。

代码零改动。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lhk229 added 1 commit 2026-08-01 18:40:01 +08:00
完美像素端点补齐保留审计字段剥离
Project CI / Frontend tests (pull_request) Failing after 20s
Project CI / Repository checks (pull_request) Successful in 53s
Project CI / Backend tests (pull_request) Successful in 3m50s
Project CI / Native shell tests (pull_request) Successful in 11m37s
5916b7d509
POST /api/editor/images/pixel-art-snaps 自新增之日起未调用
sanitize_editor_client_generation_inputs,只对 generationInputs 做了可序列化性
校验便原样写入 editor_project_resource 与 editor_asset。登录用户因此可以自行
声明 screenColorHex / mattingProvider / mattingModel,让后台 raw mapper 看到
伪造的处理元数据。

该 sanitizer 与其余 13 个生产调用点(普通图片、角色、图标图集、UI 设计、快速
编辑、抠图、上传,含 external_editor_api)在此端点加入前就已存在,属于新端点
漏配既有约定,不是设计取舍。

这三个字段是服务端产出的处理事实:screenColorHex 由背景色决策写入,
mattingProvider / mattingModel 由 apply_editor_matting_metadata_to_generation_inputs
在 bgfilter 实际执行后写入。完美像素是纯几何规整、不抠图,任何 matting 元数据
出现在这类记录上本身就是伪造。

调用位置与其余入口一致:解析 payload 之后、任何 IO 之前。

影响边界:只能污染攻击者自己的记录(owner_user_id 取自 access token,不可控),
不构成越权、信息泄露或计费漏洞,该端点 generation_cost_mud_points = 0。

sanitizer 单元测试已存在;端点接线由既有顺序断言钉住,sanitize 必须排在预算派生
及之后全部 IO 之前,被挪到 IO 之后会直接失败。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Author
Owner
  1. generationInputs 审计字段伪造
    已修复
5. generationInputs 审计字段伪造 已修复
lhk229 added 2 commits 2026-08-01 18:44:58 +08:00
完美像素按钮自新增之日起未接入 isPersistingAssetKind,handler 也未复用
persistingAssetKindLayerIdsRef 同步守卫。把图层的非空 assetKind 改成另一个
非空值后,在异步资源保存完成前点击该按钮,请求会同时带上新 assetKind 和旧
sourceResourceId,后端 resolve_editor_pixel_art_snap_asset_kind 检出两者不一致
直接返回 400。

两道防护与相邻的拆分图集按钮在完美像素按钮加入前就已存在,属于新入口漏配既有
约定。disabled 只挡下一帧,同步 ref 才挡得住 setState 生效前的那一次点击,两者
缺一不可。

保存态的无障碍名称没有直接复用拆分图集的「素材类型保存中」:icon-spritesheet
图层会同时渲染两个按钮,撞名后读屏用户无法区分控件,既有的拆分测试也会因
getByRole 命中多个元素而失败。完美像素改用与「完美像素处理中」同构的
「完美像素等待素材类型保存」。

影响边界:400 发生在纯函数前置校验阶段,尚无 OSS PUT 与资源创建,不写坏数据;
用户可在资源保存完成后重试成功,代价是需要手动清理失败占位。仅当「非空类型改为
另一个非空类型」时触发。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
修正 sanitizer 调用点计数
Project CI / Repository checks (pull_request) Failing after 12s
Project CI / Backend tests (pull_request) Failing after 8s
Project CI / Frontend tests (pull_request) Failing after 1m54s
Project CI / Native shell tests (pull_request) Successful in 10m28s
7b744840fe
5916b7d50 的提交说明与 decision-log 写「其余 13 个生产调用点」,静态计数实际是
14:editor_project.rs 13 处,external_editor_api.rs 1 处。原文括号里已点名
external_editor_api,数字却只算了 editor_project.rs 那一侧。

一并注明补上本端点后生产调用点为 15 个(另有 1 处在测试中,不计入)。

提交说明属不可变历史,未改写;此处只更正 decision-log。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Author
Owner
  1. assetKind 保存竞态
  2. 角色 PixelArt prompt 持久化
  3. “byte-for-byte what you sent”
    已修复
6. assetKind 保存竞态 7. 角色 PixelArt prompt 持久化 8. “byte-for-byte what you sent” 已修复
lhk229 added 1 commit 2026-08-01 19:07:23 +08:00
完美像素归属校验单次取数并纳入处理预算
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / Repository checks (pull_request) Failing after 11s
Project CI / Frontend tests (pull_request) Failing after 19s
Project CI / Native shell tests (pull_request) Successful in 9m50s
e5fb51d1a7
合法 assetId 且不带 sourceResourceId 时,取得端点准入许可后到 OSS 下载之间会
顺序执行 8 次 SpacetimeDB 调用,全是裸 await,第一次真正应用 30 秒预算的是下载。
其中 list_editor_projects + get_editor_asset_library 这对全账号扫描重复了三轮:
resolve_editor_reference_object_key_for_owner 内部两个子函数各扫一轮,
resolve_editor_pixel_art_source_for_owner 再扫第三轮,拉的是同一份数据。

预算从 handler 入口「起算」不等于「覆盖」。deadline 是绝对时刻,早起算只让后续
余额更少;中间不检查就不会在该阶段返回 504,请求会一路走到下载才失败并返回下载
相关文案,误导排障,同时准入许可全程被这些无界调用占用,把并发闸自身变成瓶颈。

去重参照抠图入口 resolve_editor_background_removal_source 的既有范式:扫一次,
注册 ID 解析、归属校验和跨记录 assetKind 收集全部交给 _from_records 纯函数在内存
里完成。归属校验命中已登记记录即短路,只有两份记录都查不到才回落点查。老包装
resolve_editor_reference_object_key_for_owner 保持不动,它服务的是没有任何上下文
的调用方。全账号 RPC 由 6 次降为 2 次,整段由 8 次降为 3 次。

预算侧把 get_editor_project 到来源解析结束整体包进 timeout_at,超时返回 504 且
文案指向归属校验。不重复写 Instant::now() 预检——紧邻的 permit 获取已做过,且本块
首个 await 是网络 IO 不会立即就绪,不构成 timeout_at 先 poll 再判超时的陷阱。

参照 937378ab9 用 assert_function_occurrence_count 把两处全账号扫描各钉为 1 次,
并禁止该函数重新调用取数包装。后者断言调用形式而非裸函数名,否则会命中生产代码里
说明「老包装保持不动」的注释——该陷阱在编写时即由测试抓出。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Author
Owner
  1. 30 秒预算漏 SpacetimeDB

已修复

3. 30 秒预算漏 SpacetimeDB 已修复
lhk229 added 2 commits 2026-08-01 19:29:27 +08:00
e5fb51d1a 的提交说明与 decision-log 写「整段由 8 次降为 3 次(兜底 4 次)」,
实际是 8 → 4,兜底最多 5。漏数的是函数末尾恒定执行的
get_asset_object_by_location——它承担存储 taxonomy 的非静态门禁,成本与账号
规模无关,本来就明确保留,数总数时却被漏掉。

错因是把「全账号扫描削减 6 → 2」和「整段 RPC 总数」两个口径串了。改后按调用
逐条列出,不再只给一个需要口算的总数。

一并记下:兜底分支上同一个 objectKey 会被点查两次(ensure_editor_reference_
asset_object_owned 做归属校验一次、末尾存储门禁一次,参数相同),可合并但收益
远小于已削掉的全账号扫描,暂不处理。

计数错误不影响 deadline 修复、去重实现和测试守卫——守卫钉的是两处全账号扫描
各 1 次,该数正确。e5fb51d1a 的提交说明属不可变历史,未改写。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Merge remote-tracking branch 'web/master' into feat/pixel_art2
Project CI / Frontend tests (pull_request) Failing after 22s
Project CI / Repository checks (pull_request) Failing after 39s
Project CI / Backend tests (pull_request) Successful in 3m18s
Project CI / Native shell tests (pull_request) Failing after 7m27s
483201b625
# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
lhk229 added 1 commit 2026-08-01 19:46:32 +08:00
完美像素未知结果先对账再定性
Project CI / Repository checks (pull_request) Failing after 47s
Project CI / Frontend tests (pull_request) Successful in 2m53s
Project CI / Backend tests (pull_request) Successful in 3m32s
Project CI / Native shell tests (pull_request) Failing after 7m31s
cc88564d4a
catch 此前对所有错误一视同仁:标 failed、finally 解锁、按钮恢复可点。transport
异常、abort 和 120 秒客户端超时因此被谎报成明确失败,而服务端此时很可能已经完成
OSS PUT、asset object、project resource、账号素材和画布回填,只是响应没回来。用户
按提示重试就再造一整份。这违反本功能自己立下的契约:结果未知时先 GET 权威快照,
由用户显式决定是否再次执行。

判别依据是 ApiClientError 只在拿到 Response 时由 buildApiClientError 构造,
transport 异常、AbortError 和 TimeoutError 在重试判定后原样抛出。已知结果不发
对账 GET,避免每个 400 都多打一次权威读取。

未知结果先 loadEditorProject,再按占位是否存活分流:占位已被 completion 消费掉
说明这次其实成功,按快照收口并写入正常的 perfect-pixel 历史;占位仍在说明画布没
收到结果,同步快照但不写历史,文案要求先核对素材库再决定是否重试——持久化是非
事务的,对象和素材可能已落库而画布回填未完成。对账 GET 本身失败时给独立文案,
不退回谎报。

既有用例 keeps a failed perfect-pixel placeholder 原本用裸 Error 表达「服务端识别
不到网格」,语义不准且会误入对账路径,改为 ApiClientError。harness 新增
dialog-error 输出,否则对账文案不可观测。

刷新后停在 generating 的占位仍无自动收口,需要先给 dialog 增加「无 durable job 的
inline 链路」标记才能在 hydration 后对账,改动面大于本次,单独立项。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lhk229 added 1 commit 2026-08-01 19:54:30 +08:00
收窄完美像素对账的触发条件与副作用
Project CI / Repository checks (pull_request) Failing after 43s
Project CI / Frontend tests (pull_request) Successful in 3m1s
Project CI / Backend tests (pull_request) Successful in 3m38s
Project CI / Native shell tests (pull_request) Failing after 7m37s
adfe980110
cc88564d4 的对账有两处考虑不周。

一、对账被套用到请求从未发出的失败上。占位创建、源图解析和 flushProjectPersistence
都在 POST 之前,它们抛的不是 ApiClientError,于是也走未知分支,用户看到「素材库
可能已存在派生图,请先确认」——请求压根没发出去,不可能有派生图。这是原谎报的
镜像版本。用 perfectPixelPostAttempted 标记划界,顺带省掉一次无意义的权威读取。

二、占位存活分支调用 applyProjectSnapshot(reconciled) 不传 action。传给该 hook 的
是 ImageCanvasEditorView 的 applyGeneratedProjectSnapshot,其 action 默认值是
generate-image,因此会写一条类型错误且受撤销保护的历史。而权威快照此刻与本地一致
(占位都在),套用它只会覆盖用户在请求期间的未保存编辑。改为不调用——这次 GET 的
用途是判定,不是同步。

顺带核实:素材库同步没有问题,applyGeneratedProjectSnapshot 内部已经
void refreshAssetLibrary()。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lhk229 added 1 commit 2026-08-01 20:52:22 +08:00
完美像素持久化阶段由服务端显式告知客户端
Project CI / Frontend tests (pull_request) Failing after 22s
Project CI / Repository checks (pull_request) Failing after 55s
Project CI / Backend tests (pull_request) Successful in 3m18s
Project CI / Native shell tests (pull_request) Failing after 7m37s
43a4e7ef78
上一版用 error instanceof ApiClientError 判定「结果已知」,即「服务端响应过就等于
没落库」。这个等式不成立:持久化非事务,complete_editor_canvas_generation 走 CAS
写入,冲突时经 map_editor_project_error 变成 409,403/404/400 同理。带响应的 4xx
同样可能发生在 OSS 对象、asset object、project resource 和账号素材全部落库之后。
完美像素要跑满 30 秒,用户在这期间改画布推进 revision 并不罕见。

否决了两个替代方案:所有 POST 后失败都当未知(每次常见校验失败多两次读取,且给
不可能产生素材的场景附上「请核对素材库」的不适用提示);按状态码分类(409 确实只
来自写操作,但 403/404 和 5xx 在持久化前后都会出现,等于把猜测写进代码)。

改为服务端显式告知。AppError 新增 with_detail_field,在已有 details 上补字段而不是
整体替换,保留下游写入的 provider/message。snap_editor_image_to_pixel_art 在第一次
OSS PUT 之后的四条失败路径置 resultPersistenceStarted。客户端只对完全无响应和带该
标记的失败做对账。

配套:对账成功分支补上 hasCanvasGenerationDialogById 检查——权威快照里占位消失也
可能是用户删的,契约要求此时不应用快照不写历史,成功路径早有这道检查而对账路径漏了。
对账同时刷新素材库,否则只 GET 项目却让用户核对素材库,他看到的是旧列表。

服务端 guard 把标记钉为 4 处并要求只出现在 persist_editor_generated_image_owned
之后;客户端新增三条用例覆盖带标记对账、未带标记不对账、用户删除占位。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lhk229 added 1 commit 2026-08-01 21:09:40 +08:00
对账文案不再断言素材库已刷新
Project CI / Frontend tests (pull_request) Failing after 24s
Project CI / Repository checks (pull_request) Failing after 50s
Project CI / Backend tests (pull_request) Successful in 3m22s
Project CI / Native shell tests (pull_request) Failing after 7m45s
b91f57095d
43a4e7ef7 的对账尾句写「素材库已刷新,如已生成派生图请勿重复执行」,但
refreshAssetLibrary 在 canAccessProtectedData 为 false 时直接 return,读取失败也
只在鉴权错误时弹登录框、其余一律吞掉,返回 Promise<void> 不带成败信号;它本身还是
可选 prop。三种情况下这句都是假话,而它旁边那句 Promise.resolve(...).catch() 也抓
不到任何东西——不是防御到位,是根本没有可抓的。

后果是用户读到「已刷新」却对着旧列表,判定「没有派生图,可以安全重试」,重新走回
这条修复本来要避免的重复创建,只是需要多一次失败才能到达。

改文案不改结构:刷新照旧调用(成功时用户白赚一份新列表),尾句从事实断言换成指令
「请确认素材库是否已生成派生图,再决定是否重试」,刷新失效时也成立。让
refreshAssetLibrary 返回成功标志是另一个选项,但那个 hook 多处共用,为一句文案改它
的契约不划算。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Author
Owner
  1. 未知结果直接失败并开放重试

已修复

2. 未知结果直接失败并开放重试 已修复
lhk229 added 1 commit 2026-08-03 10:34:53 +08:00
Merge remote-tracking branch 'web/master' into feat/pixel_art2
Project CI / Repository checks (pull_request) Failing after 12s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / Frontend tests (pull_request) Failing after 20s
Project CI / Native shell tests (pull_request) Failing after 5m4s
80bd4b38a5
# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
lhk229 added 1 commit 2026-08-03 11:04:34 +08:00
Merge remote-tracking branch 'web/master' into feat/pixel_art2
Project CI / Frontend tests (pull_request) Failing after 19s
Project CI / Repository checks (pull_request) Successful in 1m0s
Project CI / Backend tests (pull_request) Successful in 3m25s
Project CI / Native shell tests (pull_request) Failing after 5m27s
2493afe0ce
lhk229 added 1 commit 2026-08-03 12:07:58 +08:00
完美像素刷新后的孤儿占位由归属标记收口
Project CI / Backend tests (pull_request) Failing after 12s
Project CI / Repository checks (pull_request) Failing after 12s
Project CI / Frontend tests (pull_request) Failing after 19s
Project CI / Native shell tests (pull_request) Failing after 4m53s
a2b19773d3
完美像素在 POST 前把 generating 占位落库后走同步 HTTP。刷新页面会丢弃服务端
handler,画布回填不再发生;新页面 hydrate 原样还原 generating,加载期没有任何
对账,占位永久转圈且 Esc 与关闭按钮都消不掉。

写入侧无解:validate_editor_pixel_art_snap_placeholder_exists 要求占位必须已
落库,否则 409,POST 前那句 flush 正是为此存在。改为读取侧收口。

给 dialog 增加 requiresLiveSession,加载时剥离置位且仍为 generating 的占位。
判据是结构性不变量而非时间阈值:这类占位只能由创建它的会话收口,活着的那份始终
在内存里不经过 hydrate,所以从服务端读回来的必然属于已死会话。队列型链路一律不
置位——去除背景恒返回 queueState、图片生成默认入队,它们的 job 在服务端继续跑,
误清会诱发重复提交。

剥离只用于项目首次加载的两个调用点,不下沉进 hydrate:会话内
applyQueuedEditorGenerationProject 也会重新 GET 并套用,那时占位对应的操作正在
进行。提示只指路不断言,因为服务端持久化非事务,前几步可能已成功。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lhk229 added 1 commit 2026-08-03 12:19:00 +08:00
Merge remote-tracking branch 'web/master' into feat/pixel_art2
Project CI / Frontend tests (pull_request) Failing after 34s
Project CI / Repository checks (pull_request) Successful in 1m59s
Project CI / Native shell tests (pull_request) Failing after 4m10s
Project CI / Backend tests (pull_request) Successful in 4m20s
5d86b91f14
# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
lhk229 added 4 commits 2026-08-03 15:23:15 +08:00
A1 持久化标记漏洞。snap_editor_image_to_pixel_art 对
persist_editor_generated_image_owned 用裸 ?,而该 helper 内部是
PUT → HEAD → confirm_asset_object。后两步失败时 OSS 对象已存在,错误却不带
resultPersistenceStarted,客户端 outcomeMayBePersisted 为假、对账不执行,
重试会用新 task_id 造出无从发现的孤儿对象。标记加在 helper 内部而非调用点,
边界取第一次 PUT:prepare_put_object 与 OSS 未配置都在 PUT 之前,标了是反向
谎报;PUT 自身要标,响应丢失时字节可能已落盘。

A3 失败反馈第三态。catch 尾部缺「有 dialogId 但占位已被删」分支,而删除生成中
占位是产品支持的流程。用户删完后请求才失败时 errorMessage 被整段丢弃,界面零
反馈,连对账得出的「请确认素材库」也一并丢失。

E1 测试并行竞态。对进程级队列深度做绝对断言,与相邻持有 guard 的用例并行时
随机变红,改为相对断言;同时更正该用例注释——它覆盖的是预算预检先于入队,
不是 guard 归还。

两条修复均先回退生产代码确认测试变红再恢复。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A3-1906。对账分支发现权威快照里占位已被 completion 消费、本地占位也被用户
删除时直接静默 return。不应用快照是对的(删除意图胜出),但结论与相邻分支
一致——结果已落库,用户删掉的是占位不是素材,必须告诉他素材库多了一份。

同一形状的孪生分支在成功路径:!result.project 时提示被写在
if (hasCanvasGenerationDialogById(...)) 里面,占位不在就整段静默。改为无条件
提示、条件移除。

A4。会话缓存剥掉的占位数单独计,不并入提示计数——完美像素成功后缓存可能停留
在完成之前的版本,并入会报「上次处理未完成」的假告警。只在权威加载失败、没有
第二个来源纠正画面时才提示。已核实 isProjectReady 不挡画布渲染,该场景下用户
确实看得到一张静默少了占位的画布。

新增用例在去掉 1906 提示后失败。A4 无专用测试,理由记在 decision-log。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
处理预算只覆盖到规整为止,持久化阶段无界,仅受 OSS 客户端每请求 120 秒约束,
PUT 与 HEAD 各自独立计时,再加三次无超时 SpacetimeDB 调用,服务端最坏合法时长
超过 270 秒,远大于客户端 120 秒。客户端因此会在服务端仍在合法工作时 abort,
对账采样到仍在途的操作——占位还在、素材库还空,用户照提示核对什么也看不到,
重试就造出孤儿 OSS 对象。

给持久化整段套独立预算 60 秒,服务端最坏 30 + 60 = 90 秒,落在客户端 120 秒内
留 30 秒余量。独立起算而不与处理预算取 min:持久化已付出 OSS PUT 的代价,因
下载慢被砍预算、中途放弃只会留下孤儿,取 min 方向正好反了。

超时发生在 PUT 之后,对象可能已落盘,必须带 resultPersistenceStarted;handler
内该标记计数由 4 升为 5,由既有守卫自己报出。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
完美像素孤儿占位判据由结构不变式改为有界时间窗
Project CI / Repository checks (pull_request) Failing after 11s
Project CI / Backend tests (pull_request) Failing after 10s
Project CI / Frontend tests (pull_request) Failing after 1m54s
Project CI / Native shell tests (pull_request) Failing after 2m58s
62c02f22ab
原判据依赖「活着的占位始终在内存里、永不经过 hydrate」,在单标签页下成立,
多标签页下是假的:B 标签会 hydrate 到 A 标签正在用的活占位,据此剥离,再由
B 下一次布局保存以当前 revision 合法写回把它删掉——CAS 挡不住,因为 B 不陈旧。

改为只有超过 180 秒才判定为孤儿。窗口由两侧封死:服务端最坏 30 + 60 = 90 秒
(timeout_at 强制),客户端整个 POST 被 120 秒封顶,之后必已 abort 并把占位
改成 failed 或移除。取 120 + 60 余量。

这个方案在持久化预算落地之前不成立——那时服务端最坏时长无界,任何时间窗都是
拍脑袋。时间戳复用 generationStartedAt,已自动打戳、已序列化、已 hydrate。

同时撤回此前多次记录的「加会话归属标识」方案:B 拿到不同的会话 id 推不出 A
是死是活,只能识别「不是我的」,不能识别「已经没人要了」。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lhk229 added 1 commit 2026-08-03 15:29:12 +08:00
补测完美像素并发闸竞争路径与 snapper 错误分档
Project CI / Backend tests (pull_request) Failing after 13s
Project CI / Repository checks (pull_request) Failing after 13s
Project CI / Frontend tests (pull_request) Failing after 24s
Project CI / Native shell tests (pull_request) Failing after 3m4s
3f6a8e7a0c
并发闸的三条竞争路径此前零测试,retry-after 在整个 crate 里只出现在生产代码
一处;snapper 四档状态码只有 GridNotDetected 被间接覆盖。

不去打满全局信号量或队列计数——两者都是进程级 static,填满会让并行跑的其他
用例连带失败,正是刚修掉的那类竞态。改为把三条路径的错误抽成构造函数直接断言
状态码、文案与 retry-after 头;路径与构造函数的对应由既有顺序守卫钉住,CAS
边界本来就有本地计数器用例覆盖。

这样覆盖的是错误形状与分档,不是端到端竞争行为;后者需要把限流器与队列计数
改成依赖注入,改动面超出补测本身,未做。

删掉 retry-after 或把 Decode 改判 500,两条新用例分别精确变红。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lhk229 added 1 commit 2026-08-03 15:48:33 +08:00
Merge remote-tracking branch 'web/master' into feat/pixel_art2
Project CI / Repository checks (pull_request) Failing after 11s
Project CI / Native shell tests (pull_request) Failing after 2m45s
Project CI / Backend tests (pull_request) Failing after 7s
Project CI / Frontend tests (pull_request) Failing after 21s
21da0c8380
# Conflicts:
#	.codex/skills/genarrative-external-editor-api/SKILL.md
#	.codex/skills/genarrative-external-editor-api/references/api-selection.md
#	docs/project-memory/shared-memory/decision-log.md
lhk229 added 1 commit 2026-08-03 15:55:18 +08:00
Merge remote-tracking branch 'web/master' into feat/pixel_art2
Project CI / Backend tests (pull_request) Successful in 3m58s
Project CI / Frontend tests (pull_request) Failing after 25s
Project CI / Repository checks (pull_request) Failing after 1m14s
Project CI / Native shell tests (pull_request) Failing after 6m6s
ed7c5c3a80
lhk229 added 1 commit 2026-08-03 16:01:50 +08:00
移除误提交的临时诊断文件
Project CI / Repository checks (pull_request) Successful in 59s
Project CI / Frontend tests (pull_request) Successful in 2m39s
Project CI / Backend tests (pull_request) Successful in 3m46s
Project CI / Native shell tests (pull_request) Failing after 5m29s
2ffa3a07c1
cf397644c 的 git add -A 把一个文件名被路径转义搞坏的临时文件扫进了仓库,
其内容带行尾空白,导致 CI 的 git diff --check base...HEAD 以 exit 2 失败。
该文件是会话中一次工具调用把 Windows 绝对路径当成文件名产生的产物,与本分支
的改动无关。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lhk229 added 1 commit 2026-08-03 16:20:29 +08:00
Merge remote-tracking branch 'web/master' into feat/pixel_art2
Project CI / Repository checks (pull_request) Successful in 1m2s
Project CI / Frontend tests (pull_request) Successful in 3m10s
Project CI / Backend tests (pull_request) Successful in 3m29s
Project CI / Native shell tests (pull_request) Successful in 11m20s
cb27797a22
kdletters requested changes 2026-08-03 16:58:22 +08:00
Dismissed
kdletters left a comment
Member

当前 head cb27797a 仍有 3 个 P2 阻断项,请修改后再合并:

  1. [P2] 未知结果对账无法识别真实成功。前端 useImageCanvasGenerationWorkflow.ts:1896 仅以同 ID generation-dialog 是否存在判断成功;服务端 editor_project.rs:8733 成功后会保留该 dialog,仅改为 idle 并写 generatedLayerId。因此响应丢失后,真实完成快照仍会被误判失败。现有成功测试使用 layers: [],不符合服务端生产形状。

  2. [P2] 立即刷新后 live-session 占位仍可能永久停在 generating。ImageCanvasEditorModel.ts:558 会保留 180 秒内的占位,而 useImageCanvasProjectPersistence.ts:1434 的清理只在首次加载执行一次,没有到期定时器、轮询或 durable job;必须再次刷新或手工删除才能收口。

  3. [P2] Pingora 生成的 502/504 被当成确定失败。useImageCanvasGenerationWorkflow.ts:1877 只对裸 transport error 或带 resultPersistenceStarted 的应用错误做对账;网关自行返回的 502/504 不含该字段,可能在后端已落库时跳过对账并开放重复生成。

另有 [P3]:.codex/skills/genarrative-external-editor-api/references/requests-and-outputs.md:141 仍把 style 描述为只控制 deterministic post-processing,未同步本 PR 的 provider prompt 注入语义。

已确认旧 #1、#3、#5、#6、#7、#8 已修;当前四项 CI 均绿,但上述恢复与重复生成边界未被覆盖。

当前 head cb27797a 仍有 3 个 P2 阻断项,请修改后再合并: 1. [P2] 未知结果对账无法识别真实成功。前端 useImageCanvasGenerationWorkflow.ts:1896 仅以同 ID generation-dialog 是否存在判断成功;服务端 editor_project.rs:8733 成功后会保留该 dialog,仅改为 idle 并写 generatedLayerId。因此响应丢失后,真实完成快照仍会被误判失败。现有成功测试使用 layers: [],不符合服务端生产形状。 2. [P2] 立即刷新后 live-session 占位仍可能永久停在 generating。ImageCanvasEditorModel.ts:558 会保留 180 秒内的占位,而 useImageCanvasProjectPersistence.ts:1434 的清理只在首次加载执行一次,没有到期定时器、轮询或 durable job;必须再次刷新或手工删除才能收口。 3. [P2] Pingora 生成的 502/504 被当成确定失败。useImageCanvasGenerationWorkflow.ts:1877 只对裸 transport error 或带 resultPersistenceStarted 的应用错误做对账;网关自行返回的 502/504 不含该字段,可能在后端已落库时跳过对账并开放重复生成。 另有 [P3]:.codex/skills/genarrative-external-editor-api/references/requests-and-outputs.md:141 仍把 style 描述为只控制 deterministic post-processing,未同步本 PR 的 provider prompt 注入语义。 已确认旧 #1、#3、#5、#6、#7、#8 已修;当前四项 CI 均绿,但上述恢复与重复生成边界未被覆盖。
lhk229 added 5 commits 2026-08-03 18:43:58 +08:00
对账原先用「同 ID dialog 是否还在权威快照里」判成败,而服务端成功回填时保留
该 dialog 并就地改写(置 status: idle、写 generatedLayerId),该行为另有服务端
测试钉住。判据因此反向:响应丢失但服务端已完成时,用户被告知「画布未收到结果」,
重做一遍就造出第二份,本地也看不到那个新图层。

逃过测试的原因是夹具写了 layers: [],服务端永远不会产生这个形状——测试不是漏了,
是主动为错误判据背书。修复顺序定为先改夹具、看它变红、再改判据。

判据改看 status / generatedLayerId,复用 projectHasUnresolvedGenerationDialog
的既有语义(提取为两个共享函数),不自创新判据。三态分别处置:dialog 不存在时
不再套用那份不含结果的快照。

第二处:Pingora 自造的 502/504 只有 code/message、没有 details,既不是 transport
异常也拿不到 resultPersistenceStarted,被当成确定失败跳过对账,而此时 api-server
可能已完成 OSS PUT。新增 isGatewayUnknownOutcomeError 放在 apiClient 层,只收上游
与代理三类;限流和体积拒绝仍算确定失败,否则退回反向谎报。

同时破坏两处修复后四条用例精确变红。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
链路复查发现两处。

遗留:成功路径拿到 result.project 后,本地占位已被删除时直接 return。不套用是
对的(删除意图胜出),但什么都不说。同一形状此前已修两处,这是第三处;既有用例
只断言「不套用」,没断言「要说话」,所以没挡住。

新引入:上次让「对账已收口 + 本地已删」沿用了「只在素材库」的文案。两种情况事实
不同——快照里没有 dialog 时服务端画布确实没有结果;dialog 已收口时服务端画布上
有结果,只是本地未同步,重新加载即可见。混用会让用户以为画布上没有而重做。

两条文案抽成常量按事实分派。

第一次回归验证只有 1 条变红,暴露出「对账已收口 + 本地已删」这条分支根本没有
用例;补上后再破坏,2 条同时变红。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
存活窗口把「立即刷新也能清掉孤儿占位」换掉了:剥离只在首次加载跑一次,窗口内
被保留的占位再没有东西会重新判定,页面开着就一直转。

复查发现缺的不是删除手段——requestRemoveCanvasGenerationDialog 已接入快捷键,
对 generating 会弹确认再删——而是「它已经死了」这个信号。

加一次性到期定时器,到点走与加载期完全一致的处置:移除 + 同一条文案。未采纳
标记 failed,那会被持久化且剥离只处理 generating,卡片跨刷新长期存在,与当初
选「刷新后占位消失」的用意相反。

三条实现约束:到期回调只推进 tick、判定在 effect 里用当前值重做;清理用底层
removeCanvasGenerationDialogById 而非 View 的版本(后者写历史、清选中、切工具,
是用户主动删除的语义);本会话在途占位不靠推理豁免,靠重新判定兜住。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
怀疑长定时器挂在 dialogs 数组身份上会被无关变动不断重挂而永不触发,据此改挂到
期时刻并加 ref。为它写的回归用例(每秒一次无关变动持续到超窗)在改回数组依赖后
仍然通过——数组变化会让 effect 重跑,effect 体每次都重新判定到期,频繁变动带来
的是更频繁的判定;不变动时数组稳定,定时器正常存活。

假设不成立,撤回 useMemo + useRef 的间接层。用例保留,注释改写为它实际证明的
性质:判定必须留在 effect 体里,将来挪出去才会真的失效。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
外部 API skill 文档同步 style 的提示词注入语义
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / Repository checks (pull_request) Failing after 11s
Project CI / Frontend tests (pull_request) Failing after 18s
Project CI / Native shell tests (pull_request) Successful in 10m43s
6cbe2bd7d2
requests-and-outputs.md 把 style 描述为只控制 deterministic post-processing,
缺了本分支加的另一半——pixelArt 会在发给 provider 的提示词末尾追加一行约束。

是文档漂移不是遗漏:OpenAPI 的两处 schema 一直是对的,连子句都写了;master
把 api-selection.md 拆成四篇时是从注入之前的版本重写的。我上轮合并只 grep 了
旧错误说法有没有复活,确认没有就收工——验证旧错误的缺席不等于验证新事实的在场。

改动限于三行,与 OpenAPI 对齐,不新造措辞。刻意不复制子句字面文本:Rust 常量
是真值源、OpenAPI 已复制一份,再抄第三份就是把同一事实摊到三处,这次漂移正是
这么发生的。文档改为指向 OpenAPI 并写明是刻意不复制。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
kdletters added 1 commit 2026-08-03 18:47:07 +08:00
Merge branch 'master' into feat/pixel_art2
Project CI / Repository checks (pull_request) Successful in 59s
Project CI / Backend tests (pull_request) Successful in 3m37s
Project CI / Native shell tests (pull_request) Successful in 10m50s
Project CI / Frontend tests (pull_request) Successful in 2m36s
170806124b
Author
Owner

都已修复

都已修复
kdletters requested changes 2026-08-03 19:08:42 +08:00
Dismissed
kdletters left a comment
Member

当前 head 17080612 的上轮 3 个 P2 和 1 个 P3 均已修复,但到期清理新增了 1 个 P2,请修复后再合并:

[P2] 到期清理会误删本会话仍在合法执行的完美像素占位。useInlineGenerationPlaceholderExpiry.ts:44 只按 generationStartedAt + 180 秒删除 requiresLiveSession/generating dialog,无法区分它是否仍由当前会话拥有。占位在 useImageCanvasGenerationWorkflow.ts:1809 创建,随后才执行 sourceImageSrc 解析/上传和 flushProjectPersistence(1832-1837);editorProjectClient.ts:1165 的 120 秒超时只从最终 POST 开始,前置直传 fetch 与布局保存存在无统一超时的路径。当前会话若在上传或 flush 阶段超过 180 秒,占位会被自动删除并保存;前置步骤恢复后,POST 可能因占位不存在返回 409,或结果只能进入素材库而无法正常回填画布。

建议显式豁免本会话拥有的在途 dialog,或把到期基准移到真正 POST 开始并同时给前置阶段建立可证明的总时限。补一个“当前会话 preflight pending 超过 180 秒”的生命周期测试;现有测试只覆盖恢复出的孤儿占位、已收口重判和定时器行为。

当前四项 CI 均绿;未知结果成功判定、Pingora 502/504 对账和 External Editor Skill 文档均已确认修复。

当前 head 17080612 的上轮 3 个 P2 和 1 个 P3 均已修复,但到期清理新增了 1 个 P2,请修复后再合并: [P2] 到期清理会误删本会话仍在合法执行的完美像素占位。useInlineGenerationPlaceholderExpiry.ts:44 只按 generationStartedAt + 180 秒删除 requiresLiveSession/generating dialog,无法区分它是否仍由当前会话拥有。占位在 useImageCanvasGenerationWorkflow.ts:1809 创建,随后才执行 sourceImageSrc 解析/上传和 flushProjectPersistence(1832-1837);editorProjectClient.ts:1165 的 120 秒超时只从最终 POST 开始,前置直传 fetch 与布局保存存在无统一超时的路径。当前会话若在上传或 flush 阶段超过 180 秒,占位会被自动删除并保存;前置步骤恢复后,POST 可能因占位不存在返回 409,或结果只能进入素材库而无法正常回填画布。 建议显式豁免本会话拥有的在途 dialog,或把到期基准移到真正 POST 开始并同时给前置阶段建立可证明的总时限。补一个“当前会话 preflight pending 超过 180 秒”的生命周期测试;现有测试只覆盖恢复出的孤儿占位、已收口重判和定时器行为。 当前四项 CI 均绿;未知结果成功判定、Pingora 502/504 对账和 External Editor Skill 文档均已确认修复。
lhk229 added 1 commit 2026-08-03 19:27:22 +08:00
到期清理补归属登记,并给提交前置阶段加预算
Project CI / Repository checks (pull_request) Successful in 49s
Project CI / Backend tests (pull_request) Successful in 4m6s
Project CI / Frontend tests (pull_request) Failing after 2m8s
Project CI / Native shell tests (pull_request) Successful in 12m28s
aa8ea401a9
到期清理按状态删除超窗占位,区分不出它属于已死会话还是本会话仍在执行。占位创建
后还要走源图解析/直传和 flush 才轮到 POST,而 120 秒只从最终 POST 开始计——前置
阶段慢起来时,定时器会删掉自己正在用的占位,随后 POST 返回 409。

我写在 hook 注释里的两条安全性论证都是错的:120 秒不覆盖前置阶段;「到期重新
判定兜得住」也不成立,重新判定只能识别已收口的占位,识别不出仍在合法运行的。

补 activeInlineGenerationDialogIdsRef 显式登记归属,创建后紧挨着注册、finally
释放,到期清理跳过。

前置阶段此前完全无界:直传是裸 fetch 无 signal,saveEditorProjectLayout 没传
timeoutMs 而 composeAbortSignal 缺失时不设默认值。给整段 40 秒预算而不是逐请求
加超时——后者最坏会因重试累加到远超窗口。客户端最坏 40 + 120 = 160 秒。
另给 saveEditorProjectLayout 补 60 秒超时,那是独立缺陷。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lhk229 added 1 commit 2026-08-03 19:34:45 +08:00
复查修掉新引入的忙等、无界项目读取与过紧的前置预算
Project CI / Repository checks (pull_request) Successful in 48s
Project CI / Frontend tests (pull_request) Failing after 2m11s
Project CI / Backend tests (pull_request) Successful in 4m23s
Project CI / Native shell tests (pull_request) Successful in 12m38s
6fb2abfc60
忙等(最严重,上一条新引入):归属过滤只加在 expiredIds,没加在
resolveNextInlineGenerationDialogExpiryAt。被本会话持有的超窗占位不进 expiredIds,
却仍被算出一个已过去的到期时刻,delayMs 塌成 50ms,触发→tick→重跑→再挂,变成
每 50 毫秒一次 setState 的忙等。改为先按归属过滤出 unownedDialogs 供两处共用。

loadEditorProject 同样没传 timeoutMs,而 composeAbortSignal 缺失时不设默认值。
它是对账路径上的读取,挂住会让 catch 迟迟不结束,连带把占位拖过窗口——正是上面
那个忙等的助推。补 60 秒。

前置预算 40 秒过紧:该阶段会真的直传一张画布图层,几 MB 在较差网络下要几十秒,
用「无界」换「过紧」同样是回归。改为 90 秒,窗口同步 180→240,维持
90 + 120 = 210 < 240 并留 30 秒余量。三个常量构成的跨文件不等式新增用例钉住。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lhk229 added 1 commit 2026-08-03 19:43:07 +08:00
补齐 editorProjectClient 超时契约的断言
Project CI / Repository checks (pull_request) Successful in 55s
Project CI / Backend tests (pull_request) Successful in 3m35s
Project CI / Native shell tests (pull_request) Successful in 11m25s
Project CI / Frontend tests (pull_request) Successful in 2m29s
8d1bee9b1a
给 saveEditorProjectLayout 与 loadEditorProject 补超时后,两条精确参数断言失败:
toHaveBeenCalledWith 要求参数完全匹配,新增第四个参数即不匹配。

把 { timeoutMs: 60_000 } 写进断言,而不是放宽成 anything()。超时是契约的一部分
——一个被 flushProjectPersistence 同步等待在提交路径上、一个在对账路径上,没有
上界会把在途占位拖过存活窗口。写死之后谁删掉它测试就会红;放宽则等于让刚建立的
上界失去看守。去掉生产代码里的两个超时,两条断言同时变红。

真正的问题是验证方式:改的是 services 下的文件,却只跑了 components 的测试。
这条链路横跨两个目录,往后验证同时覆盖两处。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
kdletters requested changes 2026-08-03 19:54:03 +08:00
Dismissed
kdletters left a comment
Member

复审提交:8d1bee9b1a287346c577536bb0f6899eaafdcc64

请求更改:

[P2] 未知结果对账仍可能被素材库读取永久挂住

useImageCanvasGenerationWorkflow.ts:1978-1981Promise.all 同时等待项目快照与 refreshAssetLibraryloadEditorProject 已增加 60 秒超时,但 refreshAssetLibrary 调用的 loadEditorAssetLibraryuseImageCanvasAssetLibrary.ts:192-198editorProjectClient.ts:857-863)仍没有 timeout/signal。该 GET 不 settle 时,失败态更新、finally 中的 ownership 与同源图锁释放都无法执行;expiry hook 又会过滤 owned dialog,页面会永久停在 generating,同一源图也无法再次操作。请给整段 reconciliation 设置统一、有上界的 deadline/AbortSignal,确保任一读取或鉴权刷新挂住时仍能进入 finally

[P2] 90 秒预算只停止等待,没有取消已经启动的上传或布局保存

withPerfectPixelPrePostBudgetuseImageCanvasGenerationWorkflow.ts:237-258)只是 Promise.race。超时后 catch/finally 会释放 UI 锁并允许重试,但旧 Promise 仍继续执行。源图 fetch、上传 ticket、OSS POST、confirm 和重试等待均未接收同一 AbortSignal(useImageCanvasGenerationSubmissionWorkflow.ts:169-250editorMediaAssetUploadClient.ts:77-134editorRetryOptions.ts:52-75);每次重试又会生成新的 uploadId。于是旧上传可在 UI 已报超时后继续 confirm,用户重试会再创建另一份对象,留下迟到/孤儿写入;布局 flush 同样可能在调用方超时后继续。请把同一 deadline/AbortSignal 贯穿源素材读取、上传、confirm、重试等待和 flush/save,而不是仅在最外层停止 await。

已确认本次提交修复了 editorProjectClient 两条超时断言;定向测试 146/146、typecheck、git diff --check 通过。run 604 的 Frontend 失败发生在 npm ci ECONNRESET,属于网络安装失败,测试尚未执行,不作为本次代码 finding。

复审提交:8d1bee9b1a287346c577536bb0f6899eaafdcc64 请求更改: [P2] 未知结果对账仍可能被素材库读取永久挂住 `useImageCanvasGenerationWorkflow.ts:1978-1981` 用 `Promise.all` 同时等待项目快照与 `refreshAssetLibrary`。`loadEditorProject` 已增加 60 秒超时,但 `refreshAssetLibrary` 调用的 `loadEditorAssetLibrary`(`useImageCanvasAssetLibrary.ts:192-198`、`editorProjectClient.ts:857-863`)仍没有 timeout/signal。该 GET 不 settle 时,失败态更新、`finally` 中的 ownership 与同源图锁释放都无法执行;expiry hook 又会过滤 owned dialog,页面会永久停在 generating,同一源图也无法再次操作。请给整段 reconciliation 设置统一、有上界的 deadline/AbortSignal,确保任一读取或鉴权刷新挂住时仍能进入 `finally`。 [P2] 90 秒预算只停止等待,没有取消已经启动的上传或布局保存 `withPerfectPixelPrePostBudget`(`useImageCanvasGenerationWorkflow.ts:237-258`)只是 `Promise.race`。超时后 catch/finally 会释放 UI 锁并允许重试,但旧 Promise 仍继续执行。源图 fetch、上传 ticket、OSS POST、confirm 和重试等待均未接收同一 AbortSignal(`useImageCanvasGenerationSubmissionWorkflow.ts:169-250`、`editorMediaAssetUploadClient.ts:77-134`、`editorRetryOptions.ts:52-75`);每次重试又会生成新的 uploadId。于是旧上传可在 UI 已报超时后继续 confirm,用户重试会再创建另一份对象,留下迟到/孤儿写入;布局 flush 同样可能在调用方超时后继续。请把同一 deadline/AbortSignal 贯穿源素材读取、上传、confirm、重试等待和 flush/save,而不是仅在最外层停止 await。 已确认本次提交修复了 `editorProjectClient` 两条超时断言;定向测试 146/146、typecheck、`git diff --check` 通过。run 604 的 Frontend 失败发生在 `npm ci ECONNRESET`,属于网络安装失败,测试尚未执行,不作为本次代码 finding。
lhk229 added 1 commit 2026-08-03 20:11:10 +08:00
对账整段设界,并把提交前置预算变成真正的取消
Project CI / Repository checks (pull_request) Successful in 1m5s
Project CI / Frontend tests (pull_request) Successful in 3m9s
Project CI / Backend tests (pull_request) Successful in 4m4s
Project CI / Native shell tests (pull_request) Successful in 11m20s
89914b2255
对账用 Promise.all 同时等项目快照与 refreshAssetLibrary,而 loadEditorAssetLibrary
没有 timeoutMs。catch 也接不住永不 settle:await 不返回则 finally 永远不执行,
占位归属登记与源图层锁都释放不掉,而到期清理又豁免已登记的占位,页面永久停在
generating。这个洞是上一条留下的——给 loadEditorProject 加界时只覆盖了同一个
Promise.all 里两个 await 中的一个。

改为给整段对账设 75 秒上界,往里加新的 await 自动受约束。超时解析为 null 而不
拒绝:这段在 catch 内,抛出会穿出 async 函数变成未处理 rejection;null 则落进
既有的「快照读取失败」分支。

前置预算原先只是 Promise.race,超时后上传继续跑到 confirm 并注册对象。把同一个
AbortSignal 贯穿凭证、直传与 confirm 三步,由预算到期时 abort——只停其中一步会
留下半成品。未做重试等待与 flush 的贯穿:收益是少产生不可见的存储孤儿,而 flush
已有 60 秒上界。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Author
Owner

结论:当前 HEAD 89914b225 仍有 4 条生产问题(3×P2、1×P3)和 1 条测试问题。前三条 P2 涉及完美像素主链路的一致性,不建议当前状态合并。

1. [P2] 本地 timeout 无法取消已发送的 Spacetime procedure

  • 复现:OSS 持久化消耗了大部分 60 秒预算 → create-resource/create-asset procedure 已交给 Spacetime SDK → 外层 timeout_at 到期返回 504 → 前端立即 GET 到旧快照并解锁 → procedure 稍后才提交。
  • 后果:后续持久化步骤随 future 被丢弃而不再执行,留下 object/resource 等部分状态;用户重试会创建新的 task、object。
  • PR 归属:Spacetime SDK 不支持取消是旧基础设施,但本 PR 新增了“用可取消的本地 timeout 包裹不可取消远端 procedure”的组合。
  • 定性:真实的一致性及未知结果契约违反。

::code-comment{title="[P2] timeout 无法撤回已发送的 Spacetime procedure" body="持久化预算可能在 procedure 已交给 SDK 后到期;drop future 只停止本地等待,远端仍可稍后提交。前端此时会按旧快照判失败并解锁,留下 object、resource 等部分状态,重试再创建一份。这里需要远端有界或幂等操作及可延迟对账的 operation id,不能把 timeout_at 当作取消。" file="C:/projects/narrative/Genarrative/server-rs/crates/api-server/src/editor_project.rs" start=4967 end=4969 priority=2}

2. [P2] 素材库刷新会否决已经成功的项目对账

  • 复现:异常响应后,loadEditorProject 很快返回 idle + generatedLayerId 的成功快照,但 refreshAssetLibrary() 挂住;Promise.all 无法完成,75 秒后整体返回 null
  • 后果:成功项目快照被丢弃,本地占位被标记失败并解锁,用户仍可再次提交并产生重复产物。
  • PR 归属:整个完美像素对账分支由本 PR 新增。
  • 定性:权威项目成功被辅助刷新覆盖,属于未知结果收口契约违反。素材刷新应是 best-effort,不能参与项目结果判定。

::code-comment{title="[P2] 素材库刷新不能否决项目成功" body="Promise.all 要等两个分支;项目 GET 即使已经返回完成快照,只要无 timeout 的素材库刷新挂住,75 秒 deadline 就会返回 null 并丢弃真成功。应先独立使用项目快照判定和应用结果,素材库刷新只能异步 best-effort。" file="C:/projects/narrative/Genarrative/src/components/image-editor/useImageCanvasGenerationWorkflow.ts" start=2018 end=2022 priority=2}

3. [P2] POST 前换签超时会留下已 confirm 的孤儿对象

  • 复现:inline 源图经过 ticket → OSS PUT → confirm 成功后,继续请求本链路并不需要的 signed URL;换签没有收到 AbortSignal。让换签挂过 90 秒即可。
  • 后果:主 perfect-pixel POST 尚未发送,客户端不进入未知结果对账,但 asset object 已确认且没有项目资源或素材记录;重试产生第二个上传对象。
  • PR 归属:签名 helper 是旧的;本 PR 新增了 perfect-pixel 的 90 秒 pre-POST deadline,并把该 helper 接入这条只需要 objectKey 的路径,危险组合由本 PR 引入。
  • 定性:持久化完整性契约违反。仅补 signal 不能回滚已完成的 confirm;这里应直接使用 object-only 上传结果。

::code-comment{title="[P2] object-only 链路不应等待无取消的换签" body="完美像素源图准备最终只消费 objectKey,但这里在 PUT 和 confirm 完成后还请求 signed URL,并把 signal 写死为 undefined。换签超过 pre-POST 预算时 POST 根本没发送、对账不会触发,已 confirm 对象却成为不可见孤儿;重试再上传一份。该调用链应直接返回 object-only 上传结果。" file="C:/projects/narrative/Genarrative/src/services/image-editor/editorMediaAssetUploadClient.ts" start=164 end=170 priority=2}

4. [P3] “没有 dialog”不能证明素材已保存

  • 复现:请求执行期间,用户或另一标签页删除并保存占位;随后服务端在 PUT 后、账号素材落库前失败并返回 resultPersistenceStarted;对账项目自然找不到 dialog。
  • 后果:前端直接提示“完美像素结果已保存到素材库”,但 editor asset 可能根本不存在。
  • PR 归属:该三态判断和提示由本 PR 新增。
  • 定性:真实的结果状态/提示契约错误,但不直接破坏已有数据,P3。

::code-comment{title="[P3] 缺少 dialog 不能证明素材已落库" body="占位可能由用户先行删除,而服务端随后在 PUT、resource 或 asset 阶段失败;此时对账快照同样没有 dialog,却不保证账号素材存在。当前分支会虚假提示结果已保存到素材库,必须有 completion 成功或按 operation/task/object 查询到素材的正向证据。" file="C:/projects/narrative/Genarrative/src/components/image-editor/useImageCanvasGenerationWorkflow.ts" start=2033 end=2044 priority=3}

测试问题:[P2] 全局 Atomic 的相对断言仍会随机失败

两个默认并行运行的测试共享 EDITOR_PIXEL_ART_SNAP_QUEUE_DEPTH。另一测试可以在 before 与最终读取之间增减计数,因此相对断言仍不稳定。CI 使用默认并行 cargo test,应改用测试本地 Atomic 或显式串行化。

::code-comment{title="[P2] 相对读数仍会被并行测试污染" body="相邻测试会并行读写同一个进程级 Atomic;它可以在本测试的 before 与最终读取之间进入或释放 guard,因此相对断言仍会随机失败。请使用测试本地计数器或将共享全局状态的测试显式串行化。" file="C:/projects/narrative/Genarrative/server-rs/crates/api-server/src/editor_project.rs" start=12246 end=12259 priority=2}

已确认不再报告:原始“catch 直接失败、不对账”、永久 generating、占位已删时零反馈、resultPersistenceStarted 漏标、generationInputs 未清洗、assetKind 保存竞态、SpacetimeDB 前置预算遗漏、PixelArt prompt 文档问题均已修复。pending-save 覆盖属于 base 已修问题。

本轮三路独立复核,没有全量编译或测试;仅有前端定向 29 项通过。master 合并带来的 ESLint 错误已完全排除。

结论:当前 HEAD `89914b225` 仍有 4 条生产问题(3×P2、1×P3)和 1 条测试问题。前三条 P2 涉及完美像素主链路的一致性,不建议当前状态合并。 ### 1. [P2] 本地 timeout 无法取消已发送的 Spacetime procedure - 复现:OSS 持久化消耗了大部分 60 秒预算 → create-resource/create-asset procedure 已交给 Spacetime SDK → 外层 `timeout_at` 到期返回 504 → 前端立即 GET 到旧快照并解锁 → procedure 稍后才提交。 - 后果:后续持久化步骤随 future 被丢弃而不再执行,留下 object/resource 等部分状态;用户重试会创建新的 task、object。 - PR 归属:Spacetime SDK 不支持取消是旧基础设施,但本 PR 新增了“用可取消的本地 timeout 包裹不可取消远端 procedure”的组合。 - 定性:真实的一致性及未知结果契约违反。 ::code-comment{title="[P2] timeout 无法撤回已发送的 Spacetime procedure" body="持久化预算可能在 procedure 已交给 SDK 后到期;drop future 只停止本地等待,远端仍可稍后提交。前端此时会按旧快照判失败并解锁,留下 object、resource 等部分状态,重试再创建一份。这里需要远端有界或幂等操作及可延迟对账的 operation id,不能把 timeout_at 当作取消。" file="C:/projects/narrative/Genarrative/server-rs/crates/api-server/src/editor_project.rs" start=4967 end=4969 priority=2} ### 2. [P2] 素材库刷新会否决已经成功的项目对账 - 复现:异常响应后,`loadEditorProject` 很快返回 `idle + generatedLayerId` 的成功快照,但 `refreshAssetLibrary()` 挂住;`Promise.all` 无法完成,75 秒后整体返回 `null`。 - 后果:成功项目快照被丢弃,本地占位被标记失败并解锁,用户仍可再次提交并产生重复产物。 - PR 归属:整个完美像素对账分支由本 PR 新增。 - 定性:权威项目成功被辅助刷新覆盖,属于未知结果收口契约违反。素材刷新应是 best-effort,不能参与项目结果判定。 ::code-comment{title="[P2] 素材库刷新不能否决项目成功" body="Promise.all 要等两个分支;项目 GET 即使已经返回完成快照,只要无 timeout 的素材库刷新挂住,75 秒 deadline 就会返回 null 并丢弃真成功。应先独立使用项目快照判定和应用结果,素材库刷新只能异步 best-effort。" file="C:/projects/narrative/Genarrative/src/components/image-editor/useImageCanvasGenerationWorkflow.ts" start=2018 end=2022 priority=2} ### 3. [P2] POST 前换签超时会留下已 confirm 的孤儿对象 - 复现:inline 源图经过 ticket → OSS PUT → confirm 成功后,继续请求本链路并不需要的 signed URL;换签没有收到 AbortSignal。让换签挂过 90 秒即可。 - 后果:主 perfect-pixel POST 尚未发送,客户端不进入未知结果对账,但 asset object 已确认且没有项目资源或素材记录;重试产生第二个上传对象。 - PR 归属:签名 helper 是旧的;本 PR 新增了 perfect-pixel 的 90 秒 pre-POST deadline,并把该 helper 接入这条只需要 `objectKey` 的路径,危险组合由本 PR 引入。 - 定性:持久化完整性契约违反。仅补 signal 不能回滚已完成的 confirm;这里应直接使用 object-only 上传结果。 ::code-comment{title="[P2] object-only 链路不应等待无取消的换签" body="完美像素源图准备最终只消费 objectKey,但这里在 PUT 和 confirm 完成后还请求 signed URL,并把 signal 写死为 undefined。换签超过 pre-POST 预算时 POST 根本没发送、对账不会触发,已 confirm 对象却成为不可见孤儿;重试再上传一份。该调用链应直接返回 object-only 上传结果。" file="C:/projects/narrative/Genarrative/src/services/image-editor/editorMediaAssetUploadClient.ts" start=164 end=170 priority=2} ### 4. [P3] “没有 dialog”不能证明素材已保存 - 复现:请求执行期间,用户或另一标签页删除并保存占位;随后服务端在 PUT 后、账号素材落库前失败并返回 `resultPersistenceStarted`;对账项目自然找不到 dialog。 - 后果:前端直接提示“完美像素结果已保存到素材库”,但 editor asset 可能根本不存在。 - PR 归属:该三态判断和提示由本 PR 新增。 - 定性:真实的结果状态/提示契约错误,但不直接破坏已有数据,P3。 ::code-comment{title="[P3] 缺少 dialog 不能证明素材已落库" body="占位可能由用户先行删除,而服务端随后在 PUT、resource 或 asset 阶段失败;此时对账快照同样没有 dialog,却不保证账号素材存在。当前分支会虚假提示结果已保存到素材库,必须有 completion 成功或按 operation/task/object 查询到素材的正向证据。" file="C:/projects/narrative/Genarrative/src/components/image-editor/useImageCanvasGenerationWorkflow.ts" start=2033 end=2044 priority=3} ### 测试问题:[P2] 全局 Atomic 的相对断言仍会随机失败 两个默认并行运行的测试共享 `EDITOR_PIXEL_ART_SNAP_QUEUE_DEPTH`。另一测试可以在 `before` 与最终读取之间增减计数,因此相对断言仍不稳定。CI 使用默认并行 `cargo test`,应改用测试本地 Atomic 或显式串行化。 ::code-comment{title="[P2] 相对读数仍会被并行测试污染" body="相邻测试会并行读写同一个进程级 Atomic;它可以在本测试的 before 与最终读取之间进入或释放 guard,因此相对断言仍会随机失败。请使用测试本地计数器或将共享全局状态的测试显式串行化。" file="C:/projects/narrative/Genarrative/server-rs/crates/api-server/src/editor_project.rs" start=12246 end=12259 priority=2} 已确认不再报告:原始“catch 直接失败、不对账”、永久 `generating`、占位已删时零反馈、`resultPersistenceStarted` 漏标、generationInputs 未清洗、assetKind 保存竞态、SpacetimeDB 前置预算遗漏、PixelArt prompt 文档问题均已修复。pending-save 覆盖属于 base 已修问题。 本轮三路独立复核,没有全量编译或测试;仅有前端定向 29 项通过。master 合并带来的 ESLint 错误已完全排除。
lhk229 added 1 commit 2026-08-04 10:32:06 +08:00
修复完美像素结果持久化与未知状态收口
Project CI / Frontend tests (pull_request) Failing after 22s
Project CI / Repository checks (pull_request) Failing after 48s
Project CI / Backend tests (pull_request) Successful in 3m49s
Project CI / Native shell tests (pull_request) Successful in 11m39s
7f13af6135
为完美像素操作引入稳定标识、请求指纹和原子持久化 procedure,支持精确重放并拒绝冲突。

在首次对象存储写入后完整标记 resultPersistenceStarted,统一未知结果的 GET 对账语义。

前端持久化 perfectPixelOperation,提交前严格刷写布局,并支持 pending-confirmation、刷新恢复与原样重试。

隔离跨用户与跨项目回包,修正占位清理、动画状态和异常对账时间窗。

同步生成 SpacetimeDB 绑定,补充前后端回归测试,并更新技术文档与共享决策记录。
lhk229 added 2 commits 2026-08-04 12:36:30 +08:00
改为 object-only 源图上传并贯穿取消信号
以项目 GET 正向证据判定画布成功与 asset-only
持久化并原样重放 operation 请求,阻止未收口身份被删除
统一 75 秒绝对窗口并兼容旧 240 秒 journal
补齐时钟偏差、删除保护与 byte-for-byte 回归测试
同步更新图片画布契约与共享决策记录
修正完美像素并发闸测试竞态
Project CI / Frontend tests (pull_request) Failing after 20s
Project CI / Repository checks (pull_request) Successful in 57s
Project CI / Backend tests (pull_request) Successful in 3m37s
Project CI / Native shell tests (pull_request) Successful in 11m0s
193c17d6ed
删除过期预算测试对进程级队列 Atomic 的 before/after 断言

保留独立 Drop 覆盖并仅断言预算耗尽返回 504

同步更新完美像素、SpacetimeDB 取消边界与共享项目记忆文档
lhk229 added 2 commits 2026-08-04 13:47:08 +08:00
引入可观察的占位归属注册表,让归属释放可靠触发到期清理。

为完美像素首次提交和人工重试路径配对登记与释放,并统一恢复查询。

补充固定输入与 StrictMode 覆盖,移除依赖 rerender 的假触发。

同步图片画布技术方案与项目决策记录。
修复重复生成占位的对账判定
Project CI / Frontend tests (pull_request) Failing after 20s
Project CI / Repository checks (pull_request) Successful in 1m8s
Project CI / Backend tests (pull_request) Successful in 3m53s
Project CI / Native shell tests (pull_request) Successful in 10m53s
06d8fbb9c2
收集项目快照中全部同 ID 的生成占位原始记录。

通用队列链在任一重复占位未收口时执行第二次项目读取。

完美像素遇到重复操作占位时失败关闭为冲突。

补充两条缺陷定向测试并同步技术方案与决策记录。
lhk229 added 1 commit 2026-08-04 14:54:17 +08:00
清理完美像素链路的死代码
Project CI / Frontend tests (pull_request) Failing after 1m43s
Project CI / Repository checks (pull_request) Successful in 1m16s
Project CI / Backend tests (pull_request) Successful in 4m2s
Project CI / Native shell tests (pull_request) Successful in 11m41s
3bea6441a8
- 删除只有测试调用的 snap_pixel_art_strict 与 snap_pixel_art 两个无 deadline
  包装,公开入口收敛为 snap_pixel_art_with_deadline 与
  snap_pixel_art_strict_with_deadline;原本只写在 snap_pixel_art 头上的双输入
  尺寸与输出契约合并进保留者的文档,调用点改为显式传 None。
- 去掉持久化结果里从不被读取的 status 透传(客户端枚举与 record 字段)。该段
  原本搭载了「status 缺失即拒绝」的校验,保留为显式 is_none 拦截,并补一个
  此前缺失的回归用例锁住它。
- 快速编辑面板不再对 dialog 状态做 pending-confirmation 的运行时收窄:dialog
  由面板派生,写回方只做 failed→idle 重置,该分支恒不成立,直接取面板状态。
- 收回三个无人导入的 export(reconcilePerfectPixelProject、
  createPerfectPixelReconciliationOperation、
  INVALID_PERFECT_PIXEL_OPERATION_ERROR_MESSAGE)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Member

[P2] 75 秒对账仍不是真正有界。loadEditorProject 的 timeout 只覆盖业务 fetch 等响应头;登录刷新在 timeout 建立前无界等待,response.text() 又发生在 timeout 清理后。任一环节挂住,reconcilePerfectPixelProject 无法重新检查 deadline,首次操作的图层锁和 ownership 也无法释放。
[P2] 源图上传成功后,如果提交前布局 flush 耗尽 90 秒预算,已有 perfectPixelOperation 会被标成 failed 并解锁;同 identity 重试只接受 pending-confirmation,用户再次点击只能创建新 operation、重新上传。旧 confirmed source object 会成为孤儿,原布局 PATCH 还可能迟到。
CI 阻断:Frontend job 1747 为 2234/2235。ImageCanvasEditorGenerationIntegration.test.tsx:75-116 切换到 object-only 上传后仍只配置旧 uploader mock;本地复现 42/43。

[P2] 75 秒对账仍不是真正有界。loadEditorProject 的 timeout 只覆盖业务 fetch 等响应头;登录刷新在 timeout 建立前无界等待,response.text() 又发生在 timeout 清理后。任一环节挂住,reconcilePerfectPixelProject 无法重新检查 deadline,首次操作的图层锁和 ownership 也无法释放。 [P2] 源图上传成功后,如果提交前布局 flush 耗尽 90 秒预算,已有 perfectPixelOperation 会被标成 failed 并解锁;同 identity 重试只接受 pending-confirmation,用户再次点击只能创建新 operation、重新上传。旧 confirmed source object 会成为孤儿,原布局 PATCH 还可能迟到。 CI 阻断:[Frontend job 1747](http://192.168.35.82/git/GenarrativeAI/Genarrative/actions/runs/615/jobs/1747) 为 2234/2235。ImageCanvasEditorGenerationIntegration.test.tsx:75-116 切换到 object-only 上传后仍只配置旧 uploader mock;本地复现 42/43。
lhk229 added 1 commit 2026-08-04 17:06:59 +08:00
修复 UI 素材提取集成测试的过期上传 mock
Project CI / Backend tests (pull_request) Successful in 4m32s
Project CI / Repository checks (pull_request) Successful in 51s
Project CI / Native shell tests (pull_request) Successful in 12m18s
Project CI / Frontend tests (pull_request) Successful in 2m38s
f88b633146
inline 源图上传改走 object-only 版本后,该文件的
uploadEditorMediaAssetObjectFile 仍是裸 vi.fn(),返回 undefined,
取 objectKey 抛的 TypeError 被 extractUiDesignAssets 的 catch
吞成 window.alert,最终只表现为提取接口零调用。

补上配置好的 mock,并把两个 mock 的返回补齐成真实返回形状。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lhk229 added 1 commit 2026-08-04 17:31:10 +08:00
修复角色动画序列帧计数的定时器竞态
Project CI / Repository checks (pull_request) Failing after 13s
Project CI / Backend tests (pull_request) Failing after 13s
Project CI / Frontend tests (pull_request) Successful in 3m13s
Project CI / Native shell tests (pull_request) Successful in 11m55s
d62aa3a7cb
序列帧图层在 frames.length > 1 时默认自动播放,48 帧 6 秒算出
125ms 的真实 setInterval。该用例不用假定时器,上一条 waitFor 在
CI 上多轮询一次就越过 125ms,计数器已经跳到 2/48。

改为只钉分母。注入 400ms 延迟可稳定复现原断言的失败,并验证
新断言在同一延迟下通过。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lhk229 added 1 commit 2026-08-04 17:37:03 +08:00
Merge remote-tracking branch 'web/master' into feat/pixel_art2
Project CI / Frontend tests (pull_request) Successful in 2m55s
Project CI / Backend tests (pull_request) Successful in 3m48s
Project CI / Repository checks (pull_request) Successful in 57s
Project CI / Native shell tests (pull_request) Successful in 11m15s
88087455d6
lhk229 added 5 commits 2026-08-04 20:12:51 +08:00
为 requestJson 增加覆盖鉴权、重试和响应体读取的可选绝对 deadline。

让完美像素项目对账按 75 秒窗口和单次 10 秒上界读取权威快照。

补充鉴权等待、响应体读取、客户端透传和对账边界的定向测试与文档。
拆分源准备与严格布局保存预算,并让布局请求覆盖完整生命周期。

严格保存失败时保留原操作身份,阻止重新上传和创建第二条操作。

补充原样重试、迟到保存和删除保护的定向测试。

同步更新图片画布技术方案与项目决策记录。
说明 object-only 记录的存储形态、可见性与实际影响。

明确本 PR 不扩展到上传账本、孤儿回收或历史清理。
让 generation 占位右键删除在历史和选择副作用前进入统一请求入口。

保留未收口完美像素身份并恢复普通生成中的删除确认。

补充层命令定向测试并同步技术方案与决策记录。
完美像素结果上传前增加只读预检
Project CI / Repository checks (pull_request) Failing after 10s
Project CI / Backend tests (pull_request) Failing after 10s
Project CI / Frontend tests (pull_request) Successful in 2m46s
Project CI / Native shell tests (pull_request) Successful in 14m17s
f29c7d5918
拆分最终 PNG 的纯 prepare 与 OSS 执行阶段。

新增 SpacetimeDB 只读 preflight procedure,并在最终原子事务重复目录、布局、幂等和 revision 校验。

让 preflight、PUT、HEAD 与 persist 共享 60 秒绝对截止,并保持首个 PUT 前不标 unknown。

生成 Rust bindings,补充定向守卫与 TOCTOU 边界文档。
lhk229 added 1 commit 2026-08-04 20:50:26 +08:00
修复完美像素未收口占位交互
Project CI / Repository checks (pull_request) Failing after 11s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / Frontend tests (pull_request) Failing after 18s
Project CI / Native shell tests (pull_request) Successful in 10m29s
ccf8307a39
发现已有未收口操作时激活原占位并阻止创建新身份。

拒绝右键删除受保护占位时保留当前图层选择。

补齐生成工作流接线和定向回归测试。
lhk229 added 1 commit 2026-08-04 21:00:01 +08:00
合并最新 master 到完美像素分支
Project CI / Native shell tests (pull_request) Failing after 8m19s
Project CI / Repository checks (pull_request) Failing after 53s
Project CI / Frontend tests (pull_request) Successful in 3m5s
Project CI / Backend tests (pull_request) Successful in 3m36s
9ab232927c
纳入 master 的素材类型双层模型与 Agent Runtime 更新。

保留完美像素 operation、unknown 对账、占位保护与上传预检契约。

解决画布模型、工作流、测试及文档冲突并通过定向门禁。
lhk229 added 1 commit 2026-08-04 21:46:31 +08:00
合并远程 master 最新更新
Project CI / Frontend tests (pull_request) Successful in 3m6s
Project CI / Repository checks (pull_request) Successful in 58s
Project CI / Backend tests (pull_request) Successful in 4m2s
Project CI / Native shell tests (pull_request) Failing after 9m18s
a8407e718d
纳入游戏创作生成任务恢复与运行进度改进。

纳入 master 现有 CI 基线修复。

保持完美像素分支现有行为并通过定向静态门禁。
lhk229 marked the pull request as work in progress 2026-08-04 22:01:52 +08:00
Author
Owner

存在功能性bug

存在功能性bug
lhk229 added 4 commits 2026-08-05 13:58:42 +08:00
读边界脱敏的 model 与 provider 不再由客户端回写,结构化保存改以资源行为准

读边界按图层声明的 resourceId 回填 sourceType,hydrate 缺键时回落资源值

补齐前后端定向测试覆盖两条往返不变式,并记录读写对称不变式

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
低层删除不再对未收口 operation 抗命,随源图层与多选删除同等对待

删除确认判据收敛为 requiresGenerationDeleteConfirmation,免费的完美像素不再弹泥点确认

标记 operation 失效时一并丢弃快照,消除重试消失但占位仍在的矛盾态

翻转钉死删除保护的用例,改为覆盖删除后结果仍落库并走 asset-only 提示

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
只对已登记项目资源的图层停发 model 与 provider

缺资源行的自包含本地图片序列继续携带,避免角色动画层元数据永久丢失

补测 module 读回确实丢弃 sourceType 键,以及显式冲突的 sourceType 仍失败关闭

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
完美像素请求账本移出项目布局
Project CI / Repository checks (pull_request) Failing after 11s
Project CI / Backend tests (pull_request) Failing after 10s
Project CI / Frontend tests (pull_request) Failing after 20s
Project CI / Native shell tests (pull_request) Failing after 8m36s
2fa2f245c1
账本记的是「本机发出过哪一次 POST」,属于对账凭据而非画布内容。它此前写在
用户的画布布局里,为此派生出一条严格布局保存通道:发 POST 前必须拿到布局
保存的 revision ack。任何布局校验失败因此都会升级成完美像素的硬阻断。

改为存在本机 localStorage,按 owner + project 双键隔离;布局里只留
perfectPixelOperationId 标记,用来把这类占位与队列型占位区分开。本机写入是
同步的、不过网络、不受服务端校验影响,因此它能提供严格保存想提供的那个保证
——请求可被追溯——却不引入阻断点。严格保存通道整体删除,只保留一个不改变
失败语义的 preferLatestGenerationDialogs。

由此新出现的「账本有、占位没写进布局」窗口,由恢复 effect 覆盖:它同时遍历
内存占位与孤儿账本条目,对后者照常 GET 对账,终态给出 asset-only 提示并清
账本。

本机账本是明确设计,缺失只降级、不得构成阻断:换设备、清缓存、隐私模式、
配额写满都会读不到账本,此时带标记的占位一律收口成可删除的失败占位,用户
删掉重来即可。跨设备不再自动收口是已知且接受的代价。

布局内联账本作为 legacy 形状继续被读取,滚动部署期间的在途操作不会被一次性
判死;写入侧不再产生新的内联账本。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Author
Owner

结论:近四个提交仍有 5 条 P2、2 条 P3。均有可达链路且由这些提交引入;未计死代码。

::code-comment{title="[P2] 删除占位后可创建第二个 operation" body="这里无条件删除 dialog,但普通完美像素入口的 existingOperation 防重只扫描 canvasGenerationDialogs。旧 POST 超时并完成 unknown 对账、释放图层锁后,同一源图再次点击会生成新的 dialog、task 和 operation;旧服务端操作仍可能迟到落库,两个 task 无法被幂等合并,最终产生重复 resource/asset。这也直接违反本提交 decision-log 中“从源图重新发起仍被 existingOperation 闸拦住”的声明。删除 UI 占位可以保留,但未收口 identity 必须由独立账本参与防重。" file="C:/projects/narrative/Genarrative/src/components/image-editor/useCanvasGenerationDialogs.ts" start=233 end=240 priority=2}

::code-comment{title="[P2] 过期孤儿账本被永久跳过" body="孤儿账本一旦超过 reconcileUntil 就在这里直接 continue,因而不会进入 reconcilePerfectPixelProject。真实链路是布局尽力保存失败但 POST 已发送,浏览器关闭,75 秒后重新打开;此时只有 localStorage 账本而没有 dialog,本分支既不执行契约要求的唯一一次 10 秒即时 GET,也不提示或清账。用户随后可建立第二个 identity,而首个结果可能已经落库。应允许过期孤儿至少进入一次即时 GET。" file="C:/projects/narrative/Genarrative/src/components/image-editor/useImageCanvasGenerationWorkflow.ts" start=2776 end=2787 priority=2}

::code-comment{title="[P2] 尽力保存仍可把 POST 阻塞数分钟" body="flushProjectPersistence 虽被重新定义为不阻断生成的尽力保存,实际仍循环等待整个布局保存队列并等待封面保存。布局 PATCH 单次有 60 秒 deadline,transport failure 又最多重试三次,因此这里可在 POST 前等待约四分钟,封面生成、上传和资源登记还会继续增加时间;operation 的 75 秒对账窗口却已在 flush 前开始。结果是窗口可能在 POST 发出前耗尽,与本提交“不会引入阻断点”的契约相反。" file="C:/projects/narrative/Genarrative/src/components/image-editor/useImageCanvasProjectPersistence.ts" start=1018 end=1036 priority=2}

::code-comment{title="[P2] legacy operation 未迁移到本机账本" body="hydrate 仍接受旧布局中的内联 perfectPixelOperation,但没有任何迁移路径调用 savePerfectPixelOperation;第一次布局保存却在这里删除完整快照,只留下 operationId。升级期间的在途 operation 因此会在下一次刷新变成 failed + invalid,永久失去 exact-retry identity,并可能在重做时重复生成。应在首次剥离前把合法 legacy operation 写入本机账本,或在确认迁移完成前继续保留内联值。" file="C:/projects/narrative/Genarrative/src/components/image-editor/ImageCanvasEditorModel.ts" start=734 end=742 priority=2}

::code-comment{title="[P2] 正常成功结果会被重新 hydrate 成失败" body="applied 终态在这里先清本机账本,但服务端完成布局仍保留 perfectPixelOperationId,只把 dialog 改为 idle 并写 generatedLayerId。紧接着 applyProjectSnapshot 读取不到刚清除的账本,hydrate 便把 marker 存在但 ledger 缺失解释为 failed + perfectPixelOperationInvalid。结果图层已正常落画布,面板却显示“操作快照无效”。终态完成时应移除 marker,或 hydrate 对 idle + generatedLayerId 不再要求账本。" file="C:/projects/narrative/Genarrative/src/components/image-editor/useImageCanvasGenerationWorkflow.ts" start=2148 end=2154 priority=2}

::code-comment{title="[P3] Undo 会复活不再对账的 generating 占位" body="删除前会记录包含完整 operation 的 delete-generation-result 快照,而该历史动作允许 Undo。原请求结束后 Undo 可恢复 generating dialog,但 observedPerfectPixelRecoveryKeysRef 仍保留首次请求的 key,恢复 effect 因此不再 GET,当前会话中占位会一直处理中。删除此类 dialog 时应让历史恢复重新开放一次观察,或避免把未收口 operation 放进可撤销删除历史。" file="C:/projects/narrative/Genarrative/src/components/image-editor/ImageCanvasEditorView.tsx" start=1700 end=1703 priority=3}

::code-comment{title="[P3] 权威专题文档仍保留相反契约" body="近两个提交改成允许删除未收口 operation、POST 前布局保存仅尽力而为,但这里仍要求 unknown operation 不可删除且必须保留 identity;同文件后续仍沿用 POST 前 strict revision ACK 的旧语义。提交只追加 decision-log,没有同步其声明关联的权威专题文档,导致实现与验收依据相互矛盾。应随设计翻转同步更新这些条款。" file="C:/projects/narrative/Genarrative/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md" start=57 end=58 priority=3}

补充核验:

  • 291dd75a8 的自包含序列 model/provider 丢失已由 06a6c33ea 修复,当前 HEAD 不成立。
  • 最小运行复现确认:
    • idle + generatedLayerId + marker 会 hydrate 为 failed + invalid
    • legacy 内联 operation 经一次序列化、再无本机账本 hydrate 后会变成 failed + invalid
  • 未运行全量编译或测试,工作区保持干净。
结论:近四个提交仍有 5 条 P2、2 条 P3。均有可达链路且由这些提交引入;未计死代码。 ::code-comment{title="[P2] 删除占位后可创建第二个 operation" body="这里无条件删除 dialog,但普通完美像素入口的 existingOperation 防重只扫描 canvasGenerationDialogs。旧 POST 超时并完成 unknown 对账、释放图层锁后,同一源图再次点击会生成新的 dialog、task 和 operation;旧服务端操作仍可能迟到落库,两个 task 无法被幂等合并,最终产生重复 resource/asset。这也直接违反本提交 decision-log 中“从源图重新发起仍被 existingOperation 闸拦住”的声明。删除 UI 占位可以保留,但未收口 identity 必须由独立账本参与防重。" file="C:/projects/narrative/Genarrative/src/components/image-editor/useCanvasGenerationDialogs.ts" start=233 end=240 priority=2} ::code-comment{title="[P2] 过期孤儿账本被永久跳过" body="孤儿账本一旦超过 reconcileUntil 就在这里直接 continue,因而不会进入 reconcilePerfectPixelProject。真实链路是布局尽力保存失败但 POST 已发送,浏览器关闭,75 秒后重新打开;此时只有 localStorage 账本而没有 dialog,本分支既不执行契约要求的唯一一次 10 秒即时 GET,也不提示或清账。用户随后可建立第二个 identity,而首个结果可能已经落库。应允许过期孤儿至少进入一次即时 GET。" file="C:/projects/narrative/Genarrative/src/components/image-editor/useImageCanvasGenerationWorkflow.ts" start=2776 end=2787 priority=2} ::code-comment{title="[P2] 尽力保存仍可把 POST 阻塞数分钟" body="flushProjectPersistence 虽被重新定义为不阻断生成的尽力保存,实际仍循环等待整个布局保存队列并等待封面保存。布局 PATCH 单次有 60 秒 deadline,transport failure 又最多重试三次,因此这里可在 POST 前等待约四分钟,封面生成、上传和资源登记还会继续增加时间;operation 的 75 秒对账窗口却已在 flush 前开始。结果是窗口可能在 POST 发出前耗尽,与本提交“不会引入阻断点”的契约相反。" file="C:/projects/narrative/Genarrative/src/components/image-editor/useImageCanvasProjectPersistence.ts" start=1018 end=1036 priority=2} ::code-comment{title="[P2] legacy operation 未迁移到本机账本" body="hydrate 仍接受旧布局中的内联 perfectPixelOperation,但没有任何迁移路径调用 savePerfectPixelOperation;第一次布局保存却在这里删除完整快照,只留下 operationId。升级期间的在途 operation 因此会在下一次刷新变成 failed + invalid,永久失去 exact-retry identity,并可能在重做时重复生成。应在首次剥离前把合法 legacy operation 写入本机账本,或在确认迁移完成前继续保留内联值。" file="C:/projects/narrative/Genarrative/src/components/image-editor/ImageCanvasEditorModel.ts" start=734 end=742 priority=2} ::code-comment{title="[P2] 正常成功结果会被重新 hydrate 成失败" body="applied 终态在这里先清本机账本,但服务端完成布局仍保留 perfectPixelOperationId,只把 dialog 改为 idle 并写 generatedLayerId。紧接着 applyProjectSnapshot 读取不到刚清除的账本,hydrate 便把 marker 存在但 ledger 缺失解释为 failed + perfectPixelOperationInvalid。结果图层已正常落画布,面板却显示“操作快照无效”。终态完成时应移除 marker,或 hydrate 对 idle + generatedLayerId 不再要求账本。" file="C:/projects/narrative/Genarrative/src/components/image-editor/useImageCanvasGenerationWorkflow.ts" start=2148 end=2154 priority=2} ::code-comment{title="[P3] Undo 会复活不再对账的 generating 占位" body="删除前会记录包含完整 operation 的 delete-generation-result 快照,而该历史动作允许 Undo。原请求结束后 Undo 可恢复 generating dialog,但 observedPerfectPixelRecoveryKeysRef 仍保留首次请求的 key,恢复 effect 因此不再 GET,当前会话中占位会一直处理中。删除此类 dialog 时应让历史恢复重新开放一次观察,或避免把未收口 operation 放进可撤销删除历史。" file="C:/projects/narrative/Genarrative/src/components/image-editor/ImageCanvasEditorView.tsx" start=1700 end=1703 priority=3} ::code-comment{title="[P3] 权威专题文档仍保留相反契约" body="近两个提交改成允许删除未收口 operation、POST 前布局保存仅尽力而为,但这里仍要求 unknown operation 不可删除且必须保留 identity;同文件后续仍沿用 POST 前 strict revision ACK 的旧语义。提交只追加 decision-log,没有同步其声明关联的权威专题文档,导致实现与验收依据相互矛盾。应随设计翻转同步更新这些条款。" file="C:/projects/narrative/Genarrative/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md" start=57 end=58 priority=3} 补充核验: - `291dd75a8` 的自包含序列 model/provider 丢失已由 `06a6c33ea` 修复,当前 HEAD 不成立。 - 最小运行复现确认: - `idle + generatedLayerId + marker` 会 hydrate 为 `failed + invalid`。 - legacy 内联 operation 经一次序列化、再无本机账本 hydrate 后会变成 `failed + invalid`。 - 未运行全量编译或测试,工作区保持干净。
Author
Owner

注意:完美像素是低成本操作,所有“因意外丢失已生成资源”的低优先级问题不应构成阻断,只有主链路存在问题才阻断。同时也不应该新增加“禁止用户删除”、“禁止用户重试”、“禁止一张图处理两遍”等特性

注意:完美像素是低成本操作,所有“因意外丢失已生成资源”的低优先级问题不应构成阻断,只有主链路存在问题才阻断。同时也不应该新增加“禁止用户删除”、“禁止用户重试”、“禁止一张图处理两遍”等特性
lhk229 added 6 commits 2026-08-05 17:43:11 +08:00
阶段 3 引入的判据「布局里有 perfectPixelOperationId、本机没有账本 ⇒ 无效」
按构造就是错的:标记写进布局后寿命无限(服务端完成 completion 只做字段级
改写,从不摘标记),账本寿命却很短(收口即清、75 秒对账窗口、7 天保留期、
换设备即无)。账本消失是正常终态,不是异常。

同一个不对称造成两个缺陷:

一、每一次成功都被判成失败。settle 时先清账本、紧接着用真实 hydrate 重新
套用权威快照,成功结果被判成 failed + perfectPixelOperationInvalid;该状态
还会被下一次自动保存写回服务端,覆盖正确的 idle,且标记一旦落库就能单独
强制 failed,自我固化。历史上早已完成的占位在标记化改写后同样中招。

二、孤儿账本对账在唯一目标场景下失效。它的场景是「关掉浏览器、稍后重开」,
重开必然晚于 75 秒,而分支按 reconcileUntil 短路,等于永不生效,条目还会在
本机躺满保留期反复被跳过。

修复:凡是「缺账本 ⇒ 无效」的判据一律先排除收口态(复用既有的
isUnresolvedCanvasGenerationDialogRecord 取反)。收口态无条件忽略已落库的
无效标记与残留错误文案、强制归位 idle,让被写脏的行自愈;写边界同步收窄,
收口后不再输出标记,让标记寿命与账本对齐。

孤儿不按 reconcileUntil 短路,改走新增的 readPerfectPixelOrphanVerdict——
一次确定性的读,不轮询。dialog-missing 提示结果只进素材库;applied 静默
刷新(本次读到的就是权威状态,结果本就在眼前);未落库完全静默。三者都清
账本,读失败不清。

测试缺口的根因:工作流里所有 applied 用例的 applyProjectSnapshot 都是空桩,
跨 hook 协作从未被跑过;共享 fixture 也没还原服务端真实保留的标记。补了一条
把 verdict.project 真正喂进 splitCanvasLayoutItems 的集成用例,回退修复后它
报 expected 'failed' to be 'idle'。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
查服务端确认了一件推翻先前定性的事实:validate_editor_pixel_art_snap_
placeholder_exists 在处理前检查占位是否已经持久化,既没有同 ID 的已持久化
dialog、也没有同 operation 的稳定 resource 时直接返回 409。所以 POST 前那次
await flush 不是可省的画布同步,而是服务端硬前置,不能取消。

据此更正上一条决策里的错误声明:不是「布局保存失败只降级成 asset-only」,
而是「占位从未持久化时服务端返回 409;best-effort flush 不再提供成功 ACK,
客户端因此无法证明该前置已满足,只能提高满足它的概率」。被解除的是客户端侧
「拿不到 revision ack 就拒发」的阻断,不是端到端依赖。

缺陷:首次提交与人工 exact retry 都在这次 flush 之前就算好 submittedAt /
reconcileUntil。该 flush 没有整体上限,慢保存足以在 POST 发出前烧光整个
75 秒窗口,请求带着已过期的 deadline 发出,对账退化成读一次就收尾。

修复:flush 返回、authority 复核通过之后才用
createPerfectPixelReconciliationOperation 重新锚定,按同一 operationId
覆盖账本与 dialog,登记新的 recovery key,随后立即 POST。只覆盖时间字段,
request 与 dialog / operation / task identity 逐字节不变,不产生第二条账本。

保留 flush 前的预写而不是整体后移:flush 期间另一标签页可能加载同一项目,
服务端已有带 marker 的占位而 localStorage 跨标签共享,本机若没有账本那条占位
会被 hydrate 成 failed + invalid。provisional 账本正好堵住这个窗口。

同步修正专题文档中已被近几个提交推翻的条款:strict layout save 预算与
revision ACK 前 POST 为零、未收口 operation 不可删除、durable operation
不得被普通删除路径清理。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
近五个提交连续翻转了多条前端契约,但只追加了 decision-log。专题文档是这些
条目自己声明的「关联文档」,其中仍写着旧契约,按它做验收会把当前正确行为
判成缺陷。

更正:请求快照写入占位并 flush 布局 → 账本写本机、布局只留 marker;strict
layout save 预算与 revision ACK 前 POST 为零 → 通道已删除,改 best-effort,
并写明服务端要求占位此前已持久化否则 409、客户端无法证明只能提高概率;
窗口从快照写入起算 → 从 POST 发出时刻起算;POST 前必须取得保存成功确认
→ 保存失败不再让 POST 为零;未收口 operation 不可删除、durable operation
不得被普通删除路径清理 → 任何状态可删且不弹确认。

新增不变式:标记与账本寿命必须对齐,收口态不写标记也不判无效;账本读不到
时只有未收口占位收口成可删除失败态;恢复必须覆盖孤儿账本,孤儿走一次确定性
的读、不按 reconcileUntil 短路。

「普通按钮不得创建第二个 operation」保留为已知缺口而非静默删除:该保证只由
existingOperation 闸提供,而它只扫内存 dialog 列表,占位可删之后删掉再从源图
发起会产生第二个 identity。文档如实描述现状并标注闭合方向,代码侧未修。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
账本移出布局后,hydrate 仍认布局里的 legacy 内联快照,但没有任何路径把它
写进本机账本;而序列化会在下一次保存时把内联快照剥成 perfectPixelOperationId
标记。先前提交声称「滚动部署期间的在途操作不会被一次性判死」只对了一半:
第一次 hydrate 活下来,第二次就变成 failed + invalid,永久失去 exact retry
的 identity。

在 applyProjectSnapshot 读账本、splitCanvasLayoutItems 之后补一次性迁移。
三个条件都必要:只补写本机缺失的(本机那份可能刚在 pre-POST flush 之后被
重新锚定过,比布局里的新);只补写未收口的(收口态本就不需要账本)。

未采纳「删除后立即 flush 压缩竞态窗口」:核查发现删除已经触发既有的 450ms
防抖自动保存,该改动只能在由服务端处理耗时(数秒)主导的竞态里省下 450 毫秒,
代价是让高频操作绕过防抖。

未采纳「重做时弹确认框」:完美像素免费,最坏是素材库多一份;在常用路径上加
确认属于制造摩擦,且账本只能按 sourceResourceId 匹配同源,纯本地图层匹配不到,
覆盖不全的提醒比没有提醒更容易让人误以为安全。

Undo 复活占位在当前会话内不再对账(刷新即自愈)记为已知限制,连同根因与四种
修法各自的硬伤一并写入专题文档与决策日志。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
一、孤儿过早清账。上一条把孤儿改成「读到任何结论就清账本」,但 pending /
conflict 不是结论。浏览器关掉不会中止服务端处理——api-server 的处理与持久化
预算合计可达 90 秒,重开项目时单次 GET 没看见 resource 只说明「还不知道」。
此刻清账,服务端稍后落库便再无对账凭据,用户永远等不到「结果已进素材库」的
提示。改为只在 applied / dialog-missing 两个正向终态清账;pending / conflict
与读失败一律保留给下次加载重读,残留由保留期与条数上限兜住。

二、legacy 迁移漏 failed。判据按状态白名单列举 generating /
pending-confirmation,漏掉 failed + perfectPixelOperation——那是旧严格保存
失败的合法持久化形状,retryPerfectPixelOperation 明确接受它,「重试同一完美
像素操作」按钮正是在这个形状下出现。漏迁后下一次保存剥成 marker,再加载即
failed + invalid,重试按钮消失,用户只剩删掉重做,而那正是新 identity。判据
改用 isUnresolvedCanvasGenerationDialogRecord 取反,与 hydrate 判定收口态同
一个函数,两处不会漂移。

通用要求:涉及「这条 operation 还需不需要账本」的判断一律问「它收口了没有」,
不要列举状态——状态白名单会随语义变化静默漏项,本条就是实例。

同步更正专题文档三处残留矛盾:窗口起算点仍写作「稳定请求快照写入时」;仍称
「不会让布局校验失败升级成硬阻断」(应限定为解除客户端侧拒发,端到端 409
依赖仍在);「删除后不再对账」(应限定为结果不再自动回填画布)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
确立完美像素的优先级判据:低成本操作不为「丢资源」加限制
Project CI / Repository checks (pull_request) Failing after 10s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / Frontend tests (pull_request) Successful in 4m19s
Project CI / Native shell tests (pull_request) Failing after 10m3s
e4c2c7c25a
这条链路上反复出现同一种压力——为了防止「已生成的结果丢失关联」,不断有人
提议加限制。历史上真的加过两条(禁止删除未收口占位、随源图层清理豁免),
造成用户画布上出现删不掉的元素,后来被逐条作废;最近一轮评审又把「删除后可
创建第二个 identity」报成必须闭合的缺口,闭合方向是让本机账本参与防重。这种
压力不会自己停,写成判据。

事实前提:该操作免费、同步、纯几何规整、不进外部生成队列,重做代价接近于零,
与计费生成的风险结构根本不同,不能套用同一套「必须防止重复提交」的直觉。

判据:凡是「已生成资源丢失关联、需要用户重做或自行去素材库取回」这一类问题
一律不构成阻断项;只有主链路本身出问题才阻断——发起被拒、处理失败、结果没
落库、已落库的结果既不回填画布也不进素材库。

禁止:不得为防止上述丢失而新增任何限制用户操作的特性,包括但不限于「禁止
用户删除占位」「禁止用户重试」「禁止同一张图被处理两遍」。已作废的同类封锁
不得以任何理由重新引入。

连带把先前记为「已知缺口」的那条改记为明确接受的行为,并注明既有的
existingOperation 闸是本条确立之前的遗留,后续应放宽而非加固。本条不否定
exact retry——它是用户自愿选择的幂等路径,属于多给一个选项,不是限制。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lhk229 added 3 commits 2026-08-05 19:41:45 +08:00
死代码扫描(八路并行 + cargo 全量核对)确认:Rust 侧零新增——13 条 dead_code
警告在 master 与 HEAD 上引用计数逐一相同,全是既有的。死代码集中在 TS 侧,
且几乎同源:2fa2f245c 删掉严格布局保存通道时,删了调用点没删被调用的能力。

本次清理四条真死代码 + 一条会导致类型漂移的重复声明:

1. snapSelectedLayerToPerfectPixels 的 catch 里「operation 已形成但 POST 未
   发出」分支恒不可达。从 operation 赋值到 perfectPixelPostAttempted = true
   之间只剩 Set.add、纯函数、状态更新器,以及带 .catch 的 flush 与内部吞异常
   的 savePerfectPixelOperation,没有任何语句能抛。它挂着一句永不显示的用户
   文案,留着只会让人以为该失败态另有提示。

2. retryPerfectPixelOperation 的 postAttempted 三元同理,一并去掉分档。

3. saveEditorProjectLayout 的第三参数(signal / deadlineAt 注入)与
   EditorProjectLayoutSaveOptions 失去全部调用方,函数体内两处判断恒走默认。
   这里正是「每次重试重起 60 秒 deadline」的所在处,留着会让人以为调用方还能
   控制它。

4. 测试文件里 createEmptyEditorProjectSnapshot 全仓零引用——eslint 的
   unused-imports 不覆盖模块级函数声明,门禁放过了它。

5. useImageCanvasGenerationWorkflow 改为 import 现成的
   CanvasGenerationDialogDraft,删掉就地手写的同构类型。两份同构类型各自演化、
   改一处不会让另一处报错,是实打实的类型漂移温床。

其余七条(多余的 export、只有测试消费的路径、决策日志里指向已删符号的历史
引用)符号本身都活着,属于表面整洁,本次不动。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
本分支相对 master 新增约 16758 行,测试占 45%,而像素规整算法本体只改了
100 行。体量几乎全部来自「这条链路没有 durable job」这一个架构选择。

按存在理由把客户端机制分三层并写进文档:A 层服务端正确性与是否有 job 无关,
保留;C 层 POST 后一次对账保护的是「结果未知却谎报失败」,属于主链路,保留;
B 层跨会话续命(本机账本、刷新恢复与孤儿对账、75 秒窗口与锚点、marker 与账本
寿命对齐、exact retry、inline 占位到期与归属登记)只因为没有 job 而存在。

现在不删 B 层——它刚写完刚测过刚修完六个缺陷,删除本身有风险,收益在未来。
但迁移到 enqueue_editor_generation_job 时必须整层清除、不得与队列并存,否则
两套收口机制会产生「谁是终态权威」的二义性。写为阶段 4 的验收条件。

一并记录支撑结论的实测:requiresLiveSession 全仓仅一处置位、claim/release/has
五个调用点全在完美像素路径,故整层删除边界清晰;以及七条复查发现里 F1-F6 六条
全部落在 B 层。

另记一条待办:图集拆分的 catch 只 alert「拆分图集失败」,不区分确定失败与网关
合成的未知结果,服务端已落库却报失败属于谎报,落在主链路一侧。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Merge remote-tracking branch 'web/master' into feat/pixel_art2
Project CI / Repository checks (pull_request) Successful in 1m2s
Project CI / Frontend tests (pull_request) Successful in 3m2s
Project CI / Backend tests (pull_request) Successful in 3m36s
Project CI / Native shell tests (pull_request) Successful in 11m51s
ce81032903
# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
#	docs/project-memory/shared-memory/pitfalls.md
kdletters requested changes 2026-08-05 19:57:12 +08:00
Dismissed
kdletters left a comment
Member

当前 head 的账本迁移、孤儿即时 GET、成功态 hydrate 与专题文档已修复;删除后可重新处理、Undo 后刷新自愈按最新产品边界不再作为阻断。CI run 674 四项全绿,本地 230 项定向测试、typecheck、schema、encoding 与 diff 检查通过,merge-tree 也无内容冲突。仍有下面两条当前主链路 P2,请修复后再请求 review。

当前 head 的账本迁移、孤儿即时 GET、成功态 hydrate 与专题文档已修复;删除后可重新处理、Undo 后刷新自愈按最新产品边界不再作为阻断。CI run 674 四项全绿,本地 230 项定向测试、typecheck、schema、encoding 与 diff 检查通过,merge-tree 也无内容冲突。仍有下面两条当前主链路 P2,请修复后再请求 review。
@@ -20,0 +35,4 @@
export function requiresGenerationDeleteConfirmation(
dialog: CanvasGenerationDialogState,
) {
return dialog.status === 'generating' && !dialog.perfectPixelOperation;
Member

[P2] 源准备阶段的免费操作仍被当作计费生成

perfectPixelOperation 要到源图解析/上传完成后才写入,而这一段预算最长 90 秒;此前占位是 generating + requiresLiveSession,却没有 perfectPixelOperation,因此这里仍返回 true,用户删除免费的完美像素占位会看到「已消耗的泥点不会返还」。这与当前注释和产品决定的「完美像素任何状态直接删」相反。当前只有这条链路写 requiresLiveSession: true,可用该标记覆盖 operation 尚未形成的阶段,并补这个形状的回归测试。

[P2] 源准备阶段的免费操作仍被当作计费生成 perfectPixelOperation 要到源图解析/上传完成后才写入,而这一段预算最长 90 秒;此前占位是 generating + requiresLiveSession,却没有 perfectPixelOperation,因此这里仍返回 true,用户删除免费的完美像素占位会看到「已消耗的泥点不会返还」。这与当前注释和产品决定的「完美像素任何状态直接删」相反。当前只有这条链路写 requiresLiveSession: true,可用该标记覆盖 operation 尚未形成的阶段,并补这个形状的回归测试。
lhk229 marked this conversation as resolved
@@ -1737,0 +2397,4 @@
// 从未持久化,`validate_editor_pixel_art_snap_placeholder_exists` 直接 409。改成
// best-effort 之后客户端不再拿到成功 ACK,因此**无法证明**该前置已满足,只能提高
// 满足它的概率:占位可能已由此前的自动保存落库,PATCH 也可能成功而 ACK 丢失。
await flushProjectPersistence({
Member

[P2] 不要让辅助封面链阻塞完美像素 POST

这里同步等待 flush 的硬前置只是「占位布局已持久化」,但 flush 还会启动并等待封面保存:封面渲染中的 new Image() 没有 timeout/AbortSignal,上传与资源登记也参与同一个 await。只要某个封面图片既不触发 load 也不触发 error,布局 PATCH 即使已经成功,完美像素 POST 仍永远不会发出,图层锁与占位一直停在处理中;普通 transport failure 还会让布局请求按 60 秒上界外层重试三次。请让 pre-POST 路径只等待必要的布局提交,把封面持久化改成 fire-and-forget 或独立有界。

[P2] 不要让辅助封面链阻塞完美像素 POST 这里同步等待 flush 的硬前置只是「占位布局已持久化」,但 flush 还会启动并等待封面保存:封面渲染中的 new Image() 没有 timeout/AbortSignal,上传与资源登记也参与同一个 await。只要某个封面图片既不触发 load 也不触发 error,布局 PATCH 即使已经成功,完美像素 POST 仍永远不会发出,图层锁与占位一直停在处理中;普通 transport failure 还会让布局请求按 60 秒上界外层重试三次。请让 pre-POST 路径只等待必要的布局提交,把封面持久化改成 fire-and-forget 或独立有界。
lhk229 marked this conversation as resolved
lhk229 marked the pull request as ready for review 2026-08-05 20:03:13 +08:00
Author
Owner

兄弟路径(图集拆分)也存在的问题不应作为pr的阻断项

兄弟路径(图集拆分)也存在的问题不应作为pr的阻断项
Author
Owner

如果问题定位在找回“因意外丢失已生成资源”的链路,修复方向应当倾向于直接删除功能而非填漏洞

如果问题定位在找回“因意外丢失已生成资源”的链路,修复方向应当倾向于直接删除功能而非填漏洞
lhk229 added 2 commits 2026-08-05 20:44:16 +08:00
判据是 status === 'generating' && !perfectPixelOperation,而 perfectPixelOperation
要到源图解析/直传完成后才写入。未登记的本地图片要走 ticket → PUT → confirm,
预算上限 90 秒;这段窗口里占位是 generating、只有 requiresLiveSession、没有账本,
判据返回 true——用户删一个免费操作会被告知「已消耗的泥点不会返还」,与「完美像素
任何状态直接删、不弹确认」的决策直接矛盾。

perfectPixelOperationId marker 从占位创建那一刻就写上(operationId === dialogId
在创建时已知),判据改看 marker。marker 的语义也因此更准确:它表示「这个占位属于
一次完美像素操作」,而不是「账本已存在」。

未采纳「删掉弹窗入口」:删除入口全仓只有 requestRemoveCanvasGenerationDialog 一条,
右键、Delete、工具栏全部汇入,完美像素没有自己的删除路径可摘。「从本链路删入口」
等价于「让判据认得出本链路」,绕不开识别问题。

未采纳新增计费字段(今天唯一生产者只有完美像素,属过度设计),也未采纳判据加
requiresLiveSession(那只是换一个代理,而用短寿命字段的存在性判断长期属性正是本
缺陷的成因模式)。

连带影响已核:会话内到期清理不受影响(看的是 perfectPixelOperation 而非 marker);
只有加载时的快照清理会豁免源准备阶段的占位,使其显示为可删的失败卡而非被静默清掉,
这与「系统不替用户删」一致。

测试夹具原本带着 perfectPixelOperation,编码了与判据相同的错误假设,结构上覆盖不到
这个窗口。夹具补上 marker 还原真实形状,并新增「只有 marker、尚无账本」用例,回退
判据后报 expected true to be false。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
布局 flush 不再等待封面链
Project CI / Repository checks (pull_request) Successful in 1m11s
Project CI / Frontend tests (pull_request) Successful in 3m6s
Project CI / Backend tests (pull_request) Successful in 3m52s
Project CI / Native shell tests (pull_request) Successful in 12m6s
dee91b6889
flushProjectPersistence 显式关掉 queueProjectLayoutSave 内建的 fire-and-forget
封面分支,自己另起一份并在最后 await。于是每个 await flush 的调用方都被挂在
封面链后面:封面渲染要为每个可绘制图层取 signed URL 再 new Image() 加载,而
那个 Image 只有 onload/onerror,没有 timeout 也没有 AbortSignal,外层的
try/catch 只接得住 reject、接不住「永不 settle」。一张图不 settle,生成 POST
就永远发不出去。

这是「完美像素请求账本移出项目布局」把严格通道与普通 flush 合并成一条路径时
引入的回归——改动前 strict 分支在封面链启动之前就 return,两者是结构性隔离的。
图集拆分一直走非严格路径,暴露是既有的,但同源,一并解开。

必然发生的是延迟:封面签名含未量化的 viewport,而创建占位时会移动视口,所以
几乎每次完美像素都触发全量重渲染,而这些与服务端那个 409 前置毫无关系。可能
发生的是挂死:此时 finally 永不执行,图层锁与归属登记被永久持有,用户删掉占位
也没用——闸的另一半是静默 return,再点没有任何反应。

删除式修复:去掉 persistCover: false 覆盖、独立的 coverSave 与末尾的 await,
让封面回到 queue 内建的 fire-and-forget 分支。参数逐项等价,封面照存,只是不再
有人等它。未采纳加选项的方案,那会把同一问题留在图集拆分身上。

代价是 returnToProjects 不再等封面就跳转;该保护本就很薄——SPA 跳转不会打断
promise,真正打断它的是页面卸载,而 await 在 beforeunload 里同样救不了。

遗留:loadProjectCoverImage 里无界的 new Image() 仍是隐患,自动保存路径一样会
踩,本次只是把它移出生成链的关键路径。建议单开。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
kdletters approved these changes 2026-08-05 21:28:29 +08:00
kdletters merged commit 8f19964e3f into master 2026-08-05 21:28:41 +08:00
kdletters deleted branch feat/pixel_art2 2026-08-05 21:28:41 +08:00
Sign in to join this conversation.