补充美术素材聊天入口

新增 /art 美术素材摘要和草稿

补充 /next 与 /help 的美术入口覆盖

同步技术方案和项目决策记录
This commit is contained in:
AIGameCreator App
2026-07-03 16:48:08 +08:00
parent bc057b7ef7
commit cb1ab51b58
4 changed files with 276 additions and 6 deletions
+108
View File
@@ -1898,6 +1898,7 @@ const chatCommandHelp = [
'/history:重新读取当前项目对话历史',
'/files:列出本地项目文件',
'/assets:列出本地项目资产',
'/art:查看美术素材与下一步草稿',
'/audio:查看音频素材与下一步草稿',
'/artifacts:列出常用生成产物读取命令',
'/run-artifacts:列出最近 Run 产物读取命令',
@@ -2569,6 +2570,12 @@ function summarizeNextProjectActions(
if (nextManifest.assets.length > 0) {
addSuggestion(`查看 ${nextManifest.assets.length} 个本地资产`, '/assets');
addSuggestion(
nextManifest.assets.some(isProjectVisualAsset)
? '查看美术素材'
: '生成或同步美术素材',
'/art',
);
addSuggestion(
nextManifest.assets.some(isProjectAudioAsset)
? '查看音频素材'
@@ -2581,6 +2588,7 @@ function summarizeNextProjectActions(
'/asset-register assets/hero.png image image/png',
);
addSuggestion('同步已有画板项目资源', '/sync-canvas-project ');
addSuggestion('生成或同步美术素材', '/art');
addSuggestion('登记或导入音频素材', '/audio');
}
@@ -3009,6 +3017,89 @@ function isProjectAudioAsset(asset: GameCreationAppManifest['assets'][number]) {
);
}
function isProjectVisualAsset(
asset: GameCreationAppManifest['assets'][number],
) {
const mediaType = asset.mediaType.toLowerCase();
const kind = asset.kind.toLowerCase();
return (
mediaType.startsWith('image/') ||
mediaType.startsWith('video/') ||
mediaType === 'application/vnd.genarrative.image-sequence' ||
kind === 'image' ||
kind === 'sprite' ||
kind === 'icon' ||
kind === 'ui' ||
kind === 'background' ||
kind === 'character-animation'
);
}
function summarizeProjectVisualAssets(nextManifest: GameCreationAppManifest) {
const visualAssets = nextManifest.assets.filter(isProjectVisualAsset);
if (visualAssets.length === 0) {
return {
text: [
'美术素材:暂无登记图片、视频或序列帧。',
'可先生成首版美术,或同步已有画板项目资源。',
].join('\n'),
draftCommand: '/generate-art 首版核心美术素材',
draftCommandLabel: '生成美术',
};
}
const sourceCounts = visualAssets.reduce(
(counts, asset) => {
counts[asset.source.kind] += 1;
return counts;
},
{ uploaded: 0, generated: 0, canvas: 0 } satisfies Record<
GameCreationAppAssetSourceKind,
number
>,
);
const visibleAssets = visualAssets.slice(0, 8);
const lines = visibleAssets.map(
(asset) =>
`- ${asset.localPath} · ${asset.mediaType} · ${
assetSourceKindLabels[asset.source.kind]
}${
asset.source.canvasProjectId
? ` · 画板 ${asset.source.canvasProjectId}`
: ''
}`,
);
if (visualAssets.length > visibleAssets.length) {
lines.push(
`- 还有 ${visualAssets.length - visibleAssets.length} 个美术素材`,
);
}
const hasCanvasVisualAsset = visualAssets.some(
(asset) => asset.source.kind === 'canvas',
);
return {
text: [
`美术素材:${visualAssets.length}`,
`来源:${(Object.keys(sourceCounts) as GameCreationAppAssetSourceKind[])
.filter((source) => sourceCounts[source] > 0)
.map(
(source) =>
`${assetSourceKindLabels[source]} ${sourceCounts[source]}`,
)
.join('、')}`,
hasCanvasVisualAsset
? '画板来源:已接入'
: '画板来源:暂无 · 建议 /generate-art 首版核心美术素材',
lines.join('\n'),
].join('\n'),
draftCommand: hasCanvasVisualAsset
? '/read assets/manifest.art.json'
: '/generate-art 首版核心美术素材',
draftCommandLabel: hasCanvasVisualAsset ? '读美术清单' : '生成美术',
};
}
function summarizeProjectAudioAssets(nextManifest: GameCreationAppManifest) {
const audioAssets = nextManifest.assets.filter(isProjectAudioAsset);
if (audioAssets.length === 0) {
@@ -6350,6 +6441,23 @@ export function App() {
return;
}
if (prompt === '/art') {
if (!requireChatProjectForUserAction()) {
return;
}
const summary = summarizeProjectVisualAssets(manifest);
setMessages((current) => [
...current,
{
role: 'assistant',
text: summary.text,
draftCommand: summary.draftCommand,
draftCommandLabel: summary.draftCommandLabel,
},
]);
return;
}
if (prompt === '/audio') {
if (!requireChatProjectForUserAction()) {
return;
@@ -2420,6 +2420,7 @@ describe('AI 游戏创作 App 界面边界', () => {
expect(await screen.findByText(/下一步建议:/)).not.toBeNull();
expect(screen.getByText(/运行自检并启动本地预览:\/run/)).not.toBeNull();
expect(screen.getByText(/查看 .* 个 ready 任务:\/tasks/)).not.toBeNull();
expect(screen.getByText(/查看美术素材:\/art/)).not.toBeNull();
expect(screen.getByText(/查看发布准备清单:\/publish/)).not.toBeNull();
expect(screen.getByText(/查看试玩状态:\/playtest/)).not.toBeNull();
expect(screen.getByText(/查看评审和返工焦点:\/review/)).not.toBeNull();
@@ -2621,6 +2622,36 @@ describe('AI 游戏创作 App 界面边界', () => {
'uploaded · text/plain · uploaded',
),
).not.toBeNull();
const artManifestReadCountBefore = invoke.mock.calls.filter(
([command, args]) =>
command === 'read_local_project_file' &&
args &&
'relativePath' in args &&
args.relativePath === 'assets/manifest.art.json',
).length;
submitChat('/art');
expect(await screen.findByText(/美术素材:1 个/)).not.toBeNull();
expect(screen.getByText(/来源:画板 1/)).not.toBeNull();
expect(
screen.getByText(/assets\/canvas-sync\/tiles\.png · image\/png · 画板/),
).not.toBeNull();
const visualDraftButtons = screen.getAllByRole('button', {
name: '读美术清单',
});
fireEvent.click(visualDraftButtons[visualDraftButtons.length - 1]);
expect(composerInput).toHaveProperty(
'value',
'/read assets/manifest.art.json',
);
expect(
invoke.mock.calls.filter(
([command, args]) =>
command === 'read_local_project_file' &&
args &&
'relativePath' in args &&
args.relativePath === 'assets/manifest.art.json',
),
).toHaveLength(artManifestReadCountBefore);
const audioManifestReadCountBefore = invoke.mock.calls.filter(
([command, args]) =>
command === 'read_local_project_file' &&
@@ -2658,7 +2689,11 @@ describe('AI 游戏创作 App 界面边界', () => {
['读音频清单', '/read assets/manifest.audio.json'],
['读发布说明', '/read exports/README.md'],
] as const) {
fireEvent.click(screen.getByRole('button', { name: buttonName }));
fireEvent.click(
within(screen.getByLabelText('预览快捷操作')).getByRole('button', {
name: buttonName,
}),
);
expect(composerInput).toHaveProperty('value', draftValue);
await waitFor(() => expect(document.activeElement).toBe(composerInput));
}
@@ -8304,6 +8339,7 @@ describe('AI 游戏创作 App 界面边界', () => {
expect(
screen.getByText(/\/artifacts:列出常用生成产物读取命令/),
).not.toBeNull();
expect(screen.getByText(/\/art:查看美术素材与下一步草稿/)).not.toBeNull();
expect(
screen.getByText(/\/audio:查看音频素材与下一步草稿/),
).not.toBeNull();
@@ -14152,6 +14188,130 @@ describe('AI 游戏创作 App 界面边界', () => {
);
});
it('summarizes art assets from chat without reading project files', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
manifest.assets.push(
{
id: 'art-generated',
kind: 'image',
mediaType: 'image/png',
localPath: 'assets/generated/hero.png',
source: { kind: 'generated' },
},
{
id: 'art-canvas',
kind: 'character-animation',
mediaType: 'application/vnd.genarrative.image-sequence',
localPath: 'assets/canvas-sync/hero-run',
source: { kind: 'canvas', canvasProjectId: 'canvas-art' },
},
{
id: 'audio-asset',
kind: 'audio',
mediaType: 'audio/wav',
localPath: 'assets/audio/sfx.wav',
source: { kind: 'uploaded' },
},
);
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'append_local_permission_log') {
return {};
}
if (command === 'init_local_game_project') {
const projectPath = String(args?.projectPath ?? '');
return {
projectPath,
manifestPath: `${projectPath}/.agent/manifest.json`,
manifest,
};
}
if (command === 'read_local_conversation') {
return {
path: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: null,
messages: [],
};
}
if (command === 'append_local_conversation_message') {
return {
path: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: null,
messages: [],
};
}
if (command === 'read_local_project_file') {
throw new Error('art summary should not read project files');
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/');
submitChat('/project /tmp/authorized-game');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText('已设置本地项目:/tmp/authorized-game'),
).not.toBeNull();
invoke.mockClear();
submitChat('/art');
expect(await screen.findByText(/美术素材:2 个/)).not.toBeNull();
expect(screen.getByText(/来源:生成 1、画板 1/)).not.toBeNull();
expect(screen.getByText(/画板来源:已接入/)).not.toBeNull();
expect(
screen.getByText(/assets\/generated\/hero\.png · image\/png · 生成/),
).not.toBeNull();
expect(
screen.getByText(
/assets\/canvas-sync\/hero-run · application\/vnd\.genarrative\.image-sequence · 画板 · 画板 canvas-art/,
),
).not.toBeNull();
const artManifestDraftButtons = screen.getAllByRole('button', {
name: '读美术清单',
});
fireEvent.click(
artManifestDraftButtons[artManifestDraftButtons.length - 1],
);
expect(screen.getByLabelText('创作想法')).toHaveProperty(
'value',
'/read assets/manifest.art.json',
);
expect(invoke).not.toHaveBeenCalledWith(
'read_local_project_file',
expect.anything(),
);
expect(invoke).not.toHaveBeenCalledWith(
'get_local_game_manifest',
expect.anything(),
);
expect(invoke).not.toHaveBeenCalledWith(
'generate_platform_art_asset',
expect.anything(),
);
expect(invoke).not.toHaveBeenCalledWith(
'sync_canvas_project_assets',
expect.anything(),
);
expect(invoke).not.toHaveBeenCalledWith(
'import_canvas_asset',
expect.anything(),
);
expect(invoke).not.toHaveBeenCalledWith(
'register_local_asset',
expect.anything(),
);
expect(invoke).not.toHaveBeenCalledWith(
'run_limited_local_command',
expect.anything(),
);
});
it('registers an existing project asset from chat after confirmation', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
@@ -3875,9 +3875,10 @@
- 2026-07-03 调整:`/llm-status` 读取到的 agent 级 LLM 配置状态可回填到主窗口 Agent 状态列表、聊天侧 `/agents` 汇总和单 Agent 对话头部,显示 provider 类型、模型、流式开关和 API Key 是否已读取;密钥本体仍不能进入聊天、状态列表、manifest、trace 或本地项目文件。
- 2026-07-03 调整:开发窗口日志面板提供 `.agent/logs/command.log``.agent/logs/preview.log``.agent/logs/agent.log` 的只读查看入口,复用 `file.read` 授权策略;普通用户聊天输入 `/logs` 只列出这三个日志文件对应的 `/read ...` 草稿 / 命令并提供首个草稿,不直接读取日志,不新增普通用户日志面板,实际读取仍走聊天侧 `file.read`
- 2026-07-03 调整:单 Agent 对话面板允许用户把当前输入手动追加到该 agent 的 `memory/agents/<group>/<role>.md` 私有记忆;写入复用 `memory.write` 项目策略、项目锁和 Tauri 本地目录能力,不把普通对话流水自动混入私有记忆;聊天侧 `/agent-conversations``/agent-memories` 只列出同一批 Agent 对话与私有记忆读取命令并提供首个 `/read` 草稿,不直接读取文件。
- 2026-07-03 调整:普通用户通过聊天输入 `/art` 查看美术素材,只基于当前 manifest 盘点图片、视频和序列帧素材的数量、来源、画板接入状态和路径,并提供 `/generate-art 首版核心美术素材``/read assets/manifest.art.json` 草稿;该入口不得触发 Tauri 读写、平台生成、画板同步或新增普通用户美术面板。
- 2026-07-03 调整:主窗口新增音效登记和画板音频导入快捷入口,只填入 `/asset-register assets/audio/sfx.wav audio audio/wav``/import-canvas-asset assets/audio/sfx.wav ` 草稿;聊天输入 `/audio` 只基于当前 manifest 盘点音频素材、来源和路径,并给出登记音效或读取 `assets/manifest.audio.json` 的草稿。音乐组仍复用现有资产登记 / 画板回流链路,不新增独立音频生成系统。
- 2026-07-03 调整:主窗口新增常用生成产物读取入口,只把入口 HTML、设计、数值、美术清单、音频清单和发布说明对应的 `/read` 草稿填入聊天输入框;聊天命令 `/artifacts` 只列出同一组固定读取命令并提供首个读取草稿,`/run-artifacts` 只列出最近 trace 里的产物读取命令并提供首个 `/read` 草稿,`/logs` 只列出固定日志读取命令;实际读取仍走聊天侧 `file.read` 权限流,不直接读本地文件。
- 2026-07-03 调整:普通用户通过聊天输入 `/next` 触发下一步建议入口,只基于主窗口当前已加载的 manifest、最近 run trace 和最近命令摘要生成聊天建议,列出 `/tasks``/trace``/review``/playtest``/run``/open-preview``/assets``/audio``/publish``/artifacts``/run-artifacts``/passes``/run-files``/internals``/logs``/agent-resume ` 等安全命令草稿方向,并提供一个首选草稿;该命令不得直接执行 Tauri 读写、启动或打开预览、读取本地文件,也不得绕过原有命令确认和 `file.read` 权限流。
- 2026-07-03 调整:普通用户通过聊天输入 `/next` 触发下一步建议入口,只基于主窗口当前已加载的 manifest、最近 run trace 和最近命令摘要生成聊天建议,列出 `/tasks``/trace``/review``/playtest``/run``/open-preview``/assets``/art``/audio``/publish``/artifacts``/run-artifacts``/passes``/run-files``/internals``/logs``/agent-resume ` 等安全命令草稿方向,并提供一个首选草稿;该命令不得直接执行 Tauri 读写、启动或打开预览、读取本地文件,也不得绕过原有命令确认和 `file.read` 权限流。
- 2026-07-03 调整:普通用户通过聊天输入 `/publish` 生成发布准备清单,只基于主窗口当前已加载的 manifest、最近 run trace、预览状态、资产来源和最近命令摘要列出原型通过、预览、任务、资产、音频、包装说明和试玩包状态,并提供 `/run``/trace``/agent-resume ``/export` 草稿;该入口不得触发 Tauri 读写、不得启动或打开预览、不得读取文件,也不得新增普通用户发布面板。
- 2026-07-03 调整:普通用户通过聊天输入 `/internals` 只列出 `.agent/manifest.json``.agent/run.latest.json``.agent/spec.md``.agent/findings.md``.agent/policy.json``.agent/project.index.json``.agent/agent.db``.agent/conversations/project.jsonl``/read` 草稿,并提供首个读取草稿;该入口不得直接读取内部文件、不得触发 Tauri 读写,也不得新增普通用户内部文件面板。
- 2026-07-03 调整:普通用户通过聊天输入 `/passes` 只从当前已加载的最近 run trace artifacts 中筛选 `.agent/passes/` 轮次产物,列出 `/read` 草稿并提供首个读取草稿;该入口不得直接读取轮次文件、不得触发 Tauri 读写,也不得新增普通用户轮次面板。
File diff suppressed because one or more lines are too long