资源编辑远端失败原文透传到工具错误与诊断
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
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,11 @@
|
||||
- 决策(前缀去留):删掉第一句失败文案里的 `remote-terminal-failed:`(轮询与提交期 400 两处)。`ensure_resource_edit_phase_resumable` 里那三个 token 保留:它们与三个 phase 一一对应,是那句重试文案里区分「确定失败 / 已归档 / 待对账」的唯一手段。
|
||||
- 决策(原文边界):平台原文只进当次错误文案,仍不进账本(`terminal_failure_code` 的写入边界与既有断言不变)。平台 `user_visible_external_generation_error` 已对四种 kind 做 sanitize,图片/视频两种原样透出——与同 wire 的 `canvas_generation.rs` 口径一致,要收边界应改服务端。
|
||||
- 决策(命名与落点):尚未 typed 化的变体叫 `Other`,不叫 `Message`(后者分不清是「已渲染文案」还是「原始消息」);新错误单独放 `project/resource_editor/error.rs`,不再往主文件里塞类型定义。
|
||||
- 改动范围:`apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs`、`project/resource_editor/error.rs`(新)、`agent/tool/create_or_derive_resource/error.rs`、`agent/tool/remove_background/error.rs`、`agent/direct_tool_bridge.rs`、`docs/project-memory/shared-memory/pitfalls.md`。
|
||||
- 验证:待补(`cargo test --bin genarrative-ai-game-creator-shell -- project::resource_editor` 与 `-- agent::direct_tool_bridge`;`npm run check:encoding`;`git diff --check`)。
|
||||
- 决策(工具层承载):`RemoteGenerationFailed` 不能到工具层又被压回一句字符串。共用载体放 `agent/tool/error.rs` 的 `RemoteResourceEditFailure { serverMessage }`(两个工具共用的文案只写一份),`CreateOrDeriveResourceError` / `RemoveBackgroundError` 各加 `RemoteGenerationFailed(RemoteResourceEditFailure)` 变体;翻译用显式 `from_resource_edit_error`,不用 `impl From<ResourceEditError>`,远端终态进 `RemoteGenerationFailed`、其余 `Other` 仍落回各工具原有的「失败:<文案>」变体。这样诊断 sidecar 的 `error` 字段(typed enum 整体序列化)天然带上平台原文,LLM 侧拿到的 `message` 也带上。
|
||||
- 决策(凭据作用域的 error 类型):`with_direct_editor_api_credentials` 原本把操作限定成 `Result<_, String>`,会把 typed 错误提前压掉。新增 `with_direct_editor_api_credentials_as(operation, credentials_error)` 保留调用方 error 类型,凭据解析失败由调用方显式翻译(这里传 `ResourceEditError::Other`),旧名保持 `String` 语义、零改动。
|
||||
- 决策(future 装箱):`handle_direct_tool_bridge` 的状态机在调试测试线程的默认栈上已经贴着上限,资源编辑 arm 直接内联会顶穿(`bridge_write_file_waits_on_the_blocking_pool_instead_of_a_runtime_worker` 栈溢出)。桥里四个资源编辑 await 点用 `Box::pin` 只留指针进外层状态机;这是体积问题,不是错误用 `Box`。
|
||||
- 改动范围:`apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs`、`project/resource_editor/error.rs`(新)、`agent/tool/error.rs`、`agent/tool/create_or_derive_resource/error.rs`、`agent/tool/remove_background/error.rs`、`agent/direct_tool_bridge.rs`、`assets.rs`、`docs/project-memory/shared-memory/pitfalls.md`。
|
||||
- 验证:`cargo check --bin genarrative-ai-game-creator-shell --tests` 通过;`cargo test --bin genarrative-ai-game-creator-shell -- project::resource_editor --test-threads=1` 66 passed(并行跑会有一批 TCP fixture 用例因争用超时,串行全绿,与本次改动无关);`-- agent::tool:: agent::direct_tool_bridge` 47 passed;`npm run check:encoding` 5111 files;`git diff --check` 干净;`cargo fmt` 已跑。
|
||||
- 关联:`pitfalls.md`「远端资源编辑终态必须指出唯一出口」。
|
||||
## 2026-10-01 Web、后台与 AGC 一键联调
|
||||
|
||||
|
||||
@@ -340,7 +340,7 @@ Copy Artifact 插件在**非 SYSTEM 认证**下按「认证用户」判权:只
|
||||
|
||||
- 现象:「生成背景音乐」再次提交 0.1 秒就失败,卡片只有 `remote-terminal-failed: 远端资源编辑已明确失败,不允许再次请求`,既没有原因也没有下一步。
|
||||
- 原因:上一次同 `operationId` 的请求被平台确定性拒绝(HTTP 400 或任务 `failed`)后,账本落到 `remote-failed`,之后所有重试都在 `ensure_resource_edit_phase_resumable` 失败关闭;唯一出口是「待恢复资源编辑」里的移出恢复队列,但终态文案没有指向它。
|
||||
- 处理:终态文案带出稳定失败码,并明确「先在待恢复资源编辑中把它移出恢复队列」;上游失败原文仍不写入账本(只存分类码),首次失败的原始拒绝说明继续由当次错误文案承担——轮询终态这条路由此改成把平台 `error` 原文装进 `ResourceEditError::RemoteGenerationFailed` 原样带出(2026-10-02),不再压成「资源编辑生成失败」一句;第一句失败文案不再带 `remote-terminal-failed:` 前缀。
|
||||
- 处理:终态文案带出稳定失败码,并明确「先在待恢复资源编辑中把它移出恢复队列」;上游失败原文仍不写入账本(只存分类码),首次失败的原始拒绝说明继续由当次错误文案承担——轮询终态这条路由此改成把平台 `error` 原文装进 `ResourceEditError::RemoteGenerationFailed` 原样带出(2026-10-02),工具层再用 `RemoteResourceEditFailure` 原样传给模型与用户卡片,不再压成「资源编辑生成失败」一句;第一句失败文案不再带 `remote-terminal-failed:` 前缀。
|
||||
- 验证:`remote_failed_status_is_terminal_and_can_only_be_archived` 断言失败文案带出平台 `error` 原文、同时账本序列化不含原文;`background_removal_remote_failure_keeps_manifest_without_result` 覆盖平台没给 `error` 时的兜底文案;`submission_bad_request_is_terminal_while_gateway_failure_requires_reconciliation` 等资源编辑用例继续通过。
|
||||
- 关联:`apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs`。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user