传播插件 IPC 和媒体解析错误
插件 stdout 读取错误送达挂起 RPC 保留 UTF-8 图片和账本 JSON 解析原因 同步错误传播约定文档
This commit is contained in:
@@ -1968,7 +1968,7 @@ fn read_validated_platform_art_reference_at(
|
||||
}
|
||||
let bytes = fs::read(&source_path).map_err(|error| format!("读取参考素材失败:{error}"))?;
|
||||
let decoded = image::load_from_memory(&bytes)
|
||||
.map_err(|_| format!("参考素材不是可解析图片:{}", source.local_path))?;
|
||||
.map_err(|error| format!("参考素材不是可解析图片:{}:{error}", source.local_path))?;
|
||||
Ok((bytes, decoded))
|
||||
}
|
||||
|
||||
|
||||
@@ -160,7 +160,7 @@ struct RunningPlugin {
|
||||
#[cfg(windows)]
|
||||
_job: crate::process_session::WindowsProcessJob,
|
||||
stdin: Arc<Mutex<ChildStdin>>,
|
||||
lines: Option<Receiver<String>>,
|
||||
lines: Option<Receiver<Result<String, String>>>,
|
||||
pending: PendingRpc,
|
||||
registrations: Arc<Mutex<PluginRegistrations>>,
|
||||
next_request_id: u64,
|
||||
@@ -627,9 +627,18 @@ fn spawn_plugin(manifest: &PluginManifest, root: &Path) -> Result<RunningPlugin,
|
||||
let (sender, receiver) = mpsc::sync_channel(16);
|
||||
thread::spawn(move || {
|
||||
let mut reader = BufReader::new(stdout);
|
||||
while let Ok(Some(line)) = read_bounded_rpc_line(&mut reader) {
|
||||
if sender.send(line).is_err() {
|
||||
break;
|
||||
loop {
|
||||
match read_bounded_rpc_line(&mut reader) {
|
||||
Ok(Some(line)) => {
|
||||
if sender.send(Ok(line)).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(None) => break,
|
||||
Err(error) => {
|
||||
let _ = sender.send(Err(error));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -652,7 +661,7 @@ fn read_bounded_rpc_line(reader: &mut impl BufRead) -> Result<Option<String>, St
|
||||
loop {
|
||||
let buffer = reader
|
||||
.fill_buf()
|
||||
.map_err(|_| "读取插件输出失败".to_string())?;
|
||||
.map_err(|error| format!("读取插件输出失败:{error}"))?;
|
||||
if buffer.is_empty() {
|
||||
return if bytes.is_empty() {
|
||||
Ok(None)
|
||||
@@ -670,7 +679,7 @@ fn read_bounded_rpc_line(reader: &mut impl BufRead) -> Result<Option<String>, St
|
||||
if newline.is_some() {
|
||||
return String::from_utf8(bytes)
|
||||
.map(Some)
|
||||
.map_err(|_| "插件输出不是 UTF-8".to_string());
|
||||
.map_err(|error| format!("插件输出不是 UTF-8:{error}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1437,10 +1446,18 @@ impl PluginHost {
|
||||
let manifest = record.manifest.clone();
|
||||
let root = root.to_path_buf();
|
||||
thread::spawn(move || {
|
||||
while let Ok(line) = lines.recv() {
|
||||
let mut terminal_error = None;
|
||||
while let Ok(line_result) = lines.recv() {
|
||||
if !active.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
let line = match line_result {
|
||||
Ok(line) => line,
|
||||
Err(error) => {
|
||||
terminal_error = Some(error);
|
||||
break;
|
||||
}
|
||||
};
|
||||
let Ok(envelope) = serde_json::from_str::<RpcEnvelope>(&line) else {
|
||||
continue;
|
||||
};
|
||||
@@ -1495,8 +1512,9 @@ impl PluginHost {
|
||||
}
|
||||
if let Ok(mut waiting) = pending.lock() {
|
||||
let remaining = std::mem::take(&mut *waiting);
|
||||
let terminal_error = terminal_error.unwrap_or_else(|| "插件进程已退出".to_string());
|
||||
for (_, sender) in remaining {
|
||||
let _ = sender.send(Err("插件进程已退出".to_string()));
|
||||
let _ = sender.send(Err(terminal_error.clone()));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1454,8 +1454,8 @@ fn resolve_resource_edit_source(
|
||||
RESOURCE_EDIT_TEXT_MAX_BYTES,
|
||||
"源文本资源",
|
||||
)?;
|
||||
let text =
|
||||
String::from_utf8(bytes).map_err(|_| "文本资源必须使用 UTF-8 编码".to_string())?;
|
||||
let text = String::from_utf8(bytes)
|
||||
.map_err(|error| format!("文本资源必须使用 UTF-8 编码:{error}"))?;
|
||||
return Ok(ResourceEditSourceSnapshot {
|
||||
canonical_resource_id,
|
||||
source_path: Some(path),
|
||||
@@ -1507,7 +1507,7 @@ fn resolve_resource_edit_source(
|
||||
let (source_width, source_height) =
|
||||
if input.edit_kind == LocalProjectResourceEditKind::CharacterAnimation {
|
||||
let decoded = image::load_from_memory(&bytes)
|
||||
.map_err(|_| "生成角色动画前无法解析源图片尺寸".to_string())?;
|
||||
.map_err(|error| format!("生成角色动画前无法解析源图片尺寸:{error}"))?;
|
||||
(Some(decoded.width()), Some(decoded.height()))
|
||||
} else {
|
||||
(None, None)
|
||||
@@ -2029,8 +2029,8 @@ async fn register_resource_edit_source_image(
|
||||
.bytes
|
||||
.as_deref()
|
||||
.ok_or_else(|| "登记源图片缺少文件内容".to_string())?;
|
||||
let decoded =
|
||||
image::load_from_memory(bytes).map_err(|_| "登记源图片前无法解析图片尺寸".to_string())?;
|
||||
let decoded = image::load_from_memory(bytes)
|
||||
.map_err(|error| format!("登记源图片前无法解析图片尺寸:{error}"))?;
|
||||
let endpoint = format!(
|
||||
"/api/external/v1/editor/projects/{}/resources",
|
||||
percent_encode_query_component(remote_project_id)
|
||||
@@ -2313,7 +2313,7 @@ async fn ensure_resource_edit_source_reference(
|
||||
.as_deref()
|
||||
.ok_or_else(|| "登记源图片缺少文件内容".to_string())?;
|
||||
let decoded = image::load_from_memory(bytes)
|
||||
.map_err(|_| "登记源图片前无法解析图片尺寸".to_string())?;
|
||||
.map_err(|error| format!("登记源图片前无法解析图片尺寸:{error}"))?;
|
||||
let resource_id = if let Some(resource_id) = ledger.source_remote_resource_id.clone() {
|
||||
resource_id
|
||||
} else {
|
||||
@@ -2755,7 +2755,7 @@ async fn submit_resource_edit_remote(
|
||||
.as_deref()
|
||||
.ok_or_else(|| "资源编辑账本缺少请求正文".to_string())?;
|
||||
let mut body_value = serde_json::from_str::<serde_json::Value>(body)
|
||||
.map_err(|_| "资源编辑账本请求正文无效".to_string())?;
|
||||
.map_err(|error| format!("资源编辑账本请求正文无效:{error}"))?;
|
||||
if editor_api_mode() == EditorApiMode::PlatformAccount {
|
||||
if let Some(inputs) = body_value.as_object_mut().and_then(|body| {
|
||||
body.entry("generationInputs")
|
||||
@@ -3165,7 +3165,7 @@ fn validate_downloaded_media(
|
||||
return Err("抠图结果必须是带透明通道的 PNG".to_string());
|
||||
}
|
||||
let decoded = image::load_from_memory_with_format(bytes, image::ImageFormat::Png)
|
||||
.map_err(|_| "抠图结果不是有效的 PNG".to_string())?;
|
||||
.map_err(|error| format!("抠图结果不是有效的 PNG:{error}"))?;
|
||||
if !decoded.color().has_alpha() {
|
||||
return Err("抠图结果缺少透明通道".to_string());
|
||||
}
|
||||
@@ -4745,7 +4745,7 @@ fn write_resource_edit_result_binding(
|
||||
| LocalProjectResourceEditKind::BackgroundRemoval
|
||||
) {
|
||||
let decoded = image::load_from_memory(&bytes)
|
||||
.map_err(|_| "派生图片 binding 无法解析尺寸".to_string())?;
|
||||
.map_err(|error| format!("派生图片 binding 无法解析尺寸:{error}"))?;
|
||||
(Some(decoded.width()), Some(decoded.height()))
|
||||
} else {
|
||||
(None, None)
|
||||
@@ -5878,7 +5878,7 @@ pub(crate) async fn derive_local_project_resource_typed(
|
||||
Err(error) => Err(error),
|
||||
Ok(bytes) => (|| {
|
||||
let content = std::str::from_utf8(&bytes)
|
||||
.map_err(|_| "派生文本不是 UTF-8".to_string())
|
||||
.map_err(|error| format!("派生文本不是 UTF-8:{error}"))
|
||||
.map_err(ResourceEditError::Other)?;
|
||||
let (media_type, extension) = validate_text_derivative(
|
||||
&input.edit_kind,
|
||||
|
||||
@@ -112,6 +112,7 @@ DirectProject 已经解决过同一类问题([`【ADR】DirectProject命令接
|
||||
- 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 正文;固定句只能作为事实前缀,底层原因必须接在后面。
|
||||
- 插件 stdout RPC 读取线程不得吞掉 `fill_buf`、UTF-8 或大小上限错误后伪装成“插件进程已退出”;读线程通过挂起 RPC 的错误通道传播真实原因。
|
||||
- 浏览器启动/DevTools 握手与 Codex model-catalog 子进程失败保留 stderr、退出状态、解析错误和阶段;稳定机器码仍用于分类,但不能单独成为用户可见正文。
|
||||
- Node/npm 环境探测和 Web scaffold 构建持续排空并保存有界 stdout/stderr 尾部,失败返回退出状态、超时阶段和脱敏正文;环境预检回执同时下发 `code` 与 `diagnostic`。
|
||||
- HTTP 409 只有明确包含泥点不足事实时才映射为 `paidCreditsInsufficient`;Claude Code 的普通 409 冲突保留为 `upstreamFailed`。
|
||||
|
||||
@@ -15,6 +15,7 @@ DirectProject 回合失败在确认不是客户端内部不可归类故障时,
|
||||
- Rust 侧错误文本脱敏:只替换敏感值、URL、绝对路径和私钥内容,保留 HTTP 状态、错误码、字段名和可行动描述。
|
||||
- app-server `codexErrorInfo` 只有嵌套机器字段时,保留经有界脱敏的结构化值;`error` 是字符串或未知 JSON 形状时也保留正文;`fields=codexErrorInfo` 不能成为唯一正文。
|
||||
- 素材/参考图/资源编辑上传、资源编辑轮询、模型目录初始化和 Direct MCP stdio 的网络、JSON、写入、读写失败保留底层错误正文;不能由 `map_err(|_| 固定句)` 把它们压成无因的连接或失败提示。
|
||||
- 插件 stdout RPC 的 `fill_buf`、UTF-8 和响应大小错误通过挂起 RPC 传播,不能在读取线程里吞掉后统一说“插件进程已退出”。
|
||||
- Transport / Stream / IPC / host process / memory exhaustion 的回归测试与错误事件证据。
|
||||
|
||||
## 不做
|
||||
|
||||
Reference in New Issue
Block a user