资源编辑远端失败原文透传到工具错误与诊断
Project CI / Backend tests (pull_request) Failing after 29s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m48s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 3m54s
Project CI / Repository checks (pull_request) Failing after 15s
Project CI / Frontend tests (pull_request) Successful in 2m50s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 5m28s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m55s
Project CI / Native shell tests (pull_request) Successful in 6m48s
Project CI / Backend tests (pull_request) Failing after 29s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m48s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 3m54s
Project CI / Repository checks (pull_request) Failing after 15s
Project CI / Frontend tests (pull_request) Successful in 2m50s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 5m28s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m55s
Project CI / Native shell tests (pull_request) Successful in 6m48s
- agent/tool/error.rs 新增共用载体 RemoteResourceEditFailure { serverMessage },两个工具的平台失败文案只写一份
- CreateOrDeriveResourceError 与 RemoveBackgroundError 各加 RemoteGenerationFailed(RemoteResourceEditFailure) 变体,远端终态失败不再压回字符串
- 两个工具各加显式 from_resource_edit_error 翻译,不用 impl From<ResourceEditError>;Other 仍落回各工具原有的「失败:<文案>」变体并留 TODO
- 桥里四个资源编辑调用点改用 typed 入口,并 Box::pin 后再 await:资源编辑 future 内联会顶穿 handle_direct_tool_bridge 状态机的调试测试线程栈
- assets.rs 新增 with_direct_editor_api_credentials_as,保留调用方 error 类型,凭据解析失败由调用方显式翻译成 ResourceEditError::Other,旧名与其余调用点零改动
- 工具错误模块补单测:平台原文进 message、进序列化的 typed 错误,无原文与未分类失败各自的文案
- decision-log 补工具层承载、凭据作用域错误类型、future 装箱三条决策与真实验证结果;pitfalls 补工具层原样透传口径
This commit is contained in:
@@ -39,6 +39,7 @@ use crate::agent::tool::run_validation::error::RunValidationError;
|
||||
use crate::agent::tool::update_plan::error::UpdatePlanError;
|
||||
use crate::agent::tool::web_search::error::WebSearchError;
|
||||
use crate::agent::tool::write_file::error::WriteFileError;
|
||||
use crate::project::ResourceEditError;
|
||||
#[cfg(test)]
|
||||
use axum::extract::Query;
|
||||
use axum::extract::{DefaultBodyLimit, State};
|
||||
@@ -2272,15 +2273,18 @@ async fn bridge_create_or_derive_resource(
|
||||
return Err(CreateOrDeriveResourceError::DuplicatePendingOperations);
|
||||
}
|
||||
let completed = if let Some(pending) = matching_pending.into_iter().next() {
|
||||
with_direct_editor_api_credentials(resume_local_project_resource_edit_at(
|
||||
ResumeLocalProjectResourceEditInput {
|
||||
// 资源编辑的 future 很大;装箱后只留一个指针进 `handle_direct_tool_bridge` 的状态机,
|
||||
// 避免把调试测试线程的默认栈顶穿。
|
||||
Box::pin(with_direct_editor_api_credentials_as(
|
||||
resume_local_project_resource_edit_typed(ResumeLocalProjectResourceEditInput {
|
||||
project_path: state.root.to_string_lossy().into_owned(),
|
||||
expected_project_id: manifest.project_id,
|
||||
operation_id: pending.operation_id,
|
||||
},
|
||||
}),
|
||||
ResourceEditError::Other,
|
||||
))
|
||||
.await
|
||||
.map_err(|cause| CreateOrDeriveResourceError::ResourceGenerationFailed { cause })?
|
||||
.map_err(CreateOrDeriveResourceError::from_resource_edit_error)?
|
||||
} else {
|
||||
let turn_id = state
|
||||
.active_resource_turn_id()
|
||||
@@ -2318,9 +2322,14 @@ async fn bridge_create_or_derive_resource(
|
||||
background_mode: None,
|
||||
screen_color: None,
|
||||
};
|
||||
with_direct_editor_api_credentials(derive_local_project_resource_at(request))
|
||||
.await
|
||||
.map_err(|cause| CreateOrDeriveResourceError::ResourceGenerationFailed { cause })?
|
||||
// 资源编辑的 future 很大;装箱后只留一个指针进 `handle_direct_tool_bridge` 的状态机,
|
||||
// 避免把调试测试线程的默认栈顶穿。
|
||||
Box::pin(with_direct_editor_api_credentials_as(
|
||||
derive_local_project_resource_typed(request),
|
||||
ResourceEditError::Other,
|
||||
))
|
||||
.await
|
||||
.map_err(CreateOrDeriveResourceError::from_resource_edit_error)?
|
||||
};
|
||||
let result = bridge_completed_resource_result(
|
||||
&state.root,
|
||||
@@ -2559,13 +2568,18 @@ async fn remove_background_payload(
|
||||
return Err(RemoveBackgroundError::DuplicatePendingOperations);
|
||||
}
|
||||
let completed = if let Some(pending) = matching_pending.into_iter().next() {
|
||||
resume_local_project_resource_edit_at(ResumeLocalProjectResourceEditInput {
|
||||
project_path: state.root.to_string_lossy().into_owned(),
|
||||
expected_project_id: manifest.project_id.clone(),
|
||||
operation_id: pending.operation_id,
|
||||
})
|
||||
// 资源编辑的 future 很大;装箱后只留一个指针进 `handle_direct_tool_bridge` 的状态机,
|
||||
// 避免把调试测试线程的默认栈顶穿。
|
||||
Box::pin(with_direct_editor_api_credentials_as(
|
||||
resume_local_project_resource_edit_typed(ResumeLocalProjectResourceEditInput {
|
||||
project_path: state.root.to_string_lossy().into_owned(),
|
||||
expected_project_id: manifest.project_id.clone(),
|
||||
operation_id: pending.operation_id,
|
||||
}),
|
||||
ResourceEditError::Other,
|
||||
))
|
||||
.await
|
||||
.map_err(|cause| RemoveBackgroundError::BackgroundRemovalFailed { cause })?
|
||||
.map_err(RemoveBackgroundError::from_resource_edit_error)?
|
||||
} else {
|
||||
// id 按回合身份与请求指纹确定性派生,同指纹重试与 pending 对账语义不变。
|
||||
let turn_id = state
|
||||
@@ -2596,9 +2610,14 @@ async fn remove_background_payload(
|
||||
background_mode: Some(input.background_mode.clone()),
|
||||
screen_color: input.screen_color.clone(),
|
||||
};
|
||||
derive_local_project_resource_at(request)
|
||||
.await
|
||||
.map_err(|cause| RemoveBackgroundError::BackgroundRemovalFailed { cause })?
|
||||
// 资源编辑的 future 很大;装箱后只留一个指针进 `handle_direct_tool_bridge` 的状态机,
|
||||
// 避免把调试测试线程的默认栈顶穿。
|
||||
Box::pin(with_direct_editor_api_credentials_as(
|
||||
derive_local_project_resource_typed(request),
|
||||
ResourceEditError::Other,
|
||||
))
|
||||
.await
|
||||
.map_err(RemoveBackgroundError::from_resource_edit_error)?
|
||||
};
|
||||
emit_game_creator_manifest_invalidated(&state.root, "direct-background-removal");
|
||||
bridge_completed_resource_result(&state.root, "background-removal", "derive", completed)
|
||||
|
||||
+63
-2
@@ -4,9 +4,10 @@
|
||||
|
||||
use crate::agent::direct_tool_bridge::DirectResourceGenerationKind;
|
||||
use crate::agent::tool::error::{
|
||||
ProjectManifestRejection, ProjectPermissionRejection, ResourceCompletionRejection,
|
||||
ToolArgumentsRejection, ToolFailure, UNREGISTERED_SOURCE_HINT,
|
||||
ProjectManifestRejection, ProjectPermissionRejection, RemoteResourceEditFailure,
|
||||
ResourceCompletionRejection, ToolArgumentsRejection, ToolFailure, UNREGISTERED_SOURCE_HINT,
|
||||
};
|
||||
use crate::project::ResourceEditError;
|
||||
|
||||
/// `agc_create_or_derive_resource` 的每一种失败各占一个变体;文案写在这里,捕获处只调
|
||||
/// `to_user_msg`。
|
||||
@@ -94,6 +95,9 @@ pub(crate) enum CreateOrDeriveResourceError {
|
||||
ProjectRevisionUnavailable {
|
||||
cause: String,
|
||||
},
|
||||
RemoteGenerationFailed(RemoteResourceEditFailure),
|
||||
/// 尚未分类的本地失败(对应 `ResourceEditError::Other`)。
|
||||
// TODO refactor string-typed
|
||||
ResourceGenerationFailed {
|
||||
cause: String,
|
||||
},
|
||||
@@ -250,9 +254,66 @@ impl ToolFailure for CreateOrDeriveResourceError {
|
||||
Self::ProjectRevisionUnavailable { cause } => {
|
||||
format!("生成或派生资源失败:读取项目 revision 失败:{cause}")
|
||||
}
|
||||
Self::RemoteGenerationFailed(failure) => failure.to_user_msg(),
|
||||
Self::ResourceGenerationFailed { cause } => {
|
||||
format!("生成或派生资源失败:{cause}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CreateOrDeriveResourceError {
|
||||
/// 把资源编辑层的 typed 失败翻成这个工具自己的失败:远端终态失败保留平台原文,
|
||||
/// 其余尚未 typed 化的本地失败仍落到 `ResourceGenerationFailed`。
|
||||
pub(crate) fn from_resource_edit_error(error: ResourceEditError) -> Self {
|
||||
match error {
|
||||
ResourceEditError::RemoteGenerationFailed { server_message } => {
|
||||
Self::RemoteGenerationFailed(RemoteResourceEditFailure { server_message })
|
||||
}
|
||||
error => Self::ResourceGenerationFailed {
|
||||
cause: error.to_user_msg(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn remote_generation_failure_carries_platform_message() {
|
||||
let error = CreateOrDeriveResourceError::from_resource_edit_error(
|
||||
ResourceEditError::RemoteGenerationFailed {
|
||||
server_message: Some("provider detail".to_string()),
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(error.to_user_msg(), "资源编辑生成失败:provider detail");
|
||||
assert_eq!(
|
||||
serde_json::to_value(&error).expect("serialize typed failure")
|
||||
["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(), "资源编辑生成失败");
|
||||
}
|
||||
|
||||
#[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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +179,26 @@ impl ToolFailure for ResourceCompletionRejection {
|
||||
}
|
||||
}
|
||||
|
||||
/// 平台把远端资源编辑判成终态 `failed` 时给出的原因。
|
||||
///
|
||||
/// `agc_create_or_derive_resource` 与 `agc_remove_background` 走的是同一条远端资源编辑
|
||||
/// 通道,平台 `error` 原文必须作为数据原样带出(不是在产生点拼进前缀),两个工具共用
|
||||
/// 这一层文案。
|
||||
#[derive(serde::Serialize, Debug)]
|
||||
pub(crate) struct RemoteResourceEditFailure {
|
||||
/// 平台 `error` 字段原文;平台没给时为空。
|
||||
pub(crate) server_message: Option<String>,
|
||||
}
|
||||
|
||||
impl ToolFailure for RemoteResourceEditFailure {
|
||||
fn to_user_msg(&self) -> String {
|
||||
match &self.server_message {
|
||||
Some(server_message) => format!("资源编辑生成失败:{server_message}"),
|
||||
None => "资源编辑生成失败".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 只有客户端 Direct 回合才会放行的工具被独立客户端 MCP 调用。
|
||||
///
|
||||
/// 独立客户端 MCP 没有 Direct 回合的执行账本,不能借用其它回合的预算,所以这里直接拒绝,
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
//! 这里只放错误本身;参数解析与工具执行留在 `direct_tool_bridge`。
|
||||
|
||||
use crate::agent::tool::error::{
|
||||
ProjectManifestRejection, ProjectPermissionRejection, ResourceCompletionRejection,
|
||||
ToolArgumentsRejection, ToolFailure, UNREGISTERED_SOURCE_HINT,
|
||||
ProjectManifestRejection, ProjectPermissionRejection, RemoteResourceEditFailure,
|
||||
ResourceCompletionRejection, ToolArgumentsRejection, ToolFailure, UNREGISTERED_SOURCE_HINT,
|
||||
};
|
||||
use crate::project::ResourceEditError;
|
||||
|
||||
/// `agc_remove_background` 的每一种失败各占一个变体;文案写在这里,捕获处只调
|
||||
/// `to_user_msg`。
|
||||
@@ -16,28 +17,65 @@ pub(crate) enum RemoveBackgroundError {
|
||||
Manifest(ProjectManifestRejection),
|
||||
Completion(ResourceCompletionRejection),
|
||||
SourceLocalAssetIdEmpty,
|
||||
SourceLocalAssetIdTooLong { got_chars: usize, max_chars: usize },
|
||||
SourceLocalAssetIdTooLong {
|
||||
got_chars: usize,
|
||||
max_chars: usize,
|
||||
},
|
||||
SourceLocalAssetIdHasControlCharacters,
|
||||
SourceLocalAssetIdNotText { got: String },
|
||||
SourceLocalAssetIdNotText {
|
||||
got: String,
|
||||
},
|
||||
AssetNameEmpty,
|
||||
AssetNameTooLong { got_chars: usize, max_chars: usize },
|
||||
AssetNameTooLong {
|
||||
got_chars: usize,
|
||||
max_chars: usize,
|
||||
},
|
||||
AssetNameHasControlCharacters,
|
||||
AssetNameNotText { got: String },
|
||||
BackgroundModeNotText { got: String },
|
||||
BackgroundModeUnsupported { got: String },
|
||||
ScreenColorNotText { got: String },
|
||||
ScreenColorMalformed { got: String },
|
||||
AssetNameNotText {
|
||||
got: String,
|
||||
},
|
||||
BackgroundModeNotText {
|
||||
got: String,
|
||||
},
|
||||
BackgroundModeUnsupported {
|
||||
got: String,
|
||||
},
|
||||
ScreenColorNotText {
|
||||
got: String,
|
||||
},
|
||||
ScreenColorMalformed {
|
||||
got: String,
|
||||
},
|
||||
ScreenColorRequiresFlatMode,
|
||||
SourceAssetNotRegistered,
|
||||
SourceNotImage { media_type: String },
|
||||
CredentialsUnavailable { cause: String },
|
||||
GenerationQueueUnavailable { cause: String },
|
||||
PaidDispatchRefused { cause: String },
|
||||
PendingEditsUnreadable { cause: String },
|
||||
SourceNotImage {
|
||||
media_type: String,
|
||||
},
|
||||
CredentialsUnavailable {
|
||||
cause: String,
|
||||
},
|
||||
GenerationQueueUnavailable {
|
||||
cause: String,
|
||||
},
|
||||
PaidDispatchRefused {
|
||||
cause: String,
|
||||
},
|
||||
PendingEditsUnreadable {
|
||||
cause: String,
|
||||
},
|
||||
DuplicatePendingOperations,
|
||||
TurnIdUnavailable { cause: String },
|
||||
ProjectRevisionUnavailable { cause: String },
|
||||
BackgroundRemovalFailed { cause: String },
|
||||
TurnIdUnavailable {
|
||||
cause: String,
|
||||
},
|
||||
ProjectRevisionUnavailable {
|
||||
cause: String,
|
||||
},
|
||||
RemoteGenerationFailed(RemoteResourceEditFailure),
|
||||
/// 尚未分类的本地失败(对应 `ResourceEditError::Other`)。
|
||||
// TODO refactor string-typed
|
||||
BackgroundRemovalFailed {
|
||||
cause: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<ToolArgumentsRejection> for RemoveBackgroundError {
|
||||
@@ -137,7 +175,64 @@ impl ToolFailure for RemoveBackgroundError {
|
||||
Self::ProjectRevisionUnavailable { cause } => {
|
||||
format!("抠图失败:读取项目 revision 失败:{cause}")
|
||||
}
|
||||
Self::RemoteGenerationFailed(failure) => failure.to_user_msg(),
|
||||
Self::BackgroundRemovalFailed { cause } => format!("抠图失败:{cause}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RemoveBackgroundError {
|
||||
/// 把资源编辑层的 typed 失败翻成这个工具自己的失败:远端终态失败保留平台原文,
|
||||
/// 其余尚未 typed 化的本地失败仍落到 `BackgroundRemovalFailed`。
|
||||
pub(crate) fn from_resource_edit_error(error: ResourceEditError) -> Self {
|
||||
match error {
|
||||
ResourceEditError::RemoteGenerationFailed { server_message } => {
|
||||
Self::RemoteGenerationFailed(RemoteResourceEditFailure { server_message })
|
||||
}
|
||||
error => Self::BackgroundRemovalFailed {
|
||||
cause: error.to_user_msg(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn remote_generation_failure_carries_platform_message() {
|
||||
let error = RemoveBackgroundError::from_resource_edit_error(
|
||||
ResourceEditError::RemoteGenerationFailed {
|
||||
server_message: Some("provider detail".to_string()),
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(error.to_user_msg(), "资源编辑生成失败:provider detail");
|
||||
assert_eq!(
|
||||
serde_json::to_value(&error).expect("serialize typed failure")
|
||||
["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!(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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,10 +56,22 @@ pub(crate) async fn with_external_editor_api_credentials<T>(
|
||||
pub(crate) async fn with_direct_editor_api_credentials<T>(
|
||||
operation: impl Future<Output = Result<T, String>>,
|
||||
) -> Result<T, String> {
|
||||
with_direct_editor_api_credentials_as(operation, std::convert::identity).await
|
||||
}
|
||||
|
||||
/// 与 [`with_direct_editor_api_credentials`] 同一段作用域语义,但保留调用方自己的错误类型:
|
||||
/// 凭据解析失败由调用方用 `credentials_error` 显式翻译,不引入 `From<String>` 这种隐式转换。
|
||||
pub(crate) async fn with_direct_editor_api_credentials_as<T, E>(
|
||||
operation: impl Future<Output = Result<T, E>>,
|
||||
credentials_error: impl FnOnce(String) -> E,
|
||||
) -> Result<T, E> {
|
||||
if editor_api_mode() == EditorApiMode::PlatformAccount {
|
||||
return operation.await;
|
||||
}
|
||||
let credentials = ensure_private_external_editor_api_credentials().await?;
|
||||
let credentials = match ensure_private_external_editor_api_credentials().await {
|
||||
Ok(credentials) => credentials,
|
||||
Err(cause) => return Err(credentials_error(cause)),
|
||||
};
|
||||
with_external_editor_api_credentials(credentials, operation).await
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user