diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index 44ec4ffb1..4eff7552c 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -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 = { failed: '失败', }; +const assetSourceKindLabels: Record = { + 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, + ); + 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} run: {agentRunStatus} preview: {previewStatus} + {mainProjectSummary ? ( + {mainProjectSummary} + ) : null} {workspaceStatus}
diff --git a/apps/ai-game-creator-shell/tests/appSurface.test.ts b/apps/ai-game-creator-shell/tests/appSurface.test.ts index 488f9107a..43025e9ea 100644 --- a/apps/ai-game-creator-shell/tests/appSurface.test.ts +++ b/apps/ai-game-creator-shell/tests/appSurface.test.ts @@ -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', diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index af3cb3d14..746ea9fa9 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -3855,6 +3855,7 @@ - 2026-06-25 调整:`.agent/run.latest.json` 和 `.agent/runs/.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 面板仍只在开发窗口展示,普通用户窗口不新增面板。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index aebe214d5..99a39aa16 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -100,7 +100,7 @@ game-project/ ## v1 验收 - 用户能创建本地 Web 游戏项目。 -- 用户侧看到当前工作区项目名 / 路径、最近 run 与预览状态、聊天框、上传入口、Agent 状态列表和单 Agent 对话;最近项目资产入口显示本地路径、kind、mediaType 和来源类型,并可一键填入 `/read` 草稿,开发环境通过独立窗口查看任务拆分、专业组细节、产物、文件面板、嵌入预览和错误日志。 +- 用户侧看到当前工作区项目名 / 路径、最近 run、预览状态、任务完成数 / ready 数、资产数量 / 来源分布、最近命令摘要、聊天框、上传入口、Agent 状态列表和单 Agent 对话;最近项目资产入口显示本地路径、kind、mediaType 和来源类型,并可一键填入 `/read` 草稿,开发环境通过独立窗口查看任务拆分、专业组细节、产物、文件面板、嵌入预览和错误日志。 - 生成代码和资产进入用户本地项目目录。 - 本地 HTTP 预览能启动,并在外部浏览器展示可玩原型。 - 美术/音乐资产能从画板链路回流到本地项目。 @@ -111,7 +111,7 @@ game-project/ ## v1 验收证据矩阵 -- `npm run ai-game-creator-shell:check`:覆盖壳 typecheck、聊天命令单测、用户 / 开发窗口 UI 边界 smoke、主窗口命令按钮复用 `/help` 命令列表、灵感草稿只填充输入框不提交、能力按钮复用 `/capabilities`、LLM状态按钮复用 `/llm-status`、项目状态按钮复用 `/status`、权限按钮复用 `/policy` 且策略草稿按钮只填入 `/policy-confirm preview.start` / `/policy-confirm agent.run_status`、审计按钮复用 `/audit`、资产按钮复用 `/assets` 且资产结果可一键复用 `/read`、任务按钮复用 `/tasks`、Trace 按钮复用 `/trace`、文件按钮复用 `/files` 且文件结果可一键复用 `/read`、索引按钮复用 `/index`、记忆 / 短期记忆 / 黑板按钮复用 `/memory long|short|blackboard`、快照按钮复用 `/checkpoint`、快照列表按钮复用 `/checkpoints` 且 checkpoint 结果可一键复用 `/diff` / `/restore`、历史按钮复用 `/history`、受限命令白名单按钮复用 `/commands` 且无需项目初始化、静态自检快捷按钮复用 `/smoke`、预览状态快捷按钮复用 `/preview-status`、主窗口运行时配置面板读写 Tauri 配置目录中的 `game-creator.config.json`、支持全局与每个 agent 单独选择 LLM Provider 且不把 API Key 写入聊天、聊天侧 `/capabilities` 展示标准 Agent 能力清单且不打开开发面板、聊天侧 `/audit` 从 manifest / 本地文件 / `.agent/run.latest.json` 分别汇总用户面、6 组任务配置、6 组协作证据、任务编排、loop、记忆、本地产物、HTTP 预览、画板回流和权限日志证据且不打开开发面板;未生成 `.agent/run.latest.json` 前,`/audit` 只能标记任务配置通过,不能把 6 组协作证据误判为通过;trace 已存在但状态为 `failed`、`needs-revision`、`running`、`max-passes-exhausted` 或缺少 `Evaluator passed` 步骤时,`/audit` 不能把 loop 误判为通过。聊天侧 `/llm-status` 只显示 base_url / model / API Key 已读取状态且不泄露密钥本体、聊天侧长期记忆查看 / 追加 / 覆盖 / 删除的授权本地项目路径、上传资产写入后的 manifest 刷新和 `/assets` 聊天可见性、`/smoke` 聊天侧确认后只通过授权本地项目路径执行白名单 `game.static_smoke`、`/run` 聊天侧确认后通过授权本地项目路径执行 `game.static_smoke`、启动 `127.0.0.1` 本地预览并交给外部浏览器、`/preview` 聊天侧确认后通过授权本地项目路径启动 `127.0.0.1` 本地预览并交给外部浏览器、`/status` 聊天侧项目 / 任务 / 资产 / 预览 / 最近命令汇总、`/files` 聊天侧本地项目文件列表、`/read` 聊天侧文件读取的授权本地项目路径、`/tasks` 聊天侧任务拆分与下一步专业组展示的授权本地项目路径、聊天确认生成后实时展示 Planner / Orchestrator / 角色 brief / Generator / Evaluator / 写盘 / 自检进度,并自动读取 `.agent/run.latest.json` 在普通聊天消息里展示 Run、LLM 对话、loop 轮次、工具调用、active / carry-over 任务、返工焦点、编排轮次、最近步骤、画板同步建议命令和本地产物快照、`/trace` 聊天侧读取 `.agent/run.latest.json` 并展示 loop 轮次 / active 任务 / 返工路线 / dependency waves 的授权本地项目路径、`platform-agent` 编排测试、共享契约测试、Tauri 本地能力测试和无密钥本地 provider 端到端 smoke;用于证明独立 App、真实 LLM-compatible loop、本地落盘、自检和 HTTP 预览闭环,并覆盖 loop 跑满 3 轮失败时不会写入最终游戏产物。 +- `npm run ai-game-creator-shell:check`:覆盖壳 typecheck、聊天命令单测、用户 / 开发窗口 UI 边界 smoke、主窗口命令按钮复用 `/help` 命令列表、主窗口项目摘要从 manifest / trace 派生任务完成数、ready 数、资产来源分布和最近命令且未选项目时不显示、灵感草稿只填充输入框不提交、能力按钮复用 `/capabilities`、LLM状态按钮复用 `/llm-status`、项目状态按钮复用 `/status`、权限按钮复用 `/policy` 且策略草稿按钮只填入 `/policy-confirm preview.start` / `/policy-confirm agent.run_status`、审计按钮复用 `/audit`、资产按钮复用 `/assets` 且资产结果可一键复用 `/read`、任务按钮复用 `/tasks`、Trace 按钮复用 `/trace`、文件按钮复用 `/files` 且文件结果可一键复用 `/read`、索引按钮复用 `/index`、记忆 / 短期记忆 / 黑板按钮复用 `/memory long|short|blackboard`、快照按钮复用 `/checkpoint`、快照列表按钮复用 `/checkpoints` 且 checkpoint 结果可一键复用 `/diff` / `/restore`、历史按钮复用 `/history`、受限命令白名单按钮复用 `/commands` 且无需项目初始化、静态自检快捷按钮复用 `/smoke`、预览状态快捷按钮复用 `/preview-status`、主窗口运行时配置面板读写 Tauri 配置目录中的 `game-creator.config.json`、支持全局与每个 agent 单独选择 LLM Provider 且不把 API Key 写入聊天、聊天侧 `/capabilities` 展示标准 Agent 能力清单且不打开开发面板、聊天侧 `/audit` 从 manifest / 本地文件 / `.agent/run.latest.json` 分别汇总用户面、6 组任务配置、6 组协作证据、任务编排、loop、记忆、本地产物、HTTP 预览、画板回流和权限日志证据且不打开开发面板;未生成 `.agent/run.latest.json` 前,`/audit` 只能标记任务配置通过,不能把 6 组协作证据误判为通过;trace 已存在但状态为 `failed`、`needs-revision`、`running`、`max-passes-exhausted` 或缺少 `Evaluator passed` 步骤时,`/audit` 不能把 loop 误判为通过。聊天侧 `/llm-status` 只显示 base_url / model / API Key 已读取状态且不泄露密钥本体、聊天侧长期记忆查看 / 追加 / 覆盖 / 删除的授权本地项目路径、上传资产写入后的 manifest 刷新和 `/assets` 聊天可见性、`/smoke` 聊天侧确认后只通过授权本地项目路径执行白名单 `game.static_smoke`、`/run` 聊天侧确认后通过授权本地项目路径执行 `game.static_smoke`、启动 `127.0.0.1` 本地预览并交给外部浏览器、`/preview` 聊天侧确认后通过授权本地项目路径启动 `127.0.0.1` 本地预览并交给外部浏览器、`/status` 聊天侧项目 / 任务 / 资产 / 预览 / 最近命令汇总、`/files` 聊天侧本地项目文件列表、`/read` 聊天侧文件读取的授权本地项目路径、`/tasks` 聊天侧任务拆分与下一步专业组展示的授权本地项目路径、聊天确认生成后实时展示 Planner / Orchestrator / 角色 brief / Generator / Evaluator / 写盘 / 自检进度,并自动读取 `.agent/run.latest.json` 在普通聊天消息里展示 Run、LLM 对话、loop 轮次、工具调用、active / carry-over 任务、返工焦点、编排轮次、最近步骤、画板同步建议命令和本地产物快照、`/trace` 聊天侧读取 `.agent/run.latest.json` 并展示 loop 轮次 / active 任务 / 返工路线 / dependency waves 的授权本地项目路径、`platform-agent` 编排测试、共享契约测试、Tauri 本地能力测试和无密钥本地 provider 端到端 smoke;用于证明独立 App、真实 LLM-compatible loop、本地落盘、自检和 HTTP 预览闭环,并覆盖 loop 跑满 3 轮失败时不会写入最终游戏产物。 - 主窗口策略快捷入口只填入 `/policy-confirm preview.start`、`/policy-confirm agent.run_status`、`/policy-confirm conversation.read` 或 `/policy-confirm conversation.write` 草稿;Agent 状态栏的“继续说明”只填入 `/agent-resume ` 草稿,不直接触发 run 生命周期写入。 - 主窗口最近 checkpoint 列表展示 checkpoint id、文件数、大小和创建时间,同时提供直接对比、填入 `/diff`、确认回滚和填入 `/restore`;回滚继续走 `project.restore` 确认卡,不直接写项目文件。 - `npm run check:native-shells`:覆盖 AI 游戏创作壳的 release/dev 窗口边界、正式用户 App 不嵌入游戏预览 iframe、用户侧预览命令交给外部浏览器和 Tauri release `--no-bundle` 构建 smoke;用于证明正式发布只登记 `launcher` 启动器窗口,选择工作区后才关闭启动器并打开 `main` 主窗口,开发面板只在 debug/dev 路径打开,独立壳能完成 release 编译。