补齐AI游戏创作轻量读取草稿入口

为Trace产物和checkpoint补充只填草稿的读取与回滚入口

补充Agent输出、活动和上下文包读取草稿入口

补充黑板覆盖与清空草稿入口

同步AI游戏创作App文档和共享决策记录
This commit is contained in:
AIGameCreator App
2026-07-03 01:56:01 +08:00
parent e3fd0d483d
commit 425743820a
4 changed files with 162 additions and 11 deletions
+79 -1
View File
@@ -3123,6 +3123,14 @@ function commandDraftFromAgentRunTrace(trace: GameCreationAgentRunTrace) {
);
}
function readableArtifactPathFromAgentRunTrace(
trace: GameCreationAgentRunTrace,
) {
return trace.artifacts.find((artifact) =>
isSafeProjectRelativePath(artifact.path),
)?.path;
}
function localAgentConversationReceipt(agent: AgentStatusCard) {
return `已记录给 ${agent.title}。下一次生成会把这条对话作为该 agent 的上下文读取。`;
}
@@ -3256,9 +3264,16 @@ export function summarizeAgentRunTrace(trace: GameCreationAgentRunTrace) {
}
function summarizeAgentRunCompletionForChat(trace: GameCreationAgentRunTrace) {
const suggestedCommand = commandDraftFromAgentRunTrace(trace);
const readableArtifactPath = readableArtifactPathFromAgentRunTrace(trace);
const artifactReadCommand = readableArtifactPath
? `/read ${readableArtifactPath}`
: undefined;
return {
text: `${summarizeAgentRunTrace(trace)}\n完整 trace/trace`,
draftCommand: commandDraftFromAgentRunTrace(trace) ?? undefined,
draftCommand: suggestedCommand ?? artifactReadCommand,
draftCommandLabel:
suggestedCommand || !artifactReadCommand ? undefined : '读取首个产物',
};
}
@@ -9487,6 +9502,22 @@ export function App() {
>
</button>
<button
type="button"
disabled={!localProject}
onClick={() => prepareChatCommandDraft('/memory-set blackboard ')}
>
</button>
<button
type="button"
disabled={!localProject}
onClick={() =>
prepareChatCommandDraft('/forget-memory blackboard')
}
>
</button>
<button
type="button"
disabled={!localProject}
@@ -9647,6 +9678,15 @@ export function App() {
>
</button>
<button
type="button"
aria-label={`填入对比 ${checkpoint.checkpointId}`}
onClick={() =>
prepareChatCommandDraft(`/diff ${checkpoint.checkpointId}`)
}
>
</button>
<button
type="button"
aria-label={`回滚 ${checkpoint.checkpointId}`}
@@ -9659,6 +9699,17 @@ export function App() {
>
</button>
<button
type="button"
aria-label={`填入回滚 ${checkpoint.checkpointId}`}
onClick={() =>
prepareChatCommandDraft(
`/restore ${checkpoint.checkpointId}`,
)
}
>
</button>
</div>
))}
</div>
@@ -9817,6 +9868,33 @@ export function App() {
>
</button>
<button
type="button"
disabled={!localProject}
onClick={() =>
prepareChatCommandDraft('/read .agent/output.jsonl')
}
>
</button>
<button
type="button"
disabled={!localProject}
onClick={() =>
prepareChatCommandDraft('/read .agent/activity.jsonl')
}
>
</button>
<button
type="button"
disabled={!localProject}
onClick={() =>
prepareChatCommandDraft('/read .agent/context.bundle.json')
}
>
</button>
<button
type="button"
disabled={!localProject}
@@ -263,6 +263,14 @@ describe('AI 游戏创作 App 界面边界', () => {
(screen.getByRole('button', { name: '记到黑板' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '覆盖黑板' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '清空黑板' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '快照' }) as HTMLButtonElement)
.disabled,
@@ -323,6 +331,18 @@ describe('AI 游戏创作 App 界面边界', () => {
(screen.getByRole('button', { name: '继续' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '输出' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '活动' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '上下文包' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(
screen.getByRole('button', {
@@ -1640,7 +1660,13 @@ describe('AI 游戏创作 App 界面边界', () => {
],
},
],
artifacts: [],
artifacts: [
{
path: 'exports/README.md',
sizeBytes: 128,
checksum: 'fnv1a64:exports',
},
],
taskGraph: {
goal: '做一个厨房弹幕游戏',
readyTaskIds: [],
@@ -1917,6 +1943,17 @@ describe('AI 游戏创作 App 界面边界', () => {
expect(
await screen.findByText(/Runrun-main-shortcut-trace/),
).not.toBeNull();
expect(screen.getByText(/产物快照:/)).not.toBeNull();
expect(
screen.getByText(/exports\/README\.md · fnv1a64:exports/),
).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '读取首个产物' }));
expect(composerInput).toHaveProperty('value', '/read exports/README.md');
expect(invoke).not.toHaveBeenCalledWith('read_local_project_file', {
projectPath: '/tmp/authorized-game',
relativePath: 'exports/README.md',
commandId: 'file.read',
});
fireEvent.click(screen.getByRole('button', { name: '文件' }));
expect(await screen.findByText(/本地项目文件:/)).not.toBeNull();
@@ -1962,6 +1999,10 @@ describe('AI 游戏创作 App 界面边界', () => {
expect(screen.getByText(/跨 agent 共享约束/)).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '记到黑板' }));
expect(composerInput).toHaveProperty('value', '/remember blackboard ');
fireEvent.click(screen.getByRole('button', { name: '覆盖黑板' }));
expect(composerInput).toHaveProperty('value', '/memory-set blackboard ');
fireEvent.click(screen.getByRole('button', { name: '清空黑板' }));
expect(composerInput).toHaveProperty('value', '/forget-memory blackboard');
fireEvent.click(screen.getByRole('button', { name: '快照' }));
expect(await screen.findByText('project.checkpoint')).not.toBeNull();
@@ -1982,6 +2023,18 @@ describe('AI 游戏创作 App 界面边界', () => {
name: '对比 checkpoint-main',
}),
).not.toBeNull();
fireEvent.click(
within(checkpointList).getByRole('button', {
name: '填入对比 checkpoint-main',
}),
);
expect(composerInput).toHaveProperty('value', '/diff checkpoint-main');
fireEvent.click(
within(checkpointList).getByRole('button', {
name: '填入回滚 checkpoint-main',
}),
);
expect(composerInput).toHaveProperty('value', '/restore checkpoint-main');
fireEvent.click(
within(checkpointList).getByRole('button', {
name: '回滚 checkpoint-main',
@@ -13997,6 +14050,26 @@ describe('AI 游戏创作 App 界面边界', () => {
await screen.findByText('已设置本地项目:/tmp/authorized-game'),
).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '输出' }));
expect(screen.getByLabelText('创作想法')).toHaveProperty(
'value',
'/read .agent/output.jsonl',
);
fireEvent.click(screen.getByRole('button', { name: '活动' }));
expect(screen.getByLabelText('创作想法')).toHaveProperty(
'value',
'/read .agent/activity.jsonl',
);
fireEvent.click(screen.getByRole('button', { name: '上下文包' }));
expect(screen.getByLabelText('创作想法')).toHaveProperty(
'value',
'/read .agent/context.bundle.json',
);
expect(invoke).not.toHaveBeenCalledWith(
'read_local_project_file',
expect.objectContaining({ relativePath: '.agent/context.bundle.json' }),
);
submitChat('/agent-status');
expect(
await screen.findByText(
@@ -21,7 +21,7 @@
- 背景:AI 游戏创作 App 已有 Godcoder 式本地工程护栏、项目黑板、角色私有记忆、manifest 和 run trace;新增结构化对话记录、agent 状态列表和单 agent 对话入口时,需要避免引入平行状态源或提前承诺后台 runner 能力。
- 决策:v1 结构化对话记录统一使用本地 `.agent/conversations/` append-only JSONL。普通聊天写 `.agent/conversations/project.jsonl`;从 agent 状态列表进入单个 agent 后,用户消息、agent 回复、工具建议和错误只写对应 `.agent/conversations/agents/<agentId>.jsonl`。Agent 状态列表从 `.agent/manifest.json` 的任务 / 角色清单和 `.agent/run.latest.json` / `.agent/runs/<runId>.json` 的 step、taskGraph、passPlans、lifecycleStatus 派生,并把 `taskGraph.tasks` 的任务状态与 active / carry-over / ready 编排标记显示在主窗口和单 agent 对话入口中;单 agent 最近证据里的安全相对输入 / 输出路径只填入 `/read <path>` 草稿,仍由用户发送并走既有 `file.read` / `agent.trace_read` 权限流。不新增独立状态数据库。项目黑板和角色私有记忆继续只保存稳定摘要,不承载原始对话流水。
- 补充:App 启动先进入独立启动器窗口;用户选择工作区后,Tauri 关闭启动器并打开主窗口,主窗口读取该工作区的主 conversation、manifest 和 run trace。启动器“新建项目”先调用 `init_local_game_project` 初始化成功,默认项目名取目标文件夹名,再写最近项目并打开主窗口;遇到非空目录时先弹确认,取消或初始化失败则不打开主窗口、不写最近项目。最近工作区只保存在本机 WebView storage,可单项移除或清空,不进入项目文件或共享记忆;已初始化项目优先显示 manifest 项目名并保留路径副信息,`.agent/run.latest.json` 可读时显示最近 run 状态。最近项目路径缺失、不是目录、缺少可读 `.agent/manifest.json` 或检查失败时禁用打开,刷新只重新执行只读检查;“显示”只用系统文件管理器打开已确认存在的本地目录,未初始化但存在的目录也可显示,避免把历史路径误当新项目重建。
- 补充:主窗口“显示目录”复用同一只读目录打开能力,只打开当前本地项目目录,不初始化项目、不写项目文件、不切换工作区;主窗口头部只读显示 manifest 项目名、项目路径、最近 `.agent/run.latest.json` 的 run 状态摘要和当前预览状态,并通过“刷新状态”重新读取同一 trace,不新增状态数据库。最近项目资产入口只读展示 localPath、kind、mediaType 和 source.kind,点击仍走原 `file.read` 权限流;旁边的“读取命令”只填入 `/read <path>` 草稿,不直接读取文件或绕过权限。主窗口“记到黑板”同样只填入 `/remember blackboard ` 草稿,项目黑板写入仍走聊天确认。
- 补充:主窗口“显示目录”复用同一只读目录打开能力,只打开当前本地项目目录,不初始化项目、不写项目文件、不切换工作区;主窗口头部只读显示 manifest 项目名、项目路径、最近 `.agent/run.latest.json` 的 run 状态摘要和当前预览状态,并通过“刷新状态”重新读取同一 trace,不新增状态数据库。最近项目资产入口只读展示 localPath、kind、mediaType 和 source.kind,点击仍走原 `file.read` 权限流;旁边的“读取命令”只填入 `/read <path>` 草稿,不直接读取文件或绕过权限。主窗口“记到黑板”“覆盖黑板”“清空黑板”只填入项目黑板相关 `/remember``/memory-set``/forget-memory` 草稿,项目黑板写入或删除仍走聊天确认Agent 状态栏的 output / activity / context bundle 入口也只填入 `/read` 草稿,不直接读取 run 辅助文件
- 补充:启动器和主窗口共用同一个运行时配置弹窗,配置只读写 Tauri 应用配置目录中的 `game-creator.config.json`,不写入项目文件或对话历史。
- 补充:启动器“打开”只进入已初始化且 `.agent/manifest.json` 可读的 AI 游戏项目;路径不存在、不是文件夹或只是普通文件夹时不打开主窗口、不创建目录,用户需要创建或初始化时走“新建项目”。
- 影响范围:`apps/ai-game-creator-shell` 的主窗口 agent 状态列表、单 agent 对话入口、本地项目文件结构、共享契约和 AI 游戏创作 App 实施计划。
@@ -55,7 +55,7 @@
## 2026-06-26 AI 游戏创作 App 生成过程必须在聊天可见
- 背景:普通用户窗口只保留聊天入口,但如果生成确认后只显示“已生成草案”和本地产物路径,真实 LLM / Agent loop 会被误解成固定模板落盘。
- 决策:`game.generate_draft` 保持正式用户窗口不展示开发面板,但必须通过聊天实时显示 Planner LLM、Orchestrator、6 组角色 brief、Generator LLM、Evaluator、ArtifactWriter 和自检进度;生成完成后普通聊天消息直接展示 `.agent/run.latest.json` 的 Run、LLM 对话、loop 轮次、active / carry-over 任务、编排轮次、最近步骤、建议命令和本地产物快照,`/trace` 继续读取同一份完整证据。
- 决策:`game.generate_draft` 保持正式用户窗口不展示开发面板,但必须通过聊天实时显示 Planner LLM、Orchestrator、6 组角色 brief、Generator LLM、Evaluator、ArtifactWriter 和自检进度;生成完成后普通聊天消息直接展示 `.agent/run.latest.json` 的 Run、LLM 对话、loop 轮次、active / carry-over 任务、编排轮次、最近步骤、建议命令和本地产物快照;没有同步建议命令时,首个安全产物只提供 `/read` 草稿`/trace` 继续读取同一份完整证据。
- 影响范围:`apps/ai-game-creator-shell/src/App.tsx``apps/ai-game-creator-shell/src-tauri/src/main.rs`、AI 游戏创作 App 聊天体验和实施计划文档。
- 验证方式:运行 `npm run ai-game-creator-shell:check``npm run check:encoding``git diff --check`
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`
@@ -3864,7 +3864,7 @@
- 2026-06-24 调整:同一本地项目多次 `game.generate_draft` 必须追加 `memory/session.md``memory/project.md`,不得覆盖历史对话和创作目标记录。
- 2026-07-01 调整:AI 游戏创作 App 在 `memory/session.md``memory/project.md` 之外新增项目级黑板 `memory/blackboard.md`,只记录重要跨 agent 决策、依赖和风险摘要;每个角色 agent 拥有私有记忆 `memory/agents/<group>/<role>.md`。角色 brief 必须读取自己的私有记忆和项目黑板;`game.generate_draft` 通过 Evaluator 与 `game.static_smoke` 后,追加项目黑板摘要和各角色成功产出摘要,不得覆盖既有记忆。失败 run 仍只保留 trace 和 pass 快照,不写最终记忆摘要。
- 2026-07-01 调整:AI 游戏创作 App 借鉴 Godcoder 的本地工程护栏,但只收敛到五项本地机制:`ArtifactWriter` 写入前 checkpoint、写入后 diff、用户确认 restore;进入 LLM 前过滤密钥和本机配置痕迹;`.agent/agent.db` 继续作为轻量 JSONL 项目索引,`/index` 额外刷新 `.agent/project.index.json`;同一项目写入通过 `.agent/project.lock` 串行化;`.agent/policy.json` 记录项目级命令拒绝 / 确认策略。v1 不引入通用 IDE 插件、云工作区、SQLite 或任意 shell 代理。
- 2026-07-03 调整:主窗口最近 checkpoint 列表必须直接展示 checkpoint id、文件数、大小和创建时间,并提供填充对比与确认回滚的轻量操作;回滚仍走 `project.restore` 确认卡,不在列表按钮中直接写项目文件。
- 2026-07-03 调整:主窗口最近 checkpoint 列表必须直接展示 checkpoint id、文件数、大小和创建时间,并提供直接对比、填入 `/diff`、确认回滚和填入 `/restore` 的轻量操作;回滚仍走 `project.restore` 确认卡,不在列表按钮中直接写项目文件。
- 2026-06-24 调整:普通用户通过聊天输入 `/help` 发现可用内置命令;命令发现必须留在聊天消息里,不得因此暴露开发面板。
- 2026-06-24 调整:聊天区待确认命令的日志语义必须区分 `permission.pending``permission.confirm``permission.cancel`;待确认卡片必须展示本地写入目标路径,避免用户在不知道落盘位置时确认。
- 2026-06-24 调整:普通用户通过聊天输入 `/status` 读取 `.agent/manifest.json` 的项目状态摘要,只在聊天消息里展示项目目录、任务状态、资产数量、预览状态和最近命令;不得为了状态查看暴露任务、文件或日志面板。
File diff suppressed because one or more lines are too long