收窄完美像素对账的触发条件与副作用
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>
This commit is contained in:
@@ -5970,4 +5970,5 @@
|
||||
- 决策:未知结果先 `loadEditorProject` 取权威快照,再按占位是否存活分流。占位已被 completion 消费掉说明这次其实成功,按快照收口并写入正常的 `perfect-pixel` 历史,不报错。占位仍在说明画布没收到结果,同步快照消除本地与服务端偏差但**不写历史**,文案明确告知结果未知且素材库可能已有派生图、要求用户先核对再决定是否重试——持久化是非事务的,OSS 对象与账号素材可能已落库而画布回填未完成。对账 GET 本身失败时给出「权威快照读取失败」的独立文案,不退回谎报。
|
||||
- 未覆盖:刷新页面后停在 `generating` 的占位仍无自动收口。占位在 POST 前已由 `flushProjectPersistence` 落库,`hydrateCanvasGenerationDialog` 原样恢复 `generating`,而该链路不进 `external_generation_job`,任务侧栏轮询看不到它,inline POST 的 Promise 随旧页面销毁。需要在 hydration 后加对账,且要先给 dialog 增加「属于无 durable job 的 inline 链路」标记,改动面大于本次,单独立项。客户端 120 秒超时相对服务端 30 秒预算是四倍冗余,调小可让对账更早发生,未处理。
|
||||
- 验证:新增三条用例分别覆盖「未知但实际成功→按快照收口并写历史」「未知且占位存活→只同步快照、标失败、文案要求先核对」「`ApiClientError` 已知失败→不发对账 GET、不动快照」。既有用例 `keeps a failed perfect-pixel placeholder` 原本用裸 `Error` 表达「服务端识别不到网格」,语义不准且会误入对账路径,改为 `ApiClientError`。测试 harness 新增 `dialog-error` 输出,否则对账文案不可观测。
|
||||
- 补充(同日):对账只对真正发出过 POST 的失败生效。占位创建、源图解析和 `flushProjectPersistence` 都在 POST 之前,它们失败时请求根本没发出,此时给出「素材库可能已存在派生图」是反向谎报,与本条要修的谎报是镜像关系;用 `perfectPixelPostAttempted` 标记划界,同时省掉一次无意义的权威读取。占位存活分支也不再调用 `applyProjectSnapshot`:传给该 hook 的是 `ImageCanvasEditorView` 的 `applyGeneratedProjectSnapshot`,其 action 默认值为 `generate-image`,不传 action 会写一条类型错误且受撤销保护的历史,而权威快照此刻与本地一致(占位都在),套用只会覆盖用户在请求期间的未保存编辑。这次 GET 的用途是判定,不是同步。
|
||||
- 关联文档:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`。
|
||||
|
||||
@@ -2361,8 +2361,10 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
await waitFor(() => {
|
||||
expect(loadEditorProjectMock).toHaveBeenCalledWith('project-1');
|
||||
});
|
||||
// 中文注释:只同步快照,不带历史动作——这次没有成功。
|
||||
expect(applyProjectSnapshot).toHaveBeenCalledWith(reconciledProject);
|
||||
// 中文注释:判定用途,不做同步。传进来的 applyProjectSnapshot 是
|
||||
// applyGeneratedProjectSnapshot,不传 action 会写一条类型错误且受撤销保护的
|
||||
// generate-image 历史;而权威快照此刻与本地一致,套用只会覆盖未保存编辑。
|
||||
expect(applyProjectSnapshot).not.toHaveBeenCalled();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('dialog').textContent).toContain('failed');
|
||||
});
|
||||
@@ -2374,6 +2376,39 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('does not reconcile when the perfect-pixel request never left the client', async () => {
|
||||
// 中文注释:占位创建、源图解析和 flush 都在 POST 之前。它们失败时请求根本没发出,
|
||||
// 说「素材库可能已存在派生图」是反向谎报,也不该白打一次权威读取。
|
||||
const applyProjectSnapshot = vi.fn();
|
||||
const flushProjectPersistence = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error('画布保存失败'));
|
||||
|
||||
render(
|
||||
<GenerationWorkflowHarness
|
||||
projectId="project-1"
|
||||
initialLayers={[
|
||||
createLayer({
|
||||
objectKey: 'generated-images/editor/source.png',
|
||||
src: '/generated-images/editor/source.png',
|
||||
}),
|
||||
]}
|
||||
applyProjectSnapshot={applyProjectSnapshot}
|
||||
flushProjectPersistence={flushProjectPersistence}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '完美像素' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('dialog').textContent).toContain('failed');
|
||||
});
|
||||
expect(snapImageToPerfectPixelsMock).not.toHaveBeenCalled();
|
||||
expect(loadEditorProjectMock).not.toHaveBeenCalled();
|
||||
expect(applyProjectSnapshot).not.toHaveBeenCalled();
|
||||
expect(screen.getByTestId('dialog-error').textContent).toBe('画布保存失败');
|
||||
});
|
||||
|
||||
it('keeps a rejected perfect-pixel request out of the reconciliation path', async () => {
|
||||
// 中文注释:服务端明确响应过(ApiClientError)就是已知结果,不需要也不应该
|
||||
// 再发对账 GET,否则每个 400 都要多打一次权威读取。
|
||||
|
||||
@@ -1783,6 +1783,7 @@ export function useImageCanvasGenerationWorkflow({
|
||||
setCharacterAnimationPanel(null);
|
||||
|
||||
let perfectPixelDialogId: string | undefined;
|
||||
let perfectPixelPostAttempted = false;
|
||||
try {
|
||||
const assetLabel = `${sourceLayer.title} · 完美像素`;
|
||||
const placement = openPlacedCanvasGenerationDialog({
|
||||
@@ -1810,6 +1811,7 @@ export function useImageCanvasGenerationWorkflow({
|
||||
);
|
||||
await flushProjectPersistence();
|
||||
const sourceResourceId = sourceLayer.resourceId.trim();
|
||||
perfectPixelPostAttempted = true;
|
||||
const result = await snapImageToPerfectPixels({
|
||||
sourceImageSrc,
|
||||
projectId: normalizedProjectId,
|
||||
@@ -1855,7 +1857,11 @@ export function useImageCanvasGenerationWorkflow({
|
||||
// project resource、账号素材和画布回填,只是响应没回来。该路由是 unsafe
|
||||
// POST 且禁用自动重放,所以未知时必须先 GET 权威快照对账再决定文案:直接
|
||||
// 标 failed 会谎报结果,用户重试就再造一份对象、资源和素材。
|
||||
const outcomeIsUnknown = !(error instanceof ApiClientError);
|
||||
// 中文注释:只有真正发出过 POST 才谈得上「结果未知」。占位创建、源图解析和
|
||||
// flush 都在 POST 之前,它们失败时请求根本没发出去,此时说「素材库可能已存在
|
||||
// 派生图」是反向谎报,和这次要修的那个谎报是镜像关系。
|
||||
const outcomeIsUnknown =
|
||||
perfectPixelPostAttempted && !(error instanceof ApiClientError);
|
||||
let reconciledMessage: string | undefined;
|
||||
if (outcomeIsUnknown && perfectPixelDialogId) {
|
||||
const reconciled = await loadEditorProject(normalizedProjectId).catch(
|
||||
@@ -1879,10 +1885,13 @@ export function useImageCanvasGenerationWorkflow({
|
||||
setActiveSidebarPanel('layers');
|
||||
return;
|
||||
}
|
||||
// 中文注释:占位仍在,画布没收到结果。先同步权威快照消除本地与服务端的
|
||||
// 偏差,但不写历史——这次没有成功。持久化是非事务的,OSS 对象与账号素材
|
||||
// 仍可能已经落库,所以文案必须让用户先去核对而不是直接重试。
|
||||
applyProjectSnapshot(reconciled);
|
||||
// 中文注释:占位仍在,画布没收到结果。这里刻意不调 applyProjectSnapshot:
|
||||
// 传给本 hook 的是 ImageCanvasEditorView 的 applyGeneratedProjectSnapshot,
|
||||
// 它的 action 默认值是 `generate-image`,不传 action 会写一条类型错误且受
|
||||
// 撤销保护的历史;而权威快照此刻和本地状态一致(占位都在),套用它只会白白
|
||||
// 覆盖用户在请求期间的未保存编辑。这次 GET 的用途是判定而不是同步。
|
||||
// 持久化是非事务的,OSS 对象与账号素材仍可能已落库,所以文案必须让用户先
|
||||
// 去核对而不是直接重试。
|
||||
reconciledMessage =
|
||||
'完美像素结果未知:已核对权威快照,画布未收到结果。素材库可能已存在派生图,请先确认再决定是否重试。';
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user