收窄完美像素对账的触发条件与副作用
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

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:
2026-08-01 11:53:36 +00:00
parent cc88564d4a
commit adfe980110
3 changed files with 52 additions and 7 deletions
@@ -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 {