加入手动完美像素入口;像素艺术勾选时自动补提示词约束 #124
Reference in New Issue
Block a user
Delete Branch "feat/pixel_art2"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
perfect-pixel又禁止 Undo。ee2cad377新增了这条无合并、不可 Undo 的调用路径。ee2cad377新增。assetId且不传sourceResourceId时,下载前最多顺序执行 8 次 procedure;每次仅约 4 秒就已累计超过 30 秒。首次真正套 deadline 是 OSS 下载。ee2cad377新增;823df0f0f宣称前移预算,但没有包住 DB;5d2588dbe又让 permit 跨这些调用持有。generating,没有 job、轮询或再次 GET。generationInputs审计字段伪造screenColorHex/mattingProvider/mattingModel;入口仅序列化,随后原样写入资源和素材,后台 raw mapper 可见。isPersistingAssetKind与 handler ref guard。kind=character, style=pixelArt时 provider 收到追加后的 prompt;但原图资源仍保存role_setting,透明结果 保存“去除纯色背景”。db6dbfaf9新增了像素子句和“会写入 project resource”的错误声明。trim;角色经过 builder;图标甚至没有原始 prompt,而是由 descriptions 组装。db6dbfaf9新增。回包覆盖未保存编辑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 再拉最新快照。
这个改动面覆盖全部生成/编辑完成路径,属于画布持久化的架构调整,跟像素艺术这个分支没有关系。
已修复
已修复
已修复
已修复
当前 head
cb27797a仍有 3 个 P2 阻断项,请修改后再合并:[P2] 未知结果对账无法识别真实成功。前端 useImageCanvasGenerationWorkflow.ts:1896 仅以同 ID generation-dialog 是否存在判断成功;服务端 editor_project.rs:8733 成功后会保留该 dialog,仅改为 idle 并写 generatedLayerId。因此响应丢失后,真实完成快照仍会被误判失败。现有成功测试使用 layers: [],不符合服务端生产形状。
[P2] 立即刷新后 live-session 占位仍可能永久停在 generating。ImageCanvasEditorModel.ts:558 会保留 180 秒内的占位,而 useImageCanvasProjectPersistence.ts:1434 的清理只在首次加载执行一次,没有到期定时器、轮询或 durable job;必须再次刷新或手工删除才能收口。
[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
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 文档均已确认修复。
给 saveEditorProjectLayout 与 loadEditorProject 补超时后,两条精确参数断言失败: toHaveBeenCalledWith 要求参数完全匹配,新增第四个参数即不匹配。 把 { timeoutMs: 60_000 } 写进断言,而不是放宽成 anything()。超时是契约的一部分 ——一个被 flushProjectPersistence 同步等待在提交路径上、一个在对账路径上,没有 上界会把在途占位拖过存活窗口。写死之后谁删掉它测试就会红;放宽则等于让刚建立的 上界失去看守。去掉生产代码里的两个超时,两条断言同时变红。 真正的问题是验证方式:改的是 services 下的文件,却只跑了 components 的测试。 这条链路横跨两个目录,往后验证同时覆盖两处。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>复审提交: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。结论:当前 HEAD
89914b225仍有 4 条生产问题(3×P2、1×P3)和 1 条测试问题。前三条 P2 涉及完美像素主链路的一致性,不建议当前状态合并。1. [P2] 本地 timeout 无法取消已发送的 Spacetime procedure
timeout_at到期返回 504 → 前端立即 GET 到旧快照并解锁 → 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。::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 的孤儿对象
objectKey的路径,危险组合由本 PR 引入。::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”不能证明素材已保存
resultPersistenceStarted;对账项目自然找不到 dialog。::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 错误已完全排除。
[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。
存在功能性bug
结论:近四个提交仍有 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。failed + invalid。注意:完美像素是低成本操作,所有“因意外丢失已生成资源”的低优先级问题不应构成阻断,只有主链路存在问题才阻断。同时也不应该新增加“禁止用户删除”、“禁止用户重试”、“禁止一张图处理两遍”等特性
当前 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;[P2] 源准备阶段的免费操作仍被当作计费生成
perfectPixelOperation 要到源图解析/上传完成后才写入,而这一段预算最长 90 秒;此前占位是 generating + requiresLiveSession,却没有 perfectPixelOperation,因此这里仍返回 true,用户删除免费的完美像素占位会看到「已消耗的泥点不会返还」。这与当前注释和产品决定的「完美像素任何状态直接删」相反。当前只有这条链路写 requiresLiveSession: true,可用该标记覆盖 operation 尚未形成的阶段,并补这个形状的回归测试。
@@ -1737,0 +2397,4 @@// 从未持久化,`validate_editor_pixel_art_snap_placeholder_exists` 直接 409。改成// best-effort 之后客户端不再拿到成功 ACK,因此**无法证明**该前置已满足,只能提高// 满足它的概率:占位可能已由此前的自动保存落库,PATCH 也可能成功而 ACK 丢失。await flushProjectPersistence({[P2] 不要让辅助封面链阻塞完美像素 POST
这里同步等待 flush 的硬前置只是「占位布局已持久化」,但 flush 还会启动并等待封面保存:封面渲染中的 new Image() 没有 timeout/AbortSignal,上传与资源登记也参与同一个 await。只要某个封面图片既不触发 load 也不触发 error,布局 PATCH 即使已经成功,完美像素 POST 仍永远不会发出,图层锁与占位一直停在处理中;普通 transport failure 还会让布局请求按 60 秒上界外层重试三次。请让 pre-POST 路径只等待必要的布局提交,把封面持久化改成 fire-and-forget 或独立有界。
兄弟路径(图集拆分)也存在的问题不应作为pr的阻断项
如果问题定位在找回“因意外丢失已生成资源”的链路,修复方向应当倾向于直接删除功能而非填漏洞