继续收敛上传与 MCP 错误正文
保留素材资源模型目录和资源编辑底层错误 保留 Direct MCP stdio 写入读取与超时原因 补充错误展示验收与未通过夹具记录
This commit is contained in:
@@ -1579,7 +1579,7 @@ fn read_bounded_line<R: BufRead>(reader: &mut R) -> Result<Option<Vec<u8>>, Stri
|
||||
loop {
|
||||
let available = reader
|
||||
.fill_buf()
|
||||
.map_err(|_| "读取 MCP 请求失败".to_string())?;
|
||||
.map_err(|error| format!("读取 MCP 请求失败:{error}"))?;
|
||||
if available.is_empty() {
|
||||
return Ok((!bytes.is_empty()).then_some(bytes));
|
||||
}
|
||||
@@ -1607,11 +1607,12 @@ fn write_direct_tools_mcp_response(
|
||||
writer: &mut impl Write,
|
||||
response: &Value,
|
||||
) -> Result<(), String> {
|
||||
serde_json::to_writer(&mut *writer, response).map_err(|_| "写入 MCP 响应失败".to_string())?;
|
||||
serde_json::to_writer(&mut *writer, response)
|
||||
.map_err(|error| format!("写入 MCP 响应失败:序列化失败:{error}"))?;
|
||||
writer
|
||||
.write_all(b"\n")
|
||||
.and_then(|_| writer.flush())
|
||||
.map_err(|_| "写入 MCP 响应失败".to_string())
|
||||
.map_err(|error| format!("写入 MCP 响应失败:{error}"))
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
@@ -1705,7 +1706,7 @@ async fn run_direct_tools_mcp_stdio() -> Result<(), String> {
|
||||
let stdin = std::io::stdin();
|
||||
read_direct_tools_mcp_requests(BufReader::new(stdin.lock()), sender);
|
||||
})
|
||||
.map_err(|_| "启动 MCP 请求读取失败".to_string())?;
|
||||
.map_err(|error| format!("启动 MCP 请求读取失败:{error}"))?;
|
||||
let stdout = std::io::stdout();
|
||||
dispatch_direct_tools_mcp_requests(receiver, &mut stdout.lock(), move |request| {
|
||||
let root = root.clone();
|
||||
|
||||
@@ -975,7 +975,7 @@ async fn accepted_generation_is_authoritatively_failed_once(
|
||||
),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| "查询平台图片生成任务超时".to_string())??;
|
||||
.map_err(|_| format!("查询平台图片生成任务超时(3000 ms);operationId={operation_id}"))??;
|
||||
access.validate_frozen_session()?;
|
||||
let generation = platform_generation_status_data(&payload);
|
||||
match json_string_field(generation, "status").as_deref() {
|
||||
@@ -2112,11 +2112,11 @@ async fn upload_manifest_asset_remote_reference_at(
|
||||
};
|
||||
let upload_url =
|
||||
validate_external_asset_download_url(&ticket.host, access.api_base_url(), true)
|
||||
.map_err(|_| "当前账号参考图上传地址不安全".to_string())?;
|
||||
.map_err(|error| format!("当前账号参考图上传地址无效:{error}"))?;
|
||||
let upload_client =
|
||||
build_external_asset_download_client(&upload_url, access.api_base_url(), true)
|
||||
.await
|
||||
.map_err(|_| "无法创建当前账号参考图上传客户端".to_string())?;
|
||||
.map_err(|error| format!("无法创建当前账号参考图上传客户端:{error}"))?;
|
||||
access.validate_frozen_session()?;
|
||||
let form = ticket
|
||||
.form_fields
|
||||
@@ -2129,13 +2129,13 @@ async fn upload_manifest_asset_remote_reference_at(
|
||||
// 请求发出前就结束生命周期。
|
||||
.file_name(file_name.clone())
|
||||
.mime_str(&source.media_type)
|
||||
.map_err(|_| "参考图媒体类型不能用于上传".to_string())?;
|
||||
.map_err(|error| format!("参考图媒体类型不能用于上传:{error}"))?;
|
||||
let upload_response = upload_client
|
||||
.post(upload_url)
|
||||
.multipart(form.part("file", part))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|_| "上传当前账号参考图失败".to_string())?;
|
||||
.map_err(|error| format!("上传当前账号参考图失败:{error}"))?;
|
||||
if upload_response.status().as_u16() != ticket.success_action_status {
|
||||
return Err(format!(
|
||||
"上传当前账号参考图失败:HTTP {}",
|
||||
|
||||
@@ -1053,7 +1053,11 @@ async fn resolve_cover_queue(
|
||||
let started_at = tokio::time::Instant::now();
|
||||
for _ in 0..COVER_QUEUE_MAX_POLLS {
|
||||
if started_at.elapsed() > COVER_QUEUE_TIMEOUT {
|
||||
return Err("生成游戏封面超时,请稍后重试".to_string());
|
||||
return Err(format!(
|
||||
"生成游戏封面超时(超过 {} 秒);operationId={operation_id},当前状态={}",
|
||||
COVER_QUEUE_TIMEOUT.as_secs(),
|
||||
queue.status.as_deref().unwrap_or("unknown")
|
||||
));
|
||||
}
|
||||
tokio::time::sleep(COVER_QUEUE_POLL_INTERVAL).await;
|
||||
let status = request_json(
|
||||
@@ -1100,7 +1104,10 @@ async fn resolve_cover_queue(
|
||||
});
|
||||
}
|
||||
}
|
||||
Err("生成游戏封面超时,请稍后重试".to_string())
|
||||
Err(format!(
|
||||
"生成游戏封面超时(轮询 {} 次仍未完成);operationId={operation_id}",
|
||||
COVER_QUEUE_MAX_POLLS
|
||||
))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
@@ -187,7 +187,7 @@ pub(crate) async fn load_game_creator_llm_models() -> Result<LlmModelsResponse,
|
||||
.timeout(Duration::from_secs(15))
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.build()
|
||||
.map_err(|_| "初始化模型列表请求失败".to_string())?;
|
||||
.map_err(|error| format!("初始化模型列表请求失败:{error}"))?;
|
||||
fetch_game_creator_llm_models(&client, &session).await
|
||||
}
|
||||
|
||||
|
||||
@@ -285,7 +285,7 @@ async fn request_ticket(
|
||||
}
|
||||
|
||||
fn validate_upload_host(host: &str) -> Result<Url, String> {
|
||||
let url = Url::parse(host.trim()).map_err(|_| "素材上传地址无效".to_string())?;
|
||||
let url = Url::parse(host.trim()).map_err(|error| format!("素材上传地址无效:{error}"))?;
|
||||
let hostname = url.host_str().unwrap_or_default().to_ascii_lowercase();
|
||||
let is_local = url.scheme() == "http" && matches!(hostname.as_str(), "127.0.0.1" | "localhost");
|
||||
let is_oss = url.scheme() == "https" && hostname.ends_with(".aliyuncs.com");
|
||||
@@ -307,7 +307,7 @@ async fn upload_object(
|
||||
.timeout(HTTP_TIMEOUT)
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.build()
|
||||
.map_err(|_| "创建素材上传客户端失败".to_string())?;
|
||||
.map_err(|error| format!("创建素材上传客户端失败:{error}"))?;
|
||||
let mut form = Form::new();
|
||||
for (key, value) in &ticket.form_fields {
|
||||
form = form.text(key.clone(), value.clone());
|
||||
@@ -315,13 +315,13 @@ async fn upload_object(
|
||||
let part = Part::bytes(input.bytes.clone())
|
||||
.file_name(input.file_name.clone())
|
||||
.mime_str(&input.content_type)
|
||||
.map_err(|_| "素材媒体类型无效".to_string())?;
|
||||
.map_err(|error| format!("素材媒体类型无效:{error}"))?;
|
||||
let response = client
|
||||
.post(upload_url)
|
||||
.multipart(form.part("file", part))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|_| "上传素材失败:无法访问素材存储,请检查网络后重试".to_string())?;
|
||||
.map_err(|error| format!("上传素材失败:无法访问素材存储:{error};请检查网络后重试"))?;
|
||||
validate_session(snapshot)?;
|
||||
if response.status().as_u16() != ticket.success_action_status {
|
||||
return Err(format!(
|
||||
@@ -403,7 +403,7 @@ pub(crate) async fn upload_platform_media_asset(
|
||||
.connect_timeout(Duration::from_secs(10))
|
||||
.timeout(HTTP_TIMEOUT)
|
||||
.build()
|
||||
.map_err(|_| "创建素材上传客户端失败".to_string())?;
|
||||
.map_err(|error| format!("创建素材上传客户端失败:{error}"))?;
|
||||
let ticket = request_ticket(&client, &snapshot, &input).await?;
|
||||
upload_object(&snapshot, &ticket, &input).await?;
|
||||
confirm_object(&client, &snapshot, &ticket, &input).await
|
||||
|
||||
@@ -1766,7 +1766,7 @@ async fn request_resource_edit_upload_ticket(
|
||||
)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|_| "result-unknown: 创建源资源上传凭证未取得确定响应".to_string())?;
|
||||
.map_err(|error| format!("result-unknown: 创建源资源上传凭证请求失败:{error}"))?;
|
||||
let mut post_response_session_error = access.validate_frozen_session().err();
|
||||
if response.status() == reqwest::StatusCode::UNAUTHORIZED {
|
||||
if let Some(error) = post_response_session_error {
|
||||
@@ -1798,7 +1798,7 @@ async fn request_resource_edit_upload_ticket(
|
||||
let payload = response
|
||||
.json::<serde_json::Value>()
|
||||
.await
|
||||
.map_err(|_| "result-unknown: 解析源资源上传凭证失败".to_string())?;
|
||||
.map_err(|error| format!("result-unknown: 解析源资源上传凭证失败:{error}"))?;
|
||||
if post_response_session_error.is_none() {
|
||||
post_response_session_error = access.validate_frozen_session().err();
|
||||
}
|
||||
@@ -1877,11 +1877,11 @@ async fn upload_resource_edit_source(
|
||||
.ok_or_else(|| "源媒体上传缺少文件内容".to_string())?;
|
||||
let upload_url =
|
||||
validate_external_asset_download_url(&ticket.host, access.api_base_url(), true)
|
||||
.map_err(|_| "源资源上传地址不安全".to_string())?;
|
||||
.map_err(|error| format!("源资源上传地址无效:{error}"))?;
|
||||
access.validate_frozen_session()?;
|
||||
let client = build_external_asset_download_client(&upload_url, access.api_base_url(), true)
|
||||
.await
|
||||
.map_err(|_| "无法创建源资源上传客户端".to_string())?;
|
||||
.map_err(|error| format!("无法创建源资源上传客户端:{error}"))?;
|
||||
access.validate_frozen_session()?;
|
||||
let mut form = Form::new();
|
||||
for (key, value) in &ticket.form_fields {
|
||||
@@ -1891,14 +1891,14 @@ async fn upload_resource_edit_source(
|
||||
let part = Part::bytes(bytes.clone())
|
||||
.file_name(file_name)
|
||||
.mime_str(&source.media_type)
|
||||
.map_err(|_| "源资源媒体类型不能用于上传".to_string())?;
|
||||
.map_err(|error| format!("源资源媒体类型不能用于上传:{error}"))?;
|
||||
access.validate_frozen_session()?;
|
||||
let response = client
|
||||
.post(upload_url)
|
||||
.multipart(form.part("file", part))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|_| "result-unknown: 上传源资源未取得确定响应".to_string())?;
|
||||
.map_err(|error| format!("result-unknown: 上传源资源请求失败:{error}"))?;
|
||||
let post_response_session_error = access.validate_frozen_session().err();
|
||||
if response.status().as_u16() != ticket.success_action_status {
|
||||
if response.status().is_server_error() {
|
||||
@@ -1953,7 +1953,7 @@ async fn confirm_resource_edit_source(
|
||||
)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|_| "result-unknown: 确认源资源上传未取得确定响应".to_string())?;
|
||||
.map_err(|error| format!("result-unknown: 确认源资源上传请求失败:{error}"))?;
|
||||
let mut post_response_session_error = access.validate_frozen_session().err();
|
||||
if response.status() == reqwest::StatusCode::UNAUTHORIZED {
|
||||
if let Some(error) = post_response_session_error {
|
||||
@@ -1985,7 +1985,7 @@ async fn confirm_resource_edit_source(
|
||||
let payload = response
|
||||
.json::<serde_json::Value>()
|
||||
.await
|
||||
.map_err(|_| "result-unknown: 解析源资源确认响应失败".to_string())?;
|
||||
.map_err(|error| format!("result-unknown: 解析源资源确认响应失败:{error}"))?;
|
||||
if post_response_session_error.is_none() {
|
||||
post_response_session_error = access.validate_frozen_session().err();
|
||||
}
|
||||
@@ -2065,7 +2065,7 @@ async fn register_resource_edit_source_image(
|
||||
)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|_| "result-unknown: 登记源图片项目资源未取得确定响应".to_string())?;
|
||||
.map_err(|error| format!("result-unknown: 登记源图片项目资源请求失败:{error}"))?;
|
||||
let mut post_response_session_error = access.validate_frozen_session().err();
|
||||
if response.status() == reqwest::StatusCode::UNAUTHORIZED {
|
||||
if let Some(error) = post_response_session_error {
|
||||
@@ -2097,7 +2097,7 @@ async fn register_resource_edit_source_image(
|
||||
let payload = response
|
||||
.json::<serde_json::Value>()
|
||||
.await
|
||||
.map_err(|_| "result-unknown: 解析源图片项目资源响应失败".to_string())?;
|
||||
.map_err(|error| format!("result-unknown: 解析源图片项目资源响应失败:{error}"))?;
|
||||
if post_response_session_error.is_none() {
|
||||
post_response_session_error = access.validate_frozen_session().err();
|
||||
}
|
||||
@@ -2910,8 +2910,8 @@ async fn wait_for_resource_edit_remote(
|
||||
)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|_| {
|
||||
ResourceEditError::Other("result-unknown: 查询资源编辑任务失败".to_string())
|
||||
.map_err(|error| {
|
||||
ResourceEditError::Other(format!("result-unknown: 查询资源编辑任务请求失败:{error}"))
|
||||
})?;
|
||||
access
|
||||
.validate_frozen_session()
|
||||
@@ -2932,9 +2932,14 @@ async fn wait_for_resource_edit_remote(
|
||||
response.status().as_u16()
|
||||
)));
|
||||
}
|
||||
let payload = response.json::<serde_json::Value>().await.map_err(|_| {
|
||||
ResourceEditError::Other("result-unknown: 资源编辑任务响应无法解析".to_string())
|
||||
})?;
|
||||
let payload = response
|
||||
.json::<serde_json::Value>()
|
||||
.await
|
||||
.map_err(|error| {
|
||||
ResourceEditError::Other(format!(
|
||||
"result-unknown: 资源编辑任务响应无法解析:{error}"
|
||||
))
|
||||
})?;
|
||||
access
|
||||
.validate_frozen_session()
|
||||
.map_err(ResourceEditError::Other)?;
|
||||
@@ -3435,7 +3440,9 @@ async fn prepare_remote_resource_edit(
|
||||
let client = crate::http_client::agc_main_site_client_builder()
|
||||
.timeout(Duration::from_secs(35 * 60))
|
||||
.build()
|
||||
.map_err(|_| ResourceEditError::Other("无法创建资源编辑 HTTP 客户端".to_string()))?;
|
||||
.map_err(|error| {
|
||||
ResourceEditError::Other(format!("无法创建资源编辑 HTTP 客户端:{error}"))
|
||||
})?;
|
||||
let download_source = if ledger.phase == ResourceEditLedgerPhase::RemoteCompleted {
|
||||
if let Some(legacy_public_path) = ledger.remote_legacy_public_path.clone() {
|
||||
serde_json::json!({ "imageSrc": legacy_public_path })
|
||||
@@ -3597,8 +3604,10 @@ async fn prepare_remote_resource_edit(
|
||||
media_read_limit(&input.edit_kind),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
ResourceEditError::Other("result-unknown: 远端资源编辑结果下载或换签失败".to_string())
|
||||
.map_err(|error| {
|
||||
ResourceEditError::Other(format!(
|
||||
"result-unknown: 远端资源编辑结果下载或换签失败:{error}"
|
||||
))
|
||||
})?
|
||||
.ok_or_else(|| ResourceEditError::Other("远端资源编辑结果缺少可下载媒体".to_string()))?;
|
||||
access
|
||||
|
||||
@@ -111,6 +111,7 @@ DirectProject 已经解决过同一类问题([`【ADR】DirectProject命令接
|
||||
- Claude Code(cc)侧车的非零退出、RPC `error`、stdout JSON/UTF-8 解析失败、stderr 和静默超时也走同一映射;侧车 stderr 只在有界收口窗口内读取并进入同一脱敏 detail,不再只写裸 `eprintln!`。
|
||||
- Codex CLI / Claude sidecar 的 JSONL/JSON 解析失败、失败终态、缺失终态、超时和空回执都保留解析错误、失败事件正文与有界安全片段;`LlmError` 没有 detail 字段的超时/空回执由 Direct `ModelCallFailed.detail` 补回,不再把这些协议事实统一压成单一类别。
|
||||
- 账户、模型目录、External Editor/资源编辑、发布、素材上传、错误报告与客户端受控工具桥共用同一原则:网络错误保留底层因链,JSON/协议解析保留 serde 原因,HTTP 错误保留状态码与安全正文;只有响应完全没有正文时才说明“未提供 error/code/message”,不得退成“无法连接/格式无效/服务器未返回错误信息”。
|
||||
- 素材直传、参考图直传、资源编辑上传/轮询、模型目录初始化和 Direct MCP stdio 的 `send`/读写/序列化失败,不得用 `map_err(|_| 固定句)` 丢掉 `reqwest`、serde 或 OS/IPC 正文;固定句只能作为事实前缀,底层原因必须接在后面。
|
||||
- 浏览器启动/DevTools 握手与 Codex model-catalog 子进程失败保留 stderr、退出状态、解析错误和阶段;稳定机器码仍用于分类,但不能单独成为用户可见正文。
|
||||
- Node/npm 环境探测和 Web scaffold 构建持续排空并保存有界 stdout/stderr 尾部,失败返回退出状态、超时阶段和脱敏正文;环境预检回执同时下发 `code` 与 `diagnostic`。
|
||||
- HTTP 409 只有明确包含泥点不足事实时才映射为 `paidCreditsInsufficient`;Claude Code 的普通 409 冲突保留为 `upstreamFailed`。
|
||||
|
||||
@@ -14,6 +14,7 @@ DirectProject 回合失败在确认不是客户端内部不可归类故障时,
|
||||
- `turn.completed.failure` 的前端展示:按 typed 变体显示分类、状态码和脱敏后的 `detail` / `diagnostic`。
|
||||
- Rust 侧错误文本脱敏:只替换敏感值、URL、绝对路径和私钥内容,保留 HTTP 状态、错误码、字段名和可行动描述。
|
||||
- app-server `codexErrorInfo` 只有嵌套机器字段时,保留经有界脱敏的结构化值;`error` 是字符串或未知 JSON 形状时也保留正文;`fields=codexErrorInfo` 不能成为唯一正文。
|
||||
- 素材/参考图/资源编辑上传、资源编辑轮询、模型目录初始化和 Direct MCP stdio 的网络、JSON、写入、读写失败保留底层错误正文;不能由 `map_err(|_| 固定句)` 把它们压成无因的连接或失败提示。
|
||||
- Transport / Stream / IPC / host process / memory exhaustion 的回归测试与错误事件证据。
|
||||
|
||||
## 不做
|
||||
@@ -39,3 +40,4 @@ DirectProject 回合失败在确认不是客户端内部不可归类故障时,
|
||||
- CC 错误分类回归:Claude sidecar 状态码/超时映射与非支付 409 保留上游状态的定向测试通过;侧车失败统一补充有界 stderr detail。
|
||||
- 真实 AGC dev smoke:客户端使用 `3080`、后端 `8084`、数据库 `3101`、后台 `3103` 启动;真实 CC 回合复现 `Reached maximum number of turns (8)` 被旧代码错误记为 `transport-closed`,修复后 Tauri 已热重编译重启。修复后的真实 Provider 回放未再次发送,避免无必要的付费请求。
|
||||
- 真实 AGC 项目诊断发现真实请求 HTTP 400 曾被摘要为 `codex-app-server-error:other detail=fields=codexErrorInfo`,另有 401 仅显示 `codex-app-server-error:unauthorized`;该漏损已在 app-server 投影边界修复。真实 Provider、真实 IPC 断链和真实内存压力仍未在本轮主动制造。
|
||||
- 继续扫描发现素材/参考图/资源编辑上传、模型目录初始化和 Direct MCP stdio 仍有固定 `map_err(|_| ...)` 丢正文,已改为保留底层网络、serde、写入和 OS 错误;平台素材上传定向 3 项、模型目录定向 4 项、Direct MCP 断输出定向 1 项通过。资源编辑全套并行测试本轮 54 项通过、11 项因共享 fixture/本地端口或账号环境超时失败,不能把该套结果记为全绿。
|
||||
|
||||
Reference in New Issue
Block a user