补充下一轮小步清单聊天入口
新增 /todo 下一轮小步清单摘要 补充 /next 与 /help 的小步清单入口覆盖 验证 /todo 不触发文件读取、运行、预览或导出命令
This commit is contained in:
@@ -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(
|
||||
'Run:run-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();
|
||||
|
||||
Reference in New Issue
Block a user