重构:文本判据债改名 SuperErrorEnumFromStringTyped 并写清上游改造 TODO

- turn_error.rs 把 DomainFact 改名为故意扎眼的 SuperErrorEnumFromStringTyped,标记"从字符串读出的 typed 失败事实表"这笔待清债
- TODO 从"产生层还没有 typed 出口"改为明确指向上游改造:让产生层直接产出 typed 变体/结构化子错误、判定层改 match,之后整张表与 classify_direct_codex_error 一起删
- enum 注释写明这个名字是债标记:禁止把它改漂亮,只允许按 TODO 删掉整张表
- 只改名与注释,不改任何判定逻辑
This commit is contained in:
2026-10-02 18:10:39 +08:00
parent 394b8de9d2
commit bee23a0a25
@@ -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<NativeKind> {
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<Self> {
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 {