补充主窗口项目摘要

新增主窗口任务资产和最近命令摘要
补充项目摘要的用户面测试
同步AI游戏创作App技术方案和共享决策记录
This commit is contained in:
AIGameCreator App
2026-07-03 02:25:44 +08:00
parent 1ad51e30c6
commit 875d9fd727
4 changed files with 110 additions and 13 deletions
+61
View File
@@ -27,6 +27,7 @@ import {
type GameCreationAgentRunStep,
type GameCreationAgentToolCallTrace,
type GameCreationAgentRunTrace,
type GameCreationAppAssetSourceKind,
type GameCreationAppManifest,
type GameCreationAppPermission,
type GameCreationAppPreviewStatus,
@@ -1786,6 +1787,12 @@ const previewStatusLabels: Record<GameCreationAppPreviewStatus, string> = {
failed: '失败',
};
const assetSourceKindLabels: Record<GameCreationAppAssetSourceKind, string> = {
uploaded: '上传',
generated: '生成',
canvas: '画板',
};
const capabilityAreaLabels: Record<
(typeof GAME_CREATION_AGENT_CAPABILITIES)[number]['area'],
string
@@ -1944,6 +1951,54 @@ function summarizeProjectStatus(
.join('\n');
}
function summarizeMainProjectHeader(
nextManifest: GameCreationAppManifest,
agents: AgentStatusCard[],
) {
const tasks = taskRowsFromManifest(nextManifest);
const completedCount = tasks.filter(
(task) => task.status === 'completed',
).length;
const readyTaskIds = new Set(
selectGameCreationAppReadyTasks({ tasks }).map((task) => task.id),
);
for (const agent of agents) {
if (agent.taskGraphState === 'ready') {
readyTaskIds.add(agent.taskId);
}
}
const sourceCounts = nextManifest.assets.reduce(
(counts, asset) => {
counts[asset.source.kind] += 1;
return counts;
},
{
uploaded: 0,
generated: 0,
canvas: 0,
} satisfies Record<GameCreationAppAssetSourceKind, number>,
);
const sourceSummary = (
Object.keys(sourceCounts) as GameCreationAppAssetSourceKind[]
)
.filter((source) => sourceCounts[source] > 0)
.map((source) => `${assetSourceKindLabels[source]} ${sourceCounts[source]}`)
.join(' / ');
const commandRuns = nextManifest.commandRuns ?? [];
const latestCommandRun = commandRuns[commandRuns.length - 1];
return [
`任务:已完成 ${completedCount}/${tasks.length} · ready ${readyTaskIds.size}`,
`资产:${nextManifest.assets.length}${
sourceSummary ? ` · ${sourceSummary}` : ''
}`,
latestCommandRun
? `最近命令:${latestCommandRun.commandId} ${
latestCommandRun.status === 'completed' ? '完成' : '失败'
}`
: '最近命令:暂无',
].join(' · ');
}
function summarizeProjectFiles(files: LocalProjectFileEntry[]) {
if (files.length === 0) {
return '本地项目还没有文件。';
@@ -9189,6 +9244,9 @@ export function App() {
manifest,
agentRunTrace ?? agentRunHistory[0]?.trace ?? null,
);
const mainProjectSummary = localProject
? summarizeMainProjectHeader(manifest, agentStatusCards)
: null;
const visibleAgentRunHistory = agentRunHistory.slice(
0,
agentRunHistoryVisibleCount,
@@ -9339,6 +9397,9 @@ export function App() {
) : null}
<small>run: {agentRunStatus}</small>
<small>preview: {previewStatus}</small>
{mainProjectSummary ? (
<small aria-label="项目摘要">{mainProjectSummary}</small>
) : null}
<small>{workspaceStatus}</small>
</div>
<div className="chat-header-actions">
@@ -200,6 +200,7 @@ describe('AI 游戏创作 App 界面边界', () => {
expect(screen.getByRole('button', { name: '配置' })).not.toBeNull();
expect(screen.getByRole('button', { name: 'LLM状态' })).not.toBeNull();
expect(screen.getByRole('button', { name: '显示目录' })).not.toBeNull();
expect(screen.queryByLabelText('项目摘要')).toBeNull();
expect(
(screen.getByRole('button', { name: '灵感草稿' }) as HTMLButtonElement)
.disabled,
@@ -1665,6 +1666,22 @@ describe('AI 游戏创作 App 界面边界', () => {
localPath: 'assets/uploads/hero.txt',
source: { kind: 'uploaded' },
});
manifest.assets.push({
id: 'asset-canvas',
kind: 'canvas',
mediaType: 'image/png',
localPath: 'assets/canvas-sync/tiles.png',
source: { kind: 'canvas', canvasProjectId: 'canvas-1' },
});
manifest.commandRuns = [
{
commandId: 'game.static_smoke',
status: 'completed',
output: 'static smoke passed',
logPath: '.agent/logs/command.log',
updatedAt: 1700000003,
},
];
const trace: GameCreationAgentRunTrace = {
schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
runId: 'run-main-shortcut-trace',
@@ -1705,7 +1722,7 @@ describe('AI 游戏创作 App 界面边界', () => {
],
taskGraph: {
goal: '做一个厨房弹幕游戏',
readyTaskIds: [],
readyTaskIds: ['art-asset-plan'],
activeTaskIds: [],
carriedTaskIds: [],
repairFocus: [],
@@ -1915,6 +1932,21 @@ describe('AI 游戏创作 App 界面边界', () => {
expect(screen.getByText('未命名游戏原型')).not.toBeNull();
expect(screen.getByText('/tmp/authorized-game')).not.toBeNull();
expect(screen.getByText('preview: 未启动')).not.toBeNull();
const projectSummary = screen.getByLabelText('项目摘要');
expect(projectSummary.textContent).toContain('任务:已完成 0/');
expect(projectSummary.textContent).toContain('ready');
expect(projectSummary.textContent).toContain(
'资产:2 个 · 上传 1 / 画板 1',
);
expect(projectSummary.textContent).toContain(
'最近命令:game.static_smoke 完成',
);
expect(invoke).not.toHaveBeenCalledWith(
'get_local_game_manifest',
expect.anything(),
);
expect(screen.queryByLabelText('开发环境')).toBeNull();
expect(screen.queryByText('编排 Trace')).toBeNull();
fireEvent.click(screen.getByRole('button', { name: '能力' }));
expect(await screen.findByText(/Native Runtime 能力/)).not.toBeNull();
@@ -11572,16 +11604,19 @@ describe('AI 游戏创作 App 界面边界', () => {
submitChat('/status');
expect(await screen.findByText(/项目:未命名游戏原型/)).not.toBeNull();
expect(screen.getByText(/目录:\/tmp\/authorized-game/)).not.toBeNull();
expect(screen.getByText(/任务:已完成 1,待处理 15/)).not.toBeNull();
expect(screen.getByText(/资产:1 个/)).not.toBeNull();
expect(
screen.getByText(/预览:运行中 http:\/\/127\.0\.0\.1:3210\//),
).not.toBeNull();
expect(
screen.getByText(/最近命令:game\.static_smoke · 完成/),
).not.toBeNull();
const statusMessage = await screen.findByText((_, element) =>
element?.classList.contains('message--assistant') === true &&
element.textContent?.includes('项目:未命名游戏原型') === true &&
element.textContent.includes('目录:/tmp/authorized-game'),
);
expect(statusMessage.textContent).toContain('任务:已完成 1,待处理 15');
expect(statusMessage.textContent).toContain('资产:1 个');
expect(statusMessage.textContent).toContain(
'预览:运行中 http://127.0.0.1:3210/',
);
expect(statusMessage.textContent).toContain(
'最近命令:game.static_smoke · 完成',
);
expect(invoke).toHaveBeenCalledWith('get_local_game_manifest', {
projectPath: '/tmp/authorized-game',
commandId: 'project.status',
@@ -3855,6 +3855,7 @@
- 2026-06-25 调整:`.agent/run.latest.json``.agent/runs/<runId>.json` 必须记录 loop 的 `maxPasses``stopReason`,开发窗口直接展示该状态,避免只从 summary 文案推断 loop 是否跑满、通过、返工、写入产物或进入预览。本地 HTTP 预览的 `/` 映射到 `game/index.html`,路径解析必须 canonicalize 项目根目录和目标文件,只允许访问项目内 `game/``assets/`,拒绝 `memory/``.agent/``exports/``..`、反斜杠和符号链接越界;常见图片、音频、视频和 Web 资源必须返回对应 MIME。这样上传和画板回流资产能被生成游戏引用,但记忆、trace 和导出包不会被预览服务暴露。
- 2026-06-26 调整,2026-07-03 更新:AI 游戏创作 App 借鉴 Harbour 的控制平面思想,但不搬 Harbour 后台。最近 run 在 `.agent/run.latest.json` 增加可选 `lifecycleStatus`,并通过 `/agent-status``/agent-kill``/agent-retry``/agent-resume [说明]` 控制本地生命周期,写入 `.agent/activity.jsonl``.agent/output.jsonl``.agent/context.bundle.json`;聊天里的状态 / 控制结果可填入 `/read .agent/output.jsonl` 草稿继续查看 run 输出,但不直接读取文件或绕过 `file.read` 策略。v1 的 kill/retry/resume 只更新本地状态和上下文包,不伪装成能中断已发出的上游 LLM 请求;后续引入独立 runner 后再把 `pending` 接入 claim。
- 2026-07-03 调整:主窗口 Agent 状态栏新增“继续说明”,只把 `/agent-resume ` 填入聊天输入框,让用户补充说明后再走原确认流;策略快捷入口新增 conversation.read / conversation.write 确认草稿,同样只填输入框,不直接写 `.agent/policy.json`
- 2026-07-03 调整:主窗口 header 常驻项目摘要只从当前已加载的 manifest / trace 派生任务完成数、ready 数、资产来源分布和最近命令结果;未选择工作区时不显示,不为了摘要额外触发 Tauri 读取或写入,也不把任务、文件、run history 或预览开发面板搬进普通用户窗口。
- 2026-06-25 调整:本地 HTTP 预览静态 `HEAD` 必须返回与 `GET` 相同的真实 `Content-Length`,但不返回 body;浏览器、图片、音频和视频探测不能拿到 `Content-Length: 0` 的假响应。
- 2026-06-25 调整:普通用户通过聊天输入 `/run` 触发待确认 `game.run_local`,确认后只能复用白名单 `game.static_smoke` 自检当前 `game/index.html`,通过后启动 `127.0.0.1` 本地 HTTP 预览。独立执行 `game.static_smoke` 时如果已有 `.agent/run.latest.json`,必须追加 `Playtest / game.static_smoke` trace step,避免“运行了代码但编排 trace 不可见”。
- 2026-06-25 调整:普通用户通过聊天输入 `/trace` 触发只读 `agent.trace_read`,读取 `.agent/run.latest.json` 并在聊天里摘要 loop 轮次、stopReason、nextStep、active / carry-over 任务、repairRoutes、agent 建议命令和最近 step。trace 面板仍只在开发窗口展示,普通用户窗口不新增面板。
File diff suppressed because one or more lines are too long