839051f532
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 1m50s
Project CI / Backend tests (pull_request) Failing after 24s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m47s
Project CI / Repository checks (pull_request) Failing after 21s
Project CI / Frontend tests (pull_request) Successful in 2m38s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 3m41s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m23s
Project CI / Native shell tests (pull_request) Successful in 6m20s
- 删除 agent/runtime_driver、runtime_protocol、runtime_tools、runtime_actions、runtime_state、runtime_adapter、context_compaction、collaboration、delegation、goal 等自建 Runtime 执行面及专属测试与 fixture - CliCommand 收敛为 LlmStatus | EnvironmentCheck | PreviewServe,删除 --agent-run / --agent-task / --agent-goal-* / --agent-resume / --agent-context-compact 等 CLI 与 Tauri 命令白名单条目 - 外部 Runner 收缩为编辑器桥 RPC 与 runner.attach_gui_owner + GUI owner 参与锁 / watchdog,保留 --agent-runner - 前端删除 AgentRuntime* / AgentGoal* 状态与命令调用链,移除 agent-runtime real-e2e / steer / smoke / llm-transient-fault 脚本与对应 CI job - 内置工具错误收口为 typed error,抽取 agent/tool/error.rs 共享错误供各工具复用 - 新增 agent/redaction.rs、runtime_models.rs、json_sidecar.rs、task_lock.rs、process_identity.rs、art_manifest.rs、design_session.rs、direct_events.rs - 同步更新 ADR、decision-log、Runtime V1.1、App 实施计划、本地开发运维、pitfalls 与 CI 脚本
340 lines
12 KiB
Rust
340 lines
12 KiB
Rust
use super::*;
|
|
|
|
const PREVIEW_SERVE_USAGE: &str = "用法:--preview-serve <本地项目绝对路径>";
|
|
|
|
#[derive(Debug, Eq, PartialEq)]
|
|
pub(crate) enum CliCommand {
|
|
LlmStatus,
|
|
EnvironmentCheck,
|
|
PreviewServe { project_path: PathBuf },
|
|
}
|
|
|
|
impl CliCommand {
|
|
fn project_path_mut(&mut self) -> Option<(&mut PathBuf, bool)> {
|
|
match self {
|
|
Self::PreviewServe { project_path } => Some((project_path, false)),
|
|
Self::LlmStatus | Self::EnvironmentCheck => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn canonicalize_cli_path(
|
|
path: &Path,
|
|
label: &str,
|
|
allow_missing_leaf: bool,
|
|
) -> Result<PathBuf, String> {
|
|
if !path.is_absolute() {
|
|
return Err(format!("{label} 必须是绝对路径"));
|
|
}
|
|
match fs::canonicalize(path) {
|
|
Ok(path) => Ok(path),
|
|
Err(error) if allow_missing_leaf && error.kind() == std::io::ErrorKind::NotFound => {
|
|
let parent = path
|
|
.parent()
|
|
.ok_or_else(|| format!("{label} 缺少可解析的父目录"))?;
|
|
let file_name = path
|
|
.file_name()
|
|
.ok_or_else(|| format!("{label} 缺少目录名"))?;
|
|
let parent = fs::canonicalize(parent).map_err(|parent_error| {
|
|
format!(
|
|
"解析 {label} 父目录失败:{}: {parent_error}",
|
|
parent.display()
|
|
)
|
|
})?;
|
|
Ok(parent.join(file_name))
|
|
}
|
|
Err(error) => Err(format!("解析 {label} 失败:{}: {error}", path.display())),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn prepare_cli_command_paths(
|
|
command: &mut CliCommand,
|
|
runtime_config_dir: Option<&Path>,
|
|
) -> Result<Option<PathBuf>, String> {
|
|
let project_path = if let Some((path, allow_missing_leaf)) = command.project_path_mut() {
|
|
let canonical = canonicalize_cli_path(path, "本地项目路径", allow_missing_leaf)?;
|
|
*path = canonical.clone();
|
|
Some(canonical)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let config_dir = runtime_config_dir
|
|
.map(|path| canonicalize_cli_path(path, "--config-dir", true))
|
|
.transpose()?;
|
|
if let (Some(config_dir), Some(project_path)) = (&config_dir, &project_path) {
|
|
validate_game_creator_runtime_config_dir_outside_project(config_dir, project_path)?;
|
|
}
|
|
Ok(config_dir)
|
|
}
|
|
|
|
pub(crate) fn take_cli_runtime_config_dir(
|
|
args: &mut Vec<String>,
|
|
) -> Result<Option<PathBuf>, String> {
|
|
let positions = args
|
|
.iter()
|
|
.enumerate()
|
|
.filter_map(|(index, arg)| (arg == "--config-dir").then_some(index))
|
|
.collect::<Vec<_>>();
|
|
if positions.len() > 1 {
|
|
return Err("--config-dir 只能指定一次".to_string());
|
|
}
|
|
let Some(index) = positions.first().copied() else {
|
|
return Ok(None);
|
|
};
|
|
let value = args
|
|
.get(index + 1)
|
|
.map(String::as_str)
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.ok_or_else(|| "--config-dir 缺少目录路径".to_string())?;
|
|
let path = PathBuf::from(value);
|
|
if !path.is_absolute() {
|
|
return Err("--config-dir 必须是绝对路径".to_string());
|
|
}
|
|
args.drain(index..=index + 1);
|
|
Ok(Some(path))
|
|
}
|
|
|
|
pub(crate) fn game_creator_llm_status_lines(status: &GameCreatorLlmConfigStatus) -> Vec<String> {
|
|
let mut lines = vec![
|
|
format!("agent.mode={}", status.agent_mode),
|
|
format!("llm.configured={}", status.configured),
|
|
format!(
|
|
"llm.accountCredentialState={}",
|
|
status.account_credential_state
|
|
),
|
|
format!("llm.officialRouteLocked={}", status.official_route_locked),
|
|
format!("llm.reasoningEffort={}", status.reasoning_effort),
|
|
format!("llm.stream={}", status.stream),
|
|
format!("llm.contextWindowTokens={}", status.context_window_tokens),
|
|
format!(
|
|
"llm.autoCompactTokenLimit={}",
|
|
status.auto_compact_token_limit
|
|
),
|
|
format!("llm.requestTimeoutMs={}", status.request_timeout_ms),
|
|
format!("llm.maxRetries={}", status.max_retries),
|
|
format!("llm.retryBackoffMs={}", status.retry_backoff_ms),
|
|
];
|
|
if status.agent_mode == GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER {
|
|
lines.push(format!(
|
|
"llm.controlledWebSearchEnabled={}",
|
|
status.web_search_enabled
|
|
));
|
|
lines.push("llm.codexNativeWebSearch=disabled".to_string());
|
|
} else {
|
|
lines.push(format!(
|
|
"llm.webSearchEnabled={}",
|
|
status.web_search_enabled
|
|
));
|
|
}
|
|
for agent in &status.agents {
|
|
lines.push(format!(
|
|
"llm.agent.{}.configured={}",
|
|
agent.agent_id, agent.configured
|
|
));
|
|
lines.push(format!(
|
|
"llm.agent.{}.accountCredentialState={}",
|
|
agent.agent_id, agent.account_credential_state
|
|
));
|
|
lines.push(format!(
|
|
"llm.agent.{}.officialRouteLocked={}",
|
|
agent.agent_id, agent.official_route_locked
|
|
));
|
|
lines.push(format!(
|
|
"llm.agent.{}.reasoningEffort={}",
|
|
agent.agent_id, agent.reasoning_effort
|
|
));
|
|
lines.push(format!(
|
|
"llm.agent.{}.stream={}",
|
|
agent.agent_id, agent.stream
|
|
));
|
|
if agent.agent_mode == GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER {
|
|
lines.push(format!(
|
|
"llm.agent.{}.controlledWebSearchEnabled={}",
|
|
agent.agent_id, agent.web_search_enabled
|
|
));
|
|
lines.push(format!(
|
|
"llm.agent.{}.codexNativeWebSearch=disabled",
|
|
agent.agent_id
|
|
));
|
|
} else {
|
|
lines.push(format!(
|
|
"llm.agent.{}.webSearchEnabled={}",
|
|
agent.agent_id, agent.web_search_enabled
|
|
));
|
|
}
|
|
lines.push(format!(
|
|
"llm.agent.{}.contextWindowTokens={}",
|
|
agent.agent_id, agent.context_window_tokens
|
|
));
|
|
lines.push(format!(
|
|
"llm.agent.{}.autoCompactTokenLimit={}",
|
|
agent.agent_id, agent.auto_compact_token_limit
|
|
));
|
|
lines.push(format!(
|
|
"llm.agent.{}.requestTimeoutMs={}",
|
|
agent.agent_id, agent.request_timeout_ms
|
|
));
|
|
lines.push(format!(
|
|
"llm.agent.{}.maxRetries={}",
|
|
agent.agent_id, agent.max_retries
|
|
));
|
|
lines.push(format!(
|
|
"llm.agent.{}.retryBackoffMs={}",
|
|
agent.agent_id, agent.retry_backoff_ms
|
|
));
|
|
if let Some(error) = agent.error.as_deref() {
|
|
lines.push(format!("llm.agent.{}.error={error}", agent.agent_id));
|
|
}
|
|
}
|
|
if let Some(error) = status.error.as_deref() {
|
|
lines.push(format!("llm.error={error}"));
|
|
}
|
|
lines
|
|
}
|
|
|
|
pub(crate) fn parse_cli_command(args: &[String]) -> Result<Option<CliCommand>, String> {
|
|
if args.first().map(String::as_str) == Some("--environment-check") {
|
|
if args.len() != 1 {
|
|
return Err("用法:--environment-check".into());
|
|
}
|
|
return Ok(Some(CliCommand::EnvironmentCheck));
|
|
}
|
|
if args.first().map(String::as_str) == Some("--preview-serve") {
|
|
if args.len() != 2 || args[1].trim().is_empty() {
|
|
return Err(PREVIEW_SERVE_USAGE.to_string());
|
|
}
|
|
return Ok(Some(CliCommand::PreviewServe {
|
|
project_path: PathBuf::from(&args[1]),
|
|
}));
|
|
}
|
|
if args.first().map(String::as_str) == Some("--llm-status") {
|
|
return Ok(Some(CliCommand::LlmStatus));
|
|
}
|
|
Ok(None)
|
|
}
|
|
|
|
pub(crate) fn run_cli_command(command: CliCommand) -> Result<(), String> {
|
|
match command {
|
|
CliCommand::EnvironmentCheck => {
|
|
let runtime = tokio::runtime::Builder::new_multi_thread()
|
|
.enable_all()
|
|
.build()
|
|
.map_err(|_| "无法启动环境预检".to_string())?;
|
|
let report = runtime.block_on(crate::environment_check::host_web_creation_preflight());
|
|
println!(
|
|
"{}",
|
|
serde_json::to_string(&report).map_err(|_| "无法序列化环境预检".to_string())?
|
|
);
|
|
if report["status"] == "ready" {
|
|
Ok(())
|
|
} else {
|
|
Err("Web 游戏环境预检未通过;未启动生成".into())
|
|
}
|
|
}
|
|
CliCommand::LlmStatus => {
|
|
let status = check_game_creator_llm_config_from_config();
|
|
for line in game_creator_llm_status_lines(&status) {
|
|
println!("{line}");
|
|
}
|
|
if status.configured {
|
|
Ok(())
|
|
} else {
|
|
Err("LLM 配置未就绪".to_string())
|
|
}
|
|
}
|
|
CliCommand::PreviewServe { project_path } => {
|
|
let runtime = tokio::runtime::Builder::new_multi_thread()
|
|
.enable_all()
|
|
.build()
|
|
.map_err(|error| format!("创建 CLI runtime 失败:{error}"))?;
|
|
let registry = game_creator_preview_registry();
|
|
let preview = start_local_game_preview_at(&project_path, ®istry)?;
|
|
println!("preview.running");
|
|
println!("projectPath={}", project_path.display());
|
|
println!("previewUrl={}", preview.url);
|
|
let wait_result = std::io::stdout()
|
|
.flush()
|
|
.map_err(|error| format!("刷新本地预览 CLI 输出失败:{error}"))
|
|
.and_then(|()| {
|
|
runtime
|
|
.block_on(tokio::signal::ctrl_c())
|
|
.map_err(|error| format!("等待 Ctrl+C 失败:{error}"))
|
|
});
|
|
let stop_result = stop_local_game_preview_for_root(Some(&project_path), ®istry)
|
|
.map(|_| ())
|
|
.map_err(|error| format!("停止本地预览失败:{error}"));
|
|
match (wait_result, stop_result) {
|
|
(Ok(()), Ok(())) => Ok(()),
|
|
(Err(error), Ok(())) | (Ok(()), Err(error)) => Err(error),
|
|
(Err(wait_error), Err(stop_error)) => {
|
|
Err(format!("{wait_error};同时{stop_error}"))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn environment_check_cli_accepts_no_project_script_or_credentials() {
|
|
assert_eq!(
|
|
parse_cli_command(&["--environment-check".into()]).unwrap(),
|
|
Some(CliCommand::EnvironmentCheck)
|
|
);
|
|
assert!(
|
|
parse_cli_command(&["--environment-check".into(), "user-script.js".into()]).is_err()
|
|
);
|
|
assert!(CliCommand::EnvironmentCheck.project_path_mut().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn parses_preview_serve_without_config() {
|
|
let project_path = std::env::current_dir().expect("current directory");
|
|
let mut command = parse_cli_command(&[
|
|
"--preview-serve".to_string(),
|
|
project_path.display().to_string(),
|
|
])
|
|
.expect("parse preview serve")
|
|
.expect("preview serve command");
|
|
|
|
assert_eq!(
|
|
command,
|
|
CliCommand::PreviewServe {
|
|
project_path: project_path.clone(),
|
|
}
|
|
);
|
|
assert_eq!(
|
|
prepare_cli_command_paths(&mut command, None)
|
|
.expect("prepare preview serve without config dir"),
|
|
None
|
|
);
|
|
assert_eq!(
|
|
command,
|
|
CliCommand::PreviewServe {
|
|
project_path: fs::canonicalize(project_path).expect("canonical project path"),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn preview_serve_requires_exactly_one_non_empty_project_path() {
|
|
for args in [
|
|
vec!["--preview-serve"],
|
|
vec!["--preview-serve", " "],
|
|
vec!["--preview-serve", "/tmp/game-project", "/tmp/extra"],
|
|
] {
|
|
let args = args.into_iter().map(str::to_string).collect::<Vec<_>>();
|
|
assert_eq!(
|
|
parse_cli_command(&args),
|
|
Err(PREVIEW_SERVE_USAGE.to_string()),
|
|
"unexpected parse result for {args:?}"
|
|
);
|
|
}
|
|
}
|
|
}
|