补充下一轮小步清单聊天入口

新增 /todo 下一轮小步清单摘要

补充 /next 与 /help 的小步清单入口覆盖

验证 /todo 不触发文件读取、运行、预览或导出命令
This commit is contained in:
AIGameCreator App
2026-07-04 10:52:07 +08:00
parent acd6715589
commit 160598d8f9
2 changed files with 208 additions and 0 deletions
+117
View File
@@ -1889,6 +1889,7 @@ const chatCommandHelp = [
'/timeline:查看项目活动时间线',
'/handoff:生成当前项目交接摘要',
'/next:查看下一步建议',
'/todo:查看下一轮小步清单',
'/publish:查看发布准备清单',
'/listing:准备作品页文案清单',
'/playtest:查看试玩状态与下一步',
@@ -3028,6 +3029,104 @@ function summarizeProjectAcceptanceCriteria(
};
}
function summarizeProjectTodoList(
nextManifest: GameCreationAppManifest,
trace: GameCreationAgentRunTrace | null,
) {
const manifestTasks = taskRowsFromManifest(nextManifest);
const traceTasks = trace?.taskGraph.tasks ?? [];
const taskSource = traceTasks.length > 0 ? traceTasks : manifestTasks;
const manifestTasksById = new Map(
manifestTasks.map((task) => [task.id, task]),
);
const taskSourceById = new Map(taskSource.map((task) => [task.id, task]));
const selectedTasks: Array<{
task: GameCreationAppTaskState;
marker: string;
}> = [];
const seenTaskIds = new Set<string>();
const addTask = (taskId: string, marker: string) => {
if (seenTaskIds.has(taskId)) {
return;
}
const task = taskSourceById.get(taskId) ?? manifestTasksById.get(taskId);
if (!task) {
return;
}
selectedTasks.push({ task, marker });
seenTaskIds.add(task.id);
};
taskSource
.filter((task) => task.status === 'failed')
.forEach((task) => addTask(task.id, '失败'));
trace?.taskGraph.activeTaskIds.forEach((taskId) =>
addTask(taskId, 'active'),
);
trace?.taskGraph.carriedTaskIds.forEach((taskId) =>
addTask(taskId, 'carry'),
);
trace?.taskGraph.readyTaskIds.forEach((taskId) => addTask(taskId, 'ready'));
if (selectedTasks.length === 0) {
selectGameCreationAppReadyTasks({ tasks: manifestTasks }).forEach((task) =>
addTask(task.id, 'ready'),
);
}
if (selectedTasks.length === 0) {
taskSource
.filter((task) => task.status !== 'completed')
.slice(0, 5)
.forEach((task) => addTask(task.id, taskStatusLabels[task.status]));
}
const visibleTasks = selectedTasks.slice(0, 5);
const taskLines = visibleTasks.map(({ task, marker }, index) => {
const acceptance =
task.acceptanceCriteria.length > 0
? task.acceptanceCriteria[0]
: '暂无';
const artifact = task.artifacts[0] ?? '暂无';
return `- ${index + 1}. ${marker}${taskGroupLabels[task.group]} / ${task.role} ${task.title}(${task.id}) · ${taskStatusLabels[task.status]} · 验收:${acceptance} · 产物:${artifact}`;
});
if (selectedTasks.length > visibleTasks.length) {
taskLines.push(
`- 还有 ${selectedTasks.length - visibleTasks.length} 个候选任务`,
);
}
const blockedTrace =
trace?.lifecycleStatus === 'killed' ||
trace?.status === 'failed' ||
trace?.status === 'needs-revision' ||
trace?.stopReason === 'max-passes-exhausted';
let draftCommand = selectedTasks.length > 0 ? '/tasks' : '/next';
let draftCommandLabel = selectedTasks.length > 0 ? '查看任务' : '查看下一步';
if (blockedTrace) {
draftCommand = '/review';
draftCommandLabel = '查看评审';
}
return {
text: [
'下一轮小步:',
`- 项目:${nextManifest.name}`,
trace ? `- Run${trace.runId} · ${formatAgentRunStatus(trace)}` : null,
trace?.nextStep ? `- 编排下一步:${trace.nextStep}` : null,
taskLines.length > 0
? `- 小步清单:\n${taskLines.join('\n')}`
: '- 小步清单:暂无待处理任务',
'- 边界:只整理下一步;不读取任务文件;不启动 run;不修改项目',
`- 建议:${draftCommand}`,
]
.filter(Boolean)
.join('\n'),
draftCommand,
draftCommandLabel,
};
}
function summarizeProjectGroupProgress(
nextManifest: GameCreationAppManifest,
trace: GameCreationAgentRunTrace | null,
@@ -4232,6 +4331,7 @@ function summarizeNextProjectActions(
}
addSuggestion('查看质量检查清单', '/qa');
addSuggestion('查看最近生成变更', '/changes');
addSuggestion('查看下一轮小步清单', '/todo');
if (nextManifest.assets.length > 0) {
addSuggestion(`查看 ${nextManifest.assets.length} 个本地资产`, '/assets');
@@ -8273,6 +8373,23 @@ export function App() {
return;
}
if (prompt === '/todo') {
if (!requireChatProjectForUserAction()) {
return;
}
const summary = summarizeProjectTodoList(manifest, agentRunTrace);
setMessages((current) => [
...current,
{
role: 'assistant',
text: summary.text,
draftCommand: summary.draftCommand,
draftCommandLabel: summary.draftCommandLabel,
},
]);
return;
}
if (prompt === '/publish') {
const nextProjectPath = requireChatProjectForUserAction();
if (!nextProjectPath) {
@@ -3863,6 +3863,95 @@ describe('AI 游戏创作 App 界面边界', () => {
),
).toHaveLength(fileReadCountBeforeInternals);
const commandCountsBeforeTodo = {
manifestRead: invoke.mock.calls.filter(
([command]) => command === 'get_local_game_manifest',
).length,
fileRead: invoke.mock.calls.filter(
([command]) => command === 'read_local_project_file',
).length,
fileList: invoke.mock.calls.filter(
([command]) => command === 'list_local_project_files',
).length,
runLocal: invoke.mock.calls.filter(
([command]) => command === 'run_limited_local_command',
).length,
startPreview: invoke.mock.calls.filter(
([command]) => command === 'start_local_game_preview',
).length,
openPreview: invoke.mock.calls.filter(
([command]) => command === 'open_local_game_preview',
).length,
exportPackage: invoke.mock.calls.filter(
([command]) => command === 'export_local_project_package',
).length,
exportList: invoke.mock.calls.filter(
([command]) => command === 'list_local_project_export_packages',
).length,
};
submitChat('/todo');
const todoMessages = await screen.findAllByText(/下一轮小步:/);
const todoMessage = todoMessages[todoMessages.length - 1];
expect(todoMessage.textContent).toContain('项目:未命名游戏原型');
expect(todoMessage.textContent).toContain(
'Runrun-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed',
);
expect(todoMessage.textContent).toContain('编排下一步:preview');
expect(todoMessage.textContent).toContain(
'1. ready:美术组 / Asset 规划美术资产(art-asset-plan) · 待处理 · 验收:角色、场景、UI 和动画需求已映射到画板或本地资产 · 产物:assets/manifest.art.json',
);
expect(todoMessage.textContent).toContain(
'边界:只整理下一步;不读取任务文件;不启动 run;不修改项目',
);
expect(todoMessage.textContent).toContain('建议:/tasks');
const todoMessageList = document.querySelector('.message-list');
expect(todoMessageList).not.toBeNull();
const todoDraftButtons = within(
todoMessageList as HTMLElement,
).getAllByRole('button', { name: '查看任务' });
fireEvent.click(todoDraftButtons[todoDraftButtons.length - 1]);
expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/tasks');
expect(
invoke.mock.calls.filter(
([command]) => command === 'get_local_game_manifest',
),
).toHaveLength(commandCountsBeforeTodo.manifestRead);
expect(
invoke.mock.calls.filter(
([command]) => command === 'read_local_project_file',
),
).toHaveLength(commandCountsBeforeTodo.fileRead);
expect(
invoke.mock.calls.filter(
([command]) => command === 'list_local_project_files',
),
).toHaveLength(commandCountsBeforeTodo.fileList);
expect(
invoke.mock.calls.filter(
([command]) => command === 'run_limited_local_command',
),
).toHaveLength(commandCountsBeforeTodo.runLocal);
expect(
invoke.mock.calls.filter(
([command]) => command === 'start_local_game_preview',
),
).toHaveLength(commandCountsBeforeTodo.startPreview);
expect(
invoke.mock.calls.filter(
([command]) => command === 'open_local_game_preview',
),
).toHaveLength(commandCountsBeforeTodo.openPreview);
expect(
invoke.mock.calls.filter(
([command]) => command === 'export_local_project_package',
),
).toHaveLength(commandCountsBeforeTodo.exportPackage);
expect(
invoke.mock.calls.filter(
([command]) => command === 'list_local_project_export_packages',
),
).toHaveLength(commandCountsBeforeTodo.exportList);
const runLocalCountBeforeNext = invoke.mock.calls.filter(
([command]) => command === 'run_limited_local_command',
).length;
@@ -3888,6 +3977,7 @@ describe('AI 游戏创作 App 界面边界', () => {
expect(screen.getByText(/查看专业组进度:\/groups/)).not.toBeNull();
expect(screen.getByText(/查看质量检查清单:\/qa/)).not.toBeNull();
expect(screen.getByText(/查看最近生成变更:\/changes/)).not.toBeNull();
expect(screen.getByText(/查看下一轮小步清单:\/todo/)).not.toBeNull();
expect(screen.getByText(/查看素材署名与来源:\/credits/)).not.toBeNull();
expect(screen.getByText(/查看美术素材:\/art/)).not.toBeNull();
expect(screen.getByText(/查看发布准备清单:\/publish/)).not.toBeNull();
@@ -10197,6 +10287,7 @@ describe('AI 游戏创作 App 界面边界', () => {
expect(screen.getByText(/\/timeline:查看项目活动时间线/)).not.toBeNull();
expect(screen.getByText(/\/handoff:生成当前项目交接摘要/)).not.toBeNull();
expect(screen.getByText(/\/next:查看下一步建议/)).not.toBeNull();
expect(screen.getByText(/\/todo:查看下一轮小步清单/)).not.toBeNull();
expect(screen.getByText(/\/publish:查看发布准备清单/)).not.toBeNull();
expect(screen.getByText(/\/listing:准备作品页文案清单/)).not.toBeNull();
expect(screen.getByText(/\/playtest:查看试玩状态与下一步/)).not.toBeNull();