自查修正:拖到对话落点逐值比较、替换面板锚进画布容器并与任务面板互斥

- 拖动落点改逐值比较 sameResourceCardReferenceDrop(条数 + 对话栏矩形四值),指针在对话栏上移动不再每帧 setState、不再每帧重渲染工作台。
- 替换面板从 stage 顶层移进画布容器 .game-resource-book-manager(本身 position: relative),top 3.2rem 排在任务开关下方;相对 stage 写死的偏移会在工具条换行时压上去,与刚修的任务开关是同一类隐患。
- 打开替换会话时收起「生成任务」面板:画布右上角同一时刻只留一块浮层(任务照旧在账本里推进)。
- 面板上方补一行「替换源素材:<显示名>」:非模态之后画布上的源素材光环会被空白点击清掉,源身份得在面板这一侧留一份可读说法。
- 测试:替换用例新增「面板锚在画布容器里、写明源素材、并与任务面板互斥」(20 passed)、落点模型新增逐值比较用例(6 passed)。
- 文档:decision-log 新增本批自查条目、待办文档补登记、验收用例 C9 注脚改为 nonModal 形态。
This commit is contained in:
2026-09-21 06:21:19 +08:00
parent 7148ff9eaf
commit 5467ed6d7b
8 changed files with 362 additions and 184 deletions
@@ -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。
@@ -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({
)}
</div>
</div>
{/*
****
****
stage stage
`position: relative`
**线** / / LRU
线 `sourceUrl` 退
*/}
<div className="platform-theme platform-theme--light game-resource-replacement-panel">
{resourceReplacementSourceLabel ? (
<p
className="game-resource-replacement-panel-source"
role="note"
>
{`替换源素材:${resourceReplacementSourceLabel}`}
</p>
) : null}
<ImageCanvasProjectAssetPickerDialog
open={resourceReplacementOpen}
assets={resourceReplacementPickerEntries}
selectedAssetIds={
resourceReplacementPick
? [resourceReplacementPick.assetId]
: []
}
initialSelectionRevision={
resourceReplacementPick?.revision ?? 0
}
singleSelect
nonModal
selectionNoun="替换素材"
assetBlockedReasons={resourceReplacementBlockedReasonMap}
assetHints={resourceReplacementHintMap}
errorMessage={resourceReplacementError}
renderAssetMedia={(asset) => {
const resource =
resourceByManifestAssetId.get(asset.id) ?? null;
const previewIdentity = resource
? (resourceCardPreviews.identityByResourceId.get(
resource.id,
) ?? null)
: null;
return (
<ResourcePreviewMedia
resource={resource}
previewIdentity={previewIdentity}
preview={
previewIdentity
? (resourceCardPreviews.previews.get(
previewIdentity,
) ?? resourceCardPreviews.idlePreview)
: resourceCardPreviews.idlePreview
}
onObservePreview={resourceCardPreviews.observePreview}
onPreviewDecodeError={handleCardPreviewDecodeError}
fallbackMediaType={asset.mediaType}
/>
);
}}
onCancel={closeResourceVersionReplacement}
onConfirm={(assetIds) =>
void confirmResourceVersionReplacement(assetIds)
}
/>
</div>
</div>
</>
) : (
@@ -10779,57 +10879,6 @@ export default function ProjectDevelopmentView({
uiEditorRoute ? 'editor' : mode === 'run' ? 'run' : 'canvas'
}
/>
{/*
****
**线** / / LRU
线 `sourceUrl` 退
*/}
<div className="platform-theme platform-theme--light game-resource-replacement-panel">
<ImageCanvasProjectAssetPickerDialog
open={resourceReplacementOpen}
assets={resourceReplacementPickerEntries}
selectedAssetIds={
resourceReplacementPick ? [resourceReplacementPick.assetId] : []
}
initialSelectionRevision={resourceReplacementPick?.revision ?? 0}
singleSelect
nonModal
selectionNoun="替换素材"
assetBlockedReasons={resourceReplacementBlockedReasonMap}
assetHints={resourceReplacementHintMap}
errorMessage={resourceReplacementError}
renderAssetMedia={(asset) => {
const resource =
resourceByManifestAssetId.get(asset.id) ?? null;
const previewIdentity = resource
? (resourceCardPreviews.identityByResourceId.get(
resource.id,
) ?? null)
: null;
return (
<ResourcePreviewMedia
resource={resource}
previewIdentity={previewIdentity}
preview={
previewIdentity
? (resourceCardPreviews.previews.get(previewIdentity) ??
resourceCardPreviews.idlePreview)
: resourceCardPreviews.idlePreview
}
onObservePreview={resourceCardPreviews.observePreview}
onPreviewDecodeError={handleCardPreviewDecodeError}
fallbackMediaType={asset.mediaType}
/>
);
}}
onCancel={closeResourceVersionReplacement}
onConfirm={(assetIds) =>
void confirmResourceVersionReplacement(assetIds)
}
/>
</div>
</section>
{!uiEditorRoute ? (
@@ -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
);
}
/**
* 拖到对话要插入的引用。
*
@@ -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);
});
});
@@ -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` 函数),
* 载荷必须逐字一致,成功后关闭面板并标注血缘。
@@ -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 packDirectProject 通过原生 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 最近项目检查保持项目级隔离
- 背景:最近项目刷新会重新检查所有路径。若其中一个目录损坏、超时或不可读,清空整张状态表会让已确认正常的项目暂时全部显示“检查中”,用户只能移除坏项目后看到列表恢复。
@@ -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)只有声明级断言;「复制对话保留有效引用」未做。
@@ -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` 的**陈旧记录**不得把你直接丢进运行界面 | L145146、**L165**`run` 自动进入只认内存 registry 活体确认) | `document.querySelector('.game-workbench-stage')?.dataset.resourceViewState``resources.list` / `resources.selected.<category>` / `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` 的**陈旧记录**不得把你直接丢进运行界面 | L145146、**L165**`run` 自动进入只认内存 registry 活体确认) | `document.querySelector('.game-workbench-stage')?.dataset.resourceViewState``resources.list` / `resources.selected.<category>` / `resources.ui-editor`**该属性为 `undefined` 即说明已在 run 视图** | 陈旧记录对齐:命中"记录 running 但 registry 没跑"时调 `stop_local_game_preview` 并按真相投影 `{status:'stopped'}``sessionPreview.ts:87-101` |
| **S3** 项目页面板(可跳过) | 「项目组」页:搜索、行尾更多菜单、Escape 关闭 | 表格只显示权威数据列;Escape 关菜单并把焦点还给触发按钮;移除只改本机最近项目记录、不动磁盘 | L135139 | 无匹配状态提供「清除搜索」;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:<id>` 形态 | 「首轮进度投影」只在 Issue 交付范围的"应该"清单出现、**无编码级判据**;现有可见性由底部 Agent 状态栏承载 |
| **S5** 栏目归属 | 看资源总览的栏目缩略卡与各栏目卡片 | 固定 7 栏:`UI 交互 → 角色与对象 → 场景与环境 → 音频 → 文档 → 待归类 → 项目版本`;分区口径是 manifest `assets[].category`。**用户可经「编辑标签」面板的类型选择器改分类**(选择器显示读显示口径;写回:没碰过控件回传落盘原值、主动选过写用户选的值),改完卡片应落到新分类对应栏目、角标同步 | L50、L56、L324、L358360、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、L6162、**L67**、L347349 | 见 5.1 的四条脚本:区分"没读 / 读失败 / 解码为空 / 不适用" | **可见卡停 `idle` 的真 bug 已修但未提交**`useProjectResourceCardPreviews.ts`);未用新构建时会出现假失败 |
| 步骤 | 操作 | 期望结果 | 对应 PRD 条款 | 怎么判"过了" | 已知例外 / 未做项 |
| ------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **S4** 首轮生成 | 在右侧 Supervisor 对话里发起首轮素材生成 | 生成由**对话启动**(不用画布按钮);素材写入对应栏目并登记进 manifest;未生成的素材不显示为可编辑对象 | L25、L94、L500;飞书 §4 首轮生成 | 对话完成后 `.agent/manifest.json``assets[]` 长度增加;新卡 `data-resource-card-id``asset:<id>` 形态 | 「首轮进度投影」只在 Issue 交付范围的"应该"清单出现、**无编码级判据**;现有可见性由底部 Agent 状态栏承载 |
| **S5** 栏目归属 | 看资源总览的栏目缩略卡与各栏目卡片 | 固定 7 栏:`UI 交互 → 角色与对象 → 场景与环境 → 音频 → 文档 → 待归类 → 项目版本`;分区口径是 manifest `assets[].category`。**用户可经「编辑标签」面板的类型选择器改分类**(选择器显示读显示口径;写回:没碰过控件回传落盘原值、主动选过写用户选的值),改完卡片应落到新分类对应栏目、角标同步 | L50、L56、L324、L358360、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、L6162、**L67**、L347349 | 见 5.1 的四条脚本:区分"没读 / 读失败 / 解码为空 / 不适用" | **可见卡停 `idle` 的真 bug 已修但未提交**`useProjectResourceCardPreviews.ts`);未用新构建时会出现假失败 |
### C 阶段 · 资源管理闭环
| 步骤 | 操作 | 期望结果 | 对应 PRD 条款 | 怎么判"过了" | 已知例外 / 未做项 |
| --- | --- | --- | --- | --- | --- |
| **S7** 栏目导航 | 点总览第 0 张「所有资源」卡 → 栏目缩略卡片进栏目 → 页内底部「下一页」 | 「所有资源」是入口(第 0 张);进入后**与栏目页是同一套画本场景**——卡片宿主同为 `.game-resource-book-scene-card`、每张卡可拖、可滚轮缩放平移,各栏目按**竖向分带**平铺在同一张画布上(展开态**不出每栏一条标题栏**,整个场景只有 `all` 那一条钉在视口上的标题栏,与栏目页"一条钉住的标题栏 + 一张平铺画布"同形);全部栏目(含空栏目)都可到达;左侧悬浮栏目大纲导航已删除;滚轮不切栏目 | L56、L512、L539541 | `[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、L336337 | `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、L6162、L318319 | `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、L476478、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 排 | L3602026-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/<name>` 的引用**,后果由用户自负 |
| **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、L539541 | `[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、L336337 | `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、L6162、L318319 | `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、L476478、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 排 | L3602026-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/<name>` 的引用**,后果由用户自负 |
| **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="<category>"]`(资源总览、「所有资源」展开态、文档、待归类、项目版本都**没有**这个节点);工具栏是 `.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="当前版本:<label>"`、菜单 `role="listbox" aria-label="切换游戏版本"`、选项 `role="option" aria-selected``[data-used-by-current-version="true"]` 集合随切换变化 | **C7 只做记录层 + UI 层**;"历史版本独有资源集合被运行时按版本加载"需新增版本化资源解析机制,**本轮未实现**;版本卡卡面**不渲染**「项目修订 / 父版本」文字 |
| **S17** 播放 | 顶部「播放」 | 直接切运行视图并启动本地预览;**不再弹 `game.run_local` 二次确认**;预览占满中央可用区、无 iframe 滚动条 | L16、L7172、L122、L493 | `data-resource-view-state` 变为 `undefined``await window.__TAURI__.core.invoke('get_local_game_preview_status',{projectPath})``status==='running'` 且有 `url`(形如 `http://127.0.0.1:<随机端口>/` | 播放按钮 `disabled={!runAvailable \|\| !onPlay \|\| uiEditorRoute!==null}`,**不可用时点不动**;不可用提示实现文案为「首个可运行原型尚未完成,运行视图暂不可用」,与 PRD L165 表述不同(见 7.4 |
| **S18** 停止与退出收尾 | 聊天侧工程操作组点「停止预览」;或关闭客户端 | 预览停止、manifest `preview` 对齐 `stopped`(清 url/port);退出时也统一收尾 | L165 | 停止后 `get_local_game_preview_status` 非 running`.agent/logs/preview.log` 追加 `stopped`;退出后重开同一项目应落回 `resource-overview` | 停止按钮**不在**运行视图工具栏,在工作台右侧工程操作组;退出失败会打 `preview.gui_exit.stop_failed` |
| 步骤 | 操作 | 期望结果 | 对应 PRD 条款 | 怎么判"过了" | 已知例外 / 未做项 |
| ---------------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **S16** 版本切换 | 运行模块**右上角**版本选择器 → 切到另一版本 | 触发钮显示当前版本;切换后资源卡「当前使用」高亮与 **@ 面板「当前版本素材」同步更新**,并重载当前预览 | L165、L190、L324、L530#309 C7 | `aria-label="当前版本:<label>"`、菜单 `role="listbox" aria-label="切换游戏版本"`、选项 `role="option" aria-selected``[data-used-by-current-version="true"]` 集合随切换变化 | **C7 只做记录层 + UI 层**;"历史版本独有资源集合被运行时按版本加载"需新增版本化资源解析机制,**本轮未实现**;版本卡卡面**不渲染**「项目修订 / 父版本」文字 |
| **S17** 播放 | 顶部「播放」 | 直接切运行视图并启动本地预览;**不再弹 `game.run_local` 二次确认**;预览占满中央可用区、无 iframe 滚动条 | L16、L7172、L122、L493 | `data-resource-view-state` 变为 `undefined``await window.__TAURI__.core.invoke('get_local_game_preview_status',{projectPath})``status==='running'` 且有 `url`(形如 `http://127.0.0.1:<随机端口>/` | 播放按钮 `disabled={!runAvailable \|\| !onPlay \|\| uiEditorRoute!==null}`,**不可用时点不动**;不可用提示实现文案为「首个可运行原型尚未完成,运行视图暂不可用」,与 PRD L165 表述不同(见 7.4 |
| **S18** 停止与退出收尾 | 聊天侧工程操作组点「停止预览」;或关闭客户端 | 预览停止、manifest `preview` 对齐 `stopped`(清 url/port);退出时也统一收尾 | L165 | 停止后 `get_local_game_preview_status` 非 running`.agent/logs/preview.log` 追加 `stopped`;退出后重开同一项目应落回 `resource-overview` | 停止按钮**不在**运行视图工具栏,在工作台右侧工程操作组;退出失败会打 `preview.gui_exit.stop_failed` |
### E 阶段 · 对话侧
| 步骤 | 操作 | 期望结果 | 对应 PRD 条款 | 怎么判"过了" | 已知例外 / 未做项 |
| --- | --- | --- | --- | --- | --- |
| **S19** @ 引用 | 输入区「@」→ 两个页签 + 筛选(功能分类 + 多标签)+ 搜索 + 多选 → 「添加到对话」 | 两页签**各自独立**的搜索与筛选状态(含各自一份标签库),**不影响**资源画布筛选;已选素材不因筛选或搜索隐藏而被取消;chip 整体插入 | L187、L420426、L453461#309 C4 | `role="tablist"``aria-label="素材范围"`)内两个 `role="tab"``当前版本素材` / `全部画布素材`;输入框 aria-label 分别为 `搜索当前版本素材` / `搜索全部画布素材`;标签行是共享筛选条里 `aria-label="素材筛选标签"` 的分组,chip 用 `aria-pressed` 表达选中;chip = `span.resource-reference-chip[data-resource-reference-id]` | **chip 原子性有正式定义**chip 是 Lexical `DecoratorNode`,纯文本表示固定占 **1 个字符** `U+FFFC` ⇒ 一次 Backspace 只能整体删掉一个 chip,不可能删半截;引用身份是稳定 `resourceId`,改名后按 id 刷新显示名。**多标签是 AND**(口径来自 `packages/shared``assetTagsMatchSelection`,标签取自 manifest `assets[].tags`),并与搜索词、功能分类三者叠加 |
| **S19a** 快速编辑内 @ 引用 | 资源卡「快速编辑」→ 提示词输入区「插入素材引用」或直接输入 `@` → 选素材 → 「修改」 | 快速编辑提示词输入区与聊天输入区**同一套**引用输入区:候选、插入动作、引用模型一致;提示词文本里的引用与聊天同字面量(`@显示名`),提交后该文本原样进入 `derive_local_project_resource``prompt` | #309 C4 / C10 | 快速编辑面板内存在 `aria-label="插入素材引用"``aria-label="快速编辑提示词"`;插入后编辑区出现 `[data-resource-reference-id]` chip;派生请求 `prompt` 等于 `文本@显示名` | 该输入区不渲染内置「AI 润色」(润色仍由面板的「AI 润色」承担,带资源编辑场景上下文与长度上限);多行输入区的 Enter 只换行、不提交;点 `@` 选择器里的候选不会被判成「点外部」而收起面板 |
| **S20** 润色与发送前提醒 | ① 点「AI 润色」→ 看回填与「恢复原文」;② 直接发送(≥40 字)→ 处理提醒弹窗三条动作与「不再提醒」 | 润色只优化文本、不发送不提交;第一次成功润色落下原文快照、可重复润色、失败保留原文;未润色直接发送时弹「发送前提醒」 | L120(气泡对比度)、飞书 §12.1/12.2;#309 C8 | 润色按钮 `aria-label="AI 润色"` + `aria-busy`;出现「恢复原文」按钮;提醒弹窗为 portal `role="dialog"`、标题「发送前提醒」、动作 `关闭` / `使用原文提交` / `AI 润色` + checkbox `不再提醒`;偏好存 `localStorage['agc.chat.prompt-polish-reminder.disabled']` | 触发门禁:trim 后 ≥40 字、不以 `/` 开头、本轮未润色未确认、未勾「不再提醒」。计费不自建,走 server-rs `/api/llm/chat/completions``/api/llm/responses`;「1 泥点」这个数字源码中未硬编码 |
| 步骤 | 操作 | 期望结果 | 对应 PRD 条款 | 怎么判"过了" | 已知例外 / 未做项 |
| -------------------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **S19** @ 引用 | 输入区「@」→ 两个页签 + 筛选(功能分类 + 多标签)+ 搜索 + 多选 → 「添加到对话」 | 两页签**各自独立**的搜索与筛选状态(含各自一份标签库),**不影响**资源画布筛选;已选素材不因筛选或搜索隐藏而被取消;chip 整体插入 | L187、L420426、L453461#309 C4 | `role="tablist"``aria-label="素材范围"`)内两个 `role="tab"``当前版本素材` / `全部画布素材`;输入框 aria-label 分别为 `搜索当前版本素材` / `搜索全部画布素材`;标签行是共享筛选条里 `aria-label="素材筛选标签"` 的分组,chip 用 `aria-pressed` 表达选中;chip = `span.resource-reference-chip[data-resource-reference-id]` | **chip 原子性有正式定义**chip 是 Lexical `DecoratorNode`,纯文本表示固定占 **1 个字符** `U+FFFC` ⇒ 一次 Backspace 只能整体删掉一个 chip,不可能删半截;引用身份是稳定 `resourceId`,改名后按 id 刷新显示名。**多标签是 AND**(口径来自 `packages/shared``assetTagsMatchSelection`,标签取自 manifest `assets[].tags`),并与搜索词、功能分类三者叠加 |
| **S19a** 快速编辑内 @ 引用 | 资源卡「快速编辑」→ 提示词输入区「插入素材引用」或直接输入 `@` → 选素材 → 「修改」 | 快速编辑提示词输入区与聊天输入区**同一套**引用输入区:候选、插入动作、引用模型一致;提示词文本里的引用与聊天同字面量(`@显示名`),提交后该文本原样进入 `derive_local_project_resource``prompt` | #309 C4 / C10 | 快速编辑面板内存在 `aria-label="插入素材引用"``aria-label="快速编辑提示词"`;插入后编辑区出现 `[data-resource-reference-id]` chip;派生请求 `prompt` 等于 `文本@显示名` | 该输入区不渲染内置「AI 润色」(润色仍由面板的「AI 润色」承担,带资源编辑场景上下文与长度上限);多行输入区的 Enter 只换行、不提交;点 `@` 选择器里的候选不会被判成「点外部」而收起面板 |
| **S20** 润色与发送前提醒 | ① 点「AI 润色」→ 看回填与「恢复原文」;② 直接发送(≥40 字)→ 处理提醒弹窗三条动作与「不再提醒」 | 润色只优化文本、不发送不提交;第一次成功润色落下原文快照、可重复润色、失败保留原文;未润色直接发送时弹「发送前提醒」 | L120(气泡对比度)、飞书 §12.1/12.2;#309 C8 | 润色按钮 `aria-label="AI 润色"` + `aria-busy`;出现「恢复原文」按钮;提醒弹窗为 portal `role="dialog"`、标题「发送前提醒」、动作 `关闭` / `使用原文提交` / `AI 润色` + checkbox `不再提醒`;偏好存 `localStorage['agc.chat.prompt-polish-reminder.disabled']` | 触发门禁:trim 后 ≥40 字、不以 `/` 开头、本轮未润色未确认、未勾「不再提醒」。计费不自建,走 server-rs `/api/llm/chat/completions``/api/llm/responses`;「1 泥点」这个数字源码中未硬编码 |
### F 阶段 · 台账核对
| 步骤 | 操作 | 期望结果 | 对应 PRD 条款 | 怎么判"过了" | 已知例外 / 未做项 |
| --- | --- | --- | --- | --- | --- |
| 步骤 | 操作 | 期望结果 | 对应 PRD 条款 | 怎么判"过了" | 已知例外 / 未做项 |
| ---------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| **S21** 全程台账 | 不刷新、不重开项目,回看资源画布与布局 | 新增资源即时投影;两套布局(dependency / type)各自坐标独立、历史 `manuallyPlaced=true` 保留;搜索与详情开关不重置 viewport | L4751、L262265、L320323、L327329、L500 | 两份 sidecar 都在 `.agent/workbench/resource-layouts/{dependency,type}.json`;拖动超过 5px 阈值才提交一次 CAS,成功后 `revision` 递增;**存量项目「第一次打开」时读路径也会写回一次**(旧 `art` / `code` 分区坐标归并到新分区,`revision` +1),并给一次性提示条,条数含**无法对齐被跳过的坐标**(`data-resource-canvas-layout-*`),所以「`revision` 从 1 开始」不是异常 | 双窗口同 revision 的 CAS 冲突(L509)需开两个客户端窗口才可验;`A→B→A` 复用 epoch 的取消语义只能靠定向测试 |
## 4. 前置条件清单
| 项 | 结论 | 依据 |
| --- | --- | --- |
| **是否要重启客户端 / 重新构建** | dev 态 `npm run agc``devUrl`(默认 3080,运行时被覆盖为实际端口),前端改动走 Vite HMR、**不需要 build****Rust 侧改动必须重启客户端**;未提交改动若不在当前进程里,也必须重启一次客户端 | `apps/ai-game-creator-shell/src-tauri/tauri.conf.json:6-11``scripts/start-tauri-dev.mjs:26-44` |
| **是否要重新构建产物** | 只有 `agc:build` / `ai-game-creator-shell:build` 才产出 `../dist`;若验的是打包后的客户端,改前端必须重新 build 并重装 | `tauri.conf.json``frontendDist: "../dist"` |
| **是否要重启 api-server** | **服务端改动需要重启**。本地 `.env` / `.env.local` / `.env.secrets.local` 修改后必须重启 `api-server`;已在 `npm run dev` 终端里可直接敲 `rs api-server`provider / worker / fallback 配置改动要按角色重启对应进程 | `docs/【开发运维】本地开发验证与生产运维-2026-05-15.md:114/136/209/215/809` |
| **启动命令** | 客户端:`npm run agc`(等价 `agc:dev` / `ai-game-creator-shell:dev`);只起前后端不起窗口:`npm run agc:serve`;后端:`npm run dev:api-server` | `package.json:20/155-159/169/22` |
| **端口与健康检查** | api-server 默认 `127.0.0.1:8082`,健康检查 `GET /healthz`(先查 BgFilter worker `/readyz`);**端口会漂移,实际值以 `.app/dev-stack.json` 为准** | `scripts/dev.mjs:237``scripts/start-dev-stack.mjs:28-29/351-353` |
| **登录与余额** | 需要登录;**不需要余额也能进工作台与资源画布**,消耗泥点的是付费生成与 Agent 请求 | `src/app/AuthenticatedClient.tsx:546/568-574``index.tsx:500` |
| **测试项目** | 真机 `What do u wanna do kitten` 已有 61 assets / 5 versions、两份布局 sidecar 齐全,适合当**存量数据**验收对象(只读);要验删除与版本三分支时建议另建新项目 | 只读统计 |
| **门禁命令** | `npm run ai-game-creator-shell:typecheck``npm run test -- apps/ai-game-creator-shell/tests``npm run check:encoding``git diff --check`Rust 侧 `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml -- --test-threads=1` | `package.json:123/164/193-194/63` |
| **build-gate 代理假红** | 本地带系统代理跑 `npm run build` 必假红:`NODE_USE_ENV_PROXY=1``HTTP_PROXY/https_proxy` 会触发 `[UNDICI-EHPA] Warning: EnvHttpProxyAgent is experimental`,不被 `ExperimentalWarning` 忽略规则放行。跑前清空 `NODE_USE_ENV_PROXY/HTTP_PROXY/http_proxy/HTTPS_PROXY/https_proxy/ALL_PROXY` | `scripts/build-gate.mjs:33-37/57-67` |
| 项 | 结论 | 依据 |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| **是否要重启客户端 / 重新构建** | dev 态 `npm run agc``devUrl`(默认 3080,运行时被覆盖为实际端口),前端改动走 Vite HMR、**不需要 build****Rust 侧改动必须重启客户端**;未提交改动若不在当前进程里,也必须重启一次客户端 | `apps/ai-game-creator-shell/src-tauri/tauri.conf.json:6-11``scripts/start-tauri-dev.mjs:26-44` |
| **是否要重新构建产物** | 只有 `agc:build` / `ai-game-creator-shell:build` 才产出 `../dist`;若验的是打包后的客户端,改前端必须重新 build 并重装 | `tauri.conf.json``frontendDist: "../dist"` |
| **是否要重启 api-server** | **服务端改动需要重启**。本地 `.env` / `.env.local` / `.env.secrets.local` 修改后必须重启 `api-server`;已在 `npm run dev` 终端里可直接敲 `rs api-server`provider / worker / fallback 配置改动要按角色重启对应进程 | `docs/【开发运维】本地开发验证与生产运维-2026-05-15.md:114/136/209/215/809` |
| **启动命令** | 客户端:`npm run agc`(等价 `agc:dev` / `ai-game-creator-shell:dev`);只起前后端不起窗口:`npm run agc:serve`;后端:`npm run dev:api-server` | `package.json:20/155-159/169/22` |
| **端口与健康检查** | api-server 默认 `127.0.0.1:8082`,健康检查 `GET /healthz`(先查 BgFilter worker `/readyz`);**端口会漂移,实际值以 `.app/dev-stack.json` 为准** | `scripts/dev.mjs:237``scripts/start-dev-stack.mjs:28-29/351-353` |
| **登录与余额** | 需要登录;**不需要余额也能进工作台与资源画布**,消耗泥点的是付费生成与 Agent 请求 | `src/app/AuthenticatedClient.tsx:546/568-574``index.tsx:500` |
| **测试项目** | 真机 `What do u wanna do kitten` 已有 61 assets / 5 versions、两份布局 sidecar 齐全,适合当**存量数据**验收对象(只读);要验删除与版本三分支时建议另建新项目 | 只读统计 |
| **门禁命令** | `npm run ai-game-creator-shell:typecheck``npm run test -- apps/ai-game-creator-shell/tests``npm run check:encoding``git diff --check`Rust 侧 `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml -- --test-threads=1` | `package.json:123/164/193-194/63` |
| **build-gate 代理假红** | 本地带系统代理跑 `npm run build` 必假红:`NODE_USE_ENV_PROXY=1``HTTP_PROXY/https_proxy` 会触发 `[UNDICI-EHPA] Warning: EnvHttpProxyAgent is experimental`,不被 `ExperimentalWarning` 忽略规则放行。跑前清空 `NODE_USE_ENV_PROXY/HTTP_PROXY/http_proxy/HTTPS_PROXY/https_proxy/ALL_PROXY` | `scripts/build-gate.mjs:33-37/57-67` |
## 5. 每步"失败怎么看"
@@ -129,13 +129,13 @@
.reduce((m, e) => { const k = e.dataset.previewHasAlpha ?? '(无 alpha 判据)'; m[k] = (m[k] || 0) + 1; return m; }, {})
```
| 现象 | 说明 | 第一眼做什么 |
| --- | --- | --- |
| 可读类型 + `idle` | 该卡**从未发出预览请求**(可见性门禁没挂上,或卡片在被裁切的容器里) | 先确认跑的是包含修复的新构建(见第 1 节),再核对 observer 补挂是否生效 |
| `failed` + `data-preview-error` | 读取或解码真失败,读 `data-preview-error` 原文 | `retryable=false` 表示永久失败(超尺寸、损坏、类型不支持、不安全 SVG);可见性预取遇 failed 会直接放弃,不会自愈 |
| `loaded` 但无主体节点 | 原生返回的 dataUrl 为空、或解码不出画面 | 这类**不报错**,只能靠脚本 ④ 抓 |
| `placeholder` + `idle` | **不适用**(游戏代码、无法识别的二进制产物等),正常 | 不要当 bug 报 |
| 卡面有棋盘格但 `data-preview-has-alpha` 缺失 | **这张图其实不透明**:棋盘格是图里画进去的(旧构建会额外再铺一层卡面棋盘格,两者叠在一起) | 脚本 ⑤ 分组确认;真透明卡才允许有 `'true'` |
| 现象 | 说明 | 第一眼做什么 |
| -------------------------------------------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- |
| 可读类型 + `idle` | 该卡**从未发出预览请求**(可见性门禁没挂上,或卡片在被裁切的容器里) | 先确认跑的是包含修复的新构建(见第 1 节),再核对 observer 补挂是否生效 |
| `failed` + `data-preview-error` | 读取或解码真失败,读 `data-preview-error` 原文 | `retryable=false` 表示永久失败(超尺寸、损坏、类型不支持、不安全 SVG);可见性预取遇 failed 会直接放弃,不会自愈 |
| `loaded` 但无主体节点 | 原生返回的 dataUrl 为空、或解码不出画面 | 这类**不报错**,只能靠脚本 ④ 抓 |
| `placeholder` + `idle` | **不适用**(游戏代码、无法识别的二进制产物等),正常 | 不要当 bug 报 |
| 卡面有棋盘格但 `data-preview-has-alpha` 缺失 | **这张图其实不透明**:棋盘格是图里画进去的(旧构建会额外再铺一层卡面棋盘格,两者叠在一起) | 脚本 ⑤ 分组确认;真透明卡才允许有 `'true'` |
**IPC 侧**:预览只走 4 条命令 `read_local_project_image_preview``read_local_project_media_preview``read_local_project_text_preview``cancel_local_project_resource_preview_scope`。媒体那条的 `category` **只接受 `art` / `audio`**,传栏目值会被原生拒绝,表现是卡片全空、没有缩略图。
@@ -153,26 +153,26 @@ window.__TAURI__.core.invoke = (cmd, args) => {
### 5.2 按步骤的排障索引
| 步骤 | 出问题时第一眼看什么 |
| --- | --- |
| S2 | `.game-workbench-stage``data-resource-view-state`;缺失说明误进 run。再调 `get_local_game_preview_status` 看活体,不要看 `manifest.preview` |
| S4 | 对话后 `.agent/manifest.json``assets[]` 是否增长;底部 Agent Dock 的状态文案 |
| S5 | 各 `[data-resource-book-category]` 下的卡片数量;某栏全空而 manifest 中明显有该类资产时,通常不是分区轴坏了 |
| S6 | 5.1 的四条脚本 |
| S7 | `[data-resource-book-view]` 是否为预期值,`.game-resource-book-scene-titlebar.is-active` 的 category 是否为预期栏目 |
| S8 | `button[aria-label="搜索资源"]``aria-expanded``.game-resource-filter-panel` 是否真在 DOM(三字段同屏)。注意 jsdom 不校验可见性,只有真机截图或 `getBoundingClientRect` 才算数 |
| S9 | `.is-selected` 数量与 `aria-pressed`;工具条容器 `role="toolbar"` 是否存在 |
| S10 | 快速编辑失败文案;若为「当前素材类型不支持图片快速编辑」,说明源资源权威 `assetKind` 不在 17 项白名单里,这不是 V3 新缺陷 |
| S11 | 生成入口在"没有 invoke 桥或项目未就绪"时**整体不渲染**,而不是渲染了但点了没反应 |
| S12 | 保存后对比 `assets[n].tags``assets[n].category`,分类应逐字不变 |
| S13 | 弹窗内 `role="alert"` 的错误文案;写盘失败时应看到文件已回滚原名 |
| S14 | 是否有原生保存对话框;用户取消时是否停止后续批量保存 |
| S15 | 弹窗是否出现 `被 N 个游戏版本使用`;不勾选时版本记录仍在(可读但悬空) |
| S16 | `aria-label="当前版本:…"` 是否随切换更新;`[data-used-by-current-version="true"]` 集合是否变化 |
| S17 | `get_local_game_preview_status` 返回的 `status/url/port``.agent/logs/preview.log``running` 行 |
| S18 | 退出后重开项目是否落回资源总览;Rust 日志中的 `preview.gui_exit.stop_failed` |
| S19 | chip 的 `data-resource-reference-id`;两页签 `role="tab"``aria-selected`;空态文案(`当前版本还没有绑定素材` / `当前项目还没有已登记素材` / `没有匹配的素材` |
| S20 | 弹窗标题「发送前提醒」;`localStorage['agc.chat.prompt-polish-reminder.disabled']` 的值;`润色中…``AI 润色失败,可重试` 状态行 |
| 步骤 | 出问题时第一眼看什么 |
| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| S2 | `.game-workbench-stage``data-resource-view-state`;缺失说明误进 run。再调 `get_local_game_preview_status` 看活体,不要看 `manifest.preview` |
| S4 | 对话后 `.agent/manifest.json``assets[]` 是否增长;底部 Agent Dock 的状态文案 |
| S5 | 各 `[data-resource-book-category]` 下的卡片数量;某栏全空而 manifest 中明显有该类资产时,通常不是分区轴坏了 |
| S6 | 5.1 的四条脚本 |
| S7 | `[data-resource-book-view]` 是否为预期值,`.game-resource-book-scene-titlebar.is-active` 的 category 是否为预期栏目 |
| S8 | `button[aria-label="搜索资源"]``aria-expanded``.game-resource-filter-panel` 是否真在 DOM(三字段同屏)。注意 jsdom 不校验可见性,只有真机截图或 `getBoundingClientRect` 才算数 |
| S9 | `.is-selected` 数量与 `aria-pressed`;工具条容器 `role="toolbar"` 是否存在 |
| S10 | 快速编辑失败文案;若为「当前素材类型不支持图片快速编辑」,说明源资源权威 `assetKind` 不在 17 项白名单里,这不是 V3 新缺陷 |
| S11 | 生成入口在"没有 invoke 桥或项目未就绪"时**整体不渲染**,而不是渲染了但点了没反应 |
| S12 | 保存后对比 `assets[n].tags``assets[n].category`,分类应逐字不变 |
| S13 | 弹窗内 `role="alert"` 的错误文案;写盘失败时应看到文件已回滚原名 |
| S14 | 是否有原生保存对话框;用户取消时是否停止后续批量保存 |
| S15 | 弹窗是否出现 `被 N 个游戏版本使用`;不勾选时版本记录仍在(可读但悬空) |
| S16 | `aria-label="当前版本:…"` 是否随切换更新;`[data-used-by-current-version="true"]` 集合是否变化 |
| S17 | `get_local_game_preview_status` 返回的 `status/url/port``.agent/logs/preview.log``running` |
| S18 | 退出后重开项目是否落回资源总览;Rust 日志中的 `preview.gui_exit.stop_failed` |
| S19 | chip 的 `data-resource-reference-id`;两页签 `role="tab"``aria-selected`;空态文案(`当前版本还没有绑定素材` / `当前项目还没有已登记素材` / `没有匹配的素材` |
| S20 | 弹窗标题「发送前提醒」;`localStorage['agc.chat.prompt-polish-reminder.disabled']` 的值;`润色中…``AI 润色失败,可重试` 状态行 |
**通用日志面**WebView 的 `console.*` 会被镜像进 Rust `application.log`(标记 `__agcWebviewLogBridgeInstalled`),因此 F12 里打的日志事后也能在日志文件里复核。与本次验收最相关的两条是 `preview.gui_exit.stop_failed``[chat-markdown] render failed`
@@ -185,32 +185,32 @@ api-server 是否本次重启:□ 是 □ 否
测试项目:□ 真机 What do u wanna do kitten(只读) □ 新建项目 ____________________
```
| 步骤 | 结果 | 备注(现象 / 证据 / 截图名) |
| --- | --- | --- |
| S1 建项并进工作台 | ☐ 过 ☐ 不过 | |
| S2 落 `resource-overview` | ☐ 过 ☐ 不过 | |
| S3 项目页面板(可选) | ☐ 过 ☐ 不过 ☐ 跳过 | |
| S4 对话生成 → manifest 登记 | ☐ 过 ☐ 不过 | assets___ → ___ |
| S5 7 栏目归属正确 | ☐ 过 ☐ 不过 | 各栏计数:______ |
| S6 卡片预览台账(idle / failed / 空) | ☐ 过 ☐ 不过 | idle__ failed__ 空:__ |
| S7 栏目导航(含「所有资源」/ 下一页 / 空栏目) | ☐ 过 ☐ 不过 | |
| S8 单一筛选浮层(三字段)+ 不清条件 | ☐ 过 ☐ 不过 | |
| S9 选择 / 多选 / 框选 / 工具条 | ☐ 过 ☐ 不过 | 实际渲染的动作:______ |
| S10 图片快速编辑(新素材 + 血缘) | ☐ 过 ☐ 不过 | 是否 400:☐ 否 ☐ 是(原因:____ |
| S11 生成入口(视频 / 音效 / 背景音乐) | ☐ 过 ☐ 不过 ☐ 未验 | |
| S12 编辑标签(只改 tags) | ☐ 过 ☐ 不过 | category 是否原样:☐ 是 ☐ 否 |
| S13 重命名(id 不变) | ☐ 过 ☐ 不过 | |
| S14 下载(另存为) | ☐ 过 ☐ 不过 | |
| S15 删除三分支 | ☐ 无引用 ☐ 引用未勾选 ☐ 勾选连带 | |
| S16 版本切换 + 当前使用高亮 + @ 同步 | ☐ 过 ☐ 不过 | |
| S17 播放 → 运行视图 + 本地预览 | ☐ 过 ☐ 不过 | url______________ |
| S18 停止预览 / 退出收尾对齐 stopped | ☐ 过 ☐ 不过 | |
| S19 @ 引用两页签 + 独立筛选 + chip 原子性 | ☐ 过 ☐ 不过 | |
| S19a @ 面板标签筛选(多标签 AND,叠加关键字与功能分类) | ☐ 过 ☐ 不过 | |
| S19b 快速编辑提示词内 @ 引用(同聊天引用模型与字面量) | ☐ 过 ☐ 不过 | |
| S20 润色 + 发送前提醒 + 不再提醒 | ☐ 过 ☐ 不过 | |
| S21 台账(即时同步 / 双布局 / 无溢出) | ☐ 过 ☐ 不过 | 1280×800 ☐ 1280×720 ☐ |
| 门禁:typecheck / AGC tests / check:encoding / git diff --check | ☐ 过 ☐ 不过 | |
| 步骤 | 结果 | 备注(现象 / 证据 / 截图名) |
| --------------------------------------------------------------- | -------------------------------- | ------------------------------------- |
| S1 建项并进工作台 | ☐ 过 ☐ 不过 | |
| S2 落 `resource-overview` | ☐ 过 ☐ 不过 | |
| S3 项目页面板(可选) | ☐ 过 ☐ 不过 ☐ 跳过 | |
| S4 对话生成 → manifest 登记 | ☐ 过 ☐ 不过 | assets**_ → _** |
| S5 7 栏目归属正确 | ☐ 过 ☐ 不过 | 各栏计数:**\_\_** |
| S6 卡片预览台账(idle / failed / 空) | ☐ 过 ☐ 不过 | idle** failed** 空:\_\_ |
| S7 栏目导航(含「所有资源」/ 下一页 / 空栏目) | ☐ 过 ☐ 不过 | |
| S8 单一筛选浮层(三字段)+ 不清条件 | ☐ 过 ☐ 不过 | |
| S9 选择 / 多选 / 框选 / 工具条 | ☐ 过 ☐ 不过 | 实际渲染的动作:**\_\_** |
| S10 图片快速编辑(新素材 + 血缘) | ☐ 过 ☐ 不过 | 是否 400:☐ 否 ☐ 是(原因:\_\_\_\_ |
| S11 生成入口(视频 / 音效 / 背景音乐) | ☐ 过 ☐ 不过 ☐ 未验 | |
| S12 编辑标签(只改 tags) | ☐ 过 ☐ 不过 | category 是否原样:☐ 是 ☐ 否 |
| S13 重命名(id 不变) | ☐ 过 ☐ 不过 | |
| S14 下载(另存为) | ☐ 过 ☐ 不过 | |
| S15 删除三分支 | ☐ 无引用 ☐ 引用未勾选 ☐ 勾选连带 | |
| S16 版本切换 + 当前使用高亮 + @ 同步 | ☐ 过 ☐ 不过 | |
| S17 播放 → 运行视图 + 本地预览 | ☐ 过 ☐ 不过 | url******\_\_****** |
| S18 停止预览 / 退出收尾对齐 stopped | ☐ 过 ☐ 不过 | |
| S19 @ 引用两页签 + 独立筛选 + chip 原子性 | ☐ 过 ☐ 不过 | |
| S19a @ 面板标签筛选(多标签 AND,叠加关键字与功能分类) | ☐ 过 ☐ 不过 | |
| S19b 快速编辑提示词内 @ 引用(同聊天引用模型与字面量) | ☐ 过 ☐ 不过 | |
| S20 润色 + 发送前提醒 + 不再提醒 | ☐ 过 ☐ 不过 | |
| S21 台账(即时同步 / 双布局 / 无溢出) | ☐ 过 ☐ 不过 | 1280×800 ☐ 1280×720 ☐ |
| 门禁:typecheck / AGC tests / check:encoding / git diff --check | ☐ 过 ☐ 不过 | |
## 7. 风险与盲区
@@ -218,30 +218,30 @@ api-server 是否本次重启:□ 是 □ 否
需求源头文档《GameAgent客户端画布与资源工作台 V3.0》(飞书 wiki 文档,文档 token 以 `N4Kiw…` 开头)的文本层共 **796 行文本、5 段视频、2 张图片**。以下条款**只能靠视频或截图验收**,文本层只有"标题式"描述,缺可判定细节:
| # | 位置 | 媒体 | 文本只到哪 | 缺什么(无法从文本验收的部分) |
| --- | --- | --- | --- | --- |
| 1 | §3.1 默认视图 | **截图**(图注「资源管理视图」) | "默认进入'全部资源'视图……功能画布分类:UI 与交互、角色与对象、场景与环境、音频、文档、待归类" | 总览的**版式**:缩略卡片排布、层叠张数、缩略图尺寸与间距、标题栏位置 |
| 2 | §3.2 筛选 | **视频** `20260907-0309-29.2729736.mp4`"增加自定义标签" | "点击已有标签 / 输入新标签 / 保存后同步进全局标签库" | 面板**长什么样**、pill 排布、输入框提示文案、保存按钮位置 |
| 3 | §3.2 筛选 | **视频** `20260907-0321-46.9223141.mp4`"查看素材标签" | "标签不默认显示在卡片上,需在'编辑标签'中查看" | 查看入口与展示形态 |
| 4 | §3.2 筛选 | **视频** `20260907-0322-33.1365605.mp4`("按照标签筛选素材") | 文本给了筛选逻辑(条件即时生效 / 清空恢复 / 不影响归属) | 筛选面板的交互与视觉:多标签是 AND 还是 OR 的**界面表达**、标签汇总列表样式 |
| 5 | §3.2 筛选 | **视频** `20260907-0323-34.3948031.mp4`"删除标签" | "删除标签时从所有素材和筛选条件中移除;'管理全部标签'里可删" | 「管理全部标签」这个入口 / 面板本身**只存在于视频**,AGC 侧未找到对应实现 |
| 6 | §10.1 / §10.2 @ 引用 | **视频** `20260906-0812-23.9790394.mp4` + **截图**(提示框"请把@开始按钮并保持原位置",截图注明"参考 loveart" | "资源卡工具栏提供 @引用 工具""所有@后产生的标签都在对话框当中,用户可以在标签前后编辑上下文" | chip 的**外观**(是否带图标 / 缩略图 / 颜色)、插入后光标落点、与 Lovart 的对照样式 |
| 7 | PRD L139(项目管理页) | 文档**明确要求**提交截图 + 视频 | "截图必须使用含 Web、Godot 和无效状态的 populated fixture;视频必须演示搜索、清除、行尾菜单及可观察的项目操作结果" | 这条本身就是"必须交视频"的要求,不是可由文本判定的功能条款 |
| # | 位置 | 媒体 | 文本只到哪 | 缺什么(无法从文本验收的部分) |
| --- | ---------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- |
| 1 | §3.1 默认视图 | **截图**(图注「资源管理视图」) | "默认进入'全部资源'视图……功能画布分类:UI 与交互、角色与对象、场景与环境、音频、文档、待归类" | 总览的**版式**:缩略卡片排布、层叠张数、缩略图尺寸与间距、标题栏位置 |
| 2 | §3.2 筛选 | **视频** `20260907-0309-29.2729736.mp4`"增加自定义标签" | "点击已有标签 / 输入新标签 / 保存后同步进全局标签库" | 面板**长什么样**、pill 排布、输入框提示文案、保存按钮位置 |
| 3 | §3.2 筛选 | **视频** `20260907-0321-46.9223141.mp4`"查看素材标签" | "标签不默认显示在卡片上,需在'编辑标签'中查看" | 查看入口与展示形态 |
| 4 | §3.2 筛选 | **视频** `20260907-0322-33.1365605.mp4`("按照标签筛选素材") | 文本给了筛选逻辑(条件即时生效 / 清空恢复 / 不影响归属) | 筛选面板的交互与视觉:多标签是 AND 还是 OR 的**界面表达**、标签汇总列表样式 |
| 5 | §3.2 筛选 | **视频** `20260907-0323-34.3948031.mp4`"删除标签" | "删除标签时从所有素材和筛选条件中移除;'管理全部标签'里可删" | 「管理全部标签」这个入口 / 面板本身**只存在于视频**,AGC 侧未找到对应实现 |
| 6 | §10.1 / §10.2 @ 引用 | **视频** `20260906-0812-23.9790394.mp4` + **截图**(提示框"请把@开始按钮并保持原位置",截图注明"参考 loveart" | "资源卡工具栏提供 @引用 工具""所有@后产生的标签都在对话框当中,用户可以在标签前后编辑上下文" | chip 的**外观**(是否带图标 / 缩略图 / 颜色)、插入后光标落点、与 Lovart 的对照样式 |
| 7 | PRD L139(项目管理页) | 文档**明确要求**提交截图 + 视频 | "截图必须使用含 Web、Godot 和无效状态的 populated fixture;视频必须演示搜索、清除、行尾菜单及可观察的项目操作结果" | 这条本身就是"必须交视频"的要求,不是可由文本判定的功能条款 |
**补齐方式**:① 由产品口述 2–3 句关键视觉约束后改写为判据;② 用 `lark-cli docs +media-download` 取出这 5 段视频与 2 张图并抽取静帧后逐帧写判据;③ 降级为"人工目视,不计入自动化判据",记录表里只打勾。
### 7.2 本地环境无法验证 / 需要额外条件的项
| 项 | 为什么本地不可验 | 建议 |
| --- | --- | --- |
| 双窗口布局 CAS 冲突(L509) | 需同一项目同时开两个客户端窗口、基于同一 revision 同时写入 | 标为"需专项构造",或只跑 `resourceCanvasLayoutContract` 定向测试 |
| 4096 资源链式 fixture 性能(L522 | 需构造大规模 fixture | 交给定向测试,不进人工主线 |
| `1280×720` / `1280×800` 视觉验收(L512、L577) | 必须真实窗口 + 截图或测量 `document/body``client``scroll` | 每条视觉结论附截图;jsdom 不做布局,vitest 中的可见性断言是假守卫 |
| Windows 平台性断言 | 非 Linux 平台本地预览用**随机临时端口**;端口池耗尽回退、`GENARRATIVE_DEV_PORT_RANGE`、命令沙箱模块**只在 Linux 生效**;文件锁 Unix 用 `flock`、Windows 不共享句柄 | 涉及这些的判据不能反推平台行为。反之"delete-pending 等行为级 Windows 用例 CI 没有 runner,只在 Windows 本地跑"——本地反而能验到 CI 验不到的 |
| `build-gate` 代理假红 | 见第 4 节 | 跑前清代理变量,或用 `build:raw` |
| worktree 专属假红 | worktree 里 `check-repository-ci.sh` 第一步即"比较基线不可用"jsdom 拿不到 fit key | 改用 Windows git 逐条等价执行;这两类红不当缺陷 |
| 既有失败用例 | 技术方案记录过 AGC 前端 5 条既有失败(C3 交互改造后待更新用例);Rust 并行跑 `project::` 约 10 条环境依赖失败,仓库口径是 `--test-threads=1` | 预置"已知红名单",避免重复排查 |
| F12 看不到 Rust → api-server 的 HTTP | 见 5.1 更正 | 用 api-server 日志或 `.agent/logs` |
| 项 | 为什么本地不可验 | 建议 |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
| 双窗口布局 CAS 冲突(L509) | 需同一项目同时开两个客户端窗口、基于同一 revision 同时写入 | 标为"需专项构造",或只跑 `resourceCanvasLayoutContract` 定向测试 |
| 4096 资源链式 fixture 性能(L522 | 需构造大规模 fixture | 交给定向测试,不进人工主线 |
| `1280×720` / `1280×800` 视觉验收(L512、L577) | 必须真实窗口 + 截图或测量 `document/body``client``scroll` | 每条视觉结论附截图;jsdom 不做布局,vitest 中的可见性断言是假守卫 |
| Windows 平台性断言 | 非 Linux 平台本地预览用**随机临时端口**;端口池耗尽回退、`GENARRATIVE_DEV_PORT_RANGE`、命令沙箱模块**只在 Linux 生效**;文件锁 Unix 用 `flock`、Windows 不共享句柄 | 涉及这些的判据不能反推平台行为。反之"delete-pending 等行为级 Windows 用例 CI 没有 runner,只在 Windows 本地跑"——本地反而能验到 CI 验不到的 |
| `build-gate` 代理假红 | 见第 4 节 | 跑前清代理变量,或用 `build:raw` |
| worktree 专属假红 | worktree 里 `check-repository-ci.sh` 第一步即"比较基线不可用"jsdom 拿不到 fit key | 改用 Windows git 逐条等价执行;这两类红不当缺陷 |
| 既有失败用例 | 技术方案记录过 AGC 前端 5 条既有失败(C3 交互改造后待更新用例);Rust 并行跑 `project::` 约 10 条环境依赖失败,仓库口径是 `--test-threads=1` | 预置"已知红名单",避免重复排查 |
| F12 看不到 Rust → api-server 的 HTTP | 见 5.1 更正 | 用 api-server 日志或 `.agent/logs` |
### 7.3 已知未做 / 已取消(不要报成缺陷)
@@ -252,7 +252,7 @@ api-server 是否本次重启:□ 是 □ 否
5. **「首轮进度投影」无编码级判据**,可见性由底部 Agent 状态栏承载。
6. **浮出工具条的视觉位置未做真机核验**jsdom 不执行 Web Animations,只覆盖结构与 class)。
7. **重命名不改游戏源码中的旧 `assets/<name>` 引用**
8. **C9 参考图弹窗在 AGC 侧入口未确认**:全仓检索 `添加参考图` 只命中共享 `src/components/image-editor/*`AGC 的 `ResourceCanvasGenerationPanelView` 未引用它,可能处于"组件已实现、AGC 入口未接"状态。建议真机点一次生成 / 编辑面板确认;若无入口,C9 不计入本轮主线。**注**:资源替换复用同一个 `ImageCanvasProjectAssetPickerDialog`,它在 AGC 侧是首次使用(同一个弹窗、不同的 opt-in 参数)。
8. **C9 参考图弹窗在 AGC 侧入口未确认**:全仓检索 `添加参考图` 只命中共享 `src/components/image-editor/*`AGC 的 `ResourceCanvasGenerationPanelView` 未引用它,可能处于"组件已实现、AGC 入口未接"状态。建议真机点一次生成 / 编辑面板确认;若无入口,C9 不计入本轮主线。**注**:资源替换复用同一个 `ImageCanvasProjectAssetPickerDialog`,它在 AGC 侧是首次使用(2026-09-21 起走该组件的 `nonModal` 非模态形态:不铺遮罩、面板锚在画布容器右上角,见 S15a)。
9. **运行视图存在「点选素材」按钮**,但 #309 的"不做"清单包含运行画面点选,口径冲突待定。
10. **替换成功后运行画面不会立刻变**:按 C7 与本轮口径只做记录层 + UI 层,可见变化是资源卡"当前使用"高亮移到替换素材与 `@` 面板"当前版本素材"更新;**不会新增版本卡、也不切换版本**,这不等于替换失败。
11. **替换候选弹窗不加载缩略图**:AGC 的素材预览要经带 scope 的原生读取器拿 Blob URL,弹窗内没有同步 `src`,因此候选行只渲染类型占位(不给 `<img>` 喂空串、不挂破图)。
@@ -261,13 +261,13 @@ api-server 是否本次重启:□ 是 □ 否
### 7.4 文档与代码的偏差(需明确按哪个判)
| 偏差 | 文档表述 | 代码实际 | 建议 |
| --- | --- | --- | --- |
| 资源卡「打开详情」 | PRD L6162 要求"打开详情"与"播放 / 暂停"是可分别键盘聚焦的同级按钮 | 不存在该按钮(测试显式断言为 `null`);真正入口是 `选中资源:<栏目> <名>`,播放按钮是 `播放/暂停 <名>`;**只读信息浮层已回归**(2026-09-11 工具条「信息」动作) | 按 #309 C3(取消资源详情面板与全屏编辑路由)判,**C3 取消的仍只是可编辑详情面板与全屏编辑路由**;PRD L61–62 建议同步修订 |
| 运行入口不可用文案 | PRD L165 显示"当前无可运行版本",但仍允许点击 | 源码中无此字符串;实现为「首个可运行原型尚未完成,运行视图暂不可用」;且播放按钮 `disabled`,只有"运行"tab 不 disabled、用 `data-unavailable` 表达 | 建议按"tab 可点 + 播放按钮置灰"判,并把 PRD L165 文案对齐实现 |
| 画布生成入口 | #309「画布生成入口未接」 | 已接两条:浮层入口只做视频;音频与图片类在栏目画布底部工具栏(见 S11a / PRD §3.10 | 按代码与当前产品口径判,并更新 #309 进度段 |
| 分类手设入口 | 旧版 PRD「用户可在分类与标签面板手动设置 category」 | 已移除;面板只编辑 tags(已随 `6bdc8bbd9` 入库) | 按新口径判 |
| 真机 manifest 分类分布 | — | 落盘 `unclassified 57 / ui-interaction 3 / scene 1` | **读时自愈**会把 kind 可明确分类的资产显示到正确栏目,判定必须看 UI 栏目计数,不能看 manifest 文件,否则必然误报"分类不生效" |
| 偏差 | 文档表述 | 代码实际 | 建议 |
| ---------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| 资源卡「打开详情」 | PRD L6162 要求"打开详情"与"播放 / 暂停"是可分别键盘聚焦的同级按钮 | 不存在该按钮(测试显式断言为 `null`);真正入口是 `选中资源:<栏目> <名>`,播放按钮是 `播放/暂停 <名>`;**只读信息浮层已回归**(2026-09-11 工具条「信息」动作) | 按 #309 C3(取消资源详情面板与全屏编辑路由)判,**C3 取消的仍只是可编辑详情面板与全屏编辑路由**;PRD L61–62 建议同步修订 |
| 运行入口不可用文案 | PRD L165 显示"当前无可运行版本",但仍允许点击 | 源码中无此字符串;实现为「首个可运行原型尚未完成,运行视图暂不可用」;且播放按钮 `disabled`,只有"运行"tab 不 disabled、用 `data-unavailable` 表达 | 建议按"tab 可点 + 播放按钮置灰"判,并把 PRD L165 文案对齐实现 |
| 画布生成入口 | #309「画布生成入口未接」 | 已接两条:浮层入口只做视频;音频与图片类在栏目画布底部工具栏(见 S11a / PRD §3.10 | 按代码与当前产品口径判,并更新 #309 进度段 |
| 分类手设入口 | 旧版 PRD「用户可在分类与标签面板手动设置 category」 | 已移除;面板只编辑 tags(已随 `6bdc8bbd9` 入库) | 按新口径判 |
| 真机 manifest 分类分布 | — | 落盘 `unclassified 57 / ui-interaction 3 / scene 1` | **读时自愈**会把 kind 可明确分类的资产显示到正确栏目,判定必须看 UI 栏目计数,不能看 manifest 文件,否则必然误报"分类不生效" |
## 8. 验收边界