保留环境预检和 Web 构建错误正文
保留 Node npm stdout stderr 退出状态和超时原因 预检回执同时返回机器码与脱敏诊断 补充环境失败正文回归测试
This commit is contained in:
@@ -22,6 +22,77 @@ const SCHEMA: &str = "agc-node-runtime.v1";
|
||||
const PROBE_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const MAX_PROBE_BYTES: u64 = 4096;
|
||||
|
||||
/// 进程输出持续排空但只保留有界尾部;失败收口前精确脱敏,不能把 stderr 整份丢弃。
|
||||
#[derive(Default)]
|
||||
struct CapturedProcessOutput {
|
||||
bytes: Vec<u8>,
|
||||
total: usize,
|
||||
}
|
||||
|
||||
async fn capture_process_output(
|
||||
mut reader: impl tokio::io::AsyncRead + Unpin,
|
||||
output: &mut CapturedProcessOutput,
|
||||
limit: usize,
|
||||
) -> Result<(), std::io::Error> {
|
||||
let mut buffer = vec![0u8; 4096];
|
||||
loop {
|
||||
let count = reader.read(&mut buffer).await?;
|
||||
if count == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
output.total = output.total.saturating_add(count);
|
||||
output.bytes.extend_from_slice(&buffer[..count]);
|
||||
let excess = output.bytes.len().saturating_sub(limit);
|
||||
if excess > 0 {
|
||||
output.bytes.drain(..excess);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn environment_failure(code: &str, detail: impl std::fmt::Display) -> String {
|
||||
let detail = crate::agent::redact_agent_runtime_error(
|
||||
Path::new("__agc_no_project_root__"),
|
||||
&detail.to_string(),
|
||||
1800,
|
||||
);
|
||||
format!("{code}: {detail}")
|
||||
}
|
||||
|
||||
fn process_failure_output(
|
||||
code: &str,
|
||||
detail: impl std::fmt::Display,
|
||||
stdout: &CapturedProcessOutput,
|
||||
stderr: &CapturedProcessOutput,
|
||||
) -> String {
|
||||
// 优先保留 stderr(npm/Node 的具体故障通常在这里),stdout 留作构建报错的补充。
|
||||
environment_failure(
|
||||
code,
|
||||
format!(
|
||||
"{detail};stdoutBytes={};stderrBytes={};stderr={};stdout={}",
|
||||
stdout.total,
|
||||
stderr.total,
|
||||
String::from_utf8_lossy(&stderr.bytes),
|
||||
String::from_utf8_lossy(&stdout.bytes),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn blocked_environment_report(error: &str) -> Value {
|
||||
let candidate = error.split([':', ';']).next().unwrap_or_default();
|
||||
let code = if candidate.contains('-')
|
||||
&& candidate
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
|
||||
{
|
||||
candidate
|
||||
} else {
|
||||
"environment-check-unclassified"
|
||||
};
|
||||
let diagnostic =
|
||||
crate::agent::redact_agent_runtime_error(Path::new("__agc_no_project_root__"), error, 1800);
|
||||
json!({"status":"blocked", "code":code, "diagnostic":diagnostic})
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct NodeRuntime {
|
||||
pub node: PathBuf,
|
||||
@@ -357,11 +428,14 @@ fn valid_version(value: &str) -> bool {
|
||||
|
||||
async fn probe_version(runtime: &NodeRuntime, npm: bool) -> Result<String, String> {
|
||||
// npm 初始化也会查询用户目录;显式隔离,避免依赖 Windows 后备查询。
|
||||
let home = tempfile::tempdir().map_err(|_| "runtime-probe-home-unavailable")?;
|
||||
let home = tempfile::tempdir()
|
||||
.map_err(|error| environment_failure("runtime-probe-home-unavailable", error))?;
|
||||
let config = home.path().join("user.npmrc");
|
||||
let global_config = home.path().join("global.npmrc");
|
||||
fs::write(&config, b"").map_err(|_| "runtime-probe-home-unavailable")?;
|
||||
fs::write(&global_config, b"").map_err(|_| "runtime-probe-home-unavailable")?;
|
||||
fs::write(&config, b"")
|
||||
.map_err(|error| environment_failure("runtime-probe-home-unavailable", error))?;
|
||||
fs::write(&global_config, b"")
|
||||
.map_err(|error| environment_failure("runtime-probe-home-unavailable", error))?;
|
||||
let mut command = tokio::process::Command::new(&runtime.node);
|
||||
command
|
||||
.env_clear()
|
||||
@@ -377,7 +451,7 @@ async fn probe_version(runtime: &NodeRuntime, npm: bool) -> Result<String, Strin
|
||||
.env("npm_config_update_notifier", "false")
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
for name in ["SystemRoot", "WINDIR", "ComSpec", "TEMP", "TMP"] {
|
||||
if let Some(value) = std::env::var_os(name) {
|
||||
@@ -392,27 +466,40 @@ async fn probe_version(runtime: &NodeRuntime, npm: bool) -> Result<String, Strin
|
||||
crate::configure_windows_background_tokio_command(&mut command, true);
|
||||
let mut child = command
|
||||
.spawn()
|
||||
.map_err(|_| "runtime-version-start-failed")?;
|
||||
let mut stdout = child
|
||||
.stdout
|
||||
.take()
|
||||
.ok_or("runtime-version-output-missing")?
|
||||
.take(MAX_PROBE_BYTES + 1);
|
||||
let mut output = Vec::new();
|
||||
.map_err(|error| environment_failure("runtime-version-start-failed", error))?;
|
||||
let stdout = child.stdout.take().ok_or_else(|| {
|
||||
environment_failure("runtime-version-output-missing", "stdout 管道未建立")
|
||||
})?;
|
||||
let stderr = child.stderr.take().ok_or_else(|| {
|
||||
environment_failure("runtime-version-output-missing", "stderr 管道未建立")
|
||||
})?;
|
||||
let mut output = CapturedProcessOutput::default();
|
||||
let mut error_output = CapturedProcessOutput::default();
|
||||
let result = tokio::time::timeout(PROBE_TIMEOUT, async {
|
||||
stdout
|
||||
.read_to_end(&mut output)
|
||||
.await
|
||||
.map_err(|_| "runtime-version-output-failed")?;
|
||||
if output.len() as u64 > MAX_PROBE_BYTES {
|
||||
return Err("runtime-version-output-too-large");
|
||||
}
|
||||
let status = child
|
||||
.wait()
|
||||
.await
|
||||
.map_err(|_| "runtime-version-wait-failed")?;
|
||||
let (out, err, status) = tokio::join!(
|
||||
capture_process_output(stdout, &mut output, MAX_PROBE_BYTES as usize),
|
||||
capture_process_output(stderr, &mut error_output, MAX_PROBE_BYTES as usize),
|
||||
child.wait(),
|
||||
);
|
||||
out.map_err(|error| environment_failure("runtime-version-output-failed", error))?;
|
||||
err.map_err(|error| environment_failure("runtime-version-output-failed", error))?;
|
||||
let status =
|
||||
status.map_err(|error| environment_failure("runtime-version-wait-failed", error))?;
|
||||
if !status.success() {
|
||||
return Err("runtime-version-failed");
|
||||
return Err(process_failure_output(
|
||||
"runtime-version-failed",
|
||||
format!("exitStatus={status}"),
|
||||
&output,
|
||||
&error_output,
|
||||
));
|
||||
}
|
||||
if output.total as u64 > MAX_PROBE_BYTES {
|
||||
return Err(process_failure_output(
|
||||
"runtime-version-output-too-large",
|
||||
format!("版本输出超过 {MAX_PROBE_BYTES} 字节上限"),
|
||||
&output,
|
||||
&error_output,
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
@@ -421,15 +508,23 @@ async fn probe_version(runtime: &NodeRuntime, npm: bool) -> Result<String, Strin
|
||||
let _ = child.kill().await;
|
||||
let _ = child.wait().await;
|
||||
}
|
||||
result
|
||||
.map_err(|_| "runtime-version-timeout")?
|
||||
.map_err(str::to_string)?;
|
||||
let version = String::from_utf8(output)
|
||||
.map_err(|_| "runtime-version-invalid")?
|
||||
result.map_err(|_| {
|
||||
process_failure_output(
|
||||
"runtime-version-timeout",
|
||||
format!("版本探测超过 {} ms", PROBE_TIMEOUT.as_millis()),
|
||||
&output,
|
||||
&error_output,
|
||||
)
|
||||
})??;
|
||||
let version = String::from_utf8(output.bytes)
|
||||
.map_err(|error| environment_failure("runtime-version-invalid", error))?
|
||||
.trim()
|
||||
.to_string();
|
||||
if !valid_version(&version) {
|
||||
return Err("runtime-version-invalid".into());
|
||||
return Err(environment_failure(
|
||||
"runtime-version-invalid",
|
||||
format!("预期三段数字版本,实际输出={version}"),
|
||||
));
|
||||
}
|
||||
Ok(version)
|
||||
}
|
||||
@@ -443,11 +538,16 @@ pub(crate) async fn check_environment(root: &Path) -> Value {
|
||||
let result = tokio::task::spawn_blocking(move || resolve_node_runtime(&root)).await;
|
||||
let runtime = match result {
|
||||
Ok(Ok(runtime)) => runtime,
|
||||
Ok(Err(code)) => {
|
||||
return json!({"status":"blocked","code":code,"elapsedMs":started.elapsed().as_millis()})
|
||||
Ok(Err(error)) => {
|
||||
let mut blocked = blocked_environment_report(&error);
|
||||
blocked["elapsedMs"] = json!(started.elapsed().as_millis());
|
||||
return blocked;
|
||||
}
|
||||
Err(_) => {
|
||||
return json!({"status":"blocked","code":"runtime-check-failed","elapsedMs":started.elapsed().as_millis()})
|
||||
Err(error) => {
|
||||
return blocked_environment_report(&environment_failure(
|
||||
"runtime-check-failed",
|
||||
error,
|
||||
));
|
||||
}
|
||||
};
|
||||
let (node, npm) = tokio::join!(
|
||||
@@ -458,8 +558,10 @@ pub(crate) async fn check_environment(root: &Path) -> Value {
|
||||
(Ok(node), Ok(npm)) => {
|
||||
json!({"status":"ready","source":runtime.source,"nodeVersion":node,"npmVersion":npm,"elapsedMs":started.elapsed().as_millis()})
|
||||
}
|
||||
(Err(code), _) | (_, Err(code)) => {
|
||||
json!({"status":"blocked","code":code,"elapsedMs":started.elapsed().as_millis()})
|
||||
(Err(error), _) | (_, Err(error)) => {
|
||||
let mut blocked = blocked_environment_report(&error);
|
||||
blocked["elapsedMs"] = json!(started.elapsed().as_millis());
|
||||
blocked
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -469,8 +571,10 @@ pub(crate) async fn check_environment(root: &Path) -> Value {
|
||||
Ok(browser) => {
|
||||
json!({"status":"ready","kind":browser.kind,"product":browser.product,"protocolVersion":browser.protocol_version,"elapsedMs":started.elapsed().as_millis()})
|
||||
}
|
||||
Err(code) => {
|
||||
json!({"status":"blocked","code":code,"elapsedMs":started.elapsed().as_millis()})
|
||||
Err(error) => {
|
||||
let mut blocked = blocked_environment_report(&error);
|
||||
blocked["elapsedMs"] = json!(started.elapsed().as_millis());
|
||||
blocked
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -623,6 +727,28 @@ mod tests {
|
||||
assert!(!valid_version("v24.0.0\nSECRET"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_probe_failure_keeps_bounded_redacted_process_detail() {
|
||||
let stdout = CapturedProcessOutput {
|
||||
bytes: b"npm version probe".to_vec(),
|
||||
total: 18,
|
||||
};
|
||||
let stderr = CapturedProcessOutput {
|
||||
bytes: b"EACCES: permission denied; Authorization: Bearer private-token; C:\\Users\\private\\npmrc".to_vec(),
|
||||
total: 91,
|
||||
};
|
||||
let detail = process_failure_output(
|
||||
"runtime-version-failed",
|
||||
"exitStatus=code:1",
|
||||
&stdout,
|
||||
&stderr,
|
||||
);
|
||||
assert!(detail.contains("runtime-version-failed"), "{detail}");
|
||||
assert!(detail.contains("EACCES"), "{detail}");
|
||||
assert!(!detail.contains("private-token"), "{detail}");
|
||||
assert!(!detail.contains("C:\\Users\\private"), "{detail}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagnostic_path_removes_runtime_and_project_entries_but_keeps_other_tools() {
|
||||
let project = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -179,24 +179,6 @@ pub(crate) fn record_new_web_scaffold_at(root: &Path, project_id: &str) -> Resul
|
||||
write_receipt(root, &receipt)
|
||||
}
|
||||
|
||||
async fn drain_output<R: tokio::io::AsyncRead + Unpin>(
|
||||
mut reader: R,
|
||||
) -> Result<(), std::io::Error> {
|
||||
// 保留输出大小界限;正文不进入预检回执,也不因管道满而阻塞子进程。
|
||||
let mut total = 0usize;
|
||||
let mut buffer = [0u8; 4096];
|
||||
loop {
|
||||
let count = reader.read(&mut buffer).await?;
|
||||
if count == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
total = total.saturating_add(count);
|
||||
if total > MAX_COMMAND_OUTPUT {
|
||||
total = MAX_COMMAND_OUTPUT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn host_npm(
|
||||
runtime: &NodeRuntime,
|
||||
cwd: &Path,
|
||||
@@ -205,11 +187,14 @@ async fn host_npm(
|
||||
online: bool,
|
||||
owner: Option<(&Path, &PreparationOwner)>,
|
||||
) -> Result<(), String> {
|
||||
let home = tempfile::tempdir().map_err(|_| "web-build-home-unavailable")?;
|
||||
let home = tempfile::tempdir()
|
||||
.map_err(|error| environment_failure("web-build-home-unavailable", error))?;
|
||||
let config = home.path().join("user.npmrc");
|
||||
let global_config = home.path().join("global.npmrc");
|
||||
fs::write(&config, b"").map_err(|_| "web-build-home-unavailable")?;
|
||||
fs::write(&global_config, b"").map_err(|_| "web-build-home-unavailable")?;
|
||||
fs::write(&config, b"")
|
||||
.map_err(|error| environment_failure("web-build-home-unavailable", error))?;
|
||||
fs::write(&global_config, b"")
|
||||
.map_err(|error| environment_failure("web-build-home-unavailable", error))?;
|
||||
let mut execution = NpmExecution::new();
|
||||
if let Some((root, owner)) = owner {
|
||||
record_execution(root, owner, Some(execution.clone()))?;
|
||||
@@ -251,9 +236,11 @@ async fn host_npm(
|
||||
use std::os::unix::process::CommandExt;
|
||||
command.as_std_mut().process_group(0);
|
||||
}
|
||||
let mut child = command.spawn().map_err(|_| "web-build-start-failed")?;
|
||||
let tree =
|
||||
OwnedNpmTree::assign(&child, &execution).map_err(|_| "web-build-process-guard-failed")?;
|
||||
let mut child = command
|
||||
.spawn()
|
||||
.map_err(|error| environment_failure("web-build-start-failed", error))?;
|
||||
let tree = OwnedNpmTree::assign(&child, &execution)
|
||||
.map_err(|error| environment_failure("web-build-process-guard-failed", error))?;
|
||||
execution.process = Some(ProcessIdentity::capture(
|
||||
child.id().ok_or("web-build-process-identity-missing")?,
|
||||
)?);
|
||||
@@ -265,17 +252,28 @@ async fn host_npm(
|
||||
stdin
|
||||
.write_all(b"GO\n")
|
||||
.await
|
||||
.map_err(|_| "web-build-gate-failed")?;
|
||||
.map_err(|error| environment_failure("web-build-gate-failed", error))?;
|
||||
drop(stdin);
|
||||
let stdout = child.stdout.take().ok_or("web-build-output-missing")?;
|
||||
let stderr = child.stderr.take().ok_or("web-build-output-missing")?;
|
||||
let mut output = CapturedProcessOutput::default();
|
||||
let mut error_output = CapturedProcessOutput::default();
|
||||
let result = tokio::time::timeout(timeout, async {
|
||||
let (out, err, status) =
|
||||
tokio::join!(drain_output(stdout), drain_output(stderr), child.wait());
|
||||
out.map_err(|_| "web-build-output-failed")?;
|
||||
err.map_err(|_| "web-build-output-failed")?;
|
||||
if !status.map_err(|_| "web-build-wait-failed")?.success() {
|
||||
return Err("web-build-command-failed");
|
||||
let (out, err, status) = tokio::join!(
|
||||
capture_process_output(stdout, &mut output, MAX_COMMAND_OUTPUT),
|
||||
capture_process_output(stderr, &mut error_output, MAX_COMMAND_OUTPUT),
|
||||
child.wait()
|
||||
);
|
||||
out.map_err(|error| environment_failure("web-build-output-failed", error))?;
|
||||
err.map_err(|error| environment_failure("web-build-output-failed", error))?;
|
||||
let status = status.map_err(|error| environment_failure("web-build-wait-failed", error))?;
|
||||
if !status.success() {
|
||||
return Err(process_failure_output(
|
||||
"web-build-command-failed",
|
||||
format!("exitStatus={status}"),
|
||||
&output,
|
||||
&error_output,
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
@@ -285,10 +283,19 @@ async fn host_npm(
|
||||
let _ = child.kill().await;
|
||||
let _ = child.wait().await;
|
||||
}
|
||||
tree.terminate()
|
||||
.map_err(|_| "web-build-subtree-not-reaped")?;
|
||||
tree.terminate().map_err(|error| {
|
||||
environment_failure(
|
||||
"web-build-subtree-not-reaped",
|
||||
format!("{error};原执行错误={result:?}"),
|
||||
)
|
||||
})?;
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
while !tree.empty().map_err(|_| "web-build-subtree-not-reaped")? {
|
||||
while !tree.empty().map_err(|error| {
|
||||
environment_failure(
|
||||
"web-build-subtree-not-reaped",
|
||||
format!("{error};原执行错误={result:?}"),
|
||||
)
|
||||
})? {
|
||||
if Instant::now() >= deadline {
|
||||
return Err("web-build-subtree-not-reaped".into());
|
||||
}
|
||||
@@ -297,9 +304,14 @@ async fn host_npm(
|
||||
if let Some((root, owner)) = owner {
|
||||
record_execution(root, owner, None)?;
|
||||
}
|
||||
result
|
||||
.map_err(|_| "web-build-timeout")?
|
||||
.map_err(str::to_string)
|
||||
result.map_err(|_| {
|
||||
process_failure_output(
|
||||
"web-build-timeout",
|
||||
format!("npm 执行超过 {} ms", timeout.as_millis()),
|
||||
&output,
|
||||
&error_output,
|
||||
)
|
||||
})?
|
||||
}
|
||||
|
||||
/// 浏览器验证失败的宿主错误码。
|
||||
@@ -349,11 +361,10 @@ fn browser_validation_failure_code(error: &str) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
fn browser_validation_diagnostic(error: &str) -> Option<&str> {
|
||||
(error.starts_with("browser-recovery-")
|
||||
|| error.starts_with("browser-launch-failed:")
|
||||
|| error.starts_with("browser-cleanup-unconfirmed:"))
|
||||
.then_some(error)
|
||||
fn browser_validation_diagnostic(error: &str) -> Option<String> {
|
||||
let diagnostic =
|
||||
crate::agent::redact_agent_runtime_error(Path::new("__agc_no_project_root__"), error, 1800);
|
||||
(!diagnostic.is_empty()).then_some(diagnostic)
|
||||
}
|
||||
|
||||
pub(crate) async fn host_web_creation_preflight() -> Value {
|
||||
@@ -415,19 +426,8 @@ pub(crate) async fn host_web_creation_preflight() -> Value {
|
||||
}
|
||||
.await;
|
||||
let mut result = run.unwrap_or_else(|error| {
|
||||
let (code, diagnostic) = error
|
||||
.split_once(";diagnostic=")
|
||||
.map_or((error.as_str(), None), |(code, diagnostic)| {
|
||||
(code, Some(diagnostic))
|
||||
});
|
||||
let mut blocked = json!({
|
||||
"schemaVersion": "agc-web-creation-preflight.v1",
|
||||
"status": "blocked",
|
||||
"code": code,
|
||||
});
|
||||
if let Some(diagnostic) = diagnostic {
|
||||
blocked["diagnostic"] = json!(diagnostic);
|
||||
}
|
||||
let mut blocked = blocked_environment_report(&error);
|
||||
blocked["schemaVersion"] = json!("agc-web-creation-preflight.v1");
|
||||
blocked
|
||||
});
|
||||
result["elapsedMs"] = json!(started.elapsed().as_millis());
|
||||
@@ -623,12 +623,12 @@ mod tests {
|
||||
"web-preflight-browser-recovery-failed"
|
||||
);
|
||||
assert_eq!(
|
||||
browser_validation_diagnostic(recovery_diagnostic),
|
||||
browser_validation_diagnostic(recovery_diagnostic).as_deref(),
|
||||
Some(recovery_diagnostic)
|
||||
);
|
||||
assert_eq!(
|
||||
browser_validation_diagnostic("启动浏览器失败:原始错误"),
|
||||
None
|
||||
browser_validation_diagnostic("启动浏览器失败:原始错误").as_deref(),
|
||||
Some("启动浏览器失败:原始错误")
|
||||
);
|
||||
assert_eq!(
|
||||
browser_validation_failure_code("宿主已停止本轮浏览器验证"),
|
||||
@@ -640,7 +640,7 @@ mod tests {
|
||||
"web-preflight-browser-cleanup-failed"
|
||||
);
|
||||
assert_eq!(
|
||||
browser_validation_diagnostic(cleanup_diagnostic),
|
||||
browser_validation_diagnostic(cleanup_diagnostic).as_deref(),
|
||||
Some(cleanup_diagnostic)
|
||||
);
|
||||
assert_eq!(
|
||||
|
||||
@@ -110,6 +110,7 @@ DirectProject 已经解决过同一类问题([`【ADR】DirectProject命令接
|
||||
- Codex CLI / Claude sidecar 的 JSONL/JSON 解析失败、失败终态、缺失终态、超时和空回执都保留解析错误、失败事件正文与有界安全片段;`LlmError` 没有 detail 字段的超时/空回执由 Direct `ModelCallFailed.detail` 补回,不再把这些协议事实统一压成单一类别。
|
||||
- 账户、模型目录、External Editor/资源编辑、发布、素材上传、错误报告与客户端受控工具桥共用同一原则:网络错误保留底层因链,JSON/协议解析保留 serde 原因,HTTP 错误保留状态码与安全正文;只有响应完全没有正文时才说明“未提供 error/code/message”,不得退成“无法连接/格式无效/服务器未返回错误信息”。
|
||||
- 浏览器启动/DevTools 握手与 Codex model-catalog 子进程失败保留 stderr、退出状态、解析错误和阶段;稳定机器码仍用于分类,但不能单独成为用户可见正文。
|
||||
- Node/npm 环境探测和 Web scaffold 构建持续排空并保存有界 stdout/stderr 尾部,失败返回退出状态、超时阶段和脱敏正文;环境预检回执同时下发 `code` 与 `diagnostic`。
|
||||
- HTTP 409 只有明确包含泥点不足事实时才映射为 `paidCreditsInsufficient`;Claude Code 的普通 409 冲突保留为 `upstreamFailed`。
|
||||
- `hostDropped` 携带可选的脱敏 `detail`:panic hook 能取得的负载和位置随原回合占用进入失败事件;普通 Drop 仅记录“退出时未写终态、未观察到 panic”,不推断为网络中断。旧版本无 `detail` 的事件继续可读,界面明确标注旧事件未记录原因。
|
||||
- DirectProject 控制器的取消待发、终止、附件上传、项目历史读取和发送前置异常也复用同一份前端精确脱敏出口;这些非回合错误不得直接把 `Error.message` 原文写入状态栏、composer 或历史错误行。
|
||||
|
||||
@@ -20,6 +20,7 @@ Parent Milestone: `【里程碑】AGC错误具体文本展示-2026-10-04.md`
|
||||
11. Codex CLI / Claude sidecar 的解析失败、失败终态、缺失终态、超时和空回执保留有界脱敏正文;即使平台 `LlmError` 变体没有 detail 字段,也由统一 Direct `ModelCallFailed.detail` 继续携带原始原因。
|
||||
12. 扩展统一正文口径到账户、模型目录、External Editor/资源编辑、发布、素材上传、错误报告与客户端受控工具桥,保留 HTTP 状态、响应安全片段、JSON 解析原因、网络因链和 IPC/桥接原因。
|
||||
13. 浏览器启动/DevTools 握手与 Codex model-catalog 子进程错误保留 stderr、退出状态、解析原因和阶段信息,机器码只作为分类字段。
|
||||
14. Node/npm 探测与 Web scaffold 构建失败保留有界脱敏 stdout/stderr、退出状态和超时原因;`preflight_web_game_creation` 不再只返回机器码。
|
||||
|
||||
## 实现顺序
|
||||
|
||||
|
||||
Reference in New Issue
Block a user