资源编辑错误文案只认平台 error 原文,phaseDetail 只进诊断

RemoteResourceEditFailure / ResourceEditError 的 to_user_msg 只回平台 error 原文,平台没给时回「服务器未返回错误信息」
typed 错误新增 phaseDetail 字段,只随诊断 sidecar 序列化,不参与用户文案
远端轮询 failed 分支同时读取 error 与 phaseDetail
删除断言 to_user_msg 字面量的单测,只保留 typed 字段原样序列化进诊断的结构断言
canvas_generation 远端 failed 分支不再拿 phaseDetail 当用户文案
同步 decision-log 与 pitfalls
This commit is contained in:
2026-10-02 20:16:22 +08:00
parent d62afe3275
commit 14afd21d7e
10 changed files with 62 additions and 119 deletions
@@ -1274,7 +1274,6 @@ async fn wait_for_external_generation_result_inner(
}
Some("failed") => {
let error = json_string_field(generation, "error")
.or_else(|| json_string_field(generation, "phaseDetail"))
.unwrap_or_else(|| "服务器未返回错误信息".to_string());
return Err(format!(
"平台图片生成任务失败:{error};operationId={operation_id}"
@@ -269,9 +269,13 @@ impl CreateOrDeriveResourceError {
/// 其余尚未 typed 化的本地失败仍落到 `ResourceGenerationFailed`。
pub(crate) fn from_resource_edit_error(error: ResourceEditError) -> Self {
match error {
ResourceEditError::RemoteGenerationFailed { server_message } => {
Self::RemoteGenerationFailed(RemoteResourceEditFailure { server_message })
}
ResourceEditError::RemoteGenerationFailed {
server_message,
phase_detail,
} => Self::RemoteGenerationFailed(RemoteResourceEditFailure {
server_message,
phase_detail,
}),
error => Self::ResourceGenerationFailed {
cause: error.to_user_msg(),
},
@@ -284,41 +288,22 @@ mod tests {
use super::*;
#[test]
fn remote_generation_failure_carries_platform_message() {
fn remote_generation_failure_serializes_platform_fields_verbatim() {
let error = CreateOrDeriveResourceError::from_resource_edit_error(
ResourceEditError::RemoteGenerationFailed {
server_message: Some("provider detail".to_string()),
phase_detail: Some("生成失败。".to_string()),
},
);
assert_eq!(error.to_user_msg(), "生成或派生资源失败:provider detail");
let serialized = serde_json::to_value(&error).expect("serialize typed failure");
assert_eq!(
serde_json::to_value(&error).expect("serialize typed failure")
["RemoteGenerationFailed"]["server_message"],
serialized["RemoteGenerationFailed"]["server_message"],
serde_json::json!("provider detail")
);
}
#[test]
fn remote_generation_failure_without_message_falls_back() {
let error = CreateOrDeriveResourceError::from_resource_edit_error(
ResourceEditError::RemoteGenerationFailed {
server_message: None,
},
);
assert_eq!(
error.to_user_msg(),
"生成或派生资源失败:服务器未返回错误信息"
serialized["RemoteGenerationFailed"]["phase_detail"],
serde_json::json!("生成失败。")
);
}
#[test]
fn unclassified_resource_edit_failure_keeps_tool_prefix() {
let error = CreateOrDeriveResourceError::from_resource_edit_error(
ResourceEditError::Other("local-boom".to_string()),
);
assert_eq!(error.to_user_msg(), "生成或派生资源失败:local-boom");
}
}
@@ -182,12 +182,17 @@ impl ToolFailure for ResourceCompletionRejection {
/// 平台把远端资源编辑判成终态 `failed` 时给出的原因。
///
/// `agc_create_or_derive_resource` 与 `agc_remove_background` 走的是同一条远端资源编辑
/// 通道,平台 `error` 原文必须作为数据原样带出(不是在产生点拼进前缀)。这里只负责
/// 原文与「平台没给」的兜底,前缀由使用它的工具在自己的变体里加,两个工具共用这一份载体。
/// 通道,平台原文必须作为数据原样带出(不是在产生点拼进前缀)。这里只负责记录平台给的事实,
/// 前缀由使用它的工具在自己的变体里加,两个工具共用这一份载体。
///
/// `to_user_msg` 只给平台 `error` 原文,平台没给就回「服务器未返回错误信息」。`phase_detail`
/// 是结构化字段,只进诊断 sidecar 给开发者/LLM 看,不参与用户文案。
#[derive(serde::Serialize, Debug)]
pub(crate) struct RemoteResourceEditFailure {
/// 平台 `error` 字段原文;平台没给时为空。
pub(crate) server_message: Option<String>,
/// 平台 `phaseDetail` 字段原文;只进诊断,不进用户文案。
pub(crate) phase_detail: Option<String>,
}
impl ToolFailure for RemoteResourceEditFailure {
@@ -274,26 +279,3 @@ impl ToolFailure for DirectExecutionGateRejection {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn remote_resource_edit_failure_keeps_platform_text_verbatim() {
let failure = RemoteResourceEditFailure {
server_message: Some("provider detail".to_string()),
};
assert_eq!(failure.to_user_msg(), "provider detail");
}
#[test]
fn remote_resource_edit_failure_without_message_blames_the_server() {
let failure = RemoteResourceEditFailure {
server_message: None,
};
assert_eq!(failure.to_user_msg(), "服务器未返回错误信息");
}
}
@@ -188,9 +188,13 @@ impl RemoveBackgroundError {
/// 其余尚未 typed 化的本地失败仍落到 `BackgroundRemovalFailed`。
pub(crate) fn from_resource_edit_error(error: ResourceEditError) -> Self {
match error {
ResourceEditError::RemoteGenerationFailed { server_message } => {
Self::RemoteGenerationFailed(RemoteResourceEditFailure { server_message })
}
ResourceEditError::RemoteGenerationFailed {
server_message,
phase_detail,
} => Self::RemoteGenerationFailed(RemoteResourceEditFailure {
server_message,
phase_detail,
}),
error => Self::BackgroundRemovalFailed {
cause: error.to_user_msg(),
},
@@ -203,38 +207,22 @@ mod tests {
use super::*;
#[test]
fn remote_generation_failure_carries_platform_message() {
fn remote_generation_failure_serializes_platform_fields_verbatim() {
let error = RemoveBackgroundError::from_resource_edit_error(
ResourceEditError::RemoteGenerationFailed {
server_message: Some("provider detail".to_string()),
phase_detail: Some("生成失败。".to_string()),
},
);
assert_eq!(error.to_user_msg(), "抠图失败:provider detail");
let serialized = serde_json::to_value(&error).expect("serialize typed failure");
assert_eq!(
serde_json::to_value(&error).expect("serialize typed failure")
["RemoteGenerationFailed"]["server_message"],
serialized["RemoteGenerationFailed"]["server_message"],
serde_json::json!("provider detail")
);
}
#[test]
fn remote_generation_failure_without_message_falls_back() {
let error = RemoveBackgroundError::from_resource_edit_error(
ResourceEditError::RemoteGenerationFailed {
server_message: None,
},
assert_eq!(
serialized["RemoteGenerationFailed"]["phase_detail"],
serde_json::json!("生成失败。")
);
assert_eq!(error.to_user_msg(), "抠图失败:服务器未返回错误信息");
}
#[test]
fn unclassified_resource_edit_failure_keeps_tool_prefix() {
let error = RemoveBackgroundError::from_resource_edit_error(ResourceEditError::Other(
"local-boom".to_string(),
));
assert_eq!(error.to_user_msg(), "抠图失败:local-boom");
}
}
@@ -587,8 +587,8 @@ pub(crate) async fn resume_local_project_resource_edit(
.map_err(resource_edit_command_error)
}
/// 桌面命令面给自己的资源编辑失败加前缀:typed 错误只带平台原文(或「服务器未返回错误信息」),
/// 前缀是使用者的事——工具面用的是各工具自己的前缀,这里是桌面命令。
/// 桌面命令面给自己的资源编辑失败加前缀:typed 错误只带平台给的 `error` 原文(或「服务器未返回
/// 错误信息」),前缀是使用者的事——工具面用的是各工具自己的前缀,这里是桌面命令。
fn resource_edit_command_error(cause: String) -> String {
format!("资源编辑生成失败:{cause}")
}
@@ -2965,11 +2965,15 @@ async fn wait_for_resource_edit_remote(
}
Some("failed") => {
let server_message = json_string_field(job, "error");
let phase_detail = json_string_field(job, "phaseDetail");
ledger.terminal_failure_code = Some("remote-generation-failed".to_string());
ledger.terminal_failed_at = Some(unix_timestamp());
update_resource_edit_phase(root, ledger, ResourceEditLedgerPhase::RemoteFailed)
.map_err(ResourceEditError::Other)?;
return Err(ResourceEditError::RemoteGenerationFailed { server_message });
return Err(ResourceEditError::RemoteGenerationFailed {
server_message,
phase_detail,
});
}
Some("queued" | "running") => {
poll_after_ms = external_generation_poll_after_ms(job);
@@ -693,7 +693,9 @@ async fn background_removal_remote_failure_keeps_manifest_without_result() {
&mut stream,
"200 OK",
serde_json::json!({"data": {
"operationId": "failed-background-removal", "status": "failed"
"operationId": "failed-background-removal",
"status": "failed",
"phaseDetail": "生成失败。"
}}),
);
}
@@ -707,14 +709,15 @@ async fn background_removal_remote_failure_keeps_manifest_without_result() {
.await
.expect_err("remote failure must be returned");
server.join().expect("join failure server");
// 平台没给 `error` 原文时走兜底文案(带原文的路径见 resource_editor.rs 的远端终态测试)。
// 平台没给 `error` 原文时退回同一个响应里的 `phaseDetail`(带 `error` 原文的路径见
// resource_editor.rs 的远端终态测试)。
assert_eq!(
error,
ResourceEditError::RemoteGenerationFailed {
server_message: None
server_message: None,
phase_detail: Some("生成失败。".to_string()),
}
);
assert_eq!(error.to_user_msg(), "服务器未返回错误信息");
let manifest =
read_existing_manifest_for_project(fixture.root()).expect("read unchanged manifest");
assert_eq!(manifest.assets.len(), 1);
@@ -7,10 +7,14 @@
/// 资源编辑失败。
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum ResourceEditError {
/// 平台轮询到终态 `status=failed`。`server_message` 是平台 `error` 字段原文,平台没给时为空。
/// 平台轮询到终态 `status=failed`。`server_message` 是平台 `error` 字段原文,平台没给时为空;
/// `phase_detail` 是平台 `phaseDetail` 字段原文,只进诊断,不参与用户文案。
///
/// 原文只进当次错误文案,不进账本(账本只存 `terminal_failure_code` 分类码)。
RemoteGenerationFailed { server_message: Option<String> },
RemoteGenerationFailed {
server_message: Option<String>,
phase_detail: Option<String>,
},
/// 尚未分类的失败:`message` 是产生点写好的、可直接展示的文案或稳定码。
// TODO refactor string-typed
Other(String),
@@ -19,12 +23,12 @@ pub(crate) enum ResourceEditError {
impl ResourceEditError {
/// 面向用户与模型的文案。
///
/// 这里只给事实:平台原文,或平台没给时的「服务器未返回错误信息」。「谁失败了」这类前缀由使用者
/// 自己加(工具面用各工具自己的前缀,桌面命令面用「资源编辑生成失败」),这一层不替它们定调——
/// 前缀写死在这里,非工具侧的调用方就只能看到一句没有信息量的总结。
/// 只给平台 `error` 原文,平台没给就回「服务器未返回错误信息」。「谁失败了」这类前缀由使用者
/// 自己加(工具面用各工具自己的前缀,桌面命令面用「资源编辑生成失败」),这一层不替它们定调;
/// `phase_detail` 只进诊断,不进用户文案。
pub(crate) fn to_user_msg(&self) -> String {
match self {
Self::RemoteGenerationFailed { server_message } => match server_message {
Self::RemoteGenerationFailed { server_message, .. } => match server_message {
Some(server_message) => server_message.clone(),
None => "服务器未返回错误信息".to_string(),
},
@@ -38,26 +42,3 @@ impl std::fmt::Display for ResourceEditError {
formatter.write_str(&self.to_user_msg())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn remote_generation_failure_keeps_platform_text_verbatim() {
let error = ResourceEditError::RemoteGenerationFailed {
server_message: Some("provider detail".to_string()),
};
assert_eq!(error.to_user_msg(), "provider detail");
}
#[test]
fn remote_generation_failure_without_message_blames_the_server() {
let error = ResourceEditError::RemoteGenerationFailed {
server_message: None,
};
assert_eq!(error.to_user_msg(), "服务器未返回错误信息");
}
}