删除确认判据改看 marker,覆盖源准备阶段的免费占位
判据是 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>
This commit is contained in:
@@ -6551,3 +6551,15 @@
|
||||
- 顺带记录的待办(不在本次范围):图集拆分的 catch 只做 `window.alert('拆分图集失败')`,不区分「确定失败」与「网关合成的未知结果」。服务端可能已经切完并落库 N 个 asset 却报失败,用户照提示重拆就会拿到双份——这属于谎报,落在主链路一侧,与「丢资源不阻断」不是一类问题。最小修法是复用现成的 `isGatewayUnknownOutcomeError` 改文案,十几行,不需要照搬 B 层。
|
||||
- 影响范围:仅文档。不改代码、不改测试。
|
||||
- 关联文档:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`(已写入三层划分、B 层边界实测与清除条件)。
|
||||
|
||||
## 2026-08-05 删除确认判据改看 marker,覆盖源准备阶段的免费占位
|
||||
|
||||
- 缺陷:`requiresGenerationDeleteConfirmation` 的判据是 `status === 'generating' && !dialog.perfectPixelOperation`,而 `perfectPixelOperation` 要到源图解析 / 直传完成后才写入。未登记的本地图片要走 `ticket → PUT → confirm`,预算上限 90 秒;这段窗口里占位是 `generating`、只有 `requiresLiveSession: true`、没有账本,于是判据返回 true——用户删一个**免费**操作会被告知「已消耗的泥点不会返还」。与同日「完美像素占位恢复为可删除」里「任何状态直接删、不弹确认」的决策直接矛盾。已登记资源走短路解析、窗口接近于零,暴露只在未登记本地图片上成立。
|
||||
- 决策:`perfectPixelOperationId` marker 从占位**创建那一刻**就写上(`operationId === dialogId` 在创建时已知),判据改看 marker:`status === 'generating' && !dialog.perfectPixelOperationId`。marker 的语义也因此更准确——它表示「这个占位属于一次完美像素操作」,而不是「账本已存在」。
|
||||
- 未采纳「删掉弹窗入口」:删除入口全仓只有 `requestRemoveCanvasGenerationDialog` 一条,右键、Delete 快捷键、工具栏全部汇入,完美像素没有自己的删除路径可以摘除。「从本链路删除入口」在实现上等价于「让判据认得出本链路」,绕不开识别问题。真正删掉弹窗只能对所有生成占位一起做,那是另一个产品决定(对计费生成而言该文案是真实信息),不作为修此缺陷的副产品。
|
||||
- 未采纳「新增 `generationCostMudPoints` 字段让判据直接问是否计费」:语义上最正,但今天唯一的生产者只有完美像素,图集拆分根本不创建占位,第二个消费者并不存在,属于为一个调用方过度设计。
|
||||
- 未采纳「判据加 `requiresLiveSession === true`」:该字段全仓确实只有完美像素一处置位,一行即可修,但它的含义是「只能由本会话收口」而非「免费」。换一个代理不解决问题——**用短寿命字段的存在性判断长期属性**正是本缺陷(以及 F5)的成因模式。
|
||||
- 已核过的连带影响:会话内到期清理不受影响,`inlineGenerationPlaceholderExpiryAt` 看的是 `perfectPixelOperation` 而非 marker,源准备阶段被放弃的占位照常到期消失。受影响的只有加载时的快照清理 `dropDeadInlineGenerationPlaceholders`——它的豁免判据接受 marker,因此「源准备中途关标签页」的占位不再被静默清掉,而是在下次加载显示为可删的失败卡。这与「TTL 豁免 = 系统不替用户删,用户主动删除始终允许」一致,判为改善而非退化。id 撞车时 `openCanvasGenerationDialog` 会另生成 id,marker 会暂时指向旧值;它此刻只被当作存在性标记使用(效果仍是不弹确认),写 request 时按真实 dialogId 纠正,不影响任何判等。
|
||||
- 测试缺口的根因:原用例的夹具 `durablePerfectPixelDialog` 带着 `perfectPixelOperation`,编码了与判据相同的错误假设,结构上不可能覆盖 operation 形成之前的窗口。夹具已补上 marker 以还原真实形状,并新增「只有 marker、尚无账本」的用例,已实证:回退判据后报 `expected true to be false`。
|
||||
- 影响范围:`useCanvasGenerationDialogs.ts` 的判据、`useImageCanvasGenerationWorkflow.ts` 的占位创建。不修改服务端、SpacetimeDB schema 或对外契约。
|
||||
- 关联文档:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`。
|
||||
|
||||
@@ -41,6 +41,9 @@ function durablePerfectPixelDialog(
|
||||
status,
|
||||
composerOpen: true,
|
||||
sourceLayerId: 'layer-source',
|
||||
// 中文注释:marker 从占位创建那一刻就存在,账本形成后也一直在。夹具必须还原这个形状,
|
||||
// 否则下游用例是在测一个真实链路里不存在的状态。
|
||||
perfectPixelOperationId: dialogId,
|
||||
perfectPixelOperation: {
|
||||
version: 1,
|
||||
kind: 'perfect-pixel',
|
||||
@@ -384,6 +387,23 @@ describe('useCanvasGenerationDialogs', () => {
|
||||
},
|
||||
);
|
||||
|
||||
it('never asks for delete confirmation before the perfect-pixel ledger exists', () => {
|
||||
// 中文注释:源图解析 / 直传最长 90 秒,期间占位只有 marker、还没有账本。此前判据按账本
|
||||
// 判,用户在这段窗口里删一个免费操作会看到「已消耗的泥点不会返还」。
|
||||
expect(
|
||||
requiresGenerationDeleteConfirmation({
|
||||
id: 'perfect-pixel-preparing',
|
||||
mode: 'quick-edit',
|
||||
prompt: '完美像素',
|
||||
status: 'generating',
|
||||
composerOpen: false,
|
||||
sourceLayerId: 'layer-source',
|
||||
requiresLiveSession: true,
|
||||
perfectPixelOperationId: 'perfect-pixel-preparing',
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('still asks for delete confirmation on an ordinary generating placeholder', () => {
|
||||
expect(
|
||||
requiresGenerationDeleteConfirmation({
|
||||
|
||||
@@ -31,11 +31,19 @@ export type CanvasGenerationDialogDraft = Omit<
|
||||
* `generation_cost_mud_points = 0`,删除占位也不撤销任何在途请求——它没有取消接口,结果照常
|
||||
* 落库并进素材库,服务端 completion 发现 dialog 已不在会返回 DialogMissing 并由客户端提示。
|
||||
* 所以完美像素占位在任何状态都直接删,不额外解释。
|
||||
*
|
||||
* 判据必须看 `perfectPixelOperationId` 而**不是** `perfectPixelOperation`:后者要到源图解析 /
|
||||
* 直传完成后才写入,那一段预算最长 90 秒(未登记的本地图片要走 ticket → PUT → confirm),
|
||||
* 期间占位是 `generating` 且没有账本,按账本判会让用户删一个免费操作时看到「已消耗的泥点
|
||||
* 不会返还」。marker 在占位创建那一刻就写上,覆盖完整生命周期。
|
||||
*
|
||||
* 更一般地:这里问的是「**这次生成计不计费**」,账本的有无只是它在某一段时间内的代理。
|
||||
* 用短寿命字段的存在性去判断长期属性,正是本仓库反复出错的形状。
|
||||
*/
|
||||
export function requiresGenerationDeleteConfirmation(
|
||||
dialog: CanvasGenerationDialogState,
|
||||
) {
|
||||
return dialog.status === 'generating' && !dialog.perfectPixelOperation;
|
||||
return dialog.status === 'generating' && !dialog.perfectPixelOperationId;
|
||||
}
|
||||
|
||||
function withGenerationTimestamps<T extends GenerateDialogState | null>(
|
||||
|
||||
@@ -2283,6 +2283,7 @@ export function useImageCanvasGenerationWorkflow({
|
||||
let perfectPixelPostAttempted = false;
|
||||
try {
|
||||
const assetLabel = `${sourceLayer.title} · 完美像素`;
|
||||
const perfectPixelOperationId = createPerfectPixelOperationId();
|
||||
const placement = openPlacedCanvasGenerationDialog({
|
||||
...createQuickEditGenerationDialogDraft({
|
||||
sourceLayer,
|
||||
@@ -2294,11 +2295,21 @@ export function useImageCanvasGenerationWorkflow({
|
||||
height: sourceLayer.height,
|
||||
},
|
||||
}),
|
||||
id: createPerfectPixelOperationId(),
|
||||
id: perfectPixelOperationId,
|
||||
composerOpen: false,
|
||||
// 中文注释:marker 必须在占位诞生这一刻就写上,不能等到 operation 形成。源图解析 /
|
||||
// 直传最长 90 秒,期间占位是 `generating` 且尚无账本;删除确认判据若按账本判,用户
|
||||
// 在这段窗口里删一个**免费**操作会看到「已消耗的泥点不会返还」。
|
||||
//
|
||||
// 与 id 同值:`operationId === dialogId` 是这条链路的既有不变式。万一 id 撞车被
|
||||
// `openCanvasGenerationDialog` 换掉,marker 会暂时指向旧值——此时它仍然只被用作
|
||||
// 「这是一次完美像素操作」的存在性标记(不弹确认),随后写 request 时会按真实
|
||||
// dialogId 纠正,不影响任何判等。
|
||||
perfectPixelOperationId,
|
||||
// 中文注释:源引用尚未解析、operation 快照也尚未形成时先沿用 legacy 会话标记;
|
||||
// 这一阶段退出不会发 POST,孤儿可按 TTL 清理。稳定 request 写入后会立即移除该
|
||||
// 标记,durable operation 改由刷新后的 GET-only 恢复收口,绝不能再被 TTL 删除。
|
||||
// 这一阶段退出不会发 POST,会话内到期清理照常生效(`inlineGenerationPlaceholderExpiryAt`
|
||||
// 看的是 `perfectPixelOperation` 而非 marker)。稳定 request 写入后会立即移除该标记,
|
||||
// durable operation 改由刷新后的 GET-only 恢复收口,绝不能再被 TTL 删除。
|
||||
requiresLiveSession: true,
|
||||
});
|
||||
perfectPixelDialogId = placement.dialogId;
|
||||
|
||||
Reference in New Issue
Block a user