继续收敛 CC 与 MCP 具体错误
保留 CC 隔离目录与 Node 版本探测的底层原因 保留 Direct 工具桥指纹、回执和项目路径读取错误 让外部 MCP journal 读取与资源读取返回具体正文
This commit is contained in:
@@ -159,7 +159,7 @@ fn configure_sidecar_command(
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
configure_claude_code_environment(command, llm, home);
|
||||
configure_claude_code_environment(command, llm, home)?;
|
||||
configure_claude_code_process(command);
|
||||
Ok(())
|
||||
}
|
||||
@@ -466,7 +466,6 @@ pub(crate) fn game_creator_claude_code_cli_version_identity() -> Result<String,
|
||||
let output = std::process::Command::new(&node)
|
||||
.arg("--version")
|
||||
.stdin(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.output()
|
||||
.map_err(|error| {
|
||||
// `output()` 失败只说明进程没能启动(可执行文件缺失、权限不足或被拦截),
|
||||
@@ -474,7 +473,19 @@ pub(crate) fn game_creator_claude_code_cli_version_identity() -> Result<String,
|
||||
format!("Claude Agent SDK sidecar 的 Node runtime 启动失败:{error}")
|
||||
})?;
|
||||
if !output.status.success() {
|
||||
return Err("Claude Agent SDK sidecar 的 Node runtime 版本检查失败".to_string());
|
||||
let detail = crate::agent::redact_agent_runtime_error(
|
||||
Path::new("__agc_no_project_root__"),
|
||||
&String::from_utf8_lossy(&output.stderr),
|
||||
480,
|
||||
);
|
||||
let detail = if detail.trim().is_empty() {
|
||||
format!("退出码={}", output.status)
|
||||
} else {
|
||||
format!("退出码={};stderr={detail}", output.status)
|
||||
};
|
||||
return Err(format!(
|
||||
"Claude Agent SDK sidecar 的 Node runtime 版本检查失败:{detail}"
|
||||
));
|
||||
}
|
||||
Ok(format!(
|
||||
"claude-agent-sdk-sidecar@{};node={};entry={}",
|
||||
@@ -617,7 +628,7 @@ fn configure_claude_code_environment(
|
||||
command: &mut tokio::process::Command,
|
||||
llm: Option<&GameCreatorLlmConfig>,
|
||||
isolated_home: &Path,
|
||||
) {
|
||||
) -> Result<(), String> {
|
||||
command.env_clear();
|
||||
for name in [
|
||||
"PATH",
|
||||
@@ -641,7 +652,8 @@ fn configure_claude_code_environment(
|
||||
copy_env(command, name);
|
||||
}
|
||||
let isolated_claude_home = isolated_home.join("claude");
|
||||
let _ = std::fs::create_dir_all(&isolated_claude_home);
|
||||
std::fs::create_dir_all(&isolated_claude_home)
|
||||
.map_err(|error| format!("准备 Claude Code 隔离目录失败:{error}"))?;
|
||||
command
|
||||
.env("HOME", isolated_home)
|
||||
// Windows 上 Node 的 `os.homedir()` 只看 `USERPROFILE`;不隔离它,Claude Code 会去读
|
||||
@@ -689,6 +701,7 @@ fn configure_claude_code_environment(
|
||||
command.env("ANTHROPIC_MODEL", llm.model.trim());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn claude_result_error_detail(value: &serde_json::Value) -> Option<String> {
|
||||
@@ -2146,7 +2159,7 @@ mod tests {
|
||||
fn sidecar_environment_is_isolated_from_the_local_claude_code_home() {
|
||||
let home = std::env::temp_dir().join("agc-cc-isolated-home-test");
|
||||
let mut command = tokio::process::Command::new("node");
|
||||
configure_claude_code_environment(&mut command, None, &home);
|
||||
configure_claude_code_environment(&mut command, None, &home).unwrap();
|
||||
let envs = command
|
||||
.as_std()
|
||||
.get_envs()
|
||||
|
||||
@@ -370,7 +370,7 @@ impl ExecutionAdapter {
|
||||
Ok::<_, String>((session.root.clone(), session))
|
||||
})
|
||||
.await
|
||||
.map_err(|_| "宿主执行身份读取中断")??;
|
||||
.map_err(|error| format!("宿主执行身份读取中断:{error}"))??;
|
||||
Ok(Self::new(
|
||||
root,
|
||||
session,
|
||||
|
||||
@@ -876,13 +876,13 @@ pub(super) fn resolve_direct_codex_project_authority(
|
||||
return Err("AGC 直连项目根目录必须是绝对路径".to_string());
|
||||
}
|
||||
let project_metadata = std::fs::metadata(project_root)
|
||||
.map_err(|_| "AGC 直连项目根目录不存在或无法读取".to_string())?;
|
||||
.map_err(|error| format!("AGC 直连项目根目录不存在或无法读取:{error}"))?;
|
||||
if !project_metadata.is_dir() {
|
||||
return Err("AGC 直连项目根目录不是目录".to_string());
|
||||
}
|
||||
let project_root = project_root
|
||||
.canonicalize()
|
||||
.map_err(|_| "AGC 直连项目根目录无法安全解析".to_string())?;
|
||||
.map_err(|error| format!("AGC 直连项目根目录无法安全解析:{error}"))?;
|
||||
// 用户选择的项目目录就是 Codex 工作区根,不再强制要求 game/ 子目录。
|
||||
// 原生文件工具仍由项目文件层拒绝 .agent/**、.git/**、密钥等控制面路径。
|
||||
Ok((project_root.clone(), project_root))
|
||||
@@ -2597,8 +2597,10 @@ impl CodexAppServerConnection {
|
||||
platform_llm::LlmError::InvalidRequest("Direct 连接缺少项目目录".into())
|
||||
})?
|
||||
.canonicalize()
|
||||
.map_err(|_| {
|
||||
platform_llm::LlmError::InvalidRequest("Direct 项目目录不可用".into())
|
||||
.map_err(|error| {
|
||||
platform_llm::LlmError::InvalidRequest(format!(
|
||||
"Direct 项目目录不可用:{error}"
|
||||
))
|
||||
})?;
|
||||
let bind_root = root.clone();
|
||||
let bind_executable = executable.clone();
|
||||
@@ -2612,7 +2614,9 @@ impl CodexAppServerConnection {
|
||||
session.bind_codex_executor(&bind_executable, &bind_version)
|
||||
})
|
||||
.await
|
||||
.map_err(|_| platform_llm::LlmError::InvalidRequest("Direct 执行器绑定中断".into()))?
|
||||
.map_err(|error| {
|
||||
platform_llm::LlmError::InvalidRequest(format!("Direct 执行器绑定中断:{error}"))
|
||||
})?
|
||||
.map_err(platform_llm::LlmError::InvalidRequest)?;
|
||||
Some(root)
|
||||
} else {
|
||||
|
||||
@@ -397,7 +397,7 @@ fn closed_error(phase: ExecutionPhase) -> String {
|
||||
pub(super) fn current(root: &Path) -> Result<Arc<ExecutionSession>, String> {
|
||||
let root = root
|
||||
.canonicalize()
|
||||
.map_err(|_| "direct-execution-project: 项目不可用")?;
|
||||
.map_err(|error| format!("direct-execution-project: 项目不可用:{error}"))?;
|
||||
sessions()
|
||||
.lock()
|
||||
.map_err(|_| "direct-execution-state: 会话状态不可用")?
|
||||
|
||||
@@ -3990,7 +3990,10 @@ async fn handle_direct_tool_bridge(
|
||||
let session = super::direct_execution::current(&root)?;
|
||||
let has_evidence = !session.snapshot()?.evidence.is_empty();
|
||||
let before = if has_evidence {
|
||||
super::direct_validation::source_fingerprint(&root).ok()
|
||||
Some(
|
||||
super::direct_validation::source_fingerprint(&root)
|
||||
.map_err(|cause| format!("执行前成果指纹读取失败:{cause}"))?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -4213,10 +4216,18 @@ async fn handle_direct_tool_bridge(
|
||||
let root = state.root.clone();
|
||||
let passed = result.is_ok();
|
||||
let finished = tokio::task::spawn_blocking(move || {
|
||||
let changed = has_evidence
|
||||
&& before
|
||||
.zip(super::direct_validation::source_fingerprint(&root).ok())
|
||||
.is_none_or(|(before, after)| before != after);
|
||||
let changed = if has_evidence {
|
||||
match before {
|
||||
Some(before) => {
|
||||
let after = super::direct_validation::source_fingerprint(&root)
|
||||
.map_err(|cause| format!("执行后成果指纹读取失败:{cause}"))?;
|
||||
before != after
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if !passed && dispatch_denied {
|
||||
lease.finish_paid_dispatch_denied(changed)
|
||||
} else {
|
||||
@@ -4224,14 +4235,19 @@ async fn handle_direct_tool_bridge(
|
||||
}
|
||||
})
|
||||
.await;
|
||||
if !matches!(finished, Ok(Ok(()))) {
|
||||
let receipt_error = match finished {
|
||||
Ok(Ok(())) => None,
|
||||
Ok(Err(error)) => Some(format!("执行回执结算失败:{error}")),
|
||||
Err(error) => Some(format!("执行回执结算任务未返回:{error}")),
|
||||
};
|
||||
if let Some(cause) = receipt_error {
|
||||
return Json(compose_direct_tool_outcome(
|
||||
&state,
|
||||
request.tool.as_str(),
|
||||
&diagnostics_arguments,
|
||||
dispatch_denied,
|
||||
Err(ToolCallError::from(
|
||||
&DirectExecutionGateRejection::ReceiptNotPersisted,
|
||||
&DirectExecutionGateRejection::ReceiptNotPersisted { cause },
|
||||
)),
|
||||
));
|
||||
}
|
||||
@@ -4270,7 +4286,7 @@ async fn start_tool_bridge_for_source(
|
||||
}
|
||||
let root = root
|
||||
.canonicalize()
|
||||
.map_err(|_| "AGC 工具桥项目目录无法安全解析".to_string())?;
|
||||
.map_err(|error| format!("AGC 工具桥项目目录无法安全解析:{error}"))?;
|
||||
let route = format!("/tool-{}", uuid::Uuid::new_v4().simple());
|
||||
let listener = tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0))
|
||||
.await
|
||||
|
||||
@@ -1186,17 +1186,22 @@ fn validate_external_mcp_record_arguments(
|
||||
|
||||
fn read_external_mcp_journal(root: &Path) -> Result<Vec<Value>, String> {
|
||||
let path = external_mcp_journal_path(root);
|
||||
let Ok(bytes) = std::fs::read(&path) else {
|
||||
return Ok(Vec::new());
|
||||
let bytes = match std::fs::read(&path) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
|
||||
Err(error) => return Err(format!("读取 Codex 返回记录失败:{error}")),
|
||||
};
|
||||
if bytes.len() as u64 > EXTERNAL_MCP_JOURNAL_MAX_BYTES {
|
||||
return Err("Codex 返回记录超过客户端保留上限".to_string());
|
||||
}
|
||||
bytes
|
||||
.split(|byte| *byte == b'\n')
|
||||
.filter(|line| !line.is_empty())
|
||||
.map(|line| {
|
||||
serde_json::from_slice::<Value>(line).map_err(|_| "Codex 返回记录格式损坏".to_string())
|
||||
.enumerate()
|
||||
.filter(|(_, line)| !line.is_empty())
|
||||
.map(|(line_number, line)| {
|
||||
serde_json::from_slice::<Value>(line).map_err(|error| {
|
||||
format!("Codex 返回记录格式损坏:第 {} 行:{error}", line_number + 1)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -1504,15 +1509,20 @@ async fn handle_direct_tools_mcp_request(root: &Path, request: Value) -> Option<
|
||||
if uri != "agc://conversation/codex-responses" {
|
||||
Some(mcp_error(id, -32602, "未知资源"))
|
||||
} else {
|
||||
let text = read_external_mcp_journal(root)
|
||||
.map(|records| {
|
||||
records
|
||||
.iter()
|
||||
.map(Value::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let text = match read_external_mcp_journal(root) {
|
||||
Ok(records) => records
|
||||
.iter()
|
||||
.map(Value::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
Err(error) => {
|
||||
return Some(mcp_error(
|
||||
id,
|
||||
-32603,
|
||||
&format!("Codex 返回记录资源读取失败:{error}"),
|
||||
));
|
||||
}
|
||||
};
|
||||
if text.len() > DIRECT_TOOLS_MCP_MAX_REQUEST_BYTES {
|
||||
return Some(mcp_error(id, -32000, "Codex 返回记录资源超过响应大小上限"));
|
||||
}
|
||||
@@ -3434,6 +3444,26 @@ mod tests {
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_codex_response_journal_distinguishes_missing_from_read_and_parse_errors() {
|
||||
let temporary = crate::tests::canonical_test_tempdir("direct-tools-read-response-");
|
||||
let root = temporary.path();
|
||||
init_local_game_project_at(root, "direct-tools-read-response", "Codex 返回读取测试")
|
||||
.expect("init project");
|
||||
assert!(read_external_mcp_journal(root).unwrap().is_empty());
|
||||
|
||||
let journal = external_mcp_journal_path(root);
|
||||
std::fs::create_dir_all(&journal).unwrap();
|
||||
let error = read_external_mcp_journal(root).unwrap_err();
|
||||
assert!(error.contains("读取 Codex 返回记录失败"), "{error}");
|
||||
|
||||
std::fs::remove_dir_all(&journal).unwrap();
|
||||
std::fs::write(&journal, b"{broken-json\n").unwrap();
|
||||
let error = read_external_mcp_journal(root).unwrap_err();
|
||||
assert!(error.contains("第 1 行"), "{error}");
|
||||
assert!(error.contains("Codex 返回记录格式损坏"), "{error}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recorded_codex_response_only_writes_its_own_journal() {
|
||||
let temporary = crate::tests::canonical_test_tempdir("direct-tools-record-response-");
|
||||
|
||||
@@ -259,8 +259,8 @@ pub(crate) enum DirectExecutionGateRejection {
|
||||
SessionUnavailable { cause: String },
|
||||
/// 取执行会话的阻塞任务没有返回;保留 join/panic 事实,避免退成无因固定句。
|
||||
SessionTaskLost { cause: String },
|
||||
/// 工具已返回,但执行回执没有可靠落盘。
|
||||
ReceiptNotPersisted,
|
||||
/// 工具已返回,但执行回执没有可靠落盘;保留落盘或回执任务的具体原因。
|
||||
ReceiptNotPersisted { cause: String },
|
||||
}
|
||||
|
||||
impl ToolFailure for DirectExecutionGateRejection {
|
||||
@@ -276,10 +276,9 @@ impl ToolFailure for DirectExecutionGateRejection {
|
||||
Self::SessionTaskLost { cause } => {
|
||||
format!("宿主执行会话任务未返回:{cause}")
|
||||
}
|
||||
Self::ReceiptNotPersisted => {
|
||||
"工具已返回,但宿主执行回执未可靠落盘;请查看交付状态并核对原操作,不自动重放"
|
||||
.to_string()
|
||||
}
|
||||
Self::ReceiptNotPersisted { cause } => format!(
|
||||
"工具已返回,但宿主执行回执未可靠落盘:{cause};请查看交付状态并核对原操作,不自动重放"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,3 +86,5 @@ DirectProject 回合失败在确认不是客户端内部不可归类故障时,
|
||||
- 错误报告对话框读取最新错误事件或诊断日志失败时原先只显示“暂不可用/使用快照”;现把读取 IPC/文件正文附在状态提示中,快照回退行为保持不变。
|
||||
- WorkspaceLauncher 的清单版本读取、失效事件重读和事件订阅失败原先只写日志或静默回退;现把安全正文送入启动器状态提示,仍保留已有回退刷新路径。
|
||||
- Direct 补丁事务的目标指纹读取原先把元数据、打开、正文读取和文件超限都折叠为 `None`,可能误判文件缺失或继续比较;现只对确实 `NotFound` 保留 `missing`,其余失败返回目标相对路径、阶段和底层正文,避免补丁结果被错误归因。
|
||||
- CC/Direct 工具桥的执行前后成果指纹、回执结算 JoinError/落盘失败,以及外部 MCP journal 的读取/JSON 行解析原先仍会被 `.ok()`、固定回执句或“无记录”吞掉;现分别保留指纹阶段、回执任务/落盘原因、journal 行号和底层正文,只有真实 `NotFound` 才视为空记录。
|
||||
- 外部 MCP `resources/read` 仍把已能返回具体原因的 Codex journal 读取错误 `unwrap_or_default()` 成空资源;现把读取失败按 MCP 错误响应返回,Direct 执行会话当前项目 canonicalize 也保留底层 I/O 正文。
|
||||
|
||||
Reference in New Issue
Block a user