diff --git a/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasChrome.css b/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasChrome.css
index 5ef0c6397..99e0417d3 100644
--- a/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasChrome.css
+++ b/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasChrome.css
@@ -1219,13 +1219,40 @@
* 所以这里只负责锚定,壳样式(边框 / 底色 / 阴影 / 头部与滚动区)在共享样式表里,宿主不抄第二份。
*
* 位置取舍:右上角是画布上唯一不被栏目工具栏(左下)与缩放 / 撤销 Dock(右下)占用的稳定空位;
- * 顶部 3.5rem 起是任务开关,面板从 8.5rem 起排,两者同时开着也不重叠。
+ * 顶部 0.85rem 起是任务开关,面板从 3.2rem 起排(开关约 30px 高 + 一档间距),两者不重叠 ——
+ * 而且打开替换面板时会把「生成任务」面板收起来,同一个角只留一块浮层。
+ *
+ * 锚在**画布容器**(`.game-resource-book-manager`,本身就是 `position: relative`)而不是 stage:
+ * 相对 stage 写死的偏移会在工具条换行变高时压上去(与任务开关同一类问题)。
*/
.game-resource-replacement-panel {
position: absolute;
- top: 8.5rem;
+ top: 3.2rem;
right: 0.85rem;
z-index: 45;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ gap: 0.4rem;
+}
+
+/*
+ * 源素材那一行小字:面板改成非模态以后,画布上的「源素材」光环会被一次空白点击清掉,
+ * 而面板里只列候选、不说明在替换谁 —— 源身份得在面板这一侧留一份可读的说法。
+ */
+.game-resource-replacement-panel-source {
+ max-width: 100%;
+ overflow: hidden;
+ border: 1px solid var(--platform-subpanel-border);
+ border-radius: 999px;
+ background: var(--platform-subpanel-fill);
+ box-shadow: var(--platform-panel-shadow);
+ color: var(--platform-text-base);
+ font-size: 0.72rem;
+ font-weight: 750;
+ padding: 0.2rem 0.6rem;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
/* 栏目画布底部工具栏:栏目页(view=child)左下角的画布 chrome。
diff --git a/apps/ai-game-creator-shell/src/view/project-development/index.tsx b/apps/ai-game-creator-shell/src/view/project-development/index.tsx
index 4e6602202..f956ccf58 100644
--- a/apps/ai-game-creator-shell/src/view/project-development/index.tsx
+++ b/apps/ai-game-creator-shell/src/view/project-development/index.tsx
@@ -122,6 +122,7 @@ import {
dispatchResourceReferenceInsertMany,
isResourceReferenceOverlayTarget,
resolveActiveIterationVersion,
+ resourceDisplayName,
resourceReferenceCategoryLabel,
} from '../../features/project-workspace/resourceReferences';
import { GameRunVersionPicker } from '../../features/resource-canvas/GameRunVersionPicker';
@@ -352,6 +353,7 @@ import {
type ResourceCardReferenceDropRect,
resourceCardReferenceDropRect,
resourceCardReferenceDropReferences,
+ sameResourceCardReferenceDrop,
} from './resourceCardReferenceDropModel';
import { ResourceClassificationPanel } from './ResourceClassificationPanel';
import {
@@ -5996,7 +5998,15 @@ export default function ProjectDevelopmentView({
event.clientY,
drag.moves.map((move) => move.resourceId),
);
- setResourceCardReferenceDrop(referenceDrop);
+ // 只有条数或对话栏矩形真的变了才落状态:`pointermove` 每帧都算一次新对象,
+ // 直接把新对象塞进 state 会让整个工作台每帧重渲染一遍。
+ setResourceCardReferenceDrop((current) =>
+ current &&
+ referenceDrop &&
+ sameResourceCardReferenceDrop(current, referenceDrop)
+ ? current
+ : referenceDrop,
+ );
if (referenceDrop) {
setResourceCardDragPreview(null);
resourceDependencyOverlayRef.current?.clearDragPreview();
@@ -7017,6 +7027,9 @@ export default function ProjectDevelopmentView({
setResourceReplacementError(null);
setResourceReplacementCandidates([]);
setResourceReplacementSource(source);
+ // 画布右上角只放一块浮层:替换面板与「生成任务」侧栏抢同一个角,打开替换就把任务面板收起来
+ // (任务照旧在后台推进,收起只影响这个视图)。
+ setResourceAssetGenerationTasksPanelOpen(false);
// 从工具条重新进入口时清掉上一次在画布上点出来的目标:面板里的选中必须与这次源素材一致。
setResourceReplacementPick(null);
setResourceReplacementLoading(true);
@@ -7152,6 +7165,27 @@ export default function ProjectDevelopmentView({
() => projectResourcesByManifestAssetId(resources),
[resources],
);
+ /**
+ * 本次替换的**源素材显示名**(面板上方那行小字)。
+ *
+ * 面板改成非模态以后,画布上的「源素材」光环会被一次空白点击清掉(这是画布本来的语义),
+ * 而面板里只列候选、不说明在替换谁——源身份因此得在面板这一侧留一份可读的说法。
+ * 取投影里的显示名(与资源卡、@ 面板同源),投影里找不到时回落到 manifest 资产名。
+ */
+ const resourceReplacementSourceLabel = useMemo(() => {
+ const assetId = resourceReplacementSource?.assetId;
+ if (!assetId) {
+ return null;
+ }
+ const projected = resources.find(
+ (resource) => resource.manifestAssetId === assetId,
+ );
+ if (projected) {
+ return projected.label;
+ }
+ const asset = manifest.assets.find((entry) => entry.id === assetId);
+ return asset ? resourceDisplayName(asset) : null;
+ }, [manifest.assets, resourceReplacementSource, resources]);
/**
* 本次会话的替换血缘 → 资源卡标注(按 manifest 资产 id 取)。
*
@@ -10709,6 +10743,72 @@ export default function ProjectDevelopmentView({
)}
+ {/*
+ 版本级资源替换(直接替换):复用美术画布的参考图选择组件,但走**非模态浮层**——
+ 面板锚在**画布容器**里(任务开关下方),面板开着的时候画布照常可点:在画布上点
+ 目标素材即落进面板的当前选择,写入仍然只由面板的「确认」发起。
+ 锚在画布容器而不是 stage:绝对定位写死相对 stage 的偏移会在工具条换行时压在工具条上
+ (与任务开关同一类问题),画布容器本身就是 `position: relative`。
+ 候选缩略图走**资源卡同一条预览管线**(身份 / 队列 / LRU 缓存都共用),组件只负责把
+ 管线给的 `sourceUrl` 画出来;读不到时退回类型占位,与资源卡占位语义一致。
+ */}
+
+ {resourceReplacementSourceLabel ? (
+
+ {`替换源素材:${resourceReplacementSourceLabel}`}
+
+ ) : null}
+
{
+ const resource =
+ resourceByManifestAssetId.get(asset.id) ?? null;
+ const previewIdentity = resource
+ ? (resourceCardPreviews.identityByResourceId.get(
+ resource.id,
+ ) ?? null)
+ : null;
+ return (
+
+ );
+ }}
+ onCancel={closeResourceVersionReplacement}
+ onConfirm={(assetIds) =>
+ void confirmResourceVersionReplacement(assetIds)
+ }
+ />
+
>
) : (
@@ -10779,57 +10879,6 @@ export default function ProjectDevelopmentView({
uiEditorRoute ? 'editor' : mode === 'run' ? 'run' : 'canvas'
}
/>
- {/*
- 版本级资源替换(直接替换):复用美术画布的参考图选择组件,但走**非模态浮层**——
- 面板挂在画布右上角(在任务开关下方),面板开着的时候画布照常可点:在画布上点目标
- 素材即落进面板的当前选择,写入仍然只由面板的「确认」发起。
- 候选缩略图走**资源卡同一条预览管线**(身份 / 队列 / LRU 缓存都共用),组件只负责把
- 管线给的 `sourceUrl` 画出来;读不到时退回类型占位,与资源卡占位语义一致。
- */}
-
- {
- const resource =
- resourceByManifestAssetId.get(asset.id) ?? null;
- const previewIdentity = resource
- ? (resourceCardPreviews.identityByResourceId.get(
- resource.id,
- ) ?? null)
- : null;
- return (
-
- );
- }}
- onCancel={closeResourceVersionReplacement}
- onConfirm={(assetIds) =>
- void confirmResourceVersionReplacement(assetIds)
- }
- />
-
{!uiEditorRoute ? (
diff --git a/apps/ai-game-creator-shell/src/view/project-development/resourceCardReferenceDropModel.ts b/apps/ai-game-creator-shell/src/view/project-development/resourceCardReferenceDropModel.ts
index 2bbd50c5b..0ad91f1c4 100644
--- a/apps/ai-game-creator-shell/src/view/project-development/resourceCardReferenceDropModel.ts
+++ b/apps/ai-game-creator-shell/src/view/project-development/resourceCardReferenceDropModel.ts
@@ -48,6 +48,26 @@ export function resourceCardReferenceDropContains(
);
}
+/**
+ * 两次落点状态是否**逐值相同**。
+ *
+ * 指针在对话栏上移动时每帧都会算出一个新的落点对象;只看「有没有落点」会让每一帧都 `setState`
+ * 一次、把工作台整棵子树重渲染一遍(`pointermove` 热路径上这是实打实的浪费)。带上这一条,
+ * 只有条数或对话栏矩形真的变了才重新渲染。
+ */
+export function sameResourceCardReferenceDrop(
+ left: { count: number; rect: ResourceCardReferenceDropRect },
+ right: { count: number; rect: ResourceCardReferenceDropRect },
+) {
+ return (
+ left.count === right.count &&
+ left.rect.left === right.rect.left &&
+ left.rect.top === right.rect.top &&
+ left.rect.width === right.rect.width &&
+ left.rect.height === right.rect.height
+ );
+}
+
/**
* 拖到对话要插入的引用。
*
diff --git a/apps/ai-game-creator-shell/tests/resourceCardReferenceDropModel.test.ts b/apps/ai-game-creator-shell/tests/resourceCardReferenceDropModel.test.ts
index ce2eca7de..09567033e 100644
--- a/apps/ai-game-creator-shell/tests/resourceCardReferenceDropModel.test.ts
+++ b/apps/ai-game-creator-shell/tests/resourceCardReferenceDropModel.test.ts
@@ -5,6 +5,7 @@ import {
resourceCardReferenceDropHintLabel,
resourceCardReferenceDropRect,
resourceCardReferenceDropReferences,
+ sameResourceCardReferenceDrop,
} from '../src/view/project-development/resourceCardReferenceDropModel';
import type { ProjectResource } from '../src/view/project-development/resourceProjectionModel';
@@ -142,4 +143,27 @@ describe('拖动素材到对话:落点与引用构造', () => {
'这些素材还没登记为项目资源,不能 @ 引用',
);
});
+
+ test('落点状态逐值比较:条数或矩形变了才算变化', () => {
+ const rect = { left: 900, top: 40, width: 300, height: 600 };
+ const base = { count: 2, rect };
+ // 指针在对话栏上移动时每帧都会算出新的对象:逐值相同必须判成「没变」,
+ // 否则每一帧都会 setState 一次、把工作台整棵子树重渲染一遍。
+ expect(
+ sameResourceCardReferenceDrop(base, { count: 2, rect: { ...rect } }),
+ ).toBe(true);
+ expect(sameResourceCardReferenceDrop(base, { count: 1, rect })).toBe(false);
+ expect(
+ sameResourceCardReferenceDrop(base, {
+ count: 2,
+ rect: { ...rect, left: rect.left + 1 },
+ }),
+ ).toBe(false);
+ expect(
+ sameResourceCardReferenceDrop(base, {
+ count: 2,
+ rect: { ...rect, height: rect.height + 1 },
+ }),
+ ).toBe(false);
+ });
});
diff --git a/apps/ai-game-creator-shell/tests/resourceVersionReplacement.test.tsx b/apps/ai-game-creator-shell/tests/resourceVersionReplacement.test.tsx
index 1013d87cd..fb0d88e87 100644
--- a/apps/ai-game-creator-shell/tests/resourceVersionReplacement.test.tsx
+++ b/apps/ai-game-creator-shell/tests/resourceVersionReplacement.test.tsx
@@ -346,6 +346,13 @@ function renderReplacementWorkbench(options: RenderOptions = {}) {
if (command === 'get_local_game_manifest') {
return nextManifest;
}
+ // 「生成任务」面板与未完成编辑账本(本文件的互斥用例会开一次任务面板)。
+ if (command === 'list_local_project_asset_generations') {
+ return [];
+ }
+ if (command === 'list_pending_local_project_resource_edits') {
+ return [];
+ }
throw new Error(`unexpected invoke ${command}`);
},
);
@@ -1188,6 +1195,40 @@ describe('版本级资源替换', () => {
.value,
).toBe('final');
});
+ /**
+ * 面板锚在**画布容器**里(不是 stage 顶层):相对 stage 写死的偏移会在工具条换行变高时压上去
+ * —— 与任务开关同一类问题。同时钉住「同一个角只留一块浮层」:打开替换面板会收起任务面板
+ * (任务本身仍在账本里推进),并显示这次替换的源素材名(非模态之后源身份不再只靠画布光环)。
+ */
+ it('面板锚在画布容器里、写明源素材,并与任务面板互斥', async () => {
+ renderReplacementWorkbench();
+
+ // 先开「生成任务」面板:替换入口与它抢同一个角。
+ fireEvent.click(screen.getByRole('button', { name: '生成任务' }));
+ await screen.findByRole('region', { name: '生成任务' });
+
+ const toolbar = await selectCardAndOpenToolbar('legacy.png');
+ fireEvent.click(toolbarAction(toolbar, '替换素材'));
+ const dialog = await screen.findByRole('dialog', {
+ name: '选择替换素材',
+ });
+
+ const anchor = dialog.closest('.game-resource-replacement-panel');
+ expect(anchor).not.toBeNull();
+ expect(
+ document
+ .querySelector('.game-resource-book-manager')
+ ?.contains(anchor as Element),
+ ).toBe(true);
+ expect(
+ within(anchor as HTMLElement).getByText(/替换源素材:legacy/u),
+ ).not.toBeNull();
+ // 同一个角只留一块浮层:任务面板被收起。
+ await waitFor(() =>
+ expect(screen.queryByRole('region', { name: '生成任务' })).toBeNull(),
+ );
+ });
+
/**
* 面板里按下「确认」才写入:写入路径与旧的弹窗确认**完全同一条**(同一个 `confirm` 函数),
* 载荷必须逐字一致,成功后关闭面板并标注血缘。
diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md
index 81cd4c70b..aae019f90 100644
--- a/docs/project-memory/shared-memory/decision-log.md
+++ b/docs/project-memory/shared-memory/decision-log.md
@@ -1,5 +1,15 @@
# 决策记录
+## 2026-09-21 本批自查(PR #441):三处修正
+
+- 背景:推 PR 后按「局部到整体」自查这一批(三需求 + 验收修正),查出三条:①拖动到对话的落点在 `pointermove` 上每帧都 `setState` 一个新对象;②替换面板相对 **stage** 写死 `top: 8.5rem`(与刚修的任务开关同一类隐患:工具条换行会压上去),且它和「生成任务」面板抢画布右上角同一个位置;③替换面板不显示「在替换哪张源素材」,而非模态化之后那点线索(画布上的源素材光环)会被一次空白点击清掉。
+- 决策①:落点状态改为**逐值比较**(`sameResourceCardReferenceDrop`:条数 + 对话栏矩形四值),只有真的变了才落 state——指针在对话栏上移动不再每帧重渲染整个工作台。
+- 决策②:面板从 stage 顶层移进**画布容器** `.game-resource-book-manager`(它本身就是 `position: relative`,与左下角工具栏、右下角 Dock 同一套锚定口径),`top: 3.2rem` 排在任务开关下方;并在打开替换会话时**收起「生成任务」面板**——画布右上角同一时刻只留一块浮层(任务照旧在账本里推进,收起只影响这个视图)。
+- 决策③:面板上方补一行「替换源素材:<显示名>」(投影显示名优先、manifest 资产名回落),源身份不再只靠画布光环。
+- 验证:`resourceVersionReplacement.test.tsx` 20 passed(新增「面板锚在画布容器里、写明源素材、并与任务面板互斥」)、`resourceCardReferenceDropModel.test.ts` 6 passed(新增逐值比较)、`resourceCanvasChatReferenceDrop.test.tsx` 4 passed;`tsc -p apps/ai-game-creator-shell/tsconfig.json --noEmit` 通过。
+- 影响范围:`apps/ai-game-creator-shell/src/view/project-development/{index.tsx,resourceCardReferenceDropModel.ts}`、`.../features/resource-canvas/resourceCanvasChrome.css`、用例 3 个文件、验收用例 C9 注脚。未动 Rust / SpacetimeDB / 共享组件行为(只新增了宿主的源素材行)。
+- 已知未覆盖:仍无真机目视;替换会话与任务面板的互斥是新行为,客户端需要复看一次(若产品希望两者能同时看,把这一句收紧即可)。
+
## 2026-09-21 验收现场修正:任务开关压在工具条上、开关与面板并排
- 背景:客户端验收截图两条:①画布右上角那枚「生成任务 · N」开关与工具条(打开项目目录 / 资源面板 / 整理画布 / 管理未完成编辑 / 依赖 · 类型)**重叠**;②「生成任务」面板展开时,开关与面板**并排**摆着,产品口径是「这俩不应该并排,出来详情以后入口就应该隐藏」。
@@ -9,6 +19,7 @@
- 验证:`resourceCanvasAssetGenerationTasksPanel.test.tsx` 12 passed(新增「展开时开关让位:同一时刻只有面板那枚 ×」;计数用例改为收起态查开关、展开态查面板头部)、`resourceCanvasAssetGenerationTasksSidebarStyle.test.ts` 11 passed(锚点改成网格条目坐标:`grid-row` / `grid-column` / `justify-self` / `align-self` / `margin` 与运行档上边距;窄屏改判 `justify-self: stretch`)、`resourceCanvasGenerationTasksSidebarDismiss.test.tsx` 5 passed(收起改走面板 ×、收起后开关回来能再打开、锚点仍在 stage 里)、`resourceCanvasAssetGenerationBackgroundClose.test.tsx` 5 passed;`appSurface.test.ts` 538 passed / 17 skipped / 0 failed。
- 影响范围:`apps/ai-game-creator-shell/src/features/resource-canvas/{ResourceCanvasAssetGenerationTasksPanelView.tsx,resourceCanvasAssetGenerationTasksSidebar.css}`、`apps/ai-game-creator-shell/src/styles.css`、三个同场景用例文件、PRD §3.10 与更新时间。
- 已知未覆盖:仍无真机目视(网格落点在 Tauri 里的实际观感、工具条换行时锚点是否仍贴画布顶边需要现场复核)。
+
## 2026-09-21 AGC 替换面板改为非模态浮层,支持在画布上点选目标
- 背景:`docs/project-memory/todos/【待办】画布验收后续修复-2026-09-18.md` 的「后续需求」要求「在替换面板中支持画布点选目标」。2026-09-13 那版做的是「候选弹窗 footer 一个『点选替换』按钮 → 关掉弹窗 → 进画布点选态 → 点中即提交」:面板与画布互斥(弹窗外壳是全屏遮罩,留着它画布上的卡点不到),用户要么在面板里筛、要么离开面板。
@@ -19,6 +30,7 @@
- 验证:`resourceVersionReplacement.test.tsx` 19 passed(4 条旧点选用例改写为:非模态判据 + 点画布候选落进面板且零写入、面板确认才写入且载荷逐字一致+血缘、四类非法目标面板内报因零写入、Esc 只收面板不清选中;新增「画布点选不清掉面板搜索/分类」)、`projectAssetPickerDialogShellStyle.test.tsx` 7 passed(新增 nonModal 形态与浮层壳样式声明,含关掉即卸载)、`src/components/image-editor/ImageCanvasEditorView.test.tsx` 64 passed、`ImageCanvasEditorGenerationIntegration.test.tsx` 44 passed(网页端弹窗行为未变);连同 `appSurface.test.ts`、`projectResourceLiveIntegration.test.tsx` 共 6 个文件 694 passed / 17 skipped / 0 failed;`tsc -p apps/ai-game-creator-shell/tsconfig.json --noEmit` 通过。
- 影响范围:`src/components/image-editor/ImageCanvasProjectAssetPickerDialog.tsx`、`packages/shared/src/components/styles.css`、`apps/ai-game-creator-shell/src/view/project-development/index.tsx`、`.../features/resource-canvas/resourceCanvasChrome.css`、`apps/ai-game-creator-shell/tests/{resourceVersionReplacement.test.tsx,projectAssetPickerDialogShellStyle.test.tsx}`、PRD §5.3 / §7.8 第 8 条、`docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md` 的 S15a。未动 Rust、SpacetimeDB、external v1。
- 已知未覆盖:真机观感(面板在画布右上角的落点、与任务开关同时打开时的间距)未在 Tauri 目视确认;面板里不显示「本次替换的是哪张源素材」,源身份只靠画布上那张卡的选中光环表达——点空白清掉选中后就只剩面板自己的候选列表。
+
## 2026-09-21 AGC 资源卡拖到对话实现批量 @ 引用
- 背景:`docs/project-memory/todos/【待办】画布验收后续修复-2026-09-18.md` 的「后续需求」要求「聊天拖拽批量引用」。改前只有两条入口:聊天输入框里输入 `@` 或点 `@` 按钮开素材选择面板,以及资源卡选中工具条上那枚「引用」按钮——都要先把素材找出来再点,多选批量引用没有一次成型的路径。
@@ -28,6 +40,7 @@
- 验证:`resourceCardReferenceDropModel.test.ts` 5 passed(矩形/命中/构造/提示文案)、`resourceCanvasChatReferenceDrop.test.tsx` 4 passed(单卡拖到对话 → 1 条引用且零坐标写入、多选整批 → 2 条、拖回画布 → 照旧写手动坐标且零引用、pointercancel 收干净)、`resourceReferenceInput.test.tsx` 30 passed(新增批量事件一次派发且空批次不派发、一次 insertReferences 按序插入整批 chip);连同 `appSurface.test.ts` 在内 7 个用例文件 660 passed / 17 skipped / 0 failed;`tsc -p apps/ai-game-creator-shell/tsconfig.json --noEmit` 通过。
- 影响范围:`apps/ai-game-creator-shell/src/view/project-development/{index.tsx,resourceCardReferenceDropModel.ts}`、`.../features/project-workspace/resourceReferences.ts`、`apps/ai-game-creator-shell/src/App.tsx`、`apps/ai-game-creator-shell/src/styles.css`、`apps/ai-game-creator-shell/tests/{resourceCardReferenceDropModel.test.ts,resourceCanvasChatReferenceDrop.test.tsx,resourceReferenceInput.test.tsx}`、`docs/【功能说明】AGC聊天素材引用-2026-09-08.md`。未动 Rust、SpacetimeDB、`packages/`、共享弹窗组件。
- 已知未覆盖:真实客户端里的手感(拖到对话栏的触发距离、提示条位置、多选整批的视觉反馈)未在 Tauri 目视确认;「复制对话保留有效引用」属于同一条后续需求里的另一半,本轮未做。
+
## 2026-09-21 AGC「生成任务」侧栏移到画布右上角(照抄美术画布,保留 AGC 样式)
- 背景:`docs/project-memory/todos/【待办】画布验收后续修复-2026-09-18.md` 的「后续需求」要求「生成任务列表移到画布右上角,进行中/已完成分组、失败可见、限高滚动及自动开合」。改前状态:侧栏本体是 `position: fixed; top: 4rem; bottom: 6rem; left: 0.75rem` 的左侧贴边面板,开合口只有工具条上那一枚「生成任务 · N」按钮(资源 / 运行两个页签各渲染一次),折叠态不留任何常驻入口;分组、失败可见、限高滚动、提交后自动展开这四条当时已经具备。
@@ -38,6 +51,7 @@
- 验证:`resourceCanvasAssetGenerationTasksPanel.test.tsx` 11 passed(新增「折叠只剩右上角开关」「开关计数与面板头部同源」「锚点按工作面分档」)、`resourceCanvasAssetGenerationTasksSidebarStyle.test.ts` 11 passed(锚点坐标 / 运行档下移 / 面板不再自定位 / 开关走 token 且无硬编码色 / 窄屏占满宽度)、`resourceCanvasGenerationTasksSidebarDismiss.test.tsx` 5 passed(入口位置改为右上角锚点、工具条不再有生成任务按钮)、`resourceCanvasAssetGenerationBackgroundClose.test.tsx` 5 passed;`appSurface.test.ts` 538 passed / 17 skipped / 0 failed(与既有基线逐条一致);`tsc -p apps/ai-game-creator-shell/tsconfig.json --noEmit` 通过。
- 影响范围:`apps/ai-game-creator-shell/src/features/resource-canvas/{ResourceCanvasAssetGenerationTasksPanelView.tsx,resourceCanvasAssetGenerationTasksSidebar.css}`、`apps/ai-game-creator-shell/src/view/project-development/index.tsx`、`apps/ai-game-creator-shell/tests/{resourceCanvasAssetGenerationTasksPanel.test.tsx,resourceCanvasAssetGenerationTasksSidebarStyle.test.ts,resourceCanvasGenerationTasksSidebarDismiss.test.tsx}`、PRD §3.10。未动 Rust、SpacetimeDB、`packages/`、共享弹窗组件。
- 已知未覆盖:真实客户端观感(右上角坐标相对画布顶边的落点、与运行表现层版本入口的间距)未在 Tauri 里目视确认;窄屏(≤480px)只有声明级断言。
+
## Unity 与 Godot 常用操作指导
两种编辑器的操作指导复用客户端审核 Skill pack:DirectProject 通过原生 Skill 或既有审核资源读取入口按需取得,Agent Runtime 的对应执行工具说明嵌入同源参考。指南不改变插件可用性、执行授权或 Runner 回执;只读说明不能证明编辑器已连接。常用示例与执行失败/部分修改、保存、撤销边界在同一参考中维护,避免提示词和文档各存一份代码。
@@ -66,6 +80,7 @@ Godot 编辑器操控复用既有 AGC 插件宿主、EditorAdapter、Runner 和
- 捆绑 Codex 固定版本只在 `build_support/codex_bundle.rs` 声明一次(当前 0.155.1),构建期侧车清单、宿主补丁执行器身份、逐次审批协议允许列表和模型目录捕获共同引用;升级原生依赖时同步重取同一 tag 的 vendor 解析源码与 UPSTREAM 证据,并复跑真实目录、补丁往返与并发夹具。0.155 起原生执行入口改为统一 exec(`exec_command` + `write_stdin`,旧 `shell_command` 不再注册),宿主许可与预算照常覆盖。
- 付费许可按原回合原租约绑定并传递到实际提交点:容量与同动作锁等待可取消,每次新增 POST 前与封口共用短锁复核,封口/终止/耗尽后零新增提交;已越过提交边界的请求不丢弃,保留 operation ID 与不确定状态走 GET 对账。本地写入同理,等待项目锁后必须复核原许可,未结算或失败的写入围栏未恢复前不得封口。
- 权威合同:[AI 游戏创作智能体 App 实施计划](../../technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md)。
+
## 2026-09-20 最近项目检查保持项目级隔离
- 背景:最近项目刷新会重新检查所有路径。若其中一个目录损坏、超时或不可读,清空整张状态表会让已确认正常的项目暂时全部显示“检查中”,用户只能移除坏项目后看到列表恢复。
diff --git a/docs/project-memory/todos/【待办】画布验收后续修复-2026-09-18.md b/docs/project-memory/todos/【待办】画布验收后续修复-2026-09-18.md
index 2ac4f47cd..5eab4da0b 100644
--- a/docs/project-memory/todos/【待办】画布验收后续修复-2026-09-18.md
+++ b/docs/project-memory/todos/【待办】画布验收后续修复-2026-09-18.md
@@ -71,6 +71,8 @@
- **本批第二项(拖到对话批量引用)**:资源卡按住拖到右侧 Agent 对话栏、松手即按这次拖动真正参与位移的那批(多选整批、单选只引用按住的那张)插入 `@` 引用;落点铺虚线框 + 「松手即可 @ 引用 N 项素材」,拖回画布内松手仍是排版语义;引用口径与工具条「引用」按钮逐字一致(只认已登记素材、`source: resource-card`),派发走新增的批量事件一次事务插入。
- **本批第三项(替换面板画布点选)**:替换候选面板从全屏弹窗改为**画布右上角的非模态浮层**(共享组件新增 opt-in `nonModal`,网页端弹窗行为不变),面板开着时画布照常可点——点中合法候选即落成面板里的当前选择,写入仍只由「确认」发起;非法目标在面板里报因、零写入。旧的「点选替换」按钮、画布提示条与点选态随之退役(同一个功能不留两条 UI 路径)。
- **验收现场修正(第四批内)**:截图两条——右上角「生成任务」开关与工具条**重叠**、展开时开关与面板**并排**。锚点改挂**画布那一格网格**(不再用写死 `top` 的绝对定位,工具条换行也不会压上去),开关只在完全收起时渲染(展开后由面板头部 × 或点外部收起)。
+- **PR #441 自查修正**:拖动落点改逐值比较(不再每帧重渲染工作台);替换面板从 stage 顶层移进画布容器并排在任务开关下方,打开替换会话时收起任务面板(同一个角只留一块浮层);面板上方补「替换源素材:<显示名>」一行(非模态之后源身份不再只靠画布光环)。
+
- 本机执行过(三项合计):`resourceCanvasAssetGenerationTasksPanel` / `…SidebarStyle` / `…SidebarDismiss` / `…BackgroundClose`、`resourceCardReferenceDropModel`、`resourceCanvasChatReferenceDrop`、`resourceReferenceInput`、`resourceVersionReplacement`、`projectAssetPickerDialogShellStyle`、`ImageCanvasEditorView`、`ImageCanvasEditorGenerationIntegration`、`projectResourceLiveIntegration`、`appSurface` 全绿(定向批次多项合计 694 passed / 17 skipped);`typecheck`、`check:encoding`、`git diff --check`、prettier / eslint 通过。
- 仍未做:真实客户端目视(三项的真机手感与落点)与远程 CI 都没跑;窄屏(≤480px)只有声明级断言;「复制对话保留有效引用」未做。
diff --git a/docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md b/docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md
index 1014fa076..08df2cc5f 100644
--- a/docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md
+++ b/docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md
@@ -7,12 +7,12 @@
## 1. 验收基线的先决条件
-| 事实 | 依据 | 对验收的影响 |
-| --- | --- | --- |
-| 基线 commit 为 `ea9668e4a`(2026-09-11 15:37) | `git log -1`:`ea9668e4a`「资源总览「所有资源」卡补上资源预览」 | 验收记录表回填此 hash 作为冻结基线 |
+| 事实 | 依据 | 对验收的影响 |
+| --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
+| 基线 commit 为 `ea9668e4a`(2026-09-11 15:37) | `git log -1`:`ea9668e4a`「资源总览「所有资源」卡补上资源预览」 | 验收记录表回填此 hash 作为冻结基线 |
| 三个验收重点改动已入库,但**必须重新构建 / 重启客户端才会生效** | `2a157ea6f`「预览可见性门禁等 root 就绪再建 observer,并把登记表补挂齐」、`6bdc8bbd9`「AGC 资源分类面板改为「编辑素材标签」:只编辑标签、分类不再有手动入口」、`ea9668e4a`「资源总览「所有资源」卡补上资源预览」 | ① 可见卡停 `idle` 的修复;② 「编辑标签」只改 tags;③ 「所有资源」卡渲染真实预览。客户端若仍在跑旧构建(真机当前进程即属此列),这三条会得到**假失败** |
-| 工作区仍有 1 个非本用例范围的文件处于修改态 | `git status --short`:`apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts`(3 insertions / 2 deletions) | 只影响 AGC 全量测试结果,不影响手工验收;该文件相关用例失败时先与基线比对再定性 |
-| 真机项目落盘分类分布与旧构建指纹一致 | 真机 `.agent/manifest.json` 落盘 `category` = `unclassified 57 / ui-interaction 3 / scene 1` | 未用新构建复测时,卡片预览、标签面板、所有资源预览三条会得到**假失败** |
+| 工作区仍有 1 个非本用例范围的文件处于修改态 | `git status --short`:`apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts`(3 insertions / 2 deletions) | 只影响 AGC 全量测试结果,不影响手工验收;该文件相关用例失败时先与基线比对再定性 |
+| 真机项目落盘分类分布与旧构建指纹一致 | 真机 `.agent/manifest.json` 落盘 `category` = `unclassified 57 / ui-interaction 3 / scene 1` | 未用新构建复测时,卡片预览、标签面板、所有资源预览三条会得到**假失败** |
> 结论:验收开始前先固化"客户端构建版本 + api-server 是否重启 + 基线 commit"三件事,否则后续结论不可信。
@@ -28,72 +28,72 @@
### A 阶段 · 从零进项目
-| 步骤 | 操作 | 期望结果 | 对应 PRD 条款 | 怎么判"过了"(可观察判据) | 已知例外 / 未做项 |
-| --- | --- | --- | --- | --- | --- |
-| **S1** 建项 | 首页「做游戏」→ 输入需求 → 选择目录 → 确认为新项目 | 建项成功并直接进入项目工作台,不新建第二套页面或资产系统 | L7(四区)、L23(创作链路)、L108 | 进入后四区同时在 DOM:左侧平台导航、`.game-workbench-stage`(`aria-label="项目主视窗"`)、右侧 Supervisor 对话栏、底部 Agent Dock | 普通用户「新增资源」入口禁用(L108、L587) |
-| **S2** 落地视图 | 进项目后不动鼠标,看中央区 | 落 `resource-overview`;manifest 里 `preview.status=running` 的**陈旧记录**不得把你直接丢进运行界面 | L145–146、**L165**(`run` 自动进入只认内存 registry 活体确认) | `document.querySelector('.game-workbench-stage')?.dataset.resourceViewState` ∈ `resources.list` / `resources.selected.` / `resources.ui-editor`;**该属性为 `undefined` 即说明已在 run 视图** | 陈旧记录对齐:命中"记录 running 但 registry 没跑"时调 `stop_local_game_preview` 并按真相投影 `{status:'stopped'}`(`sessionPreview.ts:87-101`) |
-| **S3** 项目页面板(可跳过) | 「项目组」页:搜索、行尾更多菜单、Escape 关闭 | 表格只显示权威数据列;Escape 关菜单并把焦点还给触发按钮;移除只改本机最近项目记录、不动磁盘 | L135–139 | 无匹配状态提供「清除搜索」;document/body 无页面级横纵向溢出 | **L139 要求 populated fixture 截图 + 视频演示搜索/清除/行尾菜单** —— 视频条款,见 7.1 |
+| 步骤 | 操作 | 期望结果 | 对应 PRD 条款 | 怎么判"过了"(可观察判据) | 已知例外 / 未做项 |
+| --------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
+| **S1** 建项 | 首页「做游戏」→ 输入需求 → 选择目录 → 确认为新项目 | 建项成功并直接进入项目工作台,不新建第二套页面或资产系统 | L7(四区)、L23(创作链路)、L108 | 进入后四区同时在 DOM:左侧平台导航、`.game-workbench-stage`(`aria-label="项目主视窗"`)、右侧 Supervisor 对话栏、底部 Agent Dock | 普通用户「新增资源」入口禁用(L108、L587) |
+| **S2** 落地视图 | 进项目后不动鼠标,看中央区 | 落 `resource-overview`;manifest 里 `preview.status=running` 的**陈旧记录**不得把你直接丢进运行界面 | L145–146、**L165**(`run` 自动进入只认内存 registry 活体确认) | `document.querySelector('.game-workbench-stage')?.dataset.resourceViewState` ∈ `resources.list` / `resources.selected.` / `resources.ui-editor`;**该属性为 `undefined` 即说明已在 run 视图** | 陈旧记录对齐:命中"记录 running 但 registry 没跑"时调 `stop_local_game_preview` 并按真相投影 `{status:'stopped'}`(`sessionPreview.ts:87-101`) |
+| **S3** 项目页面板(可跳过) | 「项目组」页:搜索、行尾更多菜单、Escape 关闭 | 表格只显示权威数据列;Escape 关菜单并把焦点还给触发按钮;移除只改本机最近项目记录、不动磁盘 | L135–139 | 无匹配状态提供「清除搜索」;document/body 无页面级横纵向溢出 | **L139 要求 populated fixture 截图 + 视频演示搜索/清除/行尾菜单** —— 视频条款,见 7.1 |
### B 阶段 · 素材产生与登记
-| 步骤 | 操作 | 期望结果 | 对应 PRD 条款 | 怎么判"过了" | 已知例外 / 未做项 |
-| --- | --- | --- | --- | --- | --- |
-| **S4** 首轮生成 | 在右侧 Supervisor 对话里发起首轮素材生成 | 生成由**对话启动**(不用画布按钮);素材写入对应栏目并登记进 manifest;未生成的素材不显示为可编辑对象 | L25、L94、L500;飞书 §4 首轮生成 | 对话完成后 `.agent/manifest.json` 的 `assets[]` 长度增加;新卡 `data-resource-card-id` 为 `asset:` 形态 | 「首轮进度投影」只在 Issue 交付范围的"应该"清单出现、**无编码级判据**;现有可见性由底部 Agent 状态栏承载 |
-| **S5** 栏目归属 | 看资源总览的栏目缩略卡与各栏目卡片 | 固定 7 栏:`UI 交互 → 角色与对象 → 场景与环境 → 音频 → 文档 → 待归类 → 项目版本`;分区口径是 manifest `assets[].category`。**用户可经「编辑标签」面板的类型选择器改分类**(选择器显示读显示口径;写回:没碰过控件回传落盘原值、主动选过写用户选的值),改完卡片应落到新分类对应栏目、角标同步 | L50、L56、L324、L358–360、L539 | `[...document.querySelectorAll('.game-resource-book-thumbnail[data-resource-book-category]')].map(e=>e.dataset.resourceBookCategory)`;再数每栏 `.game-resource-card` 数量;角标读 `.game-resource-card-type-badge` 的 `data-resource-type` | **读时自愈**:落盘 `unclassified` 而 `kind` 能明确分类时按派生值显示(L360)。真机 57 条落盘 `unclassified` 在 UI 上应显示到正确栏目,**判定必须看 UI 计数,不能看 manifest 文件计数**。⚠ 反过来**不能**把自愈值当落盘值:判定「只改标签时分类有没有被改」必须比 manifest 文件(自愈只在读显示层,写回用 `gameCreationAppAssetPersistedCategory`)。⚠ 显式把 `kind` 已能明确分类的资产设为「待归类」会被自愈覆盖回派生栏目——已知盲区,不作为本用例的通过判据 |
-| **S6** 卡片预览台账 | 逐栏目看卡片是否显示真实主体 | 图片与安全 SVG 直接显示主体并保留透明棋盘底;视频、音频、文档、项目版本用稳定固定卡;失败显示类型占位、不挂破图、不降级为项目外 URL | L55、L61–62、**L67**、L347–349 | 见 5.1 的四条脚本:区分"没读 / 读失败 / 解码为空 / 不适用" | **可见卡停 `idle` 的真 bug 已修但未提交**(`useProjectResourceCardPreviews.ts`);未用新构建时会出现假失败 |
+| 步骤 | 操作 | 期望结果 | 对应 PRD 条款 | 怎么判"过了" | 已知例外 / 未做项 |
+| ------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **S4** 首轮生成 | 在右侧 Supervisor 对话里发起首轮素材生成 | 生成由**对话启动**(不用画布按钮);素材写入对应栏目并登记进 manifest;未生成的素材不显示为可编辑对象 | L25、L94、L500;飞书 §4 首轮生成 | 对话完成后 `.agent/manifest.json` 的 `assets[]` 长度增加;新卡 `data-resource-card-id` 为 `asset:` 形态 | 「首轮进度投影」只在 Issue 交付范围的"应该"清单出现、**无编码级判据**;现有可见性由底部 Agent 状态栏承载 |
+| **S5** 栏目归属 | 看资源总览的栏目缩略卡与各栏目卡片 | 固定 7 栏:`UI 交互 → 角色与对象 → 场景与环境 → 音频 → 文档 → 待归类 → 项目版本`;分区口径是 manifest `assets[].category`。**用户可经「编辑标签」面板的类型选择器改分类**(选择器显示读显示口径;写回:没碰过控件回传落盘原值、主动选过写用户选的值),改完卡片应落到新分类对应栏目、角标同步 | L50、L56、L324、L358–360、L539 | `[...document.querySelectorAll('.game-resource-book-thumbnail[data-resource-book-category]')].map(e=>e.dataset.resourceBookCategory)`;再数每栏 `.game-resource-card` 数量;角标读 `.game-resource-card-type-badge` 的 `data-resource-type` | **读时自愈**:落盘 `unclassified` 而 `kind` 能明确分类时按派生值显示(L360)。真机 57 条落盘 `unclassified` 在 UI 上应显示到正确栏目,**判定必须看 UI 计数,不能看 manifest 文件计数**。⚠ 反过来**不能**把自愈值当落盘值:判定「只改标签时分类有没有被改」必须比 manifest 文件(自愈只在读显示层,写回用 `gameCreationAppAssetPersistedCategory`)。⚠ 显式把 `kind` 已能明确分类的资产设为「待归类」会被自愈覆盖回派生栏目——已知盲区,不作为本用例的通过判据 |
+| **S6** 卡片预览台账 | 逐栏目看卡片是否显示真实主体 | 图片与安全 SVG 直接显示主体并保留透明棋盘底;视频、音频、文档、项目版本用稳定固定卡;失败显示类型占位、不挂破图、不降级为项目外 URL | L55、L61–62、**L67**、L347–349 | 见 5.1 的四条脚本:区分"没读 / 读失败 / 解码为空 / 不适用" | **可见卡停 `idle` 的真 bug 已修但未提交**(`useProjectResourceCardPreviews.ts`);未用新构建时会出现假失败 |
### C 阶段 · 资源管理闭环
-| 步骤 | 操作 | 期望结果 | 对应 PRD 条款 | 怎么判"过了" | 已知例外 / 未做项 |
-| --- | --- | --- | --- | --- | --- |
-| **S7** 栏目导航 | 点总览第 0 张「所有资源」卡 → 栏目缩略卡片进栏目 → 页内底部「下一页」 | 「所有资源」是入口(第 0 张);进入后**与栏目页是同一套画本场景**——卡片宿主同为 `.game-resource-book-scene-card`、每张卡可拖、可滚轮缩放平移,各栏目按**竖向分带**平铺在同一张画布上(展开态**不出每栏一条标题栏**,整个场景只有 `all` 那一条钉在视口上的标题栏,与栏目页"一条钉住的标题栏 + 一张平铺画布"同形);全部栏目(含空栏目)都可到达;左侧悬浮栏目大纲导航已删除;滚轮不切栏目 | L56、L512、L539–541 | `[data-resource-book-view="main"\|"child"]`、当前栏目 `.game-resource-book-scene-titlebar.is-active[data-resource-book-category]`(展开态这一条是 `data-resource-book-category="all"`,带「资源总览」收起入口;**展开态 `data-resource-book-category` 非 `all` 的标题栏一个都不该有**——空栏目曾因拿不到分带矩形而集体回落到世界原点叠成一摞)、卡片宿主 `.game-resource-book-scene-card[data-resource-book-category="<真实栏目>"]`(每张资源**恰好一个**宿主)、`[data-resource-book-section-scroll]`。**旧的独立网格判据已随分支删除**:`[data-resource-book-all-page="true"]` / `[data-resource-book-all-host="true"]` / `.game-resource-all-*` 都不应再出现。拖动的判据:在展开态拖一张卡,再进它所属栏目页看同一张卡**落在新位置**(两页共用同一份 `resource-layouts/{dependency,type}.json` 的栏目内局部坐标) | 「所有资源」卡渲染真实预览(`game-resource-book-preview-card`)**已提交**(`ea9668e4a`);旧构建只有占位摞。展开态的视口是内存态(不落盘),只有卡片坐标落盘 |
-| **S8** 搜索 / 筛选 | 点右下角 `搜索资源`(放大镜)按钮或按 `Ctrl/Cmd+F` → 看到关键词 / 所在区域 / 自定义标签三字段同屏 → 输入 → Esc | Dock 上只有这一颗按钮,叫出的是**唯一**的筛选浮层(不存在第二个只放关键词的搜索浮层);浮层收起**不清筛选条件**;`type=search` 原生 Esc 清空被 `preventDefault` 挡掉;条件生效时 Dock 按钮高亮 | L55、L322(筛选只隐藏卡片、不重排坐标)、L329–330、L336–337 | `button[aria-label="搜索资源"]` 的 `aria-expanded`、`title="搜索资源(Ctrl/Cmd+F)"`、`.is-active`;浮层 `.game-resource-filter-panel`(`role="dialog"` 名「筛选资源」)内三字段 `查找素材` / `所在区域` / `自定义标签`;浮层容器 `id` 等于按钮的 `aria-controls`;Esc 后浮层不在 DOM、条件仍生效、焦点回到该按钮 | 匹配字段 = **名称 / 路径 / 媒体类型 / 任务标题**,不含 `sourceLabel` |
-| **S9** 选择与多选 | 单击卡片 → `Shift`/`Ctrl`/`Meta` 再点 → 空白处拖拽框选 | 点卡 = 选中并在卡片旁浮出工具条;三种修饰键等效追加;框选可见选框 | L51、L61–62、L318–319 | `data-resource-card-id` + class 含 `is-selected` + `aria-pressed="true"`;浮出容器 `role="toolbar"`(图片 `aria-label="图片工具栏"`,音频 `"素材工具栏"`) | ⚠️ **PRD L61–62 要求的「打开详情」按钮在代码中不存在**(测试显式断言为 `null`),按 Issue #309 C3「取消资源详情面板与全屏编辑路由」判,不要判为缺失;真正入口是 `aria-label="选中资源:<栏目> <名>"` 按钮。**只读信息浮层已回归**(2026-09-11,工具条「信息」动作,见 `decision-log`):**C3 取消的仍只是可编辑详情面板与全屏编辑路由** |
-| **S10** 快速编辑(图片) | 选中 PNG/JPEG/WEBP 卡 → 工具条「快速编辑」→ 输入 → 提交(提示词下方另有「AI 润色」) | 先 `normalize_local_project_raster_resource`,再 `derive_local_project_resource(editKind='image-reference')` → 服务端 `/api/editor/images/edits`;**产出一张新素材**,源素材与源文件不变,新资源带 `referenceResourceIds` 血缘 | L123、L193、L476–478、L557;#309 C3/C10 | 提交后 `assets[]` +1、卡片 +1 且 `data-resource-card-id` 为新 id;**源卡仍在且内容未变**;工具条真渲染的只有 `quick-edit` 与 `download`;「AI 润色」`aria-label="AI 润色"` + `aria-busy`,成功回填并出现「恢复原文」,失败保留原文并给「AI 润色失败,可重试」 | ① 服务端白名单 17 项(`editor_project.rs:4120-4141`),`character-animation`/`video`/`audio`/`document`/`code` 等**仍会 400**;② 工具条 7 个动作(重绘 / 裁剪扩图 / 去背景 / 像素完美 / 切图集 / 提取 UI 素材 / 角色动画)按 opt-in **不渲染**;③ 润色结果超 `resourceEditPromptMaxLength` 会**截断并提示「已按长度上限截断」**;④ 提示词变了(改写或润色回填)必须换 `operationId`/幂等键,否则 Rust `request_fingerprint` 判「已绑定到不同资源编辑请求」 |
-| **S11** 画布生成入口(视频) | 画布级入口 → 只呈现视频 → 填名与提示词 → 提交(提示词下方另有「AI 润色」) | 无源新建视频 → 新卡产出并定位(音效 / 背景音乐已移到栏目工具栏,见 S11a) | 飞书 §4(首轮由对话启动)、L122 | 提交后 `assets[]` +1;面板是独立浮层(非在当前面板下方追加);「AI 润色」走 `polish_local_project_prompt` 并带上「这是素材生成提示词(类型)」的场景约束,成功回填、失败保留原文 | ⚠️ **Issue #309 写「画布生成入口未接」,但当前代码已接三类**(`resourceCanvasGenerationModel.ts` 仅 `video`/`sound-effect`/`background-music`);图片等类型会直接报「当前资源类型不支持无源生成」;润色结果超该类型上限(背景音乐 140 / 音效 1900 / 视频 4000)会截断并提示「已按长度上限截断」 |
-| **S12** 编辑标签 | 选中已登记素材 → 工具条「编辑标签」→ 增删标签 → 保存 | 面板**只编辑 `manifest.assets[].tags`**;`category` 读一次权威值后**原样回传**;顶部没有分类 chip 排 | L360(2026-09-11 收口)、#309 C2 | 面板标题「编辑素材标签」,底部只有「取消 / 保存标签」;保存后 `assets[n].tags` 变化而 `assets[n].category` **逐字不变** | 面板内的「删除资源」按钮保留(删除入口另见 S15 的选中卡工具条);「用户手动设置 category」不再放在本面板,改为独立入口(工具条「素材类型」与信息浮层「分类 → 设置」,选中即落盘,见 decision-log 2026-09-14) |
-| **S13** 重命名 | 工具条「重命名」→ 新文件名 → 确认 | 磁盘文件名与 manifest `localPath` 更新;**asset id 不变**;写盘失败回滚文件 | L362(资源身份稳定)、L515 | 弹窗 `ariaLabel="重命名素材"`;改名后「选中资源:…」按钮的无障碍名同步变化;引用 chip 与 @ 候选列表显示名刷新 | ⚠️ **不改游戏源码里对旧 `assets/` 的引用**,后果由用户自负 |
-| **S14** 下载 | 选中可下载卡 → 工具条「下载」 | 弹出**原生保存对话框**选目标路径 → 分块复制;取消即停止 | L507、飞书 §11.1.2 | 出现 `title="保存素材"` 的原生对话框;成功后状态提示含 `已保存 <路径>` | 语义是"另存为",不是静默直下;虚拟版本条目没有文件、不放行 |
-| **S15** 删除 | 工具条 / 标签面板「删除」→ 确认弹窗 | 三分支:① 无引用→直接删;② 被引用未勾选→只删素材、版本保留**悬空绑定**、界面不合成幽灵资源卡;③ 勾选→素材与该批版本在**同一次 manifest 写入**内一起删 | L413(版本只追加的唯一例外)、L532–533;#309 C5 | 弹窗 `ariaLabel="确认删除资源"`;被引用时出现 `被 N 个游戏版本使用` + 版本列表 + checkbox `把相关游戏版本一并删除`;无引用时该 body 整块不渲染 | **只摘登记、不删磁盘文件**;读引用命令精确名是 `read_local_project_asset_references` |
-| **S15a** 替换素材(面板内选 + 画布点选目标) | 选中被**当前版本**绑定的素材 → 工具条「替换素材」→ 画布右上角出现非模态候选面板(面板内可筛可选)→ 直接在画布上点目标素材(或点面板里的候选)→ 面板「确认」 | 面板确认与画布点选是**同一条**写入链路(`replace_local_project_version_resource`,载荷逐字一致):改该版本绑定、不建新版本;画布点选只把目标落成面板的当前选择,确认后才写入 | PRD §5.3 / §7.8 第 8 条;#309「直接替换」口径 | 面板判据:`role="dialog"` 名为「选择替换素材」、挂 `.image-canvas-editor__project-asset-picker--floating`、**没有** `.platform-overlay` 遮罩;点画布候选后面板里该候选 `aria-selected="true"`、面板里的搜索词与分类筛选保持原样、零写入;非法目标(源素材本身 / 不在权威候选里 / 分类不同 / 未登记资源)在面板里出现 `role="alert"`(「替换素材与源素材相同」/「替换素材未登记或已被删除」/「替换素材不兼容:分类不同」)且零写入;面板关闭 = 卸载;Esc 只收面板(画布全局 Esc 的清选中不随之触发) | 点选只能点**当前画布上可见**的卡:目标在别的栏目时先切栏目(替换会话跨栏目存活,不因「收起资源」或切栏目结束);面板开着时空白处仍可框选 / 平移,卡片单击语义的恢复发生在面板关闭之后 |
+| 步骤 | 操作 | 期望结果 | 对应 PRD 条款 | 怎么判"过了" | 已知例外 / 未做项 |
+| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **S7** 栏目导航 | 点总览第 0 张「所有资源」卡 → 栏目缩略卡片进栏目 → 页内底部「下一页」 | 「所有资源」是入口(第 0 张);进入后**与栏目页是同一套画本场景**——卡片宿主同为 `.game-resource-book-scene-card`、每张卡可拖、可滚轮缩放平移,各栏目按**竖向分带**平铺在同一张画布上(展开态**不出每栏一条标题栏**,整个场景只有 `all` 那一条钉在视口上的标题栏,与栏目页"一条钉住的标题栏 + 一张平铺画布"同形);全部栏目(含空栏目)都可到达;左侧悬浮栏目大纲导航已删除;滚轮不切栏目 | L56、L512、L539–541 | `[data-resource-book-view="main"\|"child"]`、当前栏目 `.game-resource-book-scene-titlebar.is-active[data-resource-book-category]`(展开态这一条是 `data-resource-book-category="all"`,带「资源总览」收起入口;**展开态 `data-resource-book-category` 非 `all` 的标题栏一个都不该有**——空栏目曾因拿不到分带矩形而集体回落到世界原点叠成一摞)、卡片宿主 `.game-resource-book-scene-card[data-resource-book-category="<真实栏目>"]`(每张资源**恰好一个**宿主)、`[data-resource-book-section-scroll]`。**旧的独立网格判据已随分支删除**:`[data-resource-book-all-page="true"]` / `[data-resource-book-all-host="true"]` / `.game-resource-all-*` 都不应再出现。拖动的判据:在展开态拖一张卡,再进它所属栏目页看同一张卡**落在新位置**(两页共用同一份 `resource-layouts/{dependency,type}.json` 的栏目内局部坐标) | 「所有资源」卡渲染真实预览(`game-resource-book-preview-card`)**已提交**(`ea9668e4a`);旧构建只有占位摞。展开态的视口是内存态(不落盘),只有卡片坐标落盘 |
+| **S8** 搜索 / 筛选 | 点右下角 `搜索资源`(放大镜)按钮或按 `Ctrl/Cmd+F` → 看到关键词 / 所在区域 / 自定义标签三字段同屏 → 输入 → Esc | Dock 上只有这一颗按钮,叫出的是**唯一**的筛选浮层(不存在第二个只放关键词的搜索浮层);浮层收起**不清筛选条件**;`type=search` 原生 Esc 清空被 `preventDefault` 挡掉;条件生效时 Dock 按钮高亮 | L55、L322(筛选只隐藏卡片、不重排坐标)、L329–330、L336–337 | `button[aria-label="搜索资源"]` 的 `aria-expanded`、`title="搜索资源(Ctrl/Cmd+F)"`、`.is-active`;浮层 `.game-resource-filter-panel`(`role="dialog"` 名「筛选资源」)内三字段 `查找素材` / `所在区域` / `自定义标签`;浮层容器 `id` 等于按钮的 `aria-controls`;Esc 后浮层不在 DOM、条件仍生效、焦点回到该按钮 | 匹配字段 = **名称 / 路径 / 媒体类型 / 任务标题**,不含 `sourceLabel` |
+| **S9** 选择与多选 | 单击卡片 → `Shift`/`Ctrl`/`Meta` 再点 → 空白处拖拽框选 | 点卡 = 选中并在卡片旁浮出工具条;三种修饰键等效追加;框选可见选框 | L51、L61–62、L318–319 | `data-resource-card-id` + class 含 `is-selected` + `aria-pressed="true"`;浮出容器 `role="toolbar"`(图片 `aria-label="图片工具栏"`,音频 `"素材工具栏"`) | ⚠️ **PRD L61–62 要求的「打开详情」按钮在代码中不存在**(测试显式断言为 `null`),按 Issue #309 C3「取消资源详情面板与全屏编辑路由」判,不要判为缺失;真正入口是 `aria-label="选中资源:<栏目> <名>"` 按钮。**只读信息浮层已回归**(2026-09-11,工具条「信息」动作,见 `decision-log`):**C3 取消的仍只是可编辑详情面板与全屏编辑路由** |
+| **S10** 快速编辑(图片) | 选中 PNG/JPEG/WEBP 卡 → 工具条「快速编辑」→ 输入 → 提交(提示词下方另有「AI 润色」) | 先 `normalize_local_project_raster_resource`,再 `derive_local_project_resource(editKind='image-reference')` → 服务端 `/api/editor/images/edits`;**产出一张新素材**,源素材与源文件不变,新资源带 `referenceResourceIds` 血缘 | L123、L193、L476–478、L557;#309 C3/C10 | 提交后 `assets[]` +1、卡片 +1 且 `data-resource-card-id` 为新 id;**源卡仍在且内容未变**;工具条真渲染的只有 `quick-edit` 与 `download`;「AI 润色」`aria-label="AI 润色"` + `aria-busy`,成功回填并出现「恢复原文」,失败保留原文并给「AI 润色失败,可重试」 | ① 服务端白名单 17 项(`editor_project.rs:4120-4141`),`character-animation`/`video`/`audio`/`document`/`code` 等**仍会 400**;② 工具条 7 个动作(重绘 / 裁剪扩图 / 去背景 / 像素完美 / 切图集 / 提取 UI 素材 / 角色动画)按 opt-in **不渲染**;③ 润色结果超 `resourceEditPromptMaxLength` 会**截断并提示「已按长度上限截断」**;④ 提示词变了(改写或润色回填)必须换 `operationId`/幂等键,否则 Rust `request_fingerprint` 判「已绑定到不同资源编辑请求」 |
+| **S11** 画布生成入口(视频) | 画布级入口 → 只呈现视频 → 填名与提示词 → 提交(提示词下方另有「AI 润色」) | 无源新建视频 → 新卡产出并定位(音效 / 背景音乐已移到栏目工具栏,见 S11a) | 飞书 §4(首轮由对话启动)、L122 | 提交后 `assets[]` +1;面板是独立浮层(非在当前面板下方追加);「AI 润色」走 `polish_local_project_prompt` 并带上「这是素材生成提示词(类型)」的场景约束,成功回填、失败保留原文 | ⚠️ **Issue #309 写「画布生成入口未接」,但当前代码已接三类**(`resourceCanvasGenerationModel.ts` 仅 `video`/`sound-effect`/`background-music`);图片等类型会直接报「当前资源类型不支持无源生成」;润色结果超该类型上限(背景音乐 140 / 音效 1900 / 视频 4000)会截断并提示「已按长度上限截断」 |
+| **S12** 编辑标签 | 选中已登记素材 → 工具条「编辑标签」→ 增删标签 → 保存 | 面板**只编辑 `manifest.assets[].tags`**;`category` 读一次权威值后**原样回传**;顶部没有分类 chip 排 | L360(2026-09-11 收口)、#309 C2 | 面板标题「编辑素材标签」,底部只有「取消 / 保存标签」;保存后 `assets[n].tags` 变化而 `assets[n].category` **逐字不变** | 面板内的「删除资源」按钮保留(删除入口另见 S15 的选中卡工具条);「用户手动设置 category」不再放在本面板,改为独立入口(工具条「素材类型」与信息浮层「分类 → 设置」,选中即落盘,见 decision-log 2026-09-14) |
+| **S13** 重命名 | 工具条「重命名」→ 新文件名 → 确认 | 磁盘文件名与 manifest `localPath` 更新;**asset id 不变**;写盘失败回滚文件 | L362(资源身份稳定)、L515 | 弹窗 `ariaLabel="重命名素材"`;改名后「选中资源:…」按钮的无障碍名同步变化;引用 chip 与 @ 候选列表显示名刷新 | ⚠️ **不改游戏源码里对旧 `assets/` 的引用**,后果由用户自负 |
+| **S14** 下载 | 选中可下载卡 → 工具条「下载」 | 弹出**原生保存对话框**选目标路径 → 分块复制;取消即停止 | L507、飞书 §11.1.2 | 出现 `title="保存素材"` 的原生对话框;成功后状态提示含 `已保存 <路径>` | 语义是"另存为",不是静默直下;虚拟版本条目没有文件、不放行 |
+| **S15** 删除 | 工具条 / 标签面板「删除」→ 确认弹窗 | 三分支:① 无引用→直接删;② 被引用未勾选→只删素材、版本保留**悬空绑定**、界面不合成幽灵资源卡;③ 勾选→素材与该批版本在**同一次 manifest 写入**内一起删 | L413(版本只追加的唯一例外)、L532–533;#309 C5 | 弹窗 `ariaLabel="确认删除资源"`;被引用时出现 `被 N 个游戏版本使用` + 版本列表 + checkbox `把相关游戏版本一并删除`;无引用时该 body 整块不渲染 | **只摘登记、不删磁盘文件**;读引用命令精确名是 `read_local_project_asset_references` |
+| **S15a** 替换素材(面板内选 + 画布点选目标) | 选中被**当前版本**绑定的素材 → 工具条「替换素材」→ 画布右上角出现非模态候选面板(面板内可筛可选)→ 直接在画布上点目标素材(或点面板里的候选)→ 面板「确认」 | 面板确认与画布点选是**同一条**写入链路(`replace_local_project_version_resource`,载荷逐字一致):改该版本绑定、不建新版本;画布点选只把目标落成面板的当前选择,确认后才写入 | PRD §5.3 / §7.8 第 8 条;#309「直接替换」口径 | 面板判据:`role="dialog"` 名为「选择替换素材」、挂 `.image-canvas-editor__project-asset-picker--floating`、**没有** `.platform-overlay` 遮罩;点画布候选后面板里该候选 `aria-selected="true"`、面板里的搜索词与分类筛选保持原样、零写入;非法目标(源素材本身 / 不在权威候选里 / 分类不同 / 未登记资源)在面板里出现 `role="alert"`(「替换素材与源素材相同」/「替换素材未登记或已被删除」/「替换素材不兼容:分类不同」)且零写入;面板关闭 = 卸载;Esc 只收面板(画布全局 Esc 的清选中不随之触发) | 点选只能点**当前画布上可见**的卡:目标在别的栏目时先切栏目(替换会话跨栏目存活,不因「收起资源」或切栏目结束);面板开着时空白处仍可框选 / 平移,卡片单击语义的恢复发生在面板关闭之后 |
| **S11a** 栏目画布底部工具栏 | 进「UI 交互 / 角色与对象 / 场景与环境 / 音频」任一栏目 → 点左下角工具栏里的入口生成 → 「收起资源」回总览看工具栏消失 | 工具栏只在矩阵四个栏目(功能画布)渲染;图片类入口走 `start_local_project_asset_generation`(提交即返回、生成在后台跑),音频入口走同一条命令的音频载荷,上传复用 `upload_local_asset`;生成 / 上传成功后走既有 manifest 刷新与资源定位 | PRD §3.10、§7.9 | DOM 判据 `[data-resource-bottom-toolbar=""]`(资源总览、「所有资源」展开态、文档、待归类、项目版本都**没有**这个节点);工具栏是 `.game-resource-book-manager` 的直接子节点(`closest('.game-resource-book-scene')` 为 `null`);每个入口一次 `start_local_project_asset_generation`:图片类载荷逐字为 `{ projectPath, projectId, taskId, kind, prompt, aspectRatio, imageSize, assetName, outputPath }`(`kind` 映射见 PRD §7.9 第 3 条;`taskId` 是前端每次提交新铸的本地任务 id),音频类载荷为 `{ projectPath, projectId, taskId, kind, prompt, assetName, idempotencyKey }`(`taskId` 即该次生成的 operation id,不发图片类那套比例 / 尺寸 / 参考 / 落点参数);该命令提交即返回,之后有 `list_local_project_asset_generations` 轮询与 `get_local_game_project_revision` + `get_local_game_manifest` 的配对读;点「生成」即把这次输入交给后台账本并**同步关闭**面板(不等 IPC、不等排队、不等生成,画布立即恢复可交互;面板 DOM 里没有阶段文案与「后台运行并关闭」这类在途按钮),关闭不等于取消,关闭后任务仍出现在「生成任务」面板(入口按钮 `aria-label="生成任务"`,非模态浮层、无 `aria-modal`)并显示后端 `phaseDetail`;第一条未终态时提交第二条 → 第二条显示「排队中。」且生成提交 IPC 次数仍为 1,第一条终态后自动补发(次数变 2);缺 `assets/art-spec.png` 时「生成图标素材 / 生成 UI 设计图」仍可点击(`aria-disabled="true"` 但**不是**原生 disabled)并给出含该路径的 `role="alert"` 原因、零生成请求;音频入口面板标题即「生成背景音乐」/「生成音效」且没有类型选择器 | ① 本地通道没有 `model` / `specType` / `replaceExisting` 入参:面板不渲染模型选择器,角色规范与自定义规范共用 `spec` 通道(靠 `assetName` 与提示词区分),「图标规范」在项目已有权威规范图时不再指向 `assets/art-spec.png`(否则会被 Rust 的防覆盖校验硬拒);② 本轮不做生成视频 / 宣发素材 / 生成游戏场景 / 选择工具 / 抓手工具;③ 既有「生成素材」浮层入口只保留生成视频,音频入口只在音频栏目工具栏出现 |
### D 阶段 · 版本与运行
-| 步骤 | 操作 | 期望结果 | 对应 PRD 条款 | 怎么判"过了" | 已知例外 / 未做项 |
-| --- | --- | --- | --- | --- | --- |
-| **S16** 版本切换 | 运行模块**右上角**版本选择器 → 切到另一版本 | 触发钮显示当前版本;切换后资源卡「当前使用」高亮与 **@ 面板「当前版本素材」同步更新**,并重载当前预览 | L165、L190、L324、L530;#309 C7 | `aria-label="当前版本: