补充美术素材聊天入口
新增 /art 美术素材摘要和草稿 补充 /next 与 /help 的美术入口覆盖 同步技术方案和项目决策记录
This commit is contained in:
@@ -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
Reference in New Issue
Block a user