diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/turn_error.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/turn_error.rs index 1dc471acc..2197d4b8e 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/turn_error.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/turn_error.rs @@ -24,8 +24,12 @@ //! - app-server 投影([`TurnError::from_model_call`]):模型 / 上游 / 通道类失败; //! - 执行适配器(`codex_app_server::execution`):宿主亲眼看到的收场事实(通道断开 / 超时 / 中断)。 //! -//! TODO(文本判据):[`DomainFact::classify`] 仍在读文本——那是产生层还没有 typed 出口的 -//! 事实(美术凭据、项目历史、执行预算)。这条债要在产生层逐个补 typed 变体后清掉。 +//! TODO(文本判据):[`SuperErrorEnumFromStringTyped::classify`] 把**产生层交上来的字符串** +//! 读回 typed 事实。这是临时债,不是设计:上游(assets 私有凭据、项目写锁 / 历史追加锁、 +//! 项目历史形状、图集恢复、执行预算、试玩次数、工具参数)目前只交 `String` detail,没有 +//! typed 出口。修法在**上游**:让这些产生层直接产出 typed 变体 / 结构化子错误,判定层改成 +//! `match`;等上游补完,这张表和 `runtime_error::classify_direct_codex_error` 一起删掉。 +//! 补齐之前:**不许往表里加词**——新增分类必须先在产生层加 typed 变体。 use std::path::Path; @@ -200,7 +204,7 @@ impl NativeKind { /// 这一条分类值不值得当作"再试一次可能修好":与 [`Self::is_terminal`] 互为反义,但语义不同—— /// 这里问的是"用户重试有没有意义",用于失败诊断的 `retryable` 字段。 /// - /// `Unauthorized` 必须与 [`DomainFact::AuthenticationRejected`] 同口径:登录态失效重登 + /// `Unauthorized` 必须与 [`SuperErrorEnumFromStringTyped::AuthenticationRejected`] 同口径:登录态失效重登 /// 之后再发一次是有意义的,`recovery_hint` 也是这么写的。两套分类路径给出相反结论,会让同一 /// 份事实的 `retryable` 取决于哪一层先认出它。 fn is_retryable(&self) -> bool { @@ -638,9 +642,11 @@ impl TurnError { // 阶段失败 / 桥变体:**认出是哪一类就拦**(产生层还没 typed 出口的深层事实才继续 // 反馈)。已归类的都是模型改不动的事实——凭据 / 权限 / 额度 / 历史一致性与契约变化, // 把同一份输入再跑一轮只会拿到同一结论;旧的字面量判据也是这个口径。 - Self::TurnFailed(payload) => DomainFact::classify(&payload.detail).is_none(), + Self::TurnFailed(payload) => { + SuperErrorEnumFromStringTyped::classify(&payload.detail).is_none() + } Self::TurnFailedUnclassified(payload) => { - DomainFact::classify(&payload.detail).is_none() + SuperErrorEnumFromStringTyped::classify(&payload.detail).is_none() } _ => false, } @@ -653,11 +659,11 @@ impl TurnError { Self::TransportClosed(_) => false, // 超时/中断后重试是常规动作:宿主已经把这一轮收干净了。 Self::TimedOut(_) | Self::TurnInterrupted(_) => true, - Self::TurnFailed(payload) => { - DomainFact::classify(&payload.detail).is_none_or(DomainFact::is_retryable) - } + Self::TurnFailed(payload) => SuperErrorEnumFromStringTyped::classify(&payload.detail) + .is_none_or(SuperErrorEnumFromStringTyped::is_retryable), Self::TurnFailedUnclassified(payload) => { - DomainFact::classify(&payload.detail).is_none_or(DomainFact::is_retryable) + SuperErrorEnumFromStringTyped::classify(&payload.detail) + .is_none_or(SuperErrorEnumFromStringTyped::is_retryable) } _ => false, } @@ -667,11 +673,11 @@ impl TurnError { pub(crate) fn public_summary(&self) -> Option<&'static str> { match self { Self::ModelCallFailed(payload) => payload.kind.public_summary(), - Self::TurnFailed(payload) => { - DomainFact::classify(&payload.detail).and_then(DomainFact::public_summary) - } + Self::TurnFailed(payload) => SuperErrorEnumFromStringTyped::classify(&payload.detail) + .and_then(SuperErrorEnumFromStringTyped::public_summary), Self::TurnFailedUnclassified(payload) => { - DomainFact::classify(&payload.detail).and_then(DomainFact::public_summary) + SuperErrorEnumFromStringTyped::classify(&payload.detail) + .and_then(SuperErrorEnumFromStringTyped::public_summary) } _ => None, } @@ -783,12 +789,15 @@ fn native_kind(detail: &str) -> Option { Some(NativeKind::from_id(id)) } -/// 深层域事实:**产生层还没有 typed 出口**的事实,在这里读成 typed 值,之后所有决策只 `match`。 +/// 深层失败事实**表**:**产生层还没有 typed 出口**的事实,在这里读成 typed 值,之后所有决策只 `match`。 +/// +/// 名字故意起得又长又扎眼(`Super…FromStringTyped`):这是**待清的债**,不是好名字。 +/// 别把它改漂亮——要么按文件顶部 TODO 让上游产出 typed 变体后整张表删掉,要么什么都别做。 /// /// 这里的判据仍然是文本,因为产生层给出来的就只有文本(平台美术/凭据、项目历史、执行预算)。 /// 规则:**新分类必须先在产生层加 typed 变体**,别往这份表里加词;每条都注明了应由谁给出 typed 事实。 #[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum DomainFact { +enum SuperErrorEnumFromStringTyped { /// 泥点余额不足:平台付费接口(`direct_paid_submission` / 美术生成 / 上游 409)。 PaidCreditsInsufficient, /// 本机私有凭据目录没准备好(`assets` 的私有凭据存储)。 @@ -827,7 +836,7 @@ enum DomainFact { Cancelled, } -impl DomainFact { +impl SuperErrorEnumFromStringTyped { fn classify(detail: &str) -> Option { let normalized = detail.to_ascii_lowercase(); let contains = |marker: &str| { @@ -838,9 +847,9 @@ impl DomainFact { } }; // 顺序即优先级:更具体的事实先判,笼统的放后面。 - const CANDIDATES: &[(DomainFact, &[&str])] = &[ + const CANDIDATES: &[(SuperErrorEnumFromStringTyped, &[&str])] = &[ ( - DomainFact::PaidCreditsInsufficient, + SuperErrorEnumFromStringTyped::PaidCreditsInsufficient, &[ "泥点余额不足", "可消费泥点不足", @@ -850,31 +859,31 @@ impl DomainFact { ], ), ( - DomainFact::CredentialStorageUnprepared, + SuperErrorEnumFromStringTyped::CredentialStorageUnprepared, &["private-external-editor-credential-storage-preparation-failed"], ), ( - DomainFact::CredentialNotPersisted, + SuperErrorEnumFromStringTyped::CredentialNotPersisted, &["private-external-editor-credential-persistence-failed"], ), ( - DomainFact::LocalDeveloperKeyMissing, + SuperErrorEnumFromStringTyped::LocalDeveloperKeyMissing, &["本机陶泥儿开发者 Key"], ), ( - DomainFact::AuthenticationRejected, + SuperErrorEnumFromStringTyped::AuthenticationRejected, &["authentication-required", "unauthorized", "http 401"], ), ( - DomainFact::PermissionDenied, + SuperErrorEnumFromStringTyped::PermissionDenied, &["permission-denied", "http 403"], ), ( - DomainFact::HistoryInjectionOversize, + SuperErrorEnumFromStringTyped::HistoryInjectionOversize, &["历史注入载荷超过单行上限"], ), ( - DomainFact::HistoryShapeUnsupported, + SuperErrorEnumFromStringTyped::HistoryShapeUnsupported, &[ "DirectProject 历史记录类型无效", "DirectProject 历史记录缺少 payload", @@ -882,15 +891,15 @@ impl DomainFact { ], ), ( - DomainFact::ProjectWriteLockContention, + SuperErrorEnumFromStringTyped::ProjectWriteLockContention, &[crate::project::PROJECT_WRITE_LOCK_CONTENTION_PREFIX], ), ( - DomainFact::HistoryContention, + SuperErrorEnumFromStringTyped::HistoryContention, &[crate::project::PROJECT_APPEND_LOCK_TIMEOUT_MARKER], ), ( - DomainFact::ArtIdentityRejected, + SuperErrorEnumFromStringTyped::ArtIdentityRejected, &[ "身份不唯一", "身份不匹配", @@ -898,22 +907,31 @@ impl DomainFact { "没有可见像素", ], ), - (DomainFact::ContractChanged, &["合同发生变化"]), ( - DomainFact::ValidationBudgetExhausted, + SuperErrorEnumFromStringTyped::ContractChanged, + &["合同发生变化"], + ), + ( + SuperErrorEnumFromStringTyped::ValidationBudgetExhausted, &["validation-budget-exhausted"], ), ( - DomainFact::ValidationAlreadyRunning, + SuperErrorEnumFromStringTyped::ValidationAlreadyRunning, &["validation-already-running"], ), ( - DomainFact::PlaytestAttemptLimitExceeded, + SuperErrorEnumFromStringTyped::PlaytestAttemptLimitExceeded, &["playtest-attempt-limit-exceeded"], ), - (DomainFact::ToolArgumentsInvalid, &["工具参数"]), - (DomainFact::Cancelled, &["取消"]), - (DomainFact::CredentialsUnavailable, &["credential", "凭据"]), + ( + SuperErrorEnumFromStringTyped::ToolArgumentsInvalid, + &["工具参数"], + ), + (SuperErrorEnumFromStringTyped::Cancelled, &["取消"]), + ( + SuperErrorEnumFromStringTyped::CredentialsUnavailable, + &["credential", "凭据"], + ), ]; CANDIDATES .iter() @@ -1018,7 +1036,7 @@ impl DomainFact { /// 阶段兜底的恢复建议:typed 分类给不出动作时,由阶段给一句与交付状态对得上的话。 fn stage_recovery_hint(stage: FailureStage, detail: &str) -> Option<&'static str> { - if let Some(fact) = DomainFact::classify(detail) { + if let Some(fact) = SuperErrorEnumFromStringTyped::classify(detail) { return Some(fact.recovery_hint()); } Some(match stage {