Compare commits
5 Commits
a38eec743a
...
36dc93222a
| Author | SHA1 | Date | |
|---|---|---|---|
| 36dc93222a | |||
| 54ae60576d | |||
| 89447ed432 | |||
| 3ebfcc0c2f | |||
| 9b5d1fe107 |
@@ -12,6 +12,8 @@ use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
|||||||
mod codex_app_server;
|
mod codex_app_server;
|
||||||
mod codex_cli;
|
mod codex_cli;
|
||||||
mod codex_provider_proxy;
|
mod codex_provider_proxy;
|
||||||
|
mod design_runtime;
|
||||||
|
mod design_tools;
|
||||||
mod direct_codex_attachments;
|
mod direct_codex_attachments;
|
||||||
mod direct_codex_audit;
|
mod direct_codex_audit;
|
||||||
mod direct_project_history;
|
mod direct_project_history;
|
||||||
@@ -19,8 +21,6 @@ mod direct_project_turn_history;
|
|||||||
mod direct_runtime;
|
mod direct_runtime;
|
||||||
mod direct_tool_bridge;
|
mod direct_tool_bridge;
|
||||||
mod direct_tools_mcp;
|
mod direct_tools_mcp;
|
||||||
mod design_runtime;
|
|
||||||
mod design_tools;
|
|
||||||
mod generation;
|
mod generation;
|
||||||
mod interaction;
|
mod interaction;
|
||||||
mod prompt;
|
mod prompt;
|
||||||
@@ -40,6 +40,7 @@ pub(crate) use codex_cli::{
|
|||||||
game_creator_codex_cli_executable_path, game_creator_codex_cli_version_identity,
|
game_creator_codex_cli_executable_path, game_creator_codex_cli_version_identity,
|
||||||
};
|
};
|
||||||
pub(crate) use codex_provider_proxy::*;
|
pub(crate) use codex_provider_proxy::*;
|
||||||
|
pub(crate) use design_runtime::*;
|
||||||
pub(crate) use direct_codex_attachments::*;
|
pub(crate) use direct_codex_attachments::*;
|
||||||
pub(crate) use direct_codex_audit::*;
|
pub(crate) use direct_codex_audit::*;
|
||||||
pub(crate) use direct_project_history::*;
|
pub(crate) use direct_project_history::*;
|
||||||
@@ -47,7 +48,6 @@ pub(crate) use direct_project_turn_history::*;
|
|||||||
pub(crate) use direct_runtime::*;
|
pub(crate) use direct_runtime::*;
|
||||||
pub(crate) use direct_tool_bridge::*;
|
pub(crate) use direct_tool_bridge::*;
|
||||||
pub(crate) use direct_tools_mcp::*;
|
pub(crate) use direct_tools_mcp::*;
|
||||||
pub(crate) use design_runtime::*;
|
|
||||||
pub(crate) use generation::*;
|
pub(crate) use generation::*;
|
||||||
pub(crate) use interaction::*;
|
pub(crate) use interaction::*;
|
||||||
pub(crate) use prompt::*;
|
pub(crate) use prompt::*;
|
||||||
|
|||||||
@@ -3078,8 +3078,8 @@ fn parse_game_creator_codex_app_server_text(
|
|||||||
response_id: Some(thread_id.to_string()),
|
response_id: Some(thread_id.to_string()),
|
||||||
usage: None,
|
usage: None,
|
||||||
tool_calls,
|
tool_calls,
|
||||||
responses_output: Vec::new(),
|
responses_output: Vec::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn read_game_creator_codex_app_server_stdout(
|
async fn read_game_creator_codex_app_server_stdout(
|
||||||
|
|||||||
@@ -593,8 +593,8 @@ fn parse_game_creator_codex_cli_response(
|
|||||||
response_id,
|
response_id,
|
||||||
usage,
|
usage,
|
||||||
tool_calls,
|
tool_calls,
|
||||||
responses_output: Vec::new(),
|
responses_output: Vec::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn request_game_creator_agent_codex_cli_with_executable(
|
async fn request_game_creator_agent_codex_cli_with_executable(
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -92,10 +92,7 @@ impl DesignResources {
|
|||||||
items.sort_by(|left, right| left.id.cmp(&right.id));
|
items.sort_by(|left, right| left.id.cmp(&right.id));
|
||||||
lines.push(format!("\n{category}"));
|
lines.push(format!("\n{category}"));
|
||||||
for item in items {
|
for item in items {
|
||||||
lines.push(format!(
|
lines.push(format!("- {}|{}:{}", item.id, item.title, item.summary));
|
||||||
"- {}|{}:{}",
|
|
||||||
item.id, item.title, item.summary
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(lines.join("\n"))
|
Ok(lines.join("\n"))
|
||||||
@@ -228,7 +225,11 @@ pub(crate) fn execute_design_file_tool(
|
|||||||
let name = workspace_display_path(&relative, &entry.file_name().to_string_lossy());
|
let name = workspace_display_path(&relative, &entry.file_name().to_string_lossy());
|
||||||
rows.push(format!(
|
rows.push(format!(
|
||||||
"{} {name}",
|
"{} {name}",
|
||||||
if child.is_dir() { "[目录]" } else { "[文件]" }
|
if child.is_dir() {
|
||||||
|
"[目录]"
|
||||||
|
} else {
|
||||||
|
"[文件]"
|
||||||
|
}
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Ok(Value::String(if rows.is_empty() {
|
Ok(Value::String(if rows.is_empty() {
|
||||||
@@ -276,10 +277,17 @@ pub(crate) fn execute_design_file_tool(
|
|||||||
}
|
}
|
||||||
let (display, path) = resolve_design_workspace_path(root, &relative)?;
|
let (display, path) = resolve_design_workspace_path(root, &relative)?;
|
||||||
if !path.is_file() {
|
if !path.is_file() {
|
||||||
return Ok(Value::String(format!("局部修改失败:文件不存在:{display}")));
|
return Ok(Value::String(format!(
|
||||||
|
"局部修改失败:文件不存在:{display}"
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
let content = fs::read_to_string(&path).map_err(|error| format!("读取失败:{error}"))?;
|
let content =
|
||||||
let newline = if content.contains("\r\n") { "\r\n" } else { "\n" };
|
fs::read_to_string(&path).map_err(|error| format!("读取失败:{error}"))?;
|
||||||
|
let newline = if content.contains("\r\n") {
|
||||||
|
"\r\n"
|
||||||
|
} else {
|
||||||
|
"\n"
|
||||||
|
};
|
||||||
let old = old.replace("\r\n", "\n").replace('\n', newline);
|
let old = old.replace("\r\n", "\n").replace('\n', newline);
|
||||||
let new = new.replace("\r\n", "\n").replace('\n', newline);
|
let new = new.replace("\r\n", "\n").replace('\n', newline);
|
||||||
let count = content.matches(&old).count();
|
let count = content.matches(&old).count();
|
||||||
@@ -453,7 +461,8 @@ fn load_design_tools(root: &Path) -> Result<Vec<platform_llm::LlmFunctionTool>,
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn read_pack_text(root: &Path, relative: &str) -> Result<String, String> {
|
fn read_pack_text(root: &Path, relative: &str) -> Result<String, String> {
|
||||||
fs::read_to_string(root.join(relative)).map_err(|error| format!("读取 {relative} 失败:{error}"))
|
fs::read_to_string(root.join(relative))
|
||||||
|
.map_err(|error| format!("读取 {relative} 失败:{error}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn optional_tool_path(args: &Value) -> Result<String, String> {
|
fn optional_tool_path(args: &Value) -> Result<String, String> {
|
||||||
|
|||||||
+6
-6
@@ -119,8 +119,8 @@ fn persist_tool_plan_handoff_repair_chain(
|
|||||||
response_id: None,
|
response_id: None,
|
||||||
usage: None,
|
usage: None,
|
||||||
tool_calls: Vec::new(),
|
tool_calls: Vec::new(),
|
||||||
responses_output: Vec::new(),
|
responses_output: Vec::new(),
|
||||||
};
|
};
|
||||||
tool_plan_handoff::write_at(
|
tool_plan_handoff::write_at(
|
||||||
root,
|
root,
|
||||||
&base_identity,
|
&base_identity,
|
||||||
@@ -1120,8 +1120,8 @@ async fn tool_plan_handoff_identity_drift_closes_entire_repair_chain_before_remo
|
|||||||
response_id: None,
|
response_id: None,
|
||||||
usage: None,
|
usage: None,
|
||||||
tool_calls: Vec::new(),
|
tool_calls: Vec::new(),
|
||||||
responses_output: Vec::new(),
|
responses_output: Vec::new(),
|
||||||
};
|
};
|
||||||
tool_plan_handoff::write_at(
|
tool_plan_handoff::write_at(
|
||||||
root,
|
root,
|
||||||
&base_identity,
|
&base_identity,
|
||||||
@@ -1440,8 +1440,8 @@ async fn tool_plan_handoff_durable_control_closes_entire_repair_chain_before_rem
|
|||||||
response_id: None,
|
response_id: None,
|
||||||
usage: None,
|
usage: None,
|
||||||
tool_calls: Vec::new(),
|
tool_calls: Vec::new(),
|
||||||
responses_output: Vec::new(),
|
responses_output: Vec::new(),
|
||||||
};
|
};
|
||||||
tool_plan_handoff::write_at(
|
tool_plan_handoff::write_at(
|
||||||
root,
|
root,
|
||||||
&base_identity,
|
&base_identity,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use serde_json::Value;
|
|
||||||
|
|
||||||
pub(crate) const DESIGN_SESSION_SCHEMA_VERSION: &str = "design-agent-session.v1";
|
pub(crate) const DESIGN_SESSION_SCHEMA_VERSION: &str = "design-agent-session.v1";
|
||||||
pub(crate) const DESIGN_SESSION_ENGINE: &str = "design-agent";
|
pub(crate) const DESIGN_SESSION_ENGINE: &str = "design-agent";
|
||||||
@@ -181,8 +181,14 @@ pub(crate) fn validate_design_session(session: &DesignSession) -> Result<(), Str
|
|||||||
|
|
||||||
pub(crate) fn read_design_session(root: &Path) -> Result<Option<DesignSession>, String> {
|
pub(crate) fn read_design_session(root: &Path) -> Result<Option<DesignSession>, String> {
|
||||||
let Some(session) = read_agent_runtime_json_sidecar_with_max_bytes::<DesignSession>(
|
let Some(session) = read_agent_runtime_json_sidecar_with_max_bytes::<DesignSession>(
|
||||||
root, DESIGN_SESSION_PATH, "策划 Agent 会话", DESIGN_SESSION_MAX_BYTES,
|
root,
|
||||||
)? else { return Ok(None); };
|
DESIGN_SESSION_PATH,
|
||||||
|
"策划 Agent 会话",
|
||||||
|
DESIGN_SESSION_MAX_BYTES,
|
||||||
|
)?
|
||||||
|
else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
validate_design_session(&session)?;
|
validate_design_session(&session)?;
|
||||||
Ok(Some(session))
|
Ok(Some(session))
|
||||||
}
|
}
|
||||||
@@ -190,7 +196,11 @@ pub(crate) fn read_design_session(root: &Path) -> Result<Option<DesignSession>,
|
|||||||
pub(crate) fn write_design_session(root: &Path, session: &DesignSession) -> Result<(), String> {
|
pub(crate) fn write_design_session(root: &Path, session: &DesignSession) -> Result<(), String> {
|
||||||
validate_design_session(session)?;
|
validate_design_session(session)?;
|
||||||
write_agent_runtime_json_sidecar_with_max_bytes(
|
write_agent_runtime_json_sidecar_with_max_bytes(
|
||||||
root, DESIGN_SESSION_PATH, "策划 Agent 会话", session, DESIGN_SESSION_MAX_BYTES,
|
root,
|
||||||
|
DESIGN_SESSION_PATH,
|
||||||
|
"策划 Agent 会话",
|
||||||
|
session,
|
||||||
|
DESIGN_SESSION_MAX_BYTES,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,7 +209,9 @@ pub(crate) fn ensure_design_session(
|
|||||||
project_id: &str,
|
project_id: &str,
|
||||||
) -> Result<DesignSession, String> {
|
) -> Result<DesignSession, String> {
|
||||||
if let Some(session) = read_design_session(root)? {
|
if let Some(session) = read_design_session(root)? {
|
||||||
if session.project_id != project_id { return Err("策划会话与当前项目不匹配".to_string()); }
|
if session.project_id != project_id {
|
||||||
|
return Err("策划会话与当前项目不匹配".to_string());
|
||||||
|
}
|
||||||
return Ok(session);
|
return Ok(session);
|
||||||
}
|
}
|
||||||
let session = new_design_session(project_id.to_string());
|
let session = new_design_session(project_id.to_string());
|
||||||
@@ -244,7 +256,9 @@ pub(crate) fn submit_design_phase_for_approval(
|
|||||||
if session.current_phase == "consultant" {
|
if session.current_phase == "consultant" {
|
||||||
return Err("顾问态不提交阶段审批".to_string());
|
return Err("顾问态不提交阶段审批".to_string());
|
||||||
}
|
}
|
||||||
if let Some(request) = &session.pending_approval { return Ok(request.clone()); }
|
if let Some(request) = &session.pending_approval {
|
||||||
|
return Ok(request.clone());
|
||||||
|
}
|
||||||
let missing = check_design_phase_artifacts(root, session, &session.current_phase)?;
|
let missing = check_design_phase_artifacts(root, session, &session.current_phase)?;
|
||||||
if !missing.is_empty() {
|
if !missing.is_empty() {
|
||||||
return Err(format!("缺少必需产物:{}", missing.join("、")));
|
return Err(format!("缺少必需产物:{}", missing.join("、")));
|
||||||
@@ -274,8 +288,10 @@ pub(crate) fn approve_design_phase(
|
|||||||
}
|
}
|
||||||
session.approved_phases.push(session.current_phase.clone());
|
session.approved_phases.push(session.current_phase.clone());
|
||||||
let next = design_phase_index(&session.current_phase)? + 1;
|
let next = design_phase_index(&session.current_phase)? + 1;
|
||||||
session.current_phase = DESIGN_PHASES.get(next)
|
session.current_phase = DESIGN_PHASES
|
||||||
.ok_or_else(|| "顾问态没有下一阶段".to_string())?.to_string();
|
.get(next)
|
||||||
|
.ok_or_else(|| "顾问态没有下一阶段".to_string())?
|
||||||
|
.to_string();
|
||||||
session.pending_clarification = None;
|
session.pending_clarification = None;
|
||||||
session.updated_at = unix_timestamp();
|
session.updated_at = unix_timestamp();
|
||||||
Ok(session.current_phase.clone())
|
Ok(session.current_phase.clone())
|
||||||
|
|||||||
@@ -3640,7 +3640,38 @@ pub(crate) fn merge_game_creator_config_file(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_game_creator_config_file(path: &Path) -> Result<Option<String>, String> {
|
/// Only files inside the managed runtime config directory (real credentials)
|
||||||
|
/// may use the private-read channel: it hardens the owner/DACL on every read.
|
||||||
|
/// Repository-adjacent fallback templates and overlay files are shared inputs
|
||||||
|
/// that can be git-tracked; privatizing one on read silently locks the
|
||||||
|
/// worktree template to whichever account happened to run the dev CLI, so
|
||||||
|
/// they must go through the non-mutating snapshot channel instead.
|
||||||
|
pub(crate) fn game_creator_config_path_is_runtime_managed(path: &Path) -> bool {
|
||||||
|
game_creator_runtime_config_dir().is_some_and(|directory| path.starts_with(directory))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_game_creator_snapshot_file_to_string(
|
||||||
|
path: &Path,
|
||||||
|
label: &str,
|
||||||
|
max_bytes: u64,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let (mut file, metadata) = open_project_snapshot_regular_file(path, label)?;
|
||||||
|
if metadata.len() > max_bytes {
|
||||||
|
return Err(format!("{label}过大,已拒绝读取:{}", path.display()));
|
||||||
|
}
|
||||||
|
let mut content = String::with_capacity(metadata.len() as usize);
|
||||||
|
file.read_to_string(&mut content)
|
||||||
|
.map_err(|error| format!("读取{label}失败:{}: {error}", path.display()))?;
|
||||||
|
let final_metadata = file
|
||||||
|
.metadata()
|
||||||
|
.map_err(|error| format!("复核{label}失败:{}: {error}", path.display()))?;
|
||||||
|
if final_metadata.len() != metadata.len() {
|
||||||
|
return Err(format!("{label}读取期间文件发生漂移:{}", path.display()));
|
||||||
|
}
|
||||||
|
Ok(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_game_creator_config_file(path: &Path) -> Result<Option<String>, String> {
|
||||||
let backup_path = game_creator_config_backup_path(path);
|
let backup_path = game_creator_config_backup_path(path);
|
||||||
let path_exists = validate_game_creator_config_file_entry(path)?;
|
let path_exists = validate_game_creator_config_file_entry(path)?;
|
||||||
let read_path = if path_exists {
|
let read_path = if path_exists {
|
||||||
@@ -3650,7 +3681,11 @@ fn read_game_creator_config_file(path: &Path) -> Result<Option<String>, String>
|
|||||||
} else {
|
} else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
let content = read_game_creator_private_file_to_string(read_path, "客户端配置", 256 * 1024)?;
|
let content = if game_creator_config_path_is_runtime_managed(read_path) {
|
||||||
|
read_game_creator_private_file_to_string(read_path, "客户端配置", 256 * 1024)?
|
||||||
|
} else {
|
||||||
|
read_game_creator_snapshot_file_to_string(read_path, "客户端配置", 256 * 1024)?
|
||||||
|
};
|
||||||
Ok(Some(content))
|
Ok(Some(content))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2779,8 +2779,8 @@ fn durable_provider_handoff_prevents_shutdown_even_when_corrupt() {
|
|||||||
response_id: Some("provider-handoff-response".to_string()),
|
response_id: Some("provider-handoff-response".to_string()),
|
||||||
usage: None,
|
usage: None,
|
||||||
tool_calls: Vec::new(),
|
tool_calls: Vec::new(),
|
||||||
responses_output: Vec::new(),
|
responses_output: Vec::new(),
|
||||||
};
|
};
|
||||||
let provider_request_id = format!("provider-request-{}", "f".repeat(64));
|
let provider_request_id = format!("provider-request-{}", "f".repeat(64));
|
||||||
crate::provider_handoff::write_at(
|
crate::provider_handoff::write_at(
|
||||||
&root,
|
&root,
|
||||||
|
|||||||
@@ -1184,6 +1184,56 @@ fn llm_config_check_reports_per_agent_status_without_leaking_keys() {
|
|||||||
fs::remove_dir_all(root).ok();
|
fs::remove_dir_all(root).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fallback_template_read_stays_on_snapshot_channel_outside_runtime_dir() {
|
||||||
|
let root = unique_project_path();
|
||||||
|
let template_dir = root.join("apps").join("ai-game-creator-shell");
|
||||||
|
fs::create_dir_all(&template_dir).expect("fallback template dir");
|
||||||
|
let template = template_dir.join(GAME_CREATOR_CONFIG_FILE_NAME);
|
||||||
|
fs::write(
|
||||||
|
&template,
|
||||||
|
"{\n \"llm\": { \"model\": \"fallback-template-model\" }\n}\n",
|
||||||
|
)
|
||||||
|
.expect("write fallback template");
|
||||||
|
// 设置一个与模板无关的 runtime dir,让分类断言真正覆盖"按路径归属"而非
|
||||||
|
// "runtime dir 为 None 时恒 false"的全局开关。
|
||||||
|
let runtime_root = unique_project_path();
|
||||||
|
fs::create_dir_all(&runtime_root).expect("unrelated runtime config dir");
|
||||||
|
let _guard = use_test_runtime_config_dir(runtime_root.clone());
|
||||||
|
|
||||||
|
// 仓库旁边的回退模板是共享输入,读取绝不能走会收紧 owner/DACL 的私有通道。
|
||||||
|
assert!(!game_creator_config_path_is_runtime_managed(&template));
|
||||||
|
assert!(game_creator_config_path_is_runtime_managed(
|
||||||
|
&runtime_root.join(GAME_CREATOR_CONFIG_FILE_NAME)
|
||||||
|
));
|
||||||
|
let content = read_game_creator_config_file(&template).expect("read fallback template");
|
||||||
|
assert!(content
|
||||||
|
.expect("fallback template content")
|
||||||
|
.contains("fallback-template-model"));
|
||||||
|
fs::remove_dir_all(root).ok();
|
||||||
|
fs::remove_dir_all(runtime_root).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn runtime_config_read_stays_on_private_channel_inside_runtime_dir() {
|
||||||
|
let root = unique_project_path();
|
||||||
|
fs::create_dir_all(&root).expect("runtime config dir");
|
||||||
|
let config_path = root.join(GAME_CREATOR_CONFIG_FILE_NAME);
|
||||||
|
fs::write(
|
||||||
|
&config_path,
|
||||||
|
"{\n \"llm\": { \"model\": \"managed-config-model\" }\n}\n",
|
||||||
|
)
|
||||||
|
.expect("write runtime config");
|
||||||
|
let _guard = use_test_runtime_config_dir(root.clone());
|
||||||
|
|
||||||
|
assert!(game_creator_config_path_is_runtime_managed(&config_path));
|
||||||
|
let content = read_game_creator_config_file(&config_path).expect("read runtime config");
|
||||||
|
assert!(content
|
||||||
|
.expect("runtime config content")
|
||||||
|
.contains("managed-config-model"));
|
||||||
|
fs::remove_dir_all(root).ok();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn llm_config_check_reports_agent_specific_config_paths() {
|
fn llm_config_check_reports_agent_specific_config_paths() {
|
||||||
let root = unique_project_path();
|
let root = unique_project_path();
|
||||||
|
|||||||
@@ -27,30 +27,35 @@ import {
|
|||||||
within,
|
within,
|
||||||
} from './harness';
|
} from './harness';
|
||||||
|
|
||||||
function emptyPlanningV2StartResult(projectId = 'local-project-draft') {
|
function homeDesignContinueView({
|
||||||
|
prompt,
|
||||||
|
pendingClarification = null,
|
||||||
|
}: {
|
||||||
|
prompt: string;
|
||||||
|
pendingClarification?: {
|
||||||
|
requestId: string;
|
||||||
|
question: string;
|
||||||
|
options: string[];
|
||||||
|
createdAt: number;
|
||||||
|
} | null;
|
||||||
|
}) {
|
||||||
return {
|
return {
|
||||||
session: {
|
session: {
|
||||||
schemaVersion: 'planning-session.v2',
|
sessionId: 'design-session-home',
|
||||||
engine: 'planning-session-v2',
|
projectId: 'local-project-draft',
|
||||||
sessionId: 'home-planning-v2-session',
|
currentPhase: 'concept',
|
||||||
projectId,
|
approvedPhases: [],
|
||||||
mode: 'gdd',
|
pendingApproval: null,
|
||||||
status: 'planning',
|
pendingClarification,
|
||||||
turnIndex: 1,
|
turnIndex: 1,
|
||||||
questionCount: 0,
|
|
||||||
questionLimit: 8,
|
|
||||||
revisionCount: 0,
|
|
||||||
currentArtifactVersion: null,
|
|
||||||
currentQuestion: null,
|
|
||||||
capabilities: { tools: [], skills: [] },
|
|
||||||
processingSeconds: 0.5,
|
|
||||||
createdAtUtc: '2026-09-03T00:00:00Z',
|
|
||||||
updatedAtUtc: '2026-09-03T00:00:01Z',
|
|
||||||
lastError: null,
|
lastError: null,
|
||||||
},
|
},
|
||||||
result: null,
|
messages: [
|
||||||
currentArtifact: null,
|
{ id: 'u1', role: 'user', text: prompt },
|
||||||
replayed: false,
|
{ id: 'a1', role: 'assistant', text: '先确认核心循环。' },
|
||||||
|
],
|
||||||
|
running: false,
|
||||||
|
canRetry: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1784,7 +1789,7 @@ export function registerHomeProjectCreationTests() {
|
|||||||
['做方案', false],
|
['做方案', false],
|
||||||
['做方案', true],
|
['做方案', true],
|
||||||
] as const)(
|
] as const)(
|
||||||
'routes %s %s creation to Planning Session V2',
|
'routes %s %s creation to the design agent',
|
||||||
async (modeLabel, automatic) => {
|
async (modeLabel, automatic) => {
|
||||||
const projectPath = `/tmp/home-${modeLabel}-${automatic ? 'enter' : 'submit'}`;
|
const projectPath = `/tmp/home-${modeLabel}-${automatic ? 'enter' : 'submit'}`;
|
||||||
const manifest = createGameCreationAppManifest(
|
const manifest = createGameCreationAppManifest(
|
||||||
@@ -1793,7 +1798,9 @@ export function registerHomeProjectCreationTests() {
|
|||||||
);
|
);
|
||||||
const supervisorHarness = createProjectSupervisorRuntimeHarness({
|
const supervisorHarness = createProjectSupervisorRuntimeHarness({
|
||||||
projectPath,
|
projectPath,
|
||||||
planningV2StartResult: emptyPlanningV2StartResult(),
|
designAgentContinueView: homeDesignContinueView({
|
||||||
|
prompt: '整理一个可玩原型',
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
const invoke = vi.fn(
|
const invoke = vi.fn(
|
||||||
async (command: string, args?: Record<string, unknown>) => {
|
async (command: string, args?: Record<string, unknown>) => {
|
||||||
@@ -1840,20 +1847,28 @@ export function registerHomeProjectCreationTests() {
|
|||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(invoke).toHaveBeenCalledWith(
|
expect(invoke).toHaveBeenCalledWith(
|
||||||
'start_planning_session_v2',
|
'continue_design_agent_session',
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
projectPath,
|
projectPath,
|
||||||
mode: 'gdd',
|
input: expect.objectContaining({
|
||||||
|
type: 'message',
|
||||||
|
text: '整理一个可玩原型',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const startCall = invoke.mock.calls.find(
|
const startCall = invoke.mock.calls.find(
|
||||||
([command]) => command === 'start_planning_session_v2',
|
([command]) => command === 'continue_design_agent_session',
|
||||||
);
|
);
|
||||||
expect(startCall?.[1]).not.toHaveProperty('attachments');
|
expect(startCall?.[1]).not.toHaveProperty('attachments');
|
||||||
expect(JSON.stringify(startCall?.[1] ?? {})).not.toContain(
|
expect(JSON.stringify(startCall?.[1] ?? {})).not.toContain(
|
||||||
'本轮用户附件',
|
'本轮用户附件',
|
||||||
);
|
);
|
||||||
|
expect(
|
||||||
|
invoke.mock.calls.some(
|
||||||
|
([command]) => command === 'start_planning_session_v2',
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
expect(invoke).not.toHaveBeenCalledWith(
|
expect(invoke).not.toHaveBeenCalledWith(
|
||||||
'chat_with_game_creator_direct_codex',
|
'chat_with_game_creator_direct_codex',
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
@@ -1882,9 +1897,9 @@ export function registerHomeProjectCreationTests() {
|
|||||||
);
|
);
|
||||||
const supervisorHarness = createProjectSupervisorRuntimeHarness({
|
const supervisorHarness = createProjectSupervisorRuntimeHarness({
|
||||||
projectPath,
|
projectPath,
|
||||||
planningV2StartResult: emptyPlanningV2StartResult(
|
designAgentContinueView: homeDesignContinueView({
|
||||||
'home-planning-attachment',
|
prompt: '整理一份可玩原型',
|
||||||
),
|
}),
|
||||||
});
|
});
|
||||||
const fileBytes = Array.from(new TextEncoder().encode('png'));
|
const fileBytes = Array.from(new TextEncoder().encode('png'));
|
||||||
const invoke = vi.fn(
|
const invoke = vi.fn(
|
||||||
@@ -1937,10 +1952,10 @@ export function registerHomeProjectCreationTests() {
|
|||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(invoke).toHaveBeenCalledWith(
|
expect(invoke).toHaveBeenCalledWith(
|
||||||
'start_planning_session_v2',
|
'continue_design_agent_session',
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
projectPath,
|
projectPath,
|
||||||
mode: 'gdd',
|
input: expect.objectContaining({ type: 'message' }),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -1951,13 +1966,18 @@ export function registerHomeProjectCreationTests() {
|
|||||||
bytes: fileBytes,
|
bytes: fileBytes,
|
||||||
});
|
});
|
||||||
const startCall = invoke.mock.calls.find(
|
const startCall = invoke.mock.calls.find(
|
||||||
([command]) => command === 'start_planning_session_v2',
|
([command]) => command === 'continue_design_agent_session',
|
||||||
);
|
);
|
||||||
expect(startCall?.[1]).not.toHaveProperty('attachments');
|
expect(startCall?.[1]).not.toHaveProperty('attachments');
|
||||||
expect(JSON.stringify(startCall?.[1] ?? {})).not.toContain('本轮用户附件');
|
expect(JSON.stringify(startCall?.[1] ?? {})).not.toContain('本轮用户附件');
|
||||||
expect(JSON.stringify(startCall?.[1] ?? {})).not.toContain(
|
expect(JSON.stringify(startCall?.[1] ?? {})).not.toContain(
|
||||||
'assets/uploads/reference.png',
|
'assets/uploads/reference.png',
|
||||||
);
|
);
|
||||||
|
expect(
|
||||||
|
invoke.mock.calls.some(
|
||||||
|
([command]) => command === 'start_planning_session_v2',
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
expect(invoke).not.toHaveBeenCalledWith(
|
expect(invoke).not.toHaveBeenCalledWith(
|
||||||
'chat_with_game_creator_direct_codex',
|
'chat_with_game_creator_direct_codex',
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
@@ -1980,53 +2000,15 @@ export function registerHomeProjectCreationTests() {
|
|||||||
);
|
);
|
||||||
const supervisorHarness = createProjectSupervisorRuntimeHarness({
|
const supervisorHarness = createProjectSupervisorRuntimeHarness({
|
||||||
projectPath,
|
projectPath,
|
||||||
expectedRunProfile: 'standard',
|
designAgentContinueView: homeDesignContinueView({
|
||||||
});
|
prompt: '2D射击游戏',
|
||||||
supervisorHarness.setPlanningV2StartResult({
|
pendingClarification: {
|
||||||
session: {
|
requestId: 'visual_direction',
|
||||||
schemaVersion: 'planning-session.v2',
|
|
||||||
engine: 'planning-session-v2',
|
|
||||||
sessionId: 'home-planning-v2-session',
|
|
||||||
projectId: 'local-project-draft',
|
|
||||||
mode: 'gdd',
|
|
||||||
status: 'awaiting_user',
|
|
||||||
turnIndex: 1,
|
|
||||||
questionCount: 1,
|
|
||||||
questionLimit: 8,
|
|
||||||
revisionCount: 0,
|
|
||||||
currentArtifactVersion: null,
|
|
||||||
currentQuestion: {
|
|
||||||
id: 'visual_direction',
|
|
||||||
header: '当前要决定:首版美术方向',
|
|
||||||
question: '首版角色规范图采用哪种美术方向?',
|
question: '首版角色规范图采用哪种美术方向?',
|
||||||
options: [
|
options: ['像素', '扁平'],
|
||||||
{ label: '像素', description: '低成本像素风。' },
|
createdAt: 1,
|
||||||
{ label: '扁平', description: '清晰的扁平插画风。' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
capabilities: { tools: [], skills: [] },
|
}),
|
||||||
processingSeconds: 1,
|
|
||||||
createdAtUtc: '2026-09-03T00:00:00Z',
|
|
||||||
updatedAtUtc: '2026-09-03T00:00:01Z',
|
|
||||||
lastError: null,
|
|
||||||
},
|
|
||||||
result: {
|
|
||||||
schemaVersion: 'planning-turn-result.v2',
|
|
||||||
kind: 'question',
|
|
||||||
payload: {
|
|
||||||
question: {
|
|
||||||
id: 'visual_direction',
|
|
||||||
header: '当前要决定:首版美术方向',
|
|
||||||
question: '首版角色规范图采用哪种美术方向?',
|
|
||||||
options: [
|
|
||||||
{ label: '像素', description: '低成本像素风。' },
|
|
||||||
{ label: '扁平', description: '清晰的扁平插画风。' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
currentArtifact: null,
|
|
||||||
replayed: false,
|
|
||||||
});
|
});
|
||||||
const invoke = vi.fn(
|
const invoke = vi.fn(
|
||||||
async (command: string, args?: Record<string, unknown>) => {
|
async (command: string, args?: Record<string, unknown>) => {
|
||||||
@@ -2063,15 +2045,24 @@ export function registerHomeProjectCreationTests() {
|
|||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(invoke).toHaveBeenCalledWith(
|
expect(invoke).toHaveBeenCalledWith(
|
||||||
'start_planning_session_v2',
|
'continue_design_agent_session',
|
||||||
expect.objectContaining({ prompt: '2D射击游戏', mode: 'gdd' }),
|
expect.objectContaining({
|
||||||
|
projectPath,
|
||||||
|
input: expect.objectContaining({
|
||||||
|
type: 'message',
|
||||||
|
text: '2D射击游戏',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const strip = await screen.findByLabelText('立项策划运行状态');
|
|
||||||
expect(
|
expect(
|
||||||
within(strip).getByText('首版角色规范图采用哪种美术方向?'),
|
invoke.mock.calls.some(
|
||||||
).not.toBeNull();
|
([command]) => command === 'start_planning_session_v2',
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
|
||||||
|
await screen.findByText('首版角色规范图采用哪种美术方向?');
|
||||||
|
expect(screen.getByRole('button', { name: '像素' })).not.toBeNull();
|
||||||
});
|
});
|
||||||
it('refreshes Direct Codex art commits while the turn is still running and after a later failure', async () => {
|
it('refreshes Direct Codex art commits while the turn is still running and after a later failure', async () => {
|
||||||
const projectPath =
|
const projectPath =
|
||||||
|
|||||||
@@ -4185,6 +4185,14 @@
|
|||||||
- 关联:`apps/ai-game-creator-shell/scripts/game-creator-config-wizard.mjs`、`apps/ai-game-creator-shell/scripts/agent-swarm-test-chat.mjs`、`apps/ai-game-creator-shell/scripts/check-config.mjs`、`apps/ai-game-creator-shell/tests/agentSwarmTestEntry.test.ts`。
|
- 关联:`apps/ai-game-creator-shell/scripts/game-creator-config-wizard.mjs`、`apps/ai-game-creator-shell/scripts/agent-swarm-test-chat.mjs`、`apps/ai-game-creator-shell/scripts/check-config.mjs`、`apps/ai-game-creator-shell/tests/agentSwarmTestEntry.test.ts`。
|
||||||
- 真实验收状态:外部 Provider 与画布 API 均可调用不等于全链路验收通过。2026-07-27 新起的独立轮次使用 `npm run agc:test:chat -- --timeout-minutes 75`,约 `59m50s` 后以退出码 `0` 完整 **PASS**:同一轮完成固定 `16` 个 manifest task exactly-once、七份基础产物、两张真实画布 PNG、当前 revision 静态检查、desktop / mobile `lane-defense-v1` playtest、唯一终态回复和安全清理;`turn.report` 的 busy / pending / running / confirmation / user-input / reconciliation 均为 `0`。此前失败轮、部分产物、单项接口成功和确定性结果仍不得与本轮拼接。
|
- 真实验收状态:外部 Provider 与画布 API 均可调用不等于全链路验收通过。2026-07-27 新起的独立轮次使用 `npm run agc:test:chat -- --timeout-minutes 75`,约 `59m50s` 后以退出码 `0` 完整 **PASS**:同一轮完成固定 `16` 个 manifest task exactly-once、七份基础产物、两张真实画布 PNG、当前 revision 静态检查、desktop / mobile `lane-defense-v1` playtest、唯一终态回复和安全清理;`turn.report` 的 busy / pending / running / confirmation / user-input / reconciliation 均为 `0`。此前失败轮、部分产物、单项接口成功和确定性结果仍不得与本轮拼接。
|
||||||
|
|
||||||
|
## 仓库回退配置模板不能被读取通道私有化锁定
|
||||||
|
|
||||||
|
- 现象:Windows 上 `apps/ai-game-creator-shell/game-creator.config.json` 莫名其妙被"加锁"(DACL 被剥成只剩一个陌生 SID,连 `Get-Acl` 都 unauthorized),开发 agent 和其他用户无法修改,cargo 也因 `include_str!` 读不到文件而不能编译;手动解锁后过一段时间又被锁。
|
||||||
|
- 原因:无 AppHandle 的开发 CLI(`llm-status`、`agent-run`、`agc:test:chat` 等)经 `game_creator_config_paths()` 从 CWD / `current_exe` 向上回溯 8 级探测到 worktree 里的 git 跟踪模板后,读取走了为 AppData 私密凭据设计的私有通道 `open_project_private_regular_file` → `prepare_game_creator_private_path_for_read`;该函数名为 "for read",在 Windows 上却无条件收紧目标 DACL 为"仅当前进程用户、禁止继承"。沙箱 agent 是其 checkout 文件的 owner,校验通过后被静默私有化,其他账号全部 Access Denied。
|
||||||
|
- 处理:配置读取按路径归属分流——`read_game_creator_config_file` 只对位于 `game_creator_runtime_config_dir()`(AppData 托管目录)内的真实凭据走私有加固读取;仓库旁边的回退模板 / local 覆盖一律走 `open_project_snapshot_regular_file` 非变异快照通道,读取绝不修改 owner / DACL。这与 `open_project_private_regular_file` 注释中"非用户明确选择的文件用 snapshot 读"的既有原则一致。
|
||||||
|
- 教训:任何名为"读前准备"的函数若附带权限收紧副作用,都必须按路径是否属于本进程托管范围设白名单;共享仓库文件、git 跟踪文件永远不在加固范围内。排查"文件莫名被锁"时优先查 DACL owner 是哪位 SID,再倒推哪个进程以该身份运行过。
|
||||||
|
- 验证:`tests::configuration::fallback_template_read_stays_on_snapshot_channel_outside_runtime_dir` 与 `runtime_config_read_stays_on_private_channel_inside_runtime_dir` 锁定两条通道的分流;`node scripts/check-config.mjs` 通过。
|
||||||
|
|
||||||
## 项目总控空态和持久 Runtime 不能依赖同一份 Session 索引
|
## 项目总控空态和持久 Runtime 不能依赖同一份 Session 索引
|
||||||
|
|
||||||
- 现象:新项目尚未发消息时右侧总控区域只剩整块空白;已有 `needs-reconciliation` Runtime 的项目重新打开后,也可能看不到失败状态卡。
|
- 现象:新项目尚未发消息时右侧总控区域只剩整块空白;已有 `needs-reconciliation` Runtime 的项目重新打开后,也可能看不到失败状态卡。
|
||||||
|
|||||||
Reference in New Issue
Block a user