diff --git a/CUserslinghAppDataLocalTempclaudeC--projects-narrative-Genarrative1f4d2af5-0931-4248-aee7-f6018dfc3969scratchpaddiff.txt b/CUserslinghAppDataLocalTempclaudeC--projects-narrative-Genarrative1f4d2af5-0931-4248-aee7-f6018dfc3969scratchpaddiff.txt
deleted file mode 100644
index 4eac74b8a..000000000
--- a/CUserslinghAppDataLocalTempclaudeC--projects-narrative-Genarrative1f4d2af5-0931-4248-aee7-f6018dfc3969scratchpaddiff.txt
+++ /dev/null
@@ -1,399 +0,0 @@
-diff --git a/src/components/image-editor/ImageCanvasSelectedLayerToolbarView.tsx b/src/components/image-editor/ImageCanvasSelectedLayerToolbarView.tsx
-index b1a423324..d8434e0e1 100644
---- a/src/components/image-editor/ImageCanvasSelectedLayerToolbarView.tsx
-+++ b/src/components/image-editor/ImageCanvasSelectedLayerToolbarView.tsx
-@@ -1,6 +1,7 @@
- import {
- Crop,
- Download,
-+ Grid2X2,
- ImageOff,
- Loader2,
- PersonStanding,
-@@ -22,6 +23,8 @@ type ImageCanvasSelectedLayerToolbarViewProps = {
- onOpenRedrawPanel: (layer: CanvasLayer) => void;
- onOpenCropExpandPanel: (layer: CanvasLayer) => void;
- onRemoveBackground: (layer: CanvasLayer) => void;
-+ onPerfectPixel: (layer: CanvasLayer) => void;
-+ isPerfectPixelProcessing: boolean;
- isSplittingIconSpritesheet: boolean;
- isPersistingAssetKind: boolean;
- onSplitIconSpritesheet: (layer: CanvasLayer) => void;
-@@ -37,6 +40,8 @@ export function ImageCanvasSelectedLayerToolbarView({
- onOpenRedrawPanel,
- onOpenCropExpandPanel,
- onRemoveBackground,
-+ onPerfectPixel,
-+ isPerfectPixelProcessing,
- isSplittingIconSpritesheet,
- isPersistingAssetKind,
- onSplitIconSpritesheet,
-@@ -120,6 +125,47 @@ export function ImageCanvasSelectedLayerToolbarView({
- icon={ImageOff}
- onClick={() => onRemoveBackground(selectedLayer)}
- />
-+
-+ ) : (
-+
-+ )
-+ }
-+ // 中文注释:素材类型保存在途时必须一并禁用。请求同时带 assetKind 和
-+ // sourceResourceId,本地类型已改但资源尚未落库时两者不一致,后端
-+ // resolve_editor_pixel_art_snap_asset_kind 会直接 400,只留下失败占位。
-+ // 与相邻的拆分图集按钮保持同一套门禁。
-+ disabled={isPersistingAssetKind || isPerfectPixelProcessing}
-+ aria-busy={isPersistingAssetKind || isPerfectPixelProcessing}
-+ onClick={() => onPerfectPixel(selectedLayer)}
-+ >
-+
-+ {isPersistingAssetKind
-+ ? '保存中'
-+ : isPerfectPixelProcessing
-+ ? '处理中'
-+ : '完美像素'}
-+
-+
- >
- ) : null}
- {selectedLayer.assetKind === 'icon-spritesheet' ? (
-diff --git a/src/components/image-editor/useImageCanvasGenerationWorkflow.ts b/src/components/image-editor/useImageCanvasGenerationWorkflow.ts
-index e40b1e752..43b404216 100644
---- a/src/components/image-editor/useImageCanvasGenerationWorkflow.ts
-+++ b/src/components/image-editor/useImageCanvasGenerationWorkflow.ts
-@@ -10,6 +10,7 @@ import {
- useState,
- } from 'react';
-
-+import { ApiClientError } from '../../services/apiClient';
- import { resolveEditorImageReferenceDataUrl } from '../../services/image-editor/editorImageReference';
- import { uploadEditorMediaAssetFile } from '../../services/image-editor/editorMediaAssetUploadClient';
- import {
-@@ -18,9 +19,11 @@ import {
- type EditorProjectLayerSnapshot,
- type EditorProjectResourceSnapshot,
- type EditorProjectSnapshot,
-+ loadEditorProject,
- splitEditorIconSpritesheet,
- } from '../../services/image-editor/editorProjectClient';
- import { resizeCropExpandFrame } from './ImageCanvasCropExpandModel';
-+import { isCanvasGenerationDialogLayoutItem } from './ImageCanvasEditorModel';
- import type {
- CanvasGenerationDialogState,
- CanvasHistoryAction,
-@@ -93,6 +96,7 @@ import {
- buildCropExpandInsetsFromFrame,
- removeImageBackground,
- renderCropExpandImage,
-+ snapImageToPerfectPixels,
- } from './ImageCanvasRasterEditModel';
- import {
- createUiAssetExtractionDraftMark,
-@@ -628,6 +632,8 @@ type GenerationWorkflowOptions = {
- project: EditorProjectSnapshot,
- action?: CanvasHistoryAction,
- ) => void;
-+ flushProjectPersistence?: () => Promise;
-+ refreshAssetLibrary?: () => Promise | void;
- onWalletBalanceMayHaveChanged?: () => void;
- };
-
-@@ -662,6 +668,8 @@ export function useImageCanvasGenerationWorkflow({
- assetFolderId,
- upsertGeneratedAsset,
- applyProjectSnapshot,
-+ flushProjectPersistence,
-+ refreshAssetLibrary,
- onWalletBalanceMayHaveChanged,
- }: GenerationWorkflowOptions) {
- const [isTaskSidebarOpen, setIsTaskSidebarOpen] = useState(false);
-@@ -677,6 +685,10 @@ export function useImageCanvasGenerationWorkflow({
- const splittingIconSpritesheetLayerIdsRef = useRef(new Set());
- const [splittingIconSpritesheetLayerIds, setSplittingIconSpritesheetLayerIds] =
- useState>(() => new Set());
-+ const perfectPixelLayerIdsRef = useRef(new Set());
-+ const [perfectPixelLayerIds, setPerfectPixelLayerIds] = useState>(
-+ () => new Set(),
-+ );
- const [isSpecMenuOpen, setIsSpecMenuOpen] = useState(false);
- const [isGenerationReferenceMenuOpen, setIsGenerationReferenceMenuOpen] =
- useState(false);
-@@ -1734,6 +1746,241 @@ export function useImageCanvasGenerationWorkflow({
- ],
- );
-
-+ const snapSelectedLayerToPerfectPixels = useCallback(
-+ async (sourceLayer: CanvasLayer) => {
-+ const normalizedProjectId = projectId?.trim();
-+ const isStaticRasterLayer =
-+ (sourceLayer.mediaType === undefined ||
-+ sourceLayer.mediaType === 'image') &&
-+ sourceLayer.assetKind !== 'character-animation';
-+ if (
-+ !isStaticRasterLayer ||
-+ !normalizedProjectId ||
-+ !applyProjectSnapshot ||
-+ !flushProjectPersistence ||
-+ perfectPixelLayerIdsRef.current.has(sourceLayer.id)
-+ ) {
-+ if (
-+ isStaticRasterLayer &&
-+ (!normalizedProjectId ||
-+ !applyProjectSnapshot ||
-+ !flushProjectPersistence)
-+ ) {
-+ showGenerationWarning('项目尚未准备好,暂时无法执行完美像素。');
-+ }
-+ return;
-+ }
-+
-+ perfectPixelLayerIdsRef.current.add(sourceLayer.id);
-+ setPerfectPixelLayerIds((currentLayerIds) => {
-+ const nextLayerIds = new Set(currentLayerIds);
-+ nextLayerIds.add(sourceLayer.id);
-+ return nextLayerIds;
-+ });
-+ closeGenerationTransientState();
-+ setImageContextMenu(null);
-+ setMetadataLayer(null);
-+ setCropExpandPanel(null);
-+ setQuickEditPanel(null);
-+ setCharacterAnimationPanel(null);
-+
-+ let perfectPixelDialogId: string | undefined;
-+ let perfectPixelPostAttempted = false;
-+ try {
-+ const assetLabel = `${sourceLayer.title} · 完美像素`;
-+ const placement = openPlacedCanvasGenerationDialog({
-+ ...createQuickEditGenerationDialogDraft({
-+ sourceLayer,
-+ prompt: '完美像素',
-+ assetLabel,
-+ status: 'generating',
-+ frame: {
-+ width: sourceLayer.width,
-+ height: sourceLayer.height,
-+ },
-+ }),
-+ composerOpen: false,
-+ // 中文注释:完美像素是同步 HTTP,服务端不建 durable job(见 snap_editor_image_to_pixel_art
-+ // 无 enqueue / spawn)。刷新掉本页会话后,没有任何东西会把这个占位推向终态,而
-+ // 服务端 409 门禁又要求占位必须先落库,所以只能在读取侧收口:置位后由项目首次
-+ // 加载时的 dropDeadInlineGenerationPlaceholders 清掉。
-+ requiresLiveSession: true,
-+ });
-+ perfectPixelDialogId = placement.dialogId;
-+ if (!placement.placeholder) {
-+ throw new Error('无法创建完美像素处理占位');
-+ }
-+
-+ const sourceImageSrc = await resolveEditorGenerationMediaReference(
-+ sourceLayer,
-+ 'image',
-+ normalizedProjectId,
-+ );
-+ await flushProjectPersistence();
-+ const sourceResourceId = sourceLayer.resourceId.trim();
-+ perfectPixelPostAttempted = true;
-+ const result = await snapImageToPerfectPixels({
-+ sourceImageSrc,
-+ projectId: normalizedProjectId,
-+ sourceResourceId:
-+ sourceResourceId &&
-+ !sourceResourceId.startsWith('local-resource-') &&
-+ !sourceResourceId.startsWith('generation-dialog:')
-+ ? sourceResourceId
-+ : undefined,
-+ assetKind: sourceLayer.assetKind,
-+ generationInputs: sourceLayer.generationInputs,
-+ assetFolderId,
-+ assetLabel,
-+ canvasCompletion: {
-+ dialogId: perfectPixelDialogId,
-+ title: assetLabel,
-+ placeholder: placement.placeholder,
-+ },
-+ });
-+ upsertGeneratedAsset?.(result.asset);
-+ if (!result.project) {
-+ if (hasCanvasGenerationDialogById(perfectPixelDialogId)) {
-+ updateCanvasGenerationDialogById(perfectPixelDialogId, () => null);
-+ showGenerationWarning(
-+ '完美像素结果已保存到素材库,画布占位已不存在。',
-+ );
-+ }
-+ return;
-+ }
-+ if (!hasCanvasGenerationDialogById(perfectPixelDialogId)) {
-+ return;
-+ }
-+ applyProjectSnapshot(result.project, {
-+ type: 'perfect-pixel',
-+ count: 1,
-+ });
-+ setActiveTool('select');
-+ setActiveSidebarPanel('layers');
-+ } catch (error) {
-+ // 中文注释:只有真正发出过 POST 才谈得上「结果可能已落库」。占位创建、源图解析
-+ // 和 flush 都在 POST 之前,它们失败时请求根本没发出去,此时提示核对素材库是反向
-+ // 谎报。
-+ //
-+ // 一旦发出过 POST,就不能只用「服务端有没有响应」判定结果是否已知。服务端持久化
-+ // 是非事务的(OSS + asset object → project resource → editor asset → canvas
-+ // completion),而 completion 失败走 map_editor_project_error 会被映射成
-+ // 403 / 404 / 409 / 400,带响应的 4xx 同样可能发生在对象、资源和账号素材全部落库
-+ // 之后。状态码分不出持久化阶段,所以服务端在第一次 OSS PUT 之后的失败路径上置
-+ // `resultPersistenceStarted`,客户端只对这类失败和完全无响应的失败做对账——常见的
-+ // 纯校验 400 / 排队 503 / 预算 504 不会白白多两次读取,也不会被附上不适用的提示。
-+ const persistenceMayHaveStarted =
-+ error instanceof ApiClientError &&
-+ (error.details as { resultPersistenceStarted?: unknown } | null)
-+ ?.resultPersistenceStarted === true;
-+ const outcomeMayBePersisted =
-+ perfectPixelPostAttempted &&
-+ Boolean(perfectPixelDialogId) &&
-+ (!(error instanceof ApiClientError) || persistenceMayHaveStarted);
-+ let reconciledMessage: string | undefined;
-+ if (outcomeMayBePersisted && perfectPixelDialogId) {
-+ // 中文注释:契约要求核对「项目 / 素材」两份快照。素材库是独立记录,只 GET
-+ // 项目却让用户去核对素材库,他看到的仍是旧列表。
-+ //
-+ // 但不能因为调了它就在文案里断言「素材库已刷新」:refreshAssetLibrary 在
-+ // `canAccessProtectedData` 为 false 时直接 return,读取失败也只在鉴权错误时
-+ // 弹登录框、其余一律吞掉,返回 `Promise` 不带成败信号;它本身还是可选
-+ // prop。刷新成功时用户白赚一份新列表,失败时文案必须仍然成立,所以下面只给
-+ // 「请确认」的指令,不给「已刷新」的事实断言。
-+ const [reconciled] = await Promise.all([
-+ loadEditorProject(normalizedProjectId).catch(() => null),
-+ Promise.resolve(refreshAssetLibrary?.()).catch(() => undefined),
-+ ]);
-+ if (reconciled) {
-+ const placeholderSurvived = reconciled.layers.some(
-+ (item) =>
-+ isCanvasGenerationDialogLayoutItem(item) &&
-+ (item as { dialog?: { id?: unknown } }).dialog?.id ===
-+ perfectPixelDialogId,
-+ );
-+ if (!placeholderSurvived) {
-+ // 中文注释:权威快照里占位不在了有两种原因——服务端 completion 消费掉,
-+ // 或者用户在请求期间主动删除。必须再查本地:本地占位也没了就是用户删的,
-+ // 契约要求此时不应用完成快照、不写历史,删除意图胜出。成功路径同一处有
-+ // 这道检查,对账路径不能漏。
-+ if (!hasCanvasGenerationDialogById(perfectPixelDialogId)) {
-+ return;
-+ }
-+ applyProjectSnapshot(reconciled, {
-+ type: 'perfect-pixel',
-+ count: 1,
-+ });
-+ setActiveTool('select');
-+ setActiveSidebarPanel('layers');
-+ return;
-+ }
-+ // 中文注释:占位仍在,画布没收到结果。这里刻意不调 applyProjectSnapshot:
-+ // 传给本 hook 的是 ImageCanvasEditorView 的 applyGeneratedProjectSnapshot,
-+ // 它的 action 默认值是 `generate-image`,不传 action 会写一条类型错误且受
-+ // 撤销保护的历史;而权威快照此刻和本地状态一致(占位都在),套用它只会白白
-+ // 覆盖用户在请求期间的未保存编辑。这次 GET 的用途是判定而不是同步。
-+ reconciledMessage = '画布未收到完美像素结果。';
-+ } else {
-+ reconciledMessage = '权威项目快照读取失败,无法确认完美像素结果。';
-+ }
-+ }
-+ const serverMessage =
-+ error instanceof Error && error.message.trim()
-+ ? error.message
-+ : undefined;
-+ // 中文注释:保留服务端原文(例如 assetKind 校验失败)便于定位,同时附上对账结论。
-+ // 持久化非事务,对象与账号素材可能已落库而只有画布回填没完成,所以必须让用户先核对
-+ // 素材库再决定,而不是直接重试。尾句只下指令、不断言素材库已经刷新——上面那次刷新
-+ // 可能是 no-op 或被静默吞掉,断言会让用户对着旧列表判定「没有派生图,可以重试」。
-+ const errorMessage = reconciledMessage
-+ ? `${serverMessage ? `${serverMessage} ` : ''}${reconciledMessage}请确认素材库是否已生成派生图,再决定是否重试。`
-+ : (serverMessage ?? '完美像素处理失败');
-+ if (
-+ perfectPixelDialogId &&
-+ hasCanvasGenerationDialogById(perfectPixelDialogId)
-+ ) {
-+ updateCanvasGenerationDialogById(perfectPixelDialogId, (dialog) => ({
-+ ...dialog,
-+ status: 'failed',
-+ errorMessage,
-+ }));
-+ } else if (!perfectPixelDialogId) {
-+ showGenerationWarning(errorMessage);
-+ }
-+ } finally {
-+ perfectPixelLayerIdsRef.current.delete(sourceLayer.id);
-+ setPerfectPixelLayerIds((currentLayerIds) => {
-+ if (!currentLayerIds.has(sourceLayer.id)) {
-+ return currentLayerIds;
-+ }
-+ const nextLayerIds = new Set(currentLayerIds);
-+ nextLayerIds.delete(sourceLayer.id);
-+ return nextLayerIds;
-+ });
-+ }
-+ },
-+ [
-+ applyProjectSnapshot,
-+ assetFolderId,
-+ closeGenerationTransientState,
-+ flushProjectPersistence,
-+ hasCanvasGenerationDialogById,
-+ openPlacedCanvasGenerationDialog,
-+ refreshAssetLibrary,
-+ projectId,
-+ setActiveSidebarPanel,
-+ setActiveTool,
-+ setCharacterAnimationPanel,
-+ setCropExpandPanel,
-+ setImageContextMenu,
-+ setMetadataLayer,
-+ setQuickEditPanel,
-+ showGenerationWarning,
-+ updateCanvasGenerationDialogById,
-+ upsertGeneratedAsset,
-+ ],
-+ );
-+
- const splitSelectedIconSpritesheet = useCallback(
- async (sourceLayer: CanvasLayer) => {
- if (
-@@ -2576,6 +2823,8 @@ export function useImageCanvasGenerationWorkflow({
- openCropExpandPanel,
- startCropExpandFrameResize,
- removeSelectedLayerBackground,
-+ snapSelectedLayerToPerfectPixels,
-+ perfectPixelLayerIds,
- splitSelectedIconSpritesheet,
- splittingIconSpritesheetLayerIds,
- taskListRefreshKey,
-@@ -2679,6 +2928,8 @@ export function useImageCanvasGenerationWorkflow({
- quickEditPanel,
- quickEditSourceLayer,
- removeSelectedLayerBackground,
-+ snapSelectedLayerToPerfectPixels,
-+ perfectPixelLayerIds,
- splitSelectedIconSpritesheet,
- splittingIconSpritesheetLayerIds,
- submitCharacterAnimation,