收敛 CLI 协议错误具体文本
保留 Claude sidecar 的解析、失败终态和退出状态 保留 Codex CLI 的失败事件与结构化输出 detail 补回 cc 超时和空回执的统一错误正文 补充脱敏协议错误回归测试与错误处理文档
This commit is contained in:
@@ -45,7 +45,7 @@ pub mod tool;
|
||||
pub(crate) use art_manifest::*;
|
||||
use claude_code_cli::*;
|
||||
pub(crate) use claude_code_cli::{
|
||||
cancel_direct_claude_code_turn_at, claude_code_failure_to_llm_error,
|
||||
cancel_direct_claude_code_turn_at, claude_code_failure_to_turn_error,
|
||||
direct_game_creator_claude_code_chat_at, direct_game_creator_claude_code_home_chat,
|
||||
game_creator_claude_code_cli_route_error, game_creator_claude_code_cli_version_identity,
|
||||
};
|
||||
|
||||
@@ -255,6 +255,7 @@ async fn run_sidecar_turn(
|
||||
let mut terminal = None;
|
||||
let mut idle_rounds = 0_u64;
|
||||
let mut silent_ms = 0_u64;
|
||||
let mut last_event_detail = None;
|
||||
loop {
|
||||
let line =
|
||||
match tokio::time::timeout(CLAUDE_CODE_EVENT_IDLE_LOG_TIMEOUT, lines.next_line())
|
||||
@@ -300,6 +301,13 @@ async fn run_sidecar_turn(
|
||||
"Claude Agent SDK sidecar 输出不是有效 JSON:{error};原文={detail}"
|
||||
))
|
||||
})?;
|
||||
last_event_detail = serde_json::to_string(&value).ok().map(|detail| {
|
||||
crate::agent::redact_agent_runtime_error(
|
||||
Path::new("__agc_no_project_root__"),
|
||||
&detail,
|
||||
480,
|
||||
)
|
||||
});
|
||||
match value.get("type").and_then(serde_json::Value::as_str) {
|
||||
Some("event") => {
|
||||
if let Some(event) = value.get("event") {
|
||||
@@ -315,12 +323,15 @@ async fn run_sidecar_turn(
|
||||
}
|
||||
Some("error") => {
|
||||
kill_claude_code_process_tree(pid);
|
||||
let detail = claude_result_error_detail(&value).or_else(|| {
|
||||
last_event_detail
|
||||
.clone()
|
||||
.filter(|detail| !detail.trim().is_empty())
|
||||
});
|
||||
return Err(SidecarTurnFailure::message(
|
||||
value
|
||||
.get("message")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("Claude Agent SDK sidecar 执行失败")
|
||||
.to_string(),
|
||||
detail
|
||||
.map(|detail| format!("Claude Agent SDK sidecar 执行失败:{detail}"))
|
||||
.unwrap_or_else(|| "Claude Agent SDK sidecar 执行失败".to_string()),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
@@ -329,13 +340,23 @@ async fn run_sidecar_turn(
|
||||
let status = child.wait().await.map_err(|error| {
|
||||
SidecarTurnFailure::message(format!("等待 Claude Agent SDK sidecar 结束失败:{error}"))
|
||||
})?;
|
||||
let exit_status = status
|
||||
.code()
|
||||
.map(|code| code.to_string())
|
||||
.unwrap_or_else(|| "signal".to_string());
|
||||
let result = terminal.ok_or_else(|| {
|
||||
SidecarTurnFailure::message("Claude Agent SDK sidecar 缺少终态".to_string())
|
||||
SidecarTurnFailure::message(format!(
|
||||
"Claude Agent SDK sidecar 缺少终态;exitStatus={exit_status};已收到 {} 个事件;最后事件={}",
|
||||
event_count,
|
||||
last_event_detail
|
||||
.filter(|detail| !detail.trim().is_empty())
|
||||
.unwrap_or_else(|| "<无有效事件>".to_string())
|
||||
))
|
||||
})?;
|
||||
if !status.success() {
|
||||
return Err(SidecarTurnFailure::message(
|
||||
"Claude Agent SDK sidecar 非零退出".to_string(),
|
||||
));
|
||||
return Err(SidecarTurnFailure::message(format!(
|
||||
"Claude Agent SDK sidecar 非零退出;exitStatus={exit_status}"
|
||||
)));
|
||||
}
|
||||
Ok(SidecarTurnResult { result })
|
||||
};
|
||||
@@ -658,22 +679,37 @@ fn configure_claude_code_environment(
|
||||
}
|
||||
|
||||
fn claude_result_error_detail(value: &serde_json::Value) -> Option<String> {
|
||||
fn value_detail(value: &serde_json::Value) -> Option<String> {
|
||||
match value {
|
||||
serde_json::Value::String(detail) => {
|
||||
let detail = detail.trim();
|
||||
(!detail.is_empty()).then(|| detail.chars().take(512).collect())
|
||||
}
|
||||
serde_json::Value::Object(object) => {
|
||||
let detail = ["message", "detail", "error", "type", "code"]
|
||||
.into_iter()
|
||||
.filter_map(|field| object.get(field).and_then(value_detail))
|
||||
.collect::<Vec<_>>()
|
||||
.join(";");
|
||||
(!detail.is_empty()).then_some(detail)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
let candidates = [
|
||||
value.get("result").and_then(serde_json::Value::as_str),
|
||||
value.get("error").and_then(serde_json::Value::as_str),
|
||||
value.get("message").and_then(serde_json::Value::as_str),
|
||||
value.get("result").and_then(value_detail),
|
||||
value.get("error").and_then(value_detail),
|
||||
value.get("message").and_then(value_detail),
|
||||
value
|
||||
.get("errors")
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.and_then(|errors| errors.first())
|
||||
.and_then(serde_json::Value::as_str),
|
||||
.and_then(value_detail),
|
||||
];
|
||||
candidates
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(str::trim)
|
||||
.find(|detail| !detail.is_empty())
|
||||
.map(|detail| detail.chars().take(512).collect())
|
||||
}
|
||||
|
||||
fn configure_claude_code_process(command: &mut tokio::process::Command) {
|
||||
@@ -806,6 +842,30 @@ pub(crate) fn claude_code_failure_to_llm_error(detail: String) -> platform_llm::
|
||||
platform_llm::LlmError::Transport(detail)
|
||||
}
|
||||
|
||||
/// 把 cc 的失败投影到 Direct 回合错误;平台 `LlmError::Timeout` / `EmptyResponse` 本身没有
|
||||
/// detail 字段,因此这两类必须在这里把 sidecar 的原始原因留在统一 `ModelCallFailed.detail` 中,
|
||||
/// 否则 stderr、超时阶段和缺失字段会在类型转换时丢掉,前端又只能显示固定兜底句。
|
||||
pub(crate) fn claude_code_failure_to_turn_error(
|
||||
detail: String,
|
||||
) -> super::codex_app_server::turn_error::TurnError {
|
||||
use super::codex_app_server::turn_error::{ModelCallFailed, ModelCallKind, TurnError};
|
||||
|
||||
let llm_error = claude_code_failure_to_llm_error(detail.clone());
|
||||
match llm_error {
|
||||
platform_llm::LlmError::Timeout { attempts } => {
|
||||
TurnError::ModelCallFailed(ModelCallFailed {
|
||||
kind: ModelCallKind::ResponseTimedOut { attempts },
|
||||
detail,
|
||||
})
|
||||
}
|
||||
platform_llm::LlmError::EmptyResponse => TurnError::ModelCallFailed(ModelCallFailed {
|
||||
kind: ModelCallKind::EmptyResponse,
|
||||
detail,
|
||||
}),
|
||||
other => TurnError::from_model_call(&other),
|
||||
}
|
||||
}
|
||||
|
||||
fn claude_code_failure_status_code(detail: &str) -> Option<u16> {
|
||||
let normalized = detail.to_ascii_lowercase();
|
||||
[
|
||||
@@ -835,8 +895,15 @@ fn parse_claude_code_result(
|
||||
stdout: &[u8],
|
||||
request: &LlmRunRequest,
|
||||
) -> Result<platform_llm::LlmRunResponse, platform_llm::LlmError> {
|
||||
let value: serde_json::Value = serde_json::from_slice(stdout).map_err(|_| {
|
||||
platform_llm::LlmError::Deserialize("Claude Code JSON 输出无效".to_string())
|
||||
let value: serde_json::Value = serde_json::from_slice(stdout).map_err(|error| {
|
||||
let detail = crate::agent::redact_agent_runtime_error(
|
||||
Path::new("__agc_no_project_root__"),
|
||||
&String::from_utf8_lossy(stdout),
|
||||
480,
|
||||
);
|
||||
platform_llm::LlmError::Deserialize(format!(
|
||||
"Claude Code JSON 输出无效:{error};原文={detail}"
|
||||
))
|
||||
})?;
|
||||
if value.get("is_error").and_then(serde_json::Value::as_bool) == Some(true) {
|
||||
let detail = claude_result_error_detail(&value)
|
||||
@@ -863,20 +930,33 @@ fn parse_claude_code_result(
|
||||
}
|
||||
(result, Vec::new())
|
||||
} else {
|
||||
let envelope = structured
|
||||
.or_else(|| serde_json::from_str(&result).ok())
|
||||
.ok_or_else(|| {
|
||||
platform_llm::LlmError::Deserialize(
|
||||
"Claude Code structured output 缺失".to_string(),
|
||||
)
|
||||
})?;
|
||||
let envelope = match structured {
|
||||
Some(value) => value,
|
||||
None => serde_json::from_str(&result).map_err(|error| {
|
||||
let detail = crate::agent::redact_agent_runtime_error(
|
||||
Path::new("__agc_no_project_root__"),
|
||||
&result,
|
||||
480,
|
||||
);
|
||||
platform_llm::LlmError::Deserialize(format!(
|
||||
"Claude Code structured output JSON 无效:{error};原文={detail}"
|
||||
))
|
||||
})?,
|
||||
};
|
||||
let calls = envelope
|
||||
.get("toolCalls")
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.ok_or_else(|| {
|
||||
platform_llm::LlmError::Deserialize(
|
||||
"Claude Code structured output 缺少 toolCalls".to_string(),
|
||||
)
|
||||
let detail =
|
||||
serde_json::to_string(&envelope).unwrap_or_else(|_| "<不可序列化>".into());
|
||||
let detail = crate::agent::redact_agent_runtime_error(
|
||||
Path::new("__agc_no_project_root__"),
|
||||
&detail,
|
||||
480,
|
||||
);
|
||||
platform_llm::LlmError::Deserialize(format!(
|
||||
"Claude Code structured output 缺少 toolCalls;原文={detail}"
|
||||
))
|
||||
})?;
|
||||
let calls = calls
|
||||
.iter()
|
||||
@@ -886,17 +966,20 @@ fn parse_claude_code_result(
|
||||
.get("name")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.ok_or_else(|| {
|
||||
platform_llm::LlmError::Deserialize(
|
||||
"Claude Code tool call 缺少 name".to_string(),
|
||||
)
|
||||
platform_llm::LlmError::Deserialize(format!(
|
||||
"Claude Code tool call 第 {} 项缺少 name;原文={}",
|
||||
index.saturating_add(1),
|
||||
serde_json::to_string(call).unwrap_or_else(|_| "<不可序列化>".into())
|
||||
))
|
||||
})?;
|
||||
let arguments = call
|
||||
.get("arguments")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.ok_or_else(|| {
|
||||
platform_llm::LlmError::Deserialize(
|
||||
"Claude Code tool call 缺少 arguments".to_string(),
|
||||
)
|
||||
platform_llm::LlmError::Deserialize(format!(
|
||||
"Claude Code tool call 第 {} 项缺少 arguments;name={name}",
|
||||
index.saturating_add(1)
|
||||
))
|
||||
})?;
|
||||
if !request.function_tools.iter().any(|tool| tool.name == name) {
|
||||
return Err(platform_llm::LlmError::Deserialize(
|
||||
@@ -1733,6 +1816,63 @@ mod tests {
|
||||
assert!(error.to_string().contains("Authentication failed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_claude_invalid_json_and_object_error_details() {
|
||||
let invalid = parse_claude_code_result(
|
||||
br#"{"is_error":false,"result":"unterminated""#,
|
||||
&request(false),
|
||||
)
|
||||
.expect_err("invalid JSON must be rejected");
|
||||
let invalid = invalid.to_string();
|
||||
assert!(invalid.contains("Claude Code JSON 输出无效"), "{invalid}");
|
||||
assert!(invalid.contains("line"), "{invalid}");
|
||||
assert!(invalid.contains("unterminated"), "{invalid}");
|
||||
|
||||
let object_error = parse_claude_code_result(
|
||||
br#"{"is_error":true,"error":{"type":"invalid_request_error","message":"HTTP 422: bad schema"}}"#,
|
||||
&request(false),
|
||||
)
|
||||
.expect_err("object error must be rejected");
|
||||
assert!(
|
||||
object_error.to_string().contains("HTTP 422: bad schema"),
|
||||
"{object_error}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cc_failure_projection_preserves_timeout_and_empty_response_detail() {
|
||||
let timeout = super::claude_code_failure_to_turn_error(
|
||||
"Claude Agent SDK sidecar 回合超时:连续 180000 ms 没有任何事件;stderr=OOM".into(),
|
||||
);
|
||||
match timeout {
|
||||
super::codex_app_server::turn_error::TurnError::ModelCallFailed(payload) => {
|
||||
assert!(matches!(
|
||||
payload.kind,
|
||||
super::codex_app_server::turn_error::ModelCallKind::ResponseTimedOut {
|
||||
attempts: 1
|
||||
}
|
||||
));
|
||||
assert!(payload.detail.contains("180000"));
|
||||
assert!(payload.detail.contains("OOM"));
|
||||
}
|
||||
other => panic!("expected timed out model failure, got {other:?}"),
|
||||
}
|
||||
|
||||
let empty = super::claude_code_failure_to_turn_error(
|
||||
"Claude Code 缺少最终回复;sidecar result 缺少 result 字段".into(),
|
||||
);
|
||||
match empty {
|
||||
super::codex_app_server::turn_error::TurnError::ModelCallFailed(payload) => {
|
||||
assert!(matches!(
|
||||
payload.kind,
|
||||
super::codex_app_server::turn_error::ModelCallKind::EmptyResponse
|
||||
));
|
||||
assert!(payload.detail.contains("缺少 result 字段"));
|
||||
}
|
||||
other => panic!("expected empty response model failure, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claude_failure_reuses_codex_error_categories() {
|
||||
assert!(matches!(
|
||||
|
||||
@@ -5428,9 +5428,7 @@ pub(crate) async fn direct_game_creator_codex_chat_at_with_optional_observer(
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
.map_err(|detail| {
|
||||
TurnError::from_model_call(&crate::agent::claude_code_failure_to_llm_error(detail))
|
||||
});
|
||||
.map_err(crate::agent::claude_code_failure_to_turn_error);
|
||||
}
|
||||
game_creator_codex_app_server_validate_llm_config(&config.llm).map_err(|error| {
|
||||
TurnError::EnvironmentNotReady(EnvironmentNotReady {
|
||||
|
||||
@@ -532,13 +532,14 @@ fn parse_game_creator_codex_cli_response(
|
||||
stdout: &[u8],
|
||||
request: &LlmRunRequest,
|
||||
) -> Result<platform_llm::LlmRunResponse, platform_llm::LlmError> {
|
||||
let stdout = std::str::from_utf8(stdout).map_err(|_| {
|
||||
platform_llm::LlmError::Deserialize("Codex CLI Agent JSONL 不是 UTF-8".to_string())
|
||||
let stdout = std::str::from_utf8(stdout).map_err(|error| {
|
||||
platform_llm::LlmError::Deserialize(format!("Codex CLI Agent JSONL 不是 UTF-8:{error}"))
|
||||
})?;
|
||||
let mut response_id = None;
|
||||
let mut final_message = None;
|
||||
let mut usage = None;
|
||||
let mut completed = false;
|
||||
let mut last_event_detail = None;
|
||||
for (index, line) in stdout.lines().enumerate() {
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
@@ -554,6 +555,11 @@ fn parse_game_creator_codex_cli_response(
|
||||
index.saturating_add(1)
|
||||
))
|
||||
})?;
|
||||
last_event_detail = Some(crate::agent::redact_agent_runtime_error(
|
||||
Path::new("__agc_no_project_root__"),
|
||||
line,
|
||||
480,
|
||||
));
|
||||
match event.get("type").and_then(serde_json::Value::as_str) {
|
||||
Some("thread.started") => {
|
||||
response_id = event
|
||||
@@ -592,11 +598,14 @@ fn parse_game_creator_codex_cli_response(
|
||||
}
|
||||
}
|
||||
Some("turn.failed") => {
|
||||
let detail = event
|
||||
let mut detail = event
|
||||
.get("error")
|
||||
.or_else(|| event.get("message"))
|
||||
.map(serde_json::Value::to_string)
|
||||
.unwrap_or_default();
|
||||
if detail.trim().is_empty() {
|
||||
detail = serde_json::to_string(&event).unwrap_or_default();
|
||||
}
|
||||
let detail = crate::agent::redact_agent_runtime_error(
|
||||
Path::new("__agc_no_project_root__"),
|
||||
detail.trim_matches('"'),
|
||||
@@ -612,9 +621,12 @@ fn parse_game_creator_codex_cli_response(
|
||||
}
|
||||
}
|
||||
if !completed {
|
||||
return Err(platform_llm::LlmError::Deserialize(
|
||||
"Codex CLI Agent 缺少 turn.completed 终态".to_string(),
|
||||
));
|
||||
return Err(platform_llm::LlmError::Deserialize(format!(
|
||||
"Codex CLI Agent 缺少 turn.completed 终态;最后事件={}",
|
||||
last_event_detail
|
||||
.filter(|detail| !detail.trim().is_empty())
|
||||
.unwrap_or_else(|| "<无有效事件>".to_string())
|
||||
)));
|
||||
}
|
||||
let text = final_message
|
||||
.filter(|text| !text.trim().is_empty())
|
||||
@@ -622,18 +634,30 @@ fn parse_game_creator_codex_cli_response(
|
||||
let tool_calls = if request.function_tools.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
let envelope = serde_json::from_str::<serde_json::Value>(&text).map_err(|_| {
|
||||
platform_llm::LlmError::Deserialize(
|
||||
"Codex CLI Agent structured output 不是严格 JSON".to_string(),
|
||||
)
|
||||
let envelope = serde_json::from_str::<serde_json::Value>(&text).map_err(|error| {
|
||||
let detail = crate::agent::redact_agent_runtime_error(
|
||||
Path::new("__agc_no_project_root__"),
|
||||
&text,
|
||||
480,
|
||||
);
|
||||
platform_llm::LlmError::Deserialize(format!(
|
||||
"Codex CLI Agent structured output JSON 无效:{error};原文={detail}"
|
||||
))
|
||||
})?;
|
||||
let calls = envelope
|
||||
.get("toolCalls")
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.ok_or_else(|| {
|
||||
platform_llm::LlmError::Deserialize(
|
||||
"Codex CLI Agent structured output 缺少 toolCalls".to_string(),
|
||||
)
|
||||
let detail =
|
||||
serde_json::to_string(&envelope).unwrap_or_else(|_| "<不可序列化>".to_string());
|
||||
let detail = crate::agent::redact_agent_runtime_error(
|
||||
Path::new("__agc_no_project_root__"),
|
||||
&detail,
|
||||
480,
|
||||
);
|
||||
platform_llm::LlmError::Deserialize(format!(
|
||||
"Codex CLI Agent structured output 缺少 toolCalls;原文={detail}"
|
||||
))
|
||||
})?;
|
||||
calls
|
||||
.iter()
|
||||
@@ -643,17 +667,26 @@ fn parse_game_creator_codex_cli_response(
|
||||
.get("name")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.ok_or_else(|| {
|
||||
platform_llm::LlmError::Deserialize(
|
||||
"Codex CLI Agent tool call 缺少 name".to_string(),
|
||||
)
|
||||
let detail = serde_json::to_string(call)
|
||||
.unwrap_or_else(|_| "<不可序列化>".to_string());
|
||||
let detail = crate::agent::redact_agent_runtime_error(
|
||||
Path::new("__agc_no_project_root__"),
|
||||
&detail,
|
||||
480,
|
||||
);
|
||||
platform_llm::LlmError::Deserialize(format!(
|
||||
"Codex CLI Agent tool call 第 {} 项缺少 name;原文={detail}",
|
||||
index.saturating_add(1)
|
||||
))
|
||||
})?;
|
||||
let arguments = call
|
||||
.get("arguments")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.ok_or_else(|| {
|
||||
platform_llm::LlmError::Deserialize(
|
||||
"Codex CLI Agent tool call 缺少 arguments".to_string(),
|
||||
)
|
||||
platform_llm::LlmError::Deserialize(format!(
|
||||
"Codex CLI Agent tool call 第 {} 项缺少 arguments;name={name}",
|
||||
index.saturating_add(1)
|
||||
))
|
||||
})?;
|
||||
if !request.function_tools.iter().any(|tool| tool.name == name) {
|
||||
return Err(platform_llm::LlmError::Deserialize(
|
||||
@@ -1248,6 +1281,15 @@ mod tests {
|
||||
assert!(failed_text.contains("HTTP 502 upstream overloaded"));
|
||||
assert!(!failed_text.contains("fixture-secret"));
|
||||
|
||||
let failed_without_message = parse_game_creator_codex_cli_response(
|
||||
b"{\"type\":\"turn.failed\",\"code\":\"EPIPE\",\"status\":\"closed\"}\n",
|
||||
&request,
|
||||
)
|
||||
.expect_err("failed terminal without message must retain event detail");
|
||||
let failed_without_message = failed_without_message.to_string();
|
||||
assert!(failed_without_message.contains("EPIPE"));
|
||||
assert!(failed_without_message.contains("closed"));
|
||||
|
||||
let invalid = parse_game_creator_codex_cli_response(
|
||||
b"{not-json Authorization: Bearer fixture-secret}\n",
|
||||
&request,
|
||||
@@ -1259,6 +1301,35 @@ mod tests {
|
||||
assert!(!invalid_text.contains("fixture-secret"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_cli_mode_preserves_missing_terminal_and_structured_output_details() {
|
||||
let request = tool_request();
|
||||
let missing_terminal = parse_game_creator_codex_cli_response(
|
||||
b"{\"type\":\"item.completed\",\"item\":{\"type\":\"error\",\"message\":\"IPC EPIPE\"}}\n",
|
||||
&request,
|
||||
)
|
||||
.expect_err("missing terminal must retain the last protocol event");
|
||||
let missing_terminal = missing_terminal.to_string();
|
||||
assert!(missing_terminal.contains("缺少 turn.completed 终态"));
|
||||
assert!(missing_terminal.contains("IPC EPIPE"));
|
||||
|
||||
let malformed_event = serde_json::json!({
|
||||
"type": "item.completed",
|
||||
"item": { "type": "agent_message", "text": r#"{"toolCalls":["# }
|
||||
});
|
||||
let malformed_stdout = format!(
|
||||
"{}\n{{\"type\":\"turn.completed\"}}\n",
|
||||
serde_json::to_string(&malformed_event).expect("serialize malformed event")
|
||||
);
|
||||
let malformed_structured =
|
||||
parse_game_creator_codex_cli_response(malformed_stdout.as_bytes(), &request)
|
||||
.expect_err("malformed structured output must retain parse detail");
|
||||
let malformed_structured = malformed_structured.to_string();
|
||||
assert!(malformed_structured.contains("structured output JSON 无效"));
|
||||
assert!(malformed_structured.contains("line"));
|
||||
assert!(malformed_structured.contains("toolCalls"));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn codex_cli_mode_builds_ephemeral_json_exec_with_prompt_on_stdin() {
|
||||
|
||||
@@ -107,7 +107,7 @@ DirectProject 已经解决过同一类问题([`【ADR】DirectProject命令接
|
||||
- 脱敏只替换敏感值、完整 URL、绝对路径和私钥内容;字段名(例如 `Authorization`、`token`、`api_key`)保留,便于判断错误类别。原始凭据值不进入用户文本、应用日志或项目诊断。
|
||||
- 上游 app-server 映射保留 `message` / `additionalDetails` 的脱敏摘要;因此 401/403/408/429/413/5xx 等状态不会因为分类为 transport 或 native 而丢失正文。
|
||||
- Claude Code(cc)侧车的非零退出、RPC `error`、stdout JSON/UTF-8 解析失败、stderr 和静默超时也走同一映射;侧车 stderr 只在有界收口窗口内读取并进入同一脱敏 detail,不再只写裸 `eprintln!`。
|
||||
- Codex CLI / Claude sidecar 的 JSONL/JSON 解析失败和失败终态保留解析错误、失败事件正文与有界安全片段,不再把无效 JSON、`turn.failed` 或缺失终态统一压成单一类别。
|
||||
- Codex CLI / Claude sidecar 的 JSONL/JSON 解析失败、失败终态、缺失终态、超时和空回执都保留解析错误、失败事件正文与有界安全片段;`LlmError` 没有 detail 字段的超时/空回执由 Direct `ModelCallFailed.detail` 补回,不再把这些协议事实统一压成单一类别。
|
||||
- HTTP 409 只有明确包含泥点不足事实时才映射为 `paidCreditsInsufficient`;Claude Code 的普通 409 冲突保留为 `upstreamFailed`。
|
||||
- `hostDropped` 携带可选的脱敏 `detail`:panic hook 能取得的负载和位置随原回合占用进入失败事件;普通 Drop 仅记录“退出时未写终态、未观察到 panic”,不推断为网络中断。旧版本无 `detail` 的事件继续可读,界面明确标注旧事件未记录原因。
|
||||
- DirectProject 控制器的取消待发、终止、附件上传、项目历史读取和发送前置异常也复用同一份前端精确脱敏出口;这些非回合错误不得直接把 `Error.message` 原文写入状态栏、composer 或历史错误行。
|
||||
|
||||
@@ -17,7 +17,7 @@ Parent Milestone: `【里程碑】AGC错误具体文本展示-2026-10-04.md`
|
||||
8. 项目资源编辑/上传/预览/恢复队列/生成结果回读的 UI 错误统一使用 `normalizeDiagnosticText`,不再直出 `Error.message`。
|
||||
9. 应用壳、运行配置、认证状态、插件启动、运行预览和策划会话的非结构化错误统一使用 `visibleClientErrorMessage`。
|
||||
10. 资产导入、邀请码、策划工作区、发布封面/截图和素材命令的 UI 错误沿用同一共享出口。
|
||||
11. Codex CLI / Claude sidecar 的解析失败与失败终态保留有界脱敏正文,避免协议错误只剩类别码。
|
||||
11. Codex CLI / Claude sidecar 的解析失败、失败终态、缺失终态、超时和空回执保留有界脱敏正文;即使平台 `LlmError` 变体没有 detail 字段,也由统一 Direct `ModelCallFailed.detail` 继续携带原始原因。
|
||||
|
||||
## 实现顺序
|
||||
|
||||
|
||||
Reference in New Issue
Block a user