增强游戏创作智能体生成过程可见性
在聊天中实时展示 Planner、Orchestrator、Generator 和 Evaluator 进度 将生成完成摘要切换为完整 run trace 和 LLM 对话证据 补齐质量评审任务、测试覆盖和实施计划文档
This commit is contained in:
@@ -31,6 +31,7 @@ use shared_contracts::game_creation_app::{
|
||||
GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, GAME_CREATION_AGENT_TOOL_CALL_MAX,
|
||||
GAME_CREATION_APP_COMMANDS, GAME_CREATION_APP_LIMITED_RUN_COMMANDS,
|
||||
};
|
||||
use tauri::Emitter;
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Serialize)]
|
||||
@@ -69,6 +70,14 @@ struct GenerateLocalGameDraftResult {
|
||||
manifest: GameCreationAppManifest,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct GameCreatorAgentProgressEvent {
|
||||
project_path: String,
|
||||
stage: String,
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct GameCreatorLlmConfigStatus {
|
||||
@@ -460,7 +469,7 @@ static AUDIO_AGENT_ROLES: [AgentRoleDefinition; 2] = [
|
||||
},
|
||||
];
|
||||
|
||||
static CODE_AGENT_ROLES: [AgentRoleDefinition; 4] = [
|
||||
static CODE_AGENT_ROLES: [AgentRoleDefinition; 5] = [
|
||||
AgentRoleDefinition {
|
||||
id: "director",
|
||||
role: "Director",
|
||||
@@ -475,6 +484,13 @@ static CODE_AGENT_ROLES: [AgentRoleDefinition; 4] = [
|
||||
tool_id: "agent.role.brief.code.code",
|
||||
brief_path_name: "code.md",
|
||||
},
|
||||
AgentRoleDefinition {
|
||||
id: "review",
|
||||
role: "Review",
|
||||
task_id: "quality-review",
|
||||
tool_id: "agent.role.brief.code.review",
|
||||
brief_path_name: "review.md",
|
||||
},
|
||||
AgentRoleDefinition {
|
||||
id: "preview",
|
||||
role: "Preview",
|
||||
@@ -540,7 +556,7 @@ const GAME_CREATOR_AGENT_GROUP_DEFINITIONS: [AgentGroupDefinition; 6] = [
|
||||
AgentGroupDefinition {
|
||||
id: "code",
|
||||
label: "程序组",
|
||||
role: "Director + Code + Preview + Playtest",
|
||||
role: "Director + Code + Review + Preview + Playtest",
|
||||
brief_path_name: "code.md",
|
||||
roles: &CODE_AGENT_ROLES,
|
||||
},
|
||||
@@ -740,10 +756,16 @@ fn get_local_game_manifest(project_path: String) -> Result<GameCreationAppManife
|
||||
|
||||
#[tauri::command]
|
||||
async fn generate_local_game_draft(
|
||||
app: tauri::AppHandle,
|
||||
project_path: String,
|
||||
prompt: String,
|
||||
) -> Result<GenerateLocalGameDraftResult, String> {
|
||||
generate_local_game_draft_at(Path::new(project_path.trim()), prompt.trim()).await
|
||||
generate_local_game_draft_at(
|
||||
Path::new(project_path.trim()),
|
||||
prompt.trim(),
|
||||
Some(&AgentProgressEmitter::new(&app, project_path.trim())),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -1049,6 +1071,7 @@ fn append_agent_db_record(root: &Path, mut record: serde_json::Value) -> Result<
|
||||
async fn generate_local_game_draft_at(
|
||||
root: &Path,
|
||||
prompt: &str,
|
||||
progress: Option<&AgentProgressEmitter<'_>>,
|
||||
) -> Result<GenerateLocalGameDraftResult, String> {
|
||||
let prompt = prompt.trim();
|
||||
if prompt.is_empty() {
|
||||
@@ -1060,14 +1083,41 @@ async fn generate_local_game_draft_at(
|
||||
let long_memory = read_optional_text(&root.join("memory/project.md"))?;
|
||||
let asset_context = render_local_asset_prompt_context(root)?;
|
||||
let long_memory = append_prompt_context(&asset_context, &long_memory);
|
||||
emit_agent_progress(
|
||||
progress,
|
||||
"llm.planner",
|
||||
"Planner 正在调用 LLM 整理规格和专业组分工",
|
||||
);
|
||||
let client = build_game_creator_llm_client_from_env()?;
|
||||
let loop_result =
|
||||
run_game_creator_agent_loop_at(root, &client, prompt, &short_memory, &long_memory).await?;
|
||||
let loop_result = run_game_creator_agent_loop_at(
|
||||
root,
|
||||
&client,
|
||||
prompt,
|
||||
&short_memory,
|
||||
&long_memory,
|
||||
progress,
|
||||
)
|
||||
.await?;
|
||||
let mut loop_result = loop_result;
|
||||
emit_agent_progress(
|
||||
progress,
|
||||
"artifact.write",
|
||||
"ArtifactWriter 正在写入本地代码、数值、美术、音乐和发布草案",
|
||||
);
|
||||
let mut result = write_local_game_draft_at(root, prompt, &loop_result.draft)?;
|
||||
append_local_artifact_write_step(root, prompt, &mut loop_result)?;
|
||||
emit_agent_progress(
|
||||
progress,
|
||||
"playtest.static_smoke",
|
||||
"Playtest 正在运行 game.static_smoke 自检",
|
||||
);
|
||||
append_static_smoke_step(root, prompt, &mut loop_result)?;
|
||||
append_agent_loop_log(root, &loop_result)?;
|
||||
emit_agent_progress(
|
||||
progress,
|
||||
"agent.complete",
|
||||
"Agent loop 已通过 Evaluator 和静态自检,正在启动本地预览",
|
||||
);
|
||||
result.manifest = read_manifest_for_project(root)?;
|
||||
Ok(result)
|
||||
}
|
||||
@@ -1342,6 +1392,37 @@ fn check_game_creator_llm_config_values(
|
||||
}
|
||||
}
|
||||
|
||||
struct AgentProgressEmitter<'a> {
|
||||
app: &'a tauri::AppHandle,
|
||||
project_path: String,
|
||||
}
|
||||
|
||||
impl<'a> AgentProgressEmitter<'a> {
|
||||
fn new(app: &'a tauri::AppHandle, project_path: &str) -> Self {
|
||||
Self {
|
||||
app,
|
||||
project_path: project_path.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn emit(&self, stage: &str, message: &str) {
|
||||
let _ = self.app.emit(
|
||||
"game-creator-agent-progress",
|
||||
GameCreatorAgentProgressEvent {
|
||||
project_path: self.project_path.clone(),
|
||||
stage: stage.to_string(),
|
||||
message: message.to_string(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_agent_progress(progress: Option<&AgentProgressEmitter<'_>>, stage: &str, message: &str) {
|
||||
if let Some(progress) = progress {
|
||||
progress.emit(stage, message);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
async fn request_llm_game_draft_with_client(
|
||||
client: &LlmClient,
|
||||
@@ -1368,6 +1449,7 @@ async fn run_game_creator_agent_loop_at(
|
||||
prompt: &str,
|
||||
short_memory: &str,
|
||||
long_memory: &str,
|
||||
progress: Option<&AgentProgressEmitter<'_>>,
|
||||
) -> Result<GameCreatorAgentLoopResult, String> {
|
||||
let spec_path = root.join(".agent/spec.md");
|
||||
let findings_path = root.join(".agent/findings.md");
|
||||
@@ -1378,6 +1460,7 @@ async fn run_game_creator_agent_loop_at(
|
||||
.map(|spec| render_planner_spec(prompt, &spec))?;
|
||||
fs::write(&spec_path, &planner_spec)
|
||||
.map_err(|error| format!("写入 Planner 规格失败:{}: {error}", spec_path.display()))?;
|
||||
emit_agent_progress(progress, "llm.planner.done", "Planner 规格已生成");
|
||||
steps.push(with_task_context(
|
||||
agent_trace_step(
|
||||
0,
|
||||
@@ -1419,6 +1502,11 @@ async fn run_game_creator_agent_loop_at(
|
||||
|
||||
let mut last_error = "Evaluator 未产出可用结果".to_string();
|
||||
for pass in 1..=GAME_CREATOR_AGENT_LOOP_MAX_PASSES {
|
||||
emit_agent_progress(
|
||||
progress,
|
||||
"agent.orchestrator",
|
||||
&format!("Orchestrator 正在规划第 {pass} 轮任务图"),
|
||||
);
|
||||
let spec_markdown = read_optional_text(&spec_path)?;
|
||||
let findings_markdown = read_optional_text(&findings_path)?;
|
||||
let agenda = write_agent_pass_agenda(root, pass, &findings_markdown)?;
|
||||
@@ -1477,22 +1565,38 @@ async fn run_game_creator_agent_loop_at(
|
||||
findings_path.display()
|
||||
)
|
||||
})?;
|
||||
steps.push(agent_trace_step(
|
||||
pass,
|
||||
"Evaluator",
|
||||
"needs-revision",
|
||||
&[],
|
||||
&[".agent/findings.md"],
|
||||
"记录专业组协作失败原因",
|
||||
"file.write.findings",
|
||||
steps.push(with_task_context(
|
||||
agent_trace_step(
|
||||
pass,
|
||||
"Evaluator",
|
||||
"needs-revision",
|
||||
&[],
|
||||
&[".agent/findings.md"],
|
||||
"记录专业组协作失败原因",
|
||||
"file.write.findings",
|
||||
),
|
||||
"code",
|
||||
"Review",
|
||||
Some("quality-review"),
|
||||
"evaluation",
|
||||
));
|
||||
last_error = issues.join(";");
|
||||
write_agent_run_trace(root, &run_id, prompt, "needs-revision", pass, &steps, None)?;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
emit_agent_progress(
|
||||
progress,
|
||||
"agent.role_briefs",
|
||||
&format!("6 组角色 brief 已完成,第 {pass} 轮交给 Generator"),
|
||||
);
|
||||
append_group_brief_steps(root, pass, &agenda.relative_path, &group_briefs, &mut steps);
|
||||
let group_briefs_markdown = render_agent_group_briefs_context(&group_briefs);
|
||||
emit_agent_progress(
|
||||
progress,
|
||||
"llm.generator",
|
||||
&format!("Generator 正在调用 LLM 生成第 {pass} 轮可运行草案"),
|
||||
);
|
||||
match request_generator_game_draft_with_client(
|
||||
client,
|
||||
prompt,
|
||||
@@ -1506,6 +1610,11 @@ async fn run_game_creator_agent_loop_at(
|
||||
.await
|
||||
{
|
||||
Ok(draft) => {
|
||||
emit_agent_progress(
|
||||
progress,
|
||||
"llm.generator.done",
|
||||
&format!("Generator 第 {pass} 轮草案已返回,Evaluator 开始质量评审"),
|
||||
);
|
||||
let pass_artifacts = write_agent_pass_artifacts(root, pass, &draft)?;
|
||||
let mut generator_input_paths = vec![
|
||||
"memory/session.md".to_string(),
|
||||
@@ -1536,30 +1645,42 @@ async fn run_game_creator_agent_loop_at(
|
||||
findings_path.display()
|
||||
)
|
||||
})?;
|
||||
steps.push(agent_trace_step_owned(
|
||||
pass,
|
||||
"Evaluator",
|
||||
if issues.is_empty() {
|
||||
"passed"
|
||||
} else {
|
||||
"needs-revision"
|
||||
},
|
||||
vec![
|
||||
pass_artifacts.game_html.clone(),
|
||||
pass_artifacts.design_markdown.clone(),
|
||||
pass_artifacts.balance_json.clone(),
|
||||
pass_artifacts.art_manifest_json.clone(),
|
||||
pass_artifacts.audio_manifest_json.clone(),
|
||||
],
|
||||
vec![".agent/findings.md".to_string()],
|
||||
if issues.is_empty() {
|
||||
"静态验收通过"
|
||||
} else {
|
||||
"发现问题,要求下一轮 Generator 修复"
|
||||
},
|
||||
"evaluator.static_html_check",
|
||||
steps.push(with_task_context(
|
||||
agent_trace_step_owned(
|
||||
pass,
|
||||
"Evaluator",
|
||||
if issues.is_empty() {
|
||||
"passed"
|
||||
} else {
|
||||
"needs-revision"
|
||||
},
|
||||
vec![
|
||||
pass_artifacts.game_html.clone(),
|
||||
pass_artifacts.design_markdown.clone(),
|
||||
pass_artifacts.balance_json.clone(),
|
||||
pass_artifacts.art_manifest_json.clone(),
|
||||
pass_artifacts.audio_manifest_json.clone(),
|
||||
pass_artifacts.publish_readme.clone(),
|
||||
],
|
||||
vec![".agent/findings.md".to_string()],
|
||||
if issues.is_empty() {
|
||||
"质量评审通过:玩法、资产、数值、程序和发布包装可进入预览试玩"
|
||||
} else {
|
||||
"质量评审发现问题,要求下一轮 Generator 修复"
|
||||
},
|
||||
"evaluator.quality_review",
|
||||
),
|
||||
"code",
|
||||
"Review",
|
||||
Some("quality-review"),
|
||||
"evaluation",
|
||||
));
|
||||
if issues.is_empty() {
|
||||
emit_agent_progress(
|
||||
progress,
|
||||
"evaluator.passed",
|
||||
&format!("Evaluator 第 {pass} 轮质量评审通过"),
|
||||
);
|
||||
write_agent_run_trace(root, &run_id, prompt, "passed", pass, &steps, None)?;
|
||||
return Ok(GameCreatorAgentLoopResult {
|
||||
run_id,
|
||||
@@ -1571,6 +1692,11 @@ async fn run_game_creator_agent_loop_at(
|
||||
});
|
||||
}
|
||||
last_error = issues.join(";");
|
||||
emit_agent_progress(
|
||||
progress,
|
||||
"evaluator.needs_revision",
|
||||
&format!("Evaluator 第 {pass} 轮要求返工:{last_error}"),
|
||||
);
|
||||
write_agent_run_trace(root, &run_id, prompt, "needs-revision", pass, &steps, None)?;
|
||||
}
|
||||
Err(error) => {
|
||||
@@ -1591,14 +1717,20 @@ async fn run_game_creator_agent_loop_at(
|
||||
findings_path.display()
|
||||
)
|
||||
})?;
|
||||
steps.push(agent_trace_step(
|
||||
pass,
|
||||
"Evaluator",
|
||||
"needs-revision",
|
||||
&[],
|
||||
&[".agent/findings.md"],
|
||||
"记录 Generator 失败原因",
|
||||
"file.write.findings",
|
||||
steps.push(with_task_context(
|
||||
agent_trace_step(
|
||||
pass,
|
||||
"Evaluator",
|
||||
"needs-revision",
|
||||
&[],
|
||||
&[".agent/findings.md"],
|
||||
"记录 Generator 失败原因",
|
||||
"file.write.findings",
|
||||
),
|
||||
"code",
|
||||
"Review",
|
||||
Some("quality-review"),
|
||||
"evaluation",
|
||||
));
|
||||
last_error = issues.join(";");
|
||||
write_agent_run_trace(root, &run_id, prompt, "needs-revision", pass, &steps, None)?;
|
||||
@@ -3119,11 +3251,13 @@ fn build_agent_run_task_graph_trace(
|
||||
&tasks,
|
||||
"preview-readiness",
|
||||
GameCreationAppTaskStatus::Completed,
|
||||
) && !steps.iter().any(|step| {
|
||||
step.task_id.as_deref() == Some("preview-playtest")
|
||||
&& step.phase == "preview"
|
||||
&& step.status == "running"
|
||||
}) {
|
||||
)
|
||||
&& !steps.iter().any(|step| {
|
||||
step.task_id.as_deref() == Some("preview-playtest")
|
||||
&& step.phase == "preview"
|
||||
&& step.status == "running"
|
||||
})
|
||||
{
|
||||
set_task_status_if_current(
|
||||
&mut tasks,
|
||||
"preview-playtest",
|
||||
@@ -3271,7 +3405,9 @@ fn task_status_from_agent_step(
|
||||
}
|
||||
|
||||
match step.phase.as_str() {
|
||||
"planning" | "handoff" | "playtest" => Some(GameCreationAppTaskStatus::Completed),
|
||||
"planning" | "handoff" | "playtest" | "evaluation" => {
|
||||
Some(GameCreationAppTaskStatus::Completed)
|
||||
}
|
||||
"preview" => Some(GameCreationAppTaskStatus::Running),
|
||||
"role-brief" if role_brief_completes_task(task_id) => {
|
||||
Some(GameCreationAppTaskStatus::Completed)
|
||||
@@ -3284,12 +3420,12 @@ fn task_status_from_agent_step(
|
||||
fn role_brief_completes_task(task_id: &str) -> bool {
|
||||
matches!(
|
||||
task_id,
|
||||
"balance-director"
|
||||
| "art-director"
|
||||
| "art-polish"
|
||||
| "audio-director"
|
||||
| "code-director"
|
||||
| "publish-strategy"
|
||||
"balance-director"
|
||||
| "art-director"
|
||||
| "art-polish"
|
||||
| "audio-director"
|
||||
| "code-director"
|
||||
| "publish-strategy"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5300,6 +5436,7 @@ fn record_draft_task_progress(
|
||||
"audio-asset-plan",
|
||||
"code-director",
|
||||
"code-prototype",
|
||||
"quality-review",
|
||||
] {
|
||||
set_task_status(
|
||||
&mut manifest,
|
||||
@@ -5734,7 +5871,8 @@ fn run_cli_command(command: CliCommand) -> Result<(), String> {
|
||||
.enable_all()
|
||||
.build()
|
||||
.map_err(|error| format!("创建 CLI runtime 失败:{error}"))?;
|
||||
let result = runtime.block_on(generate_local_game_draft_at(&project_path, &prompt))?;
|
||||
let result =
|
||||
runtime.block_on(generate_local_game_draft_at(&project_path, &prompt, None))?;
|
||||
let (preview, stop) = start_local_game_preview_for_project(&project_path)?;
|
||||
record_preview_state(
|
||||
&project_path,
|
||||
@@ -6324,7 +6462,7 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true
|
||||
std::env::set_var("GENARRATIVE_GAME_CREATOR_LLM_BASE_URL", base_url);
|
||||
std::env::set_var("GENARRATIVE_GAME_CREATOR_LLM_MODEL", "mock-game-model");
|
||||
|
||||
let result = generate_local_game_draft_at(&root, "用上传角色图做主角").await;
|
||||
let result = generate_local_game_draft_at(&root, "用上传角色图做主角", None).await;
|
||||
|
||||
restore_env("GENARRATIVE_GAME_CREATOR_LLM_API_KEY", previous_api_key);
|
||||
restore_env("GENARRATIVE_GAME_CREATOR_LLM_BASE_URL", previous_base_url);
|
||||
@@ -6407,6 +6545,7 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true
|
||||
"做一个月光厨房弹幕游戏",
|
||||
"",
|
||||
"# 项目长期记忆\n",
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("agent loop");
|
||||
@@ -6471,7 +6610,7 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true
|
||||
assert!(first_agenda.contains("mode: initial"));
|
||||
assert!(first_agenda.contains("activeTasks: design-director"));
|
||||
assert!(first_agenda.contains("wave 1: design-director"));
|
||||
assert!(first_agenda.contains("wave 11: publish-package"));
|
||||
assert!(first_agenda.contains("wave 12: publish-package"));
|
||||
let first_task_graph: Value = serde_json::from_str(
|
||||
&fs::read_to_string(root.join(".agent/passes/pass-1/task-graph.json"))
|
||||
.expect("task graph 1"),
|
||||
@@ -6620,7 +6759,7 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true
|
||||
.iter()
|
||||
.any(|task_id| task_id == "code-prototype")));
|
||||
let steps = trace["steps"].as_array().unwrap();
|
||||
assert_eq!(steps.len(), 64);
|
||||
assert_eq!(steps.len(), 66);
|
||||
assert_eq!(steps[0]["agent"], "Planner");
|
||||
assert_eq!(steps[0]["phase"], "planning");
|
||||
assert_eq!(steps[0]["taskId"], "design-director");
|
||||
@@ -6793,7 +6932,7 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true
|
||||
assert_eq!(run_history["runId"], trace["runId"]);
|
||||
assert_eq!(run_history["status"], "passed");
|
||||
assert_eq!(run_history["stopReason"], "evaluator-passed");
|
||||
assert_eq!(run_history["steps"].as_array().unwrap().len(), 64);
|
||||
assert_eq!(run_history["steps"].as_array().unwrap().len(), 66);
|
||||
let manifest: Value =
|
||||
serde_json::from_str(&fs::read_to_string(root.join(".agent/manifest.json")).unwrap())
|
||||
.expect("manifest json");
|
||||
@@ -6838,7 +6977,7 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true
|
||||
std::env::set_var("GENARRATIVE_GAME_CREATOR_LLM_BASE_URL", base_url);
|
||||
std::env::set_var("GENARRATIVE_GAME_CREATOR_LLM_MODEL", "mock-game-model");
|
||||
|
||||
let error = generate_local_game_draft_at(&root, "做一个会失败三轮的厨房游戏")
|
||||
let error = generate_local_game_draft_at(&root, "做一个会失败三轮的厨房游戏", None)
|
||||
.await
|
||||
.expect_err("max-pass failure should bubble out");
|
||||
|
||||
@@ -6990,7 +7129,7 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true
|
||||
assert_eq!(manifest["projectId"], "project-1");
|
||||
assert_eq!(manifest["name"], "像素动作原型");
|
||||
assert_eq!(manifest["assets"].as_array().unwrap().len(), 0);
|
||||
assert_eq!(manifest["tasks"].as_array().unwrap().len(), 15);
|
||||
assert_eq!(manifest["tasks"].as_array().unwrap().len(), 16);
|
||||
assert_eq!(manifest["tasks"][0]["id"], "design-director");
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
@@ -7058,6 +7197,7 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true
|
||||
assert_task_status(&manifest, "audio-asset-plan", "completed");
|
||||
assert_task_status(&manifest, "code-director", "completed");
|
||||
assert_task_status(&manifest, "code-prototype", "completed");
|
||||
assert_task_status(&manifest, "quality-review", "completed");
|
||||
assert_task_status(&manifest, "preview-readiness", "waiting-for-confirmation");
|
||||
assert_task_status(&manifest, "preview-playtest", "pending");
|
||||
assert_task_status(&manifest, "publish-strategy", "pending");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type ChangeEvent, type FormEvent, useState } from 'react';
|
||||
import { type ChangeEvent, type FormEvent, useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
createGameCreationAppManifest,
|
||||
@@ -133,6 +133,12 @@ interface ChatMessage {
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface AgentProgressEvent {
|
||||
projectPath: string;
|
||||
stage: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type PendingCommand =
|
||||
| {
|
||||
id: 'game.generate_draft';
|
||||
@@ -767,7 +773,24 @@ function summarizeSuggestedToolCalls(trace: GameCreationAgentRunTrace) {
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function summarizeLlmConversation(trace: GameCreationAgentRunTrace) {
|
||||
return trace.steps
|
||||
.filter((step) =>
|
||||
step.toolCalls.some((toolCall) => toolCall.toolId.startsWith('llm.')),
|
||||
)
|
||||
.slice(-6)
|
||||
.map((step) => {
|
||||
const toolIds = step.toolCalls
|
||||
.filter((toolCall) => toolCall.toolId.startsWith('llm.'))
|
||||
.map((toolCall) => toolCall.toolId)
|
||||
.join(', ');
|
||||
return `- ${step.agent} #${step.pass} · ${step.status} · ${step.phase} · ${toolIds}`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
export function summarizeAgentRunTrace(trace: GameCreationAgentRunTrace) {
|
||||
const llmConversation = summarizeLlmConversation(trace);
|
||||
const recentSteps = trace.steps
|
||||
.slice(-5)
|
||||
.map(
|
||||
@@ -833,6 +856,7 @@ export function summarizeAgentRunTrace(trace: GameCreationAgentRunTrace) {
|
||||
`状态:${trace.status} · ${trace.passes}/${trace.maxPasses} 轮 · ${trace.stopReason}`,
|
||||
`工具调用:${trace.toolCallCount}/${trace.maxToolCalls}`,
|
||||
`下一步:${trace.nextStep}`,
|
||||
llmConversation ? `LLM 对话:\n${llmConversation}` : null,
|
||||
taskSummary ? `任务:${taskSummary}` : null,
|
||||
`active 任务:${formatTraceTaskIds(
|
||||
trace.taskGraph.activeTaskIds,
|
||||
@@ -855,43 +879,18 @@ export function summarizeAgentRunTrace(trace: GameCreationAgentRunTrace) {
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
export function summarizeAgentRunCompletion(trace: GameCreationAgentRunTrace) {
|
||||
const repair =
|
||||
trace.taskGraph.repairFocus.length > 0
|
||||
? `\n返工焦点:${trace.taskGraph.repairFocus.join(';')}`
|
||||
: '';
|
||||
const artifacts = trace.artifacts
|
||||
.filter((artifact) =>
|
||||
[
|
||||
'game/index.html',
|
||||
'game/game_design.md',
|
||||
'game/balance.json',
|
||||
'assets/manifest.art.json',
|
||||
'assets/manifest.audio.json',
|
||||
'exports/README.md',
|
||||
].includes(artifact.path),
|
||||
)
|
||||
.map((artifact) => artifact.path)
|
||||
.join(',');
|
||||
const suggestedTools = summarizeSuggestedToolCalls(trace);
|
||||
function summarizeAgentRunCompletionForChat(
|
||||
trace: GameCreationAgentRunTrace,
|
||||
) {
|
||||
return `${summarizeAgentRunTrace(trace)}\n完整 trace:/trace`;
|
||||
}
|
||||
|
||||
function gameDraftStartedMessage() {
|
||||
return [
|
||||
`Agent loop:${trace.status} · ${trace.passes}/${trace.maxPasses} 轮 · ${trace.stopReason}`,
|
||||
`工具调用:${trace.toolCallCount}/${trace.maxToolCalls}`,
|
||||
`active 任务:${formatTraceTaskIds(
|
||||
trace.taskGraph.activeTaskIds,
|
||||
trace.taskGraph.tasks,
|
||||
)}`,
|
||||
`carry-over 任务:${formatTraceTaskIds(
|
||||
trace.taskGraph.carriedTaskIds,
|
||||
trace.taskGraph.tasks,
|
||||
)}${repair}`,
|
||||
suggestedTools ? `建议命令:\n${suggestedTools}` : null,
|
||||
artifacts ? `本地产物:${artifacts}` : null,
|
||||
'完整 trace:/trace',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
'开始调用 LLM:Planner 正在整理规格。',
|
||||
'随后 Orchestrator 编排 6 组角色 brief,Generator 生成代码和资产清单,Evaluator 做质量评审。',
|
||||
'完成后会把 run trace 和本地产物摘要发回这里。',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function pendingCommandTitle(command: PendingCommand) {
|
||||
@@ -942,7 +941,7 @@ export function pendingCommandDetail(
|
||||
return `运行自检并启动 ${projectPath}/game/`;
|
||||
}
|
||||
if (command.id === 'game.generate_draft') {
|
||||
return `调用 LLM,写入 ${projectPath}/game、assets、memory、exports,通过自检后启动本地 HTTP 预览并交给外部浏览器`;
|
||||
return `调用 LLM Planner / Generator,编排 6 组角色 brief,写入 ${projectPath}/game、assets、memory、exports,通过 Evaluator 和自检后启动本地 HTTP 预览并交给外部浏览器`;
|
||||
}
|
||||
if (command.id === 'project.create') {
|
||||
return `创建 ${command.projectPath}`;
|
||||
@@ -1028,6 +1027,37 @@ export function App() {
|
||||
`${GAME_CREATION_APP_COMMANDS.length} 个命令已登记权限。`,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const listen = window.__TAURI__?.event?.listen;
|
||||
if (!listen) {
|
||||
return;
|
||||
}
|
||||
let cleanup: (() => void) | null = null;
|
||||
let disposed = false;
|
||||
void listen<AgentProgressEvent>('game-creator-agent-progress', (event) => {
|
||||
if (
|
||||
localProject &&
|
||||
event.payload.projectPath !== localProject.projectPath
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setMessages((current) => [
|
||||
...current,
|
||||
{ role: 'assistant', text: event.payload.message },
|
||||
]);
|
||||
}).then((unlisten) => {
|
||||
if (disposed) {
|
||||
unlisten();
|
||||
return;
|
||||
}
|
||||
cleanup = unlisten;
|
||||
});
|
||||
return () => {
|
||||
disposed = true;
|
||||
cleanup?.();
|
||||
};
|
||||
}, [localProject?.projectPath]);
|
||||
|
||||
function appendLocalPermissionLog(
|
||||
projectPath: string | null,
|
||||
event: 'permission.pending' | 'permission.confirm' | 'permission.cancel',
|
||||
@@ -1534,6 +1564,11 @@ export function App() {
|
||||
|
||||
try {
|
||||
setCommandLog((current) => [...current, 'game.generate_draft']);
|
||||
setProjectStatus('正在调用 LLM');
|
||||
setMessages((current) => [
|
||||
...current,
|
||||
{ role: 'assistant', text: gameDraftStartedMessage() },
|
||||
]);
|
||||
const result = await invoke<GenerateLocalGameDraftResult>(
|
||||
'generate_local_game_draft',
|
||||
{ projectPath: nextProjectPath, prompt },
|
||||
@@ -1544,7 +1579,7 @@ export function App() {
|
||||
manifest: result.manifest,
|
||||
});
|
||||
setManifest(result.manifest);
|
||||
setProjectStatus('已生成草案');
|
||||
setProjectStatus('已生成可试玩原型');
|
||||
setCommandLog((current) => [
|
||||
...current,
|
||||
'memory.write',
|
||||
@@ -3253,7 +3288,7 @@ export function App() {
|
||||
`${trace.status} · ${trace.passes}/${trace.maxPasses} 轮 · ${trace.stopReason}`,
|
||||
);
|
||||
setCommandLog((current) => [...current, `file.read ${relativePath}`]);
|
||||
return summarizeAgentRunCompletion(trace);
|
||||
return summarizeAgentRunCompletionForChat(trace);
|
||||
} catch (error) {
|
||||
setAgentRunTrace(null);
|
||||
setAgentRunStatus(error instanceof Error ? error.message : String(error));
|
||||
|
||||
@@ -8,5 +8,11 @@ interface Window {
|
||||
args?: Record<string, unknown>,
|
||||
) => Promise<Result>;
|
||||
};
|
||||
event?: {
|
||||
listen?: <Payload = unknown>(
|
||||
event: string,
|
||||
handler: (event: { payload: Payload }) => void,
|
||||
) => Promise<() => void>;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -79,13 +79,18 @@ describe('AI 游戏创作 Agent loop 摘要', () => {
|
||||
taskGraph: {
|
||||
goal: '做一个弹幕厨房游戏',
|
||||
readyTaskIds: ['code-director'],
|
||||
activeTaskIds: ['code-director', 'publish-package'],
|
||||
activeTaskIds: ['code-director', 'quality-review', 'publish-package'],
|
||||
carriedTaskIds: ['design-director'],
|
||||
repairFocus: ['gameHtml 缺少 canvas'],
|
||||
repairRoutes: [
|
||||
{
|
||||
issue: 'gameHtml 缺少 canvas',
|
||||
taskIds: ['code-director', 'preview-readiness', 'publish-package'],
|
||||
taskIds: [
|
||||
'code-director',
|
||||
'quality-review',
|
||||
'preview-readiness',
|
||||
'publish-package',
|
||||
],
|
||||
reason: 'code-repair+dependency-impact',
|
||||
},
|
||||
],
|
||||
@@ -106,14 +111,23 @@ describe('AI 游戏创作 Agent loop 摘要', () => {
|
||||
pass: 2,
|
||||
mode: 'repair',
|
||||
summary: '第 2 轮重跑程序链路及下游发布包装',
|
||||
activeTaskIds: ['code-director', 'publish-package'],
|
||||
activeTaskIds: ['code-director', 'quality-review', 'publish-package'],
|
||||
carriedTaskIds: ['design-director'],
|
||||
dependencyWaves: [['code-director'], ['publish-package']],
|
||||
dependencyWaves: [
|
||||
['code-director'],
|
||||
['quality-review'],
|
||||
['publish-package'],
|
||||
],
|
||||
repairFocus: ['gameHtml 缺少 canvas'],
|
||||
repairRoutes: [
|
||||
{
|
||||
issue: 'gameHtml 缺少 canvas',
|
||||
taskIds: ['code-director', 'preview-readiness', 'publish-package'],
|
||||
taskIds: [
|
||||
'code-director',
|
||||
'quality-review',
|
||||
'preview-readiness',
|
||||
'publish-package',
|
||||
],
|
||||
reason: 'code-repair+dependency-impact',
|
||||
},
|
||||
],
|
||||
@@ -130,21 +144,21 @@ describe('AI 游戏创作 Agent loop 摘要', () => {
|
||||
expect(summary).toContain('工具调用:12/128');
|
||||
expect(summary).toContain('任务:已完成 2');
|
||||
expect(summary).toContain(
|
||||
'active 任务:程序组 / Director 拆解程序实现(code-director), 运营组 / Publish 整理发布包装(publish-package)',
|
||||
'active 任务:程序组 / Director 拆解程序实现(code-director), 程序组 / Review 执行质量评审(quality-review), 运营组 / Publish 整理发布包装(publish-package)',
|
||||
);
|
||||
expect(summary).toContain(
|
||||
'carry-over 任务:策划组 / Director 拆解创作方向(design-director)',
|
||||
);
|
||||
expect(summary).toContain('返工焦点:gameHtml 缺少 canvas');
|
||||
expect(summary).toContain(
|
||||
'返工路线:code-repair+dependency-impact: 程序组 / Director 拆解程序实现(code-director), 程序组 / Preview 执行静态自检(preview-readiness), 运营组 / Publish 整理发布包装(publish-package)',
|
||||
'返工路线:code-repair+dependency-impact: 程序组 / Director 拆解程序实现(code-director), 程序组 / Review 执行质量评审(quality-review), 程序组 / Preview 执行静态自检(preview-readiness), 运营组 / Publish 整理发布包装(publish-package)',
|
||||
);
|
||||
expect(summary).toContain('建议命令:');
|
||||
expect(summary).toContain('agent.tool.suggest.canvas.project_sync');
|
||||
expect(summary).toContain('/sync-canvas-project <画板项目ID>');
|
||||
expect(summary).toContain('编排轮次:');
|
||||
expect(summary).toContain(
|
||||
'pass 2 · repair · active 2 · carry 1 · waves 程序组 / Director 拆解程序实现(code-director) / 运营组 / Publish 整理发布包装(publish-package)',
|
||||
'pass 2 · repair · active 3 · carry 1 · waves 程序组 / Director 拆解程序实现(code-director) / 程序组 / Review 执行质量评审(quality-review) / 运营组 / Publish 整理发布包装(publish-package)',
|
||||
);
|
||||
expect(summary).toContain('.agent/passes/pass-2/task-graph.json');
|
||||
expect(summary).toContain('Orchestrator #2 · completed · plan');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** @vitest-environment jsdom */
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
||||
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import React from 'react';
|
||||
@@ -232,9 +232,9 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
pass: 2,
|
||||
agent: 'Evaluator',
|
||||
phase: 'evaluate',
|
||||
taskId: 'preview-readiness',
|
||||
taskId: 'quality-review',
|
||||
group: 'code',
|
||||
role: 'Preview',
|
||||
role: 'Review',
|
||||
status: 'passed',
|
||||
inputPaths: ['.agent/passes/pass-2/draft.json'],
|
||||
outputPaths: ['.agent/findings.md'],
|
||||
@@ -296,7 +296,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
repairRoutes: [
|
||||
{
|
||||
issue: '缺少输入监听',
|
||||
taskIds: ['code-prototype', 'preview-readiness'],
|
||||
taskIds: ['code-prototype', 'quality-review', 'preview-readiness'],
|
||||
reason: 'code-runtime',
|
||||
},
|
||||
],
|
||||
@@ -317,14 +317,18 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
pass: 2,
|
||||
mode: 'repair',
|
||||
summary: '第 2 轮返工',
|
||||
activeTaskIds: ['code-prototype', 'preview-readiness'],
|
||||
activeTaskIds: ['code-prototype', 'quality-review', 'preview-readiness'],
|
||||
carriedTaskIds: ['design-director'],
|
||||
dependencyWaves: [['code-prototype'], ['preview-readiness']],
|
||||
dependencyWaves: [
|
||||
['code-prototype'],
|
||||
['quality-review'],
|
||||
['preview-readiness'],
|
||||
],
|
||||
repairFocus: ['缺少输入监听'],
|
||||
repairRoutes: [
|
||||
{
|
||||
issue: '缺少输入监听',
|
||||
taskIds: ['code-prototype', 'preview-readiness'],
|
||||
taskIds: ['code-prototype', 'quality-review', 'preview-readiness'],
|
||||
reason: 'code-runtime',
|
||||
},
|
||||
],
|
||||
@@ -394,7 +398,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
expect(screen.getByText(/能力\/命令契约:通过/)).not.toBeNull();
|
||||
expect(screen.getByText(/6 组任务配置:通过/)).not.toBeNull();
|
||||
expect(screen.getByText(/6 组协作证据:通过/)).not.toBeNull();
|
||||
expect(screen.getByText(/角色任务 15 个/)).not.toBeNull();
|
||||
expect(screen.getByText(/角色任务 16 个/)).not.toBeNull();
|
||||
expect(
|
||||
screen.getByText(/Loop trace:通过 · run run-chat-audit/),
|
||||
).not.toBeNull();
|
||||
@@ -646,9 +650,9 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
pass: 3,
|
||||
agent: 'Evaluator',
|
||||
phase: 'evaluate',
|
||||
taskId: 'preview-readiness',
|
||||
taskId: 'quality-review',
|
||||
group: 'code',
|
||||
role: 'Preview',
|
||||
role: 'Review',
|
||||
status: 'needs-revision',
|
||||
inputPaths: ['.agent/passes/pass-3/draft.json'],
|
||||
outputPaths: ['.agent/findings.md'],
|
||||
@@ -679,13 +683,13 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
taskGraph: {
|
||||
goal: '做一个三轮仍失败的厨房弹幕游戏',
|
||||
readyTaskIds: [],
|
||||
activeTaskIds: ['code-prototype', 'preview-readiness'],
|
||||
activeTaskIds: ['code-prototype', 'quality-review', 'preview-readiness'],
|
||||
carriedTaskIds: ['design-director'],
|
||||
repairFocus: ['缺少输入监听'],
|
||||
repairRoutes: [
|
||||
{
|
||||
issue: '缺少输入监听',
|
||||
taskIds: ['code-prototype', 'preview-readiness'],
|
||||
taskIds: ['code-prototype', 'quality-review', 'preview-readiness'],
|
||||
reason: 'code-runtime',
|
||||
},
|
||||
],
|
||||
@@ -696,14 +700,18 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
pass: 3,
|
||||
mode: 'repair',
|
||||
summary: '第 3 轮返工仍未通过',
|
||||
activeTaskIds: ['code-prototype', 'preview-readiness'],
|
||||
activeTaskIds: ['code-prototype', 'quality-review', 'preview-readiness'],
|
||||
carriedTaskIds: ['design-director'],
|
||||
dependencyWaves: [['code-prototype'], ['preview-readiness']],
|
||||
dependencyWaves: [
|
||||
['code-prototype'],
|
||||
['quality-review'],
|
||||
['preview-readiness'],
|
||||
],
|
||||
repairFocus: ['缺少输入监听'],
|
||||
repairRoutes: [
|
||||
{
|
||||
issue: '缺少输入监听',
|
||||
taskIds: ['code-prototype', 'preview-readiness'],
|
||||
taskIds: ['code-prototype', 'quality-review', 'preview-readiness'],
|
||||
reason: 'code-runtime',
|
||||
},
|
||||
],
|
||||
@@ -1294,7 +1302,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
|
||||
expect(await screen.findByText(/项目:未命名游戏原型/)).not.toBeNull();
|
||||
expect(screen.getByText(/目录:\/tmp\/authorized-game/)).not.toBeNull();
|
||||
expect(screen.getByText(/任务:已完成 1,待处理 14/)).not.toBeNull();
|
||||
expect(screen.getByText(/任务:已完成 1,待处理 15/)).not.toBeNull();
|
||||
expect(screen.getByText(/资产:1 个/)).not.toBeNull();
|
||||
expect(
|
||||
screen.getByText(/预览:运行中 http:\/\/127\.0\.0\.1:3210\//),
|
||||
@@ -1420,11 +1428,77 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
expect(screen.getByText('game.generate_draft')).not.toBeNull();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'调用 LLM,写入 /tmp/authorized-game/game、assets、memory、exports,通过自检后启动本地 HTTP 预览并交给外部浏览器',
|
||||
'调用 LLM Planner / Generator,编排 6 组角色 brief,写入 /tmp/authorized-game/game、assets、memory、exports,通过 Evaluator 和自检后启动本地 HTTP 预览并交给外部浏览器',
|
||||
),
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('streams agent generation progress into the user chat', async () => {
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'local-project-draft',
|
||||
'未命名游戏原型',
|
||||
);
|
||||
let progressHandler:
|
||||
| ((event: {
|
||||
payload: { projectPath: string; stage: string; message: string };
|
||||
}) => void)
|
||||
| null = null;
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'append_local_permission_log') {
|
||||
return {};
|
||||
}
|
||||
if (command !== 'init_local_game_project') {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
}
|
||||
const projectPath = String(args?.projectPath ?? '');
|
||||
return {
|
||||
projectPath,
|
||||
manifestPath: `${projectPath}/.agent/manifest.json`,
|
||||
manifest,
|
||||
};
|
||||
},
|
||||
);
|
||||
const listen = vi.fn(async (_event: string, handler: typeof progressHandler) => {
|
||||
progressHandler = handler;
|
||||
return vi.fn();
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke }, event: { listen } };
|
||||
renderAppAt('/');
|
||||
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已设置本地项目:/tmp/authorized-game'),
|
||||
).not.toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
progressHandler?.({
|
||||
payload: {
|
||||
projectPath: '/tmp/other-game',
|
||||
stage: 'llm.planner',
|
||||
message: '不应该显示',
|
||||
},
|
||||
});
|
||||
progressHandler?.({
|
||||
payload: {
|
||||
projectPath: '/tmp/authorized-game',
|
||||
stage: 'llm.planner',
|
||||
message: 'Planner 正在调用 LLM 整理规格和专业组分工',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(listen).toHaveBeenCalledWith(
|
||||
'game-creator-agent-progress',
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(screen.queryByText('不应该显示')).toBeNull();
|
||||
expect(
|
||||
screen.getByText('Planner 正在调用 LLM 整理规格和专业组分工'),
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('shows agent loop evidence in chat after generation completes', async () => {
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'local-project-draft',
|
||||
@@ -1449,6 +1523,77 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
goal: '做一个反弹弹幕厨房游戏',
|
||||
coordination: 'filesystem',
|
||||
steps: [
|
||||
{
|
||||
pass: 0,
|
||||
agent: 'Planner',
|
||||
phase: 'planning',
|
||||
taskId: 'design-director',
|
||||
group: 'design',
|
||||
role: 'Director',
|
||||
status: 'completed',
|
||||
inputPaths: [
|
||||
'memory/session.md',
|
||||
'memory/project.md',
|
||||
'.agent/manifest.json',
|
||||
],
|
||||
outputPaths: ['.agent/spec.md'],
|
||||
summary: '完成玩法规格和专业组分工',
|
||||
toolCalls: [
|
||||
{
|
||||
toolId: 'llm.chat.planner',
|
||||
status: 'completed',
|
||||
inputPaths: ['memory/session.md', 'memory/project.md'],
|
||||
outputPaths: ['.agent/spec.md'],
|
||||
summary: 'Planner 规格生成',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
pass: 2,
|
||||
agent: 'Generator',
|
||||
phase: 'generate',
|
||||
taskId: 'code-prototype',
|
||||
group: 'code',
|
||||
role: 'Code',
|
||||
status: 'completed',
|
||||
inputPaths: [
|
||||
'.agent/spec.md',
|
||||
'.agent/findings.md',
|
||||
'.agent/passes/pass-2/agenda.md',
|
||||
],
|
||||
outputPaths: ['.agent/passes/pass-2/draft.json'],
|
||||
summary: '生成结构化游戏草案',
|
||||
toolCalls: [
|
||||
{
|
||||
toolId: 'llm.chat.generator',
|
||||
status: 'completed',
|
||||
inputPaths: ['.agent/spec.md', '.agent/findings.md'],
|
||||
outputPaths: ['.agent/passes/pass-2/draft.json'],
|
||||
summary: 'Generator 草案生成',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
pass: 2,
|
||||
agent: 'Evaluator',
|
||||
phase: 'evaluation',
|
||||
taskId: 'quality-review',
|
||||
group: 'code',
|
||||
role: 'Review',
|
||||
status: 'passed',
|
||||
inputPaths: ['.agent/passes/pass-2/draft.json'],
|
||||
outputPaths: ['.agent/findings.md'],
|
||||
summary: '质量评审通过',
|
||||
toolCalls: [
|
||||
{
|
||||
toolId: 'evaluator.quality_review',
|
||||
status: 'completed',
|
||||
inputPaths: ['.agent/passes/pass-2/draft.json'],
|
||||
outputPaths: ['.agent/findings.md'],
|
||||
summary: 'Evaluator 质量评审',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
pass: 2,
|
||||
agent: '美术组 / Asset',
|
||||
@@ -1492,13 +1637,13 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
taskGraph: {
|
||||
goal: '做一个反弹弹幕厨房游戏',
|
||||
readyTaskIds: ['preview-playtest'],
|
||||
activeTaskIds: ['code-director', 'preview-readiness'],
|
||||
activeTaskIds: ['code-director', 'code-prototype', 'quality-review', 'preview-readiness'],
|
||||
carriedTaskIds: ['design-director', 'design-foundation'],
|
||||
repairFocus: ['缺少输入监听'],
|
||||
repairRoutes: [
|
||||
{
|
||||
issue: '缺少输入监听',
|
||||
taskIds: ['code-director', 'preview-readiness'],
|
||||
taskIds: ['code-director', 'code-prototype', 'quality-review', 'preview-readiness'],
|
||||
reason: 'code-runtime',
|
||||
},
|
||||
],
|
||||
@@ -1509,14 +1654,19 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
pass: 2,
|
||||
mode: 'repair',
|
||||
summary: '第 2 轮按 Evaluator 反馈返工',
|
||||
activeTaskIds: ['code-director', 'preview-readiness'],
|
||||
activeTaskIds: ['code-director', 'code-prototype', 'quality-review', 'preview-readiness'],
|
||||
carriedTaskIds: ['design-director', 'design-foundation'],
|
||||
dependencyWaves: [['code-director'], ['preview-readiness']],
|
||||
dependencyWaves: [
|
||||
['code-director'],
|
||||
['code-prototype'],
|
||||
['quality-review'],
|
||||
['preview-readiness'],
|
||||
],
|
||||
repairFocus: ['缺少输入监听'],
|
||||
repairRoutes: [
|
||||
{
|
||||
issue: '缺少输入监听',
|
||||
taskIds: ['code-director', 'preview-readiness'],
|
||||
taskIds: ['code-director', 'code-prototype', 'quality-review', 'preview-readiness'],
|
||||
reason: 'code-runtime',
|
||||
},
|
||||
],
|
||||
@@ -1609,17 +1759,23 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
submitChat('做一个反弹弹幕厨房游戏');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
|
||||
expect(screen.getByText(/开始调用 LLM:Planner 正在整理规格。/)).not.toBeNull();
|
||||
expect(screen.getByText(/Generator 生成代码和资产清单/)).not.toBeNull();
|
||||
expect(
|
||||
await screen.findByText(/已保存并启动本地预览:http:\/\/127\.0\.0\.1:3210\//),
|
||||
).not.toBeNull();
|
||||
expect(screen.getByText(/已交给外部浏览器打开。/)).not.toBeNull();
|
||||
expect(screen.getByText(/Run:run-chat-generate/)).not.toBeNull();
|
||||
expect(
|
||||
screen.getByText(/Agent loop:passed · 2\/3 轮 · evaluator-passed/),
|
||||
screen.getByText(/状态:passed · 2\/3 轮 · evaluator-passed/),
|
||||
).not.toBeNull();
|
||||
expect(screen.getByText(/工具调用:36\/128/)).not.toBeNull();
|
||||
expect(screen.getByText(/LLM 对话:/)).not.toBeNull();
|
||||
expect(screen.getByText(/Planner #0 · completed · planning · llm\.chat\.planner/)).not.toBeNull();
|
||||
expect(screen.getByText(/Generator #2 · completed · generate · llm\.chat\.generator/)).not.toBeNull();
|
||||
expect(
|
||||
screen.getByText(
|
||||
/active 任务:程序组 \/ Director 拆解程序实现\(code-director\), 程序组 \/ Preview 执行静态自检\(preview-readiness\)/,
|
||||
/active 任务:程序组 \/ Director 拆解程序实现\(code-director\), 程序组 \/ Code 生成可运行原型\(code-prototype\), 程序组 \/ Review 执行质量评审\(quality-review\), 程序组 \/ Preview 执行静态自检\(preview-readiness\)/,
|
||||
),
|
||||
).not.toBeNull();
|
||||
expect(screen.getByText(/carry-over 任务:策划组/)).not.toBeNull();
|
||||
@@ -1630,11 +1786,11 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
expect(
|
||||
screen.getByText(/\/sync-canvas-project <画板项目ID>/),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
screen.getByText(
|
||||
/本地产物:game\/index\.html,game\/game_design\.md,assets\/manifest\.art\.json/,
|
||||
),
|
||||
).not.toBeNull();
|
||||
expect(screen.getByText(/编排轮次:/)).not.toBeNull();
|
||||
expect(screen.getByText(/产物快照:/)).not.toBeNull();
|
||||
expect(screen.getByText(/- game\/index\.html · fnv1a64:game/)).not.toBeNull();
|
||||
expect(screen.getByText(/最近步骤:/)).not.toBeNull();
|
||||
expect(screen.getByText(/Evaluator #2 · passed · evaluation/)).not.toBeNull();
|
||||
expect(screen.getByText(/完整 trace:\/trace/)).not.toBeNull();
|
||||
expect(screen.queryByLabelText('开发环境')).toBeNull();
|
||||
expect(screen.queryByText('编排 Trace')).toBeNull();
|
||||
@@ -1896,13 +2052,13 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
taskGraph: {
|
||||
goal: '做一个反弹弹幕厨房游戏',
|
||||
readyTaskIds: ['code-director'],
|
||||
activeTaskIds: ['code-director', 'preview-readiness'],
|
||||
activeTaskIds: ['code-director', 'code-prototype', 'quality-review', 'preview-readiness'],
|
||||
carriedTaskIds: ['design-director'],
|
||||
repairFocus: ['缺少输入监听'],
|
||||
repairRoutes: [
|
||||
{
|
||||
issue: '缺少输入监听',
|
||||
taskIds: ['code-director', 'preview-readiness'],
|
||||
taskIds: ['code-director', 'code-prototype', 'quality-review', 'preview-readiness'],
|
||||
reason: 'code-repair',
|
||||
},
|
||||
],
|
||||
@@ -1913,14 +2069,19 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
pass: 2,
|
||||
mode: 'repair',
|
||||
summary: '第 2 轮按 Evaluator 反馈返工',
|
||||
activeTaskIds: ['code-director', 'preview-readiness'],
|
||||
activeTaskIds: ['code-director', 'code-prototype', 'quality-review', 'preview-readiness'],
|
||||
carriedTaskIds: ['design-director'],
|
||||
dependencyWaves: [['code-director'], ['preview-readiness']],
|
||||
dependencyWaves: [
|
||||
['code-director'],
|
||||
['code-prototype'],
|
||||
['quality-review'],
|
||||
['preview-readiness'],
|
||||
],
|
||||
repairFocus: ['缺少输入监听'],
|
||||
repairRoutes: [
|
||||
{
|
||||
issue: '缺少输入监听',
|
||||
taskIds: ['code-director', 'preview-readiness'],
|
||||
taskIds: ['code-director', 'code-prototype', 'quality-review', 'preview-readiness'],
|
||||
reason: 'code-repair',
|
||||
},
|
||||
],
|
||||
@@ -1970,7 +2131,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
screen.getByText(
|
||||
/active 任务:程序组 \/ Director 拆解程序实现\(code-director\), 程序组 \/ Preview 执行静态自检\(preview-readiness\)/,
|
||||
/active 任务:程序组 \/ Director 拆解程序实现\(code-director\), 程序组 \/ Code 生成可运行原型\(code-prototype\), 程序组 \/ Review 执行质量评审\(quality-review\), 程序组 \/ Preview 执行静态自检\(preview-readiness\)/,
|
||||
),
|
||||
).not.toBeNull();
|
||||
expect(screen.getByText(/返工路线:code-repair/)).not.toBeNull();
|
||||
|
||||
@@ -95,7 +95,7 @@ describe('AI 游戏创作聊天记忆命令', () => {
|
||||
'/tmp/game',
|
||||
),
|
||||
).toBe(
|
||||
'调用 LLM,写入 /tmp/game/game、assets、memory、exports,通过自检后启动本地 HTTP 预览并交给外部浏览器',
|
||||
'调用 LLM Planner / Generator,编排 6 组角色 brief,写入 /tmp/game/game、assets、memory、exports,通过 Evaluator 和自检后启动本地 HTTP 预览并交给外部浏览器',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -16,6 +16,22 @@
|
||||
|
||||
---
|
||||
|
||||
## 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` 继续读取同一份完整证据。
|
||||
- 影响范围:`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`。
|
||||
|
||||
## 2026-06-26 AI 游戏创作 App 增加显式质量评审 Gate
|
||||
|
||||
- 背景:AI 游戏创作 App 已有 Evaluator loop 和静态 smoke,但任务图、能力清单和 trace 中没有单独的质检 / 评审任务,用户无法从 `/tasks`、`/trace` 或 `/audit` 看出质量评审是明确环节。
|
||||
- 决策:保持策划、美术、程序、数值、音乐、运营 6 个专业组不变,在程序组内新增 `quality-review` / `Review` 角色任务;Evaluator 的评审 step 绑定到该任务,依赖顺序为 `code-prototype -> quality-review -> preview-readiness -> preview-playtest -> publish-strategy -> publish-package`。`game.static_smoke` 只完成 `preview-readiness`,不代替质量评审。
|
||||
- 影响范围:AI 游戏创作 App 任务图、共享契约、Tauri trace / manifest 状态推导、聊天 `/capabilities` `/tasks` `/trace` `/audit` 摘要和实施计划文档。
|
||||
- 验证方式:运行 `npm run ai-game-creator-shell:check`、`npm run check:encoding` 和 `git diff --check`。
|
||||
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。
|
||||
|
||||
## 2026-06-25 AI 游戏创作 App 真实 LLM 联调用流式请求
|
||||
|
||||
- 背景:AI 游戏创作 App 的真实 OpenAI-compatible provider 验收中,小请求可返回,但 Planner 等稍长非流式请求会在上游响应前被网关空闲连接切断,表现为 TLS record 解密失败;本地无密钥 provider smoke 不能覆盖该真实网关行为。
|
||||
|
||||
@@ -86,7 +86,7 @@ game-project/
|
||||
|
||||
## v1 验收证据矩阵
|
||||
|
||||
- `npm run ai-game-creator-shell:check`:覆盖壳 typecheck、聊天命令单测、用户 / 开发窗口 UI 边界 smoke、聊天侧 `/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` 聊天侧任务拆分与下一步专业组展示的授权本地项目路径、聊天确认生成后自动读取 `.agent/run.latest.json` 并在普通聊天消息里展示 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、聊天侧 `/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 check:native-shells`:覆盖 AI 游戏创作壳的 release/dev 窗口边界、正式用户 App 不嵌入游戏预览 iframe、用户侧预览命令交给外部浏览器和 Tauri release `--no-bundle` 构建 smoke;用于证明正式用户窗口只登记 `main` 聊天窗口,开发面板只在 debug/dev 路径打开,独立壳能完成 release 编译。
|
||||
- `npm run check:encoding` 与 `git diff --check`:覆盖中文文档、中文命令文案和补丁空白;用于避免乱码、尾随空白和无关格式漂移。
|
||||
- `npm run ai-game-creator-shell:llm-status`:只检查 LLM 环境变量是否就绪,不请求上游、不显示 API Key;用于本机联调前确认配置。CLI 和桌面 App 内的 `/llm-status` / 生成入口都会先读取仓库根目录或 `apps/ai-game-creator-shell/` 下 gitignored 的 `.env.secrets.local`,再检查当前进程环境。
|
||||
@@ -100,17 +100,17 @@ game-project/
|
||||
- 普通用户侧的生成、上传、运行、自检、预览状态 / 启动 / 打开 / 停止、记忆写入和画板资产导入都必须先完成 `/project` 初始化;未初始化时只提示设置本地项目,不落到默认临时目录。
|
||||
- 终端可用 `npm run ai-game-creator-shell:llm-status` 检查 LLM 环境变量是否就绪;CLI 和桌面 App 内的 `/llm-status` / 生成入口都会先读取 gitignored 的 `.env.secrets.local`,不请求上游、不显示 API Key,缺配置时以非零状态退出或在聊天里提示未就绪。
|
||||
- 终端可用 `npm run ai-game-creator-shell:check` 跑 v1 开发验收:壳 typecheck、`platform-agent` 编排测试、共享契约测试、Tauri Rust 测试和无密钥本地 provider 端到端 smoke。
|
||||
- 终端可用 `npm run ai-game-creator-shell:agent-run -- /绝对项目路径 "游戏创作需求"` 跑一次真实 LLM 生成、落盘、`game.static_smoke` 和本地 HTTP 预览;该入口读取当前环境和 gitignored 的 `.env.secrets.local`,不把 API Key 写入仓库或项目文件。自动验证可加 `--no-wait`,例如 `npm run ai-game-creator-shell:agent-run -- --no-wait /tmp/genarrative-ai-game-test "像素风反弹弹幕厨房"`,生成预览 trace 后立即停止本地预览,避免终端卡在回车等待。真实 OpenAI-compatible 网关建议设置 `GENARRATIVE_GAME_CREATOR_LLM_STREAM=true` 跑 Planner、组内角色和 Generator,避免长请求非流式空闲断连。
|
||||
- 终端可用 `npm run ai-game-creator-shell:agent-run -- /绝对项目路径 "游戏创作需求"` 跑一次真实 LLM 生成、落盘、`game.static_smoke` 和本地 HTTP 预览;该入口读取当前环境和 gitignored 的 `.env.secrets.local`,不把 API Key 写入仓库或项目文件。自动验证可加 `--no-wait`,例如 `npm run ai-game-creator-shell:agent-run -- --no-wait /tmp/genarrative-ai-game-test "像素风反弹弹幕厨房"`,生成预览 trace 后立即停止本地预览,避免终端卡在回车等待。真实 OpenAI-compatible 网关建议设置 `GENARRATIVE_GAME_CREATOR_LLM_STREAM=true` 跑 Planner 和 Generator,避免长请求非流式空闲断连。
|
||||
- 终端可用 `npm run ai-game-creator-shell:agent-run:smoke` 跑一次无密钥本地端到端 smoke:脚本启动本机 OpenAI-compatible SSE 流式测试 provider,预置一个本地上传图片和一个本地上传音频,复用真实 `--agent-run`、Planner / Orchestrator / 角色 agent / Generator / Evaluator loop、本地落盘、`game.static_smoke` 和本地 HTTP 预览,并断言每次 provider 请求都使用 `stream: true`、provider prompt 收到图片与音频资产上下文、生成 HTML 引用这些资产、预览服务能用 `GET` 读取 `/assets/...`、用 `HEAD` 返回真实资源长度和对应 MIME、headless Chrome 打开预览后至少执行一帧游戏 JS,且通过确定性亮色探针采样证明 canvas 不是空白画布、`.agent/run.latest.json` 的 step group 覆盖 design / balance / art / audio / code / publishing 六组、第二轮会重跑 Evaluator 命中任务及其下游影响任务,未受影响角色 carry-over;随后脚本自动给 CLI 发送回车停止预览。该脚本只用于开发验证,不进入产品生成路径。
|
||||
- `npm run ai-game-creator-shell:dev` 的 Tauri `devUrl` 固定为 `http://127.0.0.1:3080/`,Vite 必须 `strictPort` 对齐;`beforeDevCommand` 先复用已经跑在 3080 且页面标题为 `AI 游戏创作` 的本 app Vite server,否则才启动新的 Vite,若端口被其它服务占用则直接失败并提示释放端口。
|
||||
- `.agent/manifest.json` 会保存 6 个专业组下 15 个组内角色任务状态,当前覆盖 `Director`、`Gameplay`、`Difficulty`、`Asset`、`Polish`、`SFX`、`Code`、`Preview`、`Playtest`、`Publish`;开发窗口的专业组面板读取 manifest,而不是前端硬编码。
|
||||
- `.agent/manifest.json` 会保存 6 个专业组下 16 个组内角色任务状态,当前覆盖 `Director`、`Gameplay`、`Difficulty`、`Asset`、`Polish`、`SFX`、`Code`、`Review`、`Preview`、`Playtest`、`Publish`;程序组内显式包含 `quality-review` 质量评审 gate,由 Evaluator trace 标记完成;开发窗口的专业组面板读取 manifest,而不是前端硬编码。
|
||||
- 共享契约和 `platform-agent` 会按任务依赖与 `completed` 状态计算当前可执行任务,作为 v1 的最小编排选择器;每轮 `Orchestrator` 的 activeTaskIds、carriedTaskIds、repairRoutes 和 dependencyWaves 由 `platform-agent` 纯编排内核产出,`apps/ai-game-creator-shell` 只负责写入 `.agent/passes/pass-N/` 和执行本地工具;`Evaluator` 会在 `.agent/findings.md` 写出 `## Repair Routes` JSON,下一轮编排优先采用该结构化 taskIds,解析不到时才退回关键词路由;返工路由会按任务图自动扩展下游影响任务,例如美术资产变化会继续触发程序预览和运营包装重算。
|
||||
- `game.generate_draft` 使用 OpenAI-compatible LLM 配置生成结构化 JSON 草案,读取 `GENARRATIVE_GAME_CREATOR_LLM_API_KEY` / `GENARRATIVE_LLM_API_KEY` / `LLM_API_KEY` / `OPENAI_API_KEY`、`GENARRATIVE_GAME_CREATOR_LLM_BASE_URL` / `GENARRATIVE_LLM_BASE_URL` / `LLM_BASE_URL` / `OPENAI_BASE_URL`、`GENARRATIVE_GAME_CREATOR_LLM_MODEL` / `GENARRATIVE_LLM_MODEL` / `LLM_MODEL` / `OPENAI_MODEL`;`GENARRATIVE_GAME_CREATOR_LLM_STREAM=true` 时 Planner、组内角色和 Generator 使用流式请求;缺少配置或模型返回非法 JSON 时直接失败,不静默回退固定模板。
|
||||
- `game.generate_draft` 使用 OpenAI-compatible LLM 配置生成结构化 JSON 草案,读取 `GENARRATIVE_GAME_CREATOR_LLM_API_KEY` / `GENARRATIVE_LLM_API_KEY` / `LLM_API_KEY` / `OPENAI_API_KEY`、`GENARRATIVE_GAME_CREATOR_LLM_BASE_URL` / `GENARRATIVE_LLM_BASE_URL` / `LLM_BASE_URL` / `OPENAI_BASE_URL`、`GENARRATIVE_GAME_CREATOR_LLM_MODEL` / `GENARRATIVE_LLM_MODEL` / `LLM_MODEL` / `OPENAI_MODEL`;`GENARRATIVE_GAME_CREATOR_LLM_STREAM=true` 时 Planner 和 Generator 使用流式请求;缺少配置或模型返回非法 JSON 时直接失败,不静默回退固定模板。
|
||||
- 聊天输入 `/llm-status` 会触发只读 `llm.config_check`,确认 LLM base_url、model 和 API Key 是否已从环境变量读取;状态消息不会显示或保存 API Key。
|
||||
- `game.generate_draft` 的 LLM JSON 必须包含 `handoffs` 数组,覆盖 `design`、`balance`、`art`、`audio`、`code`、`publishing` 6 个专业组;每组必须给出 role、summary、outputs 和 next,缺组或交接内容不完整会判定为模型输出无效并进入返工。
|
||||
- `game.generate_draft` 的真实生成路径使用最小 Planner / Orchestrator / 组内角色 agent / Generator / Evaluator loop:Planner 写 `.agent/spec.md`;每轮 Orchestrator 先写 `.agent/passes/pass-N/agenda.md` 和 `.agent/passes/pass-N/task-graph.json`,首轮全量调度 15 个角色任务,返工轮按 `.agent/findings.md` 生成结构化 `repairRoutes`,重跑命中问题的角色任务及其下游依赖任务,其余角色 brief 从上一轮 carry-over;`task-graph.json` 记录 activeTaskIds、carriedTaskIds、repairFocus、repairRoutes 和按依赖排序的 dependencyWaves;角色 brief 写入 `.agent/passes/pass-N/groups/<group>/*.md`,再汇总为 `.agent/passes/pass-N/groups/*.md`;Generator 必须读取用户需求、记忆、`.agent/spec.md`、本轮 `agenda.md`、`task-graph.json`、`.agent/findings.md` 和 6 组汇总 brief 后返回结构化 JSON;每轮会把 Generator 草案拆成 6 组交接快照,写入 `.agent/passes/pass-N/`;Evaluator 做本地静态验收并写 `.agent/findings.md`。
|
||||
- `game.generate_draft` 的真实生成路径使用最小 Planner / Orchestrator / 组内角色 agent / Generator / Evaluator loop:Planner 写 `.agent/spec.md`;每轮 Orchestrator 先写 `.agent/passes/pass-N/agenda.md` 和 `.agent/passes/pass-N/task-graph.json`,首轮全量调度 16 个角色任务,返工轮按 `.agent/findings.md` 生成结构化 `repairRoutes`,重跑命中问题的角色任务及其下游依赖任务,其余角色 brief 从上一轮 carry-over;`task-graph.json` 记录 activeTaskIds、carriedTaskIds、repairFocus、repairRoutes 和按依赖排序的 dependencyWaves;角色 brief 写入 `.agent/passes/pass-N/groups/<group>/*.md`,再汇总为 `.agent/passes/pass-N/groups/*.md`;Generator 必须读取用户需求、记忆、`.agent/spec.md`、本轮 `agenda.md`、`task-graph.json`、`.agent/findings.md` 和 6 组汇总 brief 后返回结构化 JSON;每轮会把 Generator 草案拆成 6 组交接快照,写入 `.agent/passes/pass-N/`;Evaluator 做质量评审并写 `.agent/findings.md`,通过后才进入 `game.static_smoke` 静态自检和预览试玩。
|
||||
- loop 最多执行 3 轮;Evaluator 发现 HTML 非自包含、缺少 `canvas`、缺少 `requestAnimationFrame`、缺少输入监听或用户输入未转义时,把问题写入 `.agent/findings.md` 并让下一轮 Generator 修复。3 轮仍失败则 `game.generate_draft` 失败,不写最终游戏产物。
|
||||
- loop 每次运行会写 `.agent/run.latest.json` 和 `.agent/runs/<runId>.json`,记录 `Planner` / `Orchestrator` agenda / 15 个组内角色 brief 或 carry-over / 6 个 `GroupCoordinator` 汇总 / `Generator` / 6 个专业组交接 / `Evaluator` / `ArtifactWriter` / `Playtest` step、每步 `toolCalls`、输入文件、输出文件、状态、轮次、maxPasses、toolCallCount、maxToolCalls、stopReason、nextStep 和错误摘要;Planner、角色 agent 和 Generator 的 `inputPaths` 必须包含对应记忆文件、`.agent/manifest.json` 和 agenda 等上下文来源;每个 step 必须带 phase、taskId、group 和 role,`.agent/run.latest.json.taskGraph` 必须记录 goal、readyTaskIds、activeTaskIds、carriedTaskIds、repairFocus、repairRoutes 和当前任务状态;`.agent/run.latest.json.passPlans` 必须按轮记录 mode、summary、activeTaskIds、carriedTaskIds、dependencyWaves、repairFocus 和 repairRoutes,作为 `/trace` 与开发窗口判断编排 loop 是否真实发生的直接证据;`run.latest.json` 是当前指针,`.agent/runs/` 保留历史 run trace;开发窗口读取 latest 展示编排过程,并复用 `file.list/read` 列出和载入历史 run,普通用户窗口不展示。
|
||||
- loop 每次运行会写 `.agent/run.latest.json` 和 `.agent/runs/<runId>.json`,记录 `Planner` / `Orchestrator` agenda / 16 个组内角色 brief 或 carry-over / 6 个 `GroupCoordinator` 汇总 / `Generator` / 6 个专业组交接 / `Evaluator` 质量评审 / `ArtifactWriter` / `Playtest` step、每步 `toolCalls`、输入文件、输出文件、状态、轮次、maxPasses、toolCallCount、maxToolCalls、stopReason、nextStep 和错误摘要;Planner、角色 agent 和 Generator 的 `inputPaths` 必须包含对应记忆文件、`.agent/manifest.json` 和 agenda 等上下文来源;每个 step 必须带 phase、taskId、group 和 role,`.agent/run.latest.json.taskGraph` 必须记录 goal、readyTaskIds、activeTaskIds、carriedTaskIds、repairFocus、repairRoutes 和当前任务状态;`.agent/run.latest.json.passPlans` 必须按轮记录 mode、summary、activeTaskIds、carriedTaskIds、dependencyWaves、repairFocus 和 repairRoutes,作为 `/trace` 与开发窗口判断编排 loop 是否真实发生的直接证据;`run.latest.json` 是当前指针,`.agent/runs/` 保留历史 run trace;开发窗口读取 latest 展示编排过程,并复用 `file.list/read` 列出和载入历史 run,普通用户窗口不展示。
|
||||
- `.agent/run.latest.json` 的 `artifacts` 使用结构化记录,包含相对路径、字节数和 `fnv1a64:` checksum;除最终本地产物外,也会收集 `.agent/passes/pass-N/` 快照,便于确认返工前后的产物差异。
|
||||
- 通过 Evaluator 和 `game.static_smoke` 后,Agent loop 会把本次 runId、状态、轮次、下一步、active / carry-over 任务和最终本地产物摘要追加到 `memory/session.md` 与 `memory/project.md`;下一次 Planner、组内角色和 Generator 会通过记忆输入自然读取上一轮稳定原型状态,而不只依赖开发窗口 trace。
|
||||
- `.agent/agent.db` 当前作为最小本地索引文件使用 JSONL:初始化写入 `project.init`,每次 `game.generate_draft` 追加目标、标题和本地产物路径,上传 / 登记 / 画板导入资产时追加 `asset.register` 或 `asset.update`;v1 不引入 SQLite 依赖。
|
||||
@@ -118,7 +118,7 @@ game-project/
|
||||
- `game.generate_draft` 写入最终产物后会复用白名单受限命令 `game.static_smoke` 做一次生成后自检,至少检查 `game/index.html` 包含 canvas、canvas 渲染上下文、绘制调用、主循环、非空输入监听、明确目标、失败或胜利状态和重开路径,且不使用远程资源、`eval`、`new Function`、`localStorage`、`fetch`、`WebSocket` 或 `ServiceWorker`,也不得包含固定星核传送门模板词、纯按钮计分模板或 `TODO` / `待实现` / `这里省略` 等未完成实现;画板资源占位引用允许出现在 asset id 或说明中,并把该工具调用写入 `.agent/run.latest.json` 与 `.agent/logs/command.log`;自检失败则本次命令失败,不继续启动预览。
|
||||
- `ArtifactWriter` step 使用 `file.write.local_artifacts` 工具调用记录最终写入的 `memory/`、`game/`、`assets/`、`exports/` 和 `.agent/manifest.json` 路径;写入完成后 `nextStep` 指向 `game.static_smoke`。
|
||||
- `preview.start` / `preview.stop` 会追加 `.agent/logs/preview.log`,并在 `.agent/run.latest.json` 已存在时追加 `Preview` step 和 `preview.*` toolCall,记录本地 HTTP 预览 URL 与停止事件;单全局本地预览被新项目替换时,会 best-effort 把旧项目 manifest、preview log 和 trace 记录为 stopped,避免旧项目残留 running;本地 HTTP server 的 `/` 映射到 `game/index.html`,只允许读取 canonical 后仍位于项目真实 `game/` 或真实 `assets/` 下的文件,拒绝 `memory/`、`.agent/`、`exports/`、`..`、一级 `game` / `assets` 符号链接目录和内部符号链接越界,并为常见图片、音频、视频和 Web 资源返回对应 MIME;静态 `HEAD` 返回真实 `Content-Length` 但不返回 body,确保浏览器和媒体资源探测可用;上传和画板回流资产可被生成游戏引用但不会暴露记忆或 trace;没有 run trace 的手动预览启动不阻断。
|
||||
- 聊天输入会生成待确认的 `game.generate_draft` 内置命令;用户确认后才把 LLM 返回的结构化草案写入短期记忆 `memory/session.md`、长期记忆 `memory/project.md`、设计草案 `game/game_design.md`、数值配置 `game/balance.json`、美术清单 `assets/manifest.art.json`、音乐音效清单 `assets/manifest.audio.json`、发布包装草案 `exports/README.md` 和可运行 `game/index.html`。生成完成后,普通聊天消息会自动展示最近一次 Agent loop 的轮次、工具调用、active / carry-over 任务、返工焦点、画板同步建议命令和本地产物摘要;完整证据仍由 `/trace` 读取同一份 `.agent/run.latest.json`。
|
||||
- 聊天输入会生成待确认的 `game.generate_draft` 内置命令;用户确认后,正式用户聊天会实时展示 Planner LLM、Orchestrator、6 组角色 brief、Generator LLM、Evaluator 质量评审、ArtifactWriter 和 `game.static_smoke` 的进度,再把 LLM 返回的结构化草案写入短期记忆 `memory/session.md`、长期记忆 `memory/project.md`、设计草案 `game/game_design.md`、数值配置 `game/balance.json`、美术清单 `assets/manifest.art.json`、音乐音效清单 `assets/manifest.audio.json`、发布包装草案 `exports/README.md` 和可运行 `game/index.html`。生成完成后,普通聊天消息会自动展示最近一次 Agent loop 的 Run、LLM 对话、轮次、工具调用、active / carry-over 任务、返工焦点、编排轮次、最近步骤、画板同步建议命令和本地产物快照;完整证据仍由 `/trace` 读取同一份 `.agent/run.latest.json`。
|
||||
- `game.generate_draft` 的 `game/index.html` 必须是可试玩原型,至少包含输入、主循环、目标、失败或胜利状态和重开路径;不能只输出按钮计分或纯展示页。
|
||||
- `game.generate_draft` 会校验 LLM 输出:`balance`、美术清单和音乐清单必须是 JSON object,`gameHtml` 必须是自包含 HTML、包含 `canvas` 与 `requestAnimationFrame`,不得加载远程脚本或资源,不得使用 `eval` / `new Function` / `localStorage` / `fetch` / `WebSocket` / `ServiceWorker`,不得把包含 `<` / `>` 的用户输入原样写入 HTML。
|
||||
- 同一项目内多次 `game.generate_draft` 不覆盖记忆文件,而是继续追加短期对话记录和长期创作目标记录,保留用户迭代历史。
|
||||
|
||||
@@ -87,6 +87,7 @@ export const GAME_CREATION_AGENT_CAPABILITIES = [
|
||||
area: 'agent-runtime',
|
||||
title: '组内角色协作',
|
||||
},
|
||||
{ id: 'quality-review', area: 'agent-runtime', title: '质量评审' },
|
||||
{
|
||||
id: 'repair-loop-carryover',
|
||||
area: 'agent-runtime',
|
||||
@@ -257,13 +258,23 @@ export const GAME_CREATION_APP_SEED_TASKS = [
|
||||
artifacts: ['game/'],
|
||||
acceptanceCriteria: ['本地 Web 游戏项目可以通过 HTTP server 打开'],
|
||||
},
|
||||
{
|
||||
id: 'quality-review',
|
||||
title: '执行质量评审',
|
||||
group: 'code',
|
||||
role: 'Review',
|
||||
status: 'pending',
|
||||
dependencies: ['code-prototype'],
|
||||
artifacts: ['.agent/findings.md', '.agent/run.latest.json'],
|
||||
acceptanceCriteria: ['玩法、资产、数值、程序和发布包装通过跨专业组质量评审'],
|
||||
},
|
||||
{
|
||||
id: 'preview-readiness',
|
||||
title: '执行静态自检',
|
||||
group: 'code',
|
||||
role: 'Preview',
|
||||
status: 'pending',
|
||||
dependencies: ['code-prototype'],
|
||||
dependencies: ['quality-review'],
|
||||
artifacts: ['.agent/logs/command.log', '.agent/run.latest.json'],
|
||||
acceptanceCriteria: ['HTML 自包含且通过本地静态 smoke'],
|
||||
},
|
||||
|
||||
@@ -170,12 +170,21 @@ pub fn build_game_creation_seed_task_graph(
|
||||
["game/"],
|
||||
["本地 Web 游戏项目可以通过 HTTP server 打开"],
|
||||
),
|
||||
task(
|
||||
"quality-review",
|
||||
"执行质量评审",
|
||||
GameCreationAgentGroup::Code,
|
||||
"Review",
|
||||
["code-prototype"],
|
||||
[".agent/findings.md", ".agent/run.latest.json"],
|
||||
["玩法、资产、数值、程序和发布包装通过跨专业组质量评审"],
|
||||
),
|
||||
task(
|
||||
"preview-readiness",
|
||||
"执行静态自检",
|
||||
GameCreationAgentGroup::Code,
|
||||
"Preview",
|
||||
["code-prototype"],
|
||||
["quality-review"],
|
||||
[".agent/logs/command.log", ".agent/run.latest.json"],
|
||||
["HTML 自包含且通过本地静态 smoke"],
|
||||
),
|
||||
@@ -858,7 +867,7 @@ mod tests {
|
||||
);
|
||||
|
||||
assert_eq!(plan.mode, "initial");
|
||||
assert_eq!(plan.active_task_ids.len(), 15);
|
||||
assert_eq!(plan.active_task_ids.len(), 16);
|
||||
assert!(plan.carried_task_ids.is_empty());
|
||||
assert_eq!(plan.repair_routes, Vec::new());
|
||||
assert_eq!(plan.dependency_waves[0], vec!["design-director"]);
|
||||
@@ -879,6 +888,7 @@ mod tests {
|
||||
vec![
|
||||
"code-director",
|
||||
"code-prototype",
|
||||
"quality-review",
|
||||
"preview-readiness",
|
||||
"preview-playtest",
|
||||
"publish-strategy",
|
||||
@@ -926,6 +936,7 @@ mod tests {
|
||||
plan.active_task_ids,
|
||||
vec![
|
||||
"code-prototype",
|
||||
"quality-review",
|
||||
"preview-readiness",
|
||||
"preview-playtest",
|
||||
"publish-strategy",
|
||||
@@ -936,6 +947,7 @@ mod tests {
|
||||
plan.repair_routes[0].task_ids,
|
||||
vec![
|
||||
"code-prototype",
|
||||
"quality-review",
|
||||
"preview-readiness",
|
||||
"preview-playtest",
|
||||
"publish-strategy",
|
||||
@@ -981,6 +993,7 @@ mod tests {
|
||||
"art-polish",
|
||||
"code-director",
|
||||
"code-prototype",
|
||||
"quality-review",
|
||||
"preview-readiness",
|
||||
"preview-playtest",
|
||||
"publish-strategy",
|
||||
@@ -1014,7 +1027,7 @@ mod tests {
|
||||
);
|
||||
|
||||
assert_eq!(plan.mode, "repair");
|
||||
assert_eq!(plan.active_task_ids.len(), 15);
|
||||
assert_eq!(plan.active_task_ids.len(), 16);
|
||||
assert!(plan.carried_task_ids.is_empty());
|
||||
assert_eq!(plan.repair_routes[0].reason, "cross-group-handoff");
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ pub struct GameCreationAgentCapabilityDescriptor {
|
||||
pub title: &'static str,
|
||||
}
|
||||
|
||||
pub const GAME_CREATION_AGENT_CAPABILITIES: [GameCreationAgentCapabilityDescriptor; 21] = [
|
||||
pub const GAME_CREATION_AGENT_CAPABILITIES: [GameCreationAgentCapabilityDescriptor; 22] = [
|
||||
capability("chat", "user", "聊天入口"),
|
||||
capability("file-upload", "user", "上传文件"),
|
||||
capability("built-in-commands", "agent-runtime", "内置命令调用"),
|
||||
@@ -89,6 +89,7 @@ pub const GAME_CREATION_AGENT_CAPABILITIES: [GameCreationAgentCapabilityDescript
|
||||
capability("tool-call-budget", "agent-runtime", "工具调用预算"),
|
||||
capability("multi-agent-collaboration", "agent-runtime", "多智能体协作"),
|
||||
capability("role-level-collaboration", "agent-runtime", "组内角色协作"),
|
||||
capability("quality-review", "agent-runtime", "质量评审"),
|
||||
capability(
|
||||
"repair-loop-carryover",
|
||||
"agent-runtime",
|
||||
@@ -269,12 +270,21 @@ pub fn new_game_creation_app_seed_tasks() -> Vec<GameCreationAppTaskState> {
|
||||
["game/"],
|
||||
["本地 Web 游戏项目可以通过 HTTP server 打开"],
|
||||
),
|
||||
task(
|
||||
"quality-review",
|
||||
"执行质量评审",
|
||||
GameCreationAppAgentGroup::Code,
|
||||
"Review",
|
||||
["code-prototype"],
|
||||
[".agent/findings.md", ".agent/run.latest.json"],
|
||||
["玩法、资产、数值、程序和发布包装通过跨专业组质量评审"],
|
||||
),
|
||||
task(
|
||||
"preview-readiness",
|
||||
"执行静态自检",
|
||||
GameCreationAppAgentGroup::Code,
|
||||
"Preview",
|
||||
["code-prototype"],
|
||||
["quality-review"],
|
||||
[".agent/logs/command.log", ".agent/run.latest.json"],
|
||||
["HTML 自包含且通过本地静态 smoke"],
|
||||
),
|
||||
@@ -869,6 +879,7 @@ mod tests {
|
||||
"audio-asset-plan",
|
||||
"code-director",
|
||||
"code-prototype",
|
||||
"quality-review",
|
||||
"preview-readiness",
|
||||
"preview-playtest",
|
||||
"publish-strategy",
|
||||
|
||||
Reference in New Issue
Block a user