补齐AI游戏创作轻量读取草稿入口
为Trace产物和checkpoint补充只填草稿的读取与回滚入口 补充Agent输出、活动和上下文包读取草稿入口 补充黑板覆盖与清空草稿入口 同步AI游戏创作App文档和共享决策记录
This commit is contained in:
@@ -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(/Run:run-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
Reference in New Issue
Block a user