From 964948bed29d5f78b7d4fa321d87bda6d501cc42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Sun, 4 Oct 2026 10:30:30 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E4=BC=9A=E5=91=98):=20=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E6=9D=83=E9=99=90=E8=AF=BB=E9=93=BE=E8=B7=AF=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E6=94=B9=E6=8A=A5=E7=8B=AC=E7=AB=8B=20503=20=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 MODEL_ACCESS_UNAVAILABLE(503),与 403 档位不足 / 422 模型不可用区分,不再把付费账号静默降级成 Basic - resolve_owner_agc_model_access 返回 Result 并由 load_owner_llm_catalog 透传;缺会员行 / 缺目录行仍由 module 内失败关闭并返回成功 - 入口注释列出三类错误码语义;补错误码映射单测 --- server-rs/crates/api-server/src/llm/mod.rs | 11 +++++- .../crates/api-server/src/llm/model_access.rs | 39 ++++++++++++++----- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/server-rs/crates/api-server/src/llm/mod.rs b/server-rs/crates/api-server/src/llm/mod.rs index af6fea24c..28c275fb8 100644 --- a/server-rs/crates/api-server/src/llm/mod.rs +++ b/server-rs/crates/api-server/src/llm/mod.rs @@ -126,6 +126,15 @@ mod model_catalog_tests { agc_model_resolve_error(module_runtime::AgcModelResolveError::Unavailable); assert_eq!(unavailable.status_code(), StatusCode::UNPROCESSABLE_ENTITY); } + + #[test] + fn access_lookup_failure_maps_to_service_unavailable_with_dedicated_code() { + // 读链路失败必须是独立的 503 + 专用错误码,不能与 403 档位不足 / 422 模型不可用混用, + // 也不能把付费账号静默降级成 Basic。 + let error = model_access::model_access_unavailable_error(); + assert_eq!(error.status_code(), StatusCode::SERVICE_UNAVAILABLE); + assert_eq!(error.code(), model_access::MODEL_ACCESS_UNAVAILABLE); + } } #[cfg(test)] @@ -357,7 +366,7 @@ async fn load_owner_llm_catalog( ); Ok(OwnerAgcModelCatalog { catalog: catalog?, - access, + access: access?, }) } diff --git a/server-rs/crates/api-server/src/llm/model_access.rs b/server-rs/crates/api-server/src/llm/model_access.rs index a6c094f03..c5632c36b 100644 --- a/server-rs/crates/api-server/src/llm/model_access.rs +++ b/server-rs/crates/api-server/src/llm/model_access.rs @@ -3,26 +3,47 @@ //! 模型目录本身是全局的,权限判定在 `module-runtime`;这里只负责把「当前账号」解析成 //! [`AgcModelAccess`],再把全局目录与该档位打包给路由层使用。 //! -//! 失败关闭:会员行缺失、目录行缺失、或 SpacetimeDB 读失败时都按 `Normal`(`Basic`)处理, -//! 只放宽到基础模型,不放开高性能模型。 +//! LLM 路由对外错误码(三条互不重叠,客户端据此区分处理): +//! - `MODEL_NOT_AVAILABLE_FOR_PLAN`(403):模型存在且启用,但当前会员档位不够(`module-runtime` 解析)。 +//! - `MODEL_UNAVAILABLE`(422,列表无可用模型时 503):模型不在目录、已停用,或该档在目录里没有任何可用模型。 +//! - `MODEL_ACCESS_UNAVAILABLE`(503):读取账号权限档本身失败(SpacetimeDB 传输 / procedure / 鉴权)。 +//! 注意**缺会员行 / 缺档位目录行不算这条**:module procedure 已按 `Basic` 失败关闭并返回 `ok: true`。 +//! 能走到这条说明读链路不可用,不能把付费账号静默降级成 `Basic`。 -use crate::state::AppState; +use axum::http::StatusCode; use module_runtime::{AgcModelAccess, AgcModelCatalog}; +use crate::{http_error::AppError, state::AppState}; + +/// 读取账号权限档失败(基础设施)时的专用错误码,与档位不足 / 模型不可用区分。 +pub(crate) const MODEL_ACCESS_UNAVAILABLE: &str = "MODEL_ACCESS_UNAVAILABLE"; + /// 账号视角的 AGC 模型目录:全局目录 + 该账号的权限档。 pub(crate) struct OwnerAgcModelCatalog { pub(crate) catalog: AgcModelCatalog, pub(crate) access: AgcModelAccess, } +/// 读链路失败时的统一错误:503 + [`MODEL_ACCESS_UNAVAILABLE`]。 +/// +/// 独立成函数是为了让「基础设施失败」这件事可被单测直接断言,而不是散落的构造。 +pub(crate) fn model_access_unavailable_error() -> AppError { + AppError::from_status(StatusCode::SERVICE_UNAVAILABLE) + .with_code(MODEL_ACCESS_UNAVAILABLE) + .with_message("会员权限档暂不可用,请稍后重试") +} + /// 解析账号的 AGC 模型权限档。 /// /// 走专用只读 procedure `get_profile_agc_model_access`:它只读会员账期投影与档位目录, /// 不触发换期 / 免费点刷新,适合 LLM 代理热路径。 +/// +/// 成功返回档位;读链路失败返回 503 + [`MODEL_ACCESS_UNAVAILABLE`],由路由层透传,不再静默降级成 `Basic`。 +/// 缺会员行 / 缺档位目录行仍由 module 内失败关闭到 `Basic` 并返回成功。 pub(crate) async fn resolve_owner_agc_model_access( state: &AppState, owner_user_id: &str, -) -> AgcModelAccess { +) -> Result { #[cfg(test)] if super::test_provisioned_router_credentials() .lock() @@ -30,21 +51,21 @@ pub(crate) async fn resolve_owner_agc_model_access( .contains_key(owner_user_id) { // 测试夹具目录里的模型全是缺省 `Basic`,返回 `Full` 以避免夹具再去连 SpacetimeDB。 - return AgcModelAccess::Full; + return Ok(AgcModelAccess::Full); } match state .spacetime_client() .get_profile_agc_model_access(owner_user_id.to_string()) .await { - Ok(record) => AgcModelAccess::from(record.model_access), + Ok(record) => Ok(AgcModelAccess::from(record.model_access)), Err(error) => { - tracing::warn!( + tracing::error!( user_id = %owner_user_id, error = %error, - "读取会员档位失败,AGC 模型权限按 Basic 失败关闭" + "读取会员档位失败,AGC 模型权限按 503 上报" ); - AgcModelAccess::Basic + Err(model_access_unavailable_error()) } } }