diff --git a/.agents/skills/cpr-dev-guide/SKILL.md b/.agents/skills/cpr-dev-guide/SKILL.md index e69ed6c3d..001c645a7 100644 --- a/.agents/skills/cpr-dev-guide/SKILL.md +++ b/.agents/skills/cpr-dev-guide/SKILL.md @@ -40,6 +40,12 @@ description: Codex Proxy RS 仓库的开发、修复、排障、代码审查和 3. 按变更范围读取[验证](../../../CONTRIBUTING.md#验证),区分通过、失败、跳过与未执行;提交时补读[提交约定](../../../CONTRIBUTING.md#项目约定) 4. 交付说明实际结果与验证缺口;页面证据和后端生命周期检查按上表执行,不把构建通过当作行为验收 +## 后端注释 + +- 每个 Rust 文件顶层使用简短的中文 `//!` 注释说明文件职责,覆盖生产源码、测试与构建脚本;测试文件说明测试范围或辅助用途,生成文件同步维护生成器 +- 注释不使用中文或英文句号,多句说明按语义分行;保留 URL、版本号、标识符和代码示例中有语义的点号 +- 文件说明聚焦当前功能与职责边界,局部注释解释原因和约束,避免复述代码或记录修改经过 + ## 文档硬约束 **常规文档只描述当前状态,不写变更历史**,这项检查适用于每次开发收尾,不限于专门的文档任务 diff --git a/backend/apps/gateway/src/bootstrap/command.rs b/backend/apps/gateway/src/bootstrap/command.rs index 214844327..354db1ba0 100644 --- a/backend/apps/gateway/src/bootstrap/command.rs +++ b/backend/apps/gateway/src/bootstrap/command.rs @@ -1,4 +1,4 @@ -//! 插件帮助只读启动;命令仅组装命令平面和必要写泵。 +//! 插件帮助只读启动;命令仅组装命令平面和必要写泵 use super::{ BootstrapError, GatewayConfig, @@ -6,7 +6,7 @@ use super::{ startup::{Environment, Mode, Providers}, }; -/// 这里只接收插件命名空间内的参数,不转交宿主配置或其他启动 secret。 +/// 这里只接收插件命名空间内的参数,不转交宿主配置或其他启动 secret pub struct PluginCommand { pub instance_id: Option, pub name: Option, diff --git a/backend/apps/gateway/src/bootstrap/config.rs b/backend/apps/gateway/src/bootstrap/config.rs index b3f0cc184..a665449a1 100644 --- a/backend/apps/gateway/src/bootstrap/config.rs +++ b/backend/apps/gateway/src/bootstrap/config.rs @@ -1,10 +1,12 @@ +//! 网关顶层配置的加载、路径解析与跨模块参数校验 + use gateway_host::{ConfigError, HostConfig, LoadableConfig}; use serde::Deserialize; const CONFIG_SCHEMA_VERSION: u32 = 1; -/// 顶层配置只组合各包拥有的配置段,不解释任何业务字段。 -/// 各启动配置忽略未知字段,允许升级时保留旧配置;已知字段仍按类型和业务约束校验。 +/// 顶层配置只组合各包拥有的配置段,不解释任何业务字段 +/// 各启动配置忽略未知字段,允许升级时保留旧配置;已知字段仍按类型和业务约束校验 #[derive(Debug, Deserialize)] pub struct GatewayConfig { schema_version: u32, diff --git a/backend/apps/gateway/src/bootstrap/mod.rs b/backend/apps/gateway/src/bootstrap/mod.rs index e43240254..2505a9260 100644 --- a/backend/apps/gateway/src/bootstrap/mod.rs +++ b/backend/apps/gateway/src/bootstrap/mod.rs @@ -1,4 +1,4 @@ -//! 网关唯一组合根:按启动模式连接各 Bundle。 +//! 网关唯一组合根:按启动模式连接各 Bundle mod command; mod config; @@ -10,7 +10,7 @@ pub use command::{PluginCommand, plugin_command}; pub use config::GatewayConfig; pub use server::run; -/// 组合根只保留包级错误分类,不展开内部实现或敏感配置。 +/// 组合根只保留包级错误分类,不展开内部实现或敏感配置 #[derive(Debug, thiserror::Error)] pub enum BootstrapError { #[error(transparent)] diff --git a/backend/apps/gateway/src/bootstrap/plugins.rs b/backend/apps/gateway/src/bootstrap/plugins.rs index 33c3ae1ac..d6cc8e6f2 100644 --- a/backend/apps/gateway/src/bootstrap/plugins.rs +++ b/backend/apps/gateway/src/bootstrap/plugins.rs @@ -1,4 +1,4 @@ -//! 插件 Runtime 的组装与回调资源持有;服务和命令模式复用同一组管理端口。 +//! 插件 Runtime 的组装与回调资源持有;服务和命令模式复用同一组管理端口 use std::sync::Arc; @@ -15,7 +15,7 @@ use gateway_plugin_runtime::PluginRuntime; use super::BootstrapError; -// 必须在调用方仍持有 Core 和回调资源时异步关闭,不能先退出该作用域再清理。 +// 必须在调用方仍持有 Core 和回调资源时异步关闭,不能先退出该作用域再清理 macro_rules! or_shutdown { ($runtime:expr, $result:expr) => { match $result { @@ -31,7 +31,7 @@ macro_rules! or_shutdown { pub(super) use or_shutdown; pub(super) struct ManagementPorts { - // Runtime 保存 Weak;组合根持有这些 Arc,直到插件全部关闭。 + // Runtime 保存 Weak;组合根持有这些 Arc,直到插件全部关闭 pub(super) accounts: Arc, keys: Arc, resources: Arc, diff --git a/backend/apps/gateway/src/bootstrap/server.rs b/backend/apps/gateway/src/bootstrap/server.rs index 40c0c14e3..927f295a4 100644 --- a/backend/apps/gateway/src/bootstrap/server.rs +++ b/backend/apps/gateway/src/bootstrap/server.rs @@ -1,4 +1,4 @@ -//! 服务模式完成全部组装后交给 Host 监听,并在请求和 Worker 排空后关闭插件。 +//! 服务模式完成全部组装后交给 Host 监听,并在请求和 Worker 排空后关闭插件 use gateway_host::ConfigError; @@ -8,7 +8,7 @@ use super::{ startup::{Environment, Mode, Providers}, }; -/// 按冻结顺序初始化全部 Bundle,并把进程阻塞权交给 Host。 +/// 按冻结顺序初始化全部 Bundle,并把进程阻塞权交给 Host pub async fn run() -> Result<(), BootstrapError> { let config = gateway_host::load_config::()?; let environment = Environment::initialize(config.host, config.store, Mode::Server).await?; @@ -126,7 +126,7 @@ pub async fn run() -> Result<(), BootstrapError> { if let Some(snapshot) = snapshot { return middleware.resolve(snapshot.extensions()?); } - // 数据面未就绪时,管理与诊断路由仍可使用发布候选中的插件。 + // 数据面未就绪时,管理与诊断路由仍可使用发布候选中的插件 let diagnostic = snapshots.snapshot_for_diagnostics()?; middleware.resolve(diagnostic.extensions()?) } diff --git a/backend/apps/gateway/src/bootstrap/startup.rs b/backend/apps/gateway/src/bootstrap/startup.rs index 1ad375e49..0a980d1ed 100644 --- a/backend/apps/gateway/src/bootstrap/startup.rs +++ b/backend/apps/gateway/src/bootstrap/startup.rs @@ -1,4 +1,4 @@ -//! 启动模式共享的基础设施与 Provider 组装,不启动 HTTP 或 Worker。 +//! 启动模式共享的基础设施与 Provider 组装,不启动 HTTP 或 Worker use std::{path::PathBuf, sync::Arc}; diff --git a/backend/apps/gateway/src/lib.rs b/backend/apps/gateway/src/lib.rs index 615bb4e77..544e0082b 100644 --- a/backend/apps/gateway/src/lib.rs +++ b/backend/apps/gateway/src/lib.rs @@ -1,3 +1,3 @@ -//! 网关进程组装根。 +//! 网关进程组装根 pub mod bootstrap; diff --git a/backend/apps/gateway/src/main.rs b/backend/apps/gateway/src/main.rs index bc9d02859..843c6e2e5 100644 --- a/backend/apps/gateway/src/main.rs +++ b/backend/apps/gateway/src/main.rs @@ -1,3 +1,5 @@ +//! 网关命令行入口,选择启动模式并创建异步运行时 + use std::{ env, error::Error, diff --git a/backend/apps/gateway/tests/architecture.rs b/backend/apps/gateway/tests/architecture.rs index 2bd3cf7d4..79e31f542 100644 --- a/backend/apps/gateway/tests/architecture.rs +++ b/backend/apps/gateway/tests/architecture.rs @@ -1,4 +1,4 @@ -//! docs/architecture.md 依赖 DAG 与生产源码纪律的 workspace 级机器校验。 +//! docs/architecture.md 依赖 DAG 与生产源码纪律的 workspace 级机器校验 use std::{ collections::{BTreeMap, BTreeSet}, @@ -8,7 +8,7 @@ use std::{ use syn::{Item, visit::Visit}; -/// workspace 成员冻结清单;新增 crate 必须同步扩展本文件的依赖规则。 +/// workspace 成员冻结清单;新增 crate 必须同步扩展本文件的依赖规则 pub(super) const WORKSPACE_MEMBERS: &[&str] = &[ "apps/gateway", "apps/plugin-cli", @@ -178,7 +178,7 @@ fn gateway_admin_stays_free_of_infrastructure_dependencies() { ); } -/// workspace 包名到冻结成员路径的映射。 +/// workspace 包名到冻结成员路径的映射 const PACKAGE_TO_MEMBER: &[(&str, &str)] = &[ ("codex-proxy-rs", "apps/gateway"), ("codex-proxy-plugin-cli", "apps/plugin-cli"), @@ -194,7 +194,7 @@ const PACKAGE_TO_MEMBER: &[(&str, &str)] = &[ ("provider-xai", "crates/providers/xai"), ]; -/// SDK 与 Adapter/provider 根门面的稳定合同模块;任何增减都必须同步完成边界审计。 +/// SDK 与 Adapter/provider 根门面的稳定合同模块;任何增减都必须同步完成边界审计 const ADAPTER_PUBLIC_MODULES: &[(&str, &[&str])] = &[ ("crates/gateway-plugin/sdk", &["call", "client"]), ("crates/gateway-plugin/runtime", &[]), @@ -224,13 +224,13 @@ const ADAPTER_PUBLIC_MODULES: &[(&str, &[&str])] = &[ ("crates/providers/xai", &["credential", "transport"]), ]; -/// 不对应单一生产模块、而是校验 crate/workspace 整体契约的根级测试场景。 +/// 不对应单一生产模块、而是校验 crate/workspace 整体契约的根级测试场景 const ROOT_TEST_SCENARIOS: &[(&str, &[&str])] = &[ ("apps/gateway", &["architecture"]), ("crates/gateway-api", &["architecture"]), ]; -/// 冻结的 workspace 内部运行时依赖边;新增/删除任何边都必须同步本表。 +/// 冻结的 workspace 内部运行时依赖边;新增/删除任何边都必须同步本表 const ALLOWED_INTERNAL_EDGES: &[(&str, &str)] = &[ ("codex-proxy-plugin-cli", "gateway-plugin-sdk"), ("codex-proxy-rs", "gateway-admin"), @@ -475,7 +475,7 @@ fn integration_tests_mirror_production_module_tree() { } } -/// Rust 子进程 fixture 是 Cargo 的独立 crate 根,不能要求它由测试模块再次声明。 +/// Rust 子进程 fixture 是 Cargo 的独立 crate 根,不能要求它由测试模块再次声明 fn test_source_roots() -> BTreeMap> { let metadata = cargo_metadata_json(); let mut roots = BTreeMap::>::new(); @@ -529,7 +529,7 @@ fn root_test_scenario_allowed(member: &str, module: &Path) -> bool { fn assert_module_tree(root: &Path, crate_roots: &[&str]) { let files = super::rust_files(root); - // 同一父模块的所有子文件复用一次语法解析,计数仍保留重复声明检查。 + // 同一父模块的所有子文件复用一次语法解析,计数仍保留重复声明检查 let mut declarations = BTreeMap::>::new(); for relative in &files { if crate_roots @@ -649,7 +649,7 @@ fn cargo_metadata_json() -> serde_json::Value { serde_json::from_slice(&output.stdout).expect("parse cargo metadata") } -/// 提取成员 `[dependencies]` 段内声明的依赖名;段落以下一个 `[` 表头结束。 +/// 提取成员 `[dependencies]` 段内声明的依赖名;段落以下一个 `[` 表头结束 fn dependency_names(member: &str) -> Vec { let manifest = fs::read_to_string(backend_root().join(member).join("Cargo.toml")) .expect("read member manifest"); diff --git a/backend/apps/gateway/tests/bootstrap/command.rs b/backend/apps/gateway/tests/bootstrap/command.rs index a944ab5a2..9a1578aad 100644 --- a/backend/apps/gateway/tests/bootstrap/command.rs +++ b/backend/apps/gateway/tests/bootstrap/command.rs @@ -1,3 +1,5 @@ +//! 验证网关基础命令无需加载配置,并拒绝非法或多余参数 + use std::{fs, process::Command}; #[test] diff --git a/backend/apps/gateway/tests/bootstrap/config.rs b/backend/apps/gateway/tests/bootstrap/config.rs index e2fb46a0a..b0cfbc429 100644 --- a/backend/apps/gateway/tests/bootstrap/config.rs +++ b/backend/apps/gateway/tests/bootstrap/config.rs @@ -1,3 +1,5 @@ +//! 验证顶层配置加载、路径解析、显式覆盖与敏感字段脱敏 + use std::{fs, process::Command}; use codex_proxy_rs::bootstrap::GatewayConfig; @@ -27,7 +29,7 @@ fn config_loader_should_resolve_paths_relative_to_config_file() { fn config_loader_should_share_resolved_assets_with_system_update() { const CHILD_ENV: &str = "CPR_TEST_UPDATE_ASSETS_CHILD"; let Ok(case) = std::env::var(CHILD_ENV) else { - // 使用子进程覆盖无环境变量、Docker 路径和相对路径,避免污染并行测试。 + // 使用子进程覆盖无环境变量、Docker 路径和相对路径,避免污染并行测试 for (case, web_dist) in [ ("binary", None), ("docker", Some("/app/web/dist")), @@ -379,7 +381,7 @@ fn valid_config() -> String { } fn valid_config_document() -> serde_json::Value { - // 按字段修改样例,避免注释或排版变化让测试输入悄悄失效;JSON 仍可由 YAML 文件入口加载。 + // 按字段修改样例,避免注释或排版变化让测试输入悄悄失效;JSON 仍可由 YAML 文件入口加载 config::Config::builder() .add_source(config::File::from_str( &valid_config(), diff --git a/backend/apps/gateway/tests/bootstrap/mod.rs b/backend/apps/gateway/tests/bootstrap/mod.rs index 98de9f4f2..d788cd84c 100644 --- a/backend/apps/gateway/tests/bootstrap/mod.rs +++ b/backend/apps/gateway/tests/bootstrap/mod.rs @@ -1,3 +1,5 @@ +//! 网关启动配置、命令行与服务组装的测试入口 + mod command; mod config; mod server; diff --git a/backend/apps/gateway/tests/bootstrap/server.rs b/backend/apps/gateway/tests/bootstrap/server.rs index d933aecb4..dee48b8b5 100644 --- a/backend/apps/gateway/tests/bootstrap/server.rs +++ b/backend/apps/gateway/tests/bootstrap/server.rs @@ -1,3 +1,5 @@ +//! 验证服务组装为 HTTPS 代理探测传递自定义 CA + use std::{fs, process::Command}; #[tokio::test] @@ -19,7 +21,7 @@ async fn proxy_probe_should_use_provider_custom_ca_for_https_proxies() { let Ok(directory) = std::env::var(CHILD_ENV) else { let directory = tempfile::tempdir().unwrap(); generate_proxy_test_certificates(directory.path()); - // 使用子进程隔离环境变量,避免并行测试读取到临时 CA 配置。 + // 使用子进程隔离环境变量,避免并行测试读取到临时 CA 配置 for ca_env in ["CODEX_CA_CERTIFICATE", "SSL_CERT_FILE"] { let mut child = Command::new(std::env::current_exe().unwrap()); child diff --git a/backend/apps/gateway/tests/main.rs b/backend/apps/gateway/tests/main.rs index f989ed589..764277aa9 100644 --- a/backend/apps/gateway/tests/main.rs +++ b/backend/apps/gateway/tests/main.rs @@ -1,8 +1,10 @@ +//! 网关组合根测试入口,检查源码模块纪律与冻结的文件边界 + mod architecture; mod bootstrap; -// 组合根是否混入业务策略由代码审查判断,行数和标识符/注释关键词不能证明职责越界。 -// 依赖 DAG、公开模块面和模块镜像继续由 architecture 检查,源码纪律由下方测试维护。 +// 组合根是否混入业务策略由代码审查判断,行数和标识符/注释关键词不能证明职责越界 +// 依赖 DAG、公开模块面和模块镜像继续由 architecture 检查,源码纪律由下方测试维护 use std::{ collections::BTreeSet, diff --git a/backend/apps/plugin-cli/src/main.rs b/backend/apps/plugin-cli/src/main.rs index 9ec4b1f53..817ddd1a1 100644 --- a/backend/apps/plugin-cli/src/main.rs +++ b/backend/apps/plugin-cli/src/main.rs @@ -1,3 +1,5 @@ +//! 插件打包命令行,校验输入资源并生成带摘要的平台归档 + use std::{ collections::{BTreeMap, BTreeSet}, fs::{self, File, OpenOptions}, diff --git a/backend/apps/plugin-cli/tests/main.rs b/backend/apps/plugin-cli/tests/main.rs index 19f94cf6e..b0789124c 100644 --- a/backend/apps/plugin-cli/tests/main.rs +++ b/backend/apps/plugin-cli/tests/main.rs @@ -1,3 +1,5 @@ +//! 插件打包命令行的参数、归档校验与输出行为测试 + use std::{ collections::BTreeMap, fs::{self, File}, diff --git a/backend/crates/gateway-admin/src/backup/mod.rs b/backend/crates/gateway-admin/src/backup/mod.rs index 98eb0ee1e..dc937f77b 100644 --- a/backend/crates/gateway-admin/src/backup/mod.rs +++ b/backend/crates/gateway-admin/src/backup/mod.rs @@ -1,4 +1,4 @@ -//! 备份纯策略与 Worker 任务。 +//! 备份纯策略与 Worker 任务 pub mod policy; pub mod task; diff --git a/backend/crates/gateway-admin/src/backup/policy.rs b/backend/crates/gateway-admin/src/backup/policy.rs index 39e9fc3aa..ead47eb9d 100644 --- a/backend/crates/gateway-admin/src/backup/policy.rs +++ b/backend/crates/gateway-admin/src/backup/policy.rs @@ -1,6 +1,6 @@ -//! 备份纯策略:Cron 推进与保留候选。 +//! 备份纯策略:Cron 推进与保留候选 //! -//! 本模块不持有任何端口或对象,是 `BackupService` 与 `BackupTask` 共用的纯函数集。 +//! 本模块不持有任何端口或对象,是 `BackupService` 与 `BackupTask` 共用的纯函数集 use std::str::FromStr; @@ -10,10 +10,11 @@ use cron::Schedule; use crate::model::backup::{BackupError, BackupRecord, BackupStatus, code}; -/// 向历史回溯的最大范围(分钟)。覆盖 Feb 29 + 特定星期等罕见组合的最长间隔。 +/// 向历史回溯的最大范围(分钟) +/// 覆盖 Feb 29 + 特定星期等罕见组合的最长间隔 const MAX_BACKDATE_MINUTES: i64 = 40 * 366 * 24 * 60; -/// 已解析并校验的备份计划。 +/// 已解析并校验的备份计划 #[derive(Debug, Clone)] pub struct BackupSchedule { schedule: Schedule, @@ -21,11 +22,11 @@ pub struct BackupSchedule { } impl BackupSchedule { - /// 按部署时区解析并校验 5 段 Cron。 + /// 按部署时区解析并校验 5 段 Cron /// /// # Errors /// - /// Cron 非法时返回 [`BackupError`]。 + /// Cron 非法时返回 [`BackupError`] pub fn parse( cron_expression: &str, timezone: gateway_core::time::DeploymentTimeZone, @@ -40,16 +41,16 @@ impl BackupSchedule { }) } - /// 返回最接近 `at`(含 `at` 所在分钟)的最近一次触发时间。 + /// 返回最接近 `at`(含 `at` 所在分钟)的最近一次触发时间 /// /// 若过去 `MAX_BACKDATE_MINUTES` 内没有任何触发(极罕见的稀疏计划或超长停机), - /// 返回 `None`,调用方不应为历史点补任务。 + /// 返回 `None`,调用方不应为历史点补任务 #[must_use] pub fn last_firing_at_or_before(&self, at: DateTime) -> Option> { let high_min = at.timestamp().div_euclid(60); let low_min = high_min.saturating_sub(MAX_BACKDATE_MINUTES); - // 检查窗口内是否存在触发;不存在则直接返回 None。 + // 检查窗口内是否存在触发;不存在则直接返回 None let low_instant = instant(low_min, self.timezone)?; if self .next_after(low_instant.to_utc()) @@ -58,7 +59,7 @@ impl BackupSchedule { return None; } - // 二分查找最小的 u(分钟)使 after(u) > high;prev = after(u - 1 分钟)。 + // 二分查找最小的 u(分钟)使 after(u) > high;prev = after(u - 1 分钟) let mut lo = low_min; let mut hi = high_min; while lo < hi { @@ -77,7 +78,7 @@ impl BackupSchedule { .and_then(|firing| Utc.timestamp_opt(firing * 60, 0).single()) } - /// 返回第一个严格晚于 `at` 的触发时间。 + /// 返回第一个严格晚于 `at` 的触发时间 #[must_use] pub fn next_after(&self, at: DateTime) -> Option> { let local = at.with_timezone(&self.timezone).naive_local().and_utc(); @@ -91,7 +92,7 @@ impl BackupSchedule { }) } - /// `after(u 分钟)` 返回的第一个触发时间(分钟索引),用于二分搜索。 + /// `after(u 分钟)` 返回的第一个触发时间(分钟索引),用于二分搜索 fn firing_after(&self, minute_index: i64) -> Option { let instant = instant(minute_index, self.timezone)?.to_utc(); self.next_after(instant) @@ -99,34 +100,34 @@ impl BackupSchedule { } } -/// 构造指定分钟索引(UTC epoch 分钟)在目标时区下的 `DateTime`。 +/// 构造指定分钟索引(UTC epoch 分钟)在目标时区下的 `DateTime` fn instant(minute_index: i64, timezone: Tz) -> Option> { timezone.timestamp_opt(minute_index * 60, 0).single() } -/// 一条记录被纳入删除流程的原因。 +/// 一条记录被纳入删除流程的原因 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RetentionReason { - /// 超过 `retention_days`。 + /// 超过 `retention_days` Days, - /// 超过 `retention_count`。 + /// 超过 `retention_count` Count, } -/// 保留决策。 +/// 保留决策 #[derive(Debug, Clone, PartialEq, Eq)] pub struct RetentionDecision { pub record_id: String, pub reason: RetentionReason, } -/// 从按完成时间倒序的 completed 计划备份中决定纳入删除流程的记录。 +/// 从按完成时间倒序的 completed 计划备份中决定纳入删除流程的记录 /// /// 自动保留规则: -/// - `retentionDays = 0` 时不按天数清理;`retentionCount = 0` 时不按份数清理。 -/// - 自动计划备份满足任一启用阈值即可进入删除。 -/// - 无论阈值如何,至少保留最近一个成功的计划备份(倒序下标 0)。 -/// - 手动备份不进入本规则;手工/计划记录各自的 `expires_at` 由到期扫描处理。 +/// - `retentionDays = 0` 时不按天数清理;`retentionCount = 0` 时不按份数清理 +/// - 自动计划备份满足任一启用阈值即可进入删除 +/// - 无论阈值如何,至少保留最近一个成功的计划备份(倒序下标 0) +/// - 手动备份不进入本规则;手工/计划记录各自的 `expires_at` 由到期扫描处理 #[must_use] pub fn decide_retention( retention_days: u32, @@ -139,7 +140,7 @@ pub fn decide_retention( if record.status != BackupStatus::Completed { continue; } - // 最近一个成功的计划备份永远保留。 + // 最近一个成功的计划备份永远保留 if index == 0 { continue; } diff --git a/backend/crates/gateway-admin/src/backup/task.rs b/backend/crates/gateway-admin/src/backup/task.rs index 513410af1..d2656afac 100644 --- a/backend/crates/gateway-admin/src/backup/task.rs +++ b/backend/crates/gateway-admin/src/backup/task.rs @@ -1,7 +1,8 @@ -//! Backup Worker 贡献:单个可取消 `DaemonTask` 承担调度、执行、删除收敛与保留清理。 +//! Backup Worker 贡献:单个可取消 `DaemonTask` 承担调度、执行、删除收敛与保留清理 //! -//! 当前部署边界是单副本,因此不需要 Redis lease、fencing token 或 heartbeat。长时间 -//! `pg_dump` 与上传由本 Daemon 自身持有;Host 只负责 panic 后重启、健康与关闭。 +//! 当前部署边界是单副本,因此不需要 Redis lease、fencing token 或 heartbeat +//! 长时间 +//! `pg_dump` 与上传由本 Daemon 自身持有;Host 只负责 panic 后重启、健康与关闭 use std::sync::Arc; use std::time::Duration; @@ -33,16 +34,16 @@ impl From for WorkerTaskError { use super::policy::{BackupSchedule, decide_retention}; -/// 循环间隔:管理员修改 Cron 后该时长内生效;无工作时可取消等待。 +/// 循环间隔:管理员修改 Cron 后该时长内生效;无工作时可取消等待 const BACKUP_LOOP_INTERVAL: Duration = Duration::from_secs(30); -/// 每个循环最多执行的保留删除数量。 +/// 每个循环最多执行的保留删除数量 const RETENTION_BATCH_SIZE: usize = 10; -/// 保留扫描一次读取的 completed 计划备份上限。 +/// 保留扫描一次读取的 completed 计划备份上限 const RETENTION_SCAN_LIMIT: u32 = 1000; -/// 每个循环最多完成的待删除记录数量。 +/// 每个循环最多完成的待删除记录数量 const PENDING_DELETION_BATCH: u32 = 20; -/// 备份 Daemon 任务。 +/// 备份 Daemon 任务 pub struct BackupTask { timezone: gateway_core::time::DeploymentTimeZone, repository: Arc, @@ -51,7 +52,7 @@ pub struct BackupTask { } impl BackupTask { - /// 组合仓储、导出器与对象存储适配器。 + /// 组合仓储、导出器与对象存储适配器 #[must_use] pub fn new( repository: Arc, @@ -94,13 +95,13 @@ impl BackupTask { } } - /// 执行一个周期:推进计划、恢复中间态、删除收敛、领取执行一个任务、保留清理。 + /// 执行一个周期:推进计划、恢复中间态、删除收敛、领取执行一个任务、保留清理 /// - /// 对外暴露以便集成测试逐周期驱动;daemon 循环内部复用。 + /// 对外暴露以便集成测试逐周期驱动;daemon 循环内部复用 /// /// # Errors /// - /// 仓储或基础设施不可用、任务执行失败时返回 [`WorkerTaskError`]。 + /// 仓储或基础设施不可用、任务执行失败时返回 [`WorkerTaskError`] pub async fn run_cycle(&self, cancellation: &CancellationToken) -> Result<(), WorkerTaskError> { if cancellation.is_cancelled() { return Ok(()); @@ -113,7 +114,7 @@ impl BackupTask { Ok(()) } - /// 推进到期计划并幂等插入 scheduled 任务。 + /// 推进到期计划并幂等插入 scheduled 任务 async fn advance_schedule(&self, now: DateTime) -> Result<(), WorkerTaskError> { let settings = self.repository.load_settings().await.map_err(repo_error)?; if !settings.schedule_enabled { @@ -128,7 +129,7 @@ impl BackupTask { .map_err(|_| WorkerTaskError::safe("backup schedule is invalid"))?; if settings.schedule_timezone.as_deref() != Some(timezone) { - // 时区切换只初始化未来游标,不把旧时区计划当作应补跑任务。 + // 时区切换只初始化未来游标,不把旧时区计划当作应补跑任务 if let Some(next_run_at) = schedule.next_after(now) { self.repository .advance_schedule_cursor( @@ -147,7 +148,7 @@ impl BackupTask { Some(next_run_at) if next_run_at <= now => true, Some(_) => false, None => { - // 刚启用计划但尚无游标:直接初始化。 + // 刚启用计划但尚无游标:直接初始化 if let Some(next_run_at) = schedule.next_after(now) { let _ = self .repository @@ -171,7 +172,7 @@ impl BackupTask { let scheduled_at = match schedule.last_firing_at_or_before(now) { Some(scheduled_at) => scheduled_at, None => { - // 无法从计划推导最近到期时间:推进游标避免反复触发。 + // 无法从计划推导最近到期时间:推进游标避免反复触发 if let Some(next_run_at) = schedule.next_after(now) { let _ = self .repository @@ -221,7 +222,7 @@ impl BackupTask { Ok(()) } - /// 恢复中间状态(dumping/uploading/deleting)。 + /// 恢复中间状态(dumping/uploading/deleting) async fn recover_intermediate( &self, cancellation: &CancellationToken, @@ -241,7 +242,7 @@ impl BackupTask { Ok(()) } - /// dumping:有完整暂存则继续上传,否则清理暂存并标记失败。 + /// dumping:有完整暂存则继续上传,否则清理暂存并标记失败 async fn recover_dumping( &self, record: &BackupRecord, @@ -287,7 +288,7 @@ impl BackupTask { Ok(()) } - /// uploading:远端匹配则补记完成,有完整暂存则重试上传,否则失败。 + /// uploading:远端匹配则补记完成,有完整暂存则重试上传,否则失败 async fn recover_uploading( &self, record: &BackupRecord, @@ -339,7 +340,7 @@ impl BackupTask { } } - /// 完成待删除记录(deleting → DeleteObject → 硬删除)。 + /// 完成待删除记录(deleting → DeleteObject → 硬删除) async fn finish_pending_deletions(&self) -> Result<(), WorkerTaskError> { let settings = self.repository.load_settings().await.map_err(repo_error)?; let Some(config) = BackupStorageConfig::from_settings(&settings) else { @@ -366,7 +367,7 @@ impl BackupTask { Ok(()) } - /// 领取一个 queued 任务并执行导出、上传与远端校验。 + /// 领取一个 queued 任务并执行导出、上传与远端校验 async fn process_next_task( &self, cancellation: &CancellationToken, @@ -391,7 +392,7 @@ impl BackupTask { Ok(()) } - /// 执行单个任务;内部处理全部状态迁移。 + /// 执行单个任务;内部处理全部状态迁移 async fn execute_task( &self, record: &BackupRecord, @@ -425,7 +426,7 @@ impl BackupTask { } }; - // dumping → uploading,持久化 size/sha256。 + // dumping → uploading,持久化 size/sha256 let update = StatusTransitionUpdate { size_bytes: Some(artifact.size_bytes), sha256: Some(artifact.sha256.clone()), @@ -458,7 +459,7 @@ impl BackupTask { Ok(()) } - /// 上传 + 远端校验,内部完成终态迁移。 + /// 上传 + 远端校验,内部完成终态迁移 async fn upload_and_verify( &self, record: &BackupRecord, @@ -563,7 +564,7 @@ impl BackupTask { Ok(()) } - /// 把活跃任务写入失败终态并清理暂存。 + /// 把活跃任务写入失败终态并清理暂存 async fn fail_task( &self, record: &BackupRecord, @@ -598,11 +599,11 @@ impl BackupTask { Ok(()) } - /// 执行一小批到期保留清理:先处理 `expires_at` 已到期的记录,再按天数/份数清理计划备份。 + /// 执行一小批到期保留清理:先处理 `expires_at` 已到期的记录,再按天数/份数清理计划备份 async fn run_retention_batch(&self, now: DateTime) -> Result<(), WorkerTaskError> { let settings = self.repository.load_settings().await.map_err(repo_error)?; - // 1. expires_at 已到期的记录(手动或计划)无条件进入删除。 + // 1. expires_at 已到期的记录(手动或计划)无条件进入删除 let expired = self .repository .list_expired_records(RETENTION_BATCH_SIZE as u32) @@ -620,7 +621,7 @@ impl BackupTask { self.finalize_deletion(&record).await?; } - // 2. 计划备份按 retentionDays / retentionCount 清理。 + // 2. 计划备份按 retentionDays / retentionCount 清理 let records = self .repository .list_scheduled_completed_desc(RETENTION_SCAN_LIMIT) @@ -646,7 +647,7 @@ impl BackupTask { Ok(()) } - /// 对已进入 deleting 的记录执行对象删除并硬删除记录行。 + /// 对已进入 deleting 的记录执行对象删除并硬删除记录行 async fn finalize_deletion(&self, record: &BackupRecord) -> Result<(), WorkerTaskError> { let settings = self.repository.load_settings().await.map_err(repo_error)?; let Some(config) = BackupStorageConfig::from_settings(&settings) else { diff --git a/backend/crates/gateway-admin/src/freeze_recovery.rs b/backend/crates/gateway-admin/src/freeze_recovery.rs index b3488b99d..f603023a0 100644 --- a/backend/crates/gateway-admin/src/freeze_recovery.rs +++ b/backend/crates/gateway-admin/src/freeze_recovery.rs @@ -1,8 +1,9 @@ -//! 容量熔断冻结的恢复编排:到期探测、失败顺延与自适应并发下调。 +//! 容量熔断冻结的恢复编排:到期探测、失败顺延与自适应并发下调 //! //! 冻结事实保存在可丢失的 Redis 冷却中,本服务只组合 Admin 端口: //! 观测(`AccountRuntimeStore`)、探测(`AccountsService`)与写回 -//! (原子降低并发 / 按冻结代次结束探测)。旧观测不会覆盖管理员操作。 +//! (原子降低并发 / 按冻结代次结束探测) +//! 旧观测不会覆盖管理员操作 use std::collections::BTreeMap; use std::sync::Arc; @@ -22,21 +23,21 @@ use crate::model::{MutationActor, MutationContext}; use crate::ports::store::{AccountRuntimeStore, AccountStore, SettingsStore}; use crate::use_case::accounts::AccountsService; -/// 自适应并发下调保留系数:下调到观测在途峰值的 80%。 +/// 自适应并发下调保留系数:下调到观测在途峰值的 80% const ADAPTIVE_CONCURRENCY_FACTOR: f64 = 0.8; -/// 自适应并发下限:过低的并发让账号几乎不可用,宁可保持冻结。 +/// 自适应并发下限:过低的并发让账号几乎不可用,宁可保持冻结 const ADAPTIVE_CONCURRENCY_FLOOR: u32 = 2; -/// worker 周期;冷却观测与探测都依赖该节奏。 +/// worker 周期;冷却观测与探测都依赖该节奏 pub const FREEZE_RECOVERY_INTERVAL: Duration = Duration::from_secs(30); pub const FREEZE_RECOVERY_WORKER_OWNER: &str = "account-freeze-recovery"; pub const WORKER_INITIAL_BACKOFF: Duration = Duration::from_secs(1); pub const WORKER_MAXIMUM_BACKOFF: Duration = Duration::from_secs(60); pub const WORKER_LEASE_TTL: Duration = Duration::from_secs(15 * 60); pub const WORKER_LEASE_RENEWAL: Duration = Duration::from_secs(5 * 60); -/// 系统审计上下文的固定请求 ID;账号并发更新审计由此溯源到自动冻结编排。 +/// 系统审计上下文的固定请求 ID;账号并发更新审计由此溯源到自动冻结编排 const SYSTEM_REQUEST_ID: &str = "account-freeze-recovery"; -/// 冻结恢复所需的管理端口组合。 +/// 冻结恢复所需的管理端口组合 #[derive(Clone)] pub struct FreezeRecoveryDeps { pub accounts: Arc, @@ -61,7 +62,7 @@ impl FreezeRecoveryTask { } } - /// 读取冻结策略;读取或校验失败跳过本轮,不能把未知配置当作允许解冻。 + /// 读取冻结策略;读取或校验失败跳过本轮,不能把未知配置当作允许解冻 async fn freeze_policy(&self) -> Option { let settings = self.deps.settings.load_runtime_settings().await.ok()?; gateway_core::provider_ports::ProviderFreezePolicy::try_new( @@ -95,7 +96,7 @@ impl FreezeRecoveryTask { } /// 对冻结中的账号执行自适应并发下调:目标为观测在途峰值的 80%(下限 2), - /// 只降不升;未观测到峰值证据的账号保持现状。 + /// 只降不升;未观测到峰值证据的账号保持现状 async fn adapt_concurrency_limits( &self, freezes: &BTreeMap, @@ -146,7 +147,7 @@ impl FreezeRecoveryTask { .ok() } - /// 到达探测时间后才恢复;关闭自动冻结或探测时,已有冻结仍等待原冷却结束。 + /// 到达探测时间后才恢复;关闭自动冻结或探测时,已有冻结仍等待原冷却结束 async fn recover_due_freezes( &self, freezes: &BTreeMap, @@ -179,14 +180,14 @@ impl FreezeRecoveryTask { return; }; if !item.account.enabled { - // 停用账号没有自动恢复意义;解冻交给管理员手动恢复。 + // 停用账号没有自动恢复意义;解冻交给管理员手动恢复 return; } let Ok(account) = ProviderAccountId::new(account_id.to_owned()) else { return; }; let Some(model) = self.resolve_probe_model(&account, policy).await else { - // 没有可用探测模型时按失败处理,顺延冻结等待下一轮。 + // 没有可用探测模型时按失败处理,顺延冻结等待下一轮 self.postpone(&account, freeze, policy).await; return; }; @@ -218,7 +219,7 @@ impl FreezeRecoveryTask { } } - /// 探测失败只顺延本次仍存在的冻结,旧探测不能重新冻结已手动恢复的账号。 + /// 探测失败只顺延本次仍存在的冻结,旧探测不能重新冻结已手动恢复的账号 async fn postpone( &self, account: &ProviderAccountId, @@ -281,7 +282,7 @@ impl ScheduledTask for FreezeRecoveryTask { } } -/// 自适应目标并发:观测峰值 × 0.8 向下取整,下限 2;无证据(峰值为 0)不调整。 +/// 自适应目标并发:观测峰值 × 0.8 向下取整,下限 2;无证据(峰值为 0)不调整 fn adaptive_target(peak_in_flight: u32) -> Option { if peak_in_flight == 0 { return None; @@ -290,7 +291,7 @@ fn adaptive_target(peak_in_flight: u32) -> Option { Some(scaled.max(ADAPTIVE_CONCURRENCY_FLOOR)) } -/// 消耗连接测试事件流并返回是否以 `Completed` 终止。 +/// 消耗连接测试事件流并返回是否以 `Completed` 终止 async fn drain_probe(mut events: crate::model::accounts::AccountConnectionTestEventStream) -> bool { let mut completed = false; while let Some(event) = events.next().await { diff --git a/backend/crates/gateway-admin/src/lib.rs b/backend/crates/gateway-admin/src/lib.rs index b5c065b71..2ee5f305e 100644 --- a/backend/crates/gateway-admin/src/lib.rs +++ b/backend/crates/gateway-admin/src/lib.rs @@ -1,6 +1,6 @@ -//! 管理控制面的语义模型、用例与外部能力端口。 +//! 管理控制面的语义模型、用例与外部能力端口 //! -//! 本 crate 不包含 HTTP wire、数据库实现或具体 Provider 实现。 +//! 本 crate 不包含 HTTP wire、数据库实现或具体 Provider 实现 use std::{fmt, path::Path, sync::Arc, time::Duration}; @@ -70,7 +70,7 @@ const WEAK_ADMIN_PASSWORDS: &[&str] = &[ const BACKUP_WORKER_OWNER: &str = "backup"; const DEFAULT_CLIENT_SESSION_TTL_MINUTES: u64 = 24 * 60; -/// 只用于首次幂等创建默认管理员的启动密码。 +/// 只用于首次幂等创建默认管理员的启动密码 #[derive(Clone, Deserialize)] #[serde(transparent)] pub struct InitialAdminPassword(SecretString); @@ -100,7 +100,7 @@ impl fmt::Debug for InitialAdminPassword { } } -/// 管理控制面的启动配置。 +/// 管理控制面的启动配置 #[derive(Clone, Deserialize, PartialEq, Eq)] pub struct AdminConfig { pub session_ttl_minutes: u64, @@ -108,7 +108,7 @@ pub struct AdminConfig { pub default_password: InitialAdminPassword, } -/// Client 登录域的通用启动配置。 +/// Client 登录域的通用启动配置 #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] pub struct ClientConfig { pub session_ttl_minutes: u64, @@ -123,11 +123,11 @@ impl Default for ClientConfig { } impl ClientConfig { - /// 校验 Client session TTL;当前配置不含相对路径。 + /// 校验 Client session TTL;当前配置不含相对路径 /// /// # Errors /// - /// 会话有效期为零或无法安全换算时返回错误。 + /// 会话有效期为零或无法安全换算时返回错误 pub fn resolve_and_validate(&mut self, _source_dir: &Path) -> Result<(), AdminConfigError> { if self.session_ttl_minutes == 0 || i64::try_from(self.session_ttl_minutes).is_err() { return Err(AdminConfigError::InvalidField("client.session_ttl_minutes")); @@ -137,11 +137,11 @@ impl ClientConfig { } impl AdminConfig { - /// 校验 Admin-owned 字段;当前配置不含相对路径。 + /// 校验 Admin-owned 字段;当前配置不含相对路径 /// /// # Errors /// - /// 用户名、会话有效期或初始密码不满足安全约束时返回错误。 + /// 用户名、会话有效期或初始密码不满足安全约束时返回错误 pub fn resolve_and_validate(&mut self, _source_dir: &Path) -> Result<(), AdminConfigError> { if self.default_username.trim().is_empty() || self.default_username.chars().any(char::is_control) @@ -173,7 +173,7 @@ impl fmt::Debug for AdminConfig { } } -/// Admin-owned 启动配置错误;不回显任何配置值。 +/// Admin-owned 启动配置错误;不回显任何配置值 #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] pub enum AdminConfigError { #[error("配置字段 `{0}` 不合法")] @@ -182,9 +182,9 @@ pub enum AdminConfigError { WeakInitialPassword, } -/// API 持有的管理资源能力集合。 +/// API 持有的管理资源能力集合 /// -/// 字段全部私有;调用方经 accessor 直接调用能力,不需要命名内部 `use_case` 模块。 +/// 字段全部私有;调用方经 accessor 直接调用能力,不需要命名内部 `use_case` 模块 #[derive(Clone)] pub struct AdminServices { timezone: gateway_core::time::DeploymentTimeZone, @@ -237,7 +237,7 @@ impl AdminServices { self.key_usage.as_ref() } - /// 取得账号服务的共享句柄;后台编排(冻结恢复 worker)需要持有 Arc。 + /// 取得账号服务的共享句柄;后台编排(冻结恢复 worker)需要持有 Arc #[must_use] pub fn accounts_handle(&self) -> Arc { Arc::clone(&self.accounts) @@ -292,7 +292,7 @@ impl AdminServices { self.credentials.as_ref() } - /// Runtime 只持有该窄端口的 Weak;AdminBundle 保持实际生命周期。 + /// Runtime 只持有该窄端口的 Weak;AdminBundle 保持实际生命周期 #[must_use] pub fn plugin_accounts_handle(&self) -> Arc { Arc::clone(&self.plugin_accounts) @@ -304,7 +304,7 @@ impl AdminServices { } } -/// Admin 初始化完成后的封闭能力包。 +/// Admin 初始化完成后的封闭能力包 pub struct AdminBundle { services: AdminServices, worker_contributions: Vec, @@ -316,13 +316,13 @@ impl AdminBundle { self.services.clone() } - /// 取出 Admin Worker 贡献;只能调用一次,与其它 Bundle 的贡献一并交给 Host。 + /// 取出 Admin Worker 贡献;只能调用一次,与其它 Bundle 的贡献一并交给 Host pub fn take_worker_contributions(&mut self) -> Vec { std::mem::take(&mut self.worker_contributions) } } -/// 组合根提供给控制面的运行能力;与配置和存储端口分别传入。 +/// 组合根提供给控制面的运行能力;与配置和存储端口分别传入 pub struct AdminRuntimePorts { pub timezone: gateway_core::time::DeploymentTimeZone, pub service_middleware: service::PlanSource, @@ -341,11 +341,11 @@ pub struct AdminRuntimePorts { pub client_key_verifier: Arc, } -/// 校验配置、接入已组装的 Provider 注册表并完成默认管理员幂等初始化。 +/// 校验配置、接入已组装的 Provider 注册表并完成默认管理员幂等初始化 /// /// # Errors /// -/// 配置非法或默认管理员初始化失败时返回错误。 +/// 配置非法或默认管理员初始化失败时返回错误 pub async fn initialize( config: AdminConfig, client_config: ClientConfig, @@ -355,7 +355,7 @@ pub async fn initialize( initialize_inner(config, client_config, store, runtime, None).await } -/// 使用组合根已绑定给 Runtime 的同一账号窄端口,避免为完整 Admin 重建第二实例。 +/// 使用组合根已绑定给 Runtime 的同一账号窄端口,避免为完整 Admin 重建第二实例 pub async fn initialize_with_plugin_accounts( config: AdminConfig, client_config: ClientConfig, @@ -541,7 +541,7 @@ async fn initialize_inner( }) } -/// CLI 只组合账号用例,不初始化管理员、管理服务或后台任务;写入仍复用同一事务与审计。 +/// CLI 只组合账号用例,不初始化管理员、管理服务或后台任务;写入仍复用同一事务与审计 pub fn initialize_plugin_accounts( providers: ports::provider::ProviderAdminRegistry, accounts: Arc, @@ -552,7 +552,7 @@ pub fn initialize_plugin_accounts( )) } -/// 为 Runtime 创建非秘密 Client Key 目录与预算重置的窄端口。 +/// 为 Runtime 创建非秘密 Client Key 目录与预算重置的窄端口 #[must_use] pub fn initialize_plugin_client_keys( providers: ports::provider::ProviderAdminRegistry, @@ -564,7 +564,7 @@ pub fn initialize_plugin_client_keys( Arc::new(use_case::plugin_client_keys::DefaultPluginClientKeyAccess::new(service)) } -/// 为 Runtime 组合实例自有资源写入;权限和归属在同一存储事务复核。 +/// 为 Runtime 组合实例自有资源写入;权限和归属在同一存储事务复核 #[must_use] pub fn initialize_plugin_resources( store: Arc, @@ -573,7 +573,7 @@ pub fn initialize_plugin_resources( Arc::new(use_case::plugin_resources::DefaultPluginResourceAccess { store, snapshot }) } -/// Backup Worker 注册:单个可取消 Daemon,owner 固定为 `backup`。 +/// Backup Worker 注册:单个可取消 Daemon,owner 固定为 `backup` fn backup_worker_contribution( task: backup::task::BackupTask, ) -> Result, AdminError> { @@ -592,7 +592,7 @@ fn backup_worker_contribution( Ok(vec![WorkerContribution::Registration(registration)]) } -/// 冻结恢复 Worker 注册:按固定周期扫描活跃冻结,owner 固定。 +/// 冻结恢复 Worker 注册:按固定周期扫描活跃冻结,owner 固定 fn freeze_recovery_worker_contribution( task: freeze_recovery::FreezeRecoveryTask, ) -> Result, AdminError> { @@ -623,7 +623,7 @@ fn freeze_recovery_worker_contribution( Ok(vec![WorkerContribution::Registration(registration)]) } -/// 设置服务不依赖 Web 管理会话,CLI 与服务器通过同一用例执行事务和发布。 +/// 设置服务不依赖 Web 管理会话,CLI 与服务器通过同一用例执行事务和发布 pub fn initialize_settings( store: Arc, snapshot: Arc, diff --git a/backend/crates/gateway-admin/src/model/account_groups.rs b/backend/crates/gateway-admin/src/model/account_groups.rs index c56310bde..97e915bfa 100644 --- a/backend/crates/gateway-admin/src/model/account_groups.rs +++ b/backend/crates/gateway-admin/src/model/account_groups.rs @@ -1,4 +1,4 @@ -//! Provider-neutral account group commands and projections. +//! 跨 Provider 账号分组的管理命令与查询投影 use std::collections::BTreeMap; @@ -7,7 +7,7 @@ use gateway_core::{account::AccountStatusFacts, routing::AccountGroupId}; use super::{PageSize, Revision, observability::DecimalAmount}; -/// Canonical `#RRGGBBAA` color persisted with an account group. +/// 账号分组持久化使用的标准 `#RRGGBBAA` 颜色 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AccountGroupColor(String); @@ -26,7 +26,7 @@ impl AccountGroupColor { } } -/// Group member availability at the current observation time. +/// 当前观测时刻的分组成员可用性 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct AccountGroupAccountSummary { pub available: u64, @@ -34,23 +34,23 @@ pub struct AccountGroupAccountSummary { pub total: u64, } -/// Group scheduling slots derived from available accounts and runtime leases. +/// 根据可用账号与运行时租约计算的分组调度槽位 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct AccountGroupCapacity { pub used_slots: Option, - /// `None` 表示可用账号中存在不限制并发的账号。 + /// `None` 表示可用账号中存在不限制并发的账号 pub total_slots: Option, } -/// Successful, downstream-committed USD request costs for group accounts. +/// 分组账号成功且已向下游交付的请求美元费用 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AccountGroupUsage { pub today_usd: DecimalAmount, - /// 当前 usage retention 窗口内、按请求发生时 routing group 快照归属的累计成本。 + /// 当前 usage retention 窗口内、按请求发生时 routing group 快照归属的累计成本 pub retained_total_usd: DecimalAmount, } -/// 当前页账号组 membership 对应的持久账号事实;运行态在 Admin query service 合并。 +/// 当前页账号组 membership 对应的持久账号事实;运行态在 Admin query service 合并 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AccountGroupMemberFact { pub group_id: AccountGroupId, @@ -59,7 +59,7 @@ pub struct AccountGroupMemberFact { pub total_slots: Option, } -/// Lightweight group reference embedded in account and client-key views. +/// 账号与 Client Key 视图内嵌的轻量分组引用 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AccountGroupRef { pub id: AccountGroupId, @@ -68,7 +68,7 @@ pub struct AccountGroupRef { pub enabled: bool, } -/// Account group list query. +/// 账号分组列表查询 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AccountGroupListQuery { pub page: u32, @@ -77,7 +77,7 @@ pub struct AccountGroupListQuery { pub enabled: Option, } -/// Complete account group summary. +/// 完整账号分组摘要 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AccountGroupRecord { pub disable_fast: bool, @@ -96,7 +96,7 @@ pub struct AccountGroupRecord { pub updated_at: DateTime, } -/// Paginated account groups. +/// 账号分组分页结果 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AccountGroupPage { pub config_revision: Revision, @@ -106,7 +106,7 @@ pub struct AccountGroupPage { pub page_size: u16, } -/// Create an account group. +/// 创建账号分组 #[derive(Debug, Clone, PartialEq, Eq)] pub struct CreateAccountGroup { pub disable_fast: bool, @@ -115,7 +115,7 @@ pub struct CreateAccountGroup { pub color: AccountGroupColor, } -/// Store-ready create command with a generated stable ID. +/// 已生成稳定标识、可直接提交存储的创建命令 #[derive(Debug, Clone, PartialEq, Eq)] pub struct NewAccountGroup { pub disable_fast: bool, @@ -125,7 +125,7 @@ pub struct NewAccountGroup { pub color: AccountGroupColor, } -/// Update an account group's descriptive fields. +/// 更新账号分组的描述字段 #[derive(Debug, Clone, PartialEq, Eq)] pub struct UpdateAccountGroup { pub disable_fast: Option, @@ -135,20 +135,20 @@ pub struct UpdateAccountGroup { pub color: AccountGroupColor, } -/// Enable or disable an account group. +/// 启用或停用账号分组 #[derive(Debug, Clone, PartialEq, Eq)] pub struct SetAccountGroupEnabled { pub id: AccountGroupId, pub enabled: bool, } -/// Delete an account group that is not referenced by a client key. +/// 删除未被 Client Key 引用的账号分组 #[derive(Debug, Clone, PartialEq, Eq)] pub struct DeleteAccountGroup { pub id: AccountGroupId, } -/// Result of any account-group mutation. +/// 账号分组变更结果 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AccountGroupMutation { pub config_revision: Revision, diff --git a/backend/crates/gateway-admin/src/model/accounts.rs b/backend/crates/gateway-admin/src/model/accounts.rs index 86ea9810d..375eedd93 100644 --- a/backend/crates/gateway-admin/src/model/accounts.rs +++ b/backend/crates/gateway-admin/src/model/accounts.rs @@ -1,4 +1,4 @@ -//! 多 Provider 账号目录与连接测试的公共事实。 +//! 多 Provider 账号目录与连接测试的公共事实 use std::{collections::BTreeMap, pin::Pin}; @@ -18,7 +18,7 @@ pub use gateway_core::account::{ QuotaState, resolve_account_status, }; -/// 账号可用的管理操作。 +/// 账号可用的管理操作 #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub struct ProviderAccountCapabilities { pub quota: bool, @@ -30,7 +30,7 @@ pub struct ProviderAccountCapabilities { pub consume_reset_credit: bool, } -/// 导入时统一应用的账号备注、调度与分组设置;缺省时保留原有导入语义。 +/// 导入时统一应用的账号备注、调度与分组设置;缺省时保留原有导入语义 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AccountImportSettings { pub notes: Option, @@ -41,7 +41,7 @@ pub struct AccountImportSettings { pub group_ids: Vec, } -/// 账号列表排序字段。 +/// 账号列表排序字段 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AccountSortField { Email, @@ -52,21 +52,21 @@ pub enum AccountSortField { ExpiresAt, } -/// 账号列表排序方向。 +/// 账号列表排序方向 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SortDirection { Asc, Desc, } -/// 一组完整的账号排序规则。 +/// 一组完整的账号排序规则 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct AccountSort { pub field: AccountSortField, pub direction: SortDirection, } -/// 账号列表的存储查询条件。 +/// 账号列表的存储查询条件 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AccountListQuery { pub page: u32, @@ -78,15 +78,15 @@ pub struct AccountListQuery { pub sort: Option, } -/// Admin query service 从运行态存储取得的当前账号冷却快照。 +/// Admin query service 从运行态存储取得的当前账号冷却快照 #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct AccountRuntimeSnapshot { pub cooldown: BTreeMap, - /// `None` 表示实时 lease 存储不可用;`Some` 中未出现的账号当前使用量为零。 + /// `None` 表示实时 lease 存储不可用;`Some` 中未出现的账号当前使用量为零 pub in_flight: Option>, } -/// 恢复任务读取的冻结快照;generation 将异步结果绑定到本次冻结。 +/// 恢复任务读取的冻结快照;generation 将异步结果绑定到本次冻结 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AccountFreeze { pub credential_revision: Revision, @@ -95,14 +95,14 @@ pub struct AccountFreeze { pub requires_probe: bool, } -/// Optional account membership filter. +/// 可选的账号分组成员筛选条件 #[derive(Debug, Clone, PartialEq, Eq)] pub enum AccountGroupFilter { Group(gateway_core::routing::AccountGroupId), Ungrouped, } -/// 账号公共存储投影;Provider 专属字段不进入此结构。 +/// 账号公共存储投影;Provider 专属字段不进入此结构 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AccountRecord { pub id: String, @@ -133,14 +133,14 @@ pub struct AccountRecord { pub updated_at: DateTime, } -/// 单一货币的账号成本聚合。 +/// 单一货币的账号成本聚合 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AccountCost { pub currency: String, pub amount: super::observability::DecimalAmount, } -/// 账号在一个模型上的历史用量。 +/// 账号在一个模型上的历史用量 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AccountModelUsage { pub model: String, @@ -161,14 +161,14 @@ pub struct AccountModelUsage { pub last_used_at: DateTime, } -/// 账号在一个小时窗口内的请求数。 +/// 账号在一个小时窗口内的请求数 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AccountRequestBucket { pub bucket_start: DateTime, pub request_count: u64, } -/// 账号历史用量聚合。 +/// 账号历史用量聚合 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AccountUsage { pub account_id: String, @@ -191,9 +191,9 @@ pub struct AccountUsage { pub models: Vec, } -/// 某个账号在调用方指定时间窗口内的本地用量查询。 +/// 某个账号在调用方指定时间窗口内的本地用量查询 /// -/// `key` 只用于把聚合结果关联回调用方的窗口,不承载 Provider 私有语义。 +/// `key` 只用于把聚合结果关联回调用方的窗口,不承载 Provider 私有语义 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AccountUsageWindowQuery { pub account_id: String, @@ -201,7 +201,7 @@ pub struct AccountUsageWindowQuery { pub range: TimeRange, } -/// 一个账号时间窗口用量查询的结果。 +/// 一个账号时间窗口用量查询的结果 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AccountUsageWindowResult { pub account_id: String, @@ -209,7 +209,7 @@ pub struct AccountUsageWindowResult { pub usage: AccountUsage, } -/// 账号列表页所需的完整存储事实。 +/// 账号列表页所需的完整存储事实 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AccountPage { pub config_revision: Revision, @@ -218,7 +218,7 @@ pub struct AccountPage { pub summary: AccountSummary, } -/// 同一状态快照下的账号事实与唯一状态投影。 +/// 同一状态快照下的账号事实与唯一状态投影 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AccountPageItem { pub account: AccountRecord, @@ -226,18 +226,18 @@ pub struct AccountPageItem { pub capacity: AccountCapacity, } -/// 网关配置的账号并发上限与查询时的占用,不包含排队请求或上游隐藏限制。 +/// 网关配置的账号并发上限与查询时的占用,不包含排队请求或上游隐藏限制 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct AccountCapacity { - /// 实时租约读取失败时为 `None`,不能当作空闲。 + /// 实时租约读取失败时为 `None`,不能当作空闲 pub used_slots: Option, - /// 应用账号覆盖或全局默认值后的上限;`None` 表示不限。 + /// 应用账号覆盖或全局默认值后的上限;`None` 表示不限 pub total_slots: Option, } -/// 统一账号目录的全局状态计数,不受当前筛选和分页影响。 +/// 统一账号目录的全局状态计数,不受当前筛选和分页影响 /// -/// 计数与 [`AccountStatus`] 一一对应,由 store 按派生状态聚合。 +/// 计数与 [`AccountStatus`] 一一对应,由 store 按派生状态聚合 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct AccountSummary { pub total: u64, @@ -248,11 +248,11 @@ pub struct AccountSummary { pub error: u64, } -/// 账号可编辑事实的一次性替换命令。 +/// 账号可编辑事实的一次性替换命令 #[derive(Debug, Clone, PartialEq, Eq)] pub struct UpdateAccount { pub account_id: String, - /// 缺省保留备注;空字符串清空备注。 + /// 缺省保留备注;空字符串清空备注 pub notes: Option, pub enabled: bool, pub concurrency_limit: Option, @@ -262,14 +262,14 @@ pub struct UpdateAccount { pub outbound_proxy: Option, } -/// 账号更新结果。 +/// 账号更新结果 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AccountUpdateResult { pub config_revision: Revision, pub account_id: gateway_core::account::ProviderAccountId, } -/// 仅修改显式字段的批量账号设置命令。 +/// 仅修改显式字段的批量账号设置命令 #[derive(Debug, Clone, PartialEq, Eq)] pub struct BatchUpdateAccounts { pub account_ids: Vec, @@ -281,20 +281,20 @@ pub struct BatchUpdateAccounts { pub outbound_proxy: Option, } -/// 批量账号更新结果。 +/// 批量账号更新结果 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AccountsUpdateResult { pub config_revision: Revision, pub account_ids: Vec, } -/// 账号批量删除命令。 +/// 账号批量删除命令 #[derive(Debug, Clone, PartialEq, Eq)] pub struct DeleteAccounts { pub account_ids: Vec, } -/// 账号连接测试的语义事件。 +/// 账号连接测试的语义事件 #[derive(Debug, Clone, PartialEq, Eq)] pub enum AccountConnectionTestEvent { Started { @@ -323,6 +323,6 @@ pub enum AccountConnectionTestEvent { }, } -/// 每次连接测试独占的有限事件流。 +/// 每次连接测试独占的有限事件流 pub type AccountConnectionTestEventStream = Pin + Send + 'static>>; diff --git a/backend/crates/gateway-admin/src/model/audit.rs b/backend/crates/gateway-admin/src/model/audit.rs index 3ed6f230b..eb924a6d5 100644 --- a/backend/crates/gateway-admin/src/model/audit.rs +++ b/backend/crates/gateway-admin/src/model/audit.rs @@ -1,4 +1,4 @@ -//! 管理写入的审计分类;适配器只能补充事务实际写入的字段与实体标识。 +//! 管理写入的审计分类;适配器只能补充事务实际写入的字段与实体标识 use super::{ MutationActor, MutationContext, @@ -112,7 +112,7 @@ impl MutationAuditOperation { } } -/// 分类与稳定实体标识共同组成审计意图,字段差异由同一提交事务补齐。 +/// 分类与稳定实体标识共同组成审计意图,字段差异由同一提交事务补齐 #[derive(Debug, Clone, Copy)] pub struct MutationAuditIntent<'a> { pub operation: MutationAuditOperation, diff --git a/backend/crates/gateway-admin/src/model/auth.rs b/backend/crates/gateway-admin/src/model/auth.rs index f5a50d655..280c30ac8 100644 --- a/backend/crates/gateway-admin/src/model/auth.rs +++ b/backend/crates/gateway-admin/src/model/auth.rs @@ -1,30 +1,30 @@ -//! 控制面认证、统一会话和管理员安全审计事实。 +//! 控制面认证、统一会话和管理员安全审计事实 use chrono::{DateTime, Utc}; use gateway_core::{engine::execution::ClientAuthenticationError, policy::ClientApiKeyId}; use super::{MutationActor, MutationContext, Revision}; -/// 会话审计使用的稳定管理员标识。 +/// 会话审计使用的稳定管理员标识 #[must_use] pub fn admin_session_actor_ref(admin_user_id: &str) -> String { format!("admin:{admin_user_id}") } -/// 已认证的管理主体。 +/// 已认证的管理主体 #[derive(Debug, Clone, PartialEq, Eq)] pub enum AdminPrincipal { Session { admin_user_id: String, }, ApiKey, - /// 安装者完整信任的插件;身份仅由内部 HTTP 分派端口签发。 + /// 安装者完整信任的插件;身份仅由内部 HTTP 分派端口签发 Plugin { instance_id: String, }, } -/// 传给管理用例的安全请求上下文。 +/// 传给管理用例的安全请求上下文 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AdminRequestContext { pub principal: AdminPrincipal, @@ -48,7 +48,7 @@ impl AdminRequestContext { } } -/// 登录方式只决定凭据验证流程,不能直接授予会话权限。 +/// 登录方式只决定凭据验证流程,不能直接授予会话权限 #[derive(Clone, PartialEq, Eq)] pub enum LoginCommand { Admin { @@ -76,7 +76,7 @@ impl std::fmt::Debug for LoginCommand { } } -/// 登录成功后返回的会话事实。 +/// 登录成功后返回的会话事实 #[derive(Clone, PartialEq, Eq)] pub struct LoginResult { pub session_id: String, @@ -93,7 +93,7 @@ impl std::fmt::Debug for LoginResult { } } -/// 登录状态机可被 API 精确映射的失败类型。 +/// 登录状态机可被 API 精确映射的失败类型 #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] pub enum LoginError { #[error("invalid login credentials")] @@ -114,7 +114,7 @@ impl From for LoginError { } } -/// 服务端已验证的身份绑定,不保存密码或原始 API Key。 +/// 服务端已验证的身份绑定,不保存密码或原始 API Key #[derive(Debug, Clone, PartialEq, Eq)] pub enum SessionSubject { Admin { @@ -126,7 +126,7 @@ pub enum SessionSubject { }, } -/// 仅已登录管理员可以修改自己的密码。 +/// 仅已登录管理员可以修改自己的密码 pub struct ChangePassword { pub current_password: String, pub new_password: String, @@ -138,14 +138,14 @@ impl std::fmt::Debug for ChangePassword { } } -/// 两种登录方式共用的固定有效期会话。 +/// 两种登录方式共用的固定有效期会话 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AuthSession { pub subject: SessionSubject, pub expires_at: DateTime, } -/// 安全审计事件类型。 +/// 安全审计事件类型 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AuditActorKind { AdminSession, @@ -154,7 +154,7 @@ pub enum AuditActorKind { Anonymous, } -/// 管理写操作留下的最小审计事实。 +/// 管理写操作留下的最小审计事实 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AdminAuditEvent { pub id: String, diff --git a/backend/crates/gateway-admin/src/model/backup/code.rs b/backend/crates/gateway-admin/src/model/backup/code.rs index 58e9da224..ce6294fd2 100644 --- a/backend/crates/gateway-admin/src/model/backup/code.rs +++ b/backend/crates/gateway-admin/src/model/backup/code.rs @@ -1,33 +1,33 @@ -//! 备份领域的稳定错误码。 +//! 备份领域的稳定错误码 -/// 配置或参数非法。 +/// 配置或参数非法 pub const INVALID_CONFIG: &str = "backup.invalid_config"; -/// Cron 表达式非法。 +/// Cron 表达式非法 pub const INVALID_CRON: &str = "backup.invalid_cron"; -/// IANA 时区非法。 -/// 记录不存在。 +/// IANA 时区非法 +/// 记录不存在 pub const RECORD_NOT_FOUND: &str = "backup.record_not_found"; -/// 已存在活跃任务,拒绝创建。 +/// 已存在活跃任务,拒绝创建 pub const ACTIVE_TASK_CONFLICT: &str = "backup.active_task_conflict"; -/// 记录状态与期望不符,无法迁移。 +/// 记录状态与期望不符,无法迁移 pub const STATE_CONFLICT: &str = "backup.state_conflict"; -/// 已有记录时禁止切换存储目标。 +/// 已有记录时禁止切换存储目标 pub const STORAGE_IDENTITY_LOCKED: &str = "backup.storage_identity_locked"; -/// `pg_dump` 非零退出或失败。 +/// `pg_dump` 非零退出或失败 pub const PG_DUMP_FAILED: &str = "backup.pg_dump_failed"; -/// 暂存磁盘空间不足。 +/// 暂存磁盘空间不足 pub const STAGING_SPACE_EXHAUSTED: &str = "backup.staging_space_exhausted"; -/// S3 认证失败。 +/// S3 认证失败 pub const S3_AUTH_FAILED: &str = "backup.s3_auth_failed"; -/// S3 权限拒绝。 +/// S3 权限拒绝 pub const S3_PERMISSION_DENIED: &str = "backup.s3_permission_denied"; -/// S3 上传失败。 +/// S3 上传失败 pub const S3_UPLOAD_FAILED: &str = "backup.s3_upload_failed"; -/// 远端对象校验失败。 +/// 远端对象校验失败 pub const REMOTE_VERIFICATION_FAILED: &str = "backup.remote_verification_failed"; -/// 任务被取消。 +/// 任务被取消 pub const CANCELLED: &str = "backup.cancelled"; -/// 仓储或对象存储暂不可用。 +/// 仓储或对象存储暂不可用 pub const STORE_UNAVAILABLE: &str = "backup.store_unavailable"; -/// 上游 S3 服务返回无效或失败响应。 +/// 上游 S3 服务返回无效或失败响应 pub const UPSTREAM_FAILURE: &str = "backup.upstream_failure"; diff --git a/backend/crates/gateway-admin/src/model/backup/mod.rs b/backend/crates/gateway-admin/src/model/backup/mod.rs index f40e800f1..3b3c27dcd 100644 --- a/backend/crates/gateway-admin/src/model/backup/mod.rs +++ b/backend/crates/gateway-admin/src/model/backup/mod.rs @@ -1,8 +1,10 @@ -//! S3 备份领域模型、状态机与稳定错误。 +//! S3 备份领域模型、状态机与稳定错误 //! -//! 领域类型只携带事实,不承载基础设施细节。Secret 使用 [`secrecy::SecretString`], -//! 全流程不实现 `Serialize`,避免被错误地写入 wire 或日志。管理员身份等审计事实 -//! 只存在于 `admin_audit_events`,本模型不重复保存。 +//! 领域类型只携带事实,不承载基础设施细节 +//! Secret 使用 [`secrecy::SecretString`], +//! 全流程不实现 `Serialize`,避免被错误地写入 wire 或日志 +//! 管理员身份等审计事实 +//! 只存在于 `admin_audit_events`,本模型不重复保存 use std::fmt; @@ -12,7 +14,7 @@ use uuid::Uuid; use super::PageSize; -/// 备份触发来源。 +/// 备份触发来源 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BackupTriggerKind { Manual, @@ -20,7 +22,7 @@ pub enum BackupTriggerKind { } impl BackupTriggerKind { - /// 稳定持久化值。 + /// 稳定持久化值 #[must_use] pub const fn as_str(self) -> &'static str { match self { @@ -29,7 +31,7 @@ impl BackupTriggerKind { } } - /// 解析持久化值。 + /// 解析持久化值 #[must_use] pub fn parse(value: &str) -> Option { match value { @@ -46,11 +48,11 @@ impl fmt::Display for BackupTriggerKind { } } -/// 备份任务状态机(六状态)。 +/// 备份任务状态机(六状态) /// /// 迁移 `backup_records_lifecycle_ck` 与字段空值语义共同定义合法持久化行; -/// [`Self::allows_transition_to`] 只约束领域内的显式状态迁移,两层都必须满足。 -/// 删除成功后记录行被硬删除,因此不存在 `deleted` 终态。 +/// [`Self::allows_transition_to`] 只约束领域内的显式状态迁移,两层都必须满足 +/// 删除成功后记录行被硬删除,因此不存在 `deleted` 终态 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum BackupStatus { Queued, @@ -62,7 +64,7 @@ pub enum BackupStatus { } impl BackupStatus { - /// 稳定持久化值。 + /// 稳定持久化值 #[must_use] pub const fn as_str(self) -> &'static str { match self { @@ -75,7 +77,7 @@ impl BackupStatus { } } - /// 解析持久化值。 + /// 解析持久化值 #[must_use] pub fn parse(value: &str) -> Option { match value { @@ -89,19 +91,19 @@ impl BackupStatus { } } - /// 该状态是否消耗全局活跃名额。 + /// 该状态是否消耗全局活跃名额 #[must_use] pub const fn is_active(self) -> bool { matches!(self, Self::Queued | Self::Dumping | Self::Uploading) } - /// 该状态是否允许进入删除流程。 + /// 该状态是否允许进入删除流程 #[must_use] pub const fn can_be_deleted(self) -> bool { matches!(self, Self::Completed | Self::Failed) } - /// 领域内允许的显式迁移。 + /// 领域内允许的显式迁移 #[must_use] pub fn allows_transition_to(self, target: BackupStatus) -> bool { matches!( @@ -124,7 +126,7 @@ impl fmt::Display for BackupStatus { } } -/// 已通过领域状态机校验的显式迁移。 +/// 已通过领域状态机校验的显式迁移 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct BackupStatusTransition { from: BackupStatus, @@ -132,26 +134,26 @@ pub struct BackupStatusTransition { } impl BackupStatusTransition { - /// 校验并构造显式迁移;非法状态边返回 `None`。 + /// 校验并构造显式迁移;非法状态边返回 `None` #[must_use] pub fn try_new(from: BackupStatus, to: BackupStatus) -> Option { from.allows_transition_to(to).then_some(Self { from, to }) } - /// 迁移要求的当前状态。 + /// 迁移要求的当前状态 #[must_use] pub const fn from(self) -> BackupStatus { self.from } - /// 迁移完成后的目标状态。 + /// 迁移完成后的目标状态 #[must_use] pub const fn to(self) -> BackupStatus { self.to } } -/// S3 存储配置更新命令;`secret_access_key` 为 `None` 表示保留旧值。 +/// S3 存储配置更新命令;`secret_access_key` 为 `None` 表示保留旧值 #[derive(Clone)] pub struct UpdateBackupStorageCommand { pub endpoint: String, @@ -178,7 +180,7 @@ impl fmt::Debug for UpdateBackupStorageCommand { } } -/// 调度配置更新命令。 +/// 调度配置更新命令 #[derive(Debug, Clone, PartialEq, Eq)] pub struct UpdateBackupScheduleCommand { pub schedule_enabled: bool, @@ -187,9 +189,9 @@ pub struct UpdateBackupScheduleCommand { pub retention_count: u32, } -/// 完整保存的备份设置(含 Secret)。 +/// 完整保存的备份设置(含 Secret) /// -/// 只从仓储读取;不实现 `Serialize`,防止 Secret 进入 wire 或日志。 +/// 只从仓储读取;不实现 `Serialize`,防止 Secret 进入 wire 或日志 #[derive(Clone)] pub struct BackupSettings { pub storage_revision: u64, @@ -202,7 +204,7 @@ pub struct BackupSettings { pub force_path_style: bool, pub schedule_enabled: bool, pub cron_expression: Option, - /// 记录持久化游标采用的时区,由服务端维护,不是独立部署设置。 + /// 记录持久化游标采用的时区,由服务端维护,不是独立部署设置 pub schedule_timezone: Option, pub retention_days: u32, pub retention_count: u32, @@ -236,7 +238,7 @@ impl fmt::Debug for BackupSettings { } impl BackupSettings { - /// S3 存储配置是否完整(bucket/region/密钥/前缀齐全)。 + /// S3 存储配置是否完整(bucket/region/密钥/前缀齐全) #[must_use] pub fn storage_configured(&self) -> bool { self.endpoint.is_some() @@ -247,14 +249,14 @@ impl BackupSettings { && self.prefix.is_some() } - /// 探测是否已对当前 revision 成功;`last_verified_at` 非空即表示当前配置已通过探针。 + /// 探测是否已对当前 revision 成功;`last_verified_at` 非空即表示当前配置已通过探针 #[must_use] pub fn storage_verified(&self) -> bool { self.last_verified_at.is_some() } } -/// 已经校验并可用于对象存储操作的存储配置快照。 +/// 已经校验并可用于对象存储操作的存储配置快照 #[derive(Clone)] pub struct BackupStorageConfig { pub storage_revision: u64, @@ -284,7 +286,7 @@ impl fmt::Debug for BackupStorageConfig { } impl BackupStorageConfig { - /// 从完整设置推导对象存储配置;设置不完整时返回 `None`。 + /// 从完整设置推导对象存储配置;设置不完整时返回 `None` #[must_use] pub fn from_settings(settings: &BackupSettings) -> Option { Some(Self { @@ -300,7 +302,7 @@ impl BackupStorageConfig { } } -/// 一条备份记录。 +/// 一条备份记录 #[derive(Debug, Clone, PartialEq, Eq)] pub struct BackupRecord { pub id: String, @@ -315,13 +317,13 @@ pub struct BackupRecord { pub error_message: Option, pub started_at: Option>, pub completed_at: Option>, - /// 创建时确定的过期时间;手工和计划备份均可设置,到期自动清理。 + /// 创建时确定的过期时间;手工和计划备份均可设置,到期自动清理 pub expires_at: Option>, pub created_at: DateTime, pub updated_at: DateTime, } -/// 备份记录分页查询。 +/// 备份记录分页查询 #[derive(Debug, Clone, PartialEq, Eq)] pub struct BackupRecordListQuery { pub page: u32, @@ -330,7 +332,7 @@ pub struct BackupRecordListQuery { pub trigger: Option, } -/// 备份记录分页结果。 +/// 备份记录分页结果 #[derive(Debug, Clone, PartialEq, Eq)] pub struct BackupRecordPage { pub items: Vec, @@ -340,7 +342,7 @@ pub struct BackupRecordPage { } impl BackupRecordPage { - /// 计算总页数;空集合视为一页。 + /// 计算总页数;空集合视为一页 #[must_use] pub fn total_pages(&self) -> u32 { let size = u64::from(self.page_size.get()); @@ -352,7 +354,7 @@ impl BackupRecordPage { } } -/// 创建备份任务的持久化数据;id 与对象 key 由领域生成。 +/// 创建备份任务的持久化数据;id 与对象 key 由领域生成 #[derive(Debug, Clone, PartialEq, Eq)] pub struct BackupRecordSeed { pub id: String, @@ -362,7 +364,7 @@ pub struct BackupRecordSeed { pub expires_at: Option>, } -/// 连接测试结果;只携带稳定阶段与脱敏消息。 +/// 连接测试结果;只携带稳定阶段与脱敏消息 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ConnectionTestResult { pub ok: bool, @@ -371,7 +373,7 @@ pub struct ConnectionTestResult { pub message: String, } -/// 对象存储探测阶段。 +/// 对象存储探测阶段 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ConnectionTestStage { PutObject, @@ -381,7 +383,7 @@ pub enum ConnectionTestStage { } impl ConnectionTestStage { - /// 稳定 wire 值。 + /// 稳定 wire 值 #[must_use] pub const fn as_str(self) -> &'static str { match self { @@ -393,10 +395,10 @@ impl ConnectionTestStage { } } -/// 上传到对象存储并期望远端校验的归档 metadata。 +/// 上传到对象存储并期望远端校验的归档 metadata /// /// `size_bytes` 在上传时不写入 metadata,但 `HeadObject` 会返回远端大小, -/// 用于与本地归档校验一致。 +/// 用于与本地归档校验一致 #[derive(Debug, Clone, PartialEq, Eq)] pub struct BackupObjectMetadata { pub backup_id: String, @@ -406,7 +408,7 @@ pub struct BackupObjectMetadata { } impl BackupObjectMetadata { - /// 构造上传 metadata。 + /// 构造上传 metadata #[must_use] pub const fn new( backup_id: String, @@ -423,7 +425,7 @@ impl BackupObjectMetadata { } } -/// 短时下载地址结果。 +/// 短时下载地址结果 #[derive(Clone, PartialEq, Eq)] pub struct DownloadUrlResult { pub url: String, @@ -442,10 +444,11 @@ impl fmt::Debug for DownloadUrlResult { } } -/// 备份基础设施错误:只携带稳定错误码与脱敏消息。 +/// 备份基础设施错误:只携带稳定错误码与脱敏消息 /// /// 该类型不构成第二套 kind 枚举;`gateway-admin` 边界统一映射为既有 -/// `AdminErrorKind`。稳定 `error_code` 同时用于任务记录、日志与 UI。 +/// `AdminErrorKind` +/// 稳定 `error_code` 同时用于任务记录、日志与 UI #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] #[error("{message}")] pub struct BackupError { @@ -454,32 +457,32 @@ pub struct BackupError { } impl BackupError { - /// 构造基础设施错误。 + /// 构造基础设施错误 #[must_use] pub const fn new(code: &'static str, message: String) -> Self { Self { code, message } } - /// 稳定错误码,例如 `backup.s3_auth_failed`。 + /// 稳定错误码,例如 `backup.s3_auth_failed` #[must_use] pub const fn code(&self) -> &'static str { self.code } - /// 脱敏后的可操作消息。 + /// 脱敏后的可操作消息 #[must_use] pub fn message(&self) -> &str { &self.message } } -/// 构造稳定错误码常量。 +/// 构造稳定错误码常量 pub mod code; -/// 构建对象 key:`{prefix}/YYYY/MM/DD/codex-proxy-rs_{UTC seconds}_{backup id}.dump`。 +/// 构建对象 key:`{prefix}/YYYY/MM/DD/codex-proxy-rs_{UTC seconds}_{backup id}.dump` /// /// 规范化规则:prefix 不得为空、不得以前导 `/` 开头、不得包含 `..`、反斜杠或控制字符; -/// 输出统一使用 `/` 分隔且不含空段。 +/// 输出统一使用 `/` 分隔且不含空段 pub fn build_object_key( prefix: &str, backup_id: &str, @@ -496,22 +499,22 @@ pub fn build_object_key( )) } -/// 下载文件名:`codex-proxy-rs_{短 id}.dump`,与对象 key 的文件名一致。 +/// 下载文件名:`codex-proxy-rs_{短 id}.dump`,与对象 key 的文件名一致 #[must_use] pub fn build_download_file_name(backup_id: &str) -> String { format!("codex-proxy-rs_{}.dump", short_backup_id(backup_id)) } -/// 截取备份 id 的可读短标识:去掉 `backup_` 前缀,最多保留 8 位 hex。 +/// 截取备份 id 的可读短标识:去掉 `backup_` 前缀,最多保留 8 位 hex #[must_use] fn short_backup_id(backup_id: &str) -> String { let hex = backup_id.strip_prefix("backup_").unwrap_or(backup_id); hex.get(..8).unwrap_or(hex).to_owned() } -/// 规范化并校验对象 prefix。 +/// 规范化并校验对象 prefix /// -/// 返回去掉尾部 `/` 的规范 prefix;空串、前导 `/`、`..` 段、反斜杠和控制字符均拒绝。 +/// 返回去掉尾部 `/` 的规范 prefix;空串、前导 `/`、`..` 段、反斜杠和控制字符均拒绝 pub fn normalize_prefix(prefix: &str) -> Result { if prefix.is_empty() || prefix.starts_with('/') { return Err(BackupError::new( @@ -534,13 +537,13 @@ pub fn normalize_prefix(prefix: &str) -> Result { Ok(prefix.trim_end_matches('/').to_owned()) } -/// 生成新的备份记录 id。 +/// 生成新的备份记录 id #[must_use] pub fn new_backup_id() -> String { format!("backup_{}", Uuid::now_v7().simple()) } -/// 从触发类型与计划时间构造可持久化 seed;id 与对象 key 一次生成保持一致。 +/// 从触发类型与计划时间构造可持久化 seed;id 与对象 key 一次生成保持一致 pub fn build_backup_seed( trigger_kind: BackupTriggerKind, scheduled_at: Option>, diff --git a/backend/crates/gateway-admin/src/model/client_distribution.rs b/backend/crates/gateway-admin/src/model/client_distribution.rs index faf7545dd..9b5ef9a1b 100644 --- a/backend/crates/gateway-admin/src/model/client_distribution.rs +++ b/backend/crates/gateway-admin/src/model/client_distribution.rs @@ -1,8 +1,8 @@ -//! 管理设置页使用的 Codex 客户端下载信息。 +//! 管理设置页使用的 Codex 客户端下载信息 use chrono::{DateTime, Utc}; -/// Windows 安装包架构。 +/// Windows 安装包架构 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum ClientArchitecture { X64, @@ -19,7 +19,7 @@ impl ClientArchitecture { } } -/// 下载地址的可信来源。 +/// 下载地址的可信来源 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ClientDownloadSource { MicrosoftStore, @@ -36,7 +36,7 @@ impl ClientDownloadSource { } } -/// 一个已经过 Host 校验、可以直接交给浏览器下载的安装包。 +/// 一个已经过 Host 校验、可以直接交给浏览器下载的安装包 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ClientDownloadPackage { pub architecture: ClientArchitecture, @@ -48,7 +48,7 @@ pub struct ClientDownloadPackage { pub expires_at: Option>, } -/// Codex Desktop Windows 下载解析结果。 +/// Codex Desktop Windows 下载解析结果 #[derive(Debug, Clone, PartialEq, Eq)] pub struct CodexDesktopWindowsDownloads { pub resolved_at: DateTime, diff --git a/backend/crates/gateway-admin/src/model/client_keys.rs b/backend/crates/gateway-admin/src/model/client_keys.rs index c844e2440..8c11f1ef6 100644 --- a/backend/crates/gateway-admin/src/model/client_keys.rs +++ b/backend/crates/gateway-admin/src/model/client_keys.rs @@ -1,4 +1,4 @@ -//! Client API Key 的 Command、Result 与安全秘密类型。 +//! Client API Key 的 Command、Result 与安全秘密类型 use std::{collections::BTreeMap, fmt, num::NonZeroU16}; @@ -20,16 +20,16 @@ pub type ProviderRequestProfileOverrides = pub type ProviderRequestProfileOverrideUpdates = BTreeMap>; -/// Client Key 列表保持旧 HTTP 合同允许的完整非零 `u16` 页大小。 +/// Client Key 列表保持旧 HTTP 合同允许的完整非零 `u16` 页大小 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ClientKeyPageSize(NonZeroU16); impl ClientKeyPageSize { - /// 创建 1 至 65535 的 Client Key 页大小。 + /// 创建 1 至 65535 的 Client Key 页大小 /// /// # Errors /// - /// `value` 为零时返回 [`AdminModelError::InvalidClientKeyPageSize`]。 + /// `value` 为零时返回 [`AdminModelError::InvalidClientKeyPageSize`] pub fn new(value: u16) -> Result { NonZeroU16::new(value) .map(Self) @@ -42,7 +42,7 @@ impl ClientKeyPageSize { } } -/// Client Key 列表排序字段。 +/// Client Key 列表排序字段 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ClientKeySortField { Name, @@ -51,21 +51,21 @@ pub enum ClientKeySortField { LastUsedAt, } -/// Client Key 列表排序方向。 +/// Client Key 列表排序方向 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SortDirection { Asc, Desc, } -/// Client Key 列表排序规则。 +/// Client Key 列表排序规则 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ClientKeySort { pub field: ClientKeySortField, pub direction: SortDirection, } -/// 与排序字段绑定的游标值。 +/// 与排序字段绑定的游标值 #[derive(Debug, Clone, PartialEq, Eq)] pub enum ClientKeyCursorValue { Name(String), @@ -74,7 +74,7 @@ pub enum ClientKeyCursorValue { LastUsedAt(Option>), } -/// Client Key 的稳定键集游标。 +/// Client Key 的稳定键集游标 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ClientKeyCursor { pub sort: ClientKeySort, @@ -82,7 +82,7 @@ pub struct ClientKeyCursor { pub id: ClientApiKeyId, } -/// Client Key 列表查询。 +/// Client Key 列表查询 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ClientKeyListQuery { pub cursor: Option, @@ -91,7 +91,7 @@ pub struct ClientKeyListQuery { pub sort: ClientKeySort, } -/// 不含完整明文 Key 的管理投影。 +/// 不含完整明文 Key 的管理投影 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ClientKeyRecord { pub request_profile_overrides: ProviderRequestProfileOverrides, @@ -109,7 +109,7 @@ pub struct ClientKeyRecord { pub updated_at: DateTime, } -/// Client Key 列表页。 +/// Client Key 列表页 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ClientKeyPage { pub config_revision: Revision, @@ -118,7 +118,7 @@ pub struct ClientKeyPage { pub next_cursor: Option, } -/// 仅在创建或显式读取密钥配置时返回的明文 Key;Debug 隐去明文。 +/// 仅在创建或显式读取密钥配置时返回的明文 Key;Debug 隐去明文 #[derive(Clone, PartialEq, Eq)] pub struct ClientKeySecret { pub record: ClientKeyRecord, @@ -150,7 +150,7 @@ impl fmt::Debug for ClientKeySecret { } } -/// API 提交的 Client Key 创建命令。 +/// API 提交的 Client Key 创建命令 #[derive(Debug, Clone, PartialEq, Eq)] pub struct CreateClientKey { pub request_profile_overrides: ProviderRequestProfileOverrides, @@ -162,7 +162,7 @@ pub struct CreateClientKey { pub budget: ClientBudgetLimits, } -/// 管理用例生成 ID 与明文后的持久化命令。 +/// 管理用例生成 ID 与明文后的持久化命令 #[derive(Clone, PartialEq, Eq)] pub struct NewClientKey { pub request_profile_overrides: ProviderRequestProfileOverrides, @@ -187,7 +187,7 @@ impl fmt::Debug for NewClientKey { } } -/// 修改 Client Key 的公开策略字段。 +/// 修改 Client Key 的公开策略字段 #[derive(Debug, Clone, PartialEq, Eq)] pub struct UpdateClientKey { pub request_profile_override_updates: ProviderRequestProfileOverrideUpdates, @@ -200,14 +200,14 @@ pub struct UpdateClientKey { pub weekly_limit_usd: Option, } -/// 修改 Client Key 启用状态。 +/// 修改 Client Key 启用状态 #[derive(Debug, Clone, PartialEq, Eq)] pub struct SetClientKeyEnabled { pub id: ClientApiKeyId, pub enabled: bool, } -/// 管理员选择的金额重置范围。 +/// 管理员选择的金额重置范围 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ClientKeyBudgetPeriod { Daily, @@ -215,14 +215,14 @@ pub enum ClientKeyBudgetPeriod { All, } -/// 清零所选窗口已用金额,保留限额、到期时间与历史费用。 +/// 清零所选窗口已用金额,保留限额、到期时间与历史费用 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ResetClientKeyBudget { pub id: ClientApiKeyId, pub period: ClientKeyBudgetPeriod, } -/// 仅更新指定金额上限;省略的周期保持不变,不修改已用金额或窗口。 +/// 仅更新指定金额上限;省略的周期保持不变,不修改已用金额或窗口 #[derive(Debug, Clone, PartialEq, Eq)] pub struct UpdateClientKeyBudgetLimits { pub id: ClientApiKeyId, @@ -230,20 +230,20 @@ pub struct UpdateClientKeyBudgetLimits { pub weekly_limit_usd: Option, } -/// 预算变更的调用来源;插件身份由 Runtime 给出,不能从插件请求反序列化。 +/// 预算变更的调用来源;插件身份由 Runtime 给出,不能从插件请求反序列化 #[derive(Debug, Clone)] pub enum ClientKeyBudgetMutationOrigin { Admin, Plugin(PluginResourceOwner), } -/// 删除 Client Key。 +/// 删除 Client Key #[derive(Debug, Clone, PartialEq, Eq)] pub struct DeleteClientKey { pub id: ClientApiKeyId, } -/// Client Key 创建结果;完整明文仅存在于该一次性结果中。 +/// Client Key 创建结果;完整明文仅存在于该一次性结果中 pub struct CreatedClientKey { pub config_revision: Revision, pub secret: ClientKeySecret, @@ -259,7 +259,7 @@ impl fmt::Debug for CreatedClientKey { } } -/// Client Key 普通写操作结果。 +/// Client Key 普通写操作结果 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ClientKeyMutation { pub config_revision: Revision, diff --git a/backend/crates/gateway-admin/src/model/import_tasks.rs b/backend/crates/gateway-admin/src/model/import_tasks.rs index 0a81b256f..3586236a4 100644 --- a/backend/crates/gateway-admin/src/model/import_tasks.rs +++ b/backend/crates/gateway-admin/src/model/import_tasks.rs @@ -1,4 +1,4 @@ -//! 进程内账号导入任务;只保存执行所需输入与可公开的结果。 +//! 进程内账号导入任务;只保存执行所需输入与可公开的结果 use chrono::{DateTime, Utc}; use gateway_core::{account::ProviderAccountId, routing::ProviderKind}; @@ -17,7 +17,7 @@ pub struct ImportTaskInput { #[derive(Debug)] pub struct SubmitImportTask { pub submission_id: Uuid, - /// API 对完整输入计算摘要;用于区分丢失响应后的重试与修改后的新提交。 + /// API 对完整输入计算摘要;用于区分丢失响应后的重试与修改后的新提交 pub fingerprint: [u8; 32], pub context: MutationContext, pub items: Vec, diff --git a/backend/crates/gateway-admin/src/model/key_usage.rs b/backend/crates/gateway-admin/src/model/key_usage.rs index 8c37735af..7d779ce11 100644 --- a/backend/crates/gateway-admin/src/model/key_usage.rs +++ b/backend/crates/gateway-admin/src/model/key_usage.rs @@ -1,4 +1,4 @@ -//! 单页 Key 用量的查询合同;身份范围不由调用方提供。 +//! 单页 Key 用量的查询合同;身份范围不由调用方提供 use super::{ PageSize, diff --git a/backend/crates/gateway-admin/src/model/mod.rs b/backend/crates/gateway-admin/src/model/mod.rs index cf05e381c..6d7ce6eb1 100644 --- a/backend/crates/gateway-admin/src/model/mod.rs +++ b/backend/crates/gateway-admin/src/model/mod.rs @@ -1,4 +1,4 @@ -//! 管理控制面使用的 Command、Result 与稳定值对象。 +//! 管理控制面使用的 Command、Result 与稳定值对象 use std::num::{NonZeroU16, NonZeroU64}; @@ -24,7 +24,7 @@ pub mod retention; pub mod settings; pub mod system; -/// 管理用例对外返回的稳定错误分类。 +/// 管理用例对外返回的稳定错误分类 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AdminErrorKind { Invalid, @@ -39,7 +39,7 @@ pub enum AdminErrorKind { Internal, } -/// 不携带基础设施细节、可安全返回给管理员的管理用例错误。 +/// 不携带基础设施细节、可安全返回给管理员的管理用例错误 #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] #[error("{message}")] pub struct AdminError { @@ -61,7 +61,7 @@ impl AdminError { self.kind } - /// 返回已脱敏、可公开给管理员的文案。 + /// 返回已脱敏、可公开给管理员的文案 #[must_use] pub fn message(&self) -> &str { &self.message @@ -108,7 +108,7 @@ impl AdminError { } } -/// PostgreSQL 中所有正整数 revision 的管理层表示。 +/// PostgreSQL 中所有正整数 revision 的管理层表示 #[derive( Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize, )] @@ -116,11 +116,11 @@ impl AdminError { pub struct Revision(NonZeroU64); impl Revision { - /// 创建正整数 revision。 + /// 创建正整数 revision /// /// # Errors /// - /// `value` 为零时返回 [`AdminModelError::ZeroRevision`]。 + /// `value` 为零时返回 [`AdminModelError::ZeroRevision`] pub fn new(value: u64) -> Result { NonZeroU64::new(value) .map(Self) @@ -133,18 +133,18 @@ impl Revision { } } -/// 管理列表统一使用的受限页大小。 +/// 管理列表统一使用的受限页大小 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct PageSize(NonZeroU16); impl PageSize { pub const MAX: u16 = 200; - /// 创建 1 至 200 的页大小。 + /// 创建 1 至 200 的页大小 /// /// # Errors /// - /// `value` 不在有效范围时返回 [`AdminModelError::InvalidPageSize`]。 + /// `value` 不在有效范围时返回 [`AdminModelError::InvalidPageSize`] pub fn new(value: u16) -> Result { if value > Self::MAX { return Err(AdminModelError::InvalidPageSize(value)); @@ -160,7 +160,7 @@ impl PageSize { } } -/// 可审计管理写操作的发起者。 +/// 可审计管理写操作的发起者 #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "snake_case", deny_unknown_fields)] pub enum MutationActor { @@ -169,7 +169,7 @@ pub enum MutationActor { System, } -/// 管理写操作必须携带的审计上下文。 +/// 管理写操作必须携带的审计上下文 #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "snake_case", deny_unknown_fields)] pub struct MutationContext { @@ -177,7 +177,7 @@ pub struct MutationContext { pub request_id: String, } -/// 领域值对象构造失败。 +/// 领域值对象构造失败 #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] pub enum AdminModelError { #[error("revision must be greater than zero")] diff --git a/backend/crates/gateway-admin/src/model/observability.rs b/backend/crates/gateway-admin/src/model/observability.rs index ee402d805..f3107e2ff 100644 --- a/backend/crates/gateway-admin/src/model/observability.rs +++ b/backend/crates/gateway-admin/src/model/observability.rs @@ -1,4 +1,4 @@ -//! 用量、成本、健康与错误诊断的 UTC 语义事实。 +//! 用量、成本、健康与错误诊断的 UTC 语义事实 use std::str::FromStr; @@ -6,7 +6,7 @@ use chrono::{DateTime, TimeDelta, Utc}; use super::{AdminModelError, PageSize}; -/// 页面筛选表达自然日范围,具体 UTC 边界由部署时区解析。 +/// 页面筛选表达自然日范围,具体 UTC 边界由部署时区解析 #[derive(Debug, Clone, Copy)] pub enum CalendarPeriod { Today, @@ -25,7 +25,7 @@ impl CalendarPeriod { } } -/// 外部观测查询的 UTC 时间范围。 +/// 外部观测查询的 UTC 时间范围 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct TimeRange { pub start: DateTime, @@ -46,17 +46,17 @@ impl TimeRange { let start = timezone .days_before(end, days) .ok_or(AdminModelError::InvalidTimeRange)?; - // 自然日刚开始时允许空快照,不借用前一天或伪造未来终点。 + // 自然日刚开始时允许空快照,不借用前一天或伪造未来终点 if start == end { return Ok(Self { start, end }); } Self::new(start, end) } - /// 创建最长 366 天的正时间范围。 + /// 创建最长 366 天的正时间范围 /// /// # Errors /// - /// 范围为空、反向或超过 366 天时返回错误。 + /// 范围为空、反向或超过 366 天时返回错误 pub fn new(start: DateTime, end: DateTime) -> Result { let duration = end.signed_duration_since(start); if duration <= TimeDelta::zero() || duration > TimeDelta::days(366) { @@ -66,7 +66,7 @@ impl TimeRange { } } -/// 请求结果状态。 +/// 请求结果状态 #[derive(Debug, Clone, PartialEq, Eq)] pub enum RequestOutcome { Running, @@ -77,19 +77,19 @@ pub enum RequestOutcome { Other(OtherRequestOutcome), } -/// 未知但有界的请求结果值;私有字段禁止绕过 [`RequestOutcome::new`] 构造。 +/// 未知但有界的请求结果值;私有字段禁止绕过 [`RequestOutcome::new`] 构造 #[derive(Debug, Clone, PartialEq, Eq)] pub struct OtherRequestOutcome(String); impl RequestOutcome { pub const MAX_BYTES: usize = 256; - /// 从持久化或 wire 字符串创建可扩展结果语义。 + /// 从持久化或 wire 字符串创建可扩展结果语义 /// /// # Errors /// /// 空值、超过 256 字节或含控制字符时返回 - /// [`AdminModelError::InvalidRequestOutcome`]。 + /// [`AdminModelError::InvalidRequestOutcome`] pub fn new(value: impl Into) -> Result { let value = value.into(); match value.as_str() { @@ -128,7 +128,7 @@ impl OtherRequestOutcome { } } -/// 用量记录过滤条件。 +/// 用量记录过滤条件 #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct UsageFilter { pub client_api_key_ref: Option, @@ -146,7 +146,7 @@ pub struct UsageFilter { pub search: Option, } -/// 用量记录分页查询。 +/// 用量记录分页查询 #[derive(Debug, Clone, PartialEq, Eq)] pub struct UsageQuery { pub range: TimeRange, @@ -155,7 +155,7 @@ pub struct UsageQuery { pub page_size: PageSize, } -/// 运维错误过滤条件。 +/// 运维错误过滤条件 #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct OpsErrorFilter { pub client_api_key_ref: Option, @@ -172,7 +172,7 @@ pub struct OpsErrorFilter { pub search: Option, } -/// 运维错误分页查询。 +/// 运维错误分页查询 #[derive(Debug, Clone, PartialEq, Eq)] pub struct OpsErrorQuery { pub range: TimeRange, @@ -181,7 +181,7 @@ pub struct OpsErrorQuery { pub page_size: PageSize, } -/// 用量诊断维度。 +/// 用量诊断维度 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DiagnosticDimension { Provider, @@ -193,7 +193,7 @@ pub enum DiagnosticDimension { Status, } -/// `numeric(20,10)` 的非负规范金额。 +/// `numeric(20,10)` 的非负规范金额 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct DecimalAmount(String); @@ -203,13 +203,13 @@ impl DecimalAmount { &self.0 } - /// 精确相加两个 `numeric(20,10)` 金额。 + /// 精确相加两个 `numeric(20,10)` 金额 #[must_use] pub fn checked_add(&self, other: &Self) -> Option { Self::from_decimal(self.to_decimal()?.checked_add(other.to_decimal()?)?) } - /// 将金额按非零请求数均分,保留最多十位小数。 + /// 将金额按非零请求数均分,保留最多十位小数 #[must_use] pub fn checked_div_u64(&self, divisor: u64) -> Option { Self::from_decimal(self.to_decimal()?.checked_div_u64(divisor)?) @@ -262,25 +262,25 @@ impl std::fmt::Display for DecimalAmount { } } -/// 单一币种的成本合计。 +/// 单一币种的成本合计 #[derive(Debug, Clone, PartialEq, Eq)] pub struct CurrencyCost { pub currency: String, pub amount: DecimalAmount, } -/// PostgreSQL 连续百分位返回的非负有限毫秒值。 +/// PostgreSQL 连续百分位返回的非负有限毫秒值 /// -/// 以 IEEE-754 bits 保存,既不丢失插值小数,也可安全实现 `Eq`。 +/// 以 IEEE-754 bits 保存,既不丢失插值小数,也可安全实现 `Eq` #[derive(Clone, Copy, PartialEq, Eq, Hash)] pub struct PercentileMilliseconds(u64); impl PercentileMilliseconds { - /// 校验并保存一个毫秒百分位。 + /// 校验并保存一个毫秒百分位 /// /// # Errors /// - /// 非有限值或负值返回 [`AdminModelError::InvalidLatencyPercentile`]。 + /// 非有限值或负值返回 [`AdminModelError::InvalidLatencyPercentile`] pub fn new(value: f64) -> Result { if !value.is_finite() || value < 0.0 { return Err(AdminModelError::InvalidLatencyPercentile); @@ -300,7 +300,7 @@ impl std::fmt::Debug for PercentileMilliseconds { } } -/// 延迟分布的三个稳定百分位。 +/// 延迟分布的三个稳定百分位 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct LatencyPercentiles { pub p50_ms: Option, @@ -308,7 +308,7 @@ pub struct LatencyPercentiles { pub p99_ms: Option, } -/// Provider 价格规则计算所需的持久请求事实。 +/// Provider 价格规则计算所需的持久请求事实 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderBillingInput { pub upstream_model_id: String, @@ -320,9 +320,9 @@ pub struct ProviderBillingInput { pub total: CurrencyCost, } -/// 已完整交付且由 Provider 计算费用的持久请求事实。 +/// 已完整交付且由 Provider 计算费用的持久请求事实 /// -/// 控制面仅保留通用事实,具体 Provider 负责校验已持久化总额并恢复标准费用。 +/// 控制面仅保留通用事实,具体 Provider 负责校验已持久化总额并恢复标准费用 #[derive(Debug, Clone, PartialEq, Eq)] pub struct UsageCalculatedBillingFact { pub breakdown: Option, @@ -337,7 +337,7 @@ pub struct UsageCalculatedBillingFact { pub total: CurrencyCost, } -/// Provider 已确认的逐项费用与单价。 +/// Provider 已确认的逐项费用与单价 #[derive(Debug, Clone, PartialEq, Eq)] pub struct CalculatedBillingBreakdown { pub long_context_billing_applied: bool, @@ -367,16 +367,16 @@ pub struct ImageBillingBreakdown { pub cache_read_price_per_million: CurrencyCost, } -/// 单次请求的费用语义。 +/// 单次请求的费用语义 /// -/// Provider 上报费用或无法恢复逐项价格时保留总额;Provider 验证成功后升级为完整分解。 +/// Provider 上报费用或无法恢复逐项价格时保留总额;Provider 验证成功后升级为完整分解 #[derive(Debug, Clone, PartialEq, Eq)] pub enum UsageBilling { Total { source: String, total: CurrencyCost }, Calculated(Box), } -/// 计费数据覆盖情况。 +/// 计费数据覆盖情况 #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct CostCoverage { pub provider_reported_count: u64, @@ -387,7 +387,7 @@ pub struct CostCoverage { } impl CostCoverage { - /// Provider 直接上报或 Provider 规则完整计算的已知成本数。 + /// Provider 直接上报或 Provider 规则完整计算的已知成本数 #[must_use] pub fn known_count(&self) -> u64 { self.provider_reported_count @@ -395,7 +395,7 @@ impl CostCoverage { } } -/// 请求级聚合指标。 +/// 请求级聚合指标 #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct RequestMetrics { pub request_count: u64, @@ -432,7 +432,7 @@ pub struct RequestMetrics { pub cache_hit_request_count: u64, } -/// 上游 attempt 聚合指标。 +/// 上游 attempt 聚合指标 #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct AttemptMetrics { pub attempt_count: u64, @@ -447,7 +447,7 @@ pub struct AttemptMetrics { pub costs: Vec, } -/// 时间序列粒度。 +/// 时间序列粒度 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Granularity { FifteenMinutes, @@ -455,7 +455,7 @@ pub enum Granularity { Day, } -/// 一段时间桶内的请求指标。 +/// 一段时间桶内的请求指标 #[derive(Debug, Clone, PartialEq, Eq)] pub struct RequestMetricPoint { pub bucket_start: DateTime, @@ -465,7 +465,7 @@ pub struct RequestMetricPoint { pub costs: Vec, } -/// 账号池的统一五态统计。 +/// 账号池的统一五态统计 #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct AccountPoolMetrics { pub total: u64, @@ -476,7 +476,7 @@ pub struct AccountPoolMetrics { pub error: u64, } -/// Dashboard 中一个账号的模型级用量事实。 +/// Dashboard 中一个账号的模型级用量事实 #[derive(Debug, Clone, PartialEq, Eq)] pub struct DashboardAccountModelUsage { pub model: String, @@ -497,14 +497,14 @@ pub struct DashboardAccountModelUsage { pub last_used_at: DateTime, } -/// Dashboard 中账号的单小时请求数。 +/// Dashboard 中账号的单小时请求数 #[derive(Debug, Clone, PartialEq, Eq)] pub struct DashboardAccountRequestBucket { pub bucket_start: DateTime, pub request_count: u64, } -/// Dashboard 中一个账号的完整用量事实。 +/// Dashboard 中一个账号的完整用量事实 #[derive(Debug, Clone, PartialEq)] pub struct DashboardAccountUsage { pub account_id: String, @@ -513,7 +513,7 @@ pub struct DashboardAccountUsage { pub name: String, pub email: Option, pub plan_type: Option, - /// Admin 按 Provider 补齐的套餐展示名称。 + /// Admin 按 Provider 补齐的套餐展示名称 pub plan_type_display: Option, pub request_count: u64, pub success_count: u64, @@ -531,27 +531,27 @@ pub struct DashboardAccountUsage { pub costs: Vec, pub last_used_at: Option>, pub request_buckets: Vec, - /// Provider 已持久化额度窗口投影出的代表性已用比例。 + /// Provider 已持久化额度窗口投影出的代表性已用比例 /// - /// `None` 表示上游未提供可比较的百分比,不应伪造为零。 + /// `None` 表示上游未提供可比较的百分比,不应伪造为零 pub quota_used_percent: Option, - /// Provider 返回的代表窗口;缺失时保留未知语义。 + /// Provider 返回的代表窗口;缺失时保留未知语义 pub quota_window: Option, pub models: Vec, } -/// Dashboard 当前账号池的可重建运行时槽位事实。 +/// Dashboard 当前账号池的可重建运行时槽位事实 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct DashboardRuntimeSlots { - /// 继承运行参数默认并发上限的可调度账号数。 + /// 继承运行参数默认并发上限的可调度账号数 pub inherited_accounts: u64, - /// 已配置账号级并发上限的可调度账号槽位总数。 + /// 已配置账号级并发上限的可调度账号槽位总数 pub overridden_slots: u64, - /// Redis 可用时的实时 in-flight 槽位总数;不可用时为 `None`。 + /// Redis 可用时的实时 in-flight 槽位总数;不可用时为 `None` pub used_slots: Option, } -/// 仪表盘卡片脚注展示的全历史累计。 +/// 仪表盘卡片脚注展示的全历史累计 #[derive(Debug, Clone, Default, PartialEq)] pub struct DashboardTotals { pub request_count: u64, @@ -561,7 +561,7 @@ pub struct DashboardTotals { pub billing_usd: Option, } -/// 仪表盘所需的当前区间事实及全历史累计。 +/// 仪表盘所需的当前区间事实及全历史累计 #[derive(Debug, Clone, PartialEq)] pub struct DashboardObservation { pub range: TimeRange, @@ -572,7 +572,7 @@ pub struct DashboardObservation { pub recent_requests: Vec, } -/// 使用记录表格的窄读模型。 +/// 使用记录表格的窄读模型 #[derive(Debug, Clone, PartialEq, Eq)] pub struct UsageListRecord { pub client_api_key_name: Option, @@ -584,9 +584,9 @@ pub struct UsageListRecord { pub provider_account_ref: Option, pub provider_account_name: Option, pub provider_account_email: Option, - /// 账号当前备注,按内部账号 ID 关联,不属于请求历史快照。 + /// 账号当前备注,按内部账号 ID 关联,不属于请求历史快照 pub provider_account_notes: Option, - /// 账号当前套餐,不属于请求历史快照。 + /// 账号当前套餐,不属于请求历史快照 pub provider_account_plan_type: Option, pub provider_account_plan_type_display: Option, pub provider_account_authentication_kind: Option, @@ -628,7 +628,7 @@ pub struct UsageListRecord { pub started_at: DateTime, } -/// 一次完整模型请求的公共观测记录。 +/// 一次完整模型请求的公共观测记录 #[derive(Debug, Clone, PartialEq, Eq)] pub struct UsageRecord { pub id: String, @@ -653,7 +653,7 @@ pub struct UsageRecord { pub websocket_pool: Option, pub upstream_response_model: Option, pub service_tier: Option, - /// Provider 已筛选的专有观测 JSON;管理领域保持不透明。 + /// Provider 已筛选的专有观测 JSON;管理领域保持不透明 pub provider_metadata_json: Option, pub attempt_count: u32, pub upstream_send_state: String, @@ -707,7 +707,7 @@ pub struct UsageRecord { pub completed_at: Option>, } -/// 用量分页结果。 +/// 用量分页结果 #[derive(Debug, Clone, PartialEq, Eq)] pub struct UsagePage { pub items: Vec, @@ -716,7 +716,7 @@ pub struct UsagePage { pub total: u64, } -/// 请求中的一次上游尝试或运维事件。 +/// 请求中的一次上游尝试或运维事件 #[derive(Debug, Clone, PartialEq, Eq)] pub struct UsageAttempt { pub source: String, @@ -753,7 +753,7 @@ pub struct UsageAttempt { pub occurred_at: DateTime, } -/// 一条请求及其全部尝试。 +/// 一条请求及其全部尝试 #[derive(Debug, Clone, PartialEq, Eq)] pub struct UsageDetail { pub trace: Option, @@ -762,7 +762,7 @@ pub struct UsageDetail { pub attempts: Vec, } -/// 一个 Provider 的聚合用量。 +/// 一个 Provider 的聚合用量 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderObservation { pub provider_kind: String, @@ -772,7 +772,7 @@ pub struct ProviderObservation { pub total_tokens: u64, } -/// 用量总览。 +/// 用量总览 #[derive(Debug, Clone, PartialEq, Eq)] pub struct UsageOverview { pub range: TimeRange, @@ -781,21 +781,21 @@ pub struct UsageOverview { pub providers: Vec, } -/// 用量摘要的用例结果。 +/// 用量摘要的用例结果 #[derive(Debug, Clone, PartialEq, Eq)] pub struct UsageSummary { pub overview: UsageOverview, pub average_latency_ms: Option, } -/// 诊断聚合结果,分母包含截取展示项之前的全部匹配请求。 +/// 诊断聚合结果,分母包含截取展示项之前的全部匹配请求 #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct DiagnosticsObservation { pub total_request_count: u64, pub items: Vec, } -/// 单个诊断维度值的聚合结果。 +/// 单个诊断维度值的聚合结果 #[derive(Debug, Clone, PartialEq, Eq)] pub struct DiagnosticObservation { pub key: String, @@ -817,7 +817,7 @@ pub struct DiagnosticObservation { pub costs: Vec, } -/// 统一运维错误记录。 +/// 统一运维错误记录 #[derive(Debug, Clone, PartialEq, Eq)] pub struct OpsError { pub client_api_key_name: Option, @@ -875,7 +875,7 @@ pub struct OpsError { pub stable_sort_id: String, } -/// 运维错误分页结果。 +/// 运维错误分页结果 #[derive(Debug, Clone, PartialEq, Eq)] pub struct OpsErrorPage { pub items: Vec, @@ -884,7 +884,7 @@ pub struct OpsErrorPage { pub total: u64, } -/// 仪表盘趋势指标。 +/// 仪表盘趋势指标 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TrendKind { Usage, @@ -892,7 +892,7 @@ pub enum TrendKind { Errors, } -/// 一个趋势桶的全部已计算语义,API 只负责时区标签与展示格式。 +/// 一个趋势桶的全部已计算语义,API 只负责时区标签与展示格式 #[derive(Debug, Clone, PartialEq)] pub struct TrendPoint { pub bucket_start: DateTime, @@ -908,7 +908,7 @@ pub struct TrendPoint { pub costs: Vec, } -/// 趋势的无格式化汇总。 +/// 趋势的无格式化汇总 #[derive(Debug, Clone, PartialEq)] pub struct TrendSummary { pub request_count: u64, @@ -929,7 +929,7 @@ pub struct TrendSummary { pub cost_coverage: CostCoverage, } -/// 趋势结果;API 再决定图表标签与数字格式。 +/// 趋势结果;API 再决定图表标签与数字格式 #[derive(Debug, Clone, PartialEq)] pub struct Trend { pub kind: TrendKind, @@ -937,7 +937,7 @@ pub struct Trend { pub summary: TrendSummary, } -/// 健康时间桶状态。 +/// 健康时间桶状态 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HealthStatus { Future, @@ -948,7 +948,7 @@ pub enum HealthStatus { Stable, } -/// 一个 15 分钟健康桶的语义结果。 +/// 一个 15 分钟健康桶的语义结果 #[derive(Debug, Clone, PartialEq)] pub struct HealthTimelinePoint { pub bucket_start: DateTime, @@ -961,7 +961,7 @@ pub struct HealthTimelinePoint { pub caller_error_requests: u64, } -/// 中国自然日的固定 96 槽健康时间线。 +/// 中国自然日的固定 96 槽健康时间线 #[derive(Debug, Clone, PartialEq)] pub struct HealthTimeline { pub reliability_percent: Option, @@ -974,7 +974,7 @@ pub struct HealthTimeline { pub points: Vec, } -/// Dashboard 请求画像的目标平台。 +/// Dashboard 请求画像的目标平台 #[derive(Debug, Clone, PartialEq, Eq)] pub struct DashboardWireTarget { pub os_type: String, @@ -983,7 +983,7 @@ pub struct DashboardWireTarget { pub terminal: String, } -/// Desktop 发布检查状态。 +/// Desktop 发布检查状态 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DesktopReleaseStatus { Unchecked, @@ -992,7 +992,7 @@ pub enum DesktopReleaseStatus { Failed, } -/// 与请求画像分离的 Desktop 发布检查事实。 +/// 与请求画像分离的 Desktop 发布检查事实 #[derive(Debug, Clone, PartialEq, Eq)] pub struct DashboardDesktopRelease { pub status: DesktopReleaseStatus, @@ -1008,9 +1008,9 @@ pub struct DashboardDesktopRelease { pub error: Option, } -/// Provider 拥有并实际用于上游请求的身份画像快照。 +/// Provider 拥有并实际用于上游请求的身份画像快照 /// -/// `attributes` 承载 Provider 特有的可观测字段;公共控制面不解释其语义。 +/// `attributes` 承载 Provider 特有的可观测字段;公共控制面不解释其语义 #[derive(Debug, Clone, PartialEq, Eq)] pub struct DashboardWireProfile { pub provider: String, @@ -1024,24 +1024,24 @@ pub struct DashboardWireProfile { pub release: Option, } -/// Provider 画像的差异字段;展示层只显示标签和值,不解释 Provider 语义。 +/// Provider 画像的差异字段;展示层只显示标签和值,不解释 Provider 语义 #[derive(Debug, Clone, PartialEq, Eq)] pub struct DashboardWireAttribute { pub label: String, pub value: String, } -/// 当前账号池的并发调度容量。 +/// 当前账号池的并发调度容量 #[derive(Debug, Clone, PartialEq, Eq)] pub struct DashboardCapacity { pub max_concurrent_per_account: u64, - /// `None` 表示可用账号池不限制并发。 + /// `None` 表示可用账号池不限制并发 pub total_slots: Option, pub used_slots: Option, pub available_slots: Option, } -/// Dashboard 某个自然日区间内的卡片计数。 +/// Dashboard 某个自然日区间内的卡片计数 #[derive(Debug, Clone, Copy, Default, PartialEq)] pub struct DashboardPeriodMetrics { pub request_count: u64, @@ -1053,7 +1053,7 @@ pub struct DashboardPeriodMetrics { pub observed_cached_token_rate: Option, } -/// 仪表盘聚合结果。 +/// 仪表盘聚合结果 #[derive(Debug, Clone, PartialEq)] pub struct DashboardResult { pub observation: DashboardObservation, @@ -1069,7 +1069,7 @@ pub struct DashboardResult { pub rotation_strategy: super::settings::RotationStrategy, } -/// 洞察健康时间点的语义结果。 +/// 洞察健康时间点的语义结果 #[derive(Debug, Clone, PartialEq)] pub struct UsageInsightsHealthPoint { pub total_requests: u64, @@ -1082,7 +1082,7 @@ pub struct UsageInsightsHealthPoint { pub error_rate: f64, } -/// 洞察健康汇总;失败数已排除调用方错误。 +/// 洞察健康汇总;失败数已排除调用方错误 #[derive(Debug, Clone, PartialEq)] pub struct UsageInsightsHealth { pub total_requests: u64, @@ -1096,7 +1096,7 @@ pub struct UsageInsightsHealth { pub points: Vec, } -/// 洞察性能时间点。 +/// 洞察性能时间点 #[derive(Debug, Clone, PartialEq, Eq)] pub struct UsageInsightsPerformancePoint { pub bucket_start: DateTime, @@ -1111,7 +1111,7 @@ pub struct UsageInsightsPerformancePoint { pub capacity_utilization_p95_basis_points: Option, } -/// 洞察性能汇总与可观测覆盖率。 +/// 洞察性能汇总与可观测覆盖率 #[derive(Debug, Clone, PartialEq)] pub struct UsageInsightsPerformance { pub latency_percentiles: LatencyPercentiles, @@ -1131,7 +1131,7 @@ pub struct UsageInsightsPerformance { pub points: Vec, } -/// 洞察成本时间点。 +/// 洞察成本时间点 #[derive(Debug, Clone, PartialEq)] pub struct UsageInsightsCostPoint { pub bucket_start: DateTime, @@ -1147,7 +1147,7 @@ pub struct UsageInsightsCostPoint { pub cache_hit_request_rate: Option, } -/// 洞察成本汇总。 +/// 洞察成本汇总 #[derive(Debug, Clone, PartialEq)] pub struct UsageInsightsCost { pub estimated_cost: Option, @@ -1169,7 +1169,7 @@ pub struct UsageInsightsCost { pub coverage: CostCoverage, } -/// 用量洞察的完整用例结果;API 只负责标签、时区与字符串格式化。 +/// 用量洞察的完整用例结果;API 只负责标签、时区与字符串格式化 #[derive(Debug, Clone, PartialEq)] pub struct UsageInsights { pub granularity: Granularity, @@ -1180,7 +1180,7 @@ pub struct UsageInsights { pub providers: Vec, } -/// 已完成分母计算的诊断项。 +/// 已完成分母计算的诊断项 #[derive(Debug, Clone, PartialEq)] pub struct DiagnosticsItem { pub key: String, @@ -1204,7 +1204,7 @@ pub struct DiagnosticsItem { pub total_tokens: u64, } -/// 诊断结果。 +/// 诊断结果 #[derive(Debug, Clone, PartialEq)] pub struct DiagnosticsResult { pub dimension: DiagnosticDimension, diff --git a/backend/crates/gateway-admin/src/model/plugin_client_keys.rs b/backend/crates/gateway-admin/src/model/plugin_client_keys.rs index 02695be96..703b383d5 100644 --- a/backend/crates/gateway-admin/src/model/plugin_client_keys.rs +++ b/backend/crates/gateway-admin/src/model/plugin_client_keys.rs @@ -1,11 +1,11 @@ -//! 插件可见的 Client Key 非秘密资源投影。 +//! 插件可见的 Client Key 非秘密资源投影 use gateway_core::policy::ClientApiKeyId; use gateway_core::routing::AccountGroupId; use super::PageSize; -/// 插件可读取的当前 Key 范围,不含密钥或配置秘密。 +/// 插件可读取的当前 Key 范围,不含密钥或配置秘密 #[derive(Debug, Clone, PartialEq, Eq)] pub struct PluginClientKeyFacts { pub id: ClientApiKeyId, @@ -25,7 +25,7 @@ pub struct PluginClientKeyListQuery { pub limit: PageSize, } -/// 插件只能发现调用所需的公开身份,不取得 Key 前缀、策略细节或明文。 +/// 插件只能发现调用所需的公开身份,不取得 Key 前缀、策略细节或明文 #[derive(Debug, Clone, PartialEq, Eq)] pub struct PluginClientKey { pub id: ClientApiKeyId, diff --git a/backend/crates/gateway-admin/src/model/plugin_resources.rs b/backend/crates/gateway-admin/src/model/plugin_resources.rs index 717bb3640..2c531a70f 100644 --- a/backend/crates/gateway-admin/src/model/plugin_resources.rs +++ b/backend/crates/gateway-admin/src/model/plugin_resources.rs @@ -1,4 +1,4 @@ -//! 插件资源归属由宿主调用上下文给出,不能由插件参数选择实例或代次。 +//! 插件资源归属由宿主调用上下文给出,不能由插件参数选择实例或代次 use super::Revision; @@ -17,7 +17,7 @@ pub struct ManagedResource { } pub struct ResourceMutation { - /// 没有事实变化时不递增配置 revision,也不触发发布。 + /// 没有事实变化时不递增配置 revision,也不触发发布 pub revision: Option, pub value: T, } @@ -34,7 +34,7 @@ pub struct GroupMembersChanged { pub removed: u64, } -/// 插件 Key 的首次创建设置;分组始终从本实例资源键解析。 +/// 插件 Key 的首次创建设置;分组始终从本实例资源键解析 pub struct ManagedKeyConfig { pub name: String, pub limits: gateway_core::policy::RateLimits, diff --git a/backend/crates/gateway-admin/src/model/plugins/distribution.rs b/backend/crates/gateway-admin/src/model/plugins/distribution.rs index 39719e279..65bc39537 100644 --- a/backend/crates/gateway-admin/src/model/plugins/distribution.rs +++ b/backend/crates/gateway-admin/src/model/plugins/distribution.rs @@ -1,3 +1,5 @@ +//! 插件分发来源、下载凭据、发行查询与远程安装的数据合同 + use std::{fmt, sync::Arc}; use chrono::{DateTime, Utc}; @@ -17,7 +19,7 @@ pub enum PluginUpdateSource { Github { repository: String }, } -/// 检查策略只决定查询目标,任何策略都不会自动安装或切换运行实例。 +/// 检查策略只决定查询目标,任何策略都不会自动安装或切换运行实例 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] pub enum PluginUpdatePolicy { @@ -85,7 +87,7 @@ impl fmt::Debug for PluginDistributionEgress { #[serde(rename_all = "camelCase")] pub struct PluginUpdateCheck { pub binding: PluginSourceBinding, - /// Release 元数据不是已校验的插件包,不据此声明平台或 SDK 兼容。 + /// Release 元数据不是已校验的插件包,不据此声明平台或 SDK 兼容 pub release: PluginRelease, } @@ -109,7 +111,7 @@ pub enum DownloadPurpose { Artifact, } -/// 授权目标以完整 origin 和路径段边界匹配;摘要、名称和列表均不包含 secret。 +/// 授权目标以完整 origin 和路径段边界匹配;摘要、名称和列表均不包含 secret #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct SourceCredentialInfo { @@ -174,7 +176,7 @@ pub struct PluginRelease { pub expires_at: DateTime, } -/// 下载意图不承载凭据;引用在 Admin 中解析后只交给 Host 的本次调用。 +/// 下载意图不承载凭据;引用在 Admin 中解析后只交给 Host 的本次调用 #[derive(Debug, Clone, Deserialize)] #[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] pub enum RemotePluginLocation { @@ -200,7 +202,7 @@ pub struct RemotePluginVerify { pub credential_ids: Vec, #[serde(default)] pub outbound_proxy_id: Option, - /// 更新已有插件时锁定身份;首次安装从包内清单读取。 + /// 更新已有插件时锁定身份;首次安装从包内清单读取 pub expected_plugin_id: Option, } @@ -222,7 +224,7 @@ pub struct DownloadedPlugin { pub source: PluginSource, } -/// 只读包校验结果,不代表实例配置、授权或私有状态已完成运行时准备。 +/// 只读包校验结果,不代表实例配置、授权或私有状态已完成运行时准备 #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct VerifiedPluginArtifact { diff --git a/backend/crates/gateway-admin/src/model/plugins/instances.rs b/backend/crates/gateway-admin/src/model/plugins/instances.rs index 6e9470ee6..039ad3117 100644 --- a/backend/crates/gateway-admin/src/model/plugins/instances.rs +++ b/backend/crates/gateway-admin/src/model/plugins/instances.rs @@ -1,3 +1,5 @@ +//! 插件实例配置、能力绑定、运行状态与版本切换的数据合同 + use std::collections::BTreeMap; use secrecy::SecretString; @@ -20,7 +22,7 @@ pub struct PluginCapabilityBinding { pub stage: String, pub order: i32, pub failure_policy: PluginFailurePolicy, - /// 观察绑定选择事件类型;其他能力不设置事件。 + /// 观察绑定选择事件类型;其他能力不设置事件 #[serde(default, skip_serializing_if = "Option::is_none")] pub event: Option, #[serde(default)] @@ -35,7 +37,7 @@ pub struct PluginCapabilityBinding { pub identity_bindings: Vec, } -/// 外部 principal 到既有 Client Key 的显式宿主映射。 +/// 外部 principal 到既有 Client Key 的显式宿主映射 #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct PluginFrontendIdentityBinding { @@ -53,7 +55,7 @@ impl std::fmt::Debug for PluginFrontendIdentityBinding { } } -/// 配置和 secret 分离;Debug 与 Serialize 不得无意展开实例配置。 +/// 配置和 secret 分离;Debug 与 Serialize 不得无意展开实例配置 #[derive(Clone)] pub struct PluginInstance { pub id: String, @@ -78,7 +80,7 @@ pub struct PluginInstanceMutation { pub instance: PluginInstance, } -/// 控制面展示的单实例运行状态;它描述当前进程观察到的事实,不替代持久启用配置。 +/// 控制面展示的单实例运行状态;它描述当前进程观察到的事实,不替代持久启用配置 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PluginInstanceRuntimeStatus { Disabled, @@ -91,14 +93,14 @@ pub enum PluginInstanceRuntimeStatus { Draining, } -/// 已脱敏且可安全返回给管理员的运行失败摘要。 +/// 已脱敏且可安全返回给管理员的运行失败摘要 #[derive(Debug, Clone, PartialEq, Eq)] pub struct PluginInstanceRuntimeFailure { pub code: String, pub message: String, } -/// 当前发布视图与代次索引的只读诊断。 +/// 当前发布视图与代次索引的只读诊断 #[derive(Debug, Clone, PartialEq, Eq)] pub struct PluginInstanceRuntime { pub status: PluginInstanceRuntimeStatus, @@ -120,18 +122,18 @@ pub struct PluginInstanceView { #[derive(Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct ConfigurePluginInstance { - /// 创建草稿固定一个 UUID,重试复用同一配置,不能作为编辑已有配置的入口。 + /// 创建草稿固定一个 UUID,重试复用同一配置,不能作为编辑已有配置的入口 pub creation_id: Option, - /// 编辑时检查用户看到的配置版本,避免覆盖另一窗口已经保存的内容。 + /// 编辑时检查用户看到的配置版本,避免覆盖另一窗口已经保存的内容 pub expected_revision: Option, - /// 管理员确认停用的同插件配置,与当前配置在同一事务中切换。 + /// 管理员确认停用的同插件配置,与当前配置在同一事务中切换 #[serde(default)] pub replace_instances: Vec, pub name: String, pub artifact_sha256: String, pub enabled: bool, pub configuration: serde_json::Value, - /// 缺省保留旧值;显式空对象清除全部 secret。 + /// 缺省保留旧值;显式空对象清除全部 secret pub secrets: Option>, pub bindings: Vec, } @@ -143,7 +145,7 @@ pub struct PluginInstanceReplacement { pub expected_revision: u64, } -/// 恢复已安装旧版本的配置快照,并重新通过目标版本校验。 +/// 恢复已安装旧版本的配置快照,并重新通过目标版本校验 #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct RollbackPluginInstance { @@ -159,7 +161,7 @@ pub struct PluginRollbackTarget { pub platforms: Vec, } -/// 静态候选不启动插件;实际回滚仍须复验配置、平台和私有状态兼容性。 +/// 静态候选不启动插件;实际回滚仍须复验配置、平台和私有状态兼容性 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct PluginRollbackPlan { @@ -168,7 +170,7 @@ pub struct PluginRollbackPlan { pub targets: Vec, } -/// 版本配置快照只用于服务端恢复,敏感值不实现 Debug 或 Serialize。 +/// 版本配置快照只用于服务端恢复,敏感值不实现 Debug 或 Serialize #[derive(Clone)] pub struct PluginVersionConfiguration { pub configuration: serde_json::Value, @@ -176,7 +178,7 @@ pub struct PluginVersionConfiguration { pub bindings: Vec, } -/// 切换版本的只读草稿,不包含密钥值,也不会启动插件或写入配置。 +/// 切换版本的只读草稿,不包含密钥值,也不会启动插件或写入配置 #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct PluginVersionPlan { diff --git a/backend/crates/gateway-admin/src/model/plugins/management.rs b/backend/crates/gateway-admin/src/model/plugins/management.rs index 2e8da201f..4abff1617 100644 --- a/backend/crates/gateway-admin/src/model/plugins/management.rs +++ b/backend/crates/gateway-admin/src/model/plugins/management.rs @@ -1,8 +1,10 @@ +//! 插件管理页面、静态资源、请求与授权回调的数据合同 + use std::sync::Arc; use serde::{Deserialize, Serialize}; -/// 页面和业务调用绑定同一实例版本;制品摘要单独作为不可变资源身份。 +/// 页面和业务调用绑定同一实例版本;制品摘要单独作为不可变资源身份 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct PluginManagementTarget { @@ -37,7 +39,7 @@ pub struct StartPluginManagementCallback { pub ttl_seconds: u32, } -/// state 是短期一次性秘密,只交给发起登录的管理员,不进入 Debug 或日志。 +/// state 是短期一次性秘密,只交给发起登录的管理员,不进入 Debug 或日志 #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct PluginManagementCallbackTicket { @@ -72,7 +74,7 @@ pub struct PluginManagementResource { pub public: bool, } -/// 原始正文可能含业务数据,不能派生内容型 Debug 或写入诊断日志。 +/// 原始正文可能含业务数据,不能派生内容型 Debug 或写入诊断日志 pub struct PluginManagementRequest { pub method: String, pub path: String, diff --git a/backend/crates/gateway-admin/src/model/plugins/mod.rs b/backend/crates/gateway-admin/src/model/plugins/mod.rs index 204f61480..9d3a7eb04 100644 --- a/backend/crates/gateway-admin/src/model/plugins/mod.rs +++ b/backend/crates/gateway-admin/src/model/plugins/mod.rs @@ -1,3 +1,5 @@ +//! 插件制品元数据、宿主兼容性与管理领域模型的公共入口 + use std::{ collections::{BTreeMap, BTreeSet}, sync::Arc, @@ -13,7 +15,7 @@ pub mod management; pub mod official; pub mod state; -/// 一个宿主发行版明确承诺支持的插件能力版本。 +/// 一个宿主发行版明确承诺支持的插件能力版本 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct PluginHostCapability { @@ -21,7 +23,7 @@ pub struct PluginHostCapability { pub versions: Vec, } -/// 随宿主发行物封口的插件合同;Runtime 与发布清单共用这一份声明。 +/// 随宿主发行物封口的插件合同;Runtime 与发布清单共用这一份声明 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct PluginHostCompatibility { @@ -32,7 +34,7 @@ pub struct PluginHostCompatibility { } impl PluginHostCompatibility { - /// 拒绝空集合、重复项和不受控标识,避免发行清单把“未知”解释为通配。 + /// 拒绝空集合、重复项和不受控标识,避免发行清单把“未知”解释为通配 #[must_use] pub fn is_valid(&self) -> bool { if self.schema_version != 2 @@ -101,7 +103,7 @@ fn valid_plugin_segment(value: &str) -> bool { .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-') } -/// 已安装包对目标宿主的静态要求;不包含包体、配置或 secret。 +/// 已安装包对目标宿主的静态要求;不包含包体、配置或 secret #[derive(Debug, Clone, PartialEq, Eq)] pub struct PluginCompatibilityRequirements { pub host_version: String, @@ -110,7 +112,7 @@ pub struct PluginCompatibilityRequirements { pub capabilities: Vec<(String, u32)>, } -/// 一次下载使用的非敏感出站代理身份;地址与认证由受管代理记录持有。 +/// 一次下载使用的非敏感出站代理身份;地址与认证由受管代理记录持有 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct PluginSourceEgress { @@ -118,7 +120,7 @@ pub struct PluginSourceEgress { pub revision: u64, } -/// 安装来源只保存公开定位信息,下载凭据和出站代理通过独立记录引用。 +/// 安装来源只保存公开定位信息,下载凭据和出站代理通过独立记录引用 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] pub enum PluginSource { @@ -164,7 +166,7 @@ impl PluginSource { } } -/// 管理端保留的单项贡献声明;map key 是能力标识,`id` 是实例绑定引用的扩展项身份。 +/// 管理端保留的单项贡献声明;map key 是能力标识,`id` 是实例绑定引用的扩展项身份 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PluginContribution { @@ -178,7 +180,7 @@ pub struct PluginContribution { pub output_formats: Vec, } -/// 制品清单中的展示图标;路径只用于选择已校验的包资源。 +/// 制品清单中的展示图标;路径只用于选择已校验的包资源 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(untagged)] pub enum PluginArtifactIcon { @@ -198,15 +200,15 @@ pub enum PluginIconTheme { Dark, } -/// 从不可变制品中读取的已校验图标字节。 +/// 从不可变制品中读取的已校验图标字节 #[derive(Debug, Clone, PartialEq, Eq)] pub struct PluginArtifactIconResource { pub content_type: String, pub body: Vec, } -/// 已校验制品的持久化投影;读取时忽略未知字段,可选字段缺省沿用清单语义。 -/// 身份与执行所需事实仍为必填,包清单及协议版本由 Runtime 独立校验。 +/// 已校验制品的持久化投影;读取时忽略未知字段,可选字段缺省沿用清单语义 +/// 身份与执行所需事实仍为必填,包清单及协议版本由 Runtime 独立校验 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PluginArtifactMetadata { @@ -235,7 +237,7 @@ fn empty_configuration_schema() -> serde_json::Value { serde_json::json!({}) } -/// 由包检查端口产生的不可变制品;不派生 Debug,包内可能含敏感用户数据。 +/// 由包检查端口产生的不可变制品;不派生 Debug,包内可能含敏感用户数据 #[derive(Clone)] pub struct InspectedPluginArtifact { pub metadata: PluginArtifactMetadata, diff --git a/backend/crates/gateway-admin/src/model/plugins/official.rs b/backend/crates/gateway-admin/src/model/plugins/official.rs index ca07bc5b3..3216e456d 100644 --- a/backend/crates/gateway-admin/src/model/plugins/official.rs +++ b/backend/crates/gateway-admin/src/model/plugins/official.rs @@ -1,13 +1,15 @@ +//! 官方插件发行身份与导入命令 + use super::super::Revision; -/// 编译进宿主二进制的发行身份;不能由运行时配置或插件清单覆盖。 +/// 编译进宿主二进制的发行身份;不能由运行时配置或插件清单覆盖 #[derive(Debug, Clone, PartialEq, Eq)] pub struct OfficialPluginReleaseIdentity { pub gateway_version: String, pub gateway_git_sha: String, } -/// 启动时导入官方制品的结果;导入只持久化制品,不创建或启用实例。 +/// 启动时导入官方制品的结果;导入只持久化制品,不创建或启用实例 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct OfficialPluginImport { pub artifacts: usize, diff --git a/backend/crates/gateway-admin/src/model/plugins/state.rs b/backend/crates/gateway-admin/src/model/plugins/state.rs index 07dd43115..f4c8b2bbb 100644 --- a/backend/crates/gateway-admin/src/model/plugins/state.rs +++ b/backend/crates/gateway-admin/src/model/plugins/state.rs @@ -1,10 +1,12 @@ +//! 插件私有状态的归属、代次隔离与迁移数据合同 + use std::{collections::BTreeMap, fmt}; use serde::{Deserialize, Serialize}; use crate::model::Revision; -/// 已由 Runtime 校验的命名空间 schema 与配额事实。 +/// 已由 Runtime 校验的命名空间 schema 与配额事实 #[derive(Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PluginStateSchema { @@ -42,7 +44,7 @@ pub struct PluginStateConfiguration { pub namespaces: Vec, } -/// 与实例保存同事务提交的状态配置;transition 只引用已完成的 staging 数据。 +/// 与实例保存同事务提交的状态配置;transition 只引用已完成的 staging 数据 #[derive(Clone, Default)] pub struct PluginStateCommit { pub configuration: PluginStateConfiguration, @@ -57,7 +59,8 @@ pub struct PluginStateOwnerRequest { pub configuration: PluginStateConfiguration, } -/// Store 签发的实例私有状态 fence。插件协议中从不传输这些字段。 +/// Store 签发的实例私有状态 fence +/// 插件协议中从不传输这些字段 #[derive(Clone)] pub struct PluginStateOwner { instance_id: String, @@ -136,7 +139,7 @@ impl PluginStateNamespaceOwner { } } -/// 私有值可能含插件自己的敏感状态,故意不实现 `Debug`。 +/// 私有值可能含插件自己的敏感状态,故意不实现 `Debug` #[derive(Clone)] pub struct PluginStateRecord { pub key: String, @@ -149,7 +152,7 @@ pub struct PutPluginState { pub namespace: String, pub key: String, pub value: serde_json::Value, - /// `None` 是 create-only;已有记录必须提交精确版本。 + /// `None` 是 create-only;已有记录必须提交精确版本 pub expected_version: Option, } @@ -188,7 +191,7 @@ pub struct ApplyPluginStateMigration { pub transition_id: String, pub namespace: String, pub cursor: Option, - /// Runtime 刚读取的有序源键;Store 会在写入前重新读取并逐项比对。 + /// Runtime 刚读取的有序源键;Store 会在写入前重新读取并逐项比对 pub expected_keys: Vec, pub changes: Vec, } diff --git a/backend/crates/gateway-admin/src/model/pricing.rs b/backend/crates/gateway-admin/src/model/pricing.rs index 2a83b4e20..9dbc482bf 100644 --- a/backend/crates/gateway-admin/src/model/pricing.rs +++ b/backend/crates/gateway-admin/src/model/pricing.rs @@ -1,4 +1,4 @@ -//! 全局模型定价的管理合同。 +//! 全局模型定价的管理合同 use gateway_core::metering::{ModelPriceOverride, PricingOverrides}; use std::collections::{BTreeMap, BTreeSet}; diff --git a/backend/crates/gateway-admin/src/model/provider_credentials.rs b/backend/crates/gateway-admin/src/model/provider_credentials.rs index a7fa1c039..27456c462 100644 --- a/backend/crates/gateway-admin/src/model/provider_credentials.rs +++ b/backend/crates/gateway-admin/src/model/provider_credentials.rs @@ -1,4 +1,4 @@ -//! Provider 管理能力交换的中立 Command 与 Result。 +//! Provider 管理能力交换的中立 Command 与 Result use std::{fmt, pin::Pin}; @@ -22,7 +22,7 @@ use super::{ }, }; -/// Provider-owned JSON;公共层只搬运且 Debug 不输出值。 +/// Provider-owned JSON;公共层只搬运且 Debug 不输出值 #[derive(Clone, PartialEq)] pub struct ProviderDocument(OpaqueProviderData); @@ -32,7 +32,7 @@ impl ProviderDocument { Self(data) } - /// 仅具体 Provider 可以解释内部字段。 + /// 仅具体 Provider 可以解释内部字段 #[must_use] pub const fn expose_to_provider(&self) -> &OpaqueProviderData { &self.0 @@ -50,14 +50,14 @@ impl fmt::Debug for ProviderDocument { } } -/// Provider credential 详情。 +/// Provider credential 详情 #[derive(Debug, Clone, PartialEq, Eq)] pub struct CredentialDetails { pub config_revision: Revision, pub credential: AccountRecord, } -/// Provider 正式文档批量导入命令。 +/// Provider 正式文档批量导入命令 pub struct ImportCredentials { pub outbound_proxy_id: Option, pub settings: Option, @@ -75,14 +75,14 @@ impl fmt::Debug for ImportCredentials { } } -/// 批量导入提交结果。 +/// 批量导入提交结果 #[derive(Debug, Clone, PartialEq, Eq)] pub struct CredentialImportResult { pub config_revision: Revision, pub credential_ids: Vec, } -/// Provider 解析导入文档时只接收不透明文档,不接触 revision 或审计上下文。 +/// Provider 解析导入文档时只接收不透明文档,不接触 revision 或审计上下文 pub struct PrepareCredentialImport { pub default_outbound_proxy: Option, pub document: ProviderDocument, @@ -97,7 +97,7 @@ impl fmt::Debug for PrepareCredentialImport { } } -/// Provider 已验证、可由 Store 原子创建的一份 credential。 +/// Provider 已验证、可由 Store 原子创建的一份 credential #[derive(Debug, Clone, PartialEq)] pub struct PreparedCredentialCreate { pub model_access: Option, @@ -119,14 +119,14 @@ pub struct PreparedCredentialCreate { pub credential_observed_at: DateTime, } -/// Provider 对一份导入文档的完整验证结果。 +/// Provider 对一份导入文档的完整验证结果 #[derive(Debug, Clone, PartialEq)] pub struct PreparedCredentialImport { pub provider_kind: ProviderKind, pub credentials: Vec, } -/// Admin 交给 Store 的导入事务命令。 +/// Admin 交给 Store 的导入事务命令 #[derive(Debug, Clone, PartialEq)] pub struct CredentialImportCommit { pub outbound_proxy: Option, @@ -134,7 +134,7 @@ pub struct CredentialImportCommit { pub prepared: PreparedCredentialImport, } -/// OAuth pending owner 的中立身份;不编码具体 Provider 的 Redis key 或 JSON。 +/// OAuth pending owner 的中立身份;不编码具体 Provider 的 Redis key 或 JSON #[derive(Clone, PartialEq, Eq)] pub enum AuthorizationOwner { AdminSession { admin_user_id: String }, @@ -148,7 +148,7 @@ impl fmt::Debug for AuthorizationOwner { } } -/// Provider 写入 pending payload 与 Redis owner binding 所需的全部中立字段。 +/// Provider 写入 pending payload 与 Redis owner binding 所需的全部中立字段 #[derive(Clone, PartialEq, Eq)] pub struct AuthorizationOwnerBinding { owner: AuthorizationOwner, @@ -193,17 +193,17 @@ impl fmt::Debug for AuthorizationOwnerBinding { } } -/// OAuth 完成时应创建新账号还是 CAS 更新既有 credential。 +/// OAuth 完成时应创建新账号还是 CAS 更新既有 credential /// /// 重新授权只绑定稳定的账号身份:credential revision 由服务端在临近写入时读取, -/// 长流程期间的后台刷新不得让恢复操作失效。 +/// 长流程期间的后台刷新不得让恢复操作失效 #[derive(Debug, Clone, PartialEq, Eq)] pub enum AuthorizationMutationTarget { Create { name: String }, Reauthorize { account_id: ProviderAccountId }, } -/// 必须完整进入 Provider opaque pending payload、并在 complete 后原样恢复的事务信封。 +/// 必须完整进入 Provider opaque pending payload、并在 complete 后原样恢复的事务信封 #[derive(Debug, Clone, PartialEq, Eq)] pub struct PendingAuthorizationMutation { provider_kind: ProviderKind, @@ -269,7 +269,7 @@ impl PendingAuthorizationMutation { } impl PendingAuthorizationMutation { - /// 编码 v1 中立事务字段;版本号的位置和 Provider 私有外层文档由 adapter 保持。 + /// 编码 v1 中立事务字段;版本号的位置和 Provider 私有外层文档由 adapter 保持 #[must_use] pub fn to_storage_v1(&self) -> Map { let target = match &self.target { @@ -311,11 +311,11 @@ impl PendingAuthorizationMutation { document } - /// 恢复 adapter 已确认版本为 v1 的中立事务字段。 + /// 恢复 adapter 已确认版本为 v1 的中立事务字段 /// /// # Errors /// - /// 文档字段、Provider kind 或账号身份非法时返回错误,不携带原始 pending 内容。 + /// 文档字段、Provider kind 或账号身份非法时返回错误,不携带原始 pending 内容 pub fn from_storage_v1(value: Value) -> Result { let invalid = || AdminError::invalid("invalid pending authorization mutation"); let document: StoredAuthorizationMutationV1 = @@ -385,7 +385,7 @@ enum StoredAuthorizationOwnerV1 { System, } -/// Provider 授权请求。 +/// Provider 授权请求 #[derive(Debug, Clone, PartialEq)] pub struct StartAuthorization { pub context: MutationContext, @@ -394,7 +394,7 @@ pub struct StartAuthorization { pub outbound_proxy: Option, } -/// OAuth 流程启动结果。 +/// OAuth 流程启动结果 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AuthorizationStarted { pub flow_id: String, @@ -402,35 +402,37 @@ pub struct AuthorizationStarted { pub expires_at: DateTime, } -/// refresh/rotate/reauthorize 的 lease 或 completion 生命周期。 +/// refresh/rotate/reauthorize 的 lease 或 completion 生命周期 /// -/// 该 guard 不可 Clone。Admin 只能在 Store CAS 与审计事务成功后调用 `finish`;失败路径直接 -/// drop,使 Provider 可以释放 lease 或执行补偿。 +/// 该 guard 不可 Clone +/// Admin 只能在 Store CAS 与审计事务成功后调用 `finish`;失败路径直接 +/// drop,使 Provider 可以释放 lease 或执行补偿 pub trait CredentialCommitGuard: Send + 'static { fn finish(self: Box); } -/// OAuth pending claim 在 Store 事务结果确定后执行的显式结算动作。 +/// OAuth pending claim 在 Store 事务结果确定后执行的显式结算动作 /// /// OAuth provider 必须在 credential 准备阶段持有 claim:Store 提交成功后消费 flow,提交或 -/// 校验失败后释放 flow。异步结算不能依赖 `Drop`,否则请求提前返回会让同一授权流无谓地 -/// 进入短暂的不可重试状态。 +/// 校验失败后释放 flow +/// 异步结算不能依赖 `Drop`,否则请求提前返回会让同一授权流无谓地 +/// 进入短暂的不可重试状态 #[async_trait] pub trait AuthorizationCommitGuard: Send + 'static { - /// Store 已提交 OAuth credential 后消费对应的一次性 flow。 + /// Store 已提交 OAuth credential 后消费对应的一次性 flow async fn commit(self: Box) -> Result<(), AdminError>; - /// Store 未提交 OAuth credential 时释放 claim,允许同一 flow 重试。 + /// Store 未提交 OAuth credential 时释放 claim,允许同一 flow 重试 async fn abort(self: Box) -> Result<(), AdminError>; } -/// OAuth complete 后由 Provider 返回的准备结果;Store 仍是唯一提交者。 +/// OAuth complete 后由 Provider 返回的准备结果;Store 仍是唯一提交者 pub enum PreparedAuthorizationCredential { Create(Box), Reauthorize(Box), } -/// Provider 从 opaque pending payload 恢复的信封与已验证 credential 必须一起返回。 +/// Provider 从 opaque pending payload 恢复的信封与已验证 credential 必须一起返回 pub struct PreparedAuthorizationCommit { pub pending: PendingAuthorizationMutation, pub credential: PreparedAuthorizationCredential, @@ -447,14 +449,14 @@ impl fmt::Debug for PreparedAuthorizationCommit { } } -/// Store 可持久化的 OAuth credential facts,不携带 Provider guard。 +/// Store 可持久化的 OAuth credential facts,不携带 Provider guard #[derive(Debug, Clone, PartialEq)] pub enum AuthorizationCredentialCommit { Create(Box), Reauthorize(Box), } -/// Admin 交给 Store 的 OAuth 原子事务命令。 +/// Admin 交给 Store 的 OAuth 原子事务命令 #[derive(Debug, Clone, PartialEq)] pub struct AuthorizationCommit { pub key: AuthorizationReceiptKey, @@ -463,7 +465,7 @@ pub struct AuthorizationCommit { pub credential: AuthorizationCredentialCommit, } -/// 回执以 Provider、授权流和管理员身份绑定;不持久化原始 flow 或回调材料。 +/// 回执以 Provider、授权流和管理员身份绑定;不持久化原始 flow 或回调材料 #[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct AuthorizationReceiptKey { provider_kind: ProviderKind, @@ -531,10 +533,10 @@ pub struct AuthorizationCommitResult { pub newly_committed: bool, } -/// OAuth 准备结果拆解后的 Store 命令与两个结算 guard。 +/// OAuth 准备结果拆解后的 Store 命令与两个结算 guard /// /// Store 成功时先结束 credential guard,再消费 OAuth claim;失败时丢弃 credential guard 并 -/// 释放 OAuth claim,使同一授权流可以重试。 +/// 释放 OAuth claim,使同一授权流可以重试 pub(crate) struct AuthorizationCommitSettlement { pub(crate) command: AuthorizationCommit, pub(crate) credential_guard: Option>, @@ -554,7 +556,7 @@ impl PreparedAuthorizationCommit { } } - /// 让 OAuth claim 覆盖准备完成到 Store 事务结算之间的全部窗口。 + /// 让 OAuth claim 覆盖准备完成到 Store 事务结算之间的全部窗口 #[must_use] pub fn with_authorization_guard(mut self, guard: Box) -> Self { self.authorization_guard = Some(guard); @@ -599,7 +601,7 @@ impl PreparedAuthorizationCommit { } } -/// 完成 Provider OAuth 流程。 +/// 完成 Provider OAuth 流程 #[derive(Clone, PartialEq, Eq)] pub struct CompleteAuthorization { pub settings: Option, @@ -619,14 +621,14 @@ impl fmt::Debug for CompleteAuthorization { } } -/// Credential 生命周期写操作。 +/// Credential 生命周期写操作 #[derive(Debug, Clone, PartialEq, Eq)] pub struct CredentialMutation { pub context: MutationContext, pub account_id: ProviderAccountId, } -/// Credential 写入提交结果。 +/// Credential 写入提交结果 #[derive(Debug, Clone, PartialEq, Eq)] pub struct CredentialMutationResult { pub config_revision: Revision, @@ -634,28 +636,28 @@ pub struct CredentialMutationResult { pub credential_revision: Option, } -/// 同一 Provider 管理范围内的 credential 批量删除。 +/// 同一 Provider 管理范围内的 credential 批量删除 #[derive(Debug, Clone, PartialEq, Eq)] pub struct CredentialDeletion { pub context: MutationContext, pub account_ids: Vec, } -/// Credential 批量删除提交结果。 +/// Credential 批量删除提交结果 #[derive(Debug, Clone, PartialEq, Eq)] pub struct CredentialDeletionResult { pub config_revision: Revision, pub account_ids: Vec, } -/// Provider-owned token 轮换命令。 +/// Provider-owned token 轮换命令 pub struct RotateCredential { pub mutation: CredentialMutation, pub provider_material: ProviderDocument, pub settings: Option, } -/// Provider 校验手工轮换材料时所需的非事务输入。 +/// Provider 校验手工轮换材料时所需的非事务输入 pub struct PrepareCredentialRotation { pub account: AccountRecord, pub provider_material: ProviderDocument, @@ -671,7 +673,7 @@ impl fmt::Debug for PrepareCredentialRotation { } } -/// Provider 已验证、可由 Store 以 credential revision CAS 原子提交的轮换 facts。 +/// Provider 已验证、可由 Store 以 credential revision CAS 原子提交的轮换 facts #[derive(Debug, Clone, PartialEq)] pub struct PreparedCredentialRotationFacts { pub account_id: ProviderAccountId, @@ -681,9 +683,9 @@ pub struct PreparedCredentialRotationFacts { pub name: String, pub email: Option, pub plan_type: Option, - /// Token 刷新保留提交时的资料,不以准备阶段的副本覆盖新套餐。 + /// Token 刷新保留提交时的资料,不以准备阶段的副本覆盖新套餐 pub preserve_profile: bool, - /// 连接配置变更保留当前凭据健康状态与错误事实。 + /// 连接配置变更保留当前凭据健康状态与错误事实 pub preserve_credential_state: bool, pub provider_material: ProviderDocument, pub has_refresh_token: bool, @@ -691,7 +693,7 @@ pub struct PreparedCredentialRotationFacts { pub next_refresh_at: Option>, } -/// Provider 返回的轮换准备结果;guard 必须覆盖后续 Store CAS 与审计事务。 +/// Provider 返回的轮换准备结果;guard 必须覆盖后续 Store CAS 与审计事务 pub struct PreparedCredentialRotation { facts: PreparedCredentialRotationFacts, guard: Box, @@ -706,7 +708,7 @@ impl PreparedCredentialRotation { Self { facts, guard } } - /// 仅修改连接配置时,不把配置更新视为凭据恢复成功。 + /// 仅修改连接配置时,不把配置更新视为凭据恢复成功 #[must_use] pub fn preserving_credential_state(mut self) -> Self { self.facts.preserve_credential_state = true; @@ -739,7 +741,7 @@ impl fmt::Debug for PreparedCredentialRotation { } } -/// Admin 交给 Store 的轮换或 refresh 事务命令。 +/// Admin 交给 Store 的轮换或 refresh 事务命令 #[derive(Debug, Clone, PartialEq)] pub struct CredentialRotationCommit { pub prepared: PreparedCredentialRotationFacts, @@ -757,16 +759,16 @@ impl fmt::Debug for RotateCredential { } } -/// 通用账号用量能否可靠归属到一个 Provider quota 窗口。 +/// 通用账号用量能否可靠归属到一个 Provider quota 窗口 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum QuotaLocalUsageAttribution { - /// 窗口覆盖账号的全部请求,可按账号与时间范围聚合。 + /// 窗口覆盖账号的全部请求,可按账号与时间范围聚合 AccountWide, - /// 窗口需要 Provider / 模型级归属,通用账号聚合不可用。 + /// 窗口需要 Provider / 模型级归属,通用账号聚合不可用 Unavailable, } -/// Provider quota bucket 中窗口的官方位置语义。 +/// Provider quota bucket 中窗口的官方位置语义 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ProviderQuotaWindowRole { Primary, @@ -785,7 +787,7 @@ impl ProviderQuotaWindowRole { } } -/// 一个 Provider quota 窗口的公共投影。 +/// 一个 Provider quota 窗口的公共投影 #[derive(Debug, Clone, PartialEq)] pub struct ProviderQuotaWindow { pub key: String, @@ -803,14 +805,14 @@ pub struct ProviderQuotaWindow { pub provider_data: Option, } -/// 账号用量统计周期,按周优先、月次之选择;短期限流窗口不参与统计面板。 +/// 账号用量统计周期,按周优先、月次之选择;短期限流窗口不参与统计面板 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum AccountUsagePeriod { Weekly, Monthly, } -/// Provider 解释 quota 所需的公共请求事实。 +/// Provider 解释 quota 所需的公共请求事实 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderQuotaRequest { pub account_id: ProviderAccountId, @@ -818,21 +820,21 @@ pub struct ProviderQuotaRequest { pub rolling_usage: Option, } -/// Provider 已解析的 quota 结果及其不透明差异字段。 +/// Provider 已解析的 quota 结果及其不透明差异字段 #[derive(Debug, Clone, PartialEq)] pub struct ProviderQuota { - /// 上游额度响应明确提供的套餐,可用于补全账号展示。 + /// 上游额度响应明确提供的套餐,可用于补全账号展示 pub plan_type: Option, pub observed_at: Option>, pub refresh_token_expires_at: Option>, pub windows: Vec, pub credits: Option, - /// 展示用快照级触顶事实(顶层或任一窗口触顶);不参与账号五态派生。 + /// 展示用快照级触顶事实(顶层或任一窗口触顶);不参与账号五态派生 pub limit_reached: bool, pub provider_data: Option, } -/// 上游点数余额的展示事实,保留原始十进制文本以避免精度损失。 +/// 上游点数余额的展示事实,保留原始十进制文本以避免精度损失 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderQuotaCredits { pub has_credits: bool, @@ -840,7 +842,7 @@ pub struct ProviderQuotaCredits { pub balance: Option, } -/// 空值和 `unknown` 代表未提供套餐;新的套餐标识仍按明确值保留。 +/// 空值和 `unknown` 代表未提供套餐;新的套餐标识仍按明确值保留 pub(crate) fn explicit_plan_type(value: Option<&str>) -> Option<&str> { value.filter(|value| { let value = value.trim(); @@ -848,7 +850,7 @@ pub(crate) fn explicit_plan_type(value: Option<&str>) -> Option<&str> { }) } -/// 按需查询的订阅周期,不参与额度或账号可用性判断。 +/// 按需查询的订阅周期,不参与额度或账号可用性判断 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderSubscription { pub starts_at: Option>, @@ -859,14 +861,14 @@ pub struct ProviderSubscription { pub observed_at: DateTime, } -/// 按需汇聚的个人信息;资料查询失败不丢弃可用的订阅结果。 +/// 按需汇聚的个人信息;资料查询失败不丢弃可用的订阅结果 #[derive(Debug, Clone, PartialEq)] pub struct AccountPersonalInfo { pub profile: Result, pub subscription: Option, } -/// Provider 官方个人资料中的累计摘要。 +/// Provider 官方个人资料中的累计摘要 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderProfileStatisticsSummary { pub total_text_tokens: Option, @@ -876,14 +878,14 @@ pub struct ProviderProfileStatisticsSummary { pub longest_streak_days: Option, } -/// Provider 官方个人资料中的单日 Token bucket。 +/// Provider 官方个人资料中的单日 Token bucket #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderProfileDailyUsage { pub date: NaiveDate, pub tokens: u64, } -/// Provider 官方个人资料中的插件或 Skill 调用排行。 +/// Provider 官方个人资料中的插件或 Skill 调用排行 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderProfileInvocation { pub invocation_type: String, @@ -894,7 +896,7 @@ pub struct ProviderProfileInvocation { pub usage_count: Option, } -/// Provider 官方个人资料中的活动洞察。 +/// Provider 官方个人资料中的活动洞察 #[derive(Debug, Clone, PartialEq)] pub struct ProviderProfileActivityInsights { pub fast_mode_percent: Option, @@ -906,7 +908,7 @@ pub struct ProviderProfileActivityInsights { pub invocations: Option>, } -/// Provider 已解释的官方个人资料统计。 +/// Provider 已解释的官方个人资料统计 #[derive(Debug, Clone, PartialEq)] pub struct ProviderProfileStatistics { pub display_name: Option, @@ -918,16 +920,16 @@ pub struct ProviderProfileStatistics { pub activity_insights: ProviderProfileActivityInsights, } -/// Provider 头像正文的中立字节流;公共层不解释 MIME,也不限制总字节数。 +/// Provider 头像正文的中立字节流;公共层不解释 MIME,也不限制总字节数 pub type ProviderProfileAvatarStream = Pin> + Send + 'static>>; -/// Provider 头像流在响应开始后的通用失败,不携带上游 URL 或正文。 +/// Provider 头像流在响应开始后的通用失败,不携带上游 URL 或正文 #[derive(Debug, thiserror::Error)] #[error("provider profile avatar stream failed")] pub struct ProviderProfileAvatarStreamError; -/// Provider 已校验并打开的官方头像响应。 +/// Provider 已校验并打开的官方头像响应 pub struct ProviderProfileAvatar { pub content_type: Option, pub content_length: Option, @@ -947,7 +949,7 @@ impl fmt::Debug for ProviderProfileAvatar { } } -/// Provider 返回的一张安全主动额度重置卡。 +/// Provider 返回的一张安全主动额度重置卡 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderResetCredit { pub id: String, @@ -957,14 +959,14 @@ pub struct ProviderResetCredit { pub reset_type: Option, } -/// Provider 主动额度重置卡列表。 +/// Provider 主动额度重置卡列表 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderResetCredits { pub available_count: u64, pub credits: Vec, } -/// 一次主动额度重置卡消费命令。 +/// 一次主动额度重置卡消费命令 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ConsumeProviderResetCredit { pub account_id: ProviderAccountId, @@ -972,7 +974,7 @@ pub struct ConsumeProviderResetCredit { pub redeem_request_id: Uuid, } -/// Provider 返回的主动额度重置卡消费结果。 +/// Provider 返回的主动额度重置卡消费结果 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderResetCreditResult { pub code: String, @@ -980,17 +982,18 @@ pub struct ProviderResetCreditResult { } impl ProviderQuota { - /// 保留账号已有的套餐子类型,仅在缺失时使用上游额度快照补全。 + /// 保留账号已有的套餐子类型,仅在缺失时使用上游额度快照补全 pub(crate) fn fill_missing_plan_type(&self, account_plan_type: &mut Option) { if explicit_plan_type(account_plan_type.as_deref()).is_none() { *account_plan_type = explicit_plan_type(self.plan_type.as_deref()).map(str::to_owned); } } - /// 选择账号用量统计的周/月窗口,同时返回展示所需的周期事实。 + /// 选择账号用量统计的周/月窗口,同时返回展示所需的周期事实 /// /// 只使用边界完整、可归属到整个账号的窗口;周优先于月,同周期保持 - /// Provider 投影顺序。没有符合条件的窗口时不回退到短期或历史累计。 + /// Provider 投影顺序 + /// 没有符合条件的窗口时不回退到短期或历史累计 #[must_use] pub fn usage_window(&self) -> Option<(&ProviderQuotaWindow, AccountUsagePeriod)> { self.usage_windows() @@ -998,7 +1001,7 @@ impl ProviderQuota { .min_by_key(|(_, period)| *period) } - /// 统计与预测共用窗口归属规则,避免把模型专属桶或短期限流当作账号容量。 + /// 统计与预测共用窗口归属规则,避免把模型专属桶或短期限流当作账号容量 pub(crate) fn usage_windows( &self, ) -> impl Iterator { @@ -1020,11 +1023,12 @@ impl ProviderQuota { }) } - /// 返回 Dashboard 使用的代表性额度比例。 + /// 返回 Dashboard 使用的代表性额度比例 /// /// 优先使用覆盖账号全部请求的窗口;同一归属范围内再依次使用短周期、周、月 - /// 和其它窗口,同一优先级取较高的已用比例。这里只解释跨 Provider 共享的窗口 - /// 语义,绝不读取 Provider 私有 JSON。 + /// 和其它窗口,同一优先级取较高的已用比例 + /// 这里只解释跨 Provider 共享的窗口 + /// 语义,绝不读取 Provider 私有 JSON #[must_use] pub fn representative_used_percent(&self) -> Option { if self.limit_reached { @@ -1034,7 +1038,7 @@ impl ProviderQuota { .map(|(_, used_percent)| used_percent) } - /// 选择 Dashboard 展示的真实窗口;没有百分比时仍保留 Provider 的滚动窗口语义。 + /// 选择 Dashboard 展示的真实窗口;没有百分比时仍保留 Provider 的滚动窗口语义 #[must_use] pub fn representative_window(&self) -> Option<&ProviderQuotaWindow> { self.representative_used_window() @@ -1048,10 +1052,10 @@ impl ProviderQuota { }) } - /// 将已确认的账号级额度耗尽事实投影到展示窗口。 + /// 将已确认的账号级额度耗尽事实投影到展示窗口 /// /// Provider 已指出具体触顶窗口时只归一化这些窗口;否则归一化 Dashboard - /// 同样会选择的代表窗口,避免把多个独立额度窗口全部伪造成已用尽。 + /// 同样会选择的代表窗口,避免把多个独立额度窗口全部伪造成已用尽 pub fn apply_limit_reached_display(&mut self) { if !self.limit_reached { return; @@ -1131,21 +1135,21 @@ fn is_week_window(seconds: u64) -> bool { seconds > 0 && seconds.abs_diff(WEEK_SECONDS) <= WEEK_SECONDS / 20 } -/// Provider 实时模型目录的一项。 +/// Provider 实时模型目录的一项 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderModel { pub id: UpstreamModelId, pub name: String, } -/// Provider 实时模型目录。 +/// Provider 实时模型目录 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderModels { pub models: Vec, pub observed_at: Option>, } -/// Provider 原生模型目录正文;wire 语义由对应 Provider 拥有,公共层只搬运不解释字段。 +/// Provider 原生模型目录正文;wire 语义由对应 Provider 拥有,公共层只搬运不解释字段 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderModelCatalogDocument { pub document: RawJsonPayload, @@ -1153,7 +1157,7 @@ pub struct ProviderModelCatalogDocument { pub observed_at: DateTime, } -/// 插件账号回调的服务端过滤条件;Provider 缺省时跨全部已注册类型分页。 +/// 插件账号回调的服务端过滤条件;Provider 缺省时跨全部已注册类型分页 #[derive(Debug, Clone, PartialEq, Eq)] pub struct PluginAccountListQuery { pub provider_kind: Option, @@ -1167,7 +1171,7 @@ pub struct PluginAccountPage { pub next_cursor: Option, } -/// 原始凭据与当前账号 revision 的同一读取结果。 +/// 原始凭据与当前账号 revision 的同一读取结果 #[derive(Clone, PartialEq)] pub struct PluginAccountCredential { pub account: AccountRecord, @@ -1184,7 +1188,7 @@ impl fmt::Debug for PluginAccountCredential { } } -/// Runtime 已完成 wire 校验、Admin 仍需绑定当前账号和 revision 的 prepared facts。 +/// Runtime 已完成 wire 校验、Admin 仍需绑定当前账号和 revision 的 prepared facts #[derive(Debug, Clone, PartialEq)] pub enum PreparedPluginAccountSave { Create(PreparedCredentialCreate), @@ -1201,20 +1205,20 @@ pub struct PluginAccountSaveResult { pub credential_revision: Revision, } -/// Provider 执行 refresh 时所需的当前公共账号事实。 +/// Provider 执行 refresh 时所需的当前公共账号事实 #[derive(Debug, Clone, PartialEq, Eq)] pub struct PrepareCredentialRefresh { pub account: AccountRecord, } -/// Provider 敏感导出结果。 +/// Provider 敏感导出结果 pub struct ProviderExport { pub provider_kind: ProviderKind, pub account_ids: Vec, pub document: ProviderDocument, } -/// Store 为 Provider 导出序列化准备的最小输入;material 对公共层保持不透明。 +/// Store 为 Provider 导出序列化准备的最小输入;material 对公共层保持不透明 #[derive(Debug, Clone, PartialEq)] pub struct ProviderExportCredentialInput { pub account: AccountRecord, @@ -1232,20 +1236,20 @@ impl fmt::Debug for ProviderExport { } } -/// 统一账号目录的一行完整结果。 +/// 统一账号目录的一行完整结果 #[derive(Debug, Clone, PartialEq)] pub struct AccountDirectoryItem { pub account: AccountRecord, pub capacity: super::accounts::AccountCapacity, pub capabilities: super::accounts::ProviderAccountCapabilities, - /// Provider 提供的套餐展示名称;未识别到套餐时为空。 + /// Provider 提供的套餐展示名称;未识别到套餐时为空 pub plan_type_display: Option, pub projection: gateway_core::account::AccountStatusProjection, pub usage: Option, pub quota: ProviderQuota, } -/// 统一账号目录页。 +/// 统一账号目录页 #[derive(Debug, Clone, PartialEq)] pub struct AccountDirectoryPage { pub config_revision: Revision, @@ -1254,14 +1258,14 @@ pub struct AccountDirectoryPage { pub summary: AccountSummary, } -/// 凭据刷新提交后的完整账号结果。 +/// 凭据刷新提交后的完整账号结果 #[derive(Debug, Clone, PartialEq)] pub struct AccountRefreshResult { pub config_revision: Revision, pub account: AccountDirectoryItem, } -/// 多 Provider 导出文档集合。 +/// 多 Provider 导出文档集合 pub struct AccountExportBundle { pub exported_at: DateTime, pub documents: Vec, diff --git a/backend/crates/gateway-admin/src/model/proxies.rs b/backend/crates/gateway-admin/src/model/proxies.rs index a13c1723c..d2be116f2 100644 --- a/backend/crates/gateway-admin/src/model/proxies.rs +++ b/backend/crates/gateway-admin/src/model/proxies.rs @@ -1,4 +1,4 @@ -//! 可复用的账号出口配置,以及脱敏后的连通性测试结果。 +//! 可复用的账号出口配置,以及脱敏后的连通性测试结果 use chrono::{DateTime, Utc}; use gateway_core::account::{OutboundProxy, RequestLocation}; @@ -39,7 +39,7 @@ pub struct ProxyAccountRef { pub enabled: bool, } -/// 按代理查询关联账号,分页与搜索均在存储层执行。 +/// 按代理查询关联账号,分页与搜索均在存储层执行 #[derive(Debug, Clone)] pub struct ProxyAccountListQuery { pub proxy_id: String, @@ -94,7 +94,7 @@ pub struct ProxyPage { #[derive(Debug, Clone)] pub struct NewProxy { pub auto_location: bool, - /// 由管理用例探测,随配置原子保存,不接受 HTTP 调用方提供结果。 + /// 由管理用例探测,随配置原子保存,不接受 HTTP 调用方提供结果 pub test: Option, pub location: Option, pub name: String, @@ -105,7 +105,7 @@ pub struct NewProxy { pub struct UpdateProxy { pub auto_location: Option, pub test: Option, - /// 外层为空保留配置,内层为空恢复全局继承。 + /// 外层为空保留配置,内层为空恢复全局继承 pub location: Option>, pub id: String, pub revision: Revision, @@ -119,7 +119,7 @@ pub struct ProxyMutation { pub record: ProxyRecord, } -/// 检测失败与连通性分开,双栈冲突不得继续应用旧位置。 +/// 检测失败与连通性分开,双栈冲突不得继续应用旧位置 #[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)] #[serde(tag = "status", rename_all = "camelCase")] pub enum ProxyLocationDetection { @@ -155,7 +155,7 @@ impl ProxyRecord { } } - /// 未知出口可暂用上次成功结果,已知出口变化或时区冲突必须撤销旧覆盖。 + /// 未知出口可暂用上次成功结果,已知出口变化或时区冲突必须撤销旧覆盖 #[must_use] pub fn detected_location_after_test( &self, diff --git a/backend/crates/gateway-admin/src/model/quota_forecast.rs b/backend/crates/gateway-admin/src/model/quota_forecast.rs index 65bbae662..ca8f67c17 100644 --- a/backend/crates/gateway-admin/src/model/quota_forecast.rs +++ b/backend/crates/gateway-admin/src/model/quota_forecast.rs @@ -1,4 +1,4 @@ -//! 账号额度的只读容量估算,不参与金额结算或账号调度。 +//! 账号额度的只读容量估算,不参与金额结算或账号调度 use chrono::{DateTime, Duration, Utc}; @@ -40,9 +40,9 @@ pub struct QuotaForecastSource { pub usd: Option, } -/// 为目标周期选择唯一的预测源窗口;缺少同周期窗口时复用最短的可统计窗口。 +/// 为目标周期选择唯一的预测源窗口;缺少同周期窗口时复用最短的可统计窗口 /// -/// Admin 的历史查询与最终投影必须共用这里的顺序,避免查询不会进入响应的窗口。 +/// Admin 的历史查询与最终投影必须共用这里的顺序,避免查询不会进入响应的窗口 pub(crate) fn quota_forecast_source_window( quota: &ProviderQuota, period: AccountUsagePeriod, @@ -53,8 +53,8 @@ pub(crate) fn quota_forecast_source_window( .or_else(|| quota.usage_windows().min_by_key(|(_, period)| *period)) } -/// 优先预测真实的对应窗口;缺少对应周期时只给出明确标识的 7/30 天容量折算。 -/// 本地日志不能证明站外消耗或完整留存,因此即使样本充足也不声称官方额度。 +/// 优先预测真实的对应窗口;缺少对应周期时只给出明确标识的 7/30 天容量折算 +/// 本地日志不能证明站外消耗或完整留存,因此即使样本充足也不声称官方额度 #[must_use] pub fn account_quota_forecasts( quota: &ProviderQuota, @@ -178,9 +178,9 @@ impl AccountQuotaForecast { }; self.low_sample = sample.sampled_percent < LOW_SAMPLE_PERCENT || (method == QuotaForecastMethod::Incremental && sample.block_count < 2); - // 漏记和个别缺失只影响精度,仍按已记录数值估算,不按请求数补齐未知消耗。 - // 预测是近似展示值;不复用为账单金额,也不把月折算当成自然月或额外余额。 - // 已发生的用量保持本周期累计,只把近期消耗比例用于尚未使用的额度。 + // 漏记和个别缺失只影响精度,仍按已记录数值估算,不按请求数补齐未知消耗 + // 预测是近似展示值;不复用为账单金额,也不把月折算当成自然月或额外余额 + // 已发生的用量保持本周期累计,只把近期消耗比例用于尚未使用的额度 let factor = self.target_seconds as f64 / seconds as f64; let remaining_factor = (100.0 - percent) / sample.sampled_percent; let remaining_tokens = Some(sample.usage.tokens) diff --git a/backend/crates/gateway-admin/src/model/quota_forecast_sampling.rs b/backend/crates/gateway-admin/src/model/quota_forecast_sampling.rs index aaefcec1e..5fe3498af 100644 --- a/backend/crates/gateway-admin/src/model/quota_forecast_sampling.rs +++ b/backend/crates/gateway-admin/src/model/quota_forecast_sampling.rs @@ -1,10 +1,10 @@ -//! 预测专用的配对采样;不改变完整交付用量或费用账本的统计口径。 +//! 预测专用的配对采样;不改变完整交付用量或费用账本的统计口径 use chrono::{DateTime, Utc}; use super::provider_credentials::ProviderDocument; -/// 限制带 Provider 文档的历史点数量;累计用量仍覆盖整个查询区间。 +/// 限制带 Provider 文档的历史点数量;累计用量仍覆盖整个查询区间 pub const MAX_FORECAST_HISTORY_POINTS: usize = 128; const MIN_BLOCK_PERCENT: f64 = 5.0; const RECENT_BLOCKS: usize = 3; @@ -24,7 +24,7 @@ pub struct QuotaForecastUsage { } impl QuotaForecastUsage { - /// 累计计数倒退表示不能配对,不用饱和减法掩盖数据断点。 + /// 累计计数倒退表示不能配对,不用饱和减法掩盖数据断点 fn difference(&self, baseline: &Self) -> Option { let usd = self.usd - baseline.usd; if !usd.is_finite() || usd < -1e-9 { @@ -68,7 +68,7 @@ pub struct QuotaForecastHistory { pub pending_request_count: u64, } -/// 具体 Provider 从自己的历史文档解释出的额度事实。 +/// 具体 Provider 从自己的历史文档解释出的额度事实 #[derive(Debug, Clone, PartialEq)] pub struct QuotaForecastObservation { pub used_percent: f64, @@ -101,13 +101,13 @@ pub struct QuotaForecastSample { pub observation_count: usize, pub pending_request_count: u64, pub discontinuous: bool, - /// 本周期已记录用量;近期采样仅用于预测剩余量,不能覆盖这份累计值。 + /// 本周期已记录用量;近期采样仅用于预测剩余量,不能覆盖这份累计值 pub cycle_usage: QuotaForecastUsage, pub usage: QuotaForecastUsage, } -/// 对同一额度段的累计点形成至少 5 个百分点的连续块,使用最近三个完整块及尾部。 -/// 不平均逐请求比值;重复百分比不增加块数,5/10 个百分点仅是量化噪声门槛。 +/// 对同一额度段的累计点形成至少 5 个百分点的连续块,使用最近三个完整块及尾部 +/// 不平均逐请求比值;重复百分比不增加块数,5/10 个百分点仅是量化噪声门槛 #[must_use] pub fn select_forecast_sample( key: String, @@ -139,7 +139,7 @@ pub fn select_forecast_sample( usage: current.usage.clone(), }; history.push(current); - // 重置边界不在查询起点时,从首个新观测重新计数,避免把旧周期累计带入总量。 + // 重置边界不在查询起点时,从首个新观测重新计数,避免把旧周期累计带入总量 let mut cycle_baseline = interrupted.then(|| &history[0]); let mut anchors: Vec<&QuotaForecastPoint> = Vec::new(); let mut high_water: Option<&QuotaForecastPoint> = None; @@ -149,7 +149,7 @@ pub fn select_forecast_sample( continue; } if point.used_percent < previous.used_percent { - // 一个百分点内的回落可能是量化/并发乱序;更大的回落不能跨越。 + // 一个百分点内的回落可能是量化/并发乱序;更大的回落不能跨越 if previous.used_percent - point.used_percent <= 1.0 { continue; } @@ -198,7 +198,7 @@ pub fn select_forecast_sample( sample.sampled_percent = progress; sample.block_count = anchors.len() - baseline_index - 1; sample.usage = usage; - // 断点后的新基线已有足够进度时,只消费新段;旧段从不参与本次估算。 + // 断点后的新基线已有足够进度时,只消费新段;旧段从不参与本次估算 sample.discontinuous = false; sample } diff --git a/backend/crates/gateway-admin/src/model/retention.rs b/backend/crates/gateway-admin/src/model/retention.rs index 60085feac..6f20d08f7 100644 --- a/backend/crates/gateway-admin/src/model/retention.rs +++ b/backend/crates/gateway-admin/src/model/retention.rs @@ -1,4 +1,4 @@ -//! 历史数据保留规则;存储实现只接受已校验的窗口。 +//! 历史数据保留规则;存储实现只接受已校验的窗口 use super::AdminError; diff --git a/backend/crates/gateway-admin/src/model/settings.rs b/backend/crates/gateway-admin/src/model/settings.rs index 316aedb3d..1b20732cc 100644 --- a/backend/crates/gateway-admin/src/model/settings.rs +++ b/backend/crates/gateway-admin/src/model/settings.rs @@ -1,4 +1,4 @@ -//! Runtime settings 与明文管理员 API Key 的语义模型。 +//! Runtime settings 与明文管理员 API Key 的语义模型 use std::{collections::BTreeMap, fmt}; @@ -8,21 +8,21 @@ use gateway_core::routing::{ProviderKind, PublicModelId, UpstreamModelId}; use super::Revision; -/// 客户端模型到上游模型的全局精确映射。 +/// 客户端模型到上游模型的全局精确映射 pub type ModelMappings = BTreeMap; -/// Provider-owned 请求画像选择;Provider ID 是唯一命名空间。 +/// Provider-owned 请求画像选择;Provider ID 是唯一命名空间 pub type ProviderRequestProfiles = BTreeMap; -/// Provider-owned 请求画像的部分更新;`None` 只表示显式删除该 Provider 的历史选择。 +/// Provider-owned 请求画像的部分更新;`None` 只表示显式删除该 Provider 的历史选择 pub type ProviderRequestProfileUpdates = BTreeMap>; -/// 账号调度策略;由 Core 拥有稳定值与 wire 映射。 +/// 账号调度策略;由 Core 拥有稳定值与 wire 映射 pub use gateway_core::account::RotationStrategy; -/// 完整运行设置事实。 +/// 完整运行设置事实 #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields)] pub struct RuntimeSettings { @@ -60,13 +60,13 @@ pub struct RuntimeSettings { pub updated_at: DateTime, } -/// 原子替换运行设置的命令。 +/// 原子替换运行设置的命令 #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields)] pub struct ReplaceRuntimeSettings { - /// 读取设置时的版本;与写入在同一事务内比较,防止覆盖并发更新。 + /// 读取设置时的版本;与写入在同一事务内比较,防止覆盖并发更新 pub expected_revision: Revision, - /// 只覆盖提交的 Provider;未提交项保留当前持久值。 + /// 只覆盖提交的 Provider;未提交项保留当前持久值 pub request_profile_updates: ProviderRequestProfileUpdates, pub request_location_enabled: bool, pub request_location: gateway_core::account::RequestLocation, @@ -99,7 +99,7 @@ pub struct ReplaceRuntimeSettings { pub account_warmup_model: Option, } -/// 明文管理员 API Key;按产品约束明文落库,但禁止 Debug 泄漏。 +/// 明文管理员 API Key;按产品约束明文落库,但禁止 Debug 泄漏 #[derive(Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(transparent)] pub struct AdminApiKey(String); @@ -115,7 +115,7 @@ impl AdminApiKey { &self.0 } - /// 仅供显式 regenerate 响应读取一次。 + /// 仅供显式 regenerate 响应读取一次 #[must_use] pub fn expose_for_response(&self) -> &str { &self.0 @@ -128,7 +128,7 @@ impl fmt::Debug for AdminApiKey { } } -/// 管理员 API Key 更新结果。 +/// 管理员 API Key 更新结果 #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields)] pub struct AdminApiKeyMutation { @@ -136,7 +136,7 @@ pub struct AdminApiKeyMutation { pub exists: bool, } -/// 新管理员 API Key 的一次性返回结果。 +/// 新管理员 API Key 的一次性返回结果 #[derive(serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields)] pub struct RegeneratedAdminApiKey { diff --git a/backend/crates/gateway-admin/src/model/system.rs b/backend/crates/gateway-admin/src/model/system.rs index 5fc93bf47..5c35ce519 100644 --- a/backend/crates/gateway-admin/src/model/system.rs +++ b/backend/crates/gateway-admin/src/model/system.rs @@ -1,9 +1,9 @@ -//! 版本、自更新、回滚与进程重启的 UTC 语义模型。 +//! 版本、自更新、回滚与进程重启的 UTC 语义模型 use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -/// 本次检查或安装选择的更新稳定性范围,具体版本准入由 Host 判定。 +/// 本次检查或安装选择的更新稳定性范围,具体版本准入由 Host 判定 #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] pub enum SystemUpdateChannel { @@ -28,7 +28,7 @@ impl SystemUpdateChannel { } } -/// Host 返回的本次检查通道及当前构建允许选择的范围。 +/// Host 返回的本次检查通道及当前构建允许选择的范围 #[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[serde(rename_all = "camelCase")] pub struct SystemUpdatePolicy { @@ -36,9 +36,9 @@ pub struct SystemUpdatePolicy { pub available_channels: Vec, } -/// 当前运行版本及更新检查摘要。 +/// 当前运行版本及更新检查摘要 /// -/// `deployment_mode_label` 之类的本地化字符串由 API 根据原始枚举值生成。 +/// `deployment_mode_label` 之类的本地化字符串由 API 根据原始枚举值生成 #[derive(Debug, Clone, PartialEq, Eq)] pub struct SystemVersion { pub version: String, @@ -52,9 +52,9 @@ pub struct SystemVersion { pub update_warning: Option, } -/// 发布源提供的完整更新详情。 +/// 发布源提供的完整更新详情 /// -/// 部署模式与构建类型的展示标签归 API;其余字段均为 Host 已确认的原始事实。 +/// 部署模式与构建类型的展示标签归 API;其余字段均为 Host 已确认的原始事实 #[derive(Debug, Clone, PartialEq, Eq)] pub struct SystemUpdateDetail { pub policy: SystemUpdatePolicy, @@ -71,7 +71,7 @@ pub struct SystemUpdateDetail { pub warning: Option, } -/// 系统操作类型。 +/// 系统操作类型 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SystemOperationKind { Update, @@ -79,7 +79,7 @@ pub enum SystemOperationKind { Restart, } -/// 系统操作状态。 +/// 系统操作状态 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SystemOperationStatus { Idle, @@ -88,7 +88,7 @@ pub enum SystemOperationStatus { Failed, } -/// 持久化系统操作的完整状态。 +/// 持久化系统操作的完整状态 #[derive(Debug, Clone, PartialEq, Eq)] pub struct SystemOperationState { pub operation_id: Option, @@ -101,7 +101,7 @@ pub struct SystemOperationState { pub finished_at: Option>, } -/// 最近一次系统操作及版本交换状态。 +/// 最近一次系统操作及版本交换状态 #[derive(Debug, Clone, PartialEq, Eq)] pub struct SystemUpdateStatus { pub previous_version: Option, @@ -110,7 +110,7 @@ pub struct SystemUpdateStatus { pub operation: SystemOperationState, } -/// 自更新日志级别。 +/// 自更新日志级别 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SystemUpdateEventLevel { Info, @@ -119,7 +119,7 @@ pub enum SystemUpdateEventLevel { Error, } -/// 一条已脱敏的系统更新事件。 +/// 一条已脱敏的系统更新事件 #[derive(Debug, Clone, PartialEq, Eq)] pub struct SystemUpdateEvent { pub id: String, @@ -132,7 +132,7 @@ pub struct SystemUpdateEvent { pub occurred_at: DateTime, } -/// 已受理的异步系统操作;不同 wire shape 由枚举保持为合法状态。 +/// 已受理的异步系统操作;不同 wire shape 由枚举保持为合法状态 #[derive(Debug, Clone, PartialEq, Eq)] pub enum SystemOperationAccepted { Update { @@ -163,7 +163,7 @@ impl SystemOperationAccepted { } } -/// 重启前的只读确认快照;目标发行与全局配置版本一起绑定用户确认。 +/// 重启前的只读确认快照;目标发行与全局配置版本一起绑定用户确认 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct SystemRestartPlan { diff --git a/backend/crates/gateway-admin/src/ports/backup.rs b/backend/crates/gateway-admin/src/ports/backup.rs index 1d88c3c57..b32c42fbf 100644 --- a/backend/crates/gateway-admin/src/ports/backup.rs +++ b/backend/crates/gateway-admin/src/ports/backup.rs @@ -1,9 +1,10 @@ -//! 备份控制面所需的持久化与基础设施能力。 +//! 备份控制面所需的持久化与基础设施能力 //! //! 端口方法只接受/返回领域类型与既有错误体系:`BackupRepository` 复用 //! `AdminStoreError`;`DatabaseDumpPort` 与 `BackupObjectStorePort` 返回只携带稳定 -//! 错误码的 [`BackupError`]。`gateway-store` 实现全部端口;`gateway-admin` 拥有 -//! 状态机、Cron、保留策略与 BackupTask 算法。 +//! 错误码的 [`BackupError`] +//! `gateway-store` 实现全部端口;`gateway-admin` 拥有 +//! 状态机、Cron、保留策略与 BackupTask 算法 use std::path::PathBuf; use std::sync::Arc; @@ -23,7 +24,7 @@ use crate::model::{ }; use crate::ports::store::AdminStoreResult; -/// Worker 状态迁移携带的字段更新;`None` 字段不写入。 +/// Worker 状态迁移携带的字段更新;`None` 字段不写入 #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct StatusTransitionUpdate { pub size_bytes: Option, @@ -33,23 +34,23 @@ pub struct StatusTransitionUpdate { pub completed_at: Option>, } -/// 备份配置、任务与计划游标的 PostgreSQL 仓储端口。 +/// 备份配置、任务与计划游标的 PostgreSQL 仓储端口 #[async_trait] pub trait BackupRepository: Send + Sync { - /// 读取完整设置(含 Secret)。 + /// 读取完整设置(含 Secret) async fn load_settings(&self) -> AdminStoreResult; - /// 同事务更新 S3 存储配置、递增 revision、清空验证状态并写审计事件。 + /// 同事务更新 S3 存储配置、递增 revision、清空验证状态并写审计事件 /// - /// 已有记录时禁止切换 endpoint/region/bucket/path-style(存储身份锁定)。 - /// 返回更新后的设置与新的全局 `config_revision`。 + /// 已有记录时禁止切换 endpoint/region/bucket/path-style(存储身份锁定) + /// 返回更新后的设置与新的全局 `config_revision` async fn update_storage_settings( &self, command: UpdateBackupStorageCommand, context: &MutationContext, ) -> AdminStoreResult<(BackupSettings, Revision)>; - /// 同事务更新调度配置、推进计划游标并写审计事件。 + /// 同事务更新调度配置、推进计划游标并写审计事件 async fn update_schedule_settings( &self, command: UpdateBackupScheduleCommand, @@ -58,18 +59,18 @@ pub trait BackupRepository: Send + Sync { timezone: gateway_core::time::DeploymentTimeZone, ) -> AdminStoreResult; - /// CAS 记录探针成功;`storage_revision` 与当前配置不一致时丢弃结果(返回 `false`)。 + /// CAS 记录探针成功;`storage_revision` 与当前配置不一致时丢弃结果(返回 `false`) async fn record_verification( &self, storage_revision: u64, at: DateTime, ) -> AdminStoreResult; - /// 插入 queued 任务;存在活跃任务时返回 `backup.active_task_conflict`。 + /// 插入 queued 任务;存在活跃任务时返回 `backup.active_task_conflict` async fn insert_backup_record(&self, seed: BackupRecordSeed) -> AdminStoreResult; - /// 核对启停、Cron、时区及旧游标后,同事务推进游标并插入 scheduled 任务。 - /// 配置已变化时不写入;同一时间点或活跃冲突时只推进游标,返回 `false`。 + /// 核对启停、Cron、时区及旧游标后,同事务推进游标并插入 scheduled 任务 + /// 配置已变化时不写入;同一时间点或活跃冲突时只推进游标,返回 `false` async fn insert_scheduled_record( &self, seed: BackupRecordSeed, @@ -79,29 +80,29 @@ pub trait BackupRepository: Send + Sync { expected_next_run_at: Option>, ) -> AdminStoreResult; - /// 分页查询记录。 + /// 分页查询记录 async fn list_backup_records( &self, query: BackupRecordListQuery, ) -> AdminStoreResult; - /// 读取单条记录。 + /// 读取单条记录 async fn load_backup_record(&self, id: &str) -> AdminStoreResult>; - /// 读取中间状态记录(dumping/uploading),供启动恢复。 + /// 读取中间状态记录(dumping/uploading),供启动恢复 async fn list_intermediate_records(&self) -> AdminStoreResult>; - /// 读取等待完成墓碑的记录(status = deleting)。 + /// 读取等待完成墓碑的记录(status = deleting) async fn list_pending_deletions(&self, limit: u32) -> AdminStoreResult>; - /// 读取 `expires_at` 已到期且非活跃的记录(completed/failed),供保留清理。 + /// 读取 `expires_at` 已到期且非活跃的记录(completed/failed),供保留清理 async fn list_expired_records(&self, limit: u32) -> AdminStoreResult>; - /// 原子领取最旧的 queued 任务并迁移到 dumping;无任务返回 `None`。 + /// 原子领取最旧的 queued 任务并迁移到 dumping;无任务返回 `None` async fn claim_next_queued(&self, now: DateTime) -> AdminStoreResult>; - /// 条件迁移状态:要求当前状态一致;不匹配返回 `None`。 + /// 条件迁移状态:要求当前状态一致;不匹配返回 `None` async fn transition_status( &self, id: &str, @@ -110,17 +111,17 @@ pub trait BackupRepository: Send + Sync { now: DateTime, ) -> AdminStoreResult>; - /// 迁移到删除流程(completed/failed → deleting);供管理员与保留策略调用。 + /// 迁移到删除流程(completed/failed → deleting);供管理员与保留策略调用 async fn transition_to_deleting( &self, id: &str, now: DateTime, ) -> AdminStoreResult>; - /// 远端删除成功后硬删除记录行。 + /// 远端删除成功后硬删除记录行 async fn delete_record(&self, id: &str) -> AdminStoreResult<()>; - /// 条件推进计划游标;仅当启用状态、计划表达式、时区及旧游标仍匹配时才写入,返回是否推进。 + /// 条件推进计划游标;仅当启用状态、计划表达式、时区及旧游标仍匹配时才写入,返回是否推进 async fn advance_schedule_cursor( &self, next_run_at: DateTime, @@ -130,35 +131,36 @@ pub trait BackupRepository: Send + Sync { timezone: &str, ) -> AdminStoreResult; - /// 按完成时间倒序读取 completed 计划备份,供保留策略扫描(最多 `limit` 条)。 + /// 按完成时间倒序读取 completed 计划备份,供保留策略扫描(最多 `limit` 条) async fn list_scheduled_completed_desc( &self, limit: u32, ) -> AdminStoreResult>; } -/// `pg_dump` 导出与本地暂存端口。 +/// `pg_dump` 导出与本地暂存端口 #[async_trait] pub trait DatabaseDumpPort: Send + Sync { - /// 创建归档;完成后原子落盘。取消时终止子进程并清理部分文件。 + /// 创建归档;完成后原子落盘 + /// 取消时终止子进程并清理部分文件 async fn dump(&self, request: DumpRequest) -> Result; - /// 检查暂存目录是否存在完整归档。 + /// 检查暂存目录是否存在完整归档 async fn inspect_staging(&self, backup_id: &str) -> Result, BackupError>; - /// 清理暂存目录中该备份的全部文件。 + /// 清理暂存目录中该备份的全部文件 async fn cleanup_staging(&self, backup_id: &str) -> Result<(), BackupError>; } -/// 一次 `pg_dump` 请求。 +/// 一次 `pg_dump` 请求 #[derive(Debug, Clone)] pub struct DumpRequest { pub backup_id: String, pub cancellation: gateway_core::lifecycle::CancellationToken, } -/// 已完成并落盘的归档事实。 +/// 已完成并落盘的归档事实 #[derive(Debug, Clone, PartialEq, Eq)] pub struct DumpArtifact { pub path: PathBuf, @@ -166,7 +168,7 @@ pub struct DumpArtifact { pub sha256: String, } -/// 暂存目录中已存在的完整归档。 +/// 暂存目录中已存在的完整归档 #[derive(Debug, Clone, PartialEq, Eq)] pub struct StagedArtifact { pub path: PathBuf, @@ -174,39 +176,39 @@ pub struct StagedArtifact { pub sha256: String, } -/// S3 兼容对象存储适配器端口。 +/// S3 兼容对象存储适配器端口 /// -/// 方法接受已经校验的存储配置;SDK client 的构造与缓存属于 adapter 实现细节。 +/// 方法接受已经校验的存储配置;SDK client 的构造与缓存属于 adapter 实现细节 #[async_trait] pub trait BackupObjectStorePort: Send + Sync { - /// 对专用探针对象执行 Put/Head/Get/Delete。 + /// 对专用探针对象执行 Put/Head/Get/Delete async fn test_connection( &self, config: &BackupStorageConfig, ) -> Result; - /// 分片流式上传本地归档;取消时中止 multipart 并返回 `backup.cancelled`。 + /// 分片流式上传本地归档;取消时中止 multipart 并返回 `backup.cancelled` async fn upload_file( &self, config: &BackupStorageConfig, request: UploadObjectRequest, ) -> Result<(), BackupError>; - /// 读取对象大小与 metadata;对象不存在返回 `Ok(None)`。 + /// 读取对象大小与 metadata;对象不存在返回 `Ok(None)` async fn head_object( &self, config: &BackupStorageConfig, object_key: &str, ) -> Result, BackupError>; - /// 删除对象;对象不存在视为成功。 + /// 删除对象;对象不存在视为成功 async fn delete_object( &self, config: &BackupStorageConfig, object_key: &str, ) -> Result<(), BackupError>; - /// 创建短时预签名下载 URL。 + /// 创建短时预签名下载 URL async fn presigned_download( &self, config: &BackupStorageConfig, @@ -216,7 +218,7 @@ pub trait BackupObjectStorePort: Send + Sync { ) -> Result; } -/// 一次对象上传请求。 +/// 一次对象上传请求 #[derive(Debug, Clone)] pub struct UploadObjectRequest { pub object_key: String, @@ -225,7 +227,7 @@ pub struct UploadObjectRequest { pub cancellation: gateway_core::lifecycle::CancellationToken, } -/// 备份控制面所需的全部 Store 能力集合。 +/// 备份控制面所需的全部 Store 能力集合 #[derive(Clone)] pub struct BackupStorePorts { repository: Arc, @@ -234,7 +236,7 @@ pub struct BackupStorePorts { } impl BackupStorePorts { - /// 组合备份端口。 + /// 组合备份端口 #[must_use] pub fn new( repository: Arc, @@ -248,25 +250,25 @@ impl BackupStorePorts { } } - /// 备份任务/配置仓储。 + /// 备份任务/配置仓储 #[must_use] pub fn repository(&self) -> Arc { self.repository.clone() } - /// `pg_dump` 导出与暂存。 + /// `pg_dump` 导出与暂存 #[must_use] pub fn dump(&self) -> Arc { self.dump.clone() } - /// S3/R2 对象存储适配器。 + /// S3/R2 对象存储适配器 #[must_use] pub fn object_store(&self) -> Arc { self.object_store.clone() } - /// 测试与不可用路径使用的空实现;任何调用都返回不可用错误。 + /// 测试与不可用路径使用的空实现;任何调用都返回不可用错误 #[doc(hidden)] #[must_use] pub fn disabled() -> Self { diff --git a/backend/crates/gateway-admin/src/ports/client_distribution.rs b/backend/crates/gateway-admin/src/ports/client_distribution.rs index a62de6e62..316cce7f1 100644 --- a/backend/crates/gateway-admin/src/ports/client_distribution.rs +++ b/backend/crates/gateway-admin/src/ports/client_distribution.rs @@ -1,4 +1,4 @@ -//! 客户端安装包解析的外部能力端口。 +//! 客户端安装包解析的外部能力端口 use async_trait::async_trait; diff --git a/backend/crates/gateway-admin/src/ports/mod.rs b/backend/crates/gateway-admin/src/ports/mod.rs index 96b2f4448..b6ee93195 100644 --- a/backend/crates/gateway-admin/src/ports/mod.rs +++ b/backend/crates/gateway-admin/src/ports/mod.rs @@ -1,4 +1,4 @@ -//! 管理用例依赖的外部能力端口。 +//! 管理用例依赖的外部能力端口 pub mod backup; pub mod client_distribution; diff --git a/backend/crates/gateway-admin/src/ports/plugin_accounts.rs b/backend/crates/gateway-admin/src/ports/plugin_accounts.rs index a646c3b9a..f6db87a26 100644 --- a/backend/crates/gateway-admin/src/ports/plugin_accounts.rs +++ b/backend/crates/gateway-admin/src/ports/plugin_accounts.rs @@ -1,4 +1,4 @@ -//! 插件宿主访问账号权威所需的窄端口。 +//! 插件宿主访问账号权威所需的窄端口 use async_trait::async_trait; use gateway_core::account::ProviderAccountId; @@ -11,7 +11,7 @@ use crate::model::{ }, }; -/// Runtime 只取得账号读写用例,不接触 Store、SQL 或完整管理服务集合。 +/// Runtime 只取得账号读写用例,不接触 Store、SQL 或完整管理服务集合 #[async_trait] pub trait PluginAccountAccess: Send + Sync { async fn list(&self, query: PluginAccountListQuery) -> Result; @@ -26,7 +26,7 @@ pub trait PluginAccountAccess: Send + Sync { account_id: &ProviderAccountId, ) -> Result; - /// 只读 Provider 已有额度观测;实现不得主动刷新上游或附加用量预测。 + /// 只读 Provider 已有额度观测;实现不得主动刷新上游或附加用量预测 async fn get_quota( &self, _account_id: &ProviderAccountId, @@ -34,7 +34,7 @@ pub trait PluginAccountAccess: Send + Sync { Err(AdminError::unavailable("插件额度事实查询暂不可用")) } - /// 通过 Provider 原生管理路径刷新额度;不向插件暴露凭据或执行额度重置。 + /// 通过 Provider 原生管理路径刷新额度;不向插件暴露凭据或执行额度重置 async fn refresh_quota( &self, _account_id: &ProviderAccountId, diff --git a/backend/crates/gateway-admin/src/ports/plugin_client_keys.rs b/backend/crates/gateway-admin/src/ports/plugin_client_keys.rs index c92d4ce69..5dedbef1b 100644 --- a/backend/crates/gateway-admin/src/ports/plugin_client_keys.rs +++ b/backend/crates/gateway-admin/src/ports/plugin_client_keys.rs @@ -1,4 +1,4 @@ -//! 插件 Client Key 非秘密目录与受控预算管理端口。 +//! 插件 Client Key 非秘密目录与受控预算管理端口 use async_trait::async_trait; use gateway_core::{engine::budget::ClientBudgetStatus, policy::ClientApiKeyId}; diff --git a/backend/crates/gateway-admin/src/ports/plugin_management.rs b/backend/crates/gateway-admin/src/ports/plugin_management.rs index 7a40c9155..c6a898875 100644 --- a/backend/crates/gateway-admin/src/ports/plugin_management.rs +++ b/backend/crates/gateway-admin/src/ports/plugin_management.rs @@ -1,3 +1,5 @@ +//! 插件管理页面、资源访问与授权回调的运行时端口 + use async_trait::async_trait; use gateway_core::runtime::extensions::ExtensionSetReference; @@ -9,10 +11,10 @@ use crate::model::{ }, }; -/// 仅按 Admin 固定的发布引用分派;Runtime 不自行选择当前代次,也不拥有管理员身份。 +/// 仅按 Admin 固定的发布引用分派;Runtime 不自行选择当前代次,也不拥有管理员身份 #[async_trait] pub trait PluginManagement: Send + Sync { - /// 复核固定发布代次中的可用目标;调用方已校验持久实例仍为当前版本。 + /// 复核固定发布代次中的可用目标;调用方已校验持久实例仍为当前版本 async fn validate_target( &self, published: &ExtensionSetReference, diff --git a/backend/crates/gateway-admin/src/ports/plugin_release.rs b/backend/crates/gateway-admin/src/ports/plugin_release.rs index 1d9b5c3c6..fe61fba60 100644 --- a/backend/crates/gateway-admin/src/ports/plugin_release.rs +++ b/backend/crates/gateway-admin/src/ports/plugin_release.rs @@ -1,3 +1,5 @@ +//! 官方插件发行清单与制品文件的读取端口 + use std::sync::Arc; use async_trait::async_trait; @@ -9,7 +11,7 @@ pub enum OfficialPluginReleaseReadErrorKind { Unavailable, } -/// 文件系统错误不携带部署路径,避免把宿主布局传播到管理域和日志之外。 +/// 文件系统错误不携带部署路径,避免把宿主布局传播到管理域和日志之外 #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] #[error("official plugin release files are unavailable")] pub struct OfficialPluginReleaseReadError { @@ -28,7 +30,7 @@ impl OfficialPluginReleaseReadError { } } -/// Host 只暴露与宿主发行物同信任边界的只读文件,不解释官方身份或插件包。 +/// Host 只暴露与宿主发行物同信任边界的只读文件,不解释官方身份或插件包 #[async_trait] pub trait OfficialPluginReleaseFiles: Send + Sync { async fn manifest(&self) -> Result>, OfficialPluginReleaseReadError>; diff --git a/backend/crates/gateway-admin/src/ports/plugin_resources.rs b/backend/crates/gateway-admin/src/ports/plugin_resources.rs index f39583eff..7f700a494 100644 --- a/backend/crates/gateway-admin/src/ports/plugin_resources.rs +++ b/backend/crates/gateway-admin/src/ports/plugin_resources.rs @@ -1,4 +1,4 @@ -//! 插件自有资源的管理入口与原子持久化端口。 +//! 插件自有资源的管理入口与原子持久化端口 use super::store::AdminStoreResult; use crate::model::{ diff --git a/backend/crates/gateway-admin/src/ports/plugins.rs b/backend/crates/gateway-admin/src/ports/plugins.rs index f3b31e3dd..b3540b550 100644 --- a/backend/crates/gateway-admin/src/ports/plugins.rs +++ b/backend/crates/gateway-admin/src/ports/plugins.rs @@ -1,3 +1,5 @@ +//! 插件准备、分发、持久化与私有状态生命周期的外部能力端口 + use std::{collections::BTreeMap, sync::Arc}; use async_trait::async_trait; @@ -26,10 +28,10 @@ use crate::model::{ }; use gateway_core::runtime::extensions::ExtensionSetReference; -/// Admin 准备完整候选并保活到提交后的发布结束;进程状态不能代替持久启用状态。 +/// Admin 准备完整候选并保活到提交后的发布结束;进程状态不能代替持久启用状态 #[async_trait] pub trait PluginPreparation: PluginRuntimeDiagnostics + PluginStateLifecycle { - /// 静态判断业务配置是否完整,不启动进程;不完整的安装保留为待配置。 + /// 静态判断业务配置是否完整,不启动进程;不完整的安装保留为待配置 async fn configuration_ready( &self, instance: PluginInstance, @@ -39,14 +41,14 @@ pub trait PluginPreparation: PluginRuntimeDiagnostics + PluginStateLifecycle { &self, instance: PluginInstance, ) -> Result; - /// 本次修改的实例 revision 必须等于候选 config_revision,准备失败时拒绝提交;历史故障可隔离。 + /// 本次修改的实例 revision 必须等于候选 config_revision,准备失败时拒绝提交;历史故障可隔离 async fn prepare( &self, snapshot: PluginInstanceSnapshot, ) -> Result; } -/// 运行诊断独立于发布准备;没有进程事实时显式返回 None。 +/// 运行诊断独立于发布准备;没有进程事实时显式返回 None #[async_trait] pub trait PluginRuntimeDiagnostics: Send + Sync { async fn runtime_diagnostics( @@ -57,7 +59,7 @@ pub trait PluginRuntimeDiagnostics: Send + Sync { ) -> Option>; } -/// 发布实现必须显式承担状态激活、排空和迁移,不能以缺省成功跳过生命周期。 +/// 发布实现必须显式承担状态激活、排空和迁移,不能以缺省成功跳过生命周期 #[async_trait] pub trait PluginStateLifecycle: Send + Sync { async fn activate_state( @@ -103,7 +105,7 @@ impl PluginStateStoreError { pub type PluginStateStoreResult = Result; -/// 私有状态的窄端口;不暴露 SQL、连接或跨命名空间事务。 +/// 私有状态的窄端口;不暴露 SQL、连接或跨命名空间事务 #[async_trait] pub trait PluginStateStore: Send + Sync { async fn load_owner( @@ -151,7 +153,7 @@ pub trait PluginStateStore: Send + Sync { async fn abort_transition(&self, transition_id: &str) -> PluginStateStoreResult<()>; } -/// Host 拥有联网、重定向、凭据作用域、限流与有界下载,不解释插件包。 +/// Host 拥有联网、重定向、凭据作用域、限流与有界下载,不解释插件包 #[async_trait] pub trait PluginDistribution: Send + Sync { fn validate_source(&self, source: &PluginUpdateSource) -> Result<(), AdminError>; @@ -170,7 +172,7 @@ pub trait PluginDistribution: Send + Sync { ) -> Result; } -/// Runtime 只解释插件包格式;安装事务与来源选择归 Admin。 +/// Runtime 只解释插件包格式;安装事务与来源选择归 Admin #[async_trait] pub trait PluginPackageInspector: Send + Sync { async fn inspect( @@ -179,7 +181,7 @@ pub trait PluginPackageInspector: Send + Sync { expected_sha256: Option, ) -> Result; - /// 从已安装且摘要匹配的制品中读取可展示的静态图标,不启动插件实例。 + /// 从已安装且摘要匹配的制品中读取可展示的静态图标,不启动插件实例 async fn icon( &self, _archive: Arc<[u8]>, @@ -189,7 +191,7 @@ pub trait PluginPackageInspector: Send + Sync { Err(AdminError::unavailable("插件图标读取不可用")) } - /// 只读取已校验包的静态宿主要求;不得准备或启动插件进程。 + /// 只读取已校验包的静态宿主要求;不得准备或启动插件进程 async fn compatibility( &self, _archive: Arc<[u8]>, @@ -201,13 +203,13 @@ pub trait PluginPackageInspector: Send + Sync { #[async_trait] pub trait PluginStore: Send + Sync { - /// 只复核精确管理目标的启用、接受事实和版本,不读取实例配置或密钥。 + /// 只复核精确管理目标的启用、接受事实和版本,不读取实例配置或密钥 async fn management_target_is_current( &self, target: &crate::model::plugins::management::PluginManagementTarget, ) -> AdminStoreResult; async fn load_instances(&self) -> AdminStoreResult; - /// 读取对应制品最近一次启用时提交的配置,密钥始终留在服务端。 + /// 读取对应制品最近一次启用时提交的配置,密钥始终留在服务端 async fn load_version_configuration( &self, _id: &str, @@ -215,11 +217,11 @@ pub trait PluginStore: Send + Sync { ) -> AdminStoreResult> { Ok(None) } - /// 只返回有可恢复配置的制品摘要,不读取敏感值。 + /// 只返回有可恢复配置的制品摘要,不读取敏感值 async fn configuration_versions(&self, _id: &str) -> AdminStoreResult> { Ok(Vec::new()) } - /// 在单个事务中停用确认快照中的实例,保留配置、密钥及私有状态。 + /// 在单个事务中停用确认快照中的实例,保留配置、密钥及私有状态 async fn disable_instances( &self, _ids: &[String], @@ -255,7 +257,7 @@ pub trait PluginStore: Send + Sync { self.save_instance(instance, expected_revision, context) .await } - /// 保存目标并停用明确确认的配置,必须共享事务和配置版本检查。 + /// 保存目标并停用明确确认的配置,必须共享事务和配置版本检查 async fn save_instance_replacing( &self, instance: PluginInstance, @@ -307,13 +309,13 @@ pub trait PluginStore: Send + Sync { source: PluginSource, context: &MutationContext, ) -> AdminStoreResult; - /// 确认信任并安装精确摘要的制品;安装事实不随实例设置变化。 + /// 确认信任并安装精确摘要的制品;安装事实不随实例设置变化 async fn accept_artifact( &self, digest: &str, context: &MutationContext, ) -> AdminStoreResult; - /// 删除制品及其无引用下载凭据,最后一个版本同时删除来源规则,清理与审计必须原子提交。 + /// 删除制品及其无引用下载凭据,最后一个版本同时删除来源规则,清理与审计必须原子提交 async fn delete_artifact( &self, digest: &str, diff --git a/backend/crates/gateway-admin/src/ports/pricing.rs b/backend/crates/gateway-admin/src/ports/pricing.rs index 1c14ca534..970fc2aa3 100644 --- a/backend/crates/gateway-admin/src/ports/pricing.rs +++ b/backend/crates/gateway-admin/src/ports/pricing.rs @@ -1,4 +1,4 @@ -//! 手动同步的外部价目来源;网络和外部文档解析由 Host 实现。 +//! 手动同步的外部价目来源;网络和外部文档解析由 Host 实现 use crate::model::{AdminError, pricing::PricingSyncPreview}; use async_trait::async_trait; diff --git a/backend/crates/gateway-admin/src/ports/provider.rs b/backend/crates/gateway-admin/src/ports/provider.rs index bee2e3d12..7fa213166 100644 --- a/backend/crates/gateway-admin/src/ports/provider.rs +++ b/backend/crates/gateway-admin/src/ports/provider.rs @@ -1,4 +1,4 @@ -//! 原生 Provider 管理能力与启动时注册表。 +//! 原生 Provider 管理能力与启动时注册表 use std::{collections::BTreeMap, sync::Arc}; @@ -26,14 +26,14 @@ use crate::model::{ quota_forecast_sampling::QuotaForecastObservation, }; -/// Provider 管理失败的稳定分类。 +/// Provider 管理失败的稳定分类 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ProviderAdminErrorKind { Invalid, Unsupported, NotFound, Conflict, - /// Provider 已发起不可逆操作,但无法确认最终执行结果。 + /// Provider 已发起不可逆操作,但无法确认最终执行结果 Ambiguous, Unavailable, CredentialRefreshRequired, @@ -41,11 +41,11 @@ pub enum ProviderAdminErrorKind { Internal, } -/// 不携带 OAuth 请求材料的管理错误。 +/// 不携带 OAuth 请求材料的管理错误 /// -/// `message` 是 Provider 局部诊断,可能包含原始上游正文;通用管理用例不得自动把它作为公开文案。 -/// 只有明确拥有原始诊断合同的调用方才能读取,`Debug` 始终只记录是否存在。 -/// `public_message` 则是明确标记为可公开的静态提示,不允许携带动态上游材料。 +/// `message` 是 Provider 局部诊断,可能包含原始上游正文;通用管理用例不得自动把它作为公开文案 +/// 只有明确拥有原始诊断合同的调用方才能读取,`Debug` 始终只记录是否存在 +/// `public_message` 则是明确标记为可公开的静态提示,不允许携带动态上游材料 #[derive(Clone, PartialEq, Eq, thiserror::Error)] #[error("provider admin operation failed: {kind:?}")] pub struct ProviderAdminError { @@ -81,7 +81,7 @@ impl ProviderAdminError { self } - /// Provider 明确允许公开的静态提示;不得承载上游响应或凭据材料。 + /// Provider 明确允许公开的静态提示;不得承载上游响应或凭据材料 #[must_use] pub const fn with_public_message(mut self, message: &'static str) -> Self { self.public_message = Some(message); @@ -104,20 +104,21 @@ impl ProviderAdminError { } } -/// 一个具体 Provider 对管理控制面提供的解析、验证、上游交互与运行时资源回收能力。 +/// 一个具体 Provider 对管理控制面提供的解析、验证、上游交互与运行时资源回收能力 /// /// 数据变更由 Provider 返回 prepared facts;config revision、审计与 PostgreSQL 事务 -/// 全部由 [`crate::ports::store::AccountStore`] 提交。运行时资源通知只在事务成功后发生。 +/// 全部由 [`crate::ports::store::AccountStore`] 提交 +/// 运行时资源通知只在事务成功后发生 #[async_trait] pub trait ProviderAdmin: Send + Sync { - /// 只读内置价目;没有本地计价能力的 Provider 返回空目录。 + /// 只读内置价目;没有本地计价能力的 Provider 返回空目录 fn pricing_catalog(&self) -> crate::model::pricing::ProviderPricingCatalog { Default::default() } fn provider_kind(&self) -> &ProviderKind; - /// 只读取原生 Provider 能力及账号类型,不执行网络或数据库查询。 + /// 只读取原生 Provider 能力及账号类型,不执行网络或数据库查询 fn account_capabilities( &self, _account_id: &ProviderAccountId, @@ -126,19 +127,19 @@ pub trait ProviderAdmin: Send + Sync { Default::default() } - /// 提供该 Provider 的可选客户端身份;通用管理层不解释内部字段。 + /// 提供该 Provider 的可选客户端身份;通用管理层不解释内部字段 fn client_profile_options( &self, ) -> Result { Err(ProviderAdminError::new(ProviderAdminErrorKind::Unsupported)) } - /// 没有持久选择时使用的 Provider 默认画像;只返回已准备的本地事实。 + /// 没有持久选择时使用的 Provider 默认画像;只返回已准备的本地事实 fn default_client_profile(&self) -> Option { None } - /// 校验并投影客户端身份,结果不含认证或账号材料。 + /// 校验并投影客户端身份,结果不含认证或账号材料 fn preview_client_profile( &self, _configuration: &gateway_core::account::OpaqueProviderData, @@ -146,31 +147,33 @@ pub trait ProviderAdmin: Send + Sync { Err(ProviderAdminError::new(ProviderAdminErrorKind::Unsupported)) } - /// 将原始套餐值投影为展示名称;默认保留未知 Provider 的原始名称。 + /// 将原始套餐值投影为展示名称;默认保留未知 Provider 的原始名称 fn plan_type_display(&self, plan_type: &str) -> String { plan_type.to_owned() } - /// 账号已经由控制面提交为不可调度状态,释放 Provider 持有的账号级运行时资源。 + /// 账号已经由控制面提交为不可调度状态,释放 Provider 持有的账号级运行时资源 /// - /// 无账号级运行时资源的 Provider 不需要执行额外操作。该通知发生在 Store 事务 - /// 成功之后,不参与事务成败,也不得恢复或改写已经提交的账号状态。 + /// 无账号级运行时资源的 Provider 不需要执行额外操作 + /// 该通知发生在 Store 事务 + /// 成功之后,不参与事务成败,也不得恢复或改写已经提交的账号状态 async fn account_unavailable(&self, account_id: &ProviderAccountId); - /// 账号资格事实已经由控制面提交,失效 Provider 持有的可重建派生状态。 + /// 账号资格事实已经由控制面提交,失效 Provider 持有的可重建派生状态 /// /// 该通知发生在 Store 事务成功之后、下一份 RuntimeSnapshot 编译之前;通知 - /// 不参与已提交事务成败。没有账号派生状态的 Provider 可使用默认空实现。 + /// 不参与已提交事务成败 + /// 没有账号派生状态的 Provider 可使用默认空实现 async fn account_facts_changed(&self, _account_ids: &[ProviderAccountId]) {} - /// 生成一次连接测试所需的 Provider-owned operation;Core 负责实际执行与落账。 + /// 生成一次连接测试所需的 Provider-owned operation;Core 负责实际执行与落账 async fn connection_test_operation( &self, upstream_model: &UpstreamModelId, input_text: &str, ) -> Result; - /// 返回该 Provider 实际持有的 Dashboard 上游身份画像。 + /// 返回该 Provider 实际持有的 Dashboard 上游身份画像 fn dashboard_wire_profile(&self) -> Option; fn configured_wire_profile( @@ -180,8 +183,8 @@ pub trait ProviderAdmin: Send + Sync { self.dashboard_wire_profile() } - /// 使用 Provider-owned 价格规则恢复持久请求的逐项费用。 - /// 未保存的长上下文计费标记不得从当前价格反推。 + /// 使用 Provider-owned 价格规则恢复持久请求的逐项费用 + /// 未保存的长上下文计费标记不得从当前价格反推 fn calculated_billing( &self, input: &ProviderBillingInput, @@ -212,7 +215,7 @@ pub trait ProviderAdmin: Send + Sync { command: PrepareCredentialRefresh, ) -> Result; - /// 可公开的账号连接设置;实现只能显式投影非敏感字段,不能返回凭据原文。 + /// 可公开的账号连接设置;实现只能显式投影非敏感字段,不能返回凭据原文 async fn account_configuration( &self, _account_id: &ProviderAccountId, @@ -225,7 +228,7 @@ pub trait ProviderAdmin: Send + Sync { request: ProviderQuotaRequest, ) -> Result; - /// 历史观测的协议字段仅由具体 Provider 解释;不支持时保留累计估算。 + /// 历史观测的协议字段仅由具体 Provider 解释;不支持时保留累计估算 fn quota_forecast_observation( &self, _document: &ProviderDocument, @@ -234,8 +237,8 @@ pub trait ProviderAdmin: Send + Sync { None } - /// 查询 Provider 官方个人资料统计;不支持该能力的 Provider 使用默认拒绝。 - /// 只由个人信息显式查询,不在额度刷新或后台任务中预取。 + /// 查询 Provider 官方个人资料统计;不支持该能力的 Provider 使用默认拒绝 + /// 只由个人信息显式查询,不在额度刷新或后台任务中预取 async fn subscription( &self, _account_id: &ProviderAccountId, @@ -250,7 +253,7 @@ pub trait ProviderAdmin: Send + Sync { Err(ProviderAdminError::new(ProviderAdminErrorKind::Unsupported)) } - /// 打开 Provider 官方头像字节流;不支持该能力的 Provider 使用默认拒绝。 + /// 打开 Provider 官方头像字节流;不支持该能力的 Provider 使用默认拒绝 async fn profile_avatar( &self, _account_id: &ProviderAccountId, @@ -258,7 +261,7 @@ pub trait ProviderAdmin: Send + Sync { Err(ProviderAdminError::new(ProviderAdminErrorKind::Unsupported)) } - /// 查询 Provider 主动额度重置卡;不支持该能力的 Provider 使用默认拒绝。 + /// 查询 Provider 主动额度重置卡;不支持该能力的 Provider 使用默认拒绝 async fn reset_credits( &self, _account_id: &ProviderAccountId, @@ -266,7 +269,7 @@ pub trait ProviderAdmin: Send + Sync { Err(ProviderAdminError::new(ProviderAdminErrorKind::Unsupported)) } - /// 消费 Provider 主动额度重置卡;不支持该能力的 Provider 使用默认拒绝。 + /// 消费 Provider 主动额度重置卡;不支持该能力的 Provider 使用默认拒绝 async fn consume_reset_credit( &self, _command: ConsumeProviderResetCredit, @@ -280,7 +283,7 @@ pub trait ProviderAdmin: Send + Sync { refresh: bool, ) -> Result; - /// 导出该账号的 Provider 原生模型目录正文;不提供原生目录的 Provider 使用默认拒绝。 + /// 导出该账号的 Provider 原生模型目录正文;不提供原生目录的 Provider 使用默认拒绝 async fn model_catalog_document( &self, _account_id: &ProviderAccountId, @@ -294,7 +297,7 @@ pub trait ProviderAdmin: Send + Sync { ) -> Result; } -/// 启动时注册原生 Provider,按 ProviderKind 查找管理能力。 +/// 启动时注册原生 Provider,按 ProviderKind 查找管理能力 #[derive(Clone)] pub struct ProviderAdminRegistry { providers: Arc>>, @@ -308,11 +311,11 @@ impl ProviderAdminRegistry { .collect() } - /// 创建无重复 ProviderKind 的注册表。 + /// 创建无重复 ProviderKind 的注册表 /// /// # Errors /// - /// 重复注册同一 ProviderKind 时返回 Conflict。 + /// 重复注册同一 ProviderKind 时返回 Conflict pub fn new( providers: impl IntoIterator>, ) -> Result { @@ -338,7 +341,7 @@ impl ProviderAdminRegistry { .ok_or_else(|| ProviderAdminError::new(ProviderAdminErrorKind::Unsupported)) } - /// 账号目录与关联列表共用套餐补全和展示规则,已知账号套餐优先于额度快照。 + /// 账号目录与关联列表共用套餐补全和展示规则,已知账号套餐优先于额度快照 pub(crate) fn resolve_account_plan( &self, provider_kind: &str, @@ -351,7 +354,7 @@ impl ProviderAdminRegistry { self.plan_type_display(provider_kind, plan_type.as_deref()) } - /// 账号页和 Dashboard 共用的大驼峰套餐展示名称,不修改原始套餐值。 + /// 账号页和 Dashboard 共用的大驼峰套餐展示名称,不修改原始套餐值 pub(crate) fn plan_type_display( &self, provider_kind: &str, @@ -365,11 +368,11 @@ impl ProviderAdminRegistry { || plan_type.to_owned(), |provider| provider.plan_type_display(plan_type), ); - // 所有 Provider 及未知套餐统一排版,保留 Plus 后缀的含义。 + // 所有 Provider 及未知套餐统一排版,保留 Plus 后缀的含义 Some(display.replace('+', " Plus ").to_upper_camel_case()) } - /// 返回所有已注册 Provider 的 Dashboard 上游身份画像。 + /// 返回所有已注册 Provider 的 Dashboard 上游身份画像 pub fn dashboard_wire_profiles( &self, configurations: &std::collections::BTreeMap< @@ -386,7 +389,7 @@ impl ProviderAdminRegistry { .collect() } - /// 按平台计算请求费用。 + /// 按平台计算请求费用 pub fn calculated_billing( &self, provider_kind: &ProviderKind, diff --git a/backend/crates/gateway-admin/src/ports/proxy.rs b/backend/crates/gateway-admin/src/ports/proxy.rs index c22a16a58..e03270189 100644 --- a/backend/crates/gateway-admin/src/ports/proxy.rs +++ b/backend/crates/gateway-admin/src/ports/proxy.rs @@ -1,3 +1,5 @@ +//! 账号出站代理的持久化、导入占用与连通性探测端口 + use async_trait::async_trait; use gateway_core::account::{OutboundProxy, ProviderAccountId}; @@ -12,7 +14,7 @@ use crate::model::{ #[async_trait] pub trait ProxyStore: Send + Sync { - /// 在凭据交换到提交期间保护选定代理的连接配置和测试结果。 + /// 在凭据交换到提交期间保护选定代理的连接配置和测试结果 async fn reserve_import(&self, id: &str) -> AdminStoreResult; async fn list(&self, query: ProxyListQuery) -> AdminStoreResult; async fn list_accounts( @@ -20,7 +22,7 @@ pub trait ProxyStore: Send + Sync { query: ProxyAccountListQuery, ) -> AdminStoreResult; async fn get(&self, id: &str) -> AdminStoreResult; - /// 仅在账号仍绑定指定代理时解除关联,并清除账号保存的连接地址。 + /// 仅在账号仍绑定指定代理时解除关联,并清除账号保存的连接地址 async fn remove_account( &self, proxy_id: &str, @@ -52,7 +54,7 @@ pub trait ProxyStore: Send + Sync { ) -> AdminStoreResult; } -/// 离开作用域时释放保护,错误返回和请求取消也遵循相同规则。 +/// 离开作用域时释放保护,错误返回和请求取消也遵循相同规则 pub trait ProxyImportGuard: Send + Sync {} pub struct ProxyImportReservation { diff --git a/backend/crates/gateway-admin/src/ports/retention.rs b/backend/crates/gateway-admin/src/ports/retention.rs index a4b111d17..e486c3476 100644 --- a/backend/crates/gateway-admin/src/ports/retention.rs +++ b/backend/crates/gateway-admin/src/ports/retention.rs @@ -1,4 +1,4 @@ -//! 有界保留期删除,不承载周期、暂停或重试策略。 +//! 有界保留期删除,不承载周期、暂停或重试策略 use std::num::NonZeroU32; @@ -12,7 +12,7 @@ use crate::model::retention::{RetentionPolicy, RetentionTarget}; pub trait RetentionStore: Send + Sync { async fn load_policy(&self) -> AdminStoreResult; - /// 每次最多删除 limit 行;请求关联事件随请求事务级联删除。 + /// 每次最多删除 limit 行;请求关联事件随请求事务级联删除 async fn purge_batch( &self, target: RetentionTarget, diff --git a/backend/crates/gateway-admin/src/ports/store.rs b/backend/crates/gateway-admin/src/ports/store.rs index 8352363bd..522a1ca3f 100644 --- a/backend/crates/gateway-admin/src/ports/store.rs +++ b/backend/crates/gateway-admin/src/ports/store.rs @@ -1,6 +1,6 @@ -//! 管理控制面所需的持久化能力。 +//! 管理控制面所需的持久化能力 //! -//! 端口按业务资源拆分,方法使用领域模型,不暴露连接池、事务或 Redis client。 +//! 端口按业务资源拆分,方法使用领域模型,不暴露连接池、事务或 Redis client use std::{collections::BTreeMap, net::IpAddr, sync::Arc, time::Duration}; @@ -40,7 +40,7 @@ use crate::model::{ settings::{AdminApiKey, AdminApiKeyMutation, ReplaceRuntimeSettings, RuntimeSettings}, }; -/// 管理端可判定的持久化失败类型。 +/// 管理端可判定的持久化失败类型 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AdminStoreErrorKind { Invalid, @@ -51,7 +51,7 @@ pub enum AdminStoreErrorKind { Unavailable, } -/// 隐藏数据库实现细节的持久化错误。 +/// 隐藏数据库实现细节的持久化错误 #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] #[error("{resource} store operation failed: {message}")] pub struct AdminStoreError { @@ -87,10 +87,10 @@ impl AdminStoreError { pub type AdminStoreResult = Result; -/// 账号目录与公共账号写操作。 +/// 账号目录与公共账号写操作 #[async_trait] pub trait AccountStore: Send + Sync { - /// 插件回调按授权 Provider/账号在数据库过滤后做稳定 cursor 分页。 + /// 插件回调按授权 Provider/账号在数据库过滤后做稳定 cursor 分页 async fn list_plugin_accounts( &self, query: PluginAccountListQuery, @@ -119,7 +119,7 @@ pub trait AccountStore: Send + Sync { windows: &[AccountUsageWindowQuery], ) -> AdminStoreResult>; - /// 从同一数据库语句取得截止快照的累计用量和有界历史观测。 + /// 从同一数据库语句取得截止快照的累计用量和有界历史观测 async fn load_quota_forecast_history( &self, window: &AccountUsageWindowQuery, @@ -131,7 +131,7 @@ pub trait AccountStore: Send + Sync { account_id: &gateway_core::account::ProviderAccountId, ) -> AdminStoreResult>; - /// 插件按全局账号 ID 读取时,由数据库记录提供权威 Provider 归属。 + /// 插件按全局账号 ID 读取时,由数据库记录提供权威 Provider 归属 async fn credential_details_by_id( &self, account_id: &gateway_core::account::ProviderAccountId, @@ -143,7 +143,7 @@ pub trait AccountStore: Send + Sync { account_ids: &[gateway_core::account::ProviderAccountId], ) -> AdminStoreResult>; - /// 插件凭据读取只接收账号 ID,不接受调用方另行声明 Provider。 + /// 插件凭据读取只接收账号 ID,不接受调用方另行声明 Provider async fn load_credential_for_plugin( &self, account_id: &gateway_core::account::ProviderAccountId, @@ -161,7 +161,7 @@ pub trait AccountStore: Send + Sync { context: &MutationContext, ) -> AdminStoreResult; - /// 已提交的授权结果独立于 Redis 临时状态;只有原管理员身份可读。 + /// 已提交的授权结果独立于 Redis 临时状态;只有原管理员身份可读 async fn authorization_receipt( &self, key: &crate::model::provider_credentials::AuthorizationReceiptKey, @@ -185,7 +185,7 @@ pub trait AccountStore: Send + Sync { context: &MutationContext, ) -> AdminStoreResult; - /// 在事务内按最新启用状态、账号上限和全局默认值判断,只降低并发上限。 + /// 在事务内按最新启用状态、账号上限和全局默认值判断,只降低并发上限 async fn lower_concurrency_limit( &self, account_id: &gateway_core::account::ProviderAccountId, @@ -218,7 +218,7 @@ pub trait AccountStore: Send + Sync { ) -> AdminStoreResult<()>; } -/// 可丢失账号运行态的管理读端口;跨存储编排由 Admin application service 拥有。 +/// 可丢失账号运行态的管理读端口;跨存储编排由 Admin application service 拥有 #[async_trait] pub trait AccountRuntimeStore: Send + Sync { async fn active_rate_limits(&self) -> AdminStoreResult; @@ -228,18 +228,18 @@ pub trait AccountRuntimeStore: Send + Sync { account_ids: &[String], ) -> AdminStoreResult; - /// 容量熔断自动冻结中的账号与其冻结截止时间;429 临时限流不包含在内。 + /// 容量熔断自动冻结中的账号与其冻结截止时间;429 临时限流不包含在内 async fn active_freezes( &self, ) -> AdminStoreResult>; - /// 读取容量失败窗口内观测到的在途并发峰值(自适应并发下调的证据)。 + /// 读取容量失败窗口内观测到的在途并发峰值(自适应并发下调的证据) async fn capacity_peaks( &self, account_ids: &[String], ) -> AdminStoreResult>; - /// 仅当冻结快照仍匹配时解除或顺延;旧探测不得覆盖手动恢复或新一轮冻结。 + /// 仅当冻结快照仍匹配时解除或顺延;旧探测不得覆盖手动恢复或新一轮冻结 async fn finish_freeze( &self, account_id: &str, @@ -248,12 +248,12 @@ pub trait AccountRuntimeStore: Send + Sync { ) -> AdminStoreResult; } -/// 控制面凭据、统一会话、登录限流与管理员安全审计。 +/// 控制面凭据、统一会话、登录限流与管理员安全审计 #[async_trait] pub trait AuthStore: Send + Sync { async fn load_password_hash(&self, admin_user_id: &str) -> AdminStoreResult>; - /// 密码更新与审计必须在同一事务提交;旧哈希不匹配时不写入。 + /// 密码更新与审计必须在同一事务提交;旧哈希不匹配时不写入 async fn change_password( &self, admin_user_id: &str, @@ -281,7 +281,7 @@ pub trait AuthStore: Send + Sync { id: &gateway_core::policy::ClientApiKeyId, ) -> AdminStoreResult; - /// 原子消费来源桶与全局桶的一次登录尝试;被拒绝时返回建议重试间隔。 + /// 原子消费来源桶与全局桶的一次登录尝试;被拒绝时返回建议重试间隔 async fn consume_login_attempt( &self, source_ip: IpAddr, @@ -293,10 +293,10 @@ pub trait AuthStore: Send + Sync { async fn append_audit_event(&self, event: AdminAuditEvent) -> AdminStoreResult<()>; } -/// Client API Key 资料读取与管理写入。 +/// Client API Key 资料读取与管理写入 #[async_trait] pub trait ClientKeyStore: Send + Sync { - /// 按已验证的 ID 读取资料,不读取完整明文 Key。 + /// 按已验证的 ID 读取资料,不读取完整明文 Key async fn get_client_key( &self, id: &gateway_core::policy::ClientApiKeyId, @@ -333,7 +333,7 @@ pub trait ClientKeyStore: Send + Sync { context: &MutationContext, ) -> AdminStoreResult; - /// 局部更新预算上限,保留其他策略和账本;无变化时不产生配置版本或审计。 + /// 局部更新预算上限,保留其他策略和账本;无变化时不产生配置版本或审计 async fn update_client_key_budget_limits( &self, command: UpdateClientKeyBudgetLimits, @@ -341,7 +341,7 @@ pub trait ClientKeyStore: Send + Sync { context: &MutationContext, ) -> AdminStoreResult>; - /// 仅修改运行时账本并原子记录审计,不推进配置版本。 + /// 仅修改运行时账本并原子记录审计,不推进配置版本 async fn reset_client_key_budget( &self, command: ResetClientKeyBudget, @@ -350,7 +350,7 @@ pub trait ClientKeyStore: Send + Sync { ) -> AdminStoreResult<()>; } -/// Provider-neutral account group management transactions. +/// 跨 Provider 的账号分组管理事务 #[async_trait] pub trait AccountGroupStore: Send + Sync { async fn list_account_groups( @@ -388,24 +388,24 @@ pub trait AccountGroupStore: Send + Sync { ) -> AdminStoreResult; } -/// 逐条读取的已计算费用事实;消费结束或丢弃时释放查询资源。 +/// 逐条读取的已计算费用事实;消费结束或丢弃时释放查询资源 pub type UsageCalculatedBillingStream<'a> = BoxStream<'a, AdminStoreResult>; -/// 用量、趋势、诊断与运维错误的只读能力。 +/// 用量、趋势、诊断与运维错误的只读能力 #[async_trait] pub trait ObservabilityStore: Send + Sync { - /// 返回历史统计区间和指定观测时刻下的实时账号状态。 + /// 返回历史统计区间和指定观测时刻下的实时账号状态 async fn dashboard_summary( &self, range: TimeRange, observed_at: DateTime, ) -> AdminStoreResult; - /// 返回 Dashboard 可选的实时槽位事实。 + /// 返回 Dashboard 可选的实时槽位事实 /// /// 该状态来自可丢失的运行时存储;无实现或运行时存储不可用时返回 `None`,不影响 - /// 持久观测数据的读取。 + /// 持久观测数据的读取 async fn dashboard_runtime_slots( &self, _observed_at: DateTime, @@ -421,8 +421,8 @@ pub trait ObservabilityStore: Send + Sync { filter: UsageFilter, ) -> AdminStoreResult>; - /// 流式返回可由 Provider 重新校验的已计算费用事实,不保证顺序。 - /// 查询及解码错误由流返回;调用方应逐条聚合,避免收集整个区间。 + /// 流式返回可由 Provider 重新校验的已计算费用事实,不保证顺序 + /// 查询及解码错误由流返回;调用方应逐条聚合,避免收集整个区间 fn usage_calculated_billing_facts( &self, range: TimeRange, @@ -449,7 +449,7 @@ pub trait ObservabilityStore: Send + Sync { async fn list_ops_errors(&self, query: OpsErrorQuery) -> AdminStoreResult; } -/// Runtime settings 与管理员 API Key 写入。 +/// Runtime settings 与管理员 API Key 写入 #[async_trait] pub trait SettingsStore: Send + Sync { async fn load_pricing(&self) -> AdminStoreResult; @@ -486,7 +486,7 @@ pub trait SettingsStore: Send + Sync { ) -> AdminStoreResult; } -/// 账号目录、运行态与分组所需的 Store 能力集合。 +/// 账号目录、运行态与分组所需的 Store 能力集合 #[derive(Clone)] pub struct AdminAccountStorePorts { accounts: Arc, @@ -512,9 +512,10 @@ impl AdminAccountStorePorts { } } -/// 管理用例所需能力的封闭集合。 +/// 管理用例所需能力的封闭集合 /// -/// 字段保持私有,每个 getter 只交出一种明确能力。该类型不提供通用拆包入口。 +/// 字段保持私有,每个 getter 只交出一种明确能力 +/// 该类型不提供通用拆包入口 #[derive(Clone)] pub struct AdminStorePorts { accounts: AdminAccountStorePorts, diff --git a/backend/crates/gateway-admin/src/ports/system.rs b/backend/crates/gateway-admin/src/ports/system.rs index a2cec80fd..bdc34f529 100644 --- a/backend/crates/gateway-admin/src/ports/system.rs +++ b/backend/crates/gateway-admin/src/ports/system.rs @@ -1,4 +1,4 @@ -//! Host 为系统管理用例提供的进程与操作系统能力。 +//! Host 为系统管理用例提供的进程与操作系统能力 use std::{pin::Pin, sync::Arc}; @@ -11,7 +11,7 @@ use crate::model::system::{ SystemUpdateStatus, SystemVersion, }; -/// Host 已校验并解包的同一更新候选;字节只含发行清单,不含运行配置或凭据。 +/// Host 已校验并解包的同一更新候选;字节只含发行清单,不含运行配置或凭据 #[derive(Clone)] pub struct SystemUpdateCandidate { pub target_version: String, @@ -28,7 +28,7 @@ impl std::fmt::Debug for SystemUpdateCandidate { } } -/// Host 系统操作失败类型。 +/// Host 系统操作失败类型 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SystemOperationErrorKind { Invalid, @@ -37,7 +37,7 @@ pub enum SystemOperationErrorKind { Internal, } -/// 不泄漏路径、命令行或发布凭据的系统操作错误。 +/// 不泄漏路径、命令行或发布凭据的系统操作错误 #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] #[error("system operation failed: {message}")] pub struct SystemOperationError { @@ -59,17 +59,17 @@ impl SystemOperationError { self.kind } - /// 返回 Host 已完成脱敏的客户端安全消息。 + /// 返回 Host 已完成脱敏的客户端安全消息 #[must_use] pub fn message(&self) -> &str { &self.message } } -/// 每个订阅者独占的系统事件流。 +/// 每个订阅者独占的系统事件流 pub type SystemUpdateEventStream = Pin + Send + 'static>>; -/// Admin 对启用实例和制品作兼容决策;Host 只负责候选下载、校验与文件交换。 +/// Admin 对启用实例和制品作兼容决策;Host 只负责候选下载、校验与文件交换 #[async_trait] pub trait SystemUpdatePreflight: Send + Sync { async fn validate( @@ -77,7 +77,7 @@ pub trait SystemUpdatePreflight: Send + Sync { candidate: SystemUpdateCandidate, ) -> Result; - /// 回滚保持严格预检,不能把旧目标当成具有当前重启确认能力的版本。 + /// 回滚保持严格预检,不能把旧目标当成具有当前重启确认能力的版本 async fn validate_rollback( &self, candidate: SystemUpdateCandidate, @@ -85,11 +85,11 @@ pub trait SystemUpdatePreflight: Send + Sync { self.validate(candidate).await } - /// 在文件交换临界点复核全局配置 CAS,不能用较早快照掩盖实例变更。 + /// 在文件交换临界点复核全局配置 CAS,不能用较早快照掩盖实例变更 async fn confirm_revision(&self, expected: Revision) -> Result<(), SystemOperationError>; } -/// 重启临界区内复查候选,确认后停用不兼容插件,再允许进程退出。 +/// 重启临界区内复查候选,确认后停用不兼容插件,再允许进程退出 #[async_trait] pub trait SystemRestartPreflight: Send + Sync { async fn prepare( @@ -98,7 +98,7 @@ pub trait SystemRestartPreflight: Send + Sync { ) -> Result<(), SystemOperationError>; } -/// 版本、自更新、回滚和重启能力;实现唯一归 gateway-host。 +/// 版本、自更新、回滚和重启能力;实现唯一归 gateway-host #[async_trait] pub trait SystemOperations: Send + Sync { async fn version(&self) -> Result; @@ -125,7 +125,7 @@ pub trait SystemOperations: Send + Sync { preflight: Arc, ) -> Result; - /// 返回下一次启动实际使用的发行;源码运行没有封存发行清单。 + /// 返回下一次启动实际使用的发行;源码运行没有封存发行清单 async fn restart_candidate( &self, ) -> Result, SystemOperationError> { diff --git a/backend/crates/gateway-admin/src/service/mod.rs b/backend/crates/gateway-admin/src/service/mod.rs index 530b16aa6..382d0fccb 100644 --- a/backend/crates/gateway-admin/src/service/mod.rs +++ b/backend/crates/gateway-admin/src/service/mod.rs @@ -1,4 +1,4 @@ -//! 主动服务调用的统一组合入口;普通业务方法由所属 HTTP 或执行边界包裹。 +//! 主动服务调用的统一组合入口;普通业务方法由所属 HTTP 或执行边界包裹 mod registry; mod settings; @@ -19,7 +19,7 @@ use crate::model::{AdminError, AdminErrorKind}; pub type PlanSource = Arc Option + Send + Sync>; -/// 空计划也是快照,只有未解析的外部入口才读取当前发布集合。 +/// 空计划也是快照,只有未解析的外部入口才读取当前发布集合 #[derive(Clone, Default)] pub enum Plan { #[default] @@ -27,7 +27,7 @@ pub enum Plan { Frozen(Option), } -/// 非服务入口也有父调用身份和取消信号;发布计划在首次进入公开服务时解析。 +/// 非服务入口也有父调用身份和取消信号;发布计划在首次进入公开服务时解析 #[derive(Clone)] pub struct Origin { pub request_id: String, @@ -51,7 +51,7 @@ impl From for Origin { tokio::task_local! { static CURRENT: Origin; } -/// task local 随 future poll 进入/退出,不泄漏给并行任务。 +/// task local 随 future poll 进入/退出,不泄漏给并行任务 pub async fn scope(context: impl Into, future: impl Future) -> T { CURRENT.scope(context.into(), future).await } diff --git a/backend/crates/gateway-admin/src/service/registry.rs b/backend/crates/gateway-admin/src/service/registry.rs index e3ea8c1d5..760d123d1 100644 --- a/backend/crates/gateway-admin/src/service/registry.rs +++ b/backend/crates/gateway-admin/src/service/registry.rs @@ -1,4 +1,4 @@ -//! 公开操作登记在所属服务;注册表只校验类型和分派,不实现业务。 +//! 公开操作登记在所属服务;注册表只校验类型和分派,不实现业务 use crate::model::AdminError; use futures::future::BoxFuture; @@ -21,7 +21,7 @@ impl Registry { } } - /// 网关与命令行复用同一组设置服务;注册不会执行读取或写入。 + /// 网关与命令行复用同一组设置服务;注册不会执行读取或写入 pub fn register_settings( &mut self, service: &Arc, diff --git a/backend/crates/gateway-admin/src/service/settings.rs b/backend/crates/gateway-admin/src/service/settings.rs index 99e448f8a..fa768f68e 100644 --- a/backend/crates/gateway-admin/src/service/settings.rs +++ b/backend/crates/gateway-admin/src/service/settings.rs @@ -1,4 +1,4 @@ -//! 设置服务的唯一公开操作声明;SDK 操作和数据合同由对应合同测试生成。 +//! 设置服务的唯一公开操作声明;SDK 操作和数据合同由对应合同测试生成 use crate::{ SettingsService, @@ -14,7 +14,7 @@ pub(super) fn register( registry: &mut super::Registry, service: &Arc, ) -> Result<(), AdminError> { - // 声明保留 SDK 类型名和原生参数转换,分派名称由业务方法名直接派生。 + // 声明保留 SDK 类型名和原生参数转换,分派名称由业务方法名直接派生 macro_rules! register { ($sdk:ident, $method:ident, $input:ty, $pattern:pat $(, $argument:expr)* $(,)?) => {{ let service = service.clone(); diff --git a/backend/crates/gateway-admin/src/use_case/account_groups.rs b/backend/crates/gateway-admin/src/use_case/account_groups.rs index 4bb571c7c..7616187b1 100644 --- a/backend/crates/gateway-admin/src/use_case/account_groups.rs +++ b/backend/crates/gateway-admin/src/use_case/account_groups.rs @@ -1,4 +1,4 @@ -//! Account group management use cases. +//! 账号分组的查询、配置与运行态容量聚合用例 use std::{ collections::{BTreeMap, BTreeSet}, @@ -29,7 +29,7 @@ use crate::{ use super::{map_store_error, publish_committed}; -/// API-facing account group management service. +/// 面向 API 的账号分组管理服务 #[async_trait] pub trait AccountGroupService: Send + Sync { async fn list(&self, query: AccountGroupListQuery) -> Result; diff --git a/backend/crates/gateway-admin/src/use_case/accounts.rs b/backend/crates/gateway-admin/src/use_case/accounts.rs index 0daafe833..32376cc76 100644 --- a/backend/crates/gateway-admin/src/use_case/accounts.rs +++ b/backend/crates/gateway-admin/src/use_case/accounts.rs @@ -1,4 +1,4 @@ -//! 统一账号目录与原生 Provider 分派。 +//! 统一账号目录与原生 Provider 分派 use std::{ collections::{BTreeMap, BTreeSet}, @@ -49,7 +49,7 @@ use super::{ const CONNECTION_TEST_INPUT: &str = "Reply with exactly OK."; -/// 统一账号页消费的服务。 +/// 统一账号页消费的服务 #[async_trait] pub trait AccountsService: Send + Sync { async fn list(&self, query: AccountListQuery) -> Result; @@ -279,7 +279,7 @@ impl DefaultAccountsService { accounts: &[AccountPageItem], ) -> Result, AdminError> { let now = Utc::now(); - // API Key 没有套餐周期;本地累计直接查询账号创建后仍保留的请求记录。 + // API Key 没有套餐周期;本地累计直接查询账号创建后仍保留的请求记录 let windows = accounts .iter() .filter(|item| item.account.authentication_kind == "api_key") @@ -333,7 +333,7 @@ impl DefaultAccountsService { .await { Ok(quota) => quota, - // 凭据更新后的账号投影不要求 Provider 提供额度;显式刷新仍须支持该操作。 + // 凭据更新后的账号投影不要求 Provider 提供额度;显式刷新仍须支持该操作 Err(error) if !refresh_quota && error.kind() @@ -403,7 +403,7 @@ impl AccountsService for DefaultAccountsService { .iter() .map(|item| item.account.id.clone()) .collect::>(); - // 只读取当前页的租约占用;观测失败不能把账号误报为空闲或拖垮目录。 + // 只读取当前页的租约占用;观测失败不能把账号误报为空闲或拖垮目录 let in_flight = if ids.is_empty() { None } else { @@ -429,7 +429,7 @@ impl AccountsService for DefaultAccountsService { let account_id = ProviderAccountId::new(account.id.clone()) .map_err(|_| AdminError::invalid("Provider 账号 ID 不合法"))?; // 单个账号的 quota 投影失败(Provider 未注册或 quota 读取失败)不拖垮整页: - // 该账号降级为空额度投影,其余账号与页面状态照常返回。 + // 该账号降级为空额度投影,其余账号与页面状态照常返回 let provider = match providers.require(&account.provider_kind) { Ok(provider) => provider, Err(error) => { @@ -536,7 +536,7 @@ impl AccountsService for DefaultAccountsService { }) { return Err(AdminError::invalid("账号导出列表包含重复 ID")); } - // 混选时先确认全部 Provider 已注册,拒绝路径不读取任何一组明文凭据。 + // 混选时先确认全部 Provider 已注册,拒绝路径不读取任何一组明文凭据 let export_groups = grouped .into_iter() .map(|(provider_kind, ids)| { @@ -615,7 +615,7 @@ impl AccountsService for DefaultAccountsService { .map_err(|error| map_store_error(error, "provider account recovery"))? .config_revision } else { - // 停用只表示不参与调度,重新启用不能抹除已观测的额度、凭据或冷却事实。 + // 停用只表示不参与调度,重新启用不能抹除已观测的额度、凭据或冷却事实 self.accounts .batch_update_accounts( BatchUpdateAccounts { @@ -774,7 +774,7 @@ impl AccountsService for DefaultAccountsService { let Some((window, _)) = quota_forecast_source_window("a, period) else { continue; }; - // 缺少一个周期时两个结果会复用同一窗口,只查询一次历史快照。 + // 缺少一个周期时两个结果会复用同一窗口,只查询一次历史快照 if !selected_keys.insert(window.key.as_str()) { continue; } @@ -814,8 +814,8 @@ impl AccountsService for DefaultAccountsService { .as_deref() .zip(fact.plan_type.as_deref()) .is_some_and(|(current, previous)| current.eq_ignore_ascii_case(previous)); - // 仅容纳已观测到的秒级量化抖动,不用宽时间容差合并实际重置。 - // 不匹配的段截断基线;之后的有效观测可以重新积累。 + // 仅容纳已观测到的秒级量化抖动,不用宽时间容差合并实际重置 + // 不匹配的段截断基线;之后的有效观测可以重新积累 if !same_plan || (fact.reset_at - reset_at).abs() > Duration::seconds(2) { points.clear(); interrupted = true; @@ -853,7 +853,7 @@ impl AccountsService for DefaultAccountsService { account_id: &ProviderAccountId, ) -> Result { let (initial, provider) = self.provider_for_account(account_id).await?; - // 两项读取相互独立;不因其中一项失败而取消另一项,也不触发凭据或额度刷新。 + // 两项读取相互独立;不因其中一项失败而取消另一项,也不触发凭据或额度刷新 let (profile, subscription) = futures::join!( provider.profile_statistics(account_id), provider.subscription(account_id), @@ -865,7 +865,7 @@ impl AccountsService for DefaultAccountsService { .ok() .flatten(); - // 汇聚等待期间发生重新授权、换绑或删除时,不返回混合身份的数据。 + // 汇聚等待期间发生重新授权、换绑或删除时,不返回混合身份的数据 let current = self.load_account(account_id).await?; if current.account.provider_kind != initial.account.provider_kind || current.account.credential_revision != initial.account.credential_revision @@ -921,7 +921,7 @@ impl AccountsService for DefaultAccountsService { ) -> Result { let account_id = command.account_id.clone(); // 覆盖 credential refresh + 同键重试的完整账号级临界区,避免 401 两次调用 - // 之间插入另一笔不可逆消费。 + // 之间插入另一笔不可逆消费 let lock = self.reset_credit_lock(&account_id).await; let _guard = lock.lock().await; let (_, provider) = self.provider_for_account(&account_id).await?; @@ -1049,7 +1049,7 @@ fn map_reset_credits_error_after_refresh( map_provider_error(error, "provider reset credits") } -/// 账号目录中单个账号 quota 读取失败时使用的空额度投影。 +/// 账号目录中单个账号 quota 读取失败时使用的空额度投影 fn empty_quota() -> ProviderQuota { ProviderQuota { credits: None, @@ -1073,7 +1073,7 @@ fn quota_usage_window( let seconds = i64::try_from(window.window_seconds?).ok()?; let start = reset_at.checked_sub_signed(Duration::try_seconds(seconds)?)?; let range = TimeRange::new(start, reset_at).ok()?; - // 上游百分比以该 reset 边界定义;以当前时间回推会让本地 Token 属于另一窗口。 + // 上游百分比以该 reset 边界定义;以当前时间回推会让本地 Token 属于另一窗口 Some(AccountUsageWindowQuery { account_id: account_id.to_owned(), key: window.key.clone(), diff --git a/backend/crates/gateway-admin/src/use_case/auth.rs b/backend/crates/gateway-admin/src/use_case/auth.rs index a57462204..01285f4e6 100644 --- a/backend/crates/gateway-admin/src/use_case/auth.rs +++ b/backend/crates/gateway-admin/src/use_case/auth.rs @@ -1,4 +1,4 @@ -//! 控制面登录、会话恢复与身份校验的唯一 owner。 +//! 控制面登录、会话恢复与身份校验的唯一 owner use std::{net::IpAddr, sync::Arc, time::Duration as StdDuration}; @@ -25,7 +25,7 @@ use crate::{ use super::map_store_error; -/// 所有控制面接口消费同一个会话服务,权限由服务端身份决定。 +/// 所有控制面接口消费同一个会话服务,权限由服务端身份决定 #[async_trait] pub trait AuthService: Send + Sync { async fn change_password( @@ -208,7 +208,7 @@ impl AuthService for DefaultAuthService { { return Err(AdminError::conflict("密码已变更,请重新登录")); } - // 密码事务提交后旧指纹不再匹配,会话撤销不依赖 Redis 删除成功。 + // 密码事务提交后旧指纹不再匹配,会话撤销不依赖 Redis 删除成功 Ok(()) } @@ -344,7 +344,7 @@ impl AuthService for DefaultAuthService { let _ = self.store.delete_session(&session_id).await; return Err(LoginError::Unavailable); } - // 新身份验证成功后才撤销旧会话;撤销失败时不向浏览器提交新会话。 + // 新身份验证成功后才撤销旧会话;撤销失败时不向浏览器提交新会话 if let Some(previous) = previous_session_id.filter(|value| !value.is_empty()) && self.logout(previous).await.is_err() { @@ -425,7 +425,7 @@ fn valid_admin_api_key_shape(value: &str) -> bool { && value[6..].bytes().all(|byte| byte.is_ascii_hexdigit()) } -// 指纹只绑定已加盐的密码哈希,不把密码或原始哈希复制到 Redis 会话。 +// 指纹只绑定已加盐的密码哈希,不把密码或原始哈希复制到 Redis 会话 fn password_fingerprint(password_hash: &str) -> String { URL_SAFE_NO_PAD.encode(Sha256::digest(password_hash.as_bytes())) } diff --git a/backend/crates/gateway-admin/src/use_case/backup.rs b/backend/crates/gateway-admin/src/use_case/backup.rs index 9871ea898..c43f5e91b 100644 --- a/backend/crates/gateway-admin/src/use_case/backup.rs +++ b/backend/crates/gateway-admin/src/use_case/backup.rs @@ -1,4 +1,4 @@ -//! 备份配置、手动创建、下载与删除用例。 +//! 备份配置、手动创建、下载与删除用例 use std::sync::Arc; use std::time::Duration; @@ -26,10 +26,10 @@ use crate::ports::{ use super::map_store_error; -/// 默认下载地址有效期。 +/// 默认下载地址有效期 const DOWNLOAD_TTL: Duration = Duration::from_secs(5 * 60); -/// API 消费的备份管理服务。 +/// API 消费的备份管理服务 #[async_trait] pub trait BackupService: Send + Sync { async fn load_settings(&self) -> Result; @@ -359,7 +359,7 @@ impl DefaultBackupService { } } -/// 校验存储配置;空串视为非法,Secret 缺省表示保留旧值。 +/// 校验存储配置;空串视为非法,Secret 缺省表示保留旧值 fn validate_storage(command: &UpdateBackupStorageCommand) -> Result<(), AdminError> { validate_endpoint(&command.endpoint)?; validate_nonempty("region", &command.region)?; @@ -409,7 +409,7 @@ fn validate_length(field: &str, value: &str, max: usize) -> Result<(), AdminErro Ok(()) } -/// 备份基础设施错误到既有 `AdminErrorKind` 的唯一边界映射。 +/// 备份基础设施错误到既有 `AdminErrorKind` 的唯一边界映射 fn map_backup_error(error: BackupError) -> AdminError { match error.code() { code::INVALID_CONFIG | code::INVALID_CRON => { diff --git a/backend/crates/gateway-admin/src/use_case/client_distribution.rs b/backend/crates/gateway-admin/src/use_case/client_distribution.rs index 40970d3ac..697fd0f0a 100644 --- a/backend/crates/gateway-admin/src/use_case/client_distribution.rs +++ b/backend/crates/gateway-admin/src/use_case/client_distribution.rs @@ -1,4 +1,4 @@ -//! Codex 客户端下载信息用例。 +//! Codex 客户端下载信息用例 use std::sync::Arc; diff --git a/backend/crates/gateway-admin/src/use_case/client_keys.rs b/backend/crates/gateway-admin/src/use_case/client_keys.rs index f6c1659ed..7bbc7099c 100644 --- a/backend/crates/gateway-admin/src/use_case/client_keys.rs +++ b/backend/crates/gateway-admin/src/use_case/client_keys.rs @@ -1,4 +1,4 @@ -//! Client API Key 管理用例。 +//! Client API Key 管理用例 use std::sync::Arc; @@ -25,7 +25,7 @@ use crate::{ use super::{map_store_error, publish_committed}; -/// API 消费的 Client Key 管理服务。 +/// API 消费的 Client Key 管理服务 #[async_trait] pub trait ClientKeyService: Send + Sync { async fn get(&self, id: &ClientApiKeyId) -> Result; @@ -300,7 +300,7 @@ fn validate_cursor(query: &ClientKeyListQuery) -> Result<(), AdminError> { } } -// 原生与插件创建共用相同的密钥生成规则。 +// 原生与插件创建共用相同的密钥生成规则 pub(super) fn generate_key() -> String { let mut bytes = [0_u8; 32]; OsRng.fill_bytes(&mut bytes); diff --git a/backend/crates/gateway-admin/src/use_case/credentials/flow.rs b/backend/crates/gateway-admin/src/use_case/credentials/flow.rs index e8e85da87..8cc311031 100644 --- a/backend/crates/gateway-admin/src/use_case/credentials/flow.rs +++ b/backend/crates/gateway-admin/src/use_case/credentials/flow.rs @@ -1,4 +1,4 @@ -//! 已冻结 Provider 的凭据工作流;Admin 保持事务、审计与发布的唯一所有权。 +//! 已冻结 Provider 的凭据工作流;Admin 保持事务、审计与发布的唯一所有权 use std::sync::Arc; diff --git a/backend/crates/gateway-admin/src/use_case/credentials/mod.rs b/backend/crates/gateway-admin/src/use_case/credentials/mod.rs index e0b112908..7b36b2b4f 100644 --- a/backend/crates/gateway-admin/src/use_case/credentials/mod.rs +++ b/backend/crates/gateway-admin/src/use_case/credentials/mod.rs @@ -1,3 +1,5 @@ +//! 按 Provider 分派凭据管理服务,组合授权与导入流程 + mod flow; use std::sync::Arc; @@ -11,7 +13,7 @@ use crate::{ pub use flow::ProviderCredentials; -/// 所有 Provider 共用凭据用例;每次操作从已发布目录冻结自己的管理实现。 +/// 所有 Provider 共用凭据用例;每次操作从已发布目录冻结自己的管理实现 pub struct CredentialsService { providers: ProviderAdminRegistry, accounts: Arc, diff --git a/backend/crates/gateway-admin/src/use_case/import_tasks.rs b/backend/crates/gateway-admin/src/use_case/import_tasks.rs index e23ae10cb..d3c3c35a0 100644 --- a/backend/crates/gateway-admin/src/use_case/import_tasks.rs +++ b/backend/crates/gateway-admin/src/use_case/import_tasks.rs @@ -1,4 +1,4 @@ -//! 导入队列由 Host 管理生命周期,HTTP 只负责接受输入和读取快照。 +//! 导入队列由 Host 管理生命周期,HTTP 只负责接受输入和读取快照 use std::{ collections::VecDeque, @@ -100,7 +100,7 @@ impl Entry { { self.finished_at = Some(Utc::now()); self.finished_instant = Some(Instant::now()); - // 终态仅保留结果,连已清空的输入槽位分配也一并释放。 + // 终态仅保留结果,连已清空的输入槽位分配也一并释放 self.inputs = Vec::new(); } } @@ -163,7 +163,7 @@ impl DefaultImportTasksService { .tasks .iter() .position(|entry| entry.inputs.iter().any(Option::is_some))?; - // 轮转任务,避免大批次独占后续全部执行机会。 + // 轮转任务,避免大批次独占后续全部执行机会 let mut entry = registry.tasks.remove(task_index)?; let index = entry.inputs.iter().position(Option::is_some)?; let input = entry.inputs[index].take()?; @@ -192,7 +192,7 @@ impl DefaultImportTasksService { item.account_ids = result.credential_ids; } Ok(Err(error)) => { - // 存储或发布异常也可能发生在提交之后;不自动重放可能轮换 RT 的操作。 + // 存储或发布异常也可能发生在提交之后;不自动重放可能轮换 RT 的操作 item.status = if matches!( error.kind(), AdminErrorKind::UpstreamResultUnknown @@ -335,7 +335,7 @@ impl DaemonTask for ImportTaskWorker { fn run(&self, cancellation: CancellationToken) -> BoxFuture<'_, Result<(), WorkerTaskError>> { Box::pin(async move { let mut active = FuturesUnordered::new(); - // 固定一个取消 future,避免循环反复注册中立 CancellationToken 的 waiter。 + // 固定一个取消 future,避免循环反复注册中立 CancellationToken 的 waiter let cancelled = cancellation.cancelled(); tokio::pin!(cancelled); let mut cleanup = tokio::time::interval(Duration::from_secs(60)); @@ -361,7 +361,7 @@ impl DaemonTask for ImportTaskWorker { _ = cleanup.tick() => { drop(self.0.registry()); }, } } - // Host 提供总关闭预算;此处让已发出的交换与入库尽量完成。 + // Host 提供总关闭预算;此处让已发出的交换与入库尽量完成 while active.next().await.is_some() {} Ok(()) }) diff --git a/backend/crates/gateway-admin/src/use_case/key_usage.rs b/backend/crates/gateway-admin/src/use_case/key_usage.rs index 33daa70da..2e4ead40d 100644 --- a/backend/crates/gateway-admin/src/use_case/key_usage.rs +++ b/backend/crates/gateway-admin/src/use_case/key_usage.rs @@ -1,4 +1,4 @@ -//! Key 自助查询从统一会话或只读 Key 校验取得范围,复用现有观测和额度账本。 +//! Key 自助查询从统一会话或只读 Key 校验取得范围,复用现有观测和额度账本 use std::sync::Arc; @@ -31,10 +31,10 @@ use super::{map_store_error, observability::health_timeline_at}; #[async_trait] pub trait KeyUsageService: Send + Sync { - /// 验证 Key 并只读查询当前额度,不记录 Key 使用或执行推理准入。 + /// 验证 Key 并只读查询当前额度,不记录 Key 使用或执行推理准入 async fn budget(&self, plaintext: &str) -> Result, AdminError>; - /// 使用 Core 已认证的宿主身份查询额度,不接收插件自报的 Key ID。 + /// 使用 Core 已认证的宿主身份查询额度,不接收插件自报的 Key ID async fn budget_for_client( &self, id: &ClientApiKeyId, @@ -155,7 +155,7 @@ impl KeyUsageService for DefaultKeyUsageService { let Some(id) = self.key_id(session_id).await? else { return Ok(None); }; - // 明文只按服务端会话绑定的 Key 读取,禁用或删除后不再提供配置。 + // 明文只按服务端会话绑定的 Key 读取,禁用或删除后不再提供配置 self.keys .reveal_client_key(&id) .await @@ -182,7 +182,7 @@ impl KeyUsageService for DefaultKeyUsageService { }; let filter = usage_filter(&id, query.model); let now = query.range.end; - // 健康条始终展示部署时区今日,不随历史范围或模型筛选改变。 + // 健康条始终展示部署时区今日,不随历史范围或模型筛选改变 let today = TimeRange { start: self .timezone diff --git a/backend/crates/gateway-admin/src/use_case/mod.rs b/backend/crates/gateway-admin/src/use_case/mod.rs index ebca86abc..5a3d86002 100644 --- a/backend/crates/gateway-admin/src/use_case/mod.rs +++ b/backend/crates/gateway-admin/src/use_case/mod.rs @@ -1,4 +1,4 @@ -//! 管理控制面的用例实现。 +//! 管理控制面的用例实现 pub mod account_groups; pub mod accounts; @@ -113,7 +113,7 @@ async fn publish_credentials_and_observe_quota( provider.account_facts_changed(account_ids).await; publish_committed(snapshot, revision).await?; - // 凭据已经提交;额度只是可重建的观察,不能拖住管理请求或回滚提交结果。 + // 凭据已经提交;额度只是可重建的观察,不能拖住管理请求或回滚提交结果 let provider = provider.clone(); let account_ids = account_ids.to_vec(); let request_id = request_id.to_owned(); @@ -343,7 +343,7 @@ async fn commit_authorization( if let Some(guard) = authorization_guard && let Err(error) = guard.commit().await { - // 账号与回执已经原子提交,Redis 清理失败不能把确定的成功改写为失败。 + // 账号与回执已经原子提交,Redis 清理失败不能把确定的成功改写为失败 tracing::warn!(resource, settlement_error = %error, "authorization committed but pending cleanup failed"); } Ok(result) diff --git a/backend/crates/gateway-admin/src/use_case/observability.rs b/backend/crates/gateway-admin/src/use_case/observability.rs index 5ea19eacb..ccaa07895 100644 --- a/backend/crates/gateway-admin/src/use_case/observability.rs +++ b/backend/crates/gateway-admin/src/use_case/observability.rs @@ -1,4 +1,4 @@ -//! 观测查询、趋势、费用覆盖与健康阈值规则。 +//! 观测查询、趋势、费用覆盖与健康阈值规则 use std::{ collections::{BTreeMap, HashMap, btree_map::Entry}, @@ -93,7 +93,7 @@ impl DashboardQueryCache { } } -/// API 消费的观测控制面服务。 +/// API 消费的观测控制面服务 #[async_trait] pub trait ObservabilityService: Send + Sync { async fn dashboard_summary( @@ -799,7 +799,7 @@ impl DefaultObservabilityService { } /// 补充当前套餐展示名称,逐条尽力把可校验的总额升级为完整分解;单条脏数据(非法 Provider kind、 - /// 不支持的来源或费用规则失败)只保留该条已存的总额,不影响整页返回。 + /// 不支持的来源或费用规则失败)只保留该条已存的总额,不影响整页返回 fn enrich_list_records(&self, records: &mut [crate::model::observability::UsageListRecord]) { for record in records { record.provider_account_plan_type_display = @@ -870,7 +870,7 @@ impl DefaultObservabilityService { } } -/// 按部署时区的自然日计算 15 分钟健康桶,桶数随夏令时变化。 +/// 按部署时区的自然日计算 15 分钟健康桶,桶数随夏令时变化 pub(super) fn health_timeline_at( records: &[RequestMetricPoint], now: DateTime, diff --git a/backend/crates/gateway-admin/src/use_case/plugin_accounts.rs b/backend/crates/gateway-admin/src/use_case/plugin_accounts.rs index bc3efb803..a10955bcc 100644 --- a/backend/crates/gateway-admin/src/use_case/plugin_accounts.rs +++ b/backend/crates/gateway-admin/src/use_case/plugin_accounts.rs @@ -1,4 +1,4 @@ -//! 插件宿主账号回调;复用凭据事务,不建立第二套账号权威。 +//! 插件宿主账号回调;复用凭据事务,不建立第二套账号权威 use std::sync::Arc; diff --git a/backend/crates/gateway-admin/src/use_case/plugin_client_keys.rs b/backend/crates/gateway-admin/src/use_case/plugin_client_keys.rs index 6b90f791a..288fb00eb 100644 --- a/backend/crates/gateway-admin/src/use_case/plugin_client_keys.rs +++ b/backend/crates/gateway-admin/src/use_case/plugin_client_keys.rs @@ -1,4 +1,4 @@ -//! 插件 Client Key 管理;复用管理服务,只开放非秘密目录与预算操作。 +//! 插件 Client Key 管理;复用管理服务,只开放非秘密目录与预算操作 use std::sync::Arc; diff --git a/backend/crates/gateway-admin/src/use_case/plugin_resources.rs b/backend/crates/gateway-admin/src/use_case/plugin_resources.rs index c680295a4..f33bdfed7 100644 --- a/backend/crates/gateway-admin/src/use_case/plugin_resources.rs +++ b/backend/crates/gateway-admin/src/use_case/plugin_resources.rs @@ -1,3 +1,5 @@ +//! 插件自有分组、Key 与成员关系的写入及配置发布编排 + use crate::{ model::{ AdminError, MutationContext, diff --git a/backend/crates/gateway-admin/src/use_case/plugin_update.rs b/backend/crates/gateway-admin/src/use_case/plugin_update.rs index 3a466cd73..624734060 100644 --- a/backend/crates/gateway-admin/src/use_case/plugin_update.rs +++ b/backend/crates/gateway-admin/src/use_case/plugin_update.rs @@ -1,3 +1,5 @@ +//! 系统升级与回滚前的插件兼容性检查及确认版本校验 + use std::{collections::BTreeMap, sync::Arc}; use super::publish_committed; @@ -24,7 +26,7 @@ use crate::{ use super::plugins::official::{update_compatibility, validate_update_release}; -/// 只读取启用实例及其固定包体,检查目标宿主合同;不准备或执行插件。 +/// 只读取启用实例及其固定包体,检查目标宿主合同;不准备或执行插件 pub(crate) struct PluginSystemUpdatePreflight { store: Arc, inspector: Arc, @@ -53,7 +55,7 @@ impl SystemUpdatePreflight for PluginSystemUpdatePreflight { ) -> Result { let target_version = candidate.target_version.trim_start_matches('v'); semver::Version::parse(target_version).map_err(|_| invalid("目标网关版本不合法"))?; - // 下载阶段只校验发行身份;插件兼容性在用户点击重启时检查并确认。 + // 下载阶段只校验发行身份;插件兼容性在用户点击重启时检查并确认 validate_update_release(candidate.release_manifest.as_ref(), target_version) .map_err(|_| invalid("目标发行清单不合法或版本不匹配"))?; Ok(self @@ -148,7 +150,7 @@ impl PluginSystemUpdatePreflight { ) .map_err(|_| invalid("目标发行清单不合法或版本不匹配"))?; } - // 未知目标合同不能证明兼容,列入待确认停用;不伪造目标支持范围。 + // 未知目标合同不能证明兼容,列入待确认停用;不伪造目标支持范围 let host = candidate.as_ref().and_then(|item| { update_compatibility( &item.release_manifest, diff --git a/backend/crates/gateway-admin/src/use_case/plugins/artifacts.rs b/backend/crates/gateway-admin/src/use_case/plugins/artifacts.rs index a33f5e6a5..7bb72ee06 100644 --- a/backend/crates/gateway-admin/src/use_case/plugins/artifacts.rs +++ b/backend/crates/gateway-admin/src/use_case/plugins/artifacts.rs @@ -1,3 +1,5 @@ +//! 插件制品的验证、接受、安装与删除,以及首次安装的默认实例配置 + use std::{ collections::{BTreeMap, BTreeSet}, sync::Arc, @@ -30,7 +32,7 @@ use crate::{ use super::state::PluginStateService; -/// 远程插件来源访问所需的传输与受管出站代理目录。 +/// 远程插件来源访问所需的传输与受管出站代理目录 pub struct PluginDistributionPorts { pub(super) transport: Arc, pub(super) proxies: Arc, @@ -43,7 +45,7 @@ impl PluginDistributionPorts { } } -/// 所有安装入口在取得完整包后进入同一校验与事务流程。 +/// 所有安装入口在取得完整包后进入同一校验与事务流程 pub struct PluginsService { pub(super) store: Arc, pub(super) inspector: Arc, @@ -118,7 +120,7 @@ impl PluginsService { }) } - /// 管理上传固定为自定义来源;调用方不能借通用参数声明官方身份。 + /// 管理上传固定为自定义来源;调用方不能借通用参数声明官方身份 pub async fn install_upload( &self, archive: Arc<[u8]>, @@ -135,7 +137,7 @@ impl PluginsService { self.complete_install(mutation, context).await } - /// 接受已导入但尚未安装的制品;官方发行导入也必须经过管理员的这一步。 + /// 接受已导入但尚未安装的制品;官方发行导入也必须经过管理员的这一步 pub async fn accept_artifact( &self, digest: &str, @@ -185,7 +187,7 @@ impl PluginsService { .await?; // 其它管理员写入或同插件的并发安装可能推进全局 revision;稳定 creation ID - // 配合每轮重新读取,既不会复制默认实例,也不会覆盖先完成的另一个版本。 + // 配合每轮重新读取,既不会复制默认实例,也不会覆盖先完成的另一个版本 for _ in 0..3 { let snapshot = self .store @@ -311,7 +313,7 @@ fn default_instance_id(plugin_id: &str) -> String { let digest = hash.finalize(); let mut bytes = [0_u8; 16]; bytes.copy_from_slice(&digest[..16]); - // RFC 9562 UUIDv8 保留给应用自定义派生;variant 固定为 RFC 4122。 + // RFC 9562 UUIDv8 保留给应用自定义派生;variant 固定为 RFC 4122 bytes[6] = (bytes[6] & 0x0f) | 0x80; bytes[8] = (bytes[8] & 0x3f) | 0x80; uuid::Uuid::from_bytes(bytes).to_string() diff --git a/backend/crates/gateway-admin/src/use_case/plugins/distribution.rs b/backend/crates/gateway-admin/src/use_case/plugins/distribution.rs index 2ea751d91..66495510d 100644 --- a/backend/crates/gateway-admin/src/use_case/plugins/distribution.rs +++ b/backend/crates/gateway-admin/src/use_case/plugins/distribution.rs @@ -1,3 +1,5 @@ +//! 插件更新来源与下载凭据管理,以及远程制品验证和安装编排 + use std::collections::BTreeSet; use super::PluginsService; @@ -93,7 +95,7 @@ impl PluginsService { binding.outbound_proxy_id.as_deref(), ) .await?; - // 查询期间来源或策略被修改时返回冲突,不把旧来源的候选交给下一步安装。 + // 查询期间来源或策略被修改时返回冲突,不把旧来源的候选交给下一步安装 if !self.update_sources().await?.contains(&binding) { return Err(AdminError::conflict("插件更新来源已变更,请重新检查")); } @@ -259,7 +261,7 @@ impl PluginsService { { return Err(AdminError::invalid("插件清单身份与安装选择不符")); } - // 首次解析前身份未知,按包内身份复核下载前的来源绑定。 + // 首次解析前身份未知,按包内身份复核下载前的来源绑定 let binding = bindings .iter() .find(|binding| binding.plugin_id == artifact.metadata.plugin_id); diff --git a/backend/crates/gateway-admin/src/use_case/plugins/instances.rs b/backend/crates/gateway-admin/src/use_case/plugins/instances.rs index 19e440907..a3dbda592 100644 --- a/backend/crates/gateway-admin/src/use_case/plugins/instances.rs +++ b/backend/crates/gateway-admin/src/use_case/plugins/instances.rs @@ -1,3 +1,5 @@ +//! 插件实例的配置、停用、删除与私有状态迁移编排 + use std::collections::{BTreeMap, BTreeSet}; use gateway_core::{policy::ClientApiKeyId, routing::AccountGroupId}; @@ -18,10 +20,10 @@ use crate::{ }; impl PluginsService { - /// 只校验固定包体,不启动插件;停用的旧版本仍需向管理端说明不能启用的原因。 + /// 只校验固定包体,不启动插件;停用的旧版本仍需向管理端说明不能启用的原因 async fn compatibility_warning(&self, digest: &str) -> Result, AdminError> { - // 运行宿主和摘要固定,缓存纯静态结论,避免管理页轮询反复解包。 - // 只缓存有限数量的确定结果,暂时性错误仍可重试。 + // 运行宿主和摘要固定,缓存纯静态结论,避免管理页轮询反复解包 + // 只缓存有限数量的确定结果,暂时性错误仍可重试 let mut cache = self.compatibility.lock().await; if let Some(warning) = cache.get(digest) { return Ok(warning.clone()); @@ -362,7 +364,7 @@ impl PluginsService { self.preparation .activate_state(&prepared, &result.instance) .await?; - // prepared 的强引用跨过提交与唯一发布入口,防止候选在被读取之前回收。 + // prepared 的强引用跨过提交与唯一发布入口,防止候选在被读取之前回收 publish_committed(self.snapshots.as_ref(), result.config_revision).await?; drop(prepared); Ok(result) @@ -379,7 +381,7 @@ impl PluginsService { ) -> Result { let previous_state = self.preparation.validate(previous.clone()).await?; let original_revision = snapshot.config_revision; - // 即使最终保持停用,迁移也必须由目标制品的受控候选执行。 + // 即使最终保持停用,迁移也必须由目标制品的受控候选执行 let mut migration_instance = target.clone(); migration_instance.enabled = true; snapshot.config_revision = migration_instance.revision; @@ -651,7 +653,7 @@ impl PluginsService { Ok(revision) } - /// 紧急管理修复不要求损坏插件成功准备;提交后沿用现有发布与暂停合同。 + /// 紧急管理修复不要求损坏插件成功准备;提交后沿用现有发布与暂停合同 pub async fn disable_instance( &self, id: &str, diff --git a/backend/crates/gateway-admin/src/use_case/plugins/management.rs b/backend/crates/gateway-admin/src/use_case/plugins/management.rs index 377e00095..cb7b8f417 100644 --- a/backend/crates/gateway-admin/src/use_case/plugins/management.rs +++ b/backend/crates/gateway-admin/src/use_case/plugins/management.rs @@ -1,3 +1,5 @@ +//! 插件管理请求的目标解析、身份授权与运行时分派 + use std::sync::Arc; use gateway_core::runtime::{RuntimeSnapshotHandle, extensions::ExtensionSetReference}; @@ -16,7 +18,7 @@ use crate::{ ports::{plugin_management::PluginManagement, plugins::PluginStore}, }; -/// 菜单来自发布视图;每次业务动作复核持久实例,旧页面不能继续使用已停用或换版的目标。 +/// 菜单来自发布视图;每次业务动作复核持久实例,旧页面不能继续使用已停用或换版的目标 pub struct PluginManagementService { runtime: Arc, store: Arc, @@ -24,7 +26,7 @@ pub struct PluginManagementService { } impl PluginManagementService { - /// 页面发起模型请求前复核持久目标与当前发布代次;长响应可重复调用以收敛撤销。 + /// 页面发起模型请求前复核持久目标与当前发布代次;长响应可重复调用以收敛撤销 pub async fn validate_target(&self, target: &PluginManagementTarget) -> Result<(), AdminError> { let reference = self.authorize(target).await?; self.runtime.validate_target(&reference, target).await diff --git a/backend/crates/gateway-admin/src/use_case/plugins/mod.rs b/backend/crates/gateway-admin/src/use_case/plugins/mod.rs index 06ef142d1..a0b47aad1 100644 --- a/backend/crates/gateway-admin/src/use_case/plugins/mod.rs +++ b/backend/crates/gateway-admin/src/use_case/plugins/mod.rs @@ -1,3 +1,5 @@ +//! 插件制品、实例、分发、管理页面与私有状态用例的公共入口 + mod artifacts; mod distribution; mod instances; diff --git a/backend/crates/gateway-admin/src/use_case/plugins/official.rs b/backend/crates/gateway-admin/src/use_case/plugins/official.rs index 14e734d49..7a8532d1f 100644 --- a/backend/crates/gateway-admin/src/use_case/plugins/official.rs +++ b/backend/crates/gateway-admin/src/use_case/plugins/official.rs @@ -1,3 +1,5 @@ +//! 校验官方发行清单与平台制品,并编排插件发行导入 + use std::{collections::BTreeSet, path::Path}; use serde::Deserialize; @@ -74,9 +76,9 @@ struct Platform { } impl PluginsService { - /// 从与宿主发行物同信任边界的封口清单导入官方包。 + /// 从与宿主发行物同信任边界的封口清单导入官方包 /// - /// 所有清单项和包体会先完成静态校验,再进入与上传/远程安装相同的持久化用例。 + /// 所有清单项和包体会先完成静态校验,再进入与上传/远程安装相同的持久化用例 pub async fn import_official_release( &self, files: &dyn OfficialPluginReleaseFiles, @@ -104,7 +106,7 @@ impl PluginsService { .ok_or_else(|| AdminError::invalid("当前平台不支持官方插件发行物"))?; let mut validated = Vec::with_capacity(plugins.len()); - // 先完成整份清单的静态检查;持久化故障后的已提交前缀由下次启动幂等补齐。 + // 先完成整份清单的静态检查;持久化故障后的已提交前缀由下次启动幂等补齐 for plugin in plugins { let artifact = select_artifact(&plugin, platform)?; let archive = files @@ -189,7 +191,7 @@ pub(crate) fn validate_update_release( fn parse_host_compatibility( mut value: serde_json::Value, ) -> Result { - // 旧更新器要求 v1 与空权限字段,运行兼容性仍由清单、协议和能力版本表达。 + // 旧更新器要求 v1 与空权限字段,运行兼容性仍由清单、协议和能力版本表达 if value.get("schema_version") == Some(&serde_json::json!(1)) && value.get("permissions") == Some(&serde_json::json!([])) { diff --git a/backend/crates/gateway-admin/src/use_case/plugins/state.rs b/backend/crates/gateway-admin/src/use_case/plugins/state.rs index 37672f332..045c96973 100644 --- a/backend/crates/gateway-admin/src/use_case/plugins/state.rs +++ b/backend/crates/gateway-admin/src/use_case/plugins/state.rs @@ -1,3 +1,5 @@ +//! 插件私有状态迁移的开始、撤销与存储错误映射 + use std::sync::Arc; use crate::{ diff --git a/backend/crates/gateway-admin/src/use_case/plugins/versions.rs b/backend/crates/gateway-admin/src/use_case/plugins/versions.rs index 9f5596a63..40f267c54 100644 --- a/backend/crates/gateway-admin/src/use_case/plugins/versions.rs +++ b/backend/crates/gateway-admin/src/use_case/plugins/versions.rs @@ -1,3 +1,5 @@ +//! 插件实例的版本切换与回滚计划,以及目标版本配置合并 + use super::{ PluginsService, artifacts::{configuration_defaults, default_bindings}, @@ -69,7 +71,7 @@ impl PluginsService { secrets: Some(saved.secrets), bindings: saved.bindings, }; - // 沿用同一份快照的 CAS 和状态迁移流程,不能在检查目标后重新读取并覆盖并发配置。 + // 沿用同一份快照的 CAS 和状态迁移流程,不能在检查目标后重新读取并覆盖并发配置 self.configure_from_snapshot(snapshot, Some(existing), configuration, context) .await } @@ -187,7 +189,7 @@ impl PluginsService { secrets: Some(configuration.secrets), bindings: configuration.bindings, }; - // 同一次读取的 CAS 覆盖草稿计算、准备与提交,不能覆盖并发设置。 + // 同一次读取的 CAS 覆盖草稿计算、准备与提交,不能覆盖并发设置 self.configure_from_snapshot(snapshot, Some(existing), request, context) .await } @@ -223,7 +225,7 @@ impl PluginsService { return Ok((saved, true)); } let mut configuration = instance.configuration.clone(); - // 只补充缺失的默认值,保留显式值及未知字段,让不兼容项由校验反馈给用户。 + // 只补充缺失的默认值,保留显式值及未知字段,让不兼容项由校验反馈给用户 merge_missing_defaults(&mut configuration, configuration_defaults(&target.metadata)); Ok(( PluginVersionConfiguration { @@ -242,7 +244,7 @@ fn version_bindings( target: &PluginArtifactMetadata, ) -> Vec { let mut bindings = default_bindings(target); - // 已关闭的既有能力继续关闭,新声明能力才采用包默认值。 + // 已关闭的既有能力继续关闭,新声明能力才采用包默认值 bindings.retain(|binding| { let capability = target .contributes diff --git a/backend/crates/gateway-admin/src/use_case/proxies.rs b/backend/crates/gateway-admin/src/use_case/proxies.rs index 5e37bf997..49eed24b6 100644 --- a/backend/crates/gateway-admin/src/use_case/proxies.rs +++ b/backend/crates/gateway-admin/src/use_case/proxies.rs @@ -1,3 +1,5 @@ +//! 账号出站代理的管理、账号解绑与连通性测试用例 + use std::sync::Arc; use async_trait::async_trait; @@ -59,7 +61,7 @@ pub trait ProxiesService: Send + Sync { detect_location: bool, context: &MutationContext, ) -> Result; - /// 探测未保存的连接地址,不写入代理记录或修改账号绑定。 + /// 探测未保存的连接地址,不写入代理记录或修改账号绑定 async fn probe( &self, proxy: &OutboundProxy, @@ -140,7 +142,7 @@ impl ProxiesService for DefaultProxiesService { .map_err(|error| map_store_error(error, "proxy"))?; page.items = stream::iter(page.items) .map(|mut account| async { - // 与账号目录一致,缺失套餐时读取已有额度快照;限制并发且不触发上游刷新。 + // 与账号目录一致,缺失套餐时读取已有额度快照;限制并发且不触发上游刷新 let mut cached_quota = None; if explicit_plan_type(account.plan_type.as_deref()).is_none() && let Ok(kind) = ProviderKind::new(account.provider_kind.clone()) @@ -216,7 +218,7 @@ impl ProxiesService for DefaultProxiesService { .map(|location| location.map(|value| value.normalized()).transpose()) .transpose() .map_err(|_| AdminError::invalid("代理位置不合法"))?; - // 只在开启自动模式或连接地址变化时检测,普通编辑不刷新已识别位置。 + // 只在开启自动模式或连接地址变化时检测,普通编辑不刷新已识别位置 command.test = None; if command.auto_location.is_some() || command.proxy.is_some() { let current = self @@ -287,7 +289,7 @@ impl ProxiesService for DefaultProxiesService { if record.revision != revision { return Err(AdminError::conflict("代理已被修改,请刷新后重新测试")); } - // 手动解析只为本次测试请求位置,不改变自动跟随的持久配置。 + // 手动解析只为本次测试请求位置,不改变自动跟随的持久配置 let result = self .probe .test(&record.proxy, record.auto_location || detect_location) diff --git a/backend/crates/gateway-admin/src/use_case/settings.rs b/backend/crates/gateway-admin/src/use_case/settings.rs index 08382ab5a..dfc76ecc1 100644 --- a/backend/crates/gateway-admin/src/use_case/settings.rs +++ b/backend/crates/gateway-admin/src/use_case/settings.rs @@ -1,4 +1,4 @@ -//! Runtime settings 与管理员 API Key 用例。 +//! Runtime settings 与管理员 API Key 用例 use std::sync::Arc; @@ -20,7 +20,7 @@ use crate::{ use super::{map_store_error, publish_committed}; -/// API 消费的 Runtime settings 管理服务。 +/// API 消费的 Runtime settings 管理服务 #[async_trait] pub trait SettingsService: Send + Sync { async fn preview_pricing_sync( diff --git a/backend/crates/gateway-admin/src/use_case/system.rs b/backend/crates/gateway-admin/src/use_case/system.rs index bf0badfe9..1640cabc8 100644 --- a/backend/crates/gateway-admin/src/use_case/system.rs +++ b/backend/crates/gateway-admin/src/use_case/system.rs @@ -1,4 +1,4 @@ -//! 系统管理用例。 +//! 系统管理用例 use std::sync::Arc; @@ -19,7 +19,7 @@ use crate::{ }, }; -/// API 消费的系统管理服务。 +/// API 消费的系统管理服务 #[async_trait] pub trait SystemService: Send + Sync { async fn version(&self) -> Result; @@ -44,7 +44,7 @@ pub trait SystemService: Send + Sync { ) -> Result; } -/// 保持 Host 能力窄边界的默认系统用例。 +/// 保持 Host 能力窄边界的默认系统用例 pub(crate) struct DefaultSystemService { operations: Arc, preflight: Arc, diff --git a/backend/crates/gateway-admin/tests/backup/mod.rs b/backend/crates/gateway-admin/tests/backup/mod.rs index da4823694..de812b664 100644 --- a/backend/crates/gateway-admin/tests/backup/mod.rs +++ b/backend/crates/gateway-admin/tests/backup/mod.rs @@ -1,4 +1,4 @@ -//! 备份模块测试:镜像 src/backup/ 的 policy 与 task。 +//! 备份模块测试:镜像 src/backup/ 的 policy 与 task mod policy; pub mod support; diff --git a/backend/crates/gateway-admin/tests/backup/policy.rs b/backend/crates/gateway-admin/tests/backup/policy.rs index 2888b68e1..0f7a155e7 100644 --- a/backend/crates/gateway-admin/tests/backup/policy.rs +++ b/backend/crates/gateway-admin/tests/backup/policy.rs @@ -1,4 +1,4 @@ -//! 备份调度与保留策略(backup/policy.rs)的纯逻辑测试。 +//! 备份调度与保留策略(backup/policy.rs)的纯逻辑测试 use chrono::{Duration, TimeZone as _, Utc}; @@ -55,7 +55,7 @@ fn next_after_is_strictly_later() { #[test] fn last_firing_is_at_or_before() { let schedule = BackupSchedule::parse("0 2 * * *", "Asia/Shanghai".parse().unwrap()).unwrap(); - // 北京时间 02:00 = 前一日 18:00 UTC。 + // 北京时间 02:00 = 前一日 18:00 UTC let before = Utc.with_ymd_and_hms(2026, 8, 1, 17, 59, 0).unwrap(); assert_eq!( schedule.last_firing_at_or_before(before).unwrap(), diff --git a/backend/crates/gateway-admin/tests/backup/support.rs b/backend/crates/gateway-admin/tests/backup/support.rs index 5aada9459..60b105890 100644 --- a/backend/crates/gateway-admin/tests/backup/support.rs +++ b/backend/crates/gateway-admin/tests/backup/support.rs @@ -1,8 +1,8 @@ -//! 备份测试共享 fixture:内存 fake 端口与辅助函数。 +//! 备份测试共享 fixture:内存 fake 端口与辅助函数 //! -//! 用内存 fake 端口驱动编排层,不依赖 MinIO/真实 S3。 -//! s3.rs / pg_dump.rs 是 SDK 薄适配器,协议正确性交给 aws-sdk / pg_dump 保证。 -//! 供 backup::task 与 use_case::backup 的测试复用。 +//! 用内存 fake 端口驱动编排层,不依赖 MinIO/真实 S3 +//! s3.rs / pg_dump.rs 是 SDK 薄适配器,协议正确性交给 aws-sdk / pg_dump 保证 +//! 供 backup::task 与 use_case::backup 的测试复用 use std::sync::Mutex; @@ -31,20 +31,20 @@ use secrecy::SecretString; use sha2::{Digest as _, Sha256}; use tempfile::TempDir; -/// 假归档内容;dump 与上传校验共用同一字节与校验值。 +/// 假归档内容;dump 与上传校验共用同一字节与校验值 pub(crate) const DUMP_CONTENT: &[u8] = b"custom-format-archive-bytes-0123456789"; -/// 计算一段内容的 SHA-256 十六进制。 +/// 计算一段内容的 SHA-256 十六进制 pub(crate) fn sha256_hex(content: &[u8]) -> String { hex::encode(Sha256::digest(content)) } -/// 返回 32 位十六进制的合法备份记录 id。 +/// 返回 32 位十六进制的合法备份记录 id pub(crate) fn backup_id(suffix: &str) -> String { format!("backup_{}", sha256_hex(suffix.as_bytes())) } -/// 已配置且已通过连接测试的设置快照。 +/// 已配置且已通过连接测试的设置快照 pub(crate) fn configured_settings() -> BackupSettings { let now = Utc::now(); BackupSettings { @@ -67,7 +67,7 @@ pub(crate) fn configured_settings() -> BackupSettings { } } -/// 从 seed 构造 queued 记录。 +/// 从 seed 构造 queued 记录 pub(crate) fn queued_record(seed: &BackupRecordSeed) -> BackupRecord { let now = Utc::now(); BackupRecord { @@ -89,7 +89,7 @@ pub(crate) fn queued_record(seed: &BackupRecordSeed) -> BackupRecord { } } -/// 内存版备份仓储。 +/// 内存版备份仓储 pub(crate) struct FakeBackupRepository { settings: Mutex, records: Mutex>, @@ -103,12 +103,12 @@ impl FakeBackupRepository { } } - /// 测试辅助:读取当前全部记录。 + /// 测试辅助:读取当前全部记录 pub(crate) fn all_records(&self) -> Vec { self.records.lock().expect("records").clone() } - /// 测试辅助:强制把一条记录标记为指定时间完成的 completed。 + /// 测试辅助:强制把一条记录标记为指定时间完成的 completed pub(crate) fn set_completed(&self, id: &str, completed_at: DateTime) { let mut records = self.records.lock().expect("records"); if let Some(record) = records.iter_mut().find(|record| record.id == id) { @@ -118,7 +118,7 @@ impl FakeBackupRepository { } } - /// 测试辅助:强制把一条记录迁移到指定状态。 + /// 测试辅助:强制把一条记录迁移到指定状态 pub(crate) fn force_status(&self, id: &str, status: BackupStatus) { let mut records = self.records.lock().expect("records"); if let Some(record) = records.iter_mut().find(|record| record.id == id) { @@ -450,7 +450,7 @@ impl BackupRepository for FakeBackupRepository { } } -/// 假 `pg_dump` 端口:把固定内容写入临时文件作为归档;可注入失败。 +/// 假 `pg_dump` 端口:把固定内容写入临时文件作为归档;可注入失败 pub(crate) struct FakeDumpPort { dir: TempDir, fail_dump: bool, @@ -464,7 +464,7 @@ impl FakeDumpPort { } } - /// 让下一次 `dump` 返回 `backup.pg_dump_failed`。 + /// 让下一次 `dump` 返回 `backup.pg_dump_failed` pub(crate) fn fail_next_dump(mut self) -> Self { self.fail_dump = true; self @@ -525,7 +525,7 @@ impl DatabaseDumpPort for FakeDumpPort { } } -/// 内存版对象存储:把上传内容按 key 保存并支持 Head/Delete/预签名。 +/// 内存版对象存储:把上传内容按 key 保存并支持 Head/Delete/预签名 pub(crate) struct FakeObjectStore { pub(crate) objects: Mutex>>, upload_error: Option, @@ -539,7 +539,7 @@ impl FakeObjectStore { } } - /// 让上传返回指定的脱敏 S3 错误。 + /// 让上传返回指定的脱敏 S3 错误 pub(crate) fn fail_upload(mut self, code: &'static str, message: impl Into) -> Self { self.upload_error = Some(BackupError::new(code, message.into())); self @@ -621,7 +621,7 @@ impl BackupObjectStorePort for FakeObjectStore { } } -/// 记录审计事件的假 AuthStore。 +/// 记录审计事件的假 AuthStore pub(crate) struct FakeAuthStore { audit: Mutex>, } @@ -712,7 +712,7 @@ impl AuthStore for FakeAuthStore { } } -/// 系统发起者上下文。 +/// 系统发起者上下文 pub(crate) fn system_context() -> MutationContext { MutationContext { actor: MutationActor::System, diff --git a/backend/crates/gateway-admin/tests/backup/task.rs b/backend/crates/gateway-admin/tests/backup/task.rs index cf6d1c20a..49b31287f 100644 --- a/backend/crates/gateway-admin/tests/backup/task.rs +++ b/backend/crates/gateway-admin/tests/backup/task.rs @@ -1,4 +1,4 @@ -//! BackupTask 执行链测试:驱动内存 fake 端口跑完整 dump/upload/verify/retention 流程。 +//! BackupTask 执行链测试:驱动内存 fake 端口跑完整 dump/upload/verify/retention 流程 use std::sync::Arc; @@ -50,7 +50,7 @@ async fn backup_task_runs_full_dump_upload_verify_pipeline() { Some(sha256_hex(DUMP_CONTENT).as_str()) ); - // 对象已上传且内容与本地归档一致。 + // 对象已上传且内容与本地归档一致 let stored = object_store .object(&seed.object_key) .expect("uploaded object exists"); @@ -85,7 +85,7 @@ async fn backup_task_marks_failed_when_dump_port_fails() { assert_eq!(failed.status, BackupStatus::Failed); assert_eq!(failed.error_code.as_deref(), Some("backup.pg_dump_failed")); assert!(failed.completed_at.is_some()); - // 归档未上传,对象不存在。 + // 归档未上传,对象不存在 assert!(object_store.object(&seed.object_key).is_none()); } @@ -167,7 +167,7 @@ async fn retention_cleans_expired_scheduled_backups() { let dump = Arc::new(FakeDumpPort::new()); let object_store = Arc::new(FakeObjectStore::new()); - // 造一条旧 completed 计划备份(超过 retention_days)与一条新备份。 + // 造一条旧 completed 计划备份(超过 retention_days)与一条新备份 let old_seed = BackupRecordSeed { id: backup_id("old"), trigger_kind: BackupTriggerKind::Scheduled, @@ -186,16 +186,16 @@ async fn retention_cleans_expired_scheduled_backups() { .insert_backup_record(old_seed.clone()) .await .expect("insert old"); - // 旧备份 40 天前完成(释放活跃名额),再插入新备份。 + // 旧备份 40 天前完成(释放活跃名额),再插入新备份 let now = Utc::now(); repository.set_completed(&backup_id("old"), now - Duration::days(40)); repository .insert_backup_record(new_seed.clone()) .await .expect("insert new"); - // 新备份 1 天前完成。 + // 新备份 1 天前完成 repository.set_completed(&backup_id("new"), now - Duration::days(1)); - // 两个对象都已上传。 + // 两个对象都已上传 object_store .objects .lock() @@ -207,7 +207,7 @@ async fn retention_cleans_expired_scheduled_backups() { .expect("objects") .insert(new_seed.object_key.clone(), DUMP_CONTENT.to_vec()); - // 设置 retention_days = 30。 + // 设置 retention_days = 30 repository .update_schedule_settings( UpdateBackupScheduleCommand { @@ -229,7 +229,7 @@ async fn retention_cleans_expired_scheduled_backups() { .await .expect("run cycle"); - // 旧备份被硬删除,对象被删除;新备份保留。 + // 旧备份被硬删除,对象被删除;新备份保留 let records = repository.all_records(); let ids: Vec<_> = records.iter().map(|record| record.id.as_str()).collect(); assert_eq!(ids, vec![backup_id("new").as_str()]); diff --git a/backend/crates/gateway-admin/tests/main.rs b/backend/crates/gateway-admin/tests/main.rs index 448160f15..652edfda4 100644 --- a/backend/crates/gateway-admin/tests/main.rs +++ b/backend/crates/gateway-admin/tests/main.rs @@ -1,3 +1,5 @@ +//! 管理控制面测试入口,以及通用分页、版本与查询值对象测试 + use std::str::FromStr as _; use chrono::{TimeDelta, Utc}; diff --git a/backend/crates/gateway-admin/tests/model/audit.rs b/backend/crates/gateway-admin/tests/model/audit.rs index 45d086d76..aebd71ec5 100644 --- a/backend/crates/gateway-admin/tests/model/audit.rs +++ b/backend/crates/gateway-admin/tests/model/audit.rs @@ -1,3 +1,5 @@ +//! 验证管理审计意图保留操作身份,并按提交结果选择审计分类 + use gateway_admin::model::{ MutationActor, MutationContext, audit::{MutationAuditIntent, MutationAuditOperation}, diff --git a/backend/crates/gateway-admin/tests/model/backup/mod.rs b/backend/crates/gateway-admin/tests/model/backup/mod.rs index 5984a89ff..284433f53 100644 --- a/backend/crates/gateway-admin/tests/model/backup/mod.rs +++ b/backend/crates/gateway-admin/tests/model/backup/mod.rs @@ -1,4 +1,4 @@ -//! 备份领域模型(model/backup)的纯逻辑测试。 +//! 备份领域模型(model/backup)的纯逻辑测试 use chrono::{TimeZone as _, Utc}; use secrecy::SecretString; diff --git a/backend/crates/gateway-admin/tests/model/mod.rs b/backend/crates/gateway-admin/tests/model/mod.rs index 369634178..877f0dbe7 100644 --- a/backend/crates/gateway-admin/tests/model/mod.rs +++ b/backend/crates/gateway-admin/tests/model/mod.rs @@ -1,4 +1,4 @@ -//! 模型模块测试:镜像 src/model/ 中带逻辑的模块。 +//! 模型模块测试:镜像 src/model/ 中带逻辑的模块 mod audit; mod backup; diff --git a/backend/crates/gateway-admin/tests/model/plugins/mod.rs b/backend/crates/gateway-admin/tests/model/plugins/mod.rs index dc4c2003a..56cf18c83 100644 --- a/backend/crates/gateway-admin/tests/model/plugins/mod.rs +++ b/backend/crates/gateway-admin/tests/model/plugins/mod.rs @@ -1,3 +1,5 @@ +//! 验证插件元数据兼容新增字段,同时保留已知字段的类型约束 + use gateway_admin::model::plugins::PluginArtifactMetadata; use serde_json::{Value, json}; diff --git a/backend/crates/gateway-admin/tests/model/provider_credentials.rs b/backend/crates/gateway-admin/tests/model/provider_credentials.rs index 5b2cdac4a..b2ab6e5ce 100644 --- a/backend/crates/gateway-admin/tests/model/provider_credentials.rs +++ b/backend/crates/gateway-admin/tests/model/provider_credentials.rs @@ -1,3 +1,5 @@ +//! 验证 Provider 额度窗口投影与待提交凭据变更的数据合同 + use gateway_admin::model::provider_credentials::{ AccountUsagePeriod, AuthorizationMutationTarget, AuthorizationOwnerBinding, PendingAuthorizationMutation, ProviderQuota, ProviderQuotaWindow, ProviderQuotaWindowRole, diff --git a/backend/crates/gateway-admin/tests/model/quota_forecast.rs b/backend/crates/gateway-admin/tests/model/quota_forecast.rs index 945816f27..4577d2856 100644 --- a/backend/crates/gateway-admin/tests/model/quota_forecast.rs +++ b/backend/crates/gateway-admin/tests/model/quota_forecast.rs @@ -1,3 +1,5 @@ +//! 验证额度预测的窗口选择、容量估算与无效样本边界 + use chrono::{DateTime, Duration, Utc}; use gateway_admin::model::{ accounts::{AccountCost, AccountUsage}, @@ -311,7 +313,7 @@ fn partial_costs_keep_estimates_from_known_amounts() { fn missing_tokens_and_costs_do_not_block_recorded_capacity_estimates() { let source = quota(vec![window("week", 7)]); let mut sample = samples(&source).remove(0); - // 完整请求之外混有缺少计量的成功请求,不把已有用量和费用一并作废。 + // 完整请求之外混有缺少计量的成功请求,不把已有用量和费用一并作废 sample.usage.request_count += 2; sample.usage.missing_token_count = 2; sample.usage.unavailable_cost_count = 2; diff --git a/backend/crates/gateway-admin/tests/model/quota_forecast_sampling.rs b/backend/crates/gateway-admin/tests/model/quota_forecast_sampling.rs index 67c386f50..e270d43e1 100644 --- a/backend/crates/gateway-admin/tests/model/quota_forecast_sampling.rs +++ b/backend/crates/gateway-admin/tests/model/quota_forecast_sampling.rs @@ -1,3 +1,5 @@ +//! 验证额度预测采样的去重、回退处理与近期窗口选择 + use chrono::{DateTime, Duration, Utc}; use gateway_admin::model::quota_forecast_sampling::{ QuotaForecastMethod, QuotaForecastPoint, QuotaForecastSample, QuotaForecastUsage, diff --git a/backend/crates/gateway-admin/tests/model/retention.rs b/backend/crates/gateway-admin/tests/model/retention.rs index 58d1ddbe5..4b384212a 100644 --- a/backend/crates/gateway-admin/tests/model/retention.rs +++ b/backend/crates/gateway-admin/tests/model/retention.rs @@ -1,3 +1,5 @@ +//! 验证历史保留窗口的有效范围与各类数据的独立配置 + use gateway_admin::model::retention::{RetentionPolicy, RetentionTarget}; #[test] diff --git a/backend/crates/gateway-admin/tests/use_case/account_groups.rs b/backend/crates/gateway-admin/tests/use_case/account_groups.rs index b534ee213..5543a9c5f 100644 --- a/backend/crates/gateway-admin/tests/use_case/account_groups.rs +++ b/backend/crates/gateway-admin/tests/use_case/account_groups.rs @@ -1,3 +1,5 @@ +//! 验证账号分组分页查询的运行态补充与可用容量聚合 + use std::{ collections::BTreeMap, str::FromStr as _, diff --git a/backend/crates/gateway-admin/tests/use_case/accounts.rs b/backend/crates/gateway-admin/tests/use_case/accounts.rs index 73780025a..2ee44a126 100644 --- a/backend/crates/gateway-admin/tests/use_case/accounts.rs +++ b/backend/crates/gateway-admin/tests/use_case/accounts.rs @@ -1,3 +1,5 @@ +//! 账号管理用例测试及共享 Provider、存储替身 + use std::{ collections::BTreeMap, sync::{Arc, Mutex}, @@ -726,7 +728,7 @@ impl AccountStore for FakeAccountStore { _: AccountRuntimeSnapshot, ) -> AdminStoreResult> { self.record("store.load_account"); - // probe 后的账号状态覆盖只对同一 id 生效;其余按账号列表查询。 + // probe 后的账号状态覆盖只对同一 id 生效;其余按账号列表查询 let account = self .account_after_probe .lock() @@ -1477,7 +1479,7 @@ async fn plugin_account_reads_resolve_the_authoritative_provider_from_the_accoun let events = events(); let store = FakeAccountStore::new("xai", events.clone()); // 读取端口不能让调用方提供或伪造 Provider;注册表中即使只有另一 Provider, - // 也必须按 Store 中该 account ID 的权威事实返回。 + // 也必须按 Store 中该 account ID 的权威事实返回 let registered_provider = FakeProviderAdmin::new("openai", events.clone()); let access = gateway_admin::initialize_plugin_accounts( ProviderAdminRegistry::new([registered_provider as Arc]).unwrap(), @@ -2097,7 +2099,7 @@ async fn accounts_list_should_prefer_credential_error_over_quota_exhaustion() { #[tokio::test] async fn accounts_list_should_map_unknown_credential_to_error_not_normal() { - // Unknown 不可调度,Admin 不得显示为 normal。 + // Unknown 不可调度,Admin 不得显示为 normal let provider = FakeProviderAdmin::new("openai", events()); let mut account = account_record("openai"); account.credential_state = CredentialState::Unknown; @@ -2244,7 +2246,7 @@ async fn quota_forecast_reads_raw_snapshot_and_limits_usage_to_observation_time( used_percent: Some(20.0), reset_at: Some(reset), limit_reached: true, - // Provider 自带的统计不保证与快照同一时间,预测必须重新采样。 + // Provider 自带的统计不保证与快照同一时间,预测必须重新采样 local_usage: Some(quota_local_usage("acct_test", 999_999)), provider_data: None, }], @@ -2456,7 +2458,7 @@ async fn quota_forecast_mid_cycle_sampling_accepts_small_reset_jitter_but_not_a_ .unwrap() .contains("不连续") ); - // 新额度段已有足够观测后恢复预测,但总量不能带回重置前的累计用量。 + // 新额度段已有足够观测后恢复预测,但总量不能带回重置前的累计用量 let mut first = make_point(1, 5.0, 1_750, 0); first.completed_at = now - TimeDelta::minutes(45); first.started_at = first.completed_at - TimeDelta::seconds(10); @@ -2581,7 +2583,7 @@ async fn accounts_list_should_attach_local_usage_to_quota_windows() { .as_ref() .expect("quota window local usage"); assert_eq!(usage.total_tokens, Some(4_330_000)); - // 短期额度条保留自己的本地用量,但不作为周/月统计面板的回退。 + // 短期额度条保留自己的本地用量,但不作为周/月统计面板的回退 assert!(item.usage.is_none()); let queries = store.quota_window_queries(); diff --git a/backend/crates/gateway-admin/tests/use_case/auth.rs b/backend/crates/gateway-admin/tests/use_case/auth.rs index 8cae34cc1..bbf7da7ff 100644 --- a/backend/crates/gateway-admin/tests/use_case/auth.rs +++ b/backend/crates/gateway-admin/tests/use_case/auth.rs @@ -1,3 +1,5 @@ +//! 验证管理员认证、密码变更、会话绑定与限流行为 + use std::{collections::BTreeMap, sync::Mutex}; use async_trait::async_trait; diff --git a/backend/crates/gateway-admin/tests/use_case/auth_key.rs b/backend/crates/gateway-admin/tests/use_case/auth_key.rs index 406356160..5d78319fc 100644 --- a/backend/crates/gateway-admin/tests/use_case/auth_key.rs +++ b/backend/crates/gateway-admin/tests/use_case/auth_key.rs @@ -1,3 +1,5 @@ +//! 验证 Client Key 登录、会话有效期与失效 Key 的会话撤销 + use std::{ collections::HashMap, net::{IpAddr, Ipv4Addr}, diff --git a/backend/crates/gateway-admin/tests/use_case/backup.rs b/backend/crates/gateway-admin/tests/use_case/backup.rs index 638d762ae..58739b0e6 100644 --- a/backend/crates/gateway-admin/tests/use_case/backup.rs +++ b/backend/crates/gateway-admin/tests/use_case/backup.rs @@ -1,4 +1,4 @@ -//! BackupService 用例测试:通过 AdminHarness 注入 fake 备份端口构造真实服务。 +//! BackupService 用例测试:通过 AdminHarness 注入 fake 备份端口构造真实服务 use std::sync::Arc; @@ -29,7 +29,7 @@ async fn backup_service_creates_downloads_and_deletes_with_audit() { .build() .await; - // 创建手动备份 → queued + 审计。 + // 创建手动备份 → queued + 审计 let created = services .backups() .create_backup(&system_context(), None) @@ -43,7 +43,7 @@ async fn backup_service_creates_downloads_and_deletes_with_audit() { .any(|action| action == "backup.created") ); - // 下载地址要求 completed 记录。 + // 下载地址要求 completed 记录 repository.force_status(&created.id, BackupStatus::Completed); let download = services .backups() @@ -57,7 +57,7 @@ async fn backup_service_creates_downloads_and_deletes_with_audit() { .any(|action| action == "backup.download_url_created") ); - // 删除 → deleting + 审计。 + // 删除 → deleting + 审计 let deleting = services .backups() .delete_backup(&system_context(), &created.id) @@ -94,7 +94,7 @@ async fn backup_service_rejects_download_of_non_completed_record() { .await .expect("create backup"); - // queued 记录不能创建下载地址。 + // queued 记录不能创建下载地址 let error = services .backups() .download_url(&system_context(), &created.id) diff --git a/backend/crates/gateway-admin/tests/use_case/client_keys.rs b/backend/crates/gateway-admin/tests/use_case/client_keys.rs index fd88eadb3..51c3b6f44 100644 --- a/backend/crates/gateway-admin/tests/use_case/client_keys.rs +++ b/backend/crates/gateway-admin/tests/use_case/client_keys.rs @@ -1,3 +1,5 @@ +//! Client Key 管理、预算变更与存储调用的用例测试 + use std::sync::{Arc, Mutex}; use async_trait::async_trait; diff --git a/backend/crates/gateway-admin/tests/use_case/credentials/mod.rs b/backend/crates/gateway-admin/tests/use_case/credentials/mod.rs index 9bce39311..78ac04fec 100644 --- a/backend/crates/gateway-admin/tests/use_case/credentials/mod.rs +++ b/backend/crates/gateway-admin/tests/use_case/credentials/mod.rs @@ -1,2 +1,4 @@ +//! 按 Provider 组织凭据导入、授权与提交边界测试 + mod openai; mod xai; diff --git a/backend/crates/gateway-admin/tests/use_case/credentials/openai.rs b/backend/crates/gateway-admin/tests/use_case/credentials/openai.rs index 33e3e2e4b..e5a97864d 100644 --- a/backend/crates/gateway-admin/tests/use_case/credentials/openai.rs +++ b/backend/crates/gateway-admin/tests/use_case/credentials/openai.rs @@ -1,3 +1,5 @@ +//! 验证 OpenAI 凭据准备、事务提交、授权重试与额度刷新边界 + use std::sync::Arc; use gateway_core::account::ProviderAccountId; diff --git a/backend/crates/gateway-admin/tests/use_case/credentials/xai.rs b/backend/crates/gateway-admin/tests/use_case/credentials/xai.rs index 28e8ef483..2bd05488c 100644 --- a/backend/crates/gateway-admin/tests/use_case/credentials/xai.rs +++ b/backend/crates/gateway-admin/tests/use_case/credentials/xai.rs @@ -1,3 +1,5 @@ +//! 验证 xAI 凭据提交与初始额度刷新的顺序及失败隔离 + use std::sync::Arc; use gateway_core::account::ProviderAccountId; diff --git a/backend/crates/gateway-admin/tests/use_case/freeze_recovery.rs b/backend/crates/gateway-admin/tests/use_case/freeze_recovery.rs index b01f0a775..d063be995 100644 --- a/backend/crates/gateway-admin/tests/use_case/freeze_recovery.rs +++ b/backend/crates/gateway-admin/tests/use_case/freeze_recovery.rs @@ -1,4 +1,4 @@ -//! 容量熔断冻结恢复编排回归:到期探测、失败顺延与自适应并发下调。 +//! 容量熔断冻结恢复编排回归:到期探测、失败顺延与自适应并发下调 use std::collections::BTreeMap; use std::sync::{Arc, Mutex}; @@ -123,7 +123,7 @@ fn store_unavailable() -> AdminStoreError { ) } -/// 记录解冻/顺延调用的运行态 fake;冻结与峰值证据由测试预置。 +/// 记录解冻/顺延调用的运行态 fake;冻结与峰值证据由测试预置 struct FreezeRuntimeStore { freezes: BTreeMap, peaks: BTreeMap, @@ -367,7 +367,7 @@ async fn adaptive_concurrency_lowers_limit_to_observed_peak() { run_cycle(&task).await; - // 峰值 4 × 0.8 = 3(不低于下限 2),低于全局默认 5,应下调到 3。 + // 峰值 4 × 0.8 = 3(不低于下限 2),低于全局默认 5,应下调到 3 assert_eq!( *store.lowered_limits.lock().expect("lowered limits"), vec![("acct_test".to_owned(), 3)] @@ -392,11 +392,11 @@ async fn adaptive_concurrency_never_raises_limit() { run_cycle(&task).await; - // 峰值 40 的目标 32 高于全局默认 5;只降不升,不下发任何更新。 + // 峰值 40 的目标 32 高于全局默认 5;只降不升,不下发任何更新 assert!(store.update_commands().is_empty()); } -/// 到期时间还早(超过探测提前量)的冻结不发起探测。 +/// 到期时间还早(超过探测提前量)的冻结不发起探测 #[tokio::test] async fn probe_skips_freezes_far_from_expiry() { let runtime = FreezeRuntimeStore::new( @@ -416,7 +416,7 @@ async fn probe_skips_freezes_far_from_expiry() { assert!(runtime.extended().is_empty()); } -/// 停用账号不参与探测恢复;并发下调同样跳过。 +/// 停用账号不参与探测恢复;并发下调同样跳过 #[tokio::test] async fn disabled_accounts_are_skipped() { let runtime = FreezeRuntimeStore::new( diff --git a/backend/crates/gateway-admin/tests/use_case/import_tasks.rs b/backend/crates/gateway-admin/tests/use_case/import_tasks.rs index 41d45798d..2ba8d9d7a 100644 --- a/backend/crates/gateway-admin/tests/use_case/import_tasks.rs +++ b/backend/crates/gateway-admin/tests/use_case/import_tasks.rs @@ -1,3 +1,5 @@ +//! 验证后台账号导入任务的幂等、身份隔离、并发与停止行为 + use std::{sync::Arc, time::Duration}; use gateway_admin::{ diff --git a/backend/crates/gateway-admin/tests/use_case/mod.rs b/backend/crates/gateway-admin/tests/use_case/mod.rs index 6d788a969..b7179a6f6 100644 --- a/backend/crates/gateway-admin/tests/use_case/mod.rs +++ b/backend/crates/gateway-admin/tests/use_case/mod.rs @@ -1,3 +1,5 @@ +//! 管理控制面各服务用例的测试入口 + mod account_groups; mod accounts; mod auth; diff --git a/backend/crates/gateway-admin/tests/use_case/observability.rs b/backend/crates/gateway-admin/tests/use_case/observability.rs index bee566672..509ea8dee 100644 --- a/backend/crates/gateway-admin/tests/use_case/observability.rs +++ b/backend/crates/gateway-admin/tests/use_case/observability.rs @@ -1,3 +1,5 @@ +//! 验证管理观测查询的时间范围、健康投影与运行态聚合 + use std::{ str::FromStr as _, sync::{ diff --git a/backend/crates/gateway-admin/tests/use_case/plugin_update.rs b/backend/crates/gateway-admin/tests/use_case/plugin_update.rs index 70ad8a1e6..36cf3daf0 100644 --- a/backend/crates/gateway-admin/tests/use_case/plugin_update.rs +++ b/backend/crates/gateway-admin/tests/use_case/plugin_update.rs @@ -1,3 +1,5 @@ +//! 插件参与系统升级与回滚的兼容性预检测试 + use std::{collections::BTreeMap, sync::Arc}; use async_trait::async_trait; diff --git a/backend/crates/gateway-admin/tests/use_case/plugins/artifacts.rs b/backend/crates/gateway-admin/tests/use_case/plugins/artifacts.rs index b145f97f1..3b1d6c598 100644 --- a/backend/crates/gateway-admin/tests/use_case/plugins/artifacts.rs +++ b/backend/crates/gateway-admin/tests/use_case/plugins/artifacts.rs @@ -1,3 +1,5 @@ +//! 插件制品安装、接受、默认实例与发布行为的用例测试 + use std::{ collections::BTreeMap, sync::{Arc, Mutex}, diff --git a/backend/crates/gateway-admin/tests/use_case/plugins/distribution.rs b/backend/crates/gateway-admin/tests/use_case/plugins/distribution.rs index 9cb6ab567..05b8ef9fc 100644 --- a/backend/crates/gateway-admin/tests/use_case/plugins/distribution.rs +++ b/backend/crates/gateway-admin/tests/use_case/plugins/distribution.rs @@ -1,3 +1,5 @@ +//! 插件分发来源、凭据与远程制品安装的用例测试 + use std::sync::{Arc, Mutex}; use async_trait::async_trait; diff --git a/backend/crates/gateway-admin/tests/use_case/plugins/instances.rs b/backend/crates/gateway-admin/tests/use_case/plugins/instances.rs index 83bde5d31..e338877c5 100644 --- a/backend/crates/gateway-admin/tests/use_case/plugins/instances.rs +++ b/backend/crates/gateway-admin/tests/use_case/plugins/instances.rs @@ -1,3 +1,5 @@ +//! 插件实例配置、版本变更与私有状态迁移的用例测试 + use std::{ collections::BTreeMap, sync::{ @@ -849,7 +851,7 @@ async fn configuration_retry_reuses_creation_id_and_rejects_changed_draft() { let request = || { let mut value = input(); value.creation_id = Some(creation_id.into()); - // 此用例验证创建重试,使用不需要私有状态迁移的固定版本。 + // 此用例验证创建重试,使用不需要私有状态迁移的固定版本 value.artifact_sha256 = OLD_ARTIFACT.into(); value.enabled = false; value diff --git a/backend/crates/gateway-admin/tests/use_case/plugins/mod.rs b/backend/crates/gateway-admin/tests/use_case/plugins/mod.rs index 6c511fd63..072cee754 100644 --- a/backend/crates/gateway-admin/tests/use_case/plugins/mod.rs +++ b/backend/crates/gateway-admin/tests/use_case/plugins/mod.rs @@ -1,3 +1,5 @@ +//! 插件管理用例的测试入口与共用端口替身 + use async_trait::async_trait; use gateway_admin::model::plugins::distribution::{ DownloadedPlugin, GithubReleaseQuery, PluginRelease, RemotePluginLocation, SourceCredential, diff --git a/backend/crates/gateway-admin/tests/use_case/plugins/official.rs b/backend/crates/gateway-admin/tests/use_case/plugins/official.rs index 7628ea28b..51e2e66b9 100644 --- a/backend/crates/gateway-admin/tests/use_case/plugins/official.rs +++ b/backend/crates/gateway-admin/tests/use_case/plugins/official.rs @@ -1,3 +1,5 @@ +//! 官方插件发行清单校验与平台制品导入的用例测试 + use std::{collections::BTreeMap, sync::Arc}; use async_trait::async_trait; diff --git a/backend/crates/gateway-admin/tests/use_case/proxies.rs b/backend/crates/gateway-admin/tests/use_case/proxies.rs index 399ebb8c4..f09b7a579 100644 --- a/backend/crates/gateway-admin/tests/use_case/proxies.rs +++ b/backend/crates/gateway-admin/tests/use_case/proxies.rs @@ -1,3 +1,5 @@ +//! 账号出站代理管理、占用释放与探测结果的用例测试 + use async_trait::async_trait; use gateway_admin::{ model::{MutationContext, Revision, proxies::*}, diff --git a/backend/crates/gateway-admin/tests/use_case/service_contract.rs b/backend/crates/gateway-admin/tests/use_case/service_contract.rs index 89c94119e..76c5cf3a2 100644 --- a/backend/crates/gateway-admin/tests/use_case/service_contract.rs +++ b/backend/crates/gateway-admin/tests/use_case/service_contract.rs @@ -1,4 +1,4 @@ -//! 从宿主类型和操作声明生成独立 SDK 合同;普通测试阻止两侧声明漂移。 +//! 从宿主类型和操作声明生成独立 SDK 合同;普通测试阻止两侧声明漂移 use std::{collections::BTreeMap, path::Path}; @@ -18,7 +18,7 @@ impl Parse for Operation { let method = input.parse()?; input.parse::()?; let arguments = input.parse()?; - // 后续模式和参数表达式只属于宿主调用,不进入线协议。 + // 后续模式和参数表达式只属于宿主调用,不进入线协议 input.parse::()?; let _ = syn::Pat::parse_single(input)?; while !input.is_empty() { @@ -39,7 +39,7 @@ fn source(root: &Path, path: &str) -> syn::File { syn::parse_file(&std::fs::read_to_string(root.join(path)).unwrap()).unwrap() } -// SDK 保留 wire 值,不依赖宿主的领域构造器、时区库或价格校验实现。 +// SDK 保留 wire 值,不依赖宿主的领域构造器、时区库或价格校验实现 struct WireTypes; impl VisitMut for WireTypes { fn visit_type_mut(&mut self, ty: &mut Type) { @@ -260,6 +260,10 @@ fn sdk_settings_contract_matches_host_declarations() { _ => quote!(#field: settings.#field), }); let generated = quote! { + //! 宿主设置服务的操作标识与请求、响应数据合同 + //! + //! 从宿主设置类型与 service/settings.rs 生成;更新命令见 SDK 维护说明 + use super::Operation; use serde::{Deserialize, Serialize}; use std::{collections::{BTreeMap, BTreeSet}, num::NonZeroU64}; @@ -280,7 +284,7 @@ fn sdk_settings_contract_matches_host_declarations() { let output = root.join("../gateway-plugin/sdk/src/call/services/settings.rs"); let expected = prettyplease::unparse(&syn::parse2::(generated).unwrap()); if std::env::var_os("CPR_UPDATE_SERVICE_CONTRACT").is_some() { - std::fs::write(&output, format!("// 从宿主设置类型与 service/settings.rs 生成;更新命令见 SDK 维护说明。\n{expected}")).unwrap(); + std::fs::write(&output, &expected).unwrap(); } let actual = syn::parse_file(&std::fs::read_to_string(output).unwrap()).unwrap(); assert!( diff --git a/backend/crates/gateway-admin/tests/use_case/settings.rs b/backend/crates/gateway-admin/tests/use_case/settings.rs index f0149681f..bb45bb25d 100644 --- a/backend/crates/gateway-admin/tests/use_case/settings.rs +++ b/backend/crates/gateway-admin/tests/use_case/settings.rs @@ -1,3 +1,5 @@ +//! 验证设置服务调用、Provider 参数校验与价格管理行为 + use std::{ collections::{BTreeMap, BTreeSet}, sync::{Arc, Mutex}, @@ -58,7 +60,7 @@ async fn explicit_service_calls_freeze_empty_plan_and_cancel_before_entering_ter ) .await .unwrap(); - // 内部 pricing 不再单独进入插件链;显式调用只解析一次发布集合。 + // 内部 pricing 不再单独进入插件链;显式调用只解析一次发布集合 assert_eq!(resolutions.load(Ordering::SeqCst), expected); } store.updated.lock().unwrap().take(); diff --git a/backend/crates/gateway-admin/tests/use_case/system.rs b/backend/crates/gateway-admin/tests/use_case/system.rs index a181fa919..08a46fc25 100644 --- a/backend/crates/gateway-admin/tests/use_case/system.rs +++ b/backend/crates/gateway-admin/tests/use_case/system.rs @@ -1,3 +1,5 @@ +//! 验证系统升级、回滚与重启命令向宿主端口传递确认参数 + use std::sync::Mutex; use async_trait::async_trait; diff --git a/backend/crates/gateway-api/src/admin/account_groups.rs b/backend/crates/gateway-api/src/admin/account_groups.rs index 2522fb6a5..b6be53081 100644 --- a/backend/crates/gateway-api/src/admin/account_groups.rs +++ b/backend/crates/gateway-api/src/admin/account_groups.rs @@ -1,4 +1,4 @@ -//! Account group HTTP wire and fixed routes. +//! 账号分组管理的 HTTP 数据合同与固定路由 use crate::auth::SessionState; @@ -235,7 +235,7 @@ impl From<(AccountGroupMutation, crate::time::TimePresenter)> for AccountGroupMu } } -/// Construct all fixed account-group management routes. +/// 构造账号分组管理的固定路由 pub fn router() -> Router where S: SessionState + Clone + Send + Sync + 'static, diff --git a/backend/crates/gateway-api/src/admin/accounts/credentials.rs b/backend/crates/gateway-api/src/admin/accounts/credentials.rs index 0c5480a68..ec38e9223 100644 --- a/backend/crates/gateway-api/src/admin/accounts/credentials.rs +++ b/backend/crates/gateway-api/src/admin/accounts/credentials.rs @@ -1,4 +1,4 @@ -//! Provider-owned 凭据请求、命令转换与敏感材料校验。 +//! Provider-owned 凭据请求、命令转换与敏感材料校验 use super::*; @@ -18,7 +18,7 @@ fn parse_provider(value: &str) -> Result { ProviderKind::new(value.trim().to_owned()).map_err(|_| WireValidationError::new("provider")) } -/// 导入统一设置,复用编辑账号的备注、调度和分组约束。 +/// 导入统一设置,复用编辑账号的备注、调度和分组约束 #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct AccountImportSettingsRequest { @@ -54,7 +54,7 @@ impl AccountImportSettingsRequest { } } -/// Provider-owned 账号导入请求;公共 API 不解释 `data` 内部字段。 +/// Provider-owned 账号导入请求;公共 API 不解释 `data` 内部字段 #[derive(Deserialize, Serialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct AccountImportRequest { @@ -259,7 +259,7 @@ impl UpdateAccountRequest { } } -/// 编辑 OpenAI 账号的连接设置;OAuth 仅接受传输方式。 +/// 编辑 OpenAI 账号的连接设置;OAuth 仅接受传输方式 #[derive(Clone, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct AccountConnectionUpdateRequest { diff --git a/backend/crates/gateway-api/src/admin/accounts/handlers.rs b/backend/crates/gateway-api/src/admin/accounts/handlers.rs index 37ef01af2..c48819369 100644 --- a/backend/crates/gateway-api/src/admin/accounts/handlers.rs +++ b/backend/crates/gateway-api/src/admin/accounts/handlers.rs @@ -1,10 +1,10 @@ -//! 账号管理路由与 HTTP handler 编排。 +//! 账号管理路由与 HTTP handler 编排 use crate::auth::SessionState; use super::*; -/// 构造统一账号管理路由。 +/// 构造统一账号管理路由 pub fn router() -> Router where S: SessionState + Clone + Send + Sync + 'static, @@ -434,7 +434,7 @@ where Ok(profile_avatar_response(avatar)) } -/// 将 Provider 头像流投影为受保护的同源 HTTP 响应。 +/// 将 Provider 头像流投影为受保护的同源 HTTP 响应 #[must_use] pub fn profile_avatar_response(avatar: ProviderProfileAvatar) -> Response { let ProviderProfileAvatar { diff --git a/backend/crates/gateway-api/src/admin/accounts/import_tasks.rs b/backend/crates/gateway-api/src/admin/accounts/import_tasks.rs index a08609191..fd582a92c 100644 --- a/backend/crates/gateway-api/src/admin/accounts/import_tasks.rs +++ b/backend/crates/gateway-api/src/admin/accounts/import_tasks.rs @@ -1,4 +1,4 @@ -//! 后台导入任务的有界请求与安全结果投影。 +//! 后台导入任务的有界请求与安全结果投影 use axum::extract::DefaultBodyLimit; use gateway_admin::model::import_tasks::{ diff --git a/backend/crates/gateway-api/src/admin/accounts/mod.rs b/backend/crates/gateway-api/src/admin/accounts/mod.rs index 9f933c7c6..dd78458fb 100644 --- a/backend/crates/gateway-api/src/admin/accounts/mod.rs +++ b/backend/crates/gateway-api/src/admin/accounts/mod.rs @@ -1,4 +1,4 @@ -//! 管理端账号目录的 HTTP 合同、凭据动作、路由处理与安全响应投影。 +//! 管理端账号目录的 HTTP 合同、凭据动作、路由处理与安全响应投影 use std::{collections::BTreeSet, convert::Infallible, fmt}; diff --git a/backend/crates/gateway-api/src/admin/accounts/presenter.rs b/backend/crates/gateway-api/src/admin/accounts/presenter.rs index c734865f5..6e3635aee 100644 --- a/backend/crates/gateway-api/src/admin/accounts/presenter.rs +++ b/backend/crates/gateway-api/src/admin/accounts/presenter.rs @@ -1,4 +1,4 @@ -//! Admin 领域结果到安全 HTTP wire 的展示投影。 +//! Admin 领域结果到安全 HTTP wire 的展示投影 use super::*; use gateway_admin::model::quota_forecast::{AccountQuotaForecast, AccountQuotaForecastReport}; diff --git a/backend/crates/gateway-api/src/admin/accounts/wire.rs b/backend/crates/gateway-api/src/admin/accounts/wire.rs index 89403ee95..0faa2058f 100644 --- a/backend/crates/gateway-api/src/admin/accounts/wire.rs +++ b/backend/crates/gateway-api/src/admin/accounts/wire.rs @@ -1,4 +1,4 @@ -//! 账号管理请求、响应与查询 wire contract。 +//! 账号管理请求、响应与查询 wire contract use super::*; @@ -44,7 +44,7 @@ pub(super) fn proxy_selection( } } -/// 账号列表查询参数。 +/// 账号列表查询参数 #[derive(Debug, Clone, Default, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct ListQuery { @@ -152,7 +152,7 @@ impl BatchUpdateAccountsRequest { } impl ListQuery { - /// 解析并校验全部 wire 字段,生成 Admin 查询命令。 + /// 解析并校验全部 wire 字段,生成 Admin 查询命令 pub fn validate(self) -> Result { let page = self.page.unwrap_or(1); if page == 0 { @@ -241,7 +241,7 @@ fn parse_sort_direction(value: &str) -> Option { } } -/// 账号列表响应数据。 +/// 账号列表响应数据 #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct AccountPageData { @@ -250,7 +250,7 @@ pub struct AccountPageData { pub summary: AccountSummaryView, } -/// 账号概览计数。 +/// 账号概览计数 #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct AccountSummaryView { @@ -262,7 +262,7 @@ pub struct AccountSummaryView { pub error: u64, } -/// 一条安全账号视图。 +/// 一条安全账号视图 #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct AccountView { @@ -279,14 +279,14 @@ pub struct AccountView { pub user_id: Option, pub label: Option, pub plan_type: Option, - /// 后端生成的套餐展示名称;缺失套餐时为“未知套餐”。 + /// 后端生成的套餐展示名称;缺失套餐时为“未知套餐” pub plan_type_display: String, pub authentication_kind: String, pub has_refresh_token: bool, pub status: String, - /// `status == "error"` 时的具体原因;其余状态为 `null`。 + /// `status == "error"` 时的具体原因;其余状态为 `null` pub error_reason: Option, - /// 最近一次失败的上游错误描述;仅错误状态存在。 + /// 最近一次失败的上游错误描述;仅错误状态存在 pub error_message: Option, pub enabled: bool, pub concurrency_limit: Option, @@ -339,7 +339,7 @@ impl From for Accou } } -/// 容量估算仅供管理端展示;金额不是订阅账单或可消费余额。 +/// 容量估算仅供管理端展示;金额不是订阅账单或可消费余额 #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct AccountQuotaForecastData { @@ -387,13 +387,13 @@ pub struct AccountGroupRefView { pub enabled: bool, } -/// Provider quota 安全视图。 +/// Provider quota 安全视图 #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct AccountQuotaView { pub refreshed_at_display: String, pub limit_reached: bool, - /// 冷却到期或可开始恢复探测的时间;非限流中为 `null`。 + /// 冷却到期或可开始恢复探测的时间;非限流中为 `null` pub rate_limited_until: Option, pub rate_limit_recovery_display: Option, pub rate_limit_reason: Option, @@ -402,7 +402,7 @@ pub struct AccountQuotaView { pub credits: Option, } -/// 上游点数安全视图,不透出额度响应中的其他 Provider 字段。 +/// 上游点数安全视图,不透出额度响应中的其他 Provider 字段 #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct AccountQuotaCreditsView { @@ -411,7 +411,7 @@ pub struct AccountQuotaCreditsView { pub balance: Option, } -/// 一个 quota 时间窗口。 +/// 一个 quota 时间窗口 #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct AccountQuotaWindowView { @@ -431,7 +431,7 @@ pub struct AccountQuotaWindowView { pub reset_at_display: String, } -/// 账号观测用量。 +/// 账号观测用量 #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct AccountUsageView { @@ -471,7 +471,7 @@ pub struct AccountUsageView { pub models: Vec, } -/// 凭据在单个上游模型上的观测用量。 +/// 凭据在单个上游模型上的观测用量 #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct ModelUsageView { @@ -508,7 +508,7 @@ pub struct ModelUsageView { pub last_used_at_full_display: Option, } -/// 单一货币的可查询成本。 +/// 单一货币的可查询成本 #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct CurrencyCostView { @@ -517,7 +517,7 @@ pub struct CurrencyCostView { pub estimated_amount_display: String, } -/// 账号详情类 GET 的固定 ID query。 +/// 账号详情类 GET 的固定 ID query #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct AccountIdQuery { @@ -535,7 +535,7 @@ impl AccountIdQuery { } } -/// 头像 GET 的固定 query;`version` 只参与浏览器缓存键,不进入上游请求。 +/// 头像 GET 的固定 query;`version` 只参与浏览器缓存键,不进入上游请求 #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct AccountProfileAvatarQuery { @@ -562,7 +562,7 @@ impl AccountProfileAvatarQuery { } } -/// 敏感导出的固定 query;IDs 使用逗号分隔,禁止隐式导出全部账号。 +/// 敏感导出的固定 query;IDs 使用逗号分隔,禁止隐式导出全部账号 #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct AccountExportQuery { @@ -602,7 +602,7 @@ impl AccountExportQuery { } } -/// 账号运行期动作。 +/// 账号运行期动作 #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct AccountActionRequest { @@ -620,7 +620,8 @@ impl AccountActionRequest { } } -/// 主动额度重置卡消费请求。幂等键由 UI 生成并在不确定重试时复用,与官方一致。 +/// 主动额度重置卡消费请求 +/// 幂等键由 UI 生成并在不确定重试时复用,与官方一致 #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct AccountResetCreditConsumeRequest { @@ -662,7 +663,7 @@ impl AccountResetCreditConsumeRequest { } } -/// 手工 OAuth 刷新会变更持久 credential。 +/// 手工 OAuth 刷新会变更持久 credential #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct AccountRefreshRequest { @@ -680,7 +681,7 @@ impl AccountRefreshRequest { } } -/// 连接测试 query;测试仍经唯一 Core/Provider 模型请求路径执行。 +/// 连接测试 query;测试仍经唯一 Core/Provider 模型请求路径执行 #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct AccountTestQuery { @@ -720,7 +721,7 @@ pub struct AccountModelsData { pub models: Vec, } -/// 账号模型目录文件;`catalog` 直接作为 Codex `model_catalog_json` 的文件正文落盘。 +/// 账号模型目录文件;`catalog` 直接作为 Codex `model_catalog_json` 的文件正文落盘 #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct AccountModelCatalogData { @@ -729,14 +730,14 @@ pub struct AccountModelCatalogData { pub catalog: Value, } -/// Provider 未给出 Codex 原生目录正文;该结果不能作为客户端目录文件返回。 +/// Provider 未给出 Codex 原生目录正文;该结果不能作为客户端目录文件返回 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct UnsupportedModelCatalogDocument; impl TryFrom for AccountModelCatalogData { type Error = UnsupportedModelCatalogDocument; - /// 目录正文由 Provider 按官方 wire 组装;这里只解析一次用于 JSON 响应,不改写字段。 + /// 目录正文由 Provider 按官方 wire 组装;这里只解析一次用于 JSON 响应,不改写字段 fn try_from(result: ProviderModelCatalogDocument) -> Result { if result.document.protocol() != "codex" { return Err(UnsupportedModelCatalogDocument); @@ -770,7 +771,7 @@ pub struct AccountDetailData { pub credential_configuration: Option, } -/// 个人资料、累计统计与订阅的统一响应。 +/// 个人资料、累计统计与订阅的统一响应 #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct AccountPersonalInfoData { @@ -798,7 +799,7 @@ impl From<(AccountPersonalInfo, crate::time::TimePresenter)> for AccountPersonal } } -/// 按需读取的订阅安全字段,不暴露原始上游响应。 +/// 按需读取的订阅安全字段,不暴露原始上游响应 #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct AccountSubscriptionData { @@ -832,7 +833,7 @@ impl From<(ProviderSubscription, crate::time::TimePresenter)> for AccountSubscri } } -/// Provider 官方个人资料统计响应。 +/// Provider 官方个人资料统计响应 #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct AccountProfileStatisticsData { @@ -958,7 +959,7 @@ fn profile_invocation_view(invocation: ProviderProfileInvocation) -> AccountProf } } -/// 主动额度重置卡列表响应。 +/// 主动额度重置卡列表响应 #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct AccountResetCreditsData { @@ -966,7 +967,7 @@ pub struct AccountResetCreditsData { pub credits: Vec, } -/// 一张安全主动额度重置卡视图。 +/// 一张安全主动额度重置卡视图 #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct AccountResetCreditView { @@ -978,7 +979,7 @@ pub struct AccountResetCreditView { pub reset_type: Option, } -/// 主动额度重置卡消费响应。 +/// 主动额度重置卡消费响应 #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct AccountResetCreditResultData { @@ -1025,7 +1026,7 @@ impl From<(ProviderResetCreditResult, crate::time::TimePresenter)> } } -/// Provider-owned 明文导出文档;Debug 永远不输出内部 JSON。 +/// Provider-owned 明文导出文档;Debug 永远不输出内部 JSON #[derive(Serialize)] #[serde(transparent)] pub struct AccountExportData(Value); diff --git a/backend/crates/gateway-api/src/admin/auth.rs b/backend/crates/gateway-api/src/admin/auth.rs index 8439d2fde..fa853411b 100644 --- a/backend/crates/gateway-api/src/admin/auth.rs +++ b/backend/crates/gateway-api/src/admin/auth.rs @@ -1,4 +1,4 @@ -//! 管理端权限校验与请求审计上下文。 +//! 管理端权限校验与请求审计上下文 use axum::{ extract::FromRequestParts, @@ -13,7 +13,7 @@ use super::{AdminError, wire::map_admin_service_error}; const REQUEST_ID_HEADER: &str = "x-request-id"; -/// 已通过管理员会话或部署级管理 API Key 鉴权的请求。 +/// 已通过管理员会话或部署级管理 API Key 鉴权的请求 pub struct AdminAuth { context: AdminRequestContext, } @@ -32,7 +32,7 @@ where type Rejection = AdminError; async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { - // 仅内部 Dispatcher 写入此上下文;普通客户端 headers 无法声明插件身份。 + // 仅内部 Dispatcher 写入此上下文;普通客户端 headers 无法声明插件身份 if let Some(context) = parts.extensions.get::() { return Ok(Self { context: context.clone(), @@ -50,8 +50,8 @@ where } /// request-id 层按配置的 header 名注入,同时写入与名字无关的扩展; -/// 优先读扩展,使自定义 header 名不会让管理请求失去请求上下文。 -/// header 回退覆盖未装配该层的调用方。 +/// 优先读扩展,使自定义 header 名不会让管理请求失去请求上下文 +/// header 回退覆盖未装配该层的调用方 fn admin_request_id(parts: &Parts) -> Option { parts .extensions diff --git a/backend/crates/gateway-api/src/admin/backups.rs b/backend/crates/gateway-api/src/admin/backups.rs index fd45a1d1f..2168ef4f3 100644 --- a/backend/crates/gateway-api/src/admin/backups.rs +++ b/backend/crates/gateway-api/src/admin/backups.rs @@ -1,7 +1,7 @@ -//! 备份设置、计划、手动创建、记录、下载与删除路由。 +//! 备份设置、计划、手动创建、记录、下载与删除路由 //! -//! 路径统一位于 `/api/admin/settings/backups/*`,内部由独立 `BackupService` 承担业务。 +//! 路径统一位于 `/api/admin/settings/backups/*`,内部由独立 `BackupService` 承担业务 use crate::auth::SessionState; @@ -25,7 +25,7 @@ use super::{ PageMeta, wire::map_admin_service_error, }; -/// 备份设置视图;`secretAccessKey` 返回已保存的明文凭据,由前端掩码显示。 +/// 备份设置视图;`secretAccessKey` 返回已保存的明文凭据,由前端掩码显示 #[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[serde(rename_all = "camelCase")] pub struct BackupSettingsView { @@ -85,7 +85,7 @@ impl From<(BackupSettings, crate::time::TimePresenter)> for BackupSettingsView { } } -/// 更新 S3 存储配置请求;`secretAccessKey` 缺省表示保留旧值。 +/// 更新 S3 存储配置请求;`secretAccessKey` 缺省表示保留旧值 #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct UpdateBackupStorageRequest { @@ -99,7 +99,7 @@ pub struct UpdateBackupStorageRequest { pub force_path_style: bool, } -/// 更新调度配置请求。 +/// 更新调度配置请求 #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct UpdateBackupScheduleRequest { @@ -109,7 +109,7 @@ pub struct UpdateBackupScheduleRequest { pub retention_count: u32, } -/// 备份记录 wire 视图。 +/// 备份记录 wire 视图 #[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[serde(rename_all = "camelCase")] pub struct BackupRecordView { @@ -170,7 +170,7 @@ impl From<(BackupRecord, crate::time::TimePresenter)> for BackupRecordView { } } -/// 记录列表查询参数。 +/// 记录列表查询参数 #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct BackupRecordsQuery { @@ -180,14 +180,14 @@ pub struct BackupRecordsQuery { pub trigger: Option, } -/// 下载地址请求。 +/// 下载地址请求 #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct BackupIdRequest { pub backup_id: String, } -/// 下载地址响应。 +/// 下载地址响应 #[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[serde(rename_all = "camelCase")] pub struct DownloadUrlView { @@ -206,7 +206,7 @@ impl From for DownloadUrlView { } } -/// 连接测试响应。 +/// 连接测试响应 #[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ConnectionTestView { @@ -227,7 +227,7 @@ impl From for ConnectionTestView { } } -/// 构造固定 GET/POST 备份路由。 +/// 构造固定 GET/POST 备份路由 pub fn router() -> Router where S: SessionState + Clone + Send + Sync + 'static, @@ -441,7 +441,7 @@ where )) } -/// 创建备份请求;`expiresInDays` 为 0 或缺省表示不设置过期时间。 +/// 创建备份请求;`expiresInDays` 为 0 或缺省表示不设置过期时间 #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct CreateBackupRequest { diff --git a/backend/crates/gateway-api/src/admin/client_keys.rs b/backend/crates/gateway-api/src/admin/client_keys.rs index cc3193643..dd8f6ba62 100644 --- a/backend/crates/gateway-api/src/admin/client_keys.rs +++ b/backend/crates/gateway-api/src/admin/client_keys.rs @@ -1,4 +1,4 @@ -//! Client API Key 管理 wire contract。 +//! Client API Key 管理 wire contract use crate::auth::SessionState; @@ -51,7 +51,7 @@ fn parse_budget( .transpose() } -/// Client Key 列表查询。 +/// Client Key 列表查询 #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct ListClientKeysQuery { @@ -63,7 +63,7 @@ pub struct ListClientKeysQuery { } impl ListClientKeysQuery { - /// 校验 wire 边界并直接构造管理用例查询。 + /// 校验 wire 边界并直接构造管理用例查询 pub fn into_command(self) -> Result { if self .cursor @@ -103,7 +103,7 @@ impl ListClientKeysQuery { } } -/// Client Key 数据库排序字段。 +/// Client Key 数据库排序字段 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum ClientKeySortField { @@ -113,7 +113,7 @@ pub enum ClientKeySortField { LastUsedAt, } -/// Client Key 数据库排序方向。 +/// Client Key 数据库排序方向 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum ClientKeySortDirection { @@ -121,7 +121,7 @@ pub enum ClientKeySortDirection { Desc, } -/// 已校验且会写入自描述游标的排序组合。 +/// 已校验且会写入自描述游标的排序组合 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct ClientKeySort { @@ -147,7 +147,7 @@ impl ClientKeySort { } } -/// 创建 Client Key 请求。 +/// 创建 Client Key 请求 #[derive(Clone, PartialEq, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct CreateClientKeyRequest { @@ -176,7 +176,7 @@ impl fmt::Debug for CreateClientKeyRequest { } impl CreateClientKeyRequest { - /// 校验 wire 边界并直接构造管理用例命令。 + /// 校验 wire 边界并直接构造管理用例命令 pub fn into_command(self) -> Result { validate_required_text(&self.name, "name")?; validate_optional_text(self.label.as_deref(), "label")?; @@ -212,7 +212,7 @@ impl CreateClientKeyRequest { } } -/// 更新 Client Key 请求。 +/// 更新 Client Key 请求 #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct UpdateClientKeyRequest { @@ -233,7 +233,7 @@ pub struct UpdateClientKeyRequest { } impl UpdateClientKeyRequest { - /// 校验 wire 边界并直接构造管理用例命令。 + /// 校验 wire 边界并直接构造管理用例命令 pub fn into_command(self) -> Result { validate_required_text(&self.id, "id")?; validate_required_text(&self.name, "name")?; @@ -262,7 +262,7 @@ impl UpdateClientKeyRequest { } } -/// 重置指定周期已用金额的请求。 +/// 重置指定周期已用金额的请求 #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct ResetClientKeyBudgetRequest { @@ -292,14 +292,14 @@ impl ResetClientKeyBudgetRequest { } } -/// 只携带 ID 的 Client Key mutation 请求。 +/// 只携带 ID 的 Client Key mutation 请求 #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct ClientKeyMutationRequest { id: String, } -/// 读取一次完整 Key 的 ID query。 +/// 读取一次完整 Key 的 ID query #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[serde(deny_unknown_fields)] pub struct ClientKeyIdQuery { @@ -318,7 +318,7 @@ impl ClientKeyIdQuery { } impl ClientKeyMutationRequest { - /// 校验请求并取出 ID。 + /// 校验请求并取出 ID pub fn into_id(self) -> Result { validate_required_text(&self.id, "id")?; Ok(self.id) @@ -329,14 +329,14 @@ impl ClientKeyMutationRequest { } } -/// 不含完整 Key 的管理端安全视图。 +/// 不含完整 Key 的管理端安全视图 #[derive(Debug, Clone, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ClientKeyView { provider_request_profile_overrides: ProviderRequestProfileOverrides, - /// 固定兼容字段;值始终从 provider_request_profile_overrides 派生。 + /// 固定兼容字段;值始终从 provider_request_profile_overrides 派生 openai_client_profile_override: Option>, - /// 固定兼容字段;值始终从 provider_request_profile_overrides 派生。 + /// 固定兼容字段;值始终从 provider_request_profile_overrides 派生 xai_client_profile_override: Option>, id: String, name: String, @@ -443,7 +443,7 @@ impl From<(ClientKeyRecord, crate::time::TimePresenter)> for ClientKeyView { } } -/// Client Key 列表响应数据。 +/// Client Key 列表响应数据 #[derive(Debug, Clone, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ClientKeyListData { @@ -453,7 +453,7 @@ pub struct ClientKeyListData { } impl ClientKeyListData { - /// 构造 Client Key 列表响应。 + /// 构造 Client Key 列表响应 #[must_use] pub fn new(items: Vec, next_cursor: Option, total: u64) -> Self { Self { @@ -488,7 +488,7 @@ impl TryFrom<(ClientKeyPage, crate::time::TimePresenter)> for ClientKeyListData } } -/// Client Key 创建响应;完整值只允许出现在本次序列化结果中。 +/// Client Key 创建响应;完整值只允许出现在本次序列化结果中 #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct CreatedClientKeyData { @@ -498,7 +498,7 @@ pub struct CreatedClientKeyData { } impl CreatedClientKeyData { - /// 构造一次性创建响应。 + /// 构造一次性创建响应 #[must_use] pub fn new(id: String, prefix: String, plaintext_key: String) -> Self { Self { @@ -530,7 +530,7 @@ impl fmt::Debug for CreatedClientKeyData { } } -/// 仅由显式 reveal 返回一次的完整明文 Key。 +/// 仅由显式 reveal 返回一次的完整明文 Key #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct RevealedClientKeyData { @@ -564,7 +564,7 @@ impl fmt::Debug for RevealedClientKeyData { } } -/// Client Key mutation 响应数据。 +/// Client Key mutation 响应数据 #[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[serde(rename_all = "camelCase")] pub struct MutatedClientKeyData { @@ -572,7 +572,7 @@ pub struct MutatedClientKeyData { } impl MutatedClientKeyData { - /// 构造 mutation 响应。 + /// 构造 mutation 响应 #[must_use] pub fn new(id: String) -> Self { Self { id } @@ -585,7 +585,7 @@ impl From for MutatedClientKeyData { } } -/// 解码后的 Client Key 游标字段。 +/// 解码后的 Client Key 游标字段 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ClientKeyCursorData { pub sort: ClientKeySort, @@ -593,7 +593,7 @@ pub struct ClientKeyCursorData { pub id: String, } -/// 游标中与排序字段严格对应的最后一行值。 +/// 游标中与排序字段严格对应的最后一行值 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "kind", content = "value", rename_all = "camelCase")] pub enum ClientKeyCursorValue { @@ -611,7 +611,7 @@ struct CursorWire { id: String, } -/// 把 owner 游标编码为不透明 wire 值。 +/// 把 owner 游标编码为不透明 wire 值 pub fn encode_client_key_cursor( cursor: &ClientKeyCursorData, ) -> Result { @@ -625,7 +625,7 @@ pub fn encode_client_key_cursor( Ok(URL_SAFE_NO_PAD.encode(bytes)) } -/// 解码并严格校验 Client Key 游标。 +/// 解码并严格校验 Client Key 游标 pub fn decode_client_key_cursor(encoded: &str) -> Result { if encoded.is_empty() || encoded.len() > MAX_CURSOR_BYTES { return Err(WireValidationError::new("cursor")); @@ -872,7 +872,7 @@ fn validate_optional_text( Ok(()) } -/// 构造固定 GET/POST 且 ID 仅位于 query/body 的 Client API Key 路由。 +/// 构造固定 GET/POST 且 ID 仅位于 query/body 的 Client API Key 路由 pub fn router() -> Router where S: SessionState + Clone + Send + Sync + 'static, @@ -1098,7 +1098,7 @@ fn map_service_error(error: gateway_admin::model::AdminError) -> AdminError { map_admin_service_error(error) } -// 省略表示不修改,null 表示恢复跟随通用设置。 +// 省略表示不修改,null 表示恢复跟随通用设置 fn deserialize_profile_override<'de, D: serde::Deserializer<'de>>( deserializer: D, ) -> Result>>, D::Error> { diff --git a/backend/crates/gateway-api/src/admin/extract.rs b/backend/crates/gateway-api/src/admin/extract.rs index fc54fab41..fa836c527 100644 --- a/backend/crates/gateway-api/src/admin/extract.rs +++ b/backend/crates/gateway-api/src/admin/extract.rs @@ -1,4 +1,4 @@ -//! 管理端 JSON 与 Query extractor 的稳定 rejection 映射。 +//! 管理端 JSON 与 Query extractor 的稳定 rejection 映射 use axum::{ Json, @@ -12,7 +12,7 @@ use serde::de::DeserializeOwned; use super::AdminError; -/// 将 Axum JSON rejection 收口为管理端错误信封。 +/// 将 Axum JSON rejection 收口为管理端错误信封 pub struct AdminJson(pub T); impl FromRequest for AdminJson @@ -45,7 +45,7 @@ where } } -/// 将 Axum Query rejection 收口为管理端错误信封。 +/// 将 Axum Query rejection 收口为管理端错误信封 pub struct AdminQuery(pub T); impl FromRequestParts for AdminQuery diff --git a/backend/crates/gateway-api/src/admin/mod.rs b/backend/crates/gateway-api/src/admin/mod.rs index 230f2f7b6..d7f11e980 100644 --- a/backend/crates/gateway-api/src/admin/mod.rs +++ b/backend/crates/gateway-api/src/admin/mod.rs @@ -1,4 +1,4 @@ -//! 管理端 HTTP adapter、wire contract 与固定路由。 +//! 管理端 HTTP adapter、wire contract 与固定路由 use crate::auth::SessionState; @@ -35,7 +35,7 @@ pub(crate) fn model_router() -> Router { plugins::model_router().layer(middleware::map_response(no_store)) } -/// 构造管理用例路由;模型执行桥由完整 API 组合单独装配。 +/// 构造管理用例路由;模型执行桥由完整 API 组合单独装配 pub fn router() -> Router where S: SessionState + Clone + Send + Sync + 'static, diff --git a/backend/crates/gateway-api/src/admin/observability/mod.rs b/backend/crates/gateway-api/src/admin/observability/mod.rs index a73acf784..2cb6cf082 100644 --- a/backend/crates/gateway-api/src/admin/observability/mod.rs +++ b/backend/crates/gateway-api/src/admin/observability/mod.rs @@ -1,4 +1,4 @@ -//! Dashboard、用量与诊断查询的 wire 映射和固定路由。 +//! Dashboard、用量与诊断查询的 wire 映射和固定路由 use std::collections::BTreeMap; diff --git a/backend/crates/gateway-api/src/admin/observability/presenter.rs b/backend/crates/gateway-api/src/admin/observability/presenter.rs index e2052fc5a..cd11d5ffb 100644 --- a/backend/crates/gateway-api/src/admin/observability/presenter.rs +++ b/backend/crates/gateway-api/src/admin/observability/presenter.rs @@ -1,4 +1,4 @@ -//! 观测领域事实到管理页面的展示投影。 +//! 观测领域事实到管理页面的展示投影 use super::*; diff --git a/backend/crates/gateway-api/src/admin/observability/query.rs b/backend/crates/gateway-api/src/admin/observability/query.rs index 9092d0fc8..cd0ce9a9b 100644 --- a/backend/crates/gateway-api/src/admin/observability/query.rs +++ b/backend/crates/gateway-api/src/admin/observability/query.rs @@ -1,13 +1,13 @@ -//! 查询 wire 类型、校验与领域命令映射。 +//! 查询 wire 类型、校验与领域命令映射 use super::*; -/// 观测列表默认页大小。 +/// 观测列表默认页大小 pub const DEFAULT_PAGE_SIZE: u16 = 50; -/// 观测列表允许的最大页大小。 +/// 观测列表允许的最大页大小 pub const MAX_PAGE_SIZE: u16 = 100; -/// Dashboard 查询参数。 +/// Dashboard 查询参数 #[derive(Clone, Default, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct DashboardQuery { @@ -19,13 +19,13 @@ pub struct DashboardQuery { } impl DashboardQuery { - /// 解析 dashboard 趋势类型。 + /// 解析 dashboard 趋势类型 pub fn trend_kind(&self) -> Result { TrendKind::parse(self.kind.as_deref()) } } -/// 逻辑请求列表、汇总和洞察查询参数。 +/// 逻辑请求列表、汇总和洞察查询参数 #[derive(Clone, Default, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct UsageQuery { @@ -52,7 +52,7 @@ pub struct UsageQuery { } impl UsageQuery { - /// 校验 Element Plus 风格的页码分页字段。 + /// 校验 Element Plus 风格的页码分页字段 pub fn validate_pagination(&self) -> Result<(u32, u16), WireValidationError> { let current_page = self.current_page.unwrap_or(1); if current_page == 0 { @@ -66,7 +66,7 @@ impl UsageQuery { } } -/// 详情查询参数。 +/// 详情查询参数 #[derive(Clone, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct DetailQuery { @@ -74,13 +74,13 @@ pub struct DetailQuery { } impl DetailQuery { - /// 校验详情 ID,错误不回显输入值。 + /// 校验详情 ID,错误不回显输入值 pub fn validate(&self) -> Result<(), WireValidationError> { require_text(&self.id, "id") } } -/// 诊断聚合查询参数。 +/// 诊断聚合查询参数 #[derive(Clone, Default, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct DiagnosticsQuery { @@ -96,13 +96,13 @@ pub struct DiagnosticsQuery { } impl DiagnosticsQuery { - /// 解析诊断维度。 + /// 解析诊断维度 pub fn dimension(&self) -> Result { DiagnosticDimension::parse(self.dimension.as_deref()) } } -/// 运维错误查询参数。 +/// 运维错误查询参数 #[derive(Clone, Default, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct OpsQuery { @@ -129,7 +129,7 @@ pub struct OpsQuery { } impl OpsQuery { - /// 校验 Element Plus 风格的页码分页字段。 + /// 校验 Element Plus 风格的页码分页字段 pub fn validate_pagination(&self) -> Result<(u32, u16), WireValidationError> { let current_page = self.current_page.unwrap_or(1); if current_page == 0 { @@ -143,7 +143,7 @@ impl OpsQuery { } } -/// Dashboard 趋势类型。 +/// Dashboard 趋势类型 #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)] #[serde(rename_all = "lowercase")] pub enum TrendKind { @@ -153,7 +153,7 @@ pub enum TrendKind { } impl TrendKind { - /// 从 query 值解析趋势类型。 + /// 从 query 值解析趋势类型 pub fn parse(value: Option<&str>) -> Result { match trimmed(value) { None | Some("usage") => Ok(Self::Usage), @@ -164,7 +164,7 @@ impl TrendKind { } } -/// 诊断聚合维度。 +/// 诊断聚合维度 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DiagnosticDimension { Model, @@ -177,7 +177,7 @@ pub enum DiagnosticDimension { } impl DiagnosticDimension { - /// 从 query 值解析诊断维度。 + /// 从 query 值解析诊断维度 pub fn parse(value: Option<&str>) -> Result { match trimmed(value) { None | Some("model") => Ok(Self::Model), @@ -191,7 +191,7 @@ impl DiagnosticDimension { } } - /// 返回终态响应中的稳定维度名称。 + /// 返回终态响应中的稳定维度名称 #[must_use] pub const fn display_name(self) -> &'static str { match self { @@ -206,7 +206,7 @@ impl DiagnosticDimension { } } -/// 解析 RFC3339 时间;错误不回显原始值。 +/// 解析 RFC3339 时间;错误不回显原始值 pub fn parse_datetime(value: Option<&str>) -> Result>, WireValidationError> { let Some(value) = trimmed(value) else { return Ok(None); @@ -216,7 +216,7 @@ pub fn parse_datetime(value: Option<&str>) -> Result>, Wire .map_err(|_| WireValidationError::new("timeRange")) } -/// 解析 HTTP 状态码。 +/// 解析 HTTP 状态码 pub fn parse_status(value: Option) -> Result, WireValidationError> { value .map(|value| { @@ -228,7 +228,7 @@ pub fn parse_status(value: Option) -> Result, WireValidationErr .transpose() } -/// 解析尝试序号。 +/// 解析尝试序号 pub fn parse_attempt_index(value: Option) -> Result, WireValidationError> { value .map(|value| { diff --git a/backend/crates/gateway-api/src/admin/observability/routes.rs b/backend/crates/gateway-api/src/admin/observability/routes.rs index 46cfc1d7f..ef4e3309b 100644 --- a/backend/crates/gateway-api/src/admin/observability/routes.rs +++ b/backend/crates/gateway-api/src/admin/observability/routes.rs @@ -1,4 +1,4 @@ -//! 固定 `/api/admin` 观测路由与 handler。 +//! 固定 `/api/admin` 观测路由与 handler use crate::auth::SessionState; @@ -41,7 +41,7 @@ where { let time = crate::time::TimePresenter::new(state.admin_services().timezone()); let kind = query.trend_kind().map_err(map_wire_error)?; - // 概览与独立趋势使用同一部署日界。 + // 概览与独立趋势使用同一部署日界 let range = dashboard_today_range( query.start_time.as_deref(), query.end_time.as_deref(), diff --git a/backend/crates/gateway-api/src/admin/observability/wire.rs b/backend/crates/gateway-api/src/admin/observability/wire.rs index 05520777d..15a68e80d 100644 --- a/backend/crates/gateway-api/src/admin/observability/wire.rs +++ b/backend/crates/gateway-api/src/admin/observability/wire.rs @@ -1,8 +1,8 @@ -//! 响应 DTO 与固定 wire 形状。 +//! 响应 DTO 与固定 wire 形状 use super::*; -/// 观测列表响应数据。 +/// 观测列表响应数据 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct PageData { @@ -12,7 +12,7 @@ pub struct PageData { pub total: u64, } -/// Token 详情展示。 +/// Token 详情展示 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct TokenDetailsView { @@ -34,7 +34,7 @@ pub struct TokenDetailsView { pub total_tokens_display: String, } -/// 按货币展示的成本。 +/// 按货币展示的成本 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CostView { @@ -42,7 +42,7 @@ pub struct CostView { pub estimated_amount: String, } -/// 成本覆盖状态计数。 +/// 成本覆盖状态计数 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CostCoverageView { @@ -52,7 +52,7 @@ pub struct CostCoverageView { pub not_billable: u64, } -/// Provider 受控价格规则生成的单次请求费用明细展示。 +/// Provider 受控价格规则生成的单次请求费用明细展示 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct BillingView { @@ -82,7 +82,7 @@ pub struct ImageBillingView { pub cache_read_price_display: String, } -/// 使用记录表格的窄展示。 +/// 使用记录表格的窄展示 #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct UsageListRecordView { @@ -119,7 +119,7 @@ pub struct UsageListRecordView { pub user_agent: Option, } -/// 单条逻辑请求详情展示。 +/// 单条逻辑请求详情展示 #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct UsageRecordView { @@ -169,7 +169,7 @@ pub struct UsageRecordView { pub image_input_tokens: Option, pub image_output_tokens: Option, pub message: String, - /// Provider 安全观测;Core 字段由顶层字段提供,不再复制进 metadata。 + /// Provider 安全观测;Core 字段由顶层字段提供,不再复制进 metadata pub metadata: BTreeMap, pub created_at: DateTime, pub created_at_display: String, @@ -190,14 +190,14 @@ pub struct UsageRecordView { pub logical_outcome: String, } -/// WebSocket 池决策的稳定形状;与 Provider 选择逻辑无关。 +/// WebSocket 池决策的稳定形状;与 Provider 选择逻辑无关 #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct WebSocketPoolMetadataView { pub kind: String, } -/// 逻辑请求在上游和输出阶段测得的时延事实。 +/// 逻辑请求在上游和输出阶段测得的时延事实 #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct UsageLatencyDetailsView { @@ -227,7 +227,7 @@ pub struct UsageLatencyDetailsView { pub openai_processing_ms: Option, } -/// 单次上游尝试展示。 +/// 单次上游尝试展示 #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct UsageAttemptView { @@ -261,7 +261,7 @@ pub struct UsageAttemptView { pub completed_at: Option>, } -/// 逻辑请求详情与其尝试列表。 +/// 逻辑请求详情与其尝试列表 #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct UsageRecordDetailView { @@ -270,11 +270,11 @@ pub struct UsageRecordDetailView { #[serde(flatten)] pub request: UsageRecordView, pub attempts: Vec, - /// 尝试列表是否完整;best-effort 下恒为 false。 + /// 尝试列表是否完整;best-effort 下恒为 false pub attempts_complete: bool, } -/// Dashboard 趋势数据。 +/// Dashboard 趋势数据 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct TrendData { @@ -283,7 +283,7 @@ pub struct TrendData { pub summary: Vec, } -/// Dashboard 单个趋势桶。 +/// Dashboard 单个趋势桶 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct TrendPointView { @@ -319,7 +319,7 @@ pub struct TrendPointView { pub capacity_utilization: Option, } -/// Dashboard 趋势摘要。 +/// Dashboard 趋势摘要 #[derive(Debug, Serialize)] pub struct TrendSummaryView { pub label: String, @@ -327,7 +327,7 @@ pub struct TrendSummaryView { pub ratio: Option, } -/// Dashboard 卡片集合。 +/// Dashboard 卡片集合 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct DashboardCardsView { @@ -337,7 +337,7 @@ pub struct DashboardCardsView { pub cache: DashboardCacheCardView, } -/// Dashboard 上游凭据卡片。 +/// Dashboard 上游凭据卡片 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct DashboardCredentialsCardView { @@ -349,7 +349,7 @@ pub struct DashboardCredentialsCardView { pub unavailable_value: u64, } -/// Dashboard 流量卡片。 +/// Dashboard 流量卡片 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct DashboardTrafficCardView { @@ -359,7 +359,7 @@ pub struct DashboardTrafficCardView { pub total_requests: String, } -/// Dashboard token 卡片。 +/// Dashboard token 卡片 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct DashboardTokensCardView { @@ -370,7 +370,7 @@ pub struct DashboardTokensCardView { pub total_billing_amount_usd: String, } -/// Dashboard 缓存卡片。 +/// Dashboard 缓存卡片 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct DashboardCacheCardView { @@ -382,7 +382,7 @@ pub struct DashboardCacheCardView { pub average_first_token_latency_ms: String, } -/// 旧 Dashboard 账号概览卡片所需的 Provider 安全投影。 +/// 旧 Dashboard 账号概览卡片所需的 Provider 安全投影 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct DashboardAccountUsageView { @@ -402,7 +402,7 @@ pub struct DashboardAccountUsageView { pub last_used: String, } -/// Provider 账号池的持久事实汇总。 +/// Provider 账号池的持久事实汇总 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct DashboardPoolSummaryView { @@ -414,7 +414,7 @@ pub struct DashboardPoolSummaryView { pub error: u64, } -/// 同 target 账号调度容量;Redis 未提供聚合事实时运行中槽位保持空值。 +/// 同 target 账号调度容量;Redis 未提供聚合事实时运行中槽位保持空值 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct DashboardCapacityInfoView { @@ -424,7 +424,7 @@ pub struct DashboardCapacityInfoView { pub available_slots: Option, } -/// 逻辑请求指标展示。 +/// 逻辑请求指标展示 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct RequestMetricsView { @@ -442,7 +442,7 @@ pub struct RequestMetricsView { pub total_tokens: u64, } -/// 上游尝试指标展示。 +/// 上游尝试指标展示 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct AttemptMetricsView { @@ -458,7 +458,7 @@ pub struct AttemptMetricsView { pub costs: Vec, } -/// 健康时间线单点。 +/// 健康时间线单点 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct HealthTimelinePointView { @@ -473,7 +473,7 @@ pub struct HealthTimelinePointView { pub caller_error_requests: u64, } -/// Dashboard 健康时间线。 +/// Dashboard 健康时间线 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct HealthTimelineView { @@ -489,7 +489,7 @@ pub struct HealthTimelineView { pub points: Vec, } -/// Dashboard 展示的实际 Provider 上游请求身份。 +/// Dashboard 展示的实际 Provider 上游请求身份 #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct DashboardWireProfileView { @@ -524,7 +524,7 @@ pub struct DashboardWireTargetView { pub terminal: String, } -/// 发布检查与启动画像分离;未检查时使用明确的 `unchecked` 状态。 +/// 发布检查与启动画像分离;未检查时使用明确的 `unchecked` 状态 #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct DashboardDesktopReleaseView { @@ -540,7 +540,7 @@ pub struct DashboardDesktopReleaseView { pub error: Option, } -/// Dashboard 发布检查的稳定 wire 状态。 +/// Dashboard 发布检查的稳定 wire 状态 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] #[serde(rename_all = "snake_case")] pub enum DashboardDesktopReleaseStatusView { @@ -561,7 +561,7 @@ impl From for DashboardDesktopReleaseStatusView { } } -/// Dashboard 汇总响应数据。 +/// Dashboard 汇总响应数据 #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct DashboardDataView { @@ -578,7 +578,7 @@ pub struct DashboardDataView { pub rotation_strategy: String, } -/// 用量汇总响应数据。 +/// 用量汇总响应数据 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct UsageSummaryView { @@ -593,7 +593,7 @@ pub struct UsageSummaryView { pub attempts: AttemptMetricsView, } -/// 洞察健康趋势点。 +/// 洞察健康趋势点 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct OverviewHealthPointView { @@ -608,7 +608,7 @@ pub struct OverviewHealthPointView { pub error_rate: f64, } -/// 洞察健康摘要。 +/// 洞察健康摘要 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct OverviewHealthView { @@ -625,7 +625,7 @@ pub struct OverviewHealthView { pub points: Vec, } -/// 洞察性能趋势点。 +/// 洞察性能趋势点 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct OverviewPerformancePointView { @@ -648,7 +648,7 @@ pub struct OverviewPerformancePointView { pub capacity_utilization_p95: Option, } -/// 洞察性能摘要。 +/// 洞察性能摘要 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct OverviewPerformanceView { @@ -675,7 +675,7 @@ pub struct OverviewPerformanceView { pub points: Vec, } -/// 洞察成本趋势点。 +/// 洞察成本趋势点 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct OverviewCostPointView { @@ -693,7 +693,7 @@ pub struct OverviewCostPointView { pub cache_hit_request_rate: Option, } -/// 洞察成本摘要。 +/// 洞察成本摘要 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct OverviewCostView { @@ -716,7 +716,7 @@ pub struct OverviewCostView { pub coverage: CostCoverageView, } -/// Provider 维度洞察摘要。 +/// Provider 维度洞察摘要 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ProviderOverviewView { @@ -727,7 +727,7 @@ pub struct ProviderOverviewView { pub total_tokens: u64, } -/// 用量洞察总响应。 +/// 用量洞察总响应 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct UsageInsightsOverviewView { @@ -739,7 +739,7 @@ pub struct UsageInsightsOverviewView { pub providers: Vec, } -/// 诊断聚合项目。 +/// 诊断聚合项目 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct DiagnosticItemView { @@ -764,7 +764,7 @@ pub struct DiagnosticItemView { pub total_tokens: u64, } -/// 诊断聚合响应。 +/// 诊断聚合响应 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct DiagnosticsView { @@ -772,7 +772,7 @@ pub struct DiagnosticsView { pub items: Vec, } -/// 运维错误项目。 +/// 运维错误项目 #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct OpsErrorView { @@ -814,14 +814,14 @@ pub struct OpsErrorView { pub subagent_kind: Option, pub compact: Option, pub message: String, - /// 上游错误正文或 WebSocket close/error frame 原文;不做脱敏。 + /// 上游错误正文或 WebSocket close/error frame 原文;不做脱敏 pub raw_upstream_error: Option, pub metadata: OpsErrorMetadataView, pub created_at: DateTime, pub created_at_display: String, } -/// 运维错误安全元数据;只保留可查询的标识,不回显秘密材料。 +/// 运维错误安全元数据;只保留可查询的标识,不回显秘密材料 #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct OpsErrorMetadataView { diff --git a/backend/crates/gateway-api/src/admin/plugins/artifacts.rs b/backend/crates/gateway-api/src/admin/plugins/artifacts.rs index 95b8f9b72..55ae5093b 100644 --- a/backend/crates/gateway-api/src/admin/plugins/artifacts.rs +++ b/backend/crates/gateway-api/src/admin/plugins/artifacts.rs @@ -1,3 +1,5 @@ +//! 插件制品列表、图标、上传验证与安装管理的 HTTP 接口 + use axum::{ Router, body::{Body, Bytes}, @@ -127,7 +129,7 @@ async fn icon( headers.insert(header::CONTENT_TYPE, content_type); headers.insert( header::CONTENT_SECURITY_POLICY, - // 图标只作为图片加载;SVG 可用内联样式与内嵌图片,但不能执行脚本或请求外部资源。 + // 图标只作为图片加载;SVG 可用内联样式与内嵌图片,但不能执行脚本或请求外部资源 HeaderValue::from_static("sandbox; default-src 'none'; style-src 'unsafe-inline'; img-src data:; base-uri 'none'; form-action 'none'"), ); headers.insert( diff --git a/backend/crates/gateway-api/src/admin/plugins/distribution.rs b/backend/crates/gateway-api/src/admin/plugins/distribution.rs index 5dc8d2d1f..cd24a85a9 100644 --- a/backend/crates/gateway-api/src/admin/plugins/distribution.rs +++ b/backend/crates/gateway-api/src/admin/plugins/distribution.rs @@ -1,3 +1,5 @@ +//! 插件更新来源、下载凭据与远程发行安装的 HTTP 接口 + use axum::{ Router, extract::State, diff --git a/backend/crates/gateway-api/src/admin/plugins/instances.rs b/backend/crates/gateway-api/src/admin/plugins/instances.rs index fdeb87083..997a6171b 100644 --- a/backend/crates/gateway-api/src/admin/plugins/instances.rs +++ b/backend/crates/gateway-api/src/admin/plugins/instances.rs @@ -1,3 +1,5 @@ +//! 插件实例配置、运行状态、版本切换与回滚的 HTTP 接口 + use axum::{ Router, extract::{Query, State}, diff --git a/backend/crates/gateway-api/src/admin/plugins/management.rs b/backend/crates/gateway-api/src/admin/plugins/management.rs index ebe63e742..9d2348f7c 100644 --- a/backend/crates/gateway-api/src/admin/plugins/management.rs +++ b/backend/crates/gateway-api/src/admin/plugins/management.rs @@ -1,3 +1,5 @@ +//! 插件管理页面、资源、授权回调与页面模型请求的 HTTP 边界 + use std::{net::SocketAddr, sync::Arc}; use axum::{ @@ -42,7 +44,7 @@ use crate::{ const MAXIMUM_BODY_BYTES: usize = 1024 * 1024; const MAXIMUM_MODEL_BODY_BYTES: usize = 8 * 1024 * 1024; -// HTML/SVG 即使被直接导航打开也不能获得管理端 origin;执行入口由隔离 iframe 的消息桥承载。 +// HTML/SVG 即使被直接导航打开也不能获得管理端 origin;执行入口由隔离 iframe 的消息桥承载 const RESOURCE_CSP: &str = "sandbox allow-scripts; default-src 'none'; script-src 'unsafe-inline' blob:; style-src 'unsafe-inline' blob:; img-src data: blob:; font-src data: blob:; connect-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'"; pub(super) fn router() -> Router { @@ -105,7 +107,7 @@ async fn model_responses( .map_err(map_admin_service_error)?; let client_key_id = ClientApiKeyId::new(query.client_key_id) .map_err(|_| AdminError::bad_request("Client Key 标识不合法"))?; - // 推理身份由显式 Key 决定;原始 HTTP 头仍完整交给插件,原生上游自行构造认证。 + // 推理身份由显式 Key 决定;原始 HTTP 头仍完整交给插件,原生上游自行构造认证 let headers = parts.headers; let (decoded, middleware_body) = match decode_request_with_body(&body, &headers, MAXIMUM_MODEL_BODY_BYTES) { @@ -209,7 +211,7 @@ async fn callback( Path(path): Path, request: Request, ) -> Result { - // Axum 为 GET 路由自动提供 HEAD;预取或探测不能消耗一次性登录票据。 + // Axum 为 GET 路由自动提供 HEAD;预取或探测不能消耗一次性登录票据 if request.method() != axum::http::Method::GET { return Err(AdminError::invalid_request( StatusCode::METHOD_NOT_ALLOWED, @@ -338,7 +340,7 @@ async fn public_resource( Path(path): Path, headers: HeaderMap, ) -> Result { - // 只分派已授予 public_resource 的不可变静态字节,不启动 handler 或继承 Cookie 权限。 + // 只分派已授予 public_resource 的不可变静态字节,不启动 handler 或继承 Cookie 权限 load_resource(&state, path, true, &headers).await } @@ -354,7 +356,7 @@ async fn load_resource( .resource(&path.target(), &path.path, public) .await .map_err(map_admin_service_error)?; - // 先复核会话、实例版本和资源授权;条件请求不能绕过撤销检查。 + // 先复核会话、实例版本和资源授权;条件请求不能绕过撤销检查 if result.status != 200 || result.body.len() > MAXIMUM_BODY_BYTES { return raw_response(result); } @@ -412,7 +414,7 @@ fn raw_response(result: PluginManagementResponse) -> Result String { let text = value.to_string(); @@ -19,7 +19,7 @@ pub fn format_number(value: u64) -> String { output.chars().rev().collect() } -/// 紧凑格式化:≥1000 时用 K/M/B/T/P 后缀,否则千分位。 +/// 紧凑格式化:≥1000 时用 K/M/B/T/P 后缀,否则千分位 #[must_use] pub fn format_compact_number(value: u64) -> String { if value < 1_000 { @@ -40,7 +40,8 @@ pub fn format_compact_number(value: u64) -> String { format_number(value) } -/// 货币展示格式化。USD 常规金额保留两位,小于 1 美元时最多保留四位。 +/// 货币展示格式化 +/// USD 常规金额保留两位,小于 1 美元时最多保留四位 #[must_use] pub fn format_decimal_currency(amount: &str, currency: &str) -> String { if currency != "USD" { diff --git a/backend/crates/gateway-api/src/admin/proxies.rs b/backend/crates/gateway-api/src/admin/proxies.rs index ffa8f9d8e..81e9cdab9 100644 --- a/backend/crates/gateway-api/src/admin/proxies.rs +++ b/backend/crates/gateway-api/src/admin/proxies.rs @@ -1,3 +1,5 @@ +//! 账号出站代理管理的 HTTP 请求校验、路由与展示投影 + use crate::auth::SessionState; use axum::{ diff --git a/backend/crates/gateway-api/src/admin/settings.rs b/backend/crates/gateway-api/src/admin/settings.rs index bd5a1ee95..1527217bd 100644 --- a/backend/crates/gateway-api/src/admin/settings.rs +++ b/backend/crates/gateway-api/src/admin/settings.rs @@ -1,4 +1,4 @@ -//! Runtime settings、旧设置页聚合投影与明文 Admin API Key wire。 +//! Runtime settings、旧设置页聚合投影与明文 Admin API Key wire use crate::auth::SessionState; @@ -27,22 +27,22 @@ use super::{ WireValidationError, wire::map_admin_service_error, }; -/// 客户端模型到上游模型的全局精确映射。 +/// 客户端模型到上游模型的全局精确映射 pub type ModelMappings = BTreeMap; pub type ProviderRequestProfiles = BTreeMap>; pub type ProviderRequestProfileUpdates = BTreeMap>>; -/// 运行配置投影与设置页字段的聚合响应。 +/// 运行配置投影与设置页字段的聚合响应 #[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[serde(rename_all = "camelCase")] pub struct RuntimeSettingsView { pub config_revision: u64, pub smart_scheduling_defaults: gateway_core::account::SmartSchedulingConfig, pub provider_request_profiles: ProviderRequestProfiles, - /// 固定兼容字段;值始终从 provider_request_profiles 派生。 + /// 固定兼容字段;值始终从 provider_request_profiles 派生 pub openai_client_profile: Option>, - /// 固定兼容字段;值始终从 provider_request_profiles 派生。 + /// 固定兼容字段;值始终从 provider_request_profiles 派生 pub xai_client_profile: Option>, pub request_location_enabled: bool, pub request_location: gateway_core::account::RequestLocation, @@ -77,17 +77,17 @@ pub struct RuntimeSettingsView { pub updated_at_display: String, } -/// 原子替换全局运行参数的请求。 +/// 原子替换全局运行参数的请求 #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct UpdateRuntimeSettingsRequest { pub config_revision: u64, #[serde(default)] pub provider_request_profiles: ProviderRequestProfileUpdates, - /// 兼容既有 wire;与泛化字段冲突时拒绝整个请求。 + /// 兼容既有 wire;与泛化字段冲突时拒绝整个请求 #[serde(default, deserialize_with = "deserialize_profile_update")] pub openai_client_profile: Option>, - /// 兼容既有 wire;与泛化字段冲突时拒绝整个请求。 + /// 兼容既有 wire;与泛化字段冲突时拒绝整个请求 #[serde(default, deserialize_with = "deserialize_profile_update")] pub xai_client_profile: Option>, pub request_location_enabled: bool, @@ -122,7 +122,7 @@ pub struct UpdateRuntimeSettingsRequest { } impl UpdateRuntimeSettingsRequest { - /// 校验公共运行参数。 + /// 校验公共运行参数 pub fn validate(&self) -> Result<(), WireValidationError> { self.request_location .validate() @@ -313,14 +313,14 @@ impl From<(RuntimeSettings, crate::time::TimePresenter)> for RuntimeSettingsView } } -/// 管理 API Key 状态;状态读取不回显完整值。 +/// 管理 API Key 状态;状态读取不回显完整值 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] #[serde(rename_all = "camelCase")] pub struct AdminApiKeyStatus { pub exists: bool, } -/// 管理 API Key 重新生成响应。 +/// 管理 API Key 重新生成响应 #[derive(Serialize)] pub struct RegeneratedAdminApiKey { pub key: String, @@ -335,7 +335,7 @@ impl fmt::Debug for RegeneratedAdminApiKey { } } -/// 管理 API Key 删除响应。 +/// 管理 API Key 删除响应 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] pub struct DeletedAdminApiKey { pub message: &'static str, @@ -414,7 +414,7 @@ impl Default for DeletedAdminApiKey { } } -/// 构造固定 GET/POST 设置路由。 +/// 构造固定 GET/POST 设置路由 pub fn router() -> Router where S: SessionState + Clone + Send + Sync + 'static, @@ -757,7 +757,7 @@ fn validate_optional_client_version( Ok(()) } -/// 探测模型为可选自由文本:非空、去首尾空白后不变、无控制字符且不超过 128 字节。 +/// 探测模型为可选自由文本:非空、去首尾空白后不变、无控制字符且不超过 128 字节 fn validate_optional_probe_model( value: Option<&str>, field: &'static str, @@ -835,7 +835,7 @@ fn normalize_request_profile_updates( Ok(normalized) } -// 字段省略时保留已有配置;显式 null 不能清空唯一的通用默认。 +// 字段省略时保留已有配置;显式 null 不能清空唯一的通用默认 fn deserialize_profile_update<'de, D: serde::Deserializer<'de>>( deserializer: D, ) -> Result>, D::Error> { diff --git a/backend/crates/gateway-api/src/admin/system.rs b/backend/crates/gateway-api/src/admin/system.rs index 1c7c8c4e1..faf4353db 100644 --- a/backend/crates/gateway-api/src/admin/system.rs +++ b/backend/crates/gateway-api/src/admin/system.rs @@ -1,7 +1,7 @@ -//! 系统管理接口的查询与请求 wire contract。 +//! 系统管理接口的查询与请求 wire contract //! -//! 这里不依赖更新服务或进程控制;应用层通过窄端口提供系统操作事实。 +//! 这里不依赖更新服务或进程控制;应用层通过窄端口提供系统操作事实 use crate::auth::SessionState; @@ -30,7 +30,7 @@ use super::{ wire::map_admin_service_error, }; -/// 更新详情查询参数。 +/// 更新详情查询参数 #[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct UpdateDetailQuery { @@ -44,14 +44,14 @@ impl UpdateDetailQuery { self.channel } - /// 是否强制从发布源刷新。 + /// 是否强制从发布源刷新 #[must_use] pub fn refresh(&self) -> bool { self.refresh.unwrap_or(false) } } -/// 执行更新请求。 +/// 执行更新请求 #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct UpdateRequest { @@ -60,7 +60,7 @@ pub struct UpdateRequest { } impl UpdateRequest { - /// 取出确认的目标版本原值,由更新领域负责版本规范化和校验。 + /// 取出确认的目标版本原值,由更新领域负责版本规范化和校验 #[must_use] pub fn into_target_version(self) -> String { self.target_version @@ -237,7 +237,7 @@ struct SystemUpdateEventView { at_display: String, } -/// 构造固定 GET/POST 系统管理路由。 +/// 构造固定 GET/POST 系统管理路由 pub fn router() -> Router where S: SessionState + Clone + Send + Sync + 'static, diff --git a/backend/crates/gateway-api/src/admin/wire.rs b/backend/crates/gateway-api/src/admin/wire.rs index b72e2f27d..e8f455225 100644 --- a/backend/crates/gateway-api/src/admin/wire.rs +++ b/backend/crates/gateway-api/src/admin/wire.rs @@ -1,4 +1,4 @@ -//! 管理端公共 wire、响应信封与脱敏错误。 +//! 管理端公共 wire、响应信封与脱敏错误 use std::fmt; @@ -9,33 +9,33 @@ use axum::{ }; use serde::{Deserialize, Serialize}; -/// 管理端 wire 字段校验错误;只携带稳定字段名,不回显输入值。 +/// 管理端 wire 字段校验错误;只携带稳定字段名,不回显输入值 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct WireValidationError { field: &'static str, } impl WireValidationError { - /// 构造字段校验错误。 + /// 构造字段校验错误 #[must_use] pub const fn new(field: &'static str) -> Self { Self { field } } - /// 返回未通过校验的字段名。 + /// 返回未通过校验的字段名 #[must_use] pub const fn field(self) -> &'static str { self.field } } -/// 成功响应的稳定业务码。 +/// 成功响应的稳定业务码 pub const ADMIN_OK_CODE: u32 = 200; -/// 成功响应的稳定消息。 +/// 成功响应的稳定消息 pub const ADMIN_OK_MESSAGE: &str = "OK"; -/// 页码分页元数据。 +/// 页码分页元数据 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PageMeta { @@ -46,7 +46,7 @@ pub struct PageMeta { } impl PageMeta { - /// 由分页 owner 已经校验并计算的事实构造 wire metadata。 + /// 由分页 owner 已经校验并计算的事实构造 wire metadata #[must_use] pub const fn new(page: u32, page_size: u32, total: u64, total_pages: u32) -> Self { Self { @@ -57,82 +57,82 @@ impl PageMeta { } } - /// 当前页,从 1 开始。 + /// 当前页,从 1 开始 #[must_use] pub const fn page(self) -> u32 { self.page } - /// 每页数量。 + /// 每页数量 #[must_use] pub const fn page_size(self) -> u32 { self.page_size } - /// 全部记录数量。 + /// 全部记录数量 #[must_use] pub const fn total(self) -> u64 { self.total } - /// 总页数。 + /// 总页数 #[must_use] pub const fn total_pages(self) -> u32 { self.total_pages } } -/// 管理端稳定业务错误码。 +/// 管理端稳定业务错误码 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(transparent)] pub struct AdminErrorCode(u32); impl AdminErrorCode { - /// JSON 解析失败。 + /// JSON 解析失败 pub const MALFORMED_JSON: Self = Self(40000); - /// 请求参数或状态不合法。 + /// 请求参数或状态不合法 pub const BAD_REQUEST: Self = Self(40001); - /// 时间范围不合法。 + /// 时间范围不合法 pub const INVALID_TIME_RANGE: Self = Self(40002); - /// 模型来源不合法。 + /// 模型来源不合法 pub const INVALID_MODEL_SOURCE: Self = Self(40003); - /// 缺少登录会话。 + /// 缺少登录会话 pub const SESSION_REQUIRED: Self = Self(40101); - /// 登录凭据错误。 + /// 登录凭据错误 pub const INVALID_CREDENTIALS: Self = Self(40102); - /// 管理 API Key 错误。 + /// 管理 API Key 错误 pub const INVALID_API_KEY: Self = Self(40103); - /// 当前会话无权访问。 + /// 当前会话无权访问 pub const FORBIDDEN: Self = Self(40301); - /// 资源不存在。 + /// 资源不存在 pub const NOT_FOUND: Self = Self(40401); - /// 配置 revision 或资源状态冲突。 + /// 配置 revision 或资源状态冲突 pub const CONFLICT: Self = Self(40901); - /// 登录尝试过多。 + /// 登录尝试过多 pub const TOO_MANY_LOGIN_ATTEMPTS: Self = Self(42901); - /// 管理操作请求过于频繁。 + /// 管理操作请求过于频繁 pub const RATE_LIMITED: Self = Self(42902); - /// 设置持久化失败。 + /// 设置持久化失败 pub const SETTINGS_PERSIST: Self = Self(50000); - /// 未分类内部错误。 + /// 未分类内部错误 pub const INTERNAL: Self = Self(50001); - /// 用量记录的账号投影失败。 + /// 用量记录的账号投影失败 pub const USAGE_RECORD_ACCOUNTS: Self = Self(50002); - /// 上游网关失败。 + /// 上游网关失败 pub const BAD_GATEWAY: Self = Self(50201); - /// 不可逆上游操作的执行结果未知。 + /// 不可逆上游操作的执行结果未知 pub const UPSTREAM_RESULT_UNKNOWN: Self = Self(50202); - /// 依赖的服务暂不可用。 + /// 依赖的服务暂不可用 pub const SERVICE_UNAVAILABLE: Self = Self(50301); - /// 返回用于 JSON wire contract 的数值。 + /// 返回用于 JSON wire contract 的数值 #[must_use] pub const fn value(self) -> u32 { self.0 } } -/// 管理端错误响应正文。 +/// 管理端错误响应正文 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AdminErrorBody { @@ -142,7 +142,7 @@ pub struct AdminErrorBody { } impl AdminErrorBody { - /// 构造不携带业务数据的管理端错误正文。 + /// 构造不携带业务数据的管理端错误正文 #[must_use] pub fn new(code: AdminErrorCode, message: impl Into) -> Self { Self { @@ -152,20 +152,20 @@ impl AdminErrorBody { } } - /// 稳定业务错误码。 + /// 稳定业务错误码 #[must_use] pub const fn code(&self) -> AdminErrorCode { self.code } - /// 安全的客户端错误消息。 + /// 安全的客户端错误消息 #[must_use] pub fn message(&self) -> &str { &self.message } } -/// 管理端 HTTP 错误。 +/// 管理端 HTTP 错误 pub struct AdminError { status: StatusCode, body: AdminErrorBody, @@ -352,7 +352,7 @@ impl AdminError { } } -/// 把管理用例的稳定错误分类映射到既有 HTTP 错误 contract。 +/// 把管理用例的稳定错误分类映射到既有 HTTP 错误 contract pub(crate) fn map_admin_service_error(error: gateway_admin::model::AdminError) -> AdminError { use gateway_admin::model::AdminErrorKind; @@ -368,8 +368,8 @@ pub(crate) fn map_admin_service_error(error: gateway_admin::model::AdminError) - AdminErrorKind::Unavailable => AdminError::service_unavailable(), AdminErrorKind::Internal => AdminError::internal(), }; - // Admin owner 已负责公开文案,不能在 HTTP 边界再次丢掉安全的业务原因。 - // 认证与未知内部异常仍使用固定提示,不放行底层诊断或任意 500 消息。 + // Admin owner 已负责公开文案,不能在 HTTP 边界再次丢掉安全的业务原因 + // 认证与未知内部异常仍使用固定提示,不放行底层诊断或任意 500 消息 if matches!( error.kind(), AdminErrorKind::RateLimited @@ -403,7 +403,7 @@ impl fmt::Display for AdminError { } } -/// 带独立 HTTP 状态的管理端响应。 +/// 带独立 HTTP 状态的管理端响应 pub struct AdminResponse { status: StatusCode, body: T, @@ -422,7 +422,7 @@ impl IntoResponse for AdminResponse { } } -/// 管理端响应信封。 +/// 管理端响应信封 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AdminEnvelope { @@ -432,7 +432,7 @@ pub struct AdminEnvelope { } impl AdminEnvelope { - /// 构造稳定的成功响应。 + /// 构造稳定的成功响应 #[must_use] pub fn ok(data: T) -> Self { Self { @@ -442,32 +442,32 @@ impl AdminEnvelope { } } - /// 稳定业务码。 + /// 稳定业务码 #[must_use] pub const fn code(&self) -> u32 { self.code } - /// 稳定业务消息。 + /// 稳定业务消息 #[must_use] pub fn message(&self) -> &str { &self.message } - /// 响应数据。 + /// 响应数据 #[must_use] pub const fn data(&self) -> &T { &self.data } - /// 取出响应数据。 + /// 取出响应数据 #[must_use] pub fn into_data(self) -> T { self.data } } -/// 页码分页响应数据。 +/// 页码分页响应数据 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AdminPageData { @@ -476,25 +476,25 @@ pub struct AdminPageData { } impl AdminPageData { - /// 组合查询 owner 返回的当前页数据与分页事实。 + /// 组合查询 owner 返回的当前页数据与分页事实 #[must_use] pub const fn new(items: Vec, page: PageMeta) -> Self { Self { items, page } } - /// 当前页记录。 + /// 当前页记录 #[must_use] pub fn items(&self) -> &[T] { &self.items } - /// 分页元数据。 + /// 分页元数据 #[must_use] pub const fn page(&self) -> PageMeta { self.page } - /// 拆分分页数据。 + /// 拆分分页数据 #[must_use] pub fn into_parts(self) -> (Vec, PageMeta) { (self.items, self.page) diff --git a/backend/crates/gateway-api/src/auth.rs b/backend/crates/gateway-api/src/auth.rs index 4e652cb88..094ce2db8 100644 --- a/backend/crates/gateway-api/src/auth.rs +++ b/backend/crates/gateway-api/src/auth.rs @@ -1,4 +1,4 @@ -//! 控制面统一登录、会话恢复与退出;身份由认证用例返回。 +//! 控制面统一登录、会话恢复与退出;身份由认证用例返回 use std::{fmt, net::SocketAddr}; @@ -21,7 +21,7 @@ use crate::{ session_cookie, }; -/// 控制面 HTTP adapter 消费同一组用例;权限由各入口服务端校验。 +/// 控制面 HTTP adapter 消费同一组用例;权限由各入口服务端校验 pub trait SessionState { fn admin_services(&self) -> &AdminServices; } diff --git a/backend/crates/gateway-api/src/health.rs b/backend/crates/gateway-api/src/health.rs index 4a6c7fd54..8e88fca4d 100644 --- a/backend/crates/gateway-api/src/health.rs +++ b/backend/crates/gateway-api/src/health.rs @@ -1,4 +1,4 @@ -//! `/healthz` 对 Core、Store 与 Host 关键 worker 健康事实的聚合。 +//! `/healthz` 对 Core、Store 与 Host 关键 worker 健康事实的聚合 use std::sync::Arc; use std::time::Duration; diff --git a/backend/crates/gateway-api/src/key_usage/mod.rs b/backend/crates/gateway-api/src/key_usage/mod.rs index bb27e504e..b1dea964b 100644 --- a/backend/crates/gateway-api/src/key_usage/mod.rs +++ b/backend/crates/gateway-api/src/key_usage/mod.rs @@ -1,4 +1,4 @@ -//! 当前 Key 的单页用量 API;只暴露页面需要的只读字段。 +//! 当前 Key 的单页用量 API;只暴露页面需要的只读字段 use axum::{ Router, diff --git a/backend/crates/gateway-api/src/key_usage/presenter.rs b/backend/crates/gateway-api/src/key_usage/presenter.rs index e43ce7068..89f867555 100644 --- a/backend/crates/gateway-api/src/key_usage/presenter.rs +++ b/backend/crates/gateway-api/src/key_usage/presenter.rs @@ -1,4 +1,4 @@ -//! 用量响应不含凭据;配置响应仅在显式读取时返回当前 Key 的名称与明文。 +//! 用量响应不含凭据;配置响应仅在显式读取时返回当前 Key 的名称与明文 use chrono::{DateTime, Utc}; use gateway_admin::model::{ @@ -24,7 +24,7 @@ pub(super) struct VersionView { } pub(super) fn version(version: SystemVersion) -> VersionView { - // 密钥用户仅查看构建标识,不暴露部署环境、更新状态或内部诊断。 + // 密钥用户仅查看构建标识,不暴露部署环境、更新状态或内部诊断 VersionView { version: version.version, git_sha: version.git_sha, @@ -197,7 +197,7 @@ pub(super) struct RecordView { status_code: Option, } -// Key 只查看自身请求的输出时间,不包含账号容量、调度等待等管理侧观测。 +// Key 只查看自身请求的输出时间,不包含账号容量、调度等待等管理侧观测 #[derive(Default, Serialize)] #[serde(rename_all = "camelCase")] struct OutputTimingView { @@ -233,7 +233,7 @@ fn success_record(value: UsageListRecord, time: crate::time::TimePresenter) -> R client_ip: value.client_ip, user_agent: value.user_agent, status: "success", - // 成功记录不保存 HTTP 状态,不能用 200 伪造缺失的原始事实。 + // 成功记录不保存 HTTP 状态,不能用 200 伪造缺失的原始事实 status_code: None, } } diff --git a/backend/crates/gateway-api/src/key_usage/query.rs b/backend/crates/gateway-api/src/key_usage/query.rs index a1e4285ea..f48022ee0 100644 --- a/backend/crates/gateway-api/src/key_usage/query.rs +++ b/backend/crates/gateway-api/src/key_usage/query.rs @@ -1,4 +1,4 @@ -//! 查询不接受 Key、账号或 Provider 范围;禁止未知字段穿透权限边界。 +//! 查询不接受 Key、账号或 Provider 范围;禁止未知字段穿透权限边界 use axum::http::StatusCode; use chrono::Duration; diff --git a/backend/crates/gateway-api/src/lib.rs b/backend/crates/gateway-api/src/lib.rs index 38106df2b..4ecd8cc2b 100644 --- a/backend/crates/gateway-api/src/lib.rs +++ b/backend/crates/gateway-api/src/lib.rs @@ -1,6 +1,6 @@ -//! 客户端与管理端 HTTP 协议 adapter。 +//! 客户端与管理端 HTTP 协议 adapter //! -//! 本 crate 只负责请求解码、Core/Admin 调用和 HTTP/WS/SSE delivery。 +//! 本 crate 只负责请求解码、Core/Admin 调用和 HTTP/WS/SSE delivery use std::collections::BTreeSet; use std::path::{Path, PathBuf}; @@ -39,7 +39,7 @@ mod time; pub use time::{RequestBucketView, TimePresenter}; -/// API-owned HTTP 与静态资源配置。 +/// API-owned HTTP 与静态资源配置 #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] pub struct ApiConfig { pub asset_directory: PathBuf, @@ -49,11 +49,11 @@ pub struct ApiConfig { } impl ApiConfig { - /// 解析静态资源相对路径并校验全部 HTTP 配置。 + /// 解析静态资源相对路径并校验全部 HTTP 配置 /// /// # Errors /// - /// 路径为空、origin/header 非法或 timeout 为零时返回脱敏错误。 + /// 路径为空、origin/header 非法或 timeout 为零时返回脱敏错误 pub fn resolve_and_validate(&mut self, source_dir: &Path) -> Result<(), ApiConfigError> { match std::env::var(WEB_DIST_ENV) { Ok(value) if value.trim().is_empty() => { @@ -97,7 +97,7 @@ impl ApiConfig { } } -/// API 配置非法的稳定分类。 +/// API 配置非法的稳定分类 #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] pub enum ApiConfigError { #[error("API asset directory is invalid")] @@ -112,7 +112,7 @@ pub enum ApiConfigError { InvalidRequestIdHeader, } -/// 完成组装的唯一 API router。 +/// 完成组装的唯一 API router pub struct ApiBundle { router: Router, settings: middleware::SettingsSource, @@ -122,7 +122,7 @@ pub struct ApiBundle { } impl ApiBundle { - /// 组合根从请求已冻结的快照解析计划;响应流持有同一代次。 + /// 组合根从请求已冻结的快照解析计划;响应流持有同一代次 #[must_use] pub fn with_middleware( mut self, @@ -137,7 +137,7 @@ impl ApiBundle { self } - /// 内部调用使用同一总路由;组合根持有强引用,Runtime 只保存 Weak。 + /// 内部调用使用同一总路由;组合根持有强引用,Runtime 只保存 Weak pub fn dispatcher(&self) -> Arc { Arc::new(middleware::RouterDispatcher { router: middleware::wrap( @@ -162,7 +162,7 @@ impl ApiBundle { } } -/// 组装客户端、管理端、健康检查和静态资源路由。 +/// 组装客户端、管理端、健康检查和静态资源路由 pub fn initialize( mut config: ApiConfig, execution: Arc, @@ -171,7 +171,7 @@ pub fn initialize( worker_health: Arc, lifecycle: Arc, ) -> Result { - // 配置加载已解析环境变量与相对路径,初始化只校验,避免覆盖最终目录。 + // 配置加载已解析环境变量与相对路径,初始化只校验,避免覆盖最终目录 config.validate().map_err(ApiError::Config)?; let request_id_header = HeaderName::from_str(&config.request_id_header) .map_err(|_| ApiError::Config(ApiConfigError::InvalidRequestIdHeader))?; @@ -216,7 +216,7 @@ pub fn initialize( .map_err(|_| ApiError::Config(ApiConfigError::InvalidCorsOrigin)) }) .collect::, _>>()?; - // credentials 模式禁止通配 allow_headers;显式列出鉴权与内容协商头。 + // credentials 模式禁止通配 allow_headers;显式列出鉴权与内容协商头 router = router.layer( CorsLayer::new() .allow_origin(origins) @@ -231,7 +231,7 @@ pub fn initialize( ); } // Trace 必须在 SetRequestId 内侧,span 才能捕获本服务生成的 request_id; - // 默认 DEBUG span 会被 info 日志过滤器丢弃,这里显式用 info_span。 + // 默认 DEBUG span 会被 info 日志过滤器丢弃,这里显式用 info_span let trace_layer = TraceLayer::new_for_http().make_span_with({ let request_id_header = request_id_header.clone(); move |request: &Request<_>| { @@ -280,7 +280,7 @@ async fn static_cache_control(mut response: axum::response::Response) -> axum::r response } -/// API 初始化失败的脱敏分类。 +/// API 初始化失败的脱敏分类 #[derive(Debug, thiserror::Error)] pub enum ApiError { #[error(transparent)] diff --git a/backend/crates/gateway-api/src/middleware/headers.rs b/backend/crates/gateway-api/src/middleware/headers.rs index f1f105694..107f303c0 100644 --- a/backend/crates/gateway-api/src/middleware/headers.rs +++ b/backend/crates/gateway-api/src/middleware/headers.rs @@ -1,10 +1,10 @@ -//! HTTP HeaderMap 与插件边界值的无损转换。 +//! HTTP HeaderMap 与插件边界值的无损转换 use axum::http::{HeaderMap, HeaderName, HeaderValue}; use bytes::Bytes; use gateway_core::engine::middleware::{MiddlewareError, MiddlewareHeader}; -/// 完整保留多值与非 UTF-8 header,Runtime 只转换 wire 编码。 +/// 完整保留多值与非 UTF-8 header,Runtime 只转换 wire 编码 pub(crate) fn encode_headers(headers: &HeaderMap) -> Vec { headers .iter() diff --git a/backend/crates/gateway-api/src/middleware/mod.rs b/backend/crates/gateway-api/src/middleware/mod.rs index 5cd9c287c..3e05fe103 100644 --- a/backend/crates/gateway-api/src/middleware/mod.rs +++ b/backend/crates/gateway-api/src/middleware/mod.rs @@ -1,4 +1,4 @@ -//! 路由外层只适配 HTTP 值和正文,不解释插件协议或业务策略。 +//! 路由外层只适配 HTTP 值和正文,不解释插件协议或业务策略 pub(crate) mod headers; pub(crate) mod websocket; @@ -112,7 +112,7 @@ pub(crate) fn wrap( request_id_header: HeaderName, settings: SettingsSource, ) -> Router { - // Axum 的 route layer 在匹配后运行;外层 fallback service 才能让改写后的 URI 重新匹配内层路由。 + // Axum 的 route layer 在匹配后运行;外层 fallback service 才能让改写后的 URI 重新匹配内层路由 Router::new().fallback_service(tower::service_fn(move |mut request: Request| { let router = router.clone(); let inherited = request.extensions().get::().is_some(); diff --git a/backend/crates/gateway-api/src/middleware/websocket.rs b/backend/crates/gateway-api/src/middleware/websocket.rs index fe0a4ac42..487c85ea8 100644 --- a/backend/crates/gateway-api/src/middleware/websocket.rs +++ b/backend/crates/gateway-api/src/middleware/websocket.rs @@ -1,4 +1,4 @@ -//! 服务器握手与双工传输;自定义协议及多轮执行由接管会话的插件编排。 +//! 服务器握手与双工传输;自定义协议及多轮执行由接管会话的插件编排 use std::sync::Arc; @@ -86,7 +86,7 @@ impl upgrade::WebSocketUpgrade for Upgrade { } } -/// 原生协议与插件接管会话共用消息链;调用取消不等待插件释放 transport 锁。 +/// 原生协议与插件接管会话共用消息链;调用取消不等待插件释放 transport 锁 pub(crate) async fn transform( context: core::Context, message: core::Message, @@ -152,7 +152,7 @@ impl core::Session for Session { return Ok(None); }; let message = into_core(message); - // transport 锁只保护一次读取;插件可以在收包处理中主动发送。 + // transport 锁只保护一次读取;插件可以在收包处理中主动发送 if let Some(message) = self.transform(message, core::Direction::Incoming).await? { return Ok(Some(message)); } diff --git a/backend/crates/gateway-api/src/openai/auth.rs b/backend/crates/gateway-api/src/openai/auth.rs index 155a1d8c8..387c44da1 100644 --- a/backend/crates/gateway-api/src/openai/auth.rs +++ b/backend/crates/gateway-api/src/openai/auth.rs @@ -1,4 +1,4 @@ -//! OpenAI 客户端协议的 Bearer API key 认证。 +//! OpenAI 客户端协议的 Bearer API key 认证 use axum::{ http::{HeaderMap, header::AUTHORIZATION}, @@ -17,22 +17,22 @@ use super::{ service::OpenAiService, }; -/// Client API key 鉴权失败原因。 +/// Client API key 鉴权失败原因 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ClientApiKeyAuthError { - /// 缺失 Authorization 头。 + /// 缺失 Authorization 头 MissingAuthorization, - /// Authorization 头不是合法的 Bearer token。 + /// Authorization 头不是合法的 Bearer token MalformedAuthorization, - /// Bearer token 不是 client API key 格式。 + /// Bearer token 不是 client API key 格式 InvalidKeyFormat, - /// Key 不存在、已禁用或 wire 格式无效。 + /// Key 不存在、已禁用或 wire 格式无效 InvalidKey, - /// RuntimeSnapshot 一致性保护暂停接收新请求。 + /// RuntimeSnapshot 一致性保护暂停接收新请求 RuntimeUnavailable, } -/// Client API Key 或最低版本准入失败。 +/// Client API Key 或最低版本准入失败 #[derive(Debug, Clone, PartialEq, Eq)] pub enum ClientAccessError { Authentication(ClientApiKeyAuthError), @@ -51,7 +51,7 @@ impl From for ClientAccessError { } } -/// 从有界请求头中识别出的客户端及其可选合法版本。 +/// 从有界请求头中识别出的客户端及其可选合法版本 #[derive(Debug, Clone, PartialEq, Eq)] pub struct IdentifiedCodexClient { kind: CodexClientKind, @@ -71,7 +71,7 @@ impl IdentifiedCodexClient { } impl ClientApiKeyAuthError { - /// 返回可用于日志和指标的稳定失败原因。 + /// 返回可用于日志和指标的稳定失败原因 #[must_use] pub const fn reason(self) -> &'static str { match self { @@ -84,11 +84,11 @@ impl ClientApiKeyAuthError { } } -/// 从请求头提取 Bearer Client API key。 +/// 从请求头提取 Bearer Client API key /// /// # Errors /// -/// Header 缺失、Bearer 语法错误或 Key 不能作为 HTTP Bearer 值时返回稳定错误。 +/// Header 缺失、Bearer 语法错误或 Key 不能作为 HTTP Bearer 值时返回稳定错误 pub fn bearer_client_api_key(headers: &HeaderMap) -> Result<&str, ClientApiKeyAuthError> { let raw = headers .get(AUTHORIZATION) @@ -123,7 +123,7 @@ pub(crate) async fn authenticate_client( Ok(client) } -/// 复用数据面入口认证计划,但不记录 Key 使用事实或执行推理准入。 +/// 复用数据面入口认证计划,但不记录 Key 使用事实或执行推理准入 pub(crate) async fn verify_client( service: &OpenAiService, headers: &HeaderMap, @@ -170,7 +170,7 @@ pub(crate) fn client_access_error_response(error: ClientAccessError) -> Response } } -/// Desktop 优先于内嵌 CLI/Core;未知客户端及未提供应用版本的手机远程客户端返回 `None`。 +/// Desktop 优先于内嵌 CLI/Core;未知客户端及未提供应用版本的手机远程客户端返回 `None` #[must_use] pub fn identify_codex_client(headers: &HeaderMap) -> Option { const MAXIMUM_HEADER_LENGTH: usize = 4096; @@ -182,8 +182,8 @@ pub fn identify_codex_client(headers: &HeaderMap) -> Option( fn desktop_version_from_user_agent(user_agent: &str) -> Option<&str> { let marker = "Codex Desktop;"; let start = find_ascii_case_insensitive(user_agent, marker)? + marker.len(); - // 空候选也代表已提供应用版本,不能被手机远程的缺失版本规则放行。 + // 空候选也代表已提供应用版本,不能被手机远程的缺失版本规则放行 user_agent[start..] .trim_start() .split([')', ' ', ';', ',']) diff --git a/backend/crates/gateway-api/src/openai/endpoint.rs b/backend/crates/gateway-api/src/openai/endpoint.rs index 07caed8e1..4b2fb526b 100644 --- a/backend/crates/gateway-api/src/openai/endpoint.rs +++ b/backend/crates/gateway-api/src/openai/endpoint.rs @@ -1,4 +1,4 @@ -//! OpenAI Provider 原生非流式 JSON 端点的公共交付边界。 +//! OpenAI Provider 原生非流式 JSON 端点的公共交付边界 use std::net::IpAddr; @@ -29,7 +29,7 @@ use super::{ service::OpenAiService, }; -/// Provider 自有端点也先冻结身份,再通过统一请求链进入原有执行与结算路径。 +/// Provider 自有端点也先冻结身份,再通过统一请求链进入原有执行与结算路径 pub(super) async fn provider_endpoint_response( service: OpenAiService, client: AuthenticatedClient, @@ -86,7 +86,7 @@ where } } -/// 只收集正文,不提前 commit;外层完成响应变换与校验后才提交 Core。 +/// 只收集正文,不提前 commit;外层完成响应变换与校验后才提交 Core async fn collect_raw_json_response( started: StartedExecution, ) -> Result { diff --git a/backend/crates/gateway-api/src/openai/error.rs b/backend/crates/gateway-api/src/openai/error.rs index 4580d3749..de8088f25 100644 --- a/backend/crates/gateway-api/src/openai/error.rs +++ b/backend/crates/gateway-api/src/openai/error.rs @@ -1,4 +1,4 @@ -//! OpenAI 客户端协议的稳定错误响应。 +//! OpenAI 客户端协议的稳定错误响应 use axum::{ Json, @@ -15,7 +15,7 @@ use serde_json::{Value, json}; use super::responses::ProtocolErrorBody; -/// OpenAI 风格错误响应。 +/// OpenAI 风格错误响应 pub fn openai_error_response( status: StatusCode, message: &str, @@ -31,7 +31,7 @@ pub fn openai_error_response( } } -/// Codex 将这两个容量码视为不可重试;仅在交付边界转换,内部仍保留真实上游事实。 +/// Codex 将这两个容量码视为不可重试;仅在交付边界转换,内部仍保留真实上游事实 pub(super) fn client_error_code(code: &str) -> &str { match code { "server_is_overloaded" | "slow_down" => "server_error", @@ -39,7 +39,7 @@ pub(super) fn client_error_code(code: &str) -> &str { } } -/// 为 HTTP 错误或流内失败保留原有字段,仅投影客户端需要的重试信号。 +/// 为 HTTP 错误或流内失败保留原有字段,仅投影客户端需要的重试信号 pub(super) fn capacity_error_for_client(data: &Value) -> Option { const CODE_PATHS: [&str; 2] = ["/error/code", "/response/error/code"]; let capacity_code = |value: &Value| { @@ -61,7 +61,7 @@ pub(super) fn capacity_error_for_client(data: &Value) -> Option { *code = Value::String("server_error".to_owned()); } } - // WS 包装错误按 HTTP 状态分类;保留 400/429 会使客户端终止重试。 + // WS 包装错误按 HTTP 状态分类;保留 400/429 会使客户端终止重试 for field in ["status", "status_code"] { if let Some(status) = projected.get_mut(field) && status.is_number() @@ -72,12 +72,12 @@ pub(super) fn capacity_error_for_client(data: &Value) -> Option { Some(projected) } -/// 严格协议 decoder/encoder 返回的安全错误 body。 +/// 严格协议 decoder/encoder 返回的安全错误 body pub fn protocol_error_response(status: StatusCode, body: ProtocolErrorBody) -> Response { (status, Json(body.into_value())).into_response() } -/// 下游 Client API Key 无效。 +/// 下游 Client API Key 无效 pub fn missing_client_api_key_response() -> (StatusCode, Json) { openai_error_response( StatusCode::UNAUTHORIZED, @@ -87,7 +87,7 @@ pub fn missing_client_api_key_response() -> (StatusCode, Json) { ) } -/// RuntimeSnapshot 当前不允许接收新的配置依赖请求。 +/// RuntimeSnapshot 当前不允许接收新的配置依赖请求 pub fn runtime_unavailable_response() -> (StatusCode, Json) { openai_error_response( StatusCode::SERVICE_UNAVAILABLE, @@ -97,7 +97,7 @@ pub fn runtime_unavailable_response() -> (StatusCode, Json) { ) } -/// 已识别 Codex 客户端不满足最低版本要求。 +/// 已识别 Codex 客户端不满足最低版本要求 pub fn client_version_rejection_response( rejection: &ClientVersionRejection, ) -> (StatusCode, Json) { @@ -138,7 +138,7 @@ pub fn client_version_rejection_response( ) } -/// 对外模型不存在。 +/// 对外模型不存在 pub fn model_not_found_response() -> (StatusCode, Json) { openai_error_response( StatusCode::NOT_FOUND, @@ -148,7 +148,7 @@ pub fn model_not_found_response() -> (StatusCode, Json) { ) } -/// 将 core Engine 错误收敛为稳定 Gateway 分类。 +/// 将 core Engine 错误收敛为稳定 Gateway 分类 #[must_use] pub fn gateway_error_from_engine(error: &EngineError) -> GatewayError { match error { @@ -178,7 +178,7 @@ pub fn gateway_error_from_engine(error: &EngineError) -> GatewayError { } } -/// Gateway 错误的 OpenAI HTTP 表达。 +/// Gateway 错误的 OpenAI HTTP 表达 pub fn gateway_error_response(error: &GatewayError) -> Response { let (status, default_type, default_code) = gateway_error_contract(error.kind()); let mut response = openai_error_response( @@ -204,7 +204,7 @@ pub fn gateway_error_response(error: &GatewayError) -> Response { response } -/// 在下游尚未提交时交付 Provider 的 HTTP 失败响应,并应用客户端容量恢复合同。 +/// 在下游尚未提交时交付 Provider 的 HTTP 失败响应,并应用客户端容量恢复合同 pub fn engine_error_response(error: &EngineError) -> Response { let capacity_unavailable = matches!(error, EngineError::Provider(error) if error.kind() == ProviderErrorKind::UpstreamCapacityUnavailable); @@ -251,7 +251,7 @@ pub fn engine_error_response(error: &EngineError) -> Response { if capacity_unavailable { *response.status_mut() = StatusCode::SERVICE_UNAVAILABLE; } - // 正文未完整取得时不能透传残缺响应,但已确认的上游关联 ID 仍应交付。 + // 正文未完整取得时不能透传残缺响应,但已确认的上游关联 ID 仍应交付 if let EngineError::Provider(error) = error && let Some(request_id) = error.upstream_request_id() && let Ok(value) = HeaderValue::from_str(request_id.as_str()) @@ -261,7 +261,7 @@ pub fn engine_error_response(error: &EngineError) -> Response { response } -/// Gateway 错误稳定映射,供 HTTP、SSE 和 WebSocket 共用。 +/// Gateway 错误稳定映射,供 HTTP、SSE 和 WebSocket 共用 #[must_use] pub const fn gateway_error_contract( kind: GatewayErrorKind, diff --git a/backend/crates/gateway-api/src/openai/images/http.rs b/backend/crates/gateway-api/src/openai/images/http.rs index 630c9e220..d896871ef 100644 --- a/backend/crates/gateway-api/src/openai/images/http.rs +++ b/backend/crates/gateway-api/src/openai/images/http.rs @@ -1,4 +1,4 @@ -//! Codex Images 非流式 HTTP adapter。 +//! Codex Images 非流式 HTTP adapter use std::net::SocketAddr; @@ -28,7 +28,7 @@ use crate::openai::{ const OPENAI_PROTOCOL: &str = "openai"; const IMAGE_TURN_ID_CONTEXT_KEY: &str = "image_turn_id"; -/// `POST /v1/images/generations`。 +/// `POST /v1/images/generations` pub(crate) async fn image_generations( State(state): State, connect_info: Option>>, @@ -46,7 +46,7 @@ pub(crate) async fn image_generations( .await } -/// `POST /v1/images/edits`。 +/// `POST /v1/images/edits` pub(crate) async fn image_edits( State(state): State, connect_info: Option>>, diff --git a/backend/crates/gateway-api/src/openai/images/mod.rs b/backend/crates/gateway-api/src/openai/images/mod.rs index 28647bd89..4c282ddd9 100644 --- a/backend/crates/gateway-api/src/openai/images/mod.rs +++ b/backend/crates/gateway-api/src/openai/images/mod.rs @@ -1,4 +1,4 @@ -//! OpenAI/Codex Images JSON 透明 adapter。 +//! OpenAI/Codex Images JSON 透明 adapter mod http; diff --git a/backend/crates/gateway-api/src/openai/middleware/http.rs b/backend/crates/gateway-api/src/openai/middleware/http.rs index 64ecc0b55..a9c6d96a8 100644 --- a/backend/crates/gateway-api/src/openai/middleware/http.rs +++ b/backend/crates/gateway-api/src/openai/middleware/http.rs @@ -1,4 +1,4 @@ -//! 未提交响应的 HTTP 交付;只验证协议边界,不解释或重算 Core 的业务事实。 +//! 未提交响应的 HTTP 交付;只验证协议边界,不解释或重算 Core 的业务事实 use crate::middleware::headers::{decode_headers, encode_headers}; use crate::openai::error::{ @@ -19,7 +19,7 @@ use gateway_core::{ error::GatewayError, }; -/// 只读目录/用量与协议错误共用的单帧响应;没有 Provider attempt 或计量副作用。 +/// 只读目录/用量与协议错误共用的单帧响应;没有 Provider attempt 或计量副作用 pub(crate) async fn buffered_response( protocol: &str, response: Response, @@ -70,13 +70,13 @@ impl ExpectedBody { && frame.terminal() && serde_json::from_slice::(frame.bytes()).is_ok() } - // 上游错误正文可能不是 JSON;只限定单文档交付,不改写原始错误字节。 + // 上游错误正文可能不是 JSON;只限定单文档交付,不改写原始错误字节 Self::SinglePayload => { frame.terminal() && (frame.framing() == MiddlewareFraming::RawBytes || Self::SingleJson.validate(frame).is_ok()) } - // 原生超大 SSE 帧允许按原字节片段交付;插件写入边界由 Runtime 校验。 + // 原生超大 SSE 帧允许按原字节片段交付;插件写入边界由 Runtime 校验 Self::Sse => frame.framing() == MiddlewareFraming::SseEvent, }; if valid { @@ -87,7 +87,7 @@ impl ExpectedBody { } } -/// 全部返回包装完成、首帧可读取后才提交原有 Core delivery barrier。 +/// 全部返回包装完成、首帧可读取后才提交原有 Core delivery barrier pub(crate) async fn into_http_response( response: MiddlewareResponse, expected: ExpectedBody, @@ -120,7 +120,7 @@ pub(crate) async fn into_http_response( return fail_before_commit(body, error).await; } if let Some(frame) = &first { - // 短路响应没有下游 adapter 生成的头,传输类型仍由宿主按帧合同补齐。 + // 短路响应没有下游 adapter 生成的头,传输类型仍由宿主按帧合同补齐 headers.entry(header::CONTENT_TYPE).or_insert_with(|| { HeaderValue::from_static(match frame.framing() { MiddlewareFraming::JsonDocument => "application/json", @@ -156,7 +156,7 @@ pub(crate) async fn into_http_response( }; let keep_alive = matches!(expected, ExpectedBody::Sse); let stream = Box::pin(futures::stream::try_unfold(delivery, HttpDelivery::next)); - // 固定同一条输出流;心跳不能取消并重建正在等待的 next_frame 或结算 future。 + // 固定同一条输出流;心跳不能取消并重建正在等待的 next_frame 或结算 future let stream = futures::stream::unfold(stream, move |mut stream| async move { let chunk = tokio::select! { biased; @@ -220,7 +220,7 @@ fn response_head( if status.is_informational() { return Err(MiddlewareError::InvalidState); } - // 原生上游头已在协议终端处理;插件返回后的字段只做 HTTP 类型校验。 + // 原生上游头已在协议终端处理;插件返回后的字段只做 HTTP 类型校验 Ok((status, decode_headers(headers)?)) } diff --git a/backend/crates/gateway-api/src/openai/middleware/mod.rs b/backend/crates/gateway-api/src/openai/middleware/mod.rs index f6eda74b1..1eaa86d37 100644 --- a/backend/crates/gateway-api/src/openai/middleware/mod.rs +++ b/backend/crates/gateway-api/src/openai/middleware/mod.rs @@ -1,4 +1,4 @@ -//! OpenAI 请求洋葱链与最终传输交付;不拥有 Provider 选择或计量事实。 +//! OpenAI 请求洋葱链与最终传输交付;不拥有 Provider 选择或计量事实 mod http; mod request; diff --git a/backend/crates/gateway-api/src/openai/middleware/request.rs b/backend/crates/gateway-api/src/openai/middleware/request.rs index 5de9e3faa..c6a661be7 100644 --- a/backend/crates/gateway-api/src/openai/middleware/request.rs +++ b/backend/crates/gateway-api/src/openai/middleware/request.rs @@ -1,4 +1,4 @@ -//! 请求身份冻结、单次 next 与调用代次保活;HTTP 编码在 http 子模块中完成。 +//! 请求身份冻结、单次 next 与调用代次保活;HTTP 编码在 http 子模块中完成 use crate::middleware::headers::{decode_headers, encode_headers}; @@ -48,7 +48,7 @@ impl RequestInput { } } -/// 已鉴权只读查询共用入口;不重新鉴权、计量或创建 Provider attempt。 +/// 已鉴权只读查询共用入口;不重新鉴权、计量或创建 Provider attempt pub(crate) async fn query_response( execution: Arc, client: AuthenticatedClient, @@ -161,7 +161,7 @@ struct RequestLifetime { impl Drop for RequestLifetime { fn drop(&mut self) { - // 包括还在 next 中等待、客户端断开和提前关流;不启动第二份账本终结逻辑。 + // 包括还在 next 中等待、客户端断开和提前关流;不启动第二份账本终结逻辑 self.cancellation.cancel(); } } @@ -200,7 +200,7 @@ impl MiddlewareBody for RequestBody { } } -/// 终点重新解码插件改写的请求;冻结认证身份不从这些 header 再次取得。 +/// 终点重新解码插件改写的请求;冻结认证身份不从这些 header 再次取得 pub(crate) fn request_parts( request: MiddlewareRequest, ) -> Result<(String, HeaderMap, Bytes), MiddlewareError> { diff --git a/backend/crates/gateway-api/src/openai/middleware/session.rs b/backend/crates/gateway-api/src/openai/middleware/session.rs index 7d1ea484b..4ed6220c1 100644 --- a/backend/crates/gateway-api/src/openai/middleware/session.rs +++ b/backend/crates/gateway-api/src/openai/middleware/session.rs @@ -1,4 +1,4 @@ -//! 非流式响应在洋葱链返回后才提交;既有执行守卫保管唯一终结责任。 +//! 非流式响应在洋葱链返回后才提交;既有执行守卫保管唯一终结责任 use futures::future::BoxFuture; use gateway_core::engine::middleware::{ @@ -29,13 +29,13 @@ pub(crate) fn pending_execution_response( struct BufferedExecutionBody { frame: Option, execution: PendingExecution, - // 原生结果决定是否提交执行;外层改写状态不能把失败转成 Core 成功。 + // 原生结果决定是否提交执行;外层改写状态不能把失败转成 Core 成功 commit_execution: bool, } impl MiddlewareBody for BufferedExecutionBody { fn next_frame(&mut self) -> BoxFuture<'_, Result, MiddlewareError>> { - // EOF 只表示单文档已读完;外层仍须验证正文并调用 commit,不能在此取消会话。 + // EOF 只表示单文档已读完;外层仍须验证正文并调用 commit,不能在此取消会话 Box::pin(async { Ok(self.frame.take()) }) } diff --git a/backend/crates/gateway-api/src/openai/mod.rs b/backend/crates/gateway-api/src/openai/mod.rs index f50ca8146..e55afc6d8 100644 --- a/backend/crates/gateway-api/src/openai/mod.rs +++ b/backend/crates/gateway-api/src/openai/mod.rs @@ -1,4 +1,4 @@ -//! OpenAI 客户端协议 adapter。 +//! OpenAI 客户端协议 adapter pub mod auth; mod endpoint; @@ -12,7 +12,7 @@ pub mod search; pub(crate) mod service; mod usage; -/// 客户端可读的 ID 必须能按现有请求记录检索;不以无关入口 ID 补位。 +/// 客户端可读的 ID 必须能按现有请求记录检索;不以无关入口 ID 补位 pub(crate) fn with_model_request_id( mut response: axum::response::Response, request_id: &gateway_core::engine::ModelRequestId, @@ -25,7 +25,7 @@ pub(crate) fn with_model_request_id( let headers = response.headers_mut(); let usable = |value: &HeaderValue| value.to_str().is_ok_and(|id| !id.trim().is_empty()); if !headers.get("x-request-id").is_some_and(usable) { - // 只有别名时沿用同一个上游值,避免外层 middleware 再填入入口 ID。 + // 只有别名时沿用同一个上游值,避免外层 middleware 再填入入口 ID let client_id = headers .get("x-oai-request-id") .filter(|value| usable(value)) diff --git a/backend/crates/gateway-api/src/openai/models.rs b/backend/crates/gateway-api/src/openai/models.rs index 2d78946bd..6ee51e8f8 100644 --- a/backend/crates/gateway-api/src/openai/models.rs +++ b/backend/crates/gateway-api/src/openai/models.rs @@ -1,4 +1,4 @@ -//! OpenAI 模型目录 HTTP adapter。 +//! OpenAI 模型目录 HTTP adapter use axum::{ Json, @@ -31,7 +31,8 @@ pub(crate) struct ModelsQuery { client_version: Option, } -/// `GET /v1/models`。Codex 携带 `client_version` 时返回其专用目录合同。 +/// `GET /v1/models` +/// Codex 携带 `client_version` 时返回其专用目录合同 pub(crate) async fn models( State(state): State, Query(query): Query, @@ -93,7 +94,7 @@ async fn models_response( let mut value: Value = serde_json::from_slice(payload.body()).map_err(|_| ())?; let object = value.as_object_mut().ok_or(())?; - // 别名只改变请求标识,能力、提示词、顺序等仍由上游目录决定。 + // 别名只改变请求标识,能力、提示词、顺序等仍由上游目录决定 object.insert("slug".to_owned(), Value::String(model.as_str().to_owned())); Ok(value) } @@ -104,7 +105,7 @@ async fn models_response( }; return ( StatusCode::OK, - // 目录经过账号选择/别名/多 Provider 聚合,不能借用上游 ETag 表示改写后的正文。 + // 目录经过账号选择/别名/多 Provider 聚合,不能借用上游 ETag 表示改写后的正文 [("cache-control", "private, no-store")], Json(json!({ "models": models, @@ -138,7 +139,7 @@ fn catalog_unavailable_response() -> Response { .into_response() } -/// `GET /v1/models/{model_id}`。 +/// `GET /v1/models/{model_id}` pub(crate) async fn model_detail( State(state): State, headers: HeaderMap, diff --git a/backend/crates/gateway-api/src/openai/responses/error.rs b/backend/crates/gateway-api/src/openai/responses/error.rs index 6bd8ea6ed..9469ee0d9 100644 --- a/backend/crates/gateway-api/src/openai/responses/error.rs +++ b/backend/crates/gateway-api/src/openai/responses/error.rs @@ -1,108 +1,108 @@ -//! OpenAI Responses adapter 的稳定错误 contract。 +//! OpenAI Responses adapter 的稳定错误 contract use serde::Serialize; use serde_json::Value; use thiserror::Error; -/// OpenAI 风格的协议错误 JSON。 +/// OpenAI 风格的协议错误 JSON #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct ProtocolErrorBody { - /// 错误对象。 + /// 错误对象 pub error: ProtocolError, } -/// 不包含请求正文或 prompt 的错误对象。 +/// 不包含请求正文或 prompt 的错误对象 #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct ProtocolError { - /// OpenAI 错误类别。 + /// OpenAI 错误类别 #[serde(rename = "type")] pub kind: &'static str, - /// 稳定机器码。 + /// 稳定机器码 pub code: &'static str, - /// 安全的可读信息。 + /// 安全的可读信息 pub message: String, - /// 出错字段;不包含字段值。 + /// 出错字段;不包含字段值 #[serde(skip_serializing_if = "Option::is_none")] pub param: Option, } impl ProtocolErrorBody { - /// 序列化为 JSON value。 + /// 序列化为 JSON value #[must_use] pub fn into_value(self) -> Value { serde_json::json!({ "error": self.error }) } } -/// Responses 请求解码错误。 +/// Responses 请求解码错误 #[derive(Debug, Clone, PartialEq, Eq, Error)] pub enum RequestDecodeError { - /// 请求不是合法 JSON。 + /// 请求不是合法 JSON #[error("request body must be valid JSON")] MalformedJson, - /// 请求体解压后超过允许上限。 + /// 请求体解压后超过允许上限 #[error("decompressed request body is too large")] DecompressedBodyTooLarge { - /// 当前请求快照中的解压输出上限。 + /// 当前请求快照中的解压输出上限 limit_bytes: usize, }, - /// 请求使用了网关不支持的 `Content-Encoding`。 + /// 请求使用了网关不支持的 `Content-Encoding` #[error("request content-encoding `{encoding}` is not supported")] UnsupportedContentEncoding { - /// Content-Encoding 标识。 + /// Content-Encoding 标识 encoding: String, }, - /// 顶层不是 object。 + /// 顶层不是 object #[error("request body must be a JSON object")] ExpectedObject, - /// 缺少必填字段。 + /// 缺少必填字段 #[error("required field `{field}` is missing")] MissingField { - /// 字段路径。 + /// 字段路径 field: String, }, - /// 字段类型错误。 + /// 字段类型错误 #[error("field `{field}` has an invalid type; expected {expected}")] InvalidType { - /// 字段路径。 + /// 字段路径 field: String, - /// 安全的期望类型。 + /// 安全的期望类型 expected: &'static str, }, - /// 字段为空。 + /// 字段为空 #[error("field `{field}` must not be empty")] EmptyField { - /// 字段路径。 + /// 字段路径 field: String, }, - /// 字段值不满足稳定约束。 + /// 字段值不满足稳定约束 #[error("field `{field}` has an invalid value")] InvalidValue { - /// 字段路径;不保存原始值。 + /// 字段路径;不保存原始值 field: String, }, - /// 未知字段。 + /// 未知字段 #[error("unknown field `{field}`")] UnknownField { - /// 字段路径;不保存字段值。 + /// 字段路径;不保存字段值 field: String, }, - /// 已知但尚未实现的语义。 + /// 已知但尚未实现的语义 #[error("field `{field}` is not supported by this gateway")] UnsupportedField { - /// 字段路径。 + /// 字段路径 field: String, }, - /// Core operation 拒绝规范化后的字段。 + /// Core operation 拒绝规范化后的字段 #[error("field `{field}` violates the canonical operation contract")] CanonicalContract { - /// 字段路径。 + /// 字段路径 field: String, }, } impl RequestDecodeError { - /// 转换为 OpenAI 风格且不泄露正文的错误 payload。 + /// 转换为 OpenAI 风格且不泄露正文的错误 payload #[must_use] pub fn protocol_body(&self) -> ProtocolErrorBody { let (code, message, param) = match self { @@ -170,22 +170,22 @@ impl RequestDecodeError { } } -/// OpenAI Responses 原生 wire 转发错误。 +/// OpenAI Responses 原生 wire 转发错误 #[derive(Debug, Clone, PartialEq, Eq, Error)] pub enum ResponseEncodeError { - /// 协议原生流缺少可返回的终态 response object。 + /// 协议原生流缺少可返回的终态 response object #[error("responses wire stream has no terminal response")] MissingWireTerminal, - /// 缺少完整终态输出,已交付的完成项也存在缺项或冲突,不能伪造完整结果。 + /// 缺少完整终态输出,已交付的完成项也存在缺项或冲突,不能伪造完整结果 #[error("responses wire stream has inconsistent output items")] InvalidOutputItems, - /// 终态 response object 无法序列化为完整 HTTP JSON 响应。 + /// 终态 response object 无法序列化为完整 HTTP JSON 响应 #[error("responses wire terminal serialization failed")] Serialization, } impl ResponseEncodeError { - /// 转换为不包含生成内容的 OpenAI 风格错误 payload。 + /// 转换为不包含生成内容的 OpenAI 风格错误 payload #[must_use] pub fn protocol_body(&self) -> ProtocolErrorBody { ProtocolErrorBody { diff --git a/backend/crates/gateway-api/src/openai/responses/http.rs b/backend/crates/gateway-api/src/openai/responses/http.rs index 3e9b4cea0..72470bc37 100644 --- a/backend/crates/gateway-api/src/openai/responses/http.rs +++ b/backend/crates/gateway-api/src/openai/responses/http.rs @@ -1,4 +1,4 @@ -//! OpenAI Responses HTTP 与 SSE adapter。 +//! OpenAI Responses HTTP 与 SSE adapter use std::collections::VecDeque; use std::future::Future; @@ -55,12 +55,12 @@ use super::{ const OPENAI_PROTOCOL: &str = "openai"; const AUTHORIZATION_RECHECK_INTERVAL: Duration = Duration::from_secs(1); -/// 管理页面模型桥在等待首帧和交付响应期间复核不可变插件目标。 +/// 管理页面模型桥在等待首帧和交付响应期间复核不可变插件目标 pub(crate) trait ResponseAuthorization: Send + Sync { fn authorize(&self) -> BoxFuture<'_, Result<(), GatewayError>>; } -/// `POST /v1/responses`。 +/// `POST /v1/responses` pub(crate) async fn responses( State(state): State, connect_info: Option>>, @@ -104,7 +104,7 @@ pub(crate) async fn responses( .await } -/// 两个 Responses 入口共享解码后的 HTTP 交付上下文,不携带认证策略。 +/// 两个 Responses 入口共享解码后的 HTTP 交付上下文,不携带认证策略 pub(crate) struct ResponsesHttpRequest { pub peer_address: Option, pub ingress_id: Option>, @@ -140,7 +140,7 @@ pub(crate) async fn execute_prepared_responses( let started_request_id = Arc::new(OnceLock::new()); let terminal_request_id = Arc::clone(&started_request_id); let mut middleware_headers = headers.clone(); - // 中间件正文已经由入口按上限解压;传输编码和旧长度不能与改写后的正文混用。 + // 中间件正文已经由入口按上限解压;传输编码和旧长度不能与改写后的正文混用 middleware_headers.remove(CONTENT_ENCODING); middleware_headers.remove(CONTENT_LENGTH); let input = RequestInput { @@ -363,7 +363,7 @@ impl MiddlewareBody for AuthorizedResponseBody { } } -/// 从 socket 与标准转发头提取旧 Usage 页面使用的诊断事实。 +/// 从 socket 与标准转发头提取旧 Usage 页面使用的诊断事实 pub(crate) fn request_client_context( headers: &HeaderMap, peer_address: Option, @@ -503,7 +503,7 @@ async fn streaming_execution_middleware_response( )) } -/// 直接驱动测试 session 的非流式交付;生产入口通过同一 MiddlewareResponse 路径。 +/// 直接驱动测试 session 的非流式交付;生产入口通过同一 MiddlewareResponse 路径 pub async fn collect_execution_response(session: Box) -> Response { match buffered_execution_middleware_response(session, ResponseValidationFacts::default()).await { @@ -512,7 +512,7 @@ pub async fn collect_execution_response(session: Box) -> R } } -/// 直接驱动测试 session 的 SSE 交付;生产入口通过同一 MiddlewareResponse 路径。 +/// 直接驱动测试 session 的 SSE 交付;生产入口通过同一 MiddlewareResponse 路径 pub async fn stream_execution_response( session: Box, connection_guard: Option>, @@ -535,7 +535,7 @@ fn validated_middleware_response( streaming: bool, ) -> MiddlewareResponse { if !(200..300).contains(&response.status_code()) { - // 非成功响应沿用通用 SinglePayload 边界,不能套用成功 Responses 终态状态机。 + // 非成功响应沿用通用 SinglePayload 边界,不能套用成功 Responses 终态状态机 return response; } let (protocol, status, headers, body, envelope) = response.into_parts(); @@ -726,7 +726,7 @@ fn engine_error_response_with_headers( .map(move |value| (name, value)) }) .collect::>(); - // 失败后仍须交付已采集的 turn state;只隔离 opening 身份,不丢弃会话状态。 + // 失败后仍须交付已采集的 turn state;只隔离 opening 身份,不丢弃会话状态 let mut response = apply_provider_response_headers(response, response_headers); response.headers_mut().remove("x-request-id"); response.headers_mut().remove("x-oai-request-id"); @@ -837,7 +837,7 @@ impl StreamingExecutionBody { while self.pending.is_empty() && !self.output_finished { if self.awaiting_terminal_eof { if !self.committed { - // 终态 wire 不能在首字节前被中间件整批移除。 + // 终态 wire 不能在首字节前被中间件整批移除 return Err(MiddlewareError::InvalidState); } self.verify_terminal_eof().await?; @@ -907,7 +907,7 @@ impl StreamingExecutionBody { } continue; } - // 已提交的流在确认 Core 终结后才交付成功终态,不能先输出 completed 再报结算失败。 + // 已提交的流在确认 Core 终结后才交付成功终态,不能先输出 completed 再报结算失败 if self.awaiting_terminal_eof && self.committed { self.verify_terminal_eof().await?; encoded.push_back(done_frame()); diff --git a/backend/crates/gateway-api/src/openai/responses/mod.rs b/backend/crates/gateway-api/src/openai/responses/mod.rs index c04ca9e85..d19b1c9d1 100644 --- a/backend/crates/gateway-api/src/openai/responses/mod.rs +++ b/backend/crates/gateway-api/src/openai/responses/mod.rs @@ -1,4 +1,4 @@ -//! OpenAI Responses 的透明 wire adapter 与 Core canonical facts 投影。 +//! OpenAI Responses 的透明 wire adapter 与 Core canonical facts 投影 mod error; mod http; diff --git a/backend/crates/gateway-api/src/openai/responses/request.rs b/backend/crates/gateway-api/src/openai/responses/request.rs index 46e235f70..1969009d4 100644 --- a/backend/crates/gateway-api/src/openai/responses/request.rs +++ b/backend/crates/gateway-api/src/openai/responses/request.rs @@ -1,4 +1,4 @@ -//! OpenAI Responses JSON 到 Core 路由事实与不透明 wire payload 的单一解码边界。 +//! OpenAI Responses JSON 到 Core 路由事实与不透明 wire payload 的单一解码边界 use std::{borrow::Cow, fmt, net::IpAddr}; @@ -20,12 +20,12 @@ const CODEX_TURN_METADATA_KEY: &str = "x-codex-turn-metadata"; const PASSTHROUGH_HEADERS_CONTEXT_KEY: &str = "opaque_request_headers"; const DOWNSTREAM_WEBSOCKET_CONNECTION_ID_CONTEXT_KEY: &str = "downstream_websocket_connection_id"; -/// Responses 请求进入共享解码内核时的下游传输来源。 +/// Responses 请求进入共享解码内核时的下游传输来源 #[derive(Clone, Copy)] pub(super) enum RequestDecodeSource { - /// 单次 HTTP 请求,请求头与正文属于同一请求。 + /// 单次 HTTP 请求,请求头与正文属于同一请求 Http, - /// 复用连接上的 WebSocket `response.create` 帧。 + /// 复用连接上的 WebSocket `response.create` 帧 WebSocketFrame, } @@ -53,7 +53,7 @@ pub struct OpenAiRequestHeaders { } impl OpenAiRequestHeaders { - /// 提取 OpenAI/Codex 连接级请求头上下文。 + /// 提取 OpenAI/Codex 连接级请求头上下文 #[must_use] pub fn from_headers(headers: &HeaderMap) -> Self { Self { @@ -79,7 +79,7 @@ impl OpenAiRequestHeaders { } } - /// 绑定代理为当前下游 WebSocket 分配的连接身份。 + /// 绑定代理为当前下游 WebSocket 分配的连接身份 #[must_use] pub(super) fn with_downstream_websocket_connection_id( mut self, @@ -95,7 +95,7 @@ impl OpenAiRequestHeaders { } } - /// 独立端点只消费会话身份与 turn metadata,不携带 Responses 的连接状态。 + /// 独立端点只消费会话身份与 turn metadata,不携带 Responses 的连接状态 pub(crate) fn session_context(&self) -> Map { let mut context = Map::new(); insert_protocol_context(&mut context, "session_id", self.session_id.as_ref()); @@ -172,17 +172,17 @@ fn insert_protocol_context(context: &mut Map, field: &str, value: } } -/// 客户端声明的 continuation 意图。 +/// 客户端声明的 continuation 意图 #[derive(Clone, PartialEq, Eq)] pub enum ContinuationIntent { - /// 不使用先前响应。 + /// 不使用先前响应 None, - /// 使用当前调用方可见的 OpenAI response ID。 + /// 使用当前调用方可见的 OpenAI response ID PreviousResponseId(String), } impl ContinuationIntent { - /// 返回待 history owner 解析的 response ID。 + /// 返回待 history owner 解析的 response ID #[must_use] pub fn previous_response_id(&self) -> Option<&str> { match self { @@ -201,7 +201,7 @@ impl fmt::Debug for ContinuationIntent { } } -/// Handler、Router 和 history owner 使用的请求元数据。 +/// Handler、Router 和 history owner 使用的请求元数据 #[derive(Clone, PartialEq, Eq)] pub struct ResponsesRequestMetadata { requested_model: String, @@ -213,37 +213,37 @@ pub struct ResponsesRequestMetadata { } impl ResponsesRequestMetadata { - /// 返回客户端原始模型名。 + /// 返回客户端原始模型名 #[must_use] pub fn requested_model(&self) -> &str { &self.requested_model } - /// 返回客户端是否请求 SSE。 + /// 返回客户端是否请求 SSE #[must_use] pub const fn stream(&self) -> bool { self.stream } - /// 返回客户端 storage intent。 + /// 返回客户端 storage intent #[must_use] pub const fn store(&self) -> bool { self.store } - /// 返回 continuation intent。 + /// 返回 continuation intent #[must_use] pub const fn continuation(&self) -> &ContinuationIntent { &self.continuation } - /// 返回从 HTTP 连接边界解析出的客户端地址。 + /// 返回从 HTTP 连接边界解析出的客户端地址 #[must_use] pub const fn client_ip(&self) -> Option { self.client_ip } - /// 返回经过 UTF-8 校验和空白归一化的 User-Agent。 + /// 返回经过 UTF-8 校验和空白归一化的 User-Agent #[must_use] pub fn user_agent(&self) -> Option<&str> { self.user_agent.as_deref() @@ -264,7 +264,7 @@ impl fmt::Debug for ResponsesRequestMetadata { } } -/// 一次成功解码的 Responses 请求。 +/// 一次成功解码的 Responses 请求 #[derive(Clone, PartialEq)] pub struct DecodedResponsesRequest { operation: Operation, @@ -280,32 +280,32 @@ impl DecodedResponsesRequest { Ok(self) } - /// 附着当前 WebSocket 连接保存的 Provider 私有上一轮状态。 + /// 附着当前 WebSocket 连接保存的 Provider 私有上一轮状态 #[must_use] pub fn with_provider_session_state(mut self, state: ProviderSessionState) -> Self { self.operation = self.operation.with_provider_session_state(state); self } - /// 返回协议无关 operation。 + /// 返回协议无关 operation #[must_use] pub const fn operation(&self) -> &Operation { &self.operation } - /// 返回 handler 元数据。 + /// 返回 handler 元数据 #[must_use] pub const fn metadata(&self) -> &ResponsesRequestMetadata { &self.metadata } - /// 拆分为 operation 与元数据。 + /// 拆分为 operation 与元数据 #[must_use] pub fn into_parts(self) -> (Operation, ResponsesRequestMetadata) { (self.operation, self.metadata) } - /// 附着只由 HTTP/WebSocket 连接边界提供的诊断事实。 + /// 附着只由 HTTP/WebSocket 连接边界提供的诊断事实 #[must_use] pub fn with_client_context( mut self, @@ -328,11 +328,11 @@ impl fmt::Debug for DecodedResponsesRequest { } } -/// 使用下游 OpenAI/Codex 请求头解码 `POST /v1/responses`。 +/// 使用下游 OpenAI/Codex 请求头解码 `POST /v1/responses` /// /// # Errors /// -/// 编码不支持、压缩正文损坏或超限、JSON 非法,或路由字段无效时返回安全错误。 +/// 编码不支持、压缩正文损坏或超限、JSON 非法,或路由字段无效时返回安全错误 pub fn decode_request_with_headers( body: &[u8], headers: &HeaderMap, @@ -342,10 +342,10 @@ pub fn decode_request_with_headers( decode_request_inner(&body, &OpenAiRequestHeaders::from_headers(headers)) } -/// 解码请求并返回已经解除 HTTP content encoding 的中间件正文。 +/// 解码请求并返回已经解除 HTTP content encoding 的中间件正文 /// /// 外层中间件只接收有界的协议正文;terminal 会用中间件返回的正文重新解码, -/// 因而不能把压缩字节与已经移除的传输编码语义混用。 +/// 因而不能把压缩字节与已经移除的传输编码语义混用 pub(crate) fn decode_request_with_body( body: &[u8], headers: &HeaderMap, @@ -356,13 +356,14 @@ pub(crate) fn decode_request_with_body( Ok((decoded, Bytes::copy_from_slice(&body))) } -/// zstd 回溯窗口独立于输出上限,调整设置不能放大解码器内部窗口分配。 +/// zstd 回溯窗口独立于输出上限,调整设置不能放大解码器内部窗口分配 const MAX_ZSTD_WINDOW_LOG: u32 = 26; -/// 按 `Content-Encoding` 解压下游请求体。 +/// 按 `Content-Encoding` 解压下游请求体 /// /// 未压缩与 `identity` 借用原始正文;压缩正文在读取过程中限制展开大小, -/// 避免先完整分配再检查。只接受单一编码,重复头和叠加编码不能只解释第一项。 +/// 避免先完整分配再检查 +/// 只接受单一编码,重复头和叠加编码不能只解释第一项 fn decompress_request_body<'a>( body: &'a [u8], headers: &HeaderMap, @@ -391,7 +392,7 @@ fn decompress_request_body<'a>( "zstd" => { let mut decoder = zstd::stream::read::Decoder::with_buffer(body) .map_err(|_| RequestDecodeError::MalformedJson)?; - // 输出有界之外,也限制压缩帧声明的回溯窗口,防止解码器内部过量分配。 + // 输出有界之外,也限制压缩帧声明的回溯窗口,防止解码器内部过量分配 decoder .window_log_max(MAX_ZSTD_WINDOW_LOG) .map_err(|_| RequestDecodeError::MalformedJson)?; @@ -424,7 +425,7 @@ fn read_bounded( }); } // 默认 Vec 扩容和 read_to_end 的 EOF 探测可能突破输出上限; - // 容量增长也受相同边界约束,越界探测只使用栈上分块缓冲区。 + // 容量增长也受相同边界约束,越界探测只使用栈上分块缓冲区 let required = decoded.len() + read; if required > decoded.capacity() { let capacity = decoded @@ -450,13 +451,13 @@ pub(super) fn decode_request_inner( decode_request_object(object, request_headers, RequestDecodeSource::Http) } -/// 解码已解析的顶层 object;按下游传输来源恢复连接级协议上下文。 +/// 解码已解析的顶层 object;按下游传输来源恢复连接级协议上下文 pub(super) fn decode_request_object( mut object: Map, request_headers: &OpenAiRequestHeaders, source: RequestDecodeSource, ) -> Result { - // 仅消费已识别的本地 transport 开关;未知同名值保留给未来上游协议。 + // 仅消费已识别的本地 transport 开关;未知同名值保留给未来上游协议 let use_websocket = object.get("use_websocket").and_then(Value::as_bool); if use_websocket.is_some() { object.remove("use_websocket"); @@ -469,7 +470,7 @@ pub(super) fn decode_request_object( }); } let model = model.to_owned(); - // 缺省值只决定下游交付方式,不补写正文或改变 Provider 的上游流式执行。 + // 缺省值只决定下游交付方式,不补写正文或改变 Provider 的上游流式执行 let stream = match object.get("stream") { Some(value) => value.as_bool().unwrap_or(true), None => matches!(source, RequestDecodeSource::WebSocketFrame), @@ -574,10 +575,10 @@ fn passthrough_header_name(name: &str, connection_headers: &[String]) -> bool { !matches!( name, - // 下游鉴权和账号 cookie 绝不能成为上游账号身份。 + // 下游鉴权和账号 cookie 绝不能成为上游账号身份 "authorization" | "x-api-key" - // Codex 的服务端托管认证标记只用于客户端能力判断,不代表上游身份。 + // Codex 的服务端托管认证标记只用于客户端能力判断,不代表上游身份 | "x-openai-actor-authorization" | "cookie" | "cookie2" @@ -588,12 +589,12 @@ fn passthrough_header_name(name: &str, connection_headers: &[String]) -> bool { | "x-openai-account-routing-override" | "x-openai-fedramp" // 上游指纹必须由运行时画像统一生成,客户端 originator/User-Agent/version - // 不能作为不透明头透传覆盖,避免不同下游客户端暴露不一致的设备指纹。 + // 不能作为不透明头透传覆盖,避免不同下游客户端暴露不一致的设备指纹 | "originator" | "user-agent" | "version" // 设备 attestation/integrity 头只能由官方客户端或网关自身生成, - // 客户端注入的 x-oai-attestation / X-OAI-IS 不得透传上游。 + // 客户端注入的 x-oai-attestation / X-OAI-IS 不得透传上游 | "x-oai-attestation" | "x-oai-is" | "x-oai-is-update" diff --git a/backend/crates/gateway-api/src/openai/responses/response.rs b/backend/crates/gateway-api/src/openai/responses/response.rs index c0b93b451..1a73431ff 100644 --- a/backend/crates/gateway-api/src/openai/responses/response.rs +++ b/backend/crates/gateway-api/src/openai/responses/response.rs @@ -1,4 +1,4 @@ -//! Provider OpenAI Responses wire 到客户端 transport 的透明转发边界。 +//! Provider OpenAI Responses wire 到客户端 transport 的透明转发边界 use std::collections::BTreeMap; @@ -14,11 +14,12 @@ use crate::openai::error::capacity_error_for_client; const OPENAI_PROTOCOL: &str = "openai"; -/// OpenAI Responses wire 的转发器。 +/// OpenAI Responses wire 的转发器 /// -/// OpenAI Provider 与 xAI adapter 都必须交付 OpenAI wire。wire 是客户端交付 +/// OpenAI Provider 与 xAI adapter 都必须交付 OpenAI wire +/// wire 是客户端交付 /// 与终态判断的事实来源;canonical facts 只在 wire 暂未携带 ID 时提供旁路观测, -/// 不得反过来拒绝或改写上游事件;客户端错误兼容投影只发生在编码出口。 +/// 不得反过来拒绝或改写上游事件;客户端错误兼容投影只发生在编码出口 #[derive(Debug, Default)] pub struct OpenAiResponsesEncoder { response_id: Option, @@ -28,7 +29,7 @@ pub struct OpenAiResponsesEncoder { } impl OpenAiResponsesEncoder { - /// 创建响应 wire 转发器。 + /// 创建响应 wire 转发器 #[must_use] pub const fn new() -> Self { Self { @@ -39,7 +40,7 @@ impl OpenAiResponsesEncoder { } } - /// 消费一个 Provider event,并返回 SSE frames。 + /// 消费一个 Provider event,并返回 SSE frames pub fn push_sse(&mut self, event: &ProviderEvent) -> Vec { self.observe_canonical_identity(event); let Some(wire) = openai_wire(event) else { @@ -48,8 +49,8 @@ impl OpenAiResponsesEncoder { self.observe_wire(wire); let projected = client_failure_payload(wire); let data = projected.as_ref().unwrap_or_else(|| wire.data()); - // 当前 Codex 不消费 Responses `error` event,会在 EOF 时丢失失败原因。 - // 在客户端 SSE 边界统一投影成它能识别的 `response.failed`。 + // 当前 Codex 不消费 Responses `error` event,会在 EOF 时丢失失败原因 + // 在客户端 SSE 边界统一投影成它能识别的 `response.failed` if effective_event_type(wire) == Some("error") && let Some(data) = response_failed_sse_data_from_error_event( self.response_snapshot.as_ref(), @@ -77,7 +78,7 @@ impl OpenAiResponsesEncoder { ))] } - /// 消费一个 Provider event,并返回 WebSocket JSON messages。 + /// 消费一个 Provider event,并返回 WebSocket JSON messages pub fn push_websocket(&mut self, event: &ProviderEvent) -> Vec { self.observe_canonical_identity(event); let Some(wire) = openai_wire(event).filter(|wire| wire.has_json_data()) else { @@ -88,9 +89,11 @@ impl OpenAiResponsesEncoder { let data = projected.as_ref().unwrap_or_else(|| wire.data()); // WS 客户端只对它无法消费的裸 `error` 帧做投影:codex 的 WS 端点 // 会静默忽略缺少 status 且不含内置可重试错误码的 `error` 帧,客户 - // 端只能空等到 idle 超时。投影成 `response.failed`(消息 JSON 自带 - // type 字段),让 codex 按错误码处理。可消费形状(带非 2xx status - // 或特殊错误码)保留 envelope,容量码已由共享客户端投影处理。 + // 端只能空等到 idle 超时 + // 投影成 `response.failed`(消息 JSON 自带 + // type 字段),让 codex 按错误码处理 + // 可消费形状(带非 2xx status + // 或特殊错误码)保留 envelope,容量码已由共享客户端投影处理 if effective_event_type(wire) == Some("error") && !ws_client_consumable_error(data) && let Some(data) = response_failed_sse_data_from_error_event( @@ -104,29 +107,29 @@ impl OpenAiResponsesEncoder { vec![data.to_string()] } - /// 返回是否已经看到客户端可见的 wire 终态。 + /// 返回是否已经看到客户端可见的 wire 终态 #[must_use] pub fn is_completed(&self) -> bool { self.wire_terminal.is_some() } - /// 返回是否已把 Provider 原生失败 event 交付给客户端。 + /// 返回是否已把 Provider 原生失败 event 交付给客户端 #[must_use] pub const fn has_wire_failure(&self) -> bool { self.wire_failure } - /// 返回 Core 已观察到的客户端可见 Provider 原生响应 ID。 + /// 返回 Core 已观察到的客户端可见 Provider 原生响应 ID #[must_use] pub fn response_id(&self) -> Option<&str> { self.response_id.as_deref() } - /// 校验完整响应并返回原生终态 response object。 + /// 校验完整响应并返回原生终态 response object /// /// # Errors /// - /// 缺少可转换为非流式响应的 wire 终态时返回错误。 + /// 缺少可转换为非流式响应的 wire 终态时返回错误 pub fn finish(self) -> Result { self.wire_terminal .ok_or(ResponseEncodeError::MissingWireTerminal) @@ -168,7 +171,7 @@ impl OpenAiResponsesEncoder { } } -/// 仅在非流式出口聚合同一响应的完成项,不让流式转发常驻保存输出内容。 +/// 仅在非流式出口聚合同一响应的完成项,不让流式转发常驻保存输出内容 pub(super) fn collect_response(events: &[ProviderEvent]) -> Result { let mut encoder = OpenAiResponsesEncoder::new(); let mut response_id = None; @@ -203,8 +206,8 @@ pub(super) fn collect_response(events: &[ProviderEvent]) -> Result Option { } } -/// 判断 WS 上游的 `error` 帧是否已经是客户端可直接消费的形状。 +/// 判断 WS 上游的 `error` 帧是否已经是客户端可直接消费的形状 /// /// codex 的 WS 端点只消费两类 `error` 帧:带非 2xx HTTP status 的包装错误, /// 以及连接数上限 / previous_response_not_found 这类内置可重试错误码;其余 -/// 帧(包括 status 为 2xx 的矛盾形状)会被静默忽略。可消费的帧原样透传, -/// 不可消费的才在 WS 边界投影成 `response.failed`。 +/// 帧(包括 status 为 2xx 的矛盾形状)会被静默忽略 +/// 可消费的帧原样透传, +/// 不可消费的才在 WS 边界投影成 `response.failed` fn ws_client_consumable_error(data: &Value) -> bool { if data .get("error") diff --git a/backend/crates/gateway-api/src/openai/responses/validation.rs b/backend/crates/gateway-api/src/openai/responses/validation.rs index 6c7f4aca4..936e1e398 100644 --- a/backend/crates/gateway-api/src/openai/responses/validation.rs +++ b/backend/crates/gateway-api/src/openai/responses/validation.rs @@ -1,4 +1,4 @@ -//! 插件改写后的 OpenAI Responses 投递协议复核。 +//! 插件改写后的 OpenAI Responses 投递协议复核 use std::{ collections::{BTreeMap, BTreeSet}, @@ -12,7 +12,7 @@ use serde_json::{Map, Value}; const MAX_TRACKED_OUTPUT_ITEMS: usize = 4_096; const MAX_IDENTIFIER_BYTES: usize = 1_024; -/// 插件输出不能安全投递为 OpenAI Responses 协议。 +/// 插件输出不能安全投递为 OpenAI Responses 协议 #[derive(Debug, Clone, Copy)] pub(super) struct ResponseValidationError; @@ -31,7 +31,7 @@ struct ExpectedResponseFacts { invalid: bool, } -/// 仅保存最终协议复核所需的响应 ID 与终态类别,不缓存流正文。 +/// 仅保存最终协议复核所需的响应 ID 与终态类别,不缓存流正文 #[derive(Clone, Default)] pub(super) struct ResponseValidationFacts(Arc>); @@ -145,7 +145,7 @@ impl ExpectedResponseFacts { } } -/// 只复核中间件确实改写过的完整 JSON;未经改写的原生快路保持原行为。 +/// 只复核中间件确实改写过的完整 JSON;未经改写的原生快路保持原行为 pub(super) fn validate_buffered_response( facts: &ResponseValidationFacts, modified: &[u8], @@ -171,10 +171,10 @@ pub(super) fn validate_buffered_response( validate_terminal_response(&modified, response_id, terminal) } -/// 持有实际已交付给客户端的 Responses 流状态。 +/// 持有实际已交付给客户端的 Responses 流状态 /// /// 原始前缀只旁路构建状态;直到插件首次改写或丢弃正文才启用严格拒绝,避免给 -/// 无策略原生热路增加新的兼容性条件。 +/// 无策略原生热路增加新的兼容性条件 #[derive(Debug, Default)] pub(super) struct ResponsesDeliveryValidator { active: bool, @@ -183,7 +183,7 @@ pub(super) struct ResponsesDeliveryValidator { } impl ResponsesDeliveryValidator { - /// 复核实际交付帧;只有宿主标记为已改写时才激活严格模式。 + /// 复核实际交付帧;只有宿主标记为已改写时才激活严格模式 pub(super) fn validate_frame( &mut self, delivered: &[u8], @@ -193,7 +193,7 @@ impl ResponsesDeliveryValidator { self.validate_parsed(ParsedFrame::parse(delivered), transformed, facts) } - /// WebSocket JSON message 与 SSE 共用同一响应顺序和关联校验。 + /// WebSocket JSON message 与 SSE 共用同一响应顺序和关联校验 pub(super) fn validate_websocket_frame( &mut self, delivered: &[u8], diff --git a/backend/crates/gateway-api/src/openai/responses/websocket/connection/middleware.rs b/backend/crates/gateway-api/src/openai/responses/websocket/connection/middleware.rs index c0e855034..4f2f64dae 100644 --- a/backend/crates/gateway-api/src/openai/responses/websocket/connection/middleware.rs +++ b/backend/crates/gateway-api/src/openai/responses/websocket/connection/middleware.rs @@ -1,4 +1,4 @@ -//! 插件消息组合在 transport 锁之外执行,主动发送复用唯一 writer。 +//! 插件消息组合在 transport 锁之外执行,主动发送复用唯一 writer use super::{ConnectionWriteError, PumpContext, write_message}; pub(super) use crate::middleware::websocket::CancelOnDrop; use crate::middleware::websocket::{from_core, into_core}; @@ -23,13 +23,13 @@ where } } pub(super) async fn write(&self, message: Message) -> Result<(), ConnectionWriteError> { - // 预算包含排队与真实写入,取消不等待其他调用释放锁。 + // 预算包含排队与真实写入,取消不等待其他调用释放锁 tokio::select! { biased; () = self.context.cancellation.cancelled() => Err(ConnectionWriteError::Closed), result = tokio::time::timeout(self.context.config.write_timeout, async { let mut socket = self.socket.lock().await; - // 发送中断后不能继续复用可能含半写入帧的连接。 + // 发送中断后不能继续复用可能含半写入帧的连接 let mut in_flight = CancelOnDrop(Some(self.context.cancellation.clone())); let result = write_message(&mut *socket, message, &self.context).await; if result.is_ok() { in_flight.0.take(); } diff --git a/backend/crates/gateway-api/src/openai/responses/websocket/connection/mod.rs b/backend/crates/gateway-api/src/openai/responses/websocket/connection/mod.rs index 486c78d93..49e330e7d 100644 --- a/backend/crates/gateway-api/src/openai/responses/websocket/connection/mod.rs +++ b/backend/crates/gateway-api/src/openai/responses/websocket/connection/mod.rs @@ -1,4 +1,4 @@ -//! 下游客户端 WebSocket 的单 owner pump 与有界收发边界。 +//! 下游客户端 WebSocket 的单 owner pump 与有界收发边界 mod middleware; @@ -32,7 +32,7 @@ const DOWNSTREAM_WRITE_TIMEOUT: Duration = Duration::from_secs(5 * 60); const CONNECTION_MAX_AGE: Duration = Duration::from_secs(60 * 60); const DOWNSTREAM_PING_INTERVAL: Duration = Duration::from_secs(25); -/// WebSocket pump 的写入与生命周期预算。 +/// WebSocket pump 的写入与生命周期预算 #[derive(Clone, Copy)] pub struct ConnectionConfig { write_timeout: Duration, @@ -40,14 +40,14 @@ pub struct ConnectionConfig { } impl ConnectionConfig { - /// 生产环境固定预算。 + /// 生产环境固定预算 pub const PRODUCTION: Self = Self { write_timeout: DOWNSTREAM_WRITE_TIMEOUT, max_age: CONNECTION_MAX_AGE, }; } -/// 业务层可观察的客户端输入;Ping/Pong 始终由 pump 消费。 +/// 业务层可观察的客户端输入;Ping/Pong 始终由 pump 消费 pub enum ConnectionEvent { Text(String), Binary, @@ -55,7 +55,7 @@ pub enum ConnectionEvent { Exited(PumpExitReason), } -/// 接收队列与活动期间暂存的请求共用容量;移动帧不释放它占用的名额。 +/// 接收队列与活动期间暂存的请求共用容量;移动帧不释放它占用的名额 pub(super) struct PendingConnectionEvent { pub(super) event: ConnectionEvent, _permit: Option, @@ -70,7 +70,7 @@ impl PendingConnectionEvent { } } -/// 下游写入阶段;名称刻意使用 write,而不是暗示客户端已消费的 delivery。 +/// 下游写入阶段;名称刻意使用 write,而不是暗示客户端已消费的 delivery #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FramePhase { Metadata, @@ -110,7 +110,7 @@ impl FramePhase { } } -/// 一次下游写入的请求归属与协议阶段。 +/// 一次下游写入的请求归属与协议阶段 #[derive(Clone)] pub struct WriteContext { request_id: Option>, @@ -118,7 +118,7 @@ pub struct WriteContext { } impl WriteContext { - /// 创建归属于某个请求的写入上下文。 + /// 创建归属于某个请求的写入上下文 #[must_use] pub fn request(request_id: &Arc, phase: FramePhase) -> Self { Self { @@ -127,7 +127,7 @@ impl WriteContext { } } - /// 创建连接级写入上下文。 + /// 创建连接级写入上下文 #[must_use] pub const fn connection(phase: FramePhase) -> Self { Self { @@ -137,7 +137,7 @@ impl WriteContext { } } -/// WebSocket pump 停止的稳定原因。 +/// WebSocket pump 停止的稳定原因 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PumpExitReason { ClientClose, @@ -173,7 +173,7 @@ impl PumpExitReason { } } -/// 下游 WebSocket 写入失败。 +/// 下游 WebSocket 写入失败 #[derive(Debug, Error)] pub enum ConnectionWriteError { #[error("downstream WebSocket pump is closed")] @@ -184,7 +184,7 @@ pub enum ConnectionWriteError { Transport { message: String }, } -/// 只有实际 transport 写入才记为 Written;插件丢弃消息不伪造写入成功。 +/// 只有实际 transport 写入才记为 Written;插件丢弃消息不伪造写入成功 #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum WriteOutcome { Written, @@ -237,7 +237,7 @@ impl ConnectionStats { } } -/// 协调层持有的单 owner WebSocket 连接句柄。 +/// 协调层持有的单 owner WebSocket 连接句柄 pub struct ResponsesWebSocketConnection { connection_id: Arc, opened_at: Instant, @@ -274,7 +274,7 @@ impl ResponsesWebSocketConnection { &self.connection_id } - /// 返回连接是否已经达到生命周期上限。 + /// 返回连接是否已经达到生命周期上限 #[must_use] pub fn is_expired(&self) -> bool { self.expired.load(Ordering::Acquire) @@ -284,7 +284,7 @@ impl ResponsesWebSocketConnection { self.opened_at.elapsed() } - /// 等待下一个需要业务层处理的客户端事件。 + /// 等待下一个需要业务层处理的客户端事件 pub async fn next_event(&mut self) -> Option { if self.exit_reason.is_none() { match self.exited.try_recv() { @@ -306,7 +306,7 @@ impl ResponsesWebSocketConnection { self.next_active_event().await.map(|event| event.event) } - /// 活动响应期间只消费新到的帧,已排队的下一轮请求继续保持串行。 + /// 活动响应期间只消费新到的帧,已排队的下一轮请求继续保持串行 pub(super) async fn next_active_event(&mut self) -> Option { if let Some(reason) = self.exit_reason { return Some(PendingConnectionEvent::exited(reason)); @@ -334,7 +334,7 @@ impl ResponsesWebSocketConnection { self.deferred.push_back(event); } - /// 等待连接退出,不消费留给后续串行请求的业务帧。 + /// 等待连接退出,不消费留给后续串行请求的业务帧 pub async fn wait_for_exit(&mut self) -> PumpExitReason { if let Some(reason) = self.exit_reason { return reason; @@ -346,11 +346,11 @@ impl ResponsesWebSocketConnection { reason } - /// 串行写入文本帧,并等待 pump 确认 transport 写入结果。 + /// 串行写入文本帧,并等待 pump 确认 transport 写入结果 /// /// # Errors /// - /// pump 已关闭、写入超时或 transport 失败时返回稳定错误。 + /// pump 已关闭、写入超时或 transport 失败时返回稳定错误 pub async fn send_text( &mut self, payload: String, @@ -524,7 +524,7 @@ impl Drop for ResponsesWebSocketConnection { } } -/// 为自动处理 Ping/Pong 的 WebSocket transport 启动单 owner pump。 +/// 为自动处理 Ping/Pong 的 WebSocket transport 启动单 owner pump pub fn spawn_connection( socket: S, connection_id: Arc, @@ -624,7 +624,7 @@ async fn run_pump( let (writer, mut reader) = socket.split(); let writer = Arc::new(middleware::Writer::new(writer, context.clone())); let sender: Arc = writer.clone(); - // 同一个受管任务同时驱动两侧;等待写入不能阻塞入站控制、关闭或取消。 + // 同一个受管任务同时驱动两侧;等待写入不能阻塞入站控制、关闭或取消 let reason = tokio::select! { biased; reason = read_connection(&mut reader, &incoming, sender.clone(), &context) => reason, @@ -667,7 +667,7 @@ where () = &mut deadline, if !deadline_elapsed => { deadline_elapsed = true; context.expired.store(true, Ordering::Release); - // 到期只阻止下一轮,当前响应仍由协调层按原有合同收尾。 + // 到期只阻止下一轮,当前响应仍由协调层按原有合同收尾 match emit_incoming(incoming, &event_slots, ConnectionEvent::Expired) { Ok(()) | Err(PumpExitReason::InboundOverload) => {} Err(reason) => return reason, @@ -687,7 +687,7 @@ where Some(Ok(Message::Text(payload))) => Some(ConnectionEvent::Text(payload.to_string())), Some(Ok(Message::Binary(_))) => Some(ConnectionEvent::Binary), Some(Ok(Message::Ping(_))) => { - // Axum/tungstenite 在继续读取时自动刷新 Pong,不能再手工发送一份。 + // Axum/tungstenite 在继续读取时自动刷新 Pong,不能再手工发送一份 context.stats.ping_received_count.fetch_add(1, Ordering::Relaxed); None } diff --git a/backend/crates/gateway-api/src/openai/responses/websocket/forward.rs b/backend/crates/gateway-api/src/openai/responses/websocket/forward.rs index a45eef2fc..685418a2f 100644 --- a/backend/crates/gateway-api/src/openai/responses/websocket/forward.rs +++ b/backend/crates/gateway-api/src/openai/responses/websocket/forward.rs @@ -1,4 +1,4 @@ -//! Responses WebSocket 洋葱响应体与串行 transport 交付。 +//! Responses WebSocket 洋葱响应体与串行 transport 交付 use std::collections::VecDeque; use std::sync::{Arc, Mutex}; @@ -97,7 +97,7 @@ pub(super) async fn execution_response( .iter() .map(|header| MiddlewareHeader::new(header.name(), header.value().clone())) .collect(); - // 失败也要保留会话到洋葱链返回,否则释放会话会取消仍在等待 next 的插件。 + // 失败也要保留会话到洋葱链返回,否则释放会话会取消仍在等待 next 的插件 return Ok(pending_execution_response( "openai".to_owned(), status, @@ -279,7 +279,7 @@ pub(super) async fn forward_response( fn detach_body(body: Box) { // 客户端已离线时不能让连接 handler 等待可能仍在结算的请求;close future - // 继续持有唯一正文和执行守卫,保证已启动的费用与租约清理不会被取消。 + // 继续持有唯一正文和执行守卫,保证已启动的费用与租约清理不会被取消 drop(tokio::spawn(body.close())); } @@ -303,7 +303,7 @@ async fn next_body_input( response_control: &ResponseControl, request_id: &Arc, ) -> BodyInput { - // 处理控制帧时继续持有同一个读取 future,不能取消正在加工正文的中间件。 + // 处理控制帧时继续持有同一个读取 future,不能取消正在加工正文的中间件 let frame = body.next_frame(); tokio::pin!(frame); loop { @@ -329,7 +329,7 @@ async fn next_body_input( } } ConnectionEvent::Expired => { - // 允许当前响应收尾,外层仍会在下一轮准入前关闭过期连接。 + // 允许当前响应收尾,外层仍会在下一轮准入前关闭过期连接 } ConnectionEvent::Binary => { connection.defer(event); diff --git a/backend/crates/gateway-api/src/openai/responses/websocket/mod.rs b/backend/crates/gateway-api/src/openai/responses/websocket/mod.rs index c407e33a2..20696965f 100644 --- a/backend/crates/gateway-api/src/openai/responses/websocket/mod.rs +++ b/backend/crates/gateway-api/src/openai/responses/websocket/mod.rs @@ -1,4 +1,4 @@ -//! OpenAI Responses WebSocket 的升级、连接与串行 session 编排。 +//! OpenAI Responses WebSocket 的升级、连接与串行 session 编排 pub mod connection; mod forward; @@ -55,7 +55,7 @@ pub use protocol::{ResponseCreateFrameError, decode_response_create_with_context const TEXT_FRAMES_ONLY: &str = "Responses WebSocket accepts text frames only"; const CONNECTION_LIMIT_CLOSE_REASON: &str = "Responses websocket connection limit reached"; -/// 将已认证的 `GET /v1/responses` 升级为 Responses WebSocket。 +/// 将已认证的 `GET /v1/responses` 升级为 Responses WebSocket pub(crate) async fn responses_websocket( State(state): State, connect_info: Option>>, @@ -86,14 +86,14 @@ pub(crate) async fn responses_websocket( ) } -/// 已鉴权 Responses WebSocket 升级边界。 +/// 已鉴权 Responses WebSocket 升级边界 #[derive(Clone)] pub(crate) struct ResponsesWebSocketAdapter { service: OpenAiService, } impl ResponsesWebSocketAdapter { - /// 绑定应用提供的唯一 OpenAI 客户端服务端口。 + /// 绑定应用提供的唯一 OpenAI 客户端服务端口 #[must_use] pub const fn new(service: OpenAiService) -> Self { Self { service } @@ -137,8 +137,8 @@ impl ResponsesWebSocketAdapter { }; let origin = crate::middleware::current(); websocket - // 覆盖 axum/tungstenite 的私有 64 MiB message 与 16 MiB frame 默认值。 - // Responses JSON 的协议可接受性由上游决定,代理不另设 wire 长度上限。 + // 覆盖 axum/tungstenite 的私有 64 MiB message 与 16 MiB frame 默认值 + // Responses JSON 的协议可接受性由上游决定,代理不另设 wire 长度上限 .max_message_size(usize::MAX) .max_frame_size(usize::MAX) .on_upgrade(move |socket| async move { @@ -238,7 +238,7 @@ async fn serve_responses_websocket(socket: WebSocket, session: ResponsesWebSocke continue; } }; - // deadline 与 Text 可能同时就绪;在任何上游执行开始前再次封住该竞争窗口。 + // deadline 与 Text 可能同时就绪;在任何上游执行开始前再次封住该竞争窗口 if connection.is_expired() { trace_rejected_request( &correlation_id, @@ -252,7 +252,7 @@ async fn serve_responses_websocket(socket: WebSocket, session: ResponsesWebSocke } let execution = service.execution(); let preparation = async { - // 握手的显式改写可继承,Key 策略与宿主设置仍在每轮执行前刷新。 + // 握手的显式改写可继承,Key 策略与宿主设置仍在每轮执行前刷新 let mut client = client.clone(); if let Some(settings) = client.request_settings() { let current = execution.request_settings().ok_or_else(|| { @@ -396,7 +396,7 @@ async fn serve_responses_websocket(socket: WebSocket, session: ResponsesWebSocke connection.log_summary(request_count); } -// 尚未建立模型请求的拒绝也保留原文入口,用返回给客户端的 correlation ID 检索。 +// 尚未建立模型请求的拒绝也保留原文入口,用返回给客户端的 correlation ID 检索 fn trace_rejected_request( correlation_id: &str, connection_id: &str, diff --git a/backend/crates/gateway-api/src/openai/responses/websocket/protocol.rs b/backend/crates/gateway-api/src/openai/responses/websocket/protocol.rs index 4053aad0c..edf08bbe0 100644 --- a/backend/crates/gateway-api/src/openai/responses/websocket/protocol.rs +++ b/backend/crates/gateway-api/src/openai/responses/websocket/protocol.rs @@ -1,4 +1,4 @@ -//! Responses WebSocket 入站请求与下行事件的纯协议映射。 +//! Responses WebSocket 入站请求与下行事件的纯协议映射 use axum::http::{HeaderName, HeaderValue, StatusCode}; use gateway_core::engine::EngineError; @@ -16,14 +16,14 @@ use super::super::{ request::{OpenAiRequestHeaders, RequestDecodeSource}, }; -/// 使用连接级请求头和 Provider 上下文解码官方 `response.create` 文本帧。 +/// 使用连接级请求头和 Provider 上下文解码官方 `response.create` 文本帧 /// -/// 缺省 `stream` 等价于 WebSocket 固有的流式语义;显式 `false` 会被拒绝。 +/// 缺省 `stream` 等价于 WebSocket 固有的流式语义;显式 `false` 会被拒绝 /// /// # Errors /// /// 帧不是合法 JSON object、消息类型错误、显式关闭 stream,或 Responses 请求 -/// 无法映射到 canonical operation 时返回不包含正文内容的稳定错误。 +/// 无法映射到 canonical operation 时返回不包含正文内容的稳定错误 pub fn decode_response_create_with_context( payload: &str, request_headers: &OpenAiRequestHeaders, @@ -31,7 +31,7 @@ pub fn decode_response_create_with_context( decode_response_create_inner(payload, request_headers) } -/// 中断只引用活动响应;普通创建帧仍交给原有解码与串行准入路径。 +/// 中断只引用活动响应;普通创建帧仍交给原有解码与串行准入路径 pub(super) fn decode_response_interrupt( payload: &str, ) -> Result, ResponseCreateFrameError> { @@ -40,8 +40,8 @@ pub(super) fn decode_response_interrupt( #[serde(rename = "type")] message_type: Option, } - // 非控制帧仍在原来的串行请求边界校验,不能抢先拒绝排队的普通请求。 - // 只投影类型,避免为排队的大型 response.create 再构造完整 JSON 树。 + // 非控制帧仍在原来的串行请求边界校验,不能抢先拒绝排队的普通请求 + // 只投影类型,避免为排队的大型 response.create 再构造完整 JSON 树 let Ok(frame) = serde_json::from_str::(payload) else { return Ok(None); }; @@ -93,22 +93,22 @@ fn decode_response_create_inner( .map_err(ResponseCreateFrameError::Request) } -/// `response.create` 帧的稳定安全错误。 +/// `response.create` 帧的稳定安全错误 #[derive(Debug, Clone, PartialEq, Eq, Error)] pub enum ResponseCreateFrameError { - /// 文本不是合法 JSON。 + /// 文本不是合法 JSON #[error("response.create frame must be valid JSON")] InvalidJson, - /// 顶层不是 object。 + /// 顶层不是 object #[error("response.create frame must be a JSON object")] ExpectedObject, - /// `type` 缺失或不是 `response.create`。 + /// `type` 缺失或不是 `response.create` #[error("unsupported Responses WebSocket message type")] UnsupportedType, - /// WebSocket 请求显式声明 `stream=false`。 + /// WebSocket 请求显式声明 `stream=false` #[error("Responses WebSocket requests require stream=true")] StreamingRequired, - /// 内层 Responses 请求无法映射到 canonical operation。 + /// 内层 Responses 请求无法映射到 canonical operation #[error(transparent)] Request(RequestDecodeError), } @@ -167,7 +167,7 @@ fn response_event_headers(response_headers: &[ProviderResponseHeader]) -> Map None, }; let upstream = provider.and_then(|error| error.client_visible_upstream_response()); - // 最终失败响应优先于会话快照,避免混入先前 attempt 或 opening 的关联头。 + // 最终失败响应优先于会话快照,避免混入先前 attempt 或 opening 的关联头 let mut headers = response_event_headers(upstream.map_or(response_headers, |response| response.headers())); if upstream.is_none() { - // 会话 opening 身份不证明当前失败属于该请求;仅保留其他允许的会话头。 + // 会话 opening 身份不证明当前失败属于该请求;仅保留其他允许的会话头 headers.remove("x-request-id"); headers.remove("x-oai-request-id"); } @@ -205,7 +205,7 @@ pub(super) fn initial_engine_error_event( .filter(|value| !value.is_success() && !value.is_informational()) .unwrap_or(default_status) }; - // 无原响应关联头时使用 Provider 的失败事实;不能让旧会话 ID 遮住当前失败。 + // 无原响应关联头时使用 Provider 的失败事实;不能让旧会话 ID 遮住当前失败 if !has_request_id(&headers) && let Some(request_id) = provider.and_then(|error| error.upstream_request_id()) && let Ok(value) = HeaderValue::from_str(request_id.as_str()) @@ -214,7 +214,7 @@ pub(super) fn initial_engine_error_event( { headers.insert("x-request-id".to_owned(), Value::String(value.to_owned())); } - // 只桥接 Provider 已提取的结构化错误;原始 HTML/文本正文不成为安全 fallback 的 message。 + // 只桥接 Provider 已提取的结构化错误;原始 HTML/文本正文不成为安全 fallback 的 message error_event( status, gateway.client_error_type().unwrap_or(default_type), @@ -251,8 +251,8 @@ pub(super) fn error_event( event.insert("status".to_owned(), Value::Number(status.as_u16().into())); event.insert("error".to_owned(), Value::Object(error)); if let Some(request_id) = request_id { - // Codex 从本 error 事件的 headers 读 ID,独立 metadata 和顶层 request_id 不能替代。 - // 网关身份独立保存;有上游关联头时不冒充或覆盖上游身份。 + // Codex 从本 error 事件的 headers 读 ID,独立 metadata 和顶层 request_id 不能替代 + // 网关身份独立保存;有上游关联头时不冒充或覆盖上游身份 if !has_request_id(&headers) { headers.insert( "x-request-id".to_owned(), diff --git a/backend/crates/gateway-api/src/openai/router.rs b/backend/crates/gateway-api/src/openai/router.rs index b655b5301..ea096e595 100644 --- a/backend/crates/gateway-api/src/openai/router.rs +++ b/backend/crates/gateway-api/src/openai/router.rs @@ -1,4 +1,4 @@ -//! OpenAI 客户端协议路由。 +//! OpenAI 客户端协议路由 use axum::{ Router, @@ -16,7 +16,7 @@ use super::{ use crate::ApiState; -// 公开协议路径由 API 层统一定义;Core 使用操作类型,不根据这些路径分派业务。 +// 公开协议路径由 API 层统一定义;Core 使用操作类型,不根据这些路径分派业务 pub(super) const RESPONSES_PATH: &str = "/v1/responses"; pub(super) const IMAGE_GENERATIONS_PATH: &str = "/v1/images/generations"; pub(super) const IMAGE_EDITS_PATH: &str = "/v1/images/edits"; @@ -25,7 +25,7 @@ pub(super) const MODELS_PATH: &str = "/v1/models"; pub(super) const MODEL_DETAIL_PATH: &str = "/v1/models/{model_id}"; pub(super) const USAGE_PATH: &str = "/v1/usage"; -/// 构造 OpenAI 客户端协议路由。 +/// 构造 OpenAI 客户端协议路由 pub(crate) fn router() -> Router { Router::new() .route(IMAGE_GENERATIONS_PATH, post(image_generations)) @@ -33,10 +33,10 @@ pub(crate) fn router() -> Router { .route(SEARCH_PATH, post(standalone_search)) .route(RESPONSES_PATH, get(responses_websocket).post(responses)) .route(MODELS_PATH, get(models)) - // 官方 OpenAI 模型详情合同使用 path ID;它不属于 Admin API 约束。 + // 官方 OpenAI 模型详情合同使用 path ID;它不属于 Admin API 约束 .route(MODEL_DETAIL_PATH, get(model_detail)) .merge(usage::router()) // OpenAI 数据面正文属于客户端/上游协议;代理不能用私有大小上限提前拒绝 - // 上游本可接受的未来 payload。 + // 上游本可接受的未来 payload .layer(DefaultBodyLimit::disable()) } diff --git a/backend/crates/gateway-api/src/openai/search/http.rs b/backend/crates/gateway-api/src/openai/search/http.rs index 769e13a0e..555c684fa 100644 --- a/backend/crates/gateway-api/src/openai/search/http.rs +++ b/backend/crates/gateway-api/src/openai/search/http.rs @@ -1,4 +1,4 @@ -//! Codex standalone search 非流式 HTTP adapter。 +//! Codex standalone search 非流式 HTTP adapter use std::net::SocketAddr; @@ -24,7 +24,7 @@ use crate::openai::{ const OPENAI_PROTOCOL: &str = "openai"; -/// `POST /v1/alpha/search`。 +/// `POST /v1/alpha/search` pub(crate) async fn standalone_search( State(state): State, connect_info: Option>>, diff --git a/backend/crates/gateway-api/src/openai/search/mod.rs b/backend/crates/gateway-api/src/openai/search/mod.rs index 7e5669be0..d8e27bde9 100644 --- a/backend/crates/gateway-api/src/openai/search/mod.rs +++ b/backend/crates/gateway-api/src/openai/search/mod.rs @@ -1,4 +1,4 @@ -//! Codex standalone search HTTP adapter。 +//! Codex 独立搜索的 HTTP 接口适配 mod http; diff --git a/backend/crates/gateway-api/src/openai/service.rs b/backend/crates/gateway-api/src/openai/service.rs index c8d3e4527..a6b1b53fb 100644 --- a/backend/crates/gateway-api/src/openai/service.rs +++ b/backend/crates/gateway-api/src/openai/service.rs @@ -1,4 +1,4 @@ -//! OpenAI wire adapter 到 Core 执行用例的唯一映射。 +//! OpenAI wire adapter 到 Core 执行用例的唯一映射 use std::net::IpAddr; use std::sync::Arc; @@ -20,7 +20,7 @@ use uuid::Uuid; use super::auth::ClientApiKeyAuthError; use super::responses::{ContinuationIntent, DecodedResponsesRequest}; -/// OpenAI HTTP/WS adapter 共享的 Core 与连接生命周期能力。 +/// OpenAI HTTP/WS adapter 共享的 Core 与连接生命周期能力 #[derive(Clone)] pub(crate) struct OpenAiService { execution: Arc, diff --git a/backend/crates/gateway-api/src/openai/usage.rs b/backend/crates/gateway-api/src/openai/usage.rs index ee7ebabb7..935e33873 100644 --- a/backend/crates/gateway-api/src/openai/usage.rs +++ b/backend/crates/gateway-api/src/openai/usage.rs @@ -1,4 +1,4 @@ -//! Bearer Key 的只读额度查询,与自助页面共用当前预算账本。 +//! Bearer Key 的只读额度查询,与自助页面共用当前预算账本 use std::time::SystemTime; @@ -59,7 +59,7 @@ async fn usage_response( client: AuthenticatedClient, uri: axum::http::Uri, ) -> Response { - // 范围只能来自宿主认证后的身份,不接受插件或调用者指定 Key、账号或时间窗口。 + // 范围只能来自宿主认证后的身份,不接受插件或调用者指定 Key、账号或时间窗口 if Query::::try_from_uri(&uri).is_err() { return openai_error_response( StatusCode::BAD_REQUEST, diff --git a/backend/crates/gateway-api/src/provider.rs b/backend/crates/gateway-api/src/provider.rs index 881823c3e..61be58896 100644 --- a/backend/crates/gateway-api/src/provider.rs +++ b/backend/crates/gateway-api/src/provider.rs @@ -1,4 +1,4 @@ -//! Provider 自有操作的稳定 HTTP adapter。 +//! Provider 自有操作的稳定 HTTP adapter use std::net::SocketAddr; @@ -46,7 +46,7 @@ pub(crate) fn router() -> Router { "/v1/providers/{provider}/models/{model}/count_tokens", post(count_tokens), ) - // `get()` 会隐式接受 HEAD;这里必须只匹配明确声明的 GET/POST。 + // `get()` 会隐式接受 HEAD;这里必须只匹配明确声明的 GET/POST .route( "/v1/providers/{provider}/http/{endpoint}", on(MethodFilter::GET.or(MethodFilter::POST), provider_http).head(provider_http_head), diff --git a/backend/crates/gateway-api/src/session_cookie.rs b/backend/crates/gateway-api/src/session_cookie.rs index 8ca5ce021..8a4a56a92 100644 --- a/backend/crates/gateway-api/src/session_cookie.rs +++ b/backend/crates/gateway-api/src/session_cookie.rs @@ -1,4 +1,4 @@ -//! 控制面会话 Cookie 的同源 HTTP / HTTPS 属性与解析。 +//! 控制面会话 Cookie 的同源 HTTP / HTTPS 属性与解析 use axum::http::{HeaderMap, header::ORIGIN}; use url::Url; @@ -6,8 +6,9 @@ use url::Url; pub(crate) const NAME: &str = "cpr_session"; pub(crate) fn attributes(headers: &HeaderMap) -> &'static str { - // HTTPS 反代回源 HTTP 不改变浏览器 Origin。只有单个、严格合法的 HTTP Origin - // 才允许省略 Secure;缺失、opaque 或非法来源继续 fail closed。 + // HTTPS 反代回源 HTTP 不改变浏览器 Origin + // 只有单个、严格合法的 HTTP Origin + // 才允许省略 Secure;缺失、opaque 或非法来源继续 fail closed let http_origin = headers.get_all(ORIGIN).iter().count() == 1 && headers .get(ORIGIN) diff --git a/backend/crates/gateway-api/src/time.rs b/backend/crates/gateway-api/src/time.rs index faa09a300..574447cd2 100644 --- a/backend/crates/gateway-api/src/time.rs +++ b/backend/crates/gateway-api/src/time.rs @@ -1,4 +1,4 @@ -//! 管理与自助页面共用的时间展示投影。 +//! 管理与自助页面共用的时间展示投影 use chrono::{DateTime, Utc}; use gateway_core::time::DeploymentTimeZone; diff --git a/backend/crates/gateway-api/tests/admin/account_groups.rs b/backend/crates/gateway-api/tests/admin/account_groups.rs index b0d0ce321..eda7a9cf7 100644 --- a/backend/crates/gateway-api/tests/admin/account_groups.rs +++ b/backend/crates/gateway-api/tests/admin/account_groups.rs @@ -1,3 +1,5 @@ +//! 验证账号分组 HTTP 接口的请求校验、权限与响应合同 + use axum::{ Router, body::{Body, to_bytes}, diff --git a/backend/crates/gateway-api/tests/admin/accounts/handlers.rs b/backend/crates/gateway-api/tests/admin/accounts/handlers.rs index 221f33fb0..cb3a43949 100644 --- a/backend/crates/gateway-api/tests/admin/accounts/handlers.rs +++ b/backend/crates/gateway-api/tests/admin/accounts/handlers.rs @@ -1,3 +1,5 @@ +//! 验证账号管理接口的身份要求、查询校验与服务调用边界 + use axum::{ body::{Body, to_bytes}, http::{Request, StatusCode, header}, @@ -171,7 +173,7 @@ async fn connection_update_requires_admin_and_validates_before_calling_the_servi for (authenticated, transport, oauth, expected) in [ (false, "http", false, StatusCode::UNAUTHORIZED), (true, "invalid", false, StatusCode::BAD_REQUEST), - // 夹具没有凭据 Store,合法输入必须进入服务,不能按普通设置静默保存。 + // 夹具没有凭据 Store,合法输入必须进入服务,不能按普通设置静默保存 (true, "http", false, StatusCode::SERVICE_UNAVAILABLE), (true, "http", true, StatusCode::SERVICE_UNAVAILABLE), ] { @@ -214,7 +216,7 @@ async fn personal_info_requires_admin_and_a_valid_account_query() { true, StatusCode::BAD_REQUEST, ), - // 此夹具未提供账号 Store,合法查询应透传服务不可用,而非绕过查询。 + // 此夹具未提供账号 Store,合法查询应透传服务不可用,而非绕过查询 ( "?accountId=acct_test", true, diff --git a/backend/crates/gateway-api/tests/admin/accounts/import_tasks.rs b/backend/crates/gateway-api/tests/admin/accounts/import_tasks.rs index 9ff84594e..dbadfd6f9 100644 --- a/backend/crates/gateway-api/tests/admin/accounts/import_tasks.rs +++ b/backend/crates/gateway-api/tests/admin/accounts/import_tasks.rs @@ -1,3 +1,5 @@ +//! 验证账号导入任务接口的幂等、恢复、停止与输入保护 + use axum::{ body::{Body, to_bytes}, http::{Request, StatusCode, header}, diff --git a/backend/crates/gateway-api/tests/admin/accounts/mod.rs b/backend/crates/gateway-api/tests/admin/accounts/mod.rs index f4ffbd553..c1cce8bf0 100644 --- a/backend/crates/gateway-api/tests/admin/accounts/mod.rs +++ b/backend/crates/gateway-api/tests/admin/accounts/mod.rs @@ -1,3 +1,5 @@ +//! 账号管理 HTTP 测试入口,以及订阅、资料与筛选投影测试 + mod handlers; mod import_tasks; mod presenter; diff --git a/backend/crates/gateway-api/tests/admin/accounts/presenter.rs b/backend/crates/gateway-api/tests/admin/accounts/presenter.rs index f8fafcdd2..3a62d298e 100644 --- a/backend/crates/gateway-api/tests/admin/accounts/presenter.rs +++ b/backend/crates/gateway-api/tests/admin/accounts/presenter.rs @@ -1,3 +1,5 @@ +//! 验证账号额度预测与原生模型目录的展示投影 + use bytes::Bytes; use gateway_admin::model::{ provider_credentials::{AccountUsagePeriod, ProviderModelCatalogDocument}, @@ -91,7 +93,7 @@ fn quota_forecast_projection_only_exposes_capacity_and_preserves_null_zero() { #[test] fn model_catalog_projection_keeps_upstream_document_and_rejects_non_codex_wire() { let observed_at = "2026-09-12T08:00:00Z".parse().unwrap(); - // 上游原生对象里的元数据必须原样到达客户端文件,否则 Codex 读不到推理强度和上下文窗口。 + // 上游原生对象里的元数据必须原样到达客户端文件,否则 Codex 读不到推理强度和上下文窗口 let body = serde_json::json!({ "models": [{ "slug": "gpt-5.6-luna", @@ -112,7 +114,7 @@ fn model_catalog_projection_keeps_upstream_document_and_rejects_non_codex_wire() assert_eq!(data.catalog, body); assert_eq!(data.observed_at, "2026-09-12T08:00:00+00:00"); - // 只有模型 ID 的 API 目录拼不出合法的 model_catalog_json,不能降格返回给客户端。 + // 只有模型 ID 的 API 目录拼不出合法的 model_catalog_json,不能降格返回给客户端 let adapted = RawJsonPayload::new("openai", Bytes::from_static(br#"{"models":[]}"#)) .expect("openai payload"); assert_eq!( diff --git a/backend/crates/gateway-api/tests/admin/auth.rs b/backend/crates/gateway-api/tests/admin/auth.rs index 7039f6611..f247bf6f6 100644 --- a/backend/crates/gateway-api/tests/admin/auth.rs +++ b/backend/crates/gateway-api/tests/admin/auth.rs @@ -1,3 +1,5 @@ +//! 验证管理员认证请求脱敏、会话操作与审计失败处理 + use gateway_admin::model::auth::{LoginCommand, LoginError}; use gateway_api::auth::LoginRequest; use serde_json::json; diff --git a/backend/crates/gateway-api/tests/admin/client_keys.rs b/backend/crates/gateway-api/tests/admin/client_keys.rs index b5555e977..3f80771ef 100644 --- a/backend/crates/gateway-api/tests/admin/client_keys.rs +++ b/backend/crates/gateway-api/tests/admin/client_keys.rs @@ -1,3 +1,5 @@ +//! 验证 Client Key 管理接口的预算、密钥输入与查询合同 + use chrono::{DateTime, TimeZone as _, Utc}; use gateway_api::admin::client_keys::{ self, ClientKeyCursorData, ClientKeyCursorValue, ClientKeyListData, ClientKeyMutationRequest, diff --git a/backend/crates/gateway-api/tests/admin/errors.rs b/backend/crates/gateway-api/tests/admin/errors.rs index 9582ac2a8..c326d4d31 100644 --- a/backend/crates/gateway-api/tests/admin/errors.rs +++ b/backend/crates/gateway-api/tests/admin/errors.rs @@ -1,3 +1,5 @@ +//! 验证管理接口的错误封装、状态码与输入信息脱敏 + use axum::{ Router, body::{Body, to_bytes}, @@ -454,7 +456,7 @@ mod provider { (Kind::Internal, 500, 50001, "服务内部错误"), ] { let error = ProviderAdminError::new(kind).with_message("private-provider-diagnostics"); - // 即使 Provider 错标了公开文案,未知内部异常仍不得通过 500 信封下发。 + // 即使 Provider 错标了公开文案,未知内部异常仍不得通过 500 信封下发 *fixture.provider_error.lock().unwrap() = Some(if kind == Kind::Internal { error.with_public_message("internal-detail-must-stay-hidden") } else { diff --git a/backend/crates/gateway-api/tests/admin/mod.rs b/backend/crates/gateway-api/tests/admin/mod.rs index 7b6e6a575..89d91c398 100644 --- a/backend/crates/gateway-api/tests/admin/mod.rs +++ b/backend/crates/gateway-api/tests/admin/mod.rs @@ -1,3 +1,5 @@ +//! 管理控制面 HTTP 接口的测试入口 + mod plugins; use std::{ collections::BTreeMap, diff --git a/backend/crates/gateway-api/tests/admin/observability/mod.rs b/backend/crates/gateway-api/tests/admin/observability/mod.rs index 0ff97a871..c03da4c91 100644 --- a/backend/crates/gateway-api/tests/admin/observability/mod.rs +++ b/backend/crates/gateway-api/tests/admin/observability/mod.rs @@ -1,4 +1,4 @@ -//! Admin 观测 wire/路由的固定合同测试。 +//! Admin 观测 wire/路由的固定合同测试 mod query; mod response; diff --git a/backend/crates/gateway-api/tests/admin/observability/query.rs b/backend/crates/gateway-api/tests/admin/observability/query.rs index a832080af..e8f57f33d 100644 --- a/backend/crates/gateway-api/tests/admin/observability/query.rs +++ b/backend/crates/gateway-api/tests/admin/observability/query.rs @@ -1,4 +1,4 @@ -//! 查询 wire 校验的固定合同测试。 +//! 查询 wire 校验的固定合同测试 use gateway_api::admin::observability::{ DashboardQuery, DiagnosticDimension, DiagnosticsQuery, OpsQuery, TrendKind, UsageQuery, diff --git a/backend/crates/gateway-api/tests/admin/observability/response.rs b/backend/crates/gateway-api/tests/admin/observability/response.rs index c5b7eaf01..d79013cf6 100644 --- a/backend/crates/gateway-api/tests/admin/observability/response.rs +++ b/backend/crates/gateway-api/tests/admin/observability/response.rs @@ -1,4 +1,4 @@ -//! 响应 DTO 与固定 wire 形状的合同测试。 +//! 响应 DTO 与固定 wire 形状的合同测试 use chrono::Utc; use serde_json::json; @@ -394,7 +394,7 @@ async fn zero_attempt_failure_detail_keeps_missing_upstream_facts_and_preparatio assert_eq!(data["requestId"], "req_early"); assert_eq!(data["attemptCount"], 0); assert_eq!(data["attempts"], json!([])); - // 明细仍沿用全局 best-effort 合同,不从预备 trace 推断完整尝试列表。 + // 明细仍沿用全局 best-effort 合同,不从预备 trace 推断完整尝试列表 assert_eq!(data["attemptsComplete"], false); assert_eq!(data["trace"], trace); assert_eq!(data["logicalOutcome"], "failed"); diff --git a/backend/crates/gateway-api/tests/admin/plugins/management.rs b/backend/crates/gateway-api/tests/admin/plugins/management.rs index a11a0eec9..1954286b3 100644 --- a/backend/crates/gateway-api/tests/admin/plugins/management.rs +++ b/backend/crates/gateway-api/tests/admin/plugins/management.rs @@ -1,3 +1,5 @@ +//! 插件管理页面模型调用的认证、执行与响应交付测试 + use axum::{ body::Body, http::{Request, StatusCode}, @@ -727,7 +729,7 @@ async fn public_callback_rejects_head_missing_duplicate_state_and_body_before_di ("GET", "state=one&state=two", "", StatusCode::BAD_REQUEST), ("GET", "state=one&%73tate=two", "", StatusCode::BAD_REQUEST), ("GET", "state=one", "body", StatusCode::BAD_REQUEST), - // 合法 GET 没有管理 Cookie 仍进入服务;当前测试未发布视图,返回 503 而不是 401。 + // 合法 GET 没有管理 Cookie 仍进入服务;当前测试未发布视图,返回 503 而不是 401 ("GET", "state=one", "", StatusCode::SERVICE_UNAVAILABLE), ] { let response = crate::openai::api_router_with_admin_and_execution( diff --git a/backend/crates/gateway-api/tests/admin/plugins/mod.rs b/backend/crates/gateway-api/tests/admin/plugins/mod.rs index 377aa0a37..da463be7c 100644 --- a/backend/crates/gateway-api/tests/admin/plugins/mod.rs +++ b/backend/crates/gateway-api/tests/admin/plugins/mod.rs @@ -1,3 +1,5 @@ +//! 插件管理 HTTP 测试入口,以及制品、实例与权限合同测试 + use async_trait::async_trait; use gateway_admin::model::plugins::distribution::{ DownloadedPlugin, GithubReleaseQuery, PluginRelease, RemotePluginLocation, SourceCredential, @@ -522,7 +524,7 @@ async fn plugin_mutations_require_admin_and_validate_json_on_static_post_routes( assert_eq!( response.status(), if authenticated { - // 字段缺失沿用 AdminJson 的结构校验合同。 + // 字段缺失沿用 AdminJson 的结构校验合同 StatusCode::UNPROCESSABLE_ENTITY } else { StatusCode::UNAUTHORIZED diff --git a/backend/crates/gateway-api/tests/admin/proxies.rs b/backend/crates/gateway-api/tests/admin/proxies.rs index a422dc8b3..11892ef69 100644 --- a/backend/crates/gateway-api/tests/admin/proxies.rs +++ b/backend/crates/gateway-api/tests/admin/proxies.rs @@ -1,3 +1,5 @@ +//! 验证代理管理 HTTP 接口的位置字段、部分更新与账号操作 + use std::sync::Mutex; use async_trait::async_trait; diff --git a/backend/crates/gateway-api/tests/admin/settings.rs b/backend/crates/gateway-api/tests/admin/settings.rs index 5a4f3dae8..770b0f1ec 100644 --- a/backend/crates/gateway-api/tests/admin/settings.rs +++ b/backend/crates/gateway-api/tests/admin/settings.rs @@ -1,3 +1,5 @@ +//! 验证运行设置接口的输入校验、完整投影与持久化结果 + use axum::{ Router, body::{Body, to_bytes}, @@ -557,7 +559,7 @@ async fn admin_auth_should_accept_a_configured_request_id_header_name() { let fixture = AdminTestFixture::new().await; fixture.auth.insert_session("valid-session"); // 部署把 api.request_id_header 改名后,注入的 header 不再叫 x-request-id; - // 管理请求仍须拿到请求上下文,而不是退化为 500。 + // 管理请求仍须拿到请求上下文,而不是退化为 500 let custom = HeaderName::from_static("x-trace-id"); let app = app(fixture.state()).layer(SetRequestIdLayer::new(custom, MakeRequestUuid)); let unlabelled = Request::builder() @@ -655,7 +657,7 @@ async fn request_location_should_reject_invalid_or_missing_fields_without_replac let fixture = AdminTestFixture::new().await; fixture.auth.insert_session("valid-session"); let original = update_body()["requestLocation"].clone(); - // JSON 结构或时区解析失败由 Axum 返回 422,业务校验失败返回 400。 + // JSON 结构或时区解析失败由 Axum 返回 422,业务校验失败返回 400 let mut invalid = vec![ (Value::Null, StatusCode::UNPROCESSABLE_ENTITY), (json!({}), StatusCode::UNPROCESSABLE_ENTITY), @@ -1033,7 +1035,7 @@ async fn pricing_rejects_invalid_edits_and_tampered_sync_without_writes() { .unwrap(); assert_eq!(response.status(), StatusCode::BAD_REQUEST, "{scenario}"); } - // JSON 合同错误沿用 AdminJson 的 422,业务校验错误为 400。 + // JSON 合同错误沿用 AdminJson 的 422,业务校验错误为 400 let response = app .clone() .oneshot(request( diff --git a/backend/crates/gateway-api/tests/admin/system.rs b/backend/crates/gateway-api/tests/admin/system.rs index ac72e859d..b627b8ada 100644 --- a/backend/crates/gateway-api/tests/admin/system.rs +++ b/backend/crates/gateway-api/tests/admin/system.rs @@ -1,3 +1,5 @@ +//! 验证系统升级 HTTP 接口的确认参数、异步状态与事件流 + use std::sync::Arc; use async_trait::async_trait; diff --git a/backend/crates/gateway-api/tests/admin/wire.rs b/backend/crates/gateway-api/tests/admin/wire.rs index d6176709d..9c60147ce 100644 --- a/backend/crates/gateway-api/tests/admin/wire.rs +++ b/backend/crates/gateway-api/tests/admin/wire.rs @@ -1,3 +1,5 @@ +//! 验证管理响应封装、分页结构与公开错误码保持稳定 + mod common { use gateway_api::admin::PageMeta; use serde_json::json; diff --git a/backend/crates/gateway-api/tests/architecture.rs b/backend/crates/gateway-api/tests/architecture.rs index 88499de37..2ecbdc903 100644 --- a/backend/crates/gateway-api/tests/architecture.rs +++ b/backend/crates/gateway-api/tests/architecture.rs @@ -1,3 +1,5 @@ +//! 检查 API 层依赖、源码与测试模块边界,以及管理路由约定 + use std::{fs, path::Path}; #[test] diff --git a/backend/crates/gateway-api/tests/auth.rs b/backend/crates/gateway-api/tests/auth.rs index 69a0c48dd..2bc4f3323 100644 --- a/backend/crates/gateway-api/tests/auth.rs +++ b/backend/crates/gateway-api/tests/auth.rs @@ -1,3 +1,5 @@ +//! 验证 HTTP 登录身份、会话轮换与管理员和 Key 会话的权限隔离 + use std::sync::atomic::Ordering; use axum::{ diff --git a/backend/crates/gateway-api/tests/health.rs b/backend/crates/gateway-api/tests/health.rs index 348e06466..d61d984ad 100644 --- a/backend/crates/gateway-api/tests/health.rs +++ b/backend/crates/gateway-api/tests/health.rs @@ -1,3 +1,5 @@ +//! 验证健康接口按基础设施与关键 Worker 状态判定可用性 + use std::sync::Arc; use axum::{ diff --git a/backend/crates/gateway-api/tests/key_usage/fixtures.rs b/backend/crates/gateway-api/tests/key_usage/fixtures.rs index bb3360e3f..9d89e4bf9 100644 --- a/backend/crates/gateway-api/tests/key_usage/fixtures.rs +++ b/backend/crates/gateway-api/tests/key_usage/fixtures.rs @@ -1,3 +1,5 @@ +//! Key 用量查询测试共用的请求、费用与错误记录样本 + use crate::{admin::AdminTestFixture, support::key_fixture}; use chrono::{Duration, Utc}; use gateway_admin::model::{ diff --git a/backend/crates/gateway-api/tests/key_usage/mod.rs b/backend/crates/gateway-api/tests/key_usage/mod.rs index 496167ed5..691e6f07f 100644 --- a/backend/crates/gateway-api/tests/key_usage/mod.rs +++ b/backend/crates/gateway-api/tests/key_usage/mod.rs @@ -1,4 +1,4 @@ -//! 通过真实路由验证会话隔离、范围收敛和对外字段白名单。 +//! 通过真实路由验证会话隔离、范围收敛和对外字段白名单 use std::sync::atomic::Ordering; diff --git a/backend/crates/gateway-api/tests/main.rs b/backend/crates/gateway-api/tests/main.rs index 1cb8b8725..5deced158 100644 --- a/backend/crates/gateway-api/tests/main.rs +++ b/backend/crates/gateway-api/tests/main.rs @@ -1,3 +1,5 @@ +//! API 层测试入口,以及静态页面资源解析与服务测试 + mod admin; mod architecture; mod auth; diff --git a/backend/crates/gateway-api/tests/middleware/mod.rs b/backend/crates/gateway-api/tests/middleware/mod.rs index 9f6f31204..9721385dd 100644 --- a/backend/crates/gateway-api/tests/middleware/mod.rs +++ b/backend/crates/gateway-api/tests/middleware/mod.rs @@ -1,3 +1,5 @@ +//! HTTP 与 WebSocket 中间件测试入口及请求生命周期替身 + mod websocket; use std::{ diff --git a/backend/crates/gateway-api/tests/middleware/websocket.rs b/backend/crates/gateway-api/tests/middleware/websocket.rs index fc20e4ca1..5282eb3fc 100644 --- a/backend/crates/gateway-api/tests/middleware/websocket.rs +++ b/backend/crates/gateway-api/tests/middleware/websocket.rs @@ -1,3 +1,5 @@ +//! 验证自定义 WebSocket 升级、双向收发与取消后的资源回收 + use super::*; use futures::{SinkExt as _, StreamExt as _}; use tokio_tungstenite::{ diff --git a/backend/crates/gateway-api/tests/openai/auth.rs b/backend/crates/gateway-api/tests/openai/auth.rs index 74619694f..79ef73c15 100644 --- a/backend/crates/gateway-api/tests/openai/auth.rs +++ b/backend/crates/gateway-api/tests/openai/auth.rs @@ -1,3 +1,5 @@ +//! 验证 OpenAI 入口的 Bearer 认证与插件可见认证信息边界 + use std::sync::{ Arc, atomic::{AtomicUsize, Ordering}, diff --git a/backend/crates/gateway-api/tests/openai/endpoint.rs b/backend/crates/gateway-api/tests/openai/endpoint.rs index 0c1d81dcb..06d827a61 100644 --- a/backend/crates/gateway-api/tests/openai/endpoint.rs +++ b/backend/crates/gateway-api/tests/openai/endpoint.rs @@ -1,3 +1,5 @@ +//! OpenAI 端点测试共用的缓冲响应、失败结果与交付状态替身 + use std::sync::{Arc, Mutex, atomic::AtomicBool, atomic::Ordering}; use bytes::Bytes; diff --git a/backend/crates/gateway-api/tests/openai/error.rs b/backend/crates/gateway-api/tests/openai/error.rs index 456615b51..467ad58fd 100644 --- a/backend/crates/gateway-api/tests/openai/error.rs +++ b/backend/crates/gateway-api/tests/openai/error.rs @@ -1,3 +1,5 @@ +//! 验证执行错误到 OpenAI 状态码、错误正文与重试提示的映射 + use axum::{body::to_bytes, http::StatusCode}; use gateway_core::engine::EngineError; use gateway_core::error::{ diff --git a/backend/crates/gateway-api/tests/openai/images/mod.rs b/backend/crates/gateway-api/tests/openai/images/mod.rs index bdf4eee91..8dc3737c4 100644 --- a/backend/crates/gateway-api/tests/openai/images/mod.rs +++ b/backend/crates/gateway-api/tests/openai/images/mod.rs @@ -1,3 +1,5 @@ +//! 图像生成与编辑 HTTP 接口的请求传递和响应交付测试 + use std::sync::{ Arc, Mutex, atomic::{AtomicBool, Ordering}, diff --git a/backend/crates/gateway-api/tests/openai/middleware/mod.rs b/backend/crates/gateway-api/tests/openai/middleware/mod.rs index 9c0766904..4505a4c71 100644 --- a/backend/crates/gateway-api/tests/openai/middleware/mod.rs +++ b/backend/crates/gateway-api/tests/openai/middleware/mod.rs @@ -1,3 +1,5 @@ +//! OpenAI 请求与响应中间件的改写、短路及交付行为测试 + use std::{ collections::VecDeque, sync::{ @@ -18,7 +20,7 @@ use gateway_core::{ runtime::extensions::{ExtensionSetId, ExtensionSetLease, ExtensionSetReference}, }; -/// OpenAI API 入口共用的中间件夹具;授权和 RPC 由 Runtime 行为测试覆盖。 +/// OpenAI API 入口共用的中间件夹具;授权和 RPC 由 Runtime 行为测试覆盖 #[derive(Debug, Default)] pub(super) struct RequestMiddleware { pub(super) endpoints: Mutex>, diff --git a/backend/crates/gateway-api/tests/openai/mod.rs b/backend/crates/gateway-api/tests/openai/mod.rs index ebcbd376c..ee559364c 100644 --- a/backend/crates/gateway-api/tests/openai/mod.rs +++ b/backend/crates/gateway-api/tests/openai/mod.rs @@ -1,3 +1,5 @@ +//! OpenAI 协议 HTTP 与 WebSocket 接口的测试入口 + mod auth; mod endpoint; mod error; diff --git a/backend/crates/gateway-api/tests/openai/models.rs b/backend/crates/gateway-api/tests/openai/models.rs index d06b9b49a..ab01937e4 100644 --- a/backend/crates/gateway-api/tests/openai/models.rs +++ b/backend/crates/gateway-api/tests/openai/models.rs @@ -1,3 +1,5 @@ +//! 模型列表、详情与客户端原生目录接口的投影及访问校验测试 + use std::sync::{Arc, Mutex}; use axum::{ diff --git a/backend/crates/gateway-api/tests/openai/responses/http.rs b/backend/crates/gateway-api/tests/openai/responses/http.rs index c2d3f14ad..468315367 100644 --- a/backend/crates/gateway-api/tests/openai/responses/http.rs +++ b/backend/crates/gateway-api/tests/openai/responses/http.rs @@ -1,3 +1,5 @@ +//! Responses HTTP 流式交付、错误、取消与计量边界测试 + use std::collections::VecDeque; use std::io::Read; use std::net::{IpAddr, SocketAddr}; @@ -610,7 +612,7 @@ async fn compressed_http_requests_should_preserve_execution_context_without_tran let (response, observed) = http_request_with_body("openai", compressed.into(), headers, None).await; // 捕获执行器在解码成功后主动返回 500;正文和业务头应与未压缩请求一致, - // 压缩编码及长度不能泄漏到已解压正文的上游协议上下文。 + // 压缩编码及长度不能泄漏到已解压正文的上游协议上下文 assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); assert_eq!(observed.as_ref(), Some(&expected), "{encoding}"); } @@ -728,7 +730,7 @@ async fn request_context_should_resolve_forwarded_precedence_and_peer_fallback() operation_kind: OperationKind::Generate, input: Some(json!("hello")), client_metadata: None, - // 客户端 User-Agent 仅用于本地展示,不再透传给上游指纹上下文。 + // 客户端 User-Agent 仅用于本地展示,不再透传给上游指纹上下文 protocol_context: Some(json!({"opaque_request_headers": [ ["cf-connecting-ip", STANDARD.encode(b"198.51.100.1")], ["x-real-ip", STANDARD.encode(b"198.51.100.2")], @@ -2290,7 +2292,7 @@ async fn buffered_response_collects_completed_output_items_without_rewriting_str "future_terminal_field": {"keep": true} }); let mut events = vec![provider_event_for_fact(started())]; - // 完成顺序不决定输出顺序;相同完成项重传不能产生重复正文。 + // 完成顺序不决定输出顺序;相同完成项重传不能产生重复正文 for (index, item) in [(1, &tool), (0, &message), (0, &message)] { events.push(super::openai_wire_event( vec![], @@ -3005,7 +3007,7 @@ async fn compressed_http_request_above_default_limit_should_reach_execution_afte .unwrap(); assert_eq!(error["error"]["code"], "request_too_large"); } else { - // 捕获执行服务刻意返回 500;断言完整输入到达执行层,而不是只验证路由状态码。 + // 捕获执行服务刻意返回 500;断言完整输入到达执行层,而不是只验证路由状态码 let captured = observed .lock() .unwrap() diff --git a/backend/crates/gateway-api/tests/openai/responses/mod.rs b/backend/crates/gateway-api/tests/openai/responses/mod.rs index 18f96b79b..f8544458f 100644 --- a/backend/crates/gateway-api/tests/openai/responses/mod.rs +++ b/backend/crates/gateway-api/tests/openai/responses/mod.rs @@ -1,3 +1,5 @@ +//! Responses 接口测试入口,以及协议请求与事件构造辅助 + mod http; mod request; mod websocket; @@ -407,7 +409,7 @@ fn decoder_should_preserve_ordinary_request_headers_as_opaque_multivalues() { "chatgpt-project-id", "x-openai-account-routing-override", "x-openai-fedramp", - // 上游指纹由运行时画像统一生成,客户端不得覆盖。 + // 上游指纹由运行时画像统一生成,客户端不得覆盖 "user-agent", "originator", "version", @@ -446,7 +448,7 @@ fn decoder_should_strip_http_transport_but_leave_source_headers_for_provider() { HeaderValue::from_static(r#"{"scheme":"https"}"#), ); headers.insert("accept-encoding", HeaderValue::from_static("br, gzip")); - // 正文未压缩;入口现在消费 Content-Encoding,但仍不得向上游透传。 + // 正文未压缩;入口现在消费 Content-Encoding,但仍不得向上游透传 headers.insert("content-encoding", HeaderValue::from_static("identity")); headers.insert("x-openai-future-mode", HeaderValue::from_static("keep")); @@ -573,7 +575,7 @@ fn downstream_client_headers_should_remain_opaque_without_losing_session_semanti ] ); } - // 解码不修改原始请求,CORS、鉴权和本地观测仍可读取原值。 + // 解码不修改原始请求,CORS、鉴权和本地观测仍可读取原值 assert_eq!(headers, original_headers); } } @@ -837,9 +839,10 @@ fn transparent_encoder_should_use_identical_json_for_sse_and_websocket() { #[test] fn transparent_encoder_should_translate_error_wire_to_response_failed_for_websocket() { // codex 的 WS 端点只消费带 status 的包装错误帧;上游缺少 status 的裸 - // `error` 帧会被静默忽略,客户端只能空等到 idle 超时。WS 边界与 SSE + // `error` 帧会被静默忽略,客户端只能空等到 idle 超时 + // WS 边界与 SSE // 边界一致投影成 `response.failed`,codex 将其映射为可重试错误并立即 - // 重试,而不是把失败原因拖到流 EOF。 + // 重试,而不是把失败原因拖到流 EOF let response_id = "resp_ws_error"; let started = openai_wire_event( Vec::new(), @@ -875,7 +878,7 @@ fn transparent_encoder_should_translate_error_wire_to_response_failed_for_websoc assert_eq!(payload["response"]["status"], "failed"); assert_eq!(payload["response"]["error"]["code"], "server_error"); - // 与 SSE 边界投影到同一份 data 负载,两条客户端通道行为一致。 + // 与 SSE 边界投影到同一份 data 负载,两条客户端通道行为一致 let mut sse_encoder = OpenAiResponsesEncoder::new(); sse_encoder.push_sse(&started); let frames = sse_encoder.push_sse(&error); @@ -893,7 +896,7 @@ fn transparent_encoder_should_translate_error_wire_to_response_failed_for_websoc .collect::>(); assert_eq!(sse_data, messages); - // 带非 2xx status 的包装错误帧是 codex 可直接消费的形状,必须原样透传。 + // 带非 2xx status 的包装错误帧是 codex 可直接消费的形状,必须原样透传 let mut passthrough_encoder = OpenAiResponsesEncoder::new(); passthrough_encoder.push_websocket(&started); let wrapped = openai_wire_event( diff --git a/backend/crates/gateway-api/tests/openai/responses/request.rs b/backend/crates/gateway-api/tests/openai/responses/request.rs index cf499e7a9..431b8325f 100644 --- a/backend/crates/gateway-api/tests/openai/responses/request.rs +++ b/backend/crates/gateway-api/tests/openai/responses/request.rs @@ -1,3 +1,5 @@ +//! 验证 Responses 请求解压、大小限制与原始字段保留 + use std::io::{Read, Write, copy, repeat}; use axum::http::{HeaderMap, HeaderValue, header::CONTENT_ENCODING}; @@ -127,7 +129,7 @@ fn http_decode_should_reject_zstd_frames_with_an_excessive_window() { let mut encoder = zstd::stream::write::Encoder::new(Vec::new(), 3).expect("zstd encoder"); encoder.window_log(27).expect("128 MiB window"); encoder.write_all(REQUEST).expect("write JSON"); - // 提前输出帧头,避免编码器根据最终小正文缩减窗口。 + // 提前输出帧头,避免编码器根据最终小正文缩减窗口 encoder.flush().expect("flush frame header"); let compressed = encoder.finish().expect("finish frame"); assert_eq!( @@ -170,7 +172,7 @@ fn http_decode_should_reject_bodies_above_the_decompressed_limit() { fn http_decode_should_stop_at_limit_before_reading_invalid_trailing_frames() { for encoding in ["zstd", "gzip"] { // 尾部故意损坏:若先完整解压再检查长度,就会错误地返回 invalid_json, - // 这个断言验证越界时已经停止读取,而不只是最终返回了某个超限错误。 + // 这个断言验证越界时已经停止读取,而不只是最终返回了某个超限错误 let mut compressed = encode_body(encoding, repeat(0).take(DECOMPRESSED_LIMIT + 1)); compressed.extend_from_slice(b"invalid trailing frame"); let error = decode_request_with_headers(&compressed, &headers(encoding), 64 * 1024 * 1024) diff --git a/backend/crates/gateway-api/tests/openai/responses/websocket/connection/middleware.rs b/backend/crates/gateway-api/tests/openai/responses/websocket/connection/middleware.rs index a9ac02e46..92e97b45f 100644 --- a/backend/crates/gateway-api/tests/openai/responses/websocket/connection/middleware.rs +++ b/backend/crates/gateway-api/tests/openai/responses/websocket/connection/middleware.rs @@ -1,3 +1,5 @@ +//! 验证 WebSocket 各轮次刷新身份与设置时保留握手中间件覆盖 + use super::*; use bytes::Bytes; use gateway_core::{ @@ -104,7 +106,7 @@ impl MiddlewarePlan for Plan { request: MiddlewareRequest, next: MiddlewareNext, ) -> BoxFuture<'static, Result> { - // 统一计划延续到连接内的模型调用,没有模型绑定时直接委托。 + // 统一计划延续到连接内的模型调用,没有模型绑定时直接委托 next.run(request) } } @@ -266,7 +268,7 @@ async fn rewrite_upgrade_and_control_active_response_keep_http_context_until_con ); let (trace, mut socket, server) = start_active_response_with_middleware(Some(frozen), "/custom/websocket").await; - // 升级后的默认会话仍持有入口上下文;101 正文结束不再提前取消它。 + // 升级后的默认会话仍持有入口上下文;101 正文结束不再提前取消它 assert!( !plan .http_cancellation @@ -352,7 +354,7 @@ async fn input_plugin_runs_while_a_real_socket_write_is_blocked() { let (trace, mut socket, server) = start_active_response_with_middleware(Some(frozen), "/custom/websocket").await; trace.release_terminal.notify_one(); - // 客户端不读取 64 MiB 输出,真实 TCP 写入将受接收窗口背压。 + // 客户端不读取 64 MiB 输出,真实 TCP 写入将受接收窗口背压 tokio::time::sleep(Duration::from_millis(100)).await; assert!(!trace.finalized.load(Ordering::SeqCst)); socket diff --git a/backend/crates/gateway-api/tests/openai/responses/websocket/connection/mod.rs b/backend/crates/gateway-api/tests/openai/responses/websocket/connection/mod.rs index 794433aca..385c51972 100644 --- a/backend/crates/gateway-api/tests/openai/responses/websocket/connection/mod.rs +++ b/backend/crates/gateway-api/tests/openai/responses/websocket/connection/mod.rs @@ -1,3 +1,5 @@ +//! Responses WebSocket 连接生命周期与多轮请求边界测试 + mod middleware; use std::{ @@ -240,7 +242,7 @@ impl Stream for TestSocket { fn poll_next(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll> { let event = self.incoming.poll_recv(context); - // 模拟 Axum transport 的自动 Pong;pump 不再重复实现协议层应答。 + // 模拟 Axum transport 的自动 Pong;pump 不再重复实现协议层应答 if let Poll::Ready(Some(Ok(Message::Ping(payload)))) = &event { self.written .send(Message::Pong(payload.clone())) @@ -604,7 +606,7 @@ async fn idle_connection_reaches_the_official_limit_without_starting_an_executio ); assert_eq!(trace.starts.load(Ordering::Acquire), 0); - // 服务端已因生命周期到期关闭;不再向关闭中的连接写入。 + // 服务端已因生命周期到期关闭;不再向关闭中的连接写入 drop(socket); server.abort(); } @@ -1113,7 +1115,7 @@ async fn active_response_receives_heartbeats_during_two_hundred_seconds_of_silen let ClientMessage::Ping(payload) = message else { panic!("expected heartbeat while business stream is silent: {message:?}"); }; - // 与官方 WsStream 一样,在业务响应未完成时回复同 payload 的 Pong。 + // 与官方 WsStream 一样,在业务响应未完成时回复同 payload 的 Pong socket .send(ClientMessage::Pong(payload)) .await diff --git a/backend/crates/gateway-api/tests/openai/responses/websocket/forward.rs b/backend/crates/gateway-api/tests/openai/responses/websocket/forward.rs index b7530e782..c0c407169 100644 --- a/backend/crates/gateway-api/tests/openai/responses/websocket/forward.rs +++ b/backend/crates/gateway-api/tests/openai/responses/websocket/forward.rs @@ -1,3 +1,5 @@ +//! Responses WebSocket 事件转发、错误去重与连接恢复测试 + use std::collections::VecDeque; use std::sync::{ Arc, Mutex, @@ -31,7 +33,7 @@ use crate::openai::{api_router, authenticated_client}; type TestSocket = WebSocketStream>; -// 全部使用合成响应事实和真实 API 路由,不读取账号、日志或请求转储。 +// 全部使用合成响应事实和真实 API 路由,不读取账号、日志或请求转储 struct TestServer(tokio::task::JoinHandle<()>); impl Drop for TestServer { @@ -104,7 +106,7 @@ async fn next_error(socket: &mut TestSocket) -> Value { } async fn assert_no_duplicate_failure(socket: &mut TestSocket) { - // 后续入站帧只会在本轮收敛后处理,比固定等待窗口更可靠地检查没有重复失败终态。 + // 后续入站帧只会在本轮收敛后处理,比固定等待窗口更可靠地检查没有重复失败终态 socket.send(Message::Text("{".into())).await.unwrap(); let error = next_event(socket).await; assert_eq!(error["error"]["code"], "invalid_json"); @@ -209,7 +211,7 @@ async fn initial_failure_retains_execution_until_request_middleware_returns() { #[tokio::test] async fn initial_http_or_opening_failure_preserves_status_and_structured_error() { - // 分类故意都用 Unavailable:不能把收到的 401/429/500 等按网关分类改成 502。 + // 分类故意都用 Unavailable:不能把收到的 401/429/500 等按网关分类改成 502 for status in [302, 400, 401, 403, 429, 500, 502, 503] { let provider = upstream_failure( status, @@ -460,7 +462,7 @@ async fn opening_ids_stay_in_metadata_but_do_not_identify_initial_failures() { ) .await; send_request(&mut socket).await; - // metadata 仍交付原会话观测;opening ID 不能因此成为当前失败的请求身份。 + // metadata 仍交付原会话观测;opening ID 不能因此成为当前失败的请求身份 assert_eq!( next_event(&mut socket).await, json!({ diff --git a/backend/crates/gateway-api/tests/openai/responses/websocket/interrupt.rs b/backend/crates/gateway-api/tests/openai/responses/websocket/interrupt.rs index 3265e5483..9b83dcd4b 100644 --- a/backend/crates/gateway-api/tests/openai/responses/websocket/interrupt.rs +++ b/backend/crates/gateway-api/tests/openai/responses/websocket/interrupt.rs @@ -1,3 +1,5 @@ +//! 验证 Responses WebSocket 中断向当前执行传递并释放相关资源 + use super::*; use gateway_core::engine::response_control::{ResponseControl, ResponseInterruptError}; diff --git a/backend/crates/gateway-api/tests/openai/responses/websocket/mod.rs b/backend/crates/gateway-api/tests/openai/responses/websocket/mod.rs index d5df4201f..fd6e5b871 100644 --- a/backend/crates/gateway-api/tests/openai/responses/websocket/mod.rs +++ b/backend/crates/gateway-api/tests/openai/responses/websocket/mod.rs @@ -1,3 +1,5 @@ +//! Responses WebSocket 测试入口,以及请求帧与轮次元数据解析测试 + mod connection; mod forward; mod interrupt; @@ -685,7 +687,7 @@ async fn websocket_response_metadata_should_preserve_ordinary_headers_without_bl .and_then(Value::as_object) .expect("response metadata headers"); assert_eq!(headers.get("x-future-header"), Some(&json!("future-value"))); - // 官方 metadata 是 string map,同名多值只能保持既有的后值覆盖语义。 + // 官方 metadata 是 string map,同名多值只能保持既有的后值覆盖语义 assert_eq!(headers.get("x-future-multi"), Some(&json!("second"))); assert_eq!(headers.get("x-request-id"), Some(&json!("req_upstream"))); assert_eq!( @@ -1165,7 +1167,7 @@ async fn websocket_disconnect_during_core_settlement_finishes_charge_before_rele assert_eq!(ports.releases.load(Ordering::SeqCst), 0); // 等到生产连接 guard 释放,保证 forward 的 select 已因断连取消 next_event; - // 在此之前不能打开结算屏障,否则只会验证正常完成而错过取消窗口。 + // 在此之前不能打开结算屏障,否则只会验证正常完成而错过取消窗口 socket.close(None).await.unwrap(); tokio::time::timeout(Duration::from_secs(5), lifecycle.closed.notified()) .await @@ -1214,7 +1216,7 @@ async fn get_responses_should_route_to_the_websocket_upgrade_boundary() { .expect("route WebSocket upgrade request"); // oneshot 请求不携带升级状态;426 证明 GET /v1/responses 进入的是 - // WebSocketUpgrade 边界而不是普通 HTTP handler。 + // WebSocketUpgrade 边界而不是普通 HTTP handler assert_eq!(response.status(), StatusCode::UPGRADE_REQUIRED); } @@ -1227,7 +1229,7 @@ async fn websocket_upgradability_should_be_checked_before_authentication() { .expect("route unauthenticated upgrade request"); // 升级能力在 extractor 阶段先于 handler 内的 API Key 认证被校验, - // 无效凭据得到的仍是升级失败而不是 401。 + // 无效凭据得到的仍是升级失败而不是 401 assert_eq!(response.status(), StatusCode::UPGRADE_REQUIRED); } diff --git a/backend/crates/gateway-api/tests/openai/responses/websocket/protocol.rs b/backend/crates/gateway-api/tests/openai/responses/websocket/protocol.rs index 49b8027c1..be9e68b3a 100644 --- a/backend/crates/gateway-api/tests/openai/responses/websocket/protocol.rs +++ b/backend/crates/gateway-api/tests/openai/responses/websocket/protocol.rs @@ -1,3 +1,5 @@ +//! 验证 WebSocket 请求逐帧保留来源头部并接受协议样本 + use axum::http::{HeaderMap, HeaderValue}; use base64::{Engine as _, engine::general_purpose::STANDARD}; use gateway_api::openai::responses::{OpenAiRequestHeaders, decode_response_create_with_context}; @@ -24,7 +26,7 @@ fn response_create_should_carry_source_headers_to_provider_on_every_frame() { headers.append("x-openai-future-mode", HeaderValue::from_static("second")); let request_headers = OpenAiRequestHeaders::from_headers(&headers); - // 连接级头随每一帧传给 Provider,由相同的出站边界处理来源适配。 + // 连接级头随每一帧传给 Provider,由相同的出站边界处理来源适配 for input in ["hello", "continue"] { let decoded = decode_response_create_with_context( &json!({"type": "response.create", "model": "smart-code", "input": input}).to_string(), @@ -52,7 +54,7 @@ fn response_create_should_carry_source_headers_to_provider_on_every_frame() { #[test] fn latest_official_codex_response_create_fixture_decodes_unchanged() { - // Audited against openai/codex main 94cbbddafc1776d5e377bca1b05932c697e82238. + // 已对照 openai/codex 提交 94cbbddafc1776d5e377bca1b05932c697e82238 核验 let decoded = decode_response_create( &json!({ "type": "response.create", diff --git a/backend/crates/gateway-api/tests/openai/router.rs b/backend/crates/gateway-api/tests/openai/router.rs index fa06205df..68220ff6f 100644 --- a/backend/crates/gateway-api/tests/openai/router.rs +++ b/backend/crates/gateway-api/tests/openai/router.rs @@ -1,3 +1,5 @@ +//! 验证 OpenAI 路由暴露范围、请求体限制与跨域行为 + use axum::{ body::Body, http::{Method, Request, StatusCode, header::AUTHORIZATION}, @@ -174,8 +176,8 @@ async fn responses_body_should_accept_payload_above_the_removed_private_limit() .await .expect("route request above the removed limit"); - // The handler sees the body and rejects the missing credentials. A restored body limit - // would return 413 before authentication runs. + // 请求进入处理器后应因缺少凭据而被拒绝 + // 若提前施加正文大小限制,则会在认证前返回 413 assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } diff --git a/backend/crates/gateway-api/tests/openai/search/mod.rs b/backend/crates/gateway-api/tests/openai/search/mod.rs index 0bddb7ef3..161f97bf9 100644 --- a/backend/crates/gateway-api/tests/openai/search/mod.rs +++ b/backend/crates/gateway-api/tests/openai/search/mod.rs @@ -1,3 +1,5 @@ +//! 独立搜索 HTTP 接口的请求传递、鉴权与响应交付测试 + use std::sync::{Arc, Mutex}; use std::time::SystemTime; diff --git a/backend/crates/gateway-api/tests/openai/usage.rs b/backend/crates/gateway-api/tests/openai/usage.rs index d19dd482d..3ee501237 100644 --- a/backend/crates/gateway-api/tests/openai/usage.rs +++ b/backend/crates/gateway-api/tests/openai/usage.rs @@ -1,3 +1,5 @@ +//! 验证当前 Key 预算查询的身份隔离与只读行为 + use std::sync::{ Arc, atomic::{AtomicUsize, Ordering}, diff --git a/backend/crates/gateway-api/tests/provider.rs b/backend/crates/gateway-api/tests/provider.rs index ec821607e..c1ac01534 100644 --- a/backend/crates/gateway-api/tests/provider.rs +++ b/backend/crates/gateway-api/tests/provider.rs @@ -1,3 +1,5 @@ +//! Provider 原生入口的请求捕获、协议传递与响应交付测试 + use std::{ sync::{ Arc, Mutex, diff --git a/backend/crates/gateway-api/tests/support.rs b/backend/crates/gateway-api/tests/support.rs index b7051ff1a..67c757589 100644 --- a/backend/crates/gateway-api/tests/support.rs +++ b/backend/crates/gateway-api/tests/support.rs @@ -1,3 +1,5 @@ +//! API 测试共用的应用组装、认证请求与服务替身 + use std::{ net::{IpAddr, Ipv4Addr, SocketAddr}, sync::{Arc, atomic::Ordering}, diff --git a/backend/crates/gateway-core/src/account/error.rs b/backend/crates/gateway-core/src/account/error.rs index 1ceae4e86..b5fcfe795 100644 --- a/backend/crates/gateway-core/src/account/error.rs +++ b/backend/crates/gateway-core/src/account/error.rs @@ -1,8 +1,8 @@ -//! Provider credential 值对象构造错误。 +//! Provider credential 值对象构造错误 use thiserror::Error; -/// Credential 值对象构造错误。 +/// Credential 值对象构造错误 #[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] pub enum CredentialError { #[error("credential revision must be greater than zero")] diff --git a/backend/crates/gateway-core/src/account/location.rs b/backend/crates/gateway-core/src/account/location.rs index 9d76c01f5..92ff31e07 100644 --- a/backend/crates/gateway-core/src/account/location.rs +++ b/backend/crates/gateway-core/src/account/location.rs @@ -1,4 +1,4 @@ -//! 请求位置只描述出口的业务配置,不改变代理连接身份或服务系统时区。 +//! 请求位置只描述出口的业务配置,不改变代理连接身份或服务系统时区 use serde::{Deserialize, Serialize}; @@ -41,7 +41,7 @@ impl RequestLocation { (&self.region, InvalidRequestLocation::Region), (&self.city, InvalidRequestLocation::City), ] { - // 先检查原始输入,避免 trim 掩盖首尾的换行和其他控制字符。 + // 先检查原始输入,避免 trim 掩盖首尾的换行和其他控制字符 if value.chars().any(char::is_control) || value.trim().is_empty() || value.trim().chars().count() > 128 diff --git a/backend/crates/gateway-core/src/account/mod.rs b/backend/crates/gateway-core/src/account/mod.rs index 3b6d0d42f..f59339d0d 100644 --- a/backend/crates/gateway-core/src/account/mod.rs +++ b/backend/crates/gateway-core/src/account/mod.rs @@ -1,4 +1,4 @@ -//! Provider 账号领域、持久化端口与同一 target 内的账号选择。 +//! Provider 账号领域、持久化端口与同一 target 内的账号选择 mod error; mod location; diff --git a/backend/crates/gateway-core/src/account/model.rs b/backend/crates/gateway-core/src/account/model.rs index 9c6f61272..7ae3c35a6 100644 --- a/backend/crates/gateway-core/src/account/model.rs +++ b/backend/crates/gateway-core/src/account/model.rs @@ -1,4 +1,4 @@ -//! Provider 账号、明文 credential 与持久状态值对象。 +//! Provider 账号、明文 credential 与持久状态值对象 use std::fmt; use std::num::{NonZeroU16, NonZeroU32, NonZeroU64}; @@ -11,30 +11,30 @@ use crate::validation::{IdentifierError, validate_text}; use super::CredentialError; -/// `provider_accounts.id` 的核心值对象。 +/// `provider_accounts.id` 的核心值对象 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ProviderAccountId(String); impl ProviderAccountId { - /// 校验并创建账号 ID。 + /// 校验并创建账号 ID /// /// # Errors /// - /// ID 缺少 `acct_` 前缀或不满足通用文本约束时返回错误。 + /// ID 缺少 `acct_` 前缀或不满足通用文本约束时返回错误 pub fn new(value: impl Into) -> Result { let value = value.into(); validate_text(&value, 128, false, Some("acct_"))?; Ok(Self(value)) } - /// 返回数据库 ID 文本。 + /// 返回数据库 ID 文本 #[must_use] pub fn as_str(&self) -> &str { &self.0 } } -/// Per-account scheduling concurrency override. +/// 单个账号的调度并发覆盖值 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct AccountConcurrencyLimit(NonZeroU32); @@ -58,7 +58,7 @@ impl AccountConcurrencyLimit { } } -/// 账号继承默认值或应用独立覆盖后的实际并发约束。 +/// 账号继承默认值或应用独立覆盖后的实际并发约束 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AccountConcurrency { Unlimited, @@ -66,7 +66,7 @@ pub enum AccountConcurrency { } impl AccountConcurrency { - /// 配置值零表示不限制;账号独立覆盖仍只接受正数。 + /// 配置值零表示不限制;账号独立覆盖仍只接受正数 #[must_use] pub const fn new(value: u32) -> Self { match NonZeroU32::new(value) { @@ -91,7 +91,7 @@ impl AccountConcurrency { } } - /// 扣除当前请求不可占用的预留名额;不限并发不受影响,有限上限至少保留 1 个名额。 + /// 扣除当前请求不可占用的预留名额;不限并发不受影响,有限上限至少保留 1 个名额 #[must_use] pub const fn excluding_reserved(self, reserved: u32) -> Self { match self { @@ -110,7 +110,7 @@ impl From for AccountConcurrency { } } -/// Relative scheduling priority for an account. +/// 账号的相对调度优先级 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct AccountWeight(NonZeroU16); @@ -144,7 +144,7 @@ impl fmt::Display for ProviderAccountId { } } -/// credential 轮换时可选替换的上游账号身份。 +/// credential 轮换时可选替换的上游账号身份 #[derive(Clone, PartialEq, Eq)] pub struct ProviderAccountIdentity { upstream_user_id: String, @@ -184,33 +184,33 @@ impl ProviderAccountIdentity { } } -/// `provider_accounts.credential_revision` 的正数 CAS revision。 +/// `provider_accounts.credential_revision` 的正数 CAS revision #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct CredentialRevision(NonZeroU64); impl CredentialRevision { - /// 创建正数 revision。 + /// 创建正数 revision /// /// # Errors /// - /// `value` 为零时返回错误。 + /// `value` 为零时返回错误 pub fn new(value: u64) -> Result { NonZeroU64::new(value) .map(Self) .ok_or(CredentialError::InvalidRevision) } - /// 返回 revision 数值。 + /// 返回 revision 数值 #[must_use] pub const fn get(self) -> u64 { self.0.get() } - /// 返回下一个 revision;溢出时返回错误。 + /// 返回下一个 revision;溢出时返回错误 /// /// # Errors /// - /// 当前 revision 已是 `u64::MAX` 时返回错误。 + /// 当前 revision 已是 `u64::MAX` 时返回错误 pub fn next(self) -> Result { self.get() .checked_add(1) @@ -220,26 +220,26 @@ impl CredentialRevision { } } -/// Provider-owned 的明文 credential JSON。 +/// Provider-owned 的明文 credential JSON /// -/// Core 只保证顶层是 object,绝不读取其中的 AT、RT、Cookie 或 Provider key。 +/// Core 只保证顶层是 object,绝不读取其中的 AT、RT、Cookie 或 Provider key #[derive(Clone, PartialEq)] pub struct PlaintextCredential(Map); impl PlaintextCredential { - /// 接受由具体 Provider 完整校验后的 JSON object。 + /// 接受由具体 Provider 完整校验后的 JSON object #[must_use] pub const fn new(value: Map) -> Self { Self(value) } - /// 将明文 object 借给对应 Provider adapter。 + /// 将明文 object 借给对应 Provider adapter #[must_use] pub const fn expose_to_provider(&self) -> &Map { &self.0 } - /// 将明文 object 交给 Store adapter 持久化。 + /// 将明文 object 交给 Store adapter 持久化 #[must_use] pub fn into_inner(self) -> Map { self.0 @@ -256,7 +256,7 @@ impl fmt::Debug for PlaintextCredential { } } -/// Provider-owned 的任意 JSON object;公共层只搬运、不读取内部 key。 +/// Provider-owned 的任意 JSON object;公共层只搬运、不读取内部 key #[derive(Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(transparent)] pub struct OpaqueProviderData(Map); @@ -288,10 +288,11 @@ impl fmt::Debug for OpaqueProviderData { } } -/// 已持久化的凭据/账号身份状态。 +/// 已持久化的凭据/账号身份状态 /// -/// 这里只保存凭据与账号身份事实。额度耗尽属于 [`QuotaState`],临时 429 属于 -/// 运行时冷却;二者都不得写入此枚举。 +/// 这里只保存凭据与账号身份事实 +/// 额度耗尽属于 [`QuotaState`],临时 429 属于 +/// 运行时冷却;二者都不得写入此枚举 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum CredentialState { Unknown, @@ -302,7 +303,7 @@ pub enum CredentialState { } impl CredentialState { - /// 返回数据库稳定值。 + /// 返回数据库稳定值 #[must_use] pub const fn as_str(self) -> &'static str { match self { @@ -314,7 +315,7 @@ impl CredentialState { } } - /// 返回该凭据状态对应的稳定错误原因。 + /// 返回该凭据状态对应的稳定错误原因 #[must_use] pub const fn error_reason(self) -> Option { match self { @@ -326,7 +327,7 @@ impl CredentialState { } } - /// 解析数据库稳定值。 + /// 解析数据库稳定值 #[must_use] pub fn parse(value: &str) -> Option { match value { @@ -340,7 +341,7 @@ impl CredentialState { } } -/// 额度访问结论;百分比和余额等展示值不进入此枚举。 +/// 额度访问结论;百分比和余额等展示值不进入此枚举 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum QuotaAccessState { Unknown, @@ -369,7 +370,7 @@ impl QuotaAccessState { } } -/// 权威额度耗尽证据。 +/// 权威额度耗尽证据 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum QuotaEvidence { ProviderDenied, @@ -401,7 +402,7 @@ impl QuotaEvidence { } } -/// 可审计的额度访问事实。 +/// 可审计的额度访问事实 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct QuotaState { access: QuotaAccessState, @@ -461,7 +462,7 @@ impl QuotaState { } } - /// 从持久化列恢复额度状态;非法组合返回 `None`。 + /// 从持久化列恢复额度状态;非法组合返回 `None` #[must_use] pub const fn from_persisted( access: QuotaAccessState, @@ -518,17 +519,17 @@ impl QuotaState { self.reset_at } - /// 返回当前已确认的额度耗尽结论。 + /// 返回当前已确认的额度耗尽结论 /// - /// `reset_at` 只表示何时应重新向 Provider 求证;时间到期本身不是额度恢复证据。 + /// `reset_at` 只表示何时应重新向 Provider 求证;时间到期本身不是额度恢复证据 #[must_use] pub fn is_exhausted(self) -> bool { self.access == QuotaAccessState::Exhausted } - /// 判断已耗尽额度是否到达 Provider 的下一次复核时间。 + /// 判断已耗尽额度是否到达 Provider 的下一次复核时间 /// - /// 有明确 `reset_at` 时严格等待该时刻;没有时由 Provider 传入自己的保守复核周期。 + /// 有明确 `reset_at` 时严格等待该时刻;没有时由 Provider 传入自己的保守复核周期 #[must_use] pub fn exhaustion_refresh_due(self, now: SystemTime, fallback_interval: Duration) -> bool { if !self.is_exhausted() { @@ -541,7 +542,7 @@ impl QuotaState { due_at.is_none_or(|due_at| due_at <= now) } - /// 合并一次额度访问观察;`Unknown` 不能擦除已经确认的访问结论。 + /// 合并一次额度访问观察;`Unknown` 不能擦除已经确认的访问结论 #[must_use] pub fn merge_observation(self, observation: Self) -> Self { if observation.access == QuotaAccessState::Unknown @@ -554,7 +555,7 @@ impl QuotaState { } } -/// 对外唯一的五态账号状态。 +/// 对外唯一的五态账号状态 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum AccountStatus { Normal, @@ -588,7 +589,7 @@ impl AccountStatus { } } - /// 管理页稳定业务排序:正常、限流、耗尽、错误、停用。 + /// 管理页稳定业务排序:正常、限流、耗尽、错误、停用 #[must_use] pub const fn sort_rank(self) -> u8 { match self { @@ -601,7 +602,7 @@ impl AccountStatus { } } -/// `error` 状态下的稳定原因码。 +/// `error` 状态下的稳定原因码 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum AccountErrorReason { AccountUnverified, @@ -636,15 +637,15 @@ impl AccountErrorReason { } } -/// 账号冷却的原因与恢复方式。 +/// 账号冷却的原因与恢复方式 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum AccountCooldownKind { - /// 上游 429 临时限流;默认类别,兼容升级前未标记的存量 key。 + /// 上游 429 临时限流;默认类别,兼容升级前未标记的存量 key #[default] RateLimit, - /// 容量类错误高频触发后由熔断策略写入的自动冻结。 + /// 容量类错误高频触发后由熔断策略写入的自动冻结 CapacityFreeze, - /// 到期后仍阻止调度,必须由恢复探测成功或管理员恢复解除。 + /// 到期后仍阻止调度,必须由恢复探测成功或管理员恢复解除 CapacityFreezeProbe, } @@ -679,7 +680,7 @@ impl AccountCooldownKind { } } -/// 账号级冷却投影;探测冻结的 until 是下次探测时间,不是自动放行时间。 +/// 账号级冷却投影;探测冻结的 until 是下次探测时间,不是自动放行时间 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct AccountCooldown { pub until: SystemTime, @@ -702,7 +703,7 @@ impl From for AccountCooldown { } } -/// 唯一状态解析器的完整输入事实。 +/// 唯一状态解析器的完整输入事实 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AccountStatusFacts { pub enabled: bool, @@ -714,17 +715,17 @@ pub struct AccountStatusFacts { pub last_error_message: Option, } -/// 唯一状态解析器的输出。 +/// 唯一状态解析器的输出 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AccountStatusProjection { pub status: AccountStatus, pub error_reason: Option, pub error_message: Option, - /// 仅 `rate_limited` 状态携带仍有效的运行时冷却事实。 + /// 仅 `rate_limited` 状态携带仍有效的运行时冷却事实 pub cooldown: Option, } -/// 从独立事实派生唯一、互斥的对外状态。 +/// 从独立事实派生唯一、互斥的对外状态 #[must_use] pub fn resolve_account_status( facts: &AccountStatusFacts, @@ -773,7 +774,7 @@ const fn status_projection(status: AccountStatus) -> AccountStatusProjection { } } -/// 账号持久事实;代理认证信息只通过显式 secret accessor 读取。 +/// 账号持久事实;代理认证信息只通过显式 secret accessor 读取 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderAccount { id: ProviderAccountId, @@ -801,7 +802,7 @@ pub struct ProviderAccount { } impl ProviderAccount { - /// 创建账号快照。 + /// 创建账号快照 #[must_use] pub const fn new( id: ProviderAccountId, @@ -864,7 +865,7 @@ impl ProviderAccount { #[must_use] pub fn with_outbound_proxy(mut self, proxy: Option) -> Self { - // 出口变化后不能沿用旧出口的位置;存储投影应在绑定出口后设置位置。 + // 出口变化后不能沿用旧出口的位置;存储投影应在绑定出口后设置位置 self.request_location = None; self.outbound_proxy = proxy; self @@ -896,7 +897,7 @@ impl ProviderAccount { last_error_message: Option, ) -> Self { self.enabled = enabled; - // 凭据是否可用由 Provider 判断;API Key 等认证不要求上游用户身份。 + // 凭据是否可用由 Provider 判断;API Key 等认证不要求上游用户身份 self.credential_state = credential_state; self.quota = quota; self.last_error_reason = last_error_reason; @@ -915,10 +916,10 @@ impl ProviderAccount { self } - /// 设置 RT 存在性与失败后的最早重试时刻。 + /// 设置 RT 存在性与失败后的最早重试时刻 /// /// 正常 OAuth 预刷新由 worker 使用 AT 原始过期时间与当前运行时策略动态判断, - /// 不应把提前量物化到 `next_refresh_at`。 + /// 不应把提前量物化到 `next_refresh_at` #[must_use] pub const fn with_refresh_schedule( mut self, @@ -1023,7 +1024,7 @@ impl ProviderAccount { self.access_token_expires_at } - /// 返回瞬态 OAuth 刷新失败后的最早重试时刻;正常账号为 `None`。 + /// 返回瞬态 OAuth 刷新失败后的最早重试时刻;正常账号为 `None` #[must_use] pub const fn next_refresh_at(&self) -> Option { self.next_refresh_at @@ -1034,7 +1035,7 @@ impl ProviderAccount { self.has_refresh_token } - /// 组合持久事实与请求级冷却,交给唯一解析器派生状态。 + /// 组合持久事实与请求级冷却,交给唯一解析器派生状态 #[must_use] pub fn status_projection( &self, @@ -1056,17 +1057,17 @@ impl ProviderAccount { } } -/// Store 读出的账号与 Provider-owned 明文 credential。 +/// Store 读出的账号与 Provider-owned 明文 credential #[derive(Clone, PartialEq)] pub struct LoadedCredential { pub account: ProviderAccount, pub credential: PlaintextCredential, } -/// Provider 已计算好时间边界的有界 OAuth refresh 候选查询。 +/// Provider 已计算好时间边界的有界 OAuth refresh 候选查询 /// -/// Store 只负责按持久事实筛选和稳定排序,不拥有提前量或恢复窗口语义。 -/// 调度启停不影响凭据续期;停用账号仍按 Provider 的凭据状态与时间边界刷新。 +/// Store 只负责按持久事实筛选和稳定排序,不拥有提前量或恢复窗口语义 +/// 调度启停不影响凭据续期;停用账号仍按 Provider 的凭据状态与时间边界刷新 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderRefreshQuery { provider: ProviderKind, @@ -1127,7 +1128,7 @@ impl ProviderRefreshQuery { self.limit } - /// 判断内存 Store 中的账号是否满足与 PostgreSQL 相同的候选谓词。 + /// 判断内存 Store 中的账号是否满足与 PostgreSQL 相同的候选谓词 #[must_use] pub fn contains(&self, account: &ProviderAccount) -> bool { account.provider() == &self.provider @@ -1147,11 +1148,11 @@ impl ProviderRefreshQuery { } } -/// Admin/Provider import 创建账号时的一次性明文输入。 +/// Admin/Provider import 创建账号时的一次性明文输入 #[derive(Clone, PartialEq)] pub struct NewProviderAccount { pub account: ProviderAccount, - /// 导入时显式提供的政策;省略时保留已有账号设置。 + /// 导入时显式提供的政策;省略时保留已有账号设置 pub model_access: Option, pub credential: PlaintextCredential, } @@ -1166,7 +1167,7 @@ impl fmt::Debug for NewProviderAccount { } } -/// 不改 credential revision 的管理字段更新。 +/// 不改 credential revision 的管理字段更新 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderAccountUpdate { pub account_id: ProviderAccountId, @@ -1185,7 +1186,7 @@ impl fmt::Debug for LoadedCredential { } } -/// 与 credential revision CAS 同事务提交的账号错误事实。 +/// 与 credential revision CAS 同事务提交的账号错误事实 #[derive(Debug, Clone, PartialEq, Eq)] pub struct CredentialStateWrite { pub credential_state: CredentialState, @@ -1194,7 +1195,7 @@ pub struct CredentialStateWrite { pub message: Option, } -/// [`CredentialCasUpdate`] 跨 store 边界的命名字段。 +/// [`CredentialCasUpdate`] 跨 store 边界的命名字段 pub struct CredentialCasUpdateParts { pub account_id: ProviderAccountId, pub expected_revision: CredentialRevision, @@ -1207,7 +1208,7 @@ pub struct CredentialCasUpdateParts { pub account_state: Option>, } -/// 刷新后的完整 CAS 写回。 +/// 刷新后的完整 CAS 写回 #[derive(Clone, PartialEq)] pub struct CredentialCasUpdate { account_id: ProviderAccountId, @@ -1239,11 +1240,11 @@ impl fmt::Debug for CredentialCasUpdate { } impl CredentialCasUpdate { - /// 创建同一账号 revision fence 下的完整 credential + 普通投影写回。 + /// 创建同一账号 revision fence 下的完整 credential + 普通投影写回 /// /// # Errors /// - /// profile 与 credential 指向不同账号,或无 RT 却声明下次刷新时间时失败。 + /// profile 与 credential 指向不同账号,或无 RT 却声明下次刷新时间时失败 pub fn new( account_id: ProviderAccountId, expected_revision: CredentialRevision, @@ -1272,14 +1273,14 @@ impl CredentialCasUpdate { }) } - /// 仅轮换凭据,保留提交时的账号资料,避免覆盖并发额度观测更新的套餐。 + /// 仅轮换凭据,保留提交时的账号资料,避免覆盖并发额度观测更新的套餐 #[must_use] pub const fn preserving_profile(mut self) -> Self { self.preserve_profile = true; self } - /// 将刷新调度与账号错误事实放入同一个 revision CAS。 + /// 将刷新调度与账号错误事实放入同一个 revision CAS #[must_use] pub fn with_account_state( mut self, @@ -1354,24 +1355,24 @@ impl CredentialCasUpdate { } } -/// CAS 写回结果。 +/// CAS 写回结果 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CredentialCasOutcome { Updated(CredentialRevision), Conflict, } -/// Provider quota 的一次完整观察结果。 +/// Provider quota 的一次完整观察结果 #[derive(Clone, PartialEq)] pub struct QuotaObservation { pub account_id: ProviderAccountId, pub expected_revision: CredentialRevision, pub quota: OpaqueProviderData, - /// Provider 确认的账号套餐,与额度原子写入;`None` 保留已有套餐。 + /// Provider 确认的账号套餐,与额度原子写入;`None` 保留已有套餐 pub plan_type: Option, - /// Provider 原始 quota document 的观察时间;不代表访问结论发生变化。 + /// Provider 原始 quota document 的观察时间;不代表访问结论发生变化 pub observed_at: SystemTime, - /// Provider 已从私有 JSON 归一化出的额度访问事实。 + /// Provider 已从私有 JSON 归一化出的额度访问事实 pub state: QuotaState, } @@ -1395,7 +1396,7 @@ pub enum QuotaWriteOutcome { Conflict, } -/// 只推进 Provider quota 文档的最后成功查询时间。 +/// 只推进 Provider quota 文档的最后成功查询时间 #[derive(Debug, Clone, PartialEq, Eq)] pub struct QuotaObservationTouch { pub account_id: ProviderAccountId, @@ -1403,7 +1404,7 @@ pub struct QuotaObservationTouch { pub observed_at: SystemTime, } -/// 不改 Provider 原始 quota JSON 的额度访问事实写入。 +/// 不改 Provider 原始 quota JSON 的额度访问事实写入 #[derive(Debug, Clone, PartialEq, Eq)] pub struct QuotaAccessChange { pub account_id: ProviderAccountId, @@ -1411,19 +1412,19 @@ pub struct QuotaAccessChange { pub state: QuotaState, } -/// 账号状态的 revision-fenced 写入。 +/// 账号状态的 revision-fenced 写入 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AccountStateChange { pub account_id: ProviderAccountId, pub expected_revision: CredentialRevision, pub credential_state: CredentialState, pub observed_at: SystemTime, - /// 受控错误原因;无失败事实的 Ready 写入必须清空。 + /// 受控错误原因;无失败事实的 Ready 写入必须清空 /// /// Ready 凭据可以在 AT 过期后继续 RT 恢复,此时允许保留最近一次 - /// 刷新失败原因,供错误状态投影展示。 + /// 刷新失败原因,供错误状态投影展示 pub error_reason: Option, - /// 供管理端展示的错误消息;结构化上游失败应保留原始 message,不能写入整个正文。 - /// 刷新成功或其他无失败事实的 Ready 写入必须清空。 + /// 供管理端展示的错误消息;结构化上游失败应保留原始 message,不能写入整个正文 + /// 刷新成功或其他无失败事实的 Ready 写入必须清空 pub message: Option, } diff --git a/backend/crates/gateway-core/src/account/model_access.rs b/backend/crates/gateway-core/src/account/model_access.rs index 549ce3141..5a441e6c6 100644 --- a/backend/crates/gateway-core/src/account/model_access.rs +++ b/backend/crates/gateway-core/src/account/model_access.rs @@ -1,4 +1,4 @@ -//! 管理员配置的账号模型政策,与上游模型权限分开保存和判断。 +//! 管理员配置的账号模型政策,与上游模型权限分开保存和判断 use std::collections::BTreeSet; @@ -7,10 +7,10 @@ use thiserror::Error; use crate::validation::validate_text; -/// 一个账号最多配置的精确模型 ID 数量。 +/// 一个账号最多配置的精确模型 ID 数量 pub const MAX_ACCOUNT_ACCESS_MODELS: usize = 256; -/// 账号模型政策的匹配方式。 +/// 账号模型政策的匹配方式 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum AccountModelAccessMode { @@ -20,14 +20,14 @@ pub enum AccountModelAccessMode { Denylist, } -/// 已校验的账号模型政策;私有字段保证所有边界使用相同的匹配规则。 +/// 已校验的账号模型政策;私有字段保证所有边界使用相同的匹配规则 #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct AccountModelAccess { mode: AccountModelAccessMode, models: BTreeSet, } -/// 不携带原始输入,避免在错误边界回显配置正文。 +/// 不携带原始输入,避免在错误边界回显配置正文 #[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] #[error( "modelAccess requires all with no models, or allowlist/denylist with 1–256 exact model IDs (at most 256 bytes each)" diff --git a/backend/crates/gateway-core/src/account/proxy.rs b/backend/crates/gateway-core/src/account/proxy.rs index bbb02edf0..1011fc12c 100644 --- a/backend/crates/gateway-core/src/account/proxy.rs +++ b/backend/crates/gateway-core/src/account/proxy.rs @@ -1,4 +1,4 @@ -//! Explicit account egress. Credentials never appear in Debug or ordinary admin projections. +//! 账号出站代理端点与认证值,诊断及普通管理投影不暴露凭据 use std::fmt; use url::Url; diff --git a/backend/crates/gateway-core/src/account/scope.rs b/backend/crates/gateway-core/src/account/scope.rs index cde953e87..039c875db 100644 --- a/backend/crates/gateway-core/src/account/scope.rs +++ b/backend/crates/gateway-core/src/account/scope.rs @@ -1,4 +1,4 @@ -//! 请求认证时冻结的账号范围与目录,不依赖路由选择器。 +//! 请求认证时冻结的账号范围与目录,不依赖路由选择器 use super::ProviderAccountId; use crate::identity::ProviderKind; @@ -9,12 +9,12 @@ use std::{ sync::Arc, }; -/// `account_groups.id` 的核心值对象。 +/// `account_groups.id` 的核心值对象 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct AccountGroupId(String); impl AccountGroupId { - /// 校验并创建账号分组 ID。 + /// 校验并创建账号分组 ID pub fn new(value: impl Into) -> Result { let value = value.into(); let Some(suffix) = value.strip_prefix("grp_") else { @@ -42,7 +42,7 @@ impl fmt::Display for AccountGroupId { } } -/// 快照中一个账号的 Provider 与分组归属。 +/// 快照中一个账号的 Provider 与分组归属 #[derive(Debug, Clone, PartialEq, Eq)] pub struct RuntimeAccount { provider_kind: ProviderKind, @@ -82,7 +82,7 @@ impl RuntimeAccount { } } -/// 全快照共享的账号、Provider 与分组反向索引。 +/// 全快照共享的账号、Provider 与分组反向索引 #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct RuntimeAccountDirectory { accounts: BTreeMap, @@ -135,7 +135,7 @@ impl RuntimeAccountDirectory { } } -/// 历史请求保存的账号范围种类。 +/// 历史请求保存的账号范围种类 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AccountRoutingScopeKind { All, @@ -152,7 +152,7 @@ impl AccountRoutingScopeKind { } } -/// 请求开始时冻结的分组 ID 与名称。 +/// 请求开始时冻结的分组 ID 与名称 #[derive(Debug, Clone, PartialEq, Eq)] pub struct RoutingGroupSnapshot { id: AccountGroupId, @@ -176,7 +176,7 @@ impl RoutingGroupSnapshot { } } -/// 请求历史所需的完整、稳定账号范围快照。 +/// 请求历史所需的完整、稳定账号范围快照 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AccountRoutingSnapshot { kind: AccountRoutingScopeKind, @@ -211,7 +211,7 @@ impl AccountRoutingSnapshot { } } -/// Key 持久 binding 编译出的账号权限。 +/// Key 持久 binding 编译出的账号权限 #[derive(Debug, Clone, PartialEq, Eq)] pub enum ClientRoutingScope { AllAccounts, @@ -244,7 +244,7 @@ impl ClientRoutingScope { } } -/// 一次认证随 RuntimeSnapshot 冻结的账号目录与 Key 权限。 +/// 一次认证随 RuntimeSnapshot 冻结的账号目录与 Key 权限 #[derive(Debug, Clone, PartialEq, Eq)] pub struct FrozenAccountScope { disable_fast: bool, @@ -257,7 +257,7 @@ pub struct FrozenAccountScope { } impl FrozenAccountScope { - /// 与 Key 授权范围一同冻结;具体字段只由对应 Provider 解释。 + /// 与 Key 授权范围一同冻结;具体字段只由对应 Provider 解释 #[must_use] pub fn with_request_profiles( mut self, @@ -277,7 +277,7 @@ impl FrozenAccountScope { &self.request_profiles } - /// Key 绑定分组的冻结 Fast 限制,与账号成员资格无关。 + /// Key 绑定分组的冻结 Fast 限制,与账号成员资格无关 #[must_use] pub const fn with_disable_fast(mut self, disable_fast: bool) -> Self { self.disable_fast = disable_fast; @@ -306,10 +306,10 @@ impl FrozenAccountScope { } } - /// 在已经冻结的 Key 范围内施加一次请求局部的 Provider/账号交集。 + /// 在已经冻结的 Key 范围内施加一次请求局部的 Provider/账号交集 /// /// 空集合表示该维度不额外收窄;非空集合即使没有有效交集也保持为空, - /// 不能回退为父请求的完整范围。 + /// 不能回退为父请求的完整范围 #[must_use] pub fn restricted_to( &self, @@ -340,7 +340,7 @@ impl FrozenAccountScope { restricted } - /// 排除父 attempt 当前已经持有的账号,避免嵌套调用形成账号 lease 自等待。 + /// 排除父 attempt 当前已经持有的账号,避免嵌套调用形成账号 lease 自等待 #[must_use] pub fn excluding_account(&self, account: ProviderAccountId) -> Self { let mut restricted = self.clone(); @@ -384,7 +384,7 @@ impl FrozenAccountScope { .is_some_and(|account| account.model_access.allows(upstream_model)) } - /// 目录按整个授权账号池过滤,不能只看用于获取元数据的账号政策。 + /// 目录按整个授权账号池过滤,不能只看用于获取元数据的账号政策 #[must_use] pub fn allows_provider_model(&self, provider: &ProviderKind, upstream_model: &str) -> bool { self.provider_kinds.contains(provider) @@ -398,7 +398,7 @@ impl FrozenAccountScope { &self.provider_kinds } - /// 返回冻结目录中账号所属 Provider;调用方仍须另行检查 [`Self::allows`]。 + /// 返回冻结目录中账号所属 Provider;调用方仍须另行检查 [`Self::allows`] #[must_use] pub fn account_provider(&self, account_id: &ProviderAccountId) -> Option<&ProviderKind> { self.directory diff --git a/backend/crates/gateway-core/src/account/selection.rs b/backend/crates/gateway-core/src/account/selection.rs index 78480c88d..9601ff27f 100644 --- a/backend/crates/gateway-core/src/account/selection.rs +++ b/backend/crates/gateway-core/src/account/selection.rs @@ -1,4 +1,4 @@ -//! 请求级账号资格投影、反馈统计与选择算法。 +//! 请求级账号资格投影、反馈统计与选择算法 use std::cmp::Reverse; use std::collections::{BTreeSet, HashMap}; @@ -13,7 +13,7 @@ use super::{ AccountConcurrency, AccountStatus, ProviderAccount, ProviderAccountId, SmartSchedulingConfig, }; -/// `runtime_settings.rotation_strategy` 的稳定值。 +/// `runtime_settings.rotation_strategy` 的稳定值 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "snake_case")] pub enum RotationStrategy { @@ -46,7 +46,7 @@ impl RotationStrategy { } } -/// 从 `runtime_settings` 冻结到一次请求计划的账号调度策略。 +/// 从 `runtime_settings` 冻结到一次请求计划的账号调度策略 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct AccountSelectionPolicy { strategy: RotationStrategy, @@ -77,7 +77,7 @@ impl AccountSelectionPolicy { } } - /// 只传递冻结的运行设置,Guardian 分类与预留策略由 OpenAI Provider 解释。 + /// 只传递冻结的运行设置,Guardian 分类与预留策略由 OpenAI Provider 解释 #[must_use] pub const fn with_openai_guardian_reserved_concurrency(mut self, reserved: u32) -> Self { self.openai_guardian_reserved_concurrency = reserved; @@ -127,13 +127,13 @@ impl AccountSelectionPolicy { } } -/// Store 提供并发事实,Provider 叠加自己解释的额度事实;全部信号均可重建。 +/// Store 提供并发事实,Provider 叠加自己解释的额度事实;全部信号均可重建 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AccountRuntimeSignals { pub in_flight: u32, pub last_started_at: Option, pub quota_reset_at: Option, - /// Provider 归一化的剩余额度基点:0 耗尽,10_000 全部可用。 + /// Provider 归一化的剩余额度基点:0 耗尽,10_000 全部可用 pub quota_remaining_rank: Option, pub cooldown: Option, pub failure_rate_basis_points: Option, @@ -145,7 +145,7 @@ const ACCOUNT_CAPACITY_FAILURE_EWMA_ALPHA: f64 = 0.4; const ACCOUNT_FAILURE_RATE_HALF_LIFE: Duration = Duration::from_secs(15 * 60); const EMPTY_FEEDBACK_SAMPLE: u64 = f64::NAN.to_bits(); -/// 一次真实上游 attempt 对账号级 Smart 调度产生的中立反馈。 +/// 一次真实上游 attempt 对账号级 Smart 调度产生的中立反馈 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AccountAttemptFeedback { Succeeded { first_output_ms: Option }, @@ -194,7 +194,7 @@ impl DecayingAccountFailureRate { impl Default for AccountFeedback { fn default() -> Self { Self { - // 新账号从健康基线开始,首个失败样本按 EWMA 平滑,而不是直接判死。 + // 新账号从健康基线开始,首个失败样本按 EWMA 平滑,而不是直接判死 failure_rate: Mutex::new(DecayingAccountFailureRate::default()), first_output_ms: AtomicU64::new(EMPTY_FEEDBACK_SAMPLE), } @@ -237,14 +237,14 @@ impl AccountFeedback { } } -/// 所有 Provider 共享、按 Provider 和账号隔离的进程内 Smart 健康反馈。 +/// 所有 Provider 共享、按 Provider 和账号隔离的进程内 Smart 健康反馈 #[derive(Debug, Default)] pub struct AccountFeedbackStats { accounts: RwLock>, } impl AccountFeedbackStats { - /// 读取账号当前的错误率与首个有效输出延迟 EWMA。 + /// 读取账号当前的错误率与首个有效输出延迟 EWMA #[must_use] pub fn scheduling_signals( &self, @@ -254,7 +254,7 @@ impl AccountFeedbackStats { self.scheduling_signals_at(provider_kind, account_id, Instant::now()) } - /// 读取账号在指定单调时刻的错误率与首个有效输出延迟 EWMA。 + /// 读取账号在指定单调时刻的错误率与首个有效输出延迟 EWMA #[doc(hidden)] #[must_use] pub fn scheduling_signals_at( @@ -275,7 +275,7 @@ impl AccountFeedbackStats { .unwrap_or_default() } - /// 回灌一次已经真实发送的上游 attempt。 + /// 回灌一次已经真实发送的上游 attempt pub fn report( &self, provider_kind: &ProviderKind, @@ -285,7 +285,7 @@ impl AccountFeedbackStats { self.report_at(provider_kind, account_id, feedback, Instant::now()); } - /// 回灌一次在指定单调时刻真实发送的上游 attempt。 + /// 回灌一次在指定单调时刻真实发送的上游 attempt #[doc(hidden)] pub fn report_at( &self, @@ -370,7 +370,7 @@ impl AccountRuntimeSignals { } } -/// Provider 从私有 quota JSON 投影出的中立调度事实。 +/// Provider 从私有 quota JSON 投影出的中立调度事实 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct AccountQuotaSignals { reset_at: Option, @@ -378,7 +378,7 @@ pub struct AccountQuotaSignals { } impl AccountQuotaSignals { - /// `remaining_rank` 使用剩余额度基点,范围为 0..=10_000。 + /// `remaining_rank` 使用剩余额度基点,范围为 0..=10_000 #[must_use] pub const fn new(reset_at: Option, remaining_rank: Option) -> Self { Self { @@ -398,17 +398,17 @@ impl AccountQuotaSignals { } } -/// 账号持久事实与可重建运行信号的请求级组合。 +/// 账号持久事实与可重建运行信号的请求级组合 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AccountCandidate { pub account: ProviderAccount, pub signals: AccountRuntimeSignals, } -/// 一次账号选择看到的可调度并发槽快照。 +/// 一次账号选择看到的可调度并发槽快照 /// /// `used_slots` 包含刚刚获取成功的当前请求;快照只覆盖本次请求范围内、 -/// 模型可用且未被显式排除的账号池。 +/// 模型可用且未被显式排除的账号池 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct AccountCapacitySnapshot { used_slots: u64, @@ -435,24 +435,24 @@ impl AccountCapacitySnapshot { } } -/// 一次账号选择使用的全局策略快照。 +/// 一次账号选择使用的全局策略快照 #[derive(Debug, Clone)] pub struct AccountSelectionContext { pub policy: AccountSelectionPolicy, pub now: SystemTime, pub excluded_accounts: BTreeSet, pub preferred_account: Option, - /// 优先账号可调度时,是否优先于其它账号的持久权重。 + /// 优先账号可调度时,是否优先于其它账号的持久权重 pub preferred_account_overrides_weight: bool, pub round_robin_cursor: u64, pub eligibility: AccountEligibilityPolicy, pub account_scope: Option>, - /// 本请求不可占用的每账号预留并发名额,由 Provider 按请求类别决定;0 表示不预留。 + /// 本请求不可占用的每账号预留并发名额,由 Provider 按请求类别决定;0 表示不预留 pub reserved_concurrency: u32, } impl AccountSelectionContext { - /// 本请求在该账号上可使用的并发上限;资格判断、租约与策略投影必须共用这一口径。 + /// 本请求在该账号上可使用的并发上限;资格判断、租约与策略投影必须共用这一口径 #[must_use] pub fn concurrency_limit(&self, account: &ProviderAccount) -> AccountConcurrency { account @@ -461,11 +461,11 @@ impl AccountSelectionContext { } } -/// 选择账号时是否执行本地调度资格投影。 +/// 选择账号时是否执行本地调度资格投影 /// /// 管理端对指定账号执行诊断时,会直接向上游确认实际状态;该模式跳过 /// `enabled`、可用性、冷却和 token 到期等本地投影,仍保留租约、并发和 -/// 请求间隔约束,且只能与固定账号约束组合使用。 +/// 请求间隔约束,且只能与固定账号约束组合使用 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum AccountEligibilityPolicy { #[default] @@ -480,7 +480,7 @@ impl AccountEligibilityPolicy { } } -/// 候选账号未进入调度池的约束。 +/// 候选账号未进入调度池的约束 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AccountSchedulingBlocker { OutsideClientScope, @@ -491,7 +491,7 @@ pub enum AccountSchedulingBlocker { LowerWeight, } -/// 优先账号在本次选择中的处理结果。 +/// 优先账号在本次选择中的处理结果 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PreferredAccountSelection { NotRequested, @@ -501,7 +501,7 @@ pub enum PreferredAccountSelection { Blocked(AccountSchedulingBlocker), } -/// 一次完整账号选择;同时保留优先账号决策,供 Provider 记录调度遥测。 +/// 一次完整账号选择;同时保留优先账号决策,供 Provider 记录调度遥测 #[derive(Debug, Clone, Copy)] pub struct AccountSelection<'a> { candidate: &'a AccountCandidate, @@ -520,19 +520,19 @@ impl<'a> AccountSelection<'a> { self.preferred } - /// 显式策略选号不受 Provider 的软亲和等待覆盖;委托内置选号不属于显式选择。 + /// 显式策略选号不受 Provider 的软亲和等待覆盖;委托内置选号不属于显式选择 #[must_use] pub const fn is_policy_choice(self) -> bool { self.policy_choice } } -/// 同一 target 内唯一的账号排序器。 +/// 同一 target 内唯一的账号排序器 #[derive(Debug, Default, Clone, Copy)] pub struct AccountSelector; impl AccountSelector { - /// 返回调度策略可见的全部合格候选;权重层授权由调用策略的宿主适配器裁剪。 + /// 返回调度策略可见的全部合格候选;权重层授权由调用策略的宿主适配器裁剪 #[must_use] pub(crate) fn policy_candidates<'a>( &self, @@ -545,7 +545,7 @@ impl AccountSelector { .collect() } - /// 对插件返回的 ID 再执行同一资格判断,并保留原有亲和遥测结果。 + /// 对插件返回的 ID 再执行同一资格判断,并保留原有亲和遥测结果 #[must_use] pub(crate) fn select_policy_candidate<'a>( &self, @@ -574,7 +574,7 @@ impl AccountSelector { }) } - /// 汇总与本次调度约束一致的并发容量,供请求级观测使用。 + /// 汇总与本次调度约束一致的并发容量,供请求级观测使用 #[must_use] pub fn capacity_snapshot( &self, @@ -607,7 +607,7 @@ impl AccountSelector { }) } - /// 从可调度账号中确定一个候选;这里只消费 Provider 已解析的额度投影。 + /// 从可调度账号中确定一个候选;这里只消费 Provider 已解析的额度投影 #[must_use] pub fn select<'a>( &self, @@ -627,7 +627,7 @@ impl AccountSelector { .max()?; let (mut preferred, preferred_candidate) = self.preferred_decision(candidates, context); if let Some(candidate) = preferred_candidate { - // 权重回切只裁决内置策略的软亲和,插件显式选号按实际选择记录结果。 + // 权重回切只裁决内置策略的软亲和,插件显式选号按实际选择记录结果 let prefer_higher_weight = context.policy.strategy() == RotationStrategy::Smart && context.policy.smart_scheduling().prefer_higher_weight(); if (!context.preferred_account_overrides_weight || prefer_higher_weight) @@ -719,7 +719,7 @@ impl AccountSelector { } } - /// 只有本地并发/调度间隔可等待;账号权限、失效、额度与上游冷却仍立即排除。 + /// 只有本地并发/调度间隔可等待;账号权限、失效、额度与上游冷却仍立即排除 #[must_use] pub fn wait_candidates( &self, @@ -751,7 +751,7 @@ impl AccountSelector { .collect() } - /// Provider 先投影续写范围与资格,再将等待候选交给共用评分规则选择队列。 + /// Provider 先投影续写范围与资格,再将等待候选交给共用评分规则选择队列 pub async fn wait_for_capacity( &self, waiting: &mut CapacityWait<'_, ProviderAccountId>, @@ -764,7 +764,7 @@ impl AccountSelector { if context.policy.strategy() != RotationStrategy::Smart || queue_weight == 0.0 { return waiting.wait(keys).await; } - // 账号信号在队列锁外评分,锁内只读取实时队长并查表,避免扫描候选阻塞其他等待者。 + // 账号信号在队列锁外评分,锁内只读取实时队长并查表,避免扫描候选阻塞其他等待者 let scores = candidates .iter() .map(|candidate| { @@ -833,9 +833,9 @@ impl AccountSelector { } } -// 首输出 10 秒时延迟得分减半;固定尺度不随其他候选账号变化。 +// 首输出 10 秒时延迟得分减半;固定尺度不随其他候选账号变化 const SMART_LATENCY_HALF_SCORE_MS: f64 = 10_000.0; -// 距重置一小时时得分减半;未知和已过期时间不提供重置奖励。 +// 距重置一小时时得分减半;未知和已过期时间不提供重置奖励 const SMART_RESET_HALF_SCORE_SECONDS: f64 = 3_600.0; fn capacity_utilization( @@ -872,7 +872,7 @@ fn select_smart_candidate<'a>( .map(|(_, score)| *score) .max_by(f64::total_cmp)?; ranked.retain(|(_, score)| best_score - score <= config.score_tolerance()); - // 轮换顺序保持稳定,避免分数轻微交错与 cursor 同步后仍反复命中同一账号。 + // 轮换顺序保持稳定,避免分数轻微交错与 cursor 同步后仍反复命中同一账号 ranked.sort_unstable_by(|(left, _), (right, _)| left.account.id().cmp(right.account.id())); let index = (cursor % ranked.len() as u64) as usize; Some(ranked[index].0) diff --git a/backend/crates/gateway-core/src/account/smart_scheduling.rs b/backend/crates/gateway-core/src/account/smart_scheduling.rs index 02fc6ac8b..ada5e080c 100644 --- a/backend/crates/gateway-core/src/account/smart_scheduling.rs +++ b/backend/crates/gateway-core/src/account/smart_scheduling.rs @@ -1,4 +1,4 @@ -//! 智能调度参数;固定十分位保证快照可精确比较,浮点数只用于 wire 与评分。 +//! 智能调度参数;固定十分位保证快照可精确比较,浮点数只用于 wire 与评分 use serde::{Deserialize, Serialize}; @@ -27,7 +27,7 @@ pub enum SmartSchedulingConfigError { } impl SmartSchedulingConfig { - /// 顺序为负载、剩余额度、健康、首输出延迟、额度重置和排队压力。 + /// 顺序为负载、剩余额度、健康、首输出延迟、额度重置和排队压力 pub fn new( weights: [f64; 6], prefer_higher_weight: bool, @@ -73,8 +73,8 @@ impl SmartSchedulingConfig { } pub(crate) fn score_tolerance(self) -> f64 { - // 与系数同步缩放,避免仅放大相同比例的系数就改变近似最优候选集合。 - // 排队系数只参与入队选择,不能放大立即选号的候选容差。 + // 与系数同步缩放,避免仅放大相同比例的系数就改变近似最优候选集合 + // 排队系数只参与入队选择,不能放大立即选号的候选容差 0.05 * f64::from( self.weights[..5] .iter() diff --git a/backend/crates/gateway-core/src/account/store.rs b/backend/crates/gateway-core/src/account/store.rs index 925c0afcb..03c01498c 100644 --- a/backend/crates/gateway-core/src/account/store.rs +++ b/backend/crates/gateway-core/src/account/store.rs @@ -1,4 +1,4 @@ -//! Provider 账号持久化端口。 +//! Provider 账号持久化端口 use async_trait::async_trait; @@ -12,7 +12,7 @@ use super::{ QuotaObservationTouch, QuotaWriteOutcome, }; -/// `provider_accounts` 的数据库中立端口。 +/// `provider_accounts` 的数据库中立端口 #[async_trait] pub trait ProviderAccountStore: Send + Sync { async fn create_account(&self, account: NewProviderAccount) -> Result<(), StoreError>; @@ -29,7 +29,7 @@ pub trait ProviderAccountStore: Send + Sync { provider: &ProviderKind, ) -> Result, StoreError>; - /// 一次有界查询返回账号事实和 revision-fenced 明文 credential。 + /// 一次有界查询返回账号事实和 revision-fenced 明文 credential async fn list_refresh_candidates( &self, query: ProviderRefreshQuery, @@ -41,10 +41,10 @@ pub trait ProviderAccountStore: Send + Sync { expected_revision: CredentialRevision, ) -> Result; - /// 读取账号当前 credential 及其 revision,不做任何版本比对。 + /// 读取账号当前 credential 及其 revision,不做任何版本比对 /// /// 管理写入必须在临近 CAS 时用它取 fence,而不是携带调用方持有的旧 revision: - /// 后台刷新随时会推进 revision,用陈旧快照会把正常的恢复操作误判为冲突。 + /// 后台刷新随时会推进 revision,用陈旧快照会把正常的恢复操作误判为冲突 async fn load_current_credential( &self, account: &ProviderAccountId, diff --git a/backend/crates/gateway-core/src/concurrency.rs b/backend/crates/gateway-core/src/concurrency.rs index 1bb331253..5d59a3a98 100644 --- a/backend/crates/gateway-core/src/concurrency.rs +++ b/backend/crates/gateway-core/src/concurrency.rs @@ -1,4 +1,4 @@ -//! 随请求存活的有界等待位置;执行容量仍由原子准入/租约端口裁决。 +//! 随请求存活的有界等待位置;执行容量仍由原子准入/租约端口裁决 use std::collections::{HashMap, VecDeque}; use std::hash::Hash; @@ -18,7 +18,7 @@ pub struct ConcurrencyQueuePolicy { pub timeout: Duration, } -/// 从首次入队开始计时,密钥、账号与后续重试共享同一个截止时刻。 +/// 从首次入队开始计时,密钥、账号与后续重试共享同一个截止时刻 #[derive(Debug, Clone, Default)] pub struct ConcurrencyWaitBudget { deadline: Arc>, @@ -43,7 +43,7 @@ pub enum QueueRejection { Timeout, } -/// 同一队列内的等待类别:高优先级排在已有高优先级等待者之后、全部普通等待者之前。 +/// 同一队列内的等待类别:高优先级排在已有高优先级等待者之后、全部普通等待者之前 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum WaitPriority { #[default] @@ -61,7 +61,7 @@ struct QueueState { total: usize, } -/// 只保存等待者与唤醒器,既不持有请求正文,也不复制运行中并发计数。 +/// 只保存等待者与唤醒器,既不持有请求正文,也不复制运行中并发计数 pub struct ConcurrencyWaitQueue { state: Arc>>, } @@ -88,7 +88,7 @@ impl ConcurrencyWaitQueue { .is_some_and(|queue| !queue.is_empty()) } - /// 是否存在排在该类别请求之前的等待者;普通请求让位于全部等待者,高优先级只让位于同类。 + /// 是否存在排在该类别请求之前的等待者;普通请求让位于全部等待者,高优先级只让位于同类 fn has_waiters_ahead_of(&self, key: &K, priority: WaitPriority) -> bool { self.state .lock() @@ -111,8 +111,8 @@ impl ConcurrencyWaitQueue { self.enqueue_with_priority(keys, max_waiting, deadline, |_, waiting| -(waiting as f64)) } - /// 在同一锁内读取队长并入队,分数越大越优先,同分沿用候选顺序。 - /// 回调只做内存评分,不得阻塞或再次访问本队列。 + /// 在同一锁内读取队长并入队,分数越大越优先,同分沿用候选顺序 + /// 回调只做内存评分,不得阻塞或再次访问本队列 pub fn enqueue_with_priority( &self, keys: &[K], @@ -123,7 +123,7 @@ impl ConcurrencyWaitQueue { self.enqueue_ranked(keys, max_waiting, deadline, WaitPriority::Normal, priority) } - /// 高优先级等待者不受单队列 `max_waiting` 限制,但仍要求排队已开启并受全局容量约束。 + /// 高优先级等待者不受单队列 `max_waiting` 限制,但仍要求排队已开启并受全局容量约束 fn enqueue_ranked( &self, keys: &[K], @@ -162,7 +162,7 @@ impl ConcurrencyWaitQueue { .position(|waiter| waiter.priority == WaitPriority::Normal) .unwrap_or(queue.len()), }; - // 插到队首时原队首失去位置,它的有界重查会自然让出;新队首随后自行轮询到位。 + // 插到队首时原队首失去位置,它的有界重查会自然让出;新队首随后自行轮询到位 queue.insert( position, Waiter { @@ -180,7 +180,7 @@ impl ConcurrencyWaitQueue { } } -/// Drop 同步回收等待位置,包括尚未轮到队首的取消请求。 +/// Drop 同步回收等待位置,包括尚未轮到队首的取消请求 pub struct WaitTicket { state: Arc>>, key: K, @@ -223,7 +223,7 @@ impl WaitTicket { } pub async fn retry(&self) -> Result<(), QueueRejection> { - // 租约释放可能经过后台队列;有界重查同时覆盖释放通知之前及 TTL 到期的空位。 + // 租约释放可能经过后台队列;有界重查同时覆盖释放通知之前及 TTL 到期的空位 Delay::new( CAPACITY_RECHECK_INTERVAL.min(self.deadline.saturating_duration_since(Instant::now())), ) @@ -262,7 +262,7 @@ impl Drop for WaitTicket { } } -/// 单层等待位置与观测;时间预算由请求共享,重选账号不会重新开始计时。 +/// 单层等待位置与观测;时间预算由请求共享,重选账号不会重新开始计时 pub struct CapacityWait<'a, K: Eq + Hash> { queue: &'a ConcurrencyWaitQueue, policy: ConcurrencyQueuePolicy, @@ -301,7 +301,7 @@ impl<'a, K: Clone + Eq + Hash> CapacityWait<'a, K> { #[must_use] pub fn can_try(&self, key: &K) -> bool { if let Some(ticket) = self.ticket.as_ref().filter(|ticket| ticket.key() == key) { - // 重读容量期间可能被高优先级请求插队,持有 ticket 不代表仍有选号资格。 + // 重读容量期间可能被高优先级请求插队,持有 ticket 不代表仍有选号资格 ticket.is_head() } else { !self.queue.has_waiters_ahead_of(key, self.priority) @@ -319,7 +319,7 @@ impl<'a, K: Clone + Eq + Hash> CapacityWait<'a, K> { .await } - /// 只在首次入队或原账号不再合格时重新选队列,已取得的位置不因评分变化而移动。 + /// 只在首次入队或原账号不再合格时重新选队列,已取得的位置不因评分变化而移动 pub async fn wait_with_priority( &mut self, keys: &[K], diff --git a/backend/crates/gateway-core/src/diagnostics/capture.rs b/backend/crates/gateway-core/src/diagnostics/capture.rs index 5182f4d27..7b5f854a3 100644 --- a/backend/crates/gateway-core/src/diagnostics/capture.rs +++ b/backend/crates/gateway-core/src/diagnostics/capture.rs @@ -1,9 +1,10 @@ -//! 诊断内容的统一筛选边界。未知键名与值只保留有界结构和摘要。 +//! 诊断内容的统一筛选边界 +//! 未知键名与值只保留有界结构和摘要 use serde_json::{Map, Value, json}; use sha2::{Digest, Sha256}; -/// 原始字节的长度与 SHA-256,不复制或保留正文。 +/// 原始字节的长度与 SHA-256,不复制或保留正文 #[must_use] pub fn body_fingerprint(bytes: &[u8]) -> Value { json!({"bytes": bytes.len(), "sha256": fingerprint_hex(bytes)}) @@ -23,13 +24,13 @@ fn fingerprint_hex(bytes: &[u8]) -> String { .collect() } -/// 仅保留有限协议骨架;任意 JSON(包括 headers 和 metadata)不能声明自己可信。 +/// 仅保留有限协议骨架;任意 JSON(包括 headers 和 metadata)不能声明自己可信 #[must_use] pub fn diagnostic_json(value: &Value) -> Value { capture(value, "", Context::Protocol, 0, &mut 96) } -/// 只有 transport 的上游 metadata / error 帧顶层才是受控头部边界。 +/// 只有 transport 的上游 metadata / error 帧顶层才是受控头部边界 pub(super) fn diagnostic_event_json(value: &Value, stage: &str) -> Value { let context = if stage == "upstream.event" && value.get("headers").is_some_and(Value::is_object) @@ -87,7 +88,7 @@ fn capture(value: &Value, key: &str, context: Context, depth: usize, budget: &mu "length": items.len(), "sample": items.iter().take(4) .map(|item| { - // 受控头值只接受一层字符串数组;畸形嵌套不能继承头部放行规则。 + // 受控头值只接受一层字符串数组;畸形嵌套不能继承头部放行规则 let context = if matches!(context, Context::HeaderValue) && !item.is_string() { Context::Opaque } else { @@ -169,7 +170,7 @@ fn protocol_value(key: &str, value: &str) -> bool { } } -/// 请求/响应头保持多值;安全 trace ID 原样保存,未知值只保存摘要。 +/// 请求/响应头保持多值;安全 trace ID 原样保存,未知值只保存摘要 #[must_use] pub fn diagnostic_headers<'a>(headers: impl IntoIterator) -> Value { let mut result = Map::new(); @@ -248,7 +249,7 @@ pub(super) fn diagnostic_event_type(value: &str) -> Value { } } -/// 这里只决定诊断可明文展示的阶段,不校验协议;未知事件仍采集摘要,不能按字符形状放行。 +/// 这里只决定诊断可明文展示的阶段,不校验协议;未知事件仍采集摘要,不能按字符形状放行 fn known_event_type(value: &str) -> bool { matches!( value, diff --git a/backend/crates/gateway-core/src/diagnostics/mod.rs b/backend/crates/gateway-core/src/diagnostics/mod.rs index ad3f346f8..338f6866a 100644 --- a/backend/crates/gateway-core/src/diagnostics/mod.rs +++ b/backend/crates/gateway-core/src/diagnostics/mod.rs @@ -1,4 +1,4 @@ -//! 有界、请求局部的诊断证据;不参与路由、重试或客户端协议决策。 +//! 有界、请求局部的诊断证据;不参与路由、重试或客户端协议决策 mod capture; mod selection; diff --git a/backend/crates/gateway-core/src/diagnostics/selection.rs b/backend/crates/gateway-core/src/diagnostics/selection.rs index 9226c3096..dd5b4ed2c 100644 --- a/backend/crates/gateway-core/src/diagnostics/selection.rs +++ b/backend/crates/gateway-core/src/diagnostics/selection.rs @@ -1,4 +1,4 @@ -//! 复用选号时的输入,保留有界的候选评分与阻断原因。 +//! 复用选号时的输入,保留有界的候选评分与阻断原因 use serde_json::json; @@ -8,11 +8,11 @@ use crate::account::{ smart_score, }; -// 单个 trace 事件最多 4 KiB;优先保留实际选中的账号。 +// 单个 trace 事件最多 4 KiB;优先保留实际选中的账号 const MAX_CANDIDATES: usize = 6; impl TraceContext { - /// 记录实际选号输入与结果;只复用评分规则,不参与调度决策。 + /// 记录实际选号输入与结果;只复用评分规则,不参与调度决策 pub fn account_selection( &self, candidates: &[AccountCandidate], diff --git a/backend/crates/gateway-core/src/diagnostics/stream.rs b/backend/crates/gateway-core/src/diagnostics/stream.rs index 4e6dc434c..b0b5c1475 100644 --- a/backend/crates/gateway-core/src/diagnostics/stream.rs +++ b/backend/crates/gateway-core/src/diagnostics/stream.rs @@ -1,4 +1,4 @@ -//! 仅用于观测的有界流分帧;超过限额时明确记缺口,不影响实际协议解析。 +//! 仅用于观测的有界流分帧;超过限额时明确记缺口,不影响实际协议解析 use serde_json::json; @@ -12,7 +12,7 @@ pub enum StreamFormat { JsonLines, } -/// 同一 exchange 持有一个 observer,跨 HTTP chunk 拼接完整事件。 +/// 同一 exchange 持有一个 observer,跨 HTTP chunk 拼接完整事件 pub struct StreamCapture { trace: TraceContext, format: StreamFormat, diff --git a/backend/crates/gateway-core/src/diagnostics/trace.rs b/backend/crates/gateway-core/src/diagnostics/trace.rs index f692b1bc5..17328989d 100644 --- a/backend/crates/gateway-core/src/diagnostics/trace.rs +++ b/backend/crates/gateway-core/src/diagnostics/trace.rs @@ -1,4 +1,4 @@ -//! 首部与最近事件同时保留,流式 delta 合并,所有淘汰均有计数。 +//! 首部与最近事件同时保留,流式 delta 合并,所有淘汰均有计数 use std::{ collections::VecDeque, @@ -18,7 +18,7 @@ const HEAD_EVENTS: usize = 8; const MAX_DATA_BYTES: usize = 4096; const MAX_BUFFER_BYTES: usize = 64 * 1024; -/// 显式传给异步任务的关联上下文;默认值禁用捕获,避免全局请求映射。 +/// 显式传给异步任务的关联上下文;默认值禁用捕获,避免全局请求映射 #[derive(Clone, Default)] pub struct TraceContext { state: Option>>, @@ -65,13 +65,13 @@ struct TraceEvent { } impl TraceContext { - /// 是否已启用请求诊断,供调用方避免构造不会保存的事实。 + /// 是否已启用请求诊断,供调用方避免构造不会保存的事实 #[must_use] pub const fn is_enabled(&self) -> bool { self.state.is_some() } - /// 一个模型请求只创建一次,所有 attempt 共享同一有界时间线。 + /// 一个模型请求只创建一次,所有 attempt 共享同一有界时间线 #[must_use] pub fn new(request_id: &str) -> Self { Self { @@ -103,7 +103,7 @@ impl TraceContext { } } - /// 一个 attempt 中的每次实际 transport exchange 也有独立编号。 + /// 一个 attempt 中的每次实际 transport exchange 也有独立编号 #[must_use] pub fn exchange(&self, transport: &'static str) -> Self { let mut context = self.clone(); @@ -118,13 +118,13 @@ impl TraceContext { context } - /// details 只允许调用方构造的诊断事实;不接收原始请求/响应正文。 + /// details 只允许调用方构造的诊断事实;不接收原始请求/响应正文 pub fn record(&self, stage: &'static str, data: Value) { self.push(stage, data, false); } - /// 同一个头部边界只采集一次:默认脱敏,显式开启 dump 时另存完整字节。 - /// facts 必须为调用方构造的安全对象;仅受控诊断头可明文保留,完整头部另存 dump。 + /// 同一个头部边界只采集一次:默认脱敏,显式开启 dump 时另存完整字节 + /// facts 必须为调用方构造的安全对象;仅受控诊断头可明文保留,完整头部另存 dump pub fn headers<'a>( &self, stage: &'static str, @@ -156,14 +156,14 @@ impl TraceContext { } } - /// 入站 JSON 先记录再解析业务语义,包括未知事件和 metadata。 - /// 原文仅在现有 request_dump 开关打开时写入其独立文件。 + /// 入站 JSON 先记录再解析业务语义,包括未知事件和 metadata + /// 原文仅在现有 request_dump 开关打开时写入其独立文件 pub fn capture(&self, stage: &'static str, bytes: &[u8]) { self.dump(stage, bytes); self.capture_event(stage, bytes); } - /// StreamCapture 已转储原始 chunk 时,只提取完整事件的安全摘要。 + /// StreamCapture 已转储原始 chunk 时,只提取完整事件的安全摘要 pub fn capture_event(&self, stage: &'static str, bytes: &[u8]) { self.capture_named_event(stage, bytes, None); } @@ -183,7 +183,7 @@ impl TraceContext { .and_then(|v| v.get("type")) .and_then(Value::as_str) .or(name); - // 后缀只用于观测合并,不授予明文权限;未知事件用摘要区分,避免合并成同一类。 + // 后缀只用于观测合并,不授予明文权限;未知事件用摘要区分,避免合并成同一类 let delta = event_type.is_some_and(|kind| kind.ends_with(".delta")); let data = json!({ "body": fingerprint, @@ -195,7 +195,7 @@ impl TraceContext { self.push(stage, data, delta); } - /// 只记录业务解析边界;metadata 已由 transport 在解析前捕获。 + /// 只记录业务解析边界;metadata 已由 transport 在解析前捕获 pub fn wire_event(&self, protocol: &str, event_type: Option<&str>) { let delta = event_type.is_some_and(|kind| kind.ends_with(".delta")); self.push( @@ -208,7 +208,7 @@ impl TraceContext { ); } - /// 完整报文捕获与摘要使用相同 request / attempt / exchange ID。 + /// 完整报文捕获与摘要使用相同 request / attempt / exchange ID pub fn dump(&self, stage: &'static str, bytes: &[u8]) { if !tracing::enabled!(target: "request_dump", tracing::Level::INFO) { return; @@ -221,7 +221,7 @@ impl TraceContext { state.wire_bytes = state.wire_bytes.saturating_add(bytes.len() as u64); let wire_sequence = state.wire_frames; let chunk_count = bytes.len().div_ceil(32 * 1024).max(1); - // Each log entry stays bounded even for images or very large WebSocket frames. + // 图像或大型 WebSocket 帧也必须遵守单条日志的大小上限 for chunk_index in 0..chunk_count { let start = chunk_index * 32 * 1024; let chunk = &bytes[start..bytes.len().min(start + 32 * 1024)]; @@ -234,7 +234,7 @@ impl TraceContext { } } - /// 与请求终态原子持久化;旧记录没有此字段,不伪造历史时间线。 + /// 与请求终态原子持久化;旧记录没有此字段,不伪造历史时间线 #[must_use] pub fn snapshot(&self) -> Option { let state = self @@ -286,7 +286,7 @@ impl TraceContext { last.last_elapsed_ms = elapsed_ms; return; } - // 普通日志只输出有界安全事实;进程意外退出时仍可按 ID 检索已发生阶段。 + // 普通日志只输出有界安全事实;进程意外退出时仍可按 ID 检索已发生阶段 tracing::info!(target: "request_trace", request_id = %state.request_id, attempt_index = self.attempt_index, exchange_id = self.exchange_id, sequence, elapsed_ms, stage, data = %data, "request trace"); @@ -305,8 +305,8 @@ impl TraceContext { bytes, }); while state.events.len() > MAX_EVENTS || state.bytes > MAX_BUFFER_BYTES { - // Preserve failures and retry decisions ahead of routine middle-of-stream events. - // Head/tail and important stages compete under the same hard memory limit. + // 相比流中间的常规事件,优先保留失败与重试决策 + // 首尾事件与重要阶段共用同一内存上限 let index = (HEAD_EVENTS..state.events.len().saturating_sub(8)) .find(|index| { !matches!( diff --git a/backend/crates/gateway-core/src/engine/admission.rs b/backend/crates/gateway-core/src/engine/admission.rs index 0973adbe5..72eaa46a4 100644 --- a/backend/crates/gateway-core/src/engine/admission.rs +++ b/backend/crates/gateway-core/src/engine/admission.rs @@ -1,4 +1,4 @@ -//! Client Key 准入、释放与进程启动恢复的中立契约。 +//! Client Key 准入、释放与进程启动恢复的中立契约 use std::time::{Duration, SystemTime}; @@ -15,7 +15,7 @@ pub struct ClientAdmissionRequest { pub model_request_id: ModelRequestId, pub client_api_key_id: ClientApiKeyId, pub lease_ttl: Duration, - /// 有等待者时只允许队首取得槽位,RPM 仍由原子准入检查。 + /// 有等待者时只允许队首取得槽位,RPM 仍由原子准入检查 pub allow_concurrency_acquire: bool, pub limits: RateLimits, } @@ -62,7 +62,7 @@ pub struct ClientAdmissionRestoreResult { pub struct ClientAdmissionError; pub trait ClientAdmissionPort: Send + Sync { - /// 仅续期已经准入的并发槽位,不重复消费 RPM。 + /// 仅续期已经准入的并发槽位,不重复消费 RPM fn maintain( &self, _key: &ClientApiKeyId, @@ -73,7 +73,7 @@ pub trait ClientAdmissionPort: Send + Sync { Box::new(()) } - /// 请求 future 被取消时移交幂等释放,具体实现拥有异步清理执行器。 + /// 请求 future 被取消时移交幂等释放,具体实现拥有异步清理执行器 fn abandon(&self, client_api_key_id: &ClientApiKeyId, model_request_id: &ModelRequestId); fn admit( @@ -108,7 +108,7 @@ pub struct ClientAdmissionStartupRecoveryReport { pub restored_running_requests: u64, } -/// 监听端口前完成 PostgreSQL 终态收敛与 Redis 热状态恢复。 +/// 监听端口前完成 PostgreSQL 终态收敛与 Redis 热状态恢复 pub async fn restore_client_admission_startup( execution: &dyn ExecutionStore, recovery: &dyn ClientAdmissionRecoveryPort, diff --git a/backend/crates/gateway-core/src/engine/authentication.rs b/backend/crates/gateway-core/src/engine/authentication.rs index 6c23f28d3..d6a546ac2 100644 --- a/backend/crates/gateway-core/src/engine/authentication.rs +++ b/backend/crates/gateway-core/src/engine/authentication.rs @@ -1,4 +1,4 @@ -//! 数据面入口认证计划;插件只产生 principal,Core 解析既有 Client Key 策略。 +//! 数据面入口认证计划;插件只产生 principal,Core 解析既有 Client Key 策略 use std::{ collections::BTreeMap, @@ -15,7 +15,7 @@ use crate::{ pub const MAXIMUM_AUTHORIZATION_BYTES: usize = 8 * 1024; -/// API 从数据面 Authorization 头构造的有界认证信封。 +/// API 从数据面 Authorization 头构造的有界认证信封 #[derive(Clone, PartialEq, Eq)] pub struct ClientAuthenticationRequest { settings: Option, @@ -39,7 +39,7 @@ impl ClientAuthenticationRequest { self.settings.take() } - /// 创建入口认证信封;HTTP 语法与缺失头仍由 API owner 负责。 + /// 创建入口认证信封;HTTP 语法与缺失头仍由 API owner 负责 pub fn new(authorization: impl Into) -> Result { let authorization = authorization.into(); if authorization.is_empty() @@ -61,7 +61,7 @@ impl ClientAuthenticationRequest { }) } - /// 原生调用方显式构造 Bearer 信封;仍经过同一长度与字符校验。 + /// 原生调用方显式构造 Bearer 信封;仍经过同一长度与字符校验 pub fn bearer(plaintext: &str) -> Result { Self::new(format!("Bearer {plaintext}")) } @@ -90,7 +90,7 @@ impl fmt::Debug for ClientAuthenticationRequest { #[error("client authorization is invalid")] pub struct ClientAuthenticationRequestError; -/// 插件认证结果不携带宿主执行身份。 +/// 插件认证结果不携带宿主执行身份 #[derive(Clone, PartialEq, Eq)] pub enum FrontendAuthenticationDecision { Authenticated { principal: String }, @@ -115,17 +115,17 @@ impl fmt::Debug for FrontendAuthenticationDecision { #[error("frontend authentication provider is unavailable")] pub struct FrontendAuthenticationError; -/// 一个发布代次中唯一的入口认证计划。 +/// 一个发布代次中唯一的入口认证计划 pub trait FrontendAuthenticationPlan: Send + Sync { fn authenticate<'a>( &'a self, request: &'a ClientAuthenticationRequest, ) -> BoxFuture<'a, Result>; - /// 映射由管理员配置冻结,插件结果不能选择 Key ID。 + /// 映射由管理员配置冻结,插件结果不能选择 Key ID fn client_key_id(&self, principal: &str) -> Option; - /// 独占计划不允许 `NotMatched` 回退原生 Bearer。 + /// 独占计划不允许 `NotMatched` 回退原生 Bearer fn exclusive(&self) -> bool; } @@ -133,7 +133,7 @@ pub trait FrontendAuthenticationPlan: Send + Sync { #[error("frontend authentication generation is already registered")] pub struct FrontendAuthenticationRegistrationError; -/// 按已发布集合解析的非拥有索引;旧代次由快照和在途调用保活。 +/// 按已发布集合解析的非拥有索引;旧代次由快照和在途调用保活 #[derive(Clone, Default)] pub struct FrontendAuthenticationExtensionIndex { sets: Arc>>>, diff --git a/backend/crates/gateway-core/src/engine/budget.rs b/backend/crates/gateway-core/src/engine/budget.rs index 1d5d30cea..101de66e2 100644 --- a/backend/crates/gateway-core/src/engine/budget.rs +++ b/backend/crates/gateway-core/src/engine/budget.rs @@ -1,4 +1,4 @@ -//! 按 Key 持久化 USD 限额与已取得费用,不依赖尽力写入的请求观测。 +//! 按 Key 持久化 USD 限额与已取得费用,不依赖尽力写入的请求观测 use std::time::SystemTime; @@ -34,7 +34,7 @@ pub struct ClientBudgetStatus { pub struct ClientBudgetCharge { pub key_id: ClientApiKeyId, pub request_id: ModelRequestId, - /// 已取得的 USD 费用,包含重试;缺少费用的尝试按零累计。 + /// 已取得的 USD 费用,包含重试;缺少费用的尝试按零累计 pub amount_usd: Decimal, pub completed_at: SystemTime, } @@ -44,9 +44,9 @@ pub struct ClientBudgetCharge { pub struct ClientBudgetError; pub trait ClientBudgetPort: Send + Sync { - /// 原子检查当前限额与已用金额,不创建预扣费或待结算记录。 + /// 原子检查当前限额与已用金额,不创建预扣费或待结算记录 fn admit(&self, key_id: ClientApiKeyId) -> BoxFuture<'_, Result<(), GatewayError>>; - /// 按网关请求 ID 幂等累计已取得费用;写入失败由 Store 重试。 + /// 按网关请求 ID 幂等累计已取得费用;写入失败由 Store 重试 fn settle(&self, charge: ClientBudgetCharge) -> BoxFuture<'_, Result<(), ClientBudgetError>>; } diff --git a/backend/crates/gateway-core/src/engine/connection.rs b/backend/crates/gateway-core/src/engine/connection.rs index db5aa8108..f843735c3 100644 --- a/backend/crates/gateway-core/src/engine/connection.rs +++ b/backend/crates/gateway-core/src/engine/connection.rs @@ -1,4 +1,4 @@ -//! 请求内共享的发送前连接预算;不投影账号或 Provider 健康状态。 +//! 请求内共享的发送前连接预算;不投影账号或 Provider 健康状态 use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; @@ -13,7 +13,7 @@ struct State { completed: bool, } -/// 换账号、换传输和内部重试共享同一预算;成功发送后的协议恢复不重置它。 +/// 换账号、换传输和内部重试共享同一预算;成功发送后的协议恢复不重置它 #[derive(Debug, Clone)] pub struct ConnectionBudget { state: Arc>, @@ -31,7 +31,7 @@ impl Default for ConnectionBudget { pub struct ConnectionBudgetExhausted; impl ConnectionBudget { - /// 绑定单调时钟;克隆预算继续共享状态和时钟,不依赖具体异步运行时。 + /// 绑定单调时钟;克隆预算继续共享状态和时钟,不依赖具体异步运行时 #[must_use] pub fn with_clock(now: fn() -> Instant) -> Self { Self { @@ -40,8 +40,8 @@ impl ConnectionBudget { } } - /// 在可能建立连接前预约一次 opening;返回本次连接允许使用的剩余时间。 - /// HTTP 连接池隐藏了是否冷建连,因此一次 HTTP opening 也计入次数上限。 + /// 在可能建立连接前预约一次 opening;返回本次连接允许使用的剩余时间 + /// HTTP 连接池隐藏了是否冷建连,因此一次 HTTP opening 也计入次数上限 pub fn begin(&self) -> Result, ConnectionBudgetExhausted> { let mut state = self .state @@ -60,7 +60,7 @@ impl ConnectionBudget { Ok(Some(remaining)) } - /// 仅连接成功不足以清零;确认业务发送后退出新增的 NotSent 恢复策略。 + /// 仅连接成功不足以清零;确认业务发送后退出新增的 NotSent 恢复策略 pub fn complete(&self) { self.state .lock() @@ -96,7 +96,7 @@ impl ConnectionBudget { })) } - /// 首次建连后的等待也计入恢复窗口;不缩短已发送请求的生成时间。 + /// 首次建连后的等待也计入恢复窗口;不缩短已发送请求的生成时间 #[must_use] pub fn startup_remaining(&self) -> Option { let state = self @@ -111,7 +111,7 @@ impl ConnectionBudget { }) } - /// 额外 attempt 最多三个;抖动由请求 ID 派生,重试不依赖运行时随机源。 + /// 额外 attempt 最多三个;抖动由请求 ID 派生,重试不依赖运行时随机源 #[must_use] pub fn retry_delay(&self, retries: u32, request_id: &str) -> Option { if retries >= MAX_OPENINGS - 1 { diff --git a/backend/crates/gateway-core/src/engine/continuation.rs b/backend/crates/gateway-core/src/engine/continuation.rs index be11f80da..afd8faff5 100644 --- a/backend/crates/gateway-core/src/engine/continuation.rs +++ b/backend/crates/gateway-core/src/engine/continuation.rs @@ -1,7 +1,7 @@ -//! Provider 原生 previous-response 的调用方隔离、账号绑定与复用约束。 +//! Provider 原生 previous-response 的调用方隔离、账号绑定与复用约束 //! //! Core 不解释 Provider transcript;同一客户端连接需要的可携带状态由 -//! [`ProviderSessionState`](crate::operation::ProviderSessionState) 不透明承载。 +//! [`ProviderSessionState`](crate::operation::ProviderSessionState) 不透明承载 use std::fmt; @@ -12,15 +12,16 @@ use crate::identity::ProviderKind; use crate::operation::ProviderSessionState; use crate::policy::ClientApiKeyId; -/// 客户端或 Provider 传递的 opaque response handle。 +/// 客户端或 Provider 传递的 opaque response handle /// -/// Codex 定义这个值的语义和格式。网关只将其作为亲和查找键或同账号上游 -/// continuation 的载体,不得按私有长度、字符集或前缀规则拒绝、归一化或改写。 +/// Codex 定义这个值的语义和格式 +/// 网关只将其作为亲和查找键或同账号上游 +/// continuation 的载体,不得按私有长度、字符集或前缀规则拒绝、归一化或改写 #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct PreviousResponseId(String); impl PreviousResponseId { - /// 按原样创建 opaque response handle。 + /// 按原样创建 opaque response handle #[must_use] pub fn new(value: impl Into) -> Self { Self(value.into()) @@ -38,24 +39,24 @@ impl fmt::Debug for PreviousResponseId { } } -/// Provider 原生 response handle 的续接范围。 +/// Provider 原生 response handle 的续接范围 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum NativeContinuationScope { - /// 上游已持久化,可由同账号的新连接继续。 + /// 上游已持久化,可由同账号的新连接继续 Persisted, - /// 只存在于完成上一轮的 WebSocket。 + /// 只存在于完成上一轮的 WebSocket ConnectionLocal, } -/// 从可丢失会话亲和存储恢复的 native previous-response pin。 +/// 从可丢失会话亲和存储恢复的 native previous-response pin /// /// 该值同时绑定调用方 API Key、Provider 与账号,阻止 native handle 和不透明 -/// Provider state 被跨租户或跨上游目标复用。 +/// Provider state 被跨租户或跨上游目标复用 #[derive(Clone, PartialEq)] pub struct NativeContinuationPin { - /// 客户端提交、用于查找可丢失会话亲和记录的 response ID。 + /// 客户端提交、用于查找可丢失会话亲和记录的 response ID previous_response_id: PreviousResponseId, - /// Provider 原生 response handle。 + /// Provider 原生 response handle upstream_response_id: PreviousResponseId, client_api_key_id: ClientApiKeyId, provider: ProviderKind, @@ -84,14 +85,14 @@ impl NativeContinuationPin { } } - /// 设置 Store 已确认的原生续接范围。 + /// 设置 Store 已确认的原生续接范围 #[must_use] pub const fn with_scope(mut self, scope: NativeContinuationScope) -> Self { self.scope = scope; self } - /// 附着仅由对应 Provider 解释的不透明会话状态。 + /// 附着仅由对应 Provider 解释的不透明会话状态 #[must_use] pub fn with_session_state(mut self, state: ProviderSessionState) -> Self { self.session_state = Some(Box::new(state)); @@ -103,7 +104,7 @@ impl NativeContinuationPin { &self.previous_response_id } - /// 返回只允许发送给已冻结 Provider 目标的原生上游 handle。 + /// 返回只允许发送给已冻结 Provider 目标的原生上游 handle #[must_use] pub const fn upstream_response_id(&self) -> &PreviousResponseId { &self.upstream_response_id @@ -134,13 +135,13 @@ impl NativeContinuationPin { self.scope } - /// 返回与本 pin 同账号绑定的 Provider 私有会话状态。 + /// 返回与本 pin 同账号绑定的 Provider 私有会话状态 #[must_use] pub fn session_state(&self) -> Option<&ProviderSessionState> { self.session_state.as_deref() } - /// 校验本次 route/account 选择没有破坏 native pin。 + /// 校验本次 route/account 选择没有破坏 native pin #[must_use] pub fn matches(&self, provider: &ProviderKind, account: &ProviderAccountId) -> bool { self.provider == *provider && self.account == *account @@ -165,10 +166,10 @@ impl fmt::Debug for NativeContinuationPin { } } -/// 一次请求最终采用的 previous-response 绑定方式。 +/// 一次请求最终采用的 previous-response 绑定方式 /// /// 已命中网关历史的 handle 携带完整账号 pin;未命中历史的外部 handle 只保留 -/// 客户端提交的 opaque ID,由目标 Provider 在首次且唯一一次 attempt 中解释。 +/// 客户端提交的 opaque ID,由目标 Provider 在首次且唯一一次 attempt 中解释 #[derive(Clone, PartialEq)] pub enum ContinuationBinding { Pinned(NativeContinuationPin), @@ -205,14 +206,14 @@ impl fmt::Debug for ContinuationBinding { } } -/// Native continuation lookup 的稳定失败分类。 +/// Native continuation lookup 的稳定失败分类 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum NativeContinuationStoreErrorKind { - /// Redis 等可丢失协调存储暂不可用;调用方可以按外部 continuation fail-open。 + /// Redis 等可丢失协调存储暂不可用;调用方可以按外部 continuation fail-open Unavailable, - /// 存储中的记录无法还原为可信 pin;不得将原始 handle 继续透传上游。 + /// 存储中的记录无法还原为可信 pin;不得将原始 handle 继续透传上游 InvalidData, - /// 已存在的记录属于另一个客户端 API Key;必须 fail closed。 + /// 已存在的记录属于另一个客户端 API Key;必须 fail closed OwnershipMismatch, } @@ -224,7 +225,7 @@ pub struct NativeContinuationStoreError { } impl NativeContinuationStoreError { - /// 构造可 fail-open 的协调存储不可用错误;`detail` 不应包含 response handle。 + /// 构造可 fail-open 的协调存储不可用错误;`detail` 不应包含 response handle #[must_use] pub fn unavailable(detail: impl Into) -> Self { Self { @@ -233,7 +234,7 @@ impl NativeContinuationStoreError { } } - /// 构造必须 fail closed 的无效记录错误;`detail` 不应包含 response handle。 + /// 构造必须 fail closed 的无效记录错误;`detail` 不应包含 response handle #[must_use] pub fn invalid_data(detail: impl Into) -> Self { Self { @@ -242,7 +243,7 @@ impl NativeContinuationStoreError { } } - /// 构造不泄露记录归属细节的跨调用方错误。 + /// 构造不泄露记录归属细节的跨调用方错误 #[must_use] pub fn ownership_mismatch() -> Self { Self { @@ -262,10 +263,10 @@ impl NativeContinuationStoreError { } } -/// 可丢失的 previous-response 亲和存储端口。 +/// 可丢失的 previous-response 亲和存储端口 /// /// 亲和记录按调用方 API Key 隔离;Redis 不可用或超时必须由调用方 fail-open, -/// 但命中其他 Key 的记录必须 fail closed。 +/// 但命中其他 Key 的记录必须 fail closed pub trait NativeContinuationPort: Send + Sync { fn resolve<'a>( &'a self, diff --git a/backend/crates/gateway-core/src/engine/coordinator.rs b/backend/crates/gateway-core/src/engine/coordinator.rs index 039630b8d..d809cf68c 100644 --- a/backend/crates/gateway-core/src/engine/coordinator.rs +++ b/backend/crates/gateway-core/src/engine/coordinator.rs @@ -1,4 +1,4 @@ -//! 唯一的账号重试、发送与下游 commit barrier owner。 +//! 唯一的账号重试、发送与下游 commit barrier owner use crate::concurrency::ConcurrencyWaitBudget; use crate::diagnostics::TraceContext; @@ -40,7 +40,7 @@ use futures::future::{BoxFuture, Fuse}; use futures::{FutureExt, StreamExt, pin_mut, select_biased}; use futures_timer::Delay; -/// Request 级协调器;不会创建或写入 `request_attempts`。 +/// Request 级协调器;不会创建或写入 `request_attempts` pub struct AttemptCoordinator { engine: Arc>, } @@ -172,11 +172,11 @@ where } } - /// 接纳请求并返回由 Core 完整拥有 retry/commit 的流式会话。 + /// 接纳请求并返回由 Core 完整拥有 retry/commit 的流式会话 /// /// # Errors /// - /// 取消、已过 deadline 或 continuation 与路由不匹配时返回稳定错误。 + /// 取消、已过 deadline 或 continuation 与路由不匹配时返回稳定错误 pub async fn start( &self, request: NewModelRequest, @@ -217,9 +217,9 @@ where .await } - /// 对固定账号执行诊断请求。 + /// 对固定账号执行诊断请求 /// - /// 仅跳过该账号的本地可用性投影;账号租约、并发和请求间隔仍必须满足。 + /// 仅跳过该账号的本地可用性投影;账号租约、并发和请求间隔仍必须满足 pub async fn start_diagnostic( &self, request: NewModelRequest, @@ -409,10 +409,10 @@ struct PendingAttemptRetry { transport_recovery: bool, } -/// API 可逐事件消费的 Core 执行会话。 +/// API 可逐事件消费的 Core 执行会话 /// /// API 只能提交下游 delivery 边界;账号重试、断流终结与 -/// `model_requests` 写回均留在本类型内。 +/// `model_requests` 写回均留在本类型内 pub struct ResponseExecutionSession { engine: Arc>, request_id: ModelRequestId, @@ -427,7 +427,7 @@ pub struct ResponseExecutionSession { trace: TraceContext, deadline: Deadline, lease: Option>, - /// 会话级 deadline 计时器;deadline 固定,帧循环内复用而非逐事件新建。 + /// 会话级 deadline 计时器;deadline 固定,帧循环内复用而非逐事件新建 deadline_timer: Fuse>, pending_request: Option, requested_model: Option, @@ -449,27 +449,27 @@ pub struct ResponseExecutionSession { continuation_attempt: ContinuationAttempt, account_state_owner: Option, cancellation: CancellationToken, - /// 所有实际上游 attempt 数;包含同账号传输重试,作为持久化序号。 + /// 所有实际上游 attempt 数;包含同账号传输重试,作为持久化序号 attempts: u32, - /// WebSocket 观察序列在同一逻辑请求内按实际上游 wire 单调递增。 + /// WebSocket 观察序列在同一逻辑请求内按实际上游 wire 单调递增 websocket_observation_sequence: u64, - /// 路由预算只统计正常选号/账号恢复,不被 Provider-owned 传输预算消耗。 + /// 路由预算只统计正常选号/账号恢复,不被 Provider-owned 传输预算消耗 routing_attempts: u32, candidate_index: usize, excluded_accounts: BTreeSet, credential_recovery_attempted_accounts: BTreeSet, /// 凭据恢复后的一次性同账号钉选;只约束紧随其后的 replay attempt, - /// attempt 建立时即被消费,后续可重试错误仍可换号消耗剩余重试预算。 - /// 与 `required_account`(外部指定、贯穿整个请求)语义不同,不可合并。 + /// attempt 建立时即被消费,后续可重试错误仍可换号消耗剩余重试预算 + /// 与 `required_account`(外部指定、贯穿整个请求)语义不同,不可合并 recovery_account: Option, - /// 提交前同账号退避/传输恢复共用的等待与钉选状态;业务重试消耗路由预算。 + /// 提交前同账号退避/传输恢复共用的等待与钉选状态;业务重试消耗路由预算 pending_retry: Option, - /// 请求内按账号累计的瞬时拒绝重试次数,不能跨请求污染账号健康状态。 + /// 请求内按账号累计的瞬时拒绝重试次数,不能跨请求污染账号健康状态 transient_retry_counts: BTreeMap, - /// 只在首次进入对应 Provider 时解析,避免后台发布更新改变同一请求的重试身份。 + /// 只在首次进入对应 Provider 时解析,避免后台发布更新改变同一请求的重试身份 request_profiles: BTreeMap, current: Option, - /// 请求级发送状态水位;跨 attempt 单调不降,终态写回不得低于此档。 + /// 请求级发送状态水位;跨 attempt 单调不降,终态写回不得低于此档 send_state_watermark: UpstreamSendState, downstream_committed_at: Option, client_status_code: Option, @@ -478,12 +478,12 @@ pub struct ResponseExecutionSession { finalization: Option, finalized_at: Option, image_generation_requested: bool, - /// 最近一次为无感恢复而被丢弃的原始上游错误;只在后续空选路时成为终态。 + /// 最近一次为无感恢复而被丢弃的原始上游错误;只在后续空选路时成为终态 last_retryable_failure: Option, - /// 与 `last_retryable_failure` 同属一个 attempt 的原始失败批次。 + /// 与 `last_retryable_failure` 同属一个 attempt 的原始失败批次 last_retryable_failure_events: Vec, last_observed_provider: Option, - /// 原子失败批次已交给协议层、但尚待下游提交后收敛的原 Provider 错误。 + /// 原子失败批次已交给协议层、但尚待下游提交后收敛的原 Provider 错误 pending_terminal_failure: Option, } @@ -501,22 +501,22 @@ impl ResponseExecutionSession where S: ExecutionStore + 'static, { - /// 首次驱动前接入密钥准入使用的预算,所有账号尝试随后共享它。 + /// 首次驱动前接入密钥准入使用的预算,所有账号尝试随后共享它 pub(crate) fn with_concurrency_wait_budget(mut self, budget: ConcurrencyWaitBudget) -> Self { self.concurrency_wait_budget = budget; self } - /// 当前请求共享的诊断上下文。 + /// 当前请求共享的诊断上下文 pub fn trace(&self) -> TraceContext { self.trace.clone() } - /// 读取下一条 canonical event;首条未提交事件会携带 commit 要求。 + /// 读取下一条 canonical event;首条未提交事件会携带 commit 要求 /// /// # Errors /// - /// 未提交上一条首事件、Provider 失败、取消或超时时返回错误;观测写入失败只记录告警。 + /// 未提交上一条首事件、Provider 失败、取消或超时时返回错误;观测写入失败只记录告警 pub async fn next_event(&mut self) -> Result, EngineError> { self.resume_finalization().await; if self.delivery_pending { @@ -567,11 +567,11 @@ where } } - /// 非流式协议可在任何下游提交前收集一个完整、可丢弃重试的结果。 + /// 非流式协议可在任何下游提交前收集一个完整、可丢弃重试的结果 /// /// # Errors /// - /// 会话已提交、已有待提交结果或执行失败时返回错误。 + /// 会话已提交、已有待提交结果或执行失败时返回错误 pub async fn collect_uncommitted(&mut self) -> Result, EngineError> { self.resume_finalization().await; if self.downstream_committed_at.is_some() || self.delivery_pending { @@ -604,11 +604,11 @@ where } } - /// 在协议 adapter 真正写出首字节前记录下游不可撤回边界。 + /// 在协议 adapter 真正写出首字节前记录下游不可撤回边界 /// /// # Errors /// - /// 没有待提交结果或重复提交时返回错误;观测 Store 失败只记服务端告警。 + /// 没有待提交结果或重复提交时返回错误;观测 Store 失败只记服务端告警 pub async fn commit_downstream( &mut self, client_status_code: Option, @@ -642,8 +642,8 @@ where Ok(()) } - /// 仅当 HTTP 流插件明确丢弃了整个尚未提交的非终态批次时释放交付屏障。 - /// 原始 Provider facts 已经观察,不回滚计量,也不把丢弃误记为客户端 commit。 + /// 仅当 HTTP 流插件明确丢弃了整个尚未提交的非终态批次时释放交付屏障 + /// 原始 Provider facts 已经观察,不回滚计量,也不把丢弃误记为客户端 commit pub fn discard_pending_delivery(&mut self) -> Result<(), EngineError> { if !self.delivery_pending || self.downstream_committed_at.is_some() @@ -656,11 +656,11 @@ where Ok(()) } - /// 在 HTTP adapter 已确定首字节前错误响应后补写最终状态。 + /// 在 HTTP adapter 已确定首字节前错误响应后补写最终状态 /// /// # Errors /// - /// 状态已经写入时返回错误;观测 Store 失败只记服务端告警。 + /// 状态已经写入时返回错误;观测 Store 失败只记服务端告警 pub async fn record_client_status( &mut self, client_status_code: u16, @@ -689,7 +689,7 @@ where #[must_use] pub fn budget_charge(&self) -> super::budget::ClientBudgetCharge { - // 超出数据库可表示范围时保留最大金额,避免溢出后误记为零。 + // 超出数据库可表示范围时保留最大金额,避免溢出后误记为零 let amount_usd = self .budget_prior_attempts_usd .checked_add(self.budget_attempt_usd()) @@ -706,8 +706,8 @@ where if self.budget_attempt_already_counted { return Decimal::ZERO; } - // Key 只累计已取得的 USD 费用;缺少费用的请求按零结算。 - // 发送状态仍用于判断重放是否安全并保留在诊断中,不能据此推断存在欠费。 + // Key 只累计已取得的 USD 费用;缺少费用的请求按零结算 + // 发送状态仍用于判断重放是否安全并保留在诊断中,不能据此推断存在欠费 self.observation .cost .total() @@ -716,7 +716,7 @@ where .unwrap_or(Decimal::ZERO) } - /// 返回最终选中 attempt 已公开给协议层的安全响应头。 + /// 返回最终选中 attempt 已公开给协议层的安全响应头 #[must_use] pub fn response_headers(&self) -> &[ProviderResponseHeader] { self.current @@ -726,7 +726,7 @@ where .unwrap_or_default() } - /// 返回最终选中 attempt 观察到的上游 HTTP 状态码。 + /// 返回最终选中 attempt 观察到的上游 HTTP 状态码 #[must_use] pub fn response_status_code(&self) -> Option { self.current @@ -735,9 +735,9 @@ where .and_then(ProviderResponseObservation::status_code) } - /// 将已完成响应的账号事实与 Provider 私有状态封装为可丢失的亲和记录。 + /// 将已完成响应的账号事实与 Provider 私有状态封装为可丢失的亲和记录 /// - /// Core 不读取 `state` 内容;Provider 将在后续同账号 continuation 时自行解释。 + /// Core 不读取 `state` 内容;Provider 将在后续同账号 continuation 时自行解释 #[must_use] pub fn native_continuation_pin( &self, @@ -774,14 +774,14 @@ where ) } - /// 请求取消;实际终态在下一次会话 poll 时由 Core 持久化。 + /// 请求取消;实际终态在下一次会话 poll 时由 Core 持久化 pub fn cancel(&self) { self.cancellation.cancel(); } - /// 丢弃尚未提交的 delivery,并收敛为取消终态;已有终态写入继续保留原结果。 + /// 丢弃尚未提交的 delivery,并收敛为取消终态;已有终态写入继续保留原结果 /// - /// 观测写入失败只记录告警,不改变取消结果。 + /// 观测写入失败只记录告警,不改变取消结果 pub async fn cancel_and_finalize(&mut self) -> Result<(), EngineError> { self.cancellation.cancel(); self.resume_finalization().await; @@ -830,8 +830,8 @@ where } PollBoundary::Deadline => { // 会话 deadline 是网关自身的请求预算,不是上游超时; - // 真正的上游超时会作为流错误进入 `handle_stream_error` 记账。 - // 这里不写 provider 失败,避免把本地预算到期归因为上游故障。 + // 真正的上游超时会作为流错误进入 `handle_stream_error` 记账 + // 这里不写 provider 失败,避免把本地预算到期归因为上游故障 self.finish_interruption(&EngineError::Deadline).await?; return Err(EngineError::Deadline); } @@ -958,7 +958,7 @@ where .and_then(NonZeroU32::new) .ok_or(EngineError::EmptyRoutingPlan)?; // 请求局部恢复钉选在此被一次性消费,只绑定本次 replay attempt; - // 外部 required_account 每次 attempt 都重新生效。 + // 外部 required_account 每次 attempt 都重新生效 let (pinned_account, attempt_transport) = if let Some(recovery) = pending_retry { (Some(recovery.account), recovery.transport) } else { @@ -1083,7 +1083,7 @@ where return Err(EngineError::Cancelled); } ProviderBoundary::Deadline => { - // 网关预算到期是本请求的终态,不推定候选 Provider 不可用。 + // 网关预算到期是本请求的终态,不推定候选 Provider 不可用 self.finish_interruption(&EngineError::Deadline).await?; return Err(EngineError::Deadline); } @@ -1296,7 +1296,7 @@ where capacity_used_slots: capacity.map(|snapshot| snapshot.used_slots()), capacity_total_slots: capacity.map(|snapshot| snapshot.total_slots()), }; - // 合法冷流在可取消的观测写入前归会话所有,首写等待中断也不能退回零次或重新选号。 + // 合法冷流在可取消的观测写入前归会话所有,首写等待中断也不能退回零次或重新选号 self.attempts = next_attempt.get(); if !is_transport_recovery { self.routing_attempts = self.routing_attempts.saturating_add(1); @@ -1405,13 +1405,13 @@ where current.response_observation = Some(observation); } - /// 失败可重试时要求丢弃本 attempt;预算耗尽时可返回最后一批原始失败事件。 + /// 失败可重试时要求丢弃本 attempt;预算耗尽时可返回最后一批原始失败事件 async fn handle_stream_error( &mut self, mut error: ProviderError, ) -> Result { // 原始 wire 只活在 request-local 决策状态;clone、attempt 记录与持久化终态 - // 均只接触已剥离的稳定错误字段。 + // 均只接触已剥离的稳定错误字段 record_trace_error(&self.trace.attempt(self.attempts), &error); let mut atomic_client_events = error.take_atomic_client_events(); let current = self.current.take().ok_or(EngineError::NoActiveAttempt)?; @@ -1426,15 +1426,16 @@ where } self.record_provider_failure(current.metadata.provider().clone()); // attempt_send_state 是本 attempt 自身的发送事实;共享 effect 单独作为 - // 一票否决的重试门。持久化与终态用请求级水位,不能把早先 Provider attempt - // 的 sent 传染给当前 attempt,但任何已观测外部副作用都必须阻止重放。 + // 一票否决的重试门 + // 持久化与终态用请求级水位,不能把早先 Provider attempt + // 的 sent 传染给当前 attempt,但任何已观测外部副作用都必须阻止重放 let attempt_send_state = if current.send_observed { UpstreamSendState::Sent } else { error.send_state() }; if attempt_send_state != UpstreamSendState::NotSent { - // 已发送的容量拒绝、凭据恢复沿用原策略,不再受首次建连窗口限制。 + // 已发送的容量拒绝、凭据恢复沿用原策略,不再受首次建连窗口限制 self.connection_budget.complete(); } let execution_effect_observed = self.execution_effect_observed(); @@ -1468,7 +1469,7 @@ where Some(crate::error::PreDeliveryRetry::AccountRotation) ) && self.routing_attempts < self.plan.max_attempts().get(); - // 只有尚未发送的逻辑请求进入新增策略;已有发送后安全恢复保持原有路由规则。 + // 只有尚未发送的逻辑请求进入新增策略;已有发送后安全恢复保持原有路由规则 let connection_retry_requested = self.current_send_state() == UpstreamSendState::NotSent && matches!( error.pre_delivery_retry(), @@ -1609,14 +1610,14 @@ where } } // 原始 wire/HTTP response 由 request-local 所有权保留到下一次 attempt - // 成功,或最终空选路时返回客户端;持久化只取得稳定事实快照。 + // 成功,或最终空选路时返回客户端;持久化只取得稳定事实快照 let persistence_error = self.request_persisted.then(|| error.stable_snapshot()); if same_account_retry { let account = current.metadata.provider_account_id().clone(); self.credential_recovery_attempted_accounts .insert(account.clone()); // 只钉住紧随其后的 replay attempt;replay 再遇可重试错误时, - // ordinary/continuation 重试门不受影响,仍可换号。 + // ordinary/continuation 重试门不受影响,仍可换号 self.recovery_account = Some(account); } else if let Some(delay) = transient_retry { self.pending_retry = Some(PendingAttemptRetry { @@ -1710,7 +1711,7 @@ where decision = decision => Ok(decision), } }?; - // 策略只收窄宿主已允许的恢复,返回后仍复核期限和外部副作用。 + // 策略只收窄宿主已允许的恢复,返回后仍复核期限和外部副作用 Ok(allowed && decision != super::policy::RetryDecision::Stop && !self.execution_effect_observed() @@ -1805,11 +1806,12 @@ where true } - /// 原生续写的原账号在本地调度阶段已不可用时,交给 Provider 执行跨账号恢复。 + /// 原生续写的原账号在本地调度阶段已不可用时,交给 Provider 执行跨账号恢复 /// - /// 这不是强制 Smart:下一次 attempt 仍使用请求配置的调度策略。只有网关持有 + /// 这不是强制 Smart:下一次 attempt 仍使用请求配置的调度策略 + /// 只有网关持有 /// 对应 Provider 的会话状态时才进入恢复;是否保留 native handle、执行 probe - /// 或使用完整 transcript,由 Provider 自己的协议边界决定。 + /// 或使用完整 transcript,由 Provider 自己的协议边界决定 fn prepare_unavailable_native_continuation_replay(&mut self, error: &ProviderError) -> bool { if self.account_selection.required_account().is_some() || self.continuation_attempt != ContinuationAttempt::Native @@ -1844,7 +1846,7 @@ where } fn reset_uncommitted_observations(&mut self) { - // 响应观测按尝试隔离;被丢弃尝试中已取得的费用仍计入 Key。 + // 响应观测按尝试隔离;被丢弃尝试中已取得的费用仍计入 Key self.budget_prior_attempts_usd = self .budget_prior_attempts_usd .checked_add(self.budget_attempt_usd()) @@ -2099,7 +2101,7 @@ where ) }); let mut persisted = self.request_persisted; - // 首次合并写失败后不能以零 attempt 补行;建流前也只有确定未发送的请求可按零次收敛。 + // 首次合并写失败后不能以零 attempt 补行;建流前也只有确定未发送的请求可按零次收敛 let request = if !persisted && finalization.attempt_count == 0 && finalization.send_state == UpstreamSendState::NotSent @@ -2110,7 +2112,7 @@ where }; let store = Arc::clone(self.engine.store()); let request_id = self.request_id.clone(); - // 终态写入由会话持有;取消事件等待只暂停同一个 future,不重建请求或覆盖原失败。 + // 终态写入由会话持有;取消事件等待只暂停同一个 future,不重建请求或覆盖原失败 self.finalization = Some(RequestFinalization::Pending(Box::pin(async move { if let Some(request) = request { persisted = best_effort_store_write( @@ -2218,7 +2220,7 @@ where } /// 抬升并返回请求级发送水位;attempt 间切换(`current` 被取走)后, - /// 后续终态沿用已达到的最高档,不会把已落库的 `sent` 写回 `not_sent`。 + /// 后续终态沿用已达到的最高档,不会把已落库的 `sent` 写回 `not_sent` fn raise_send_watermark(&mut self, observed: UpstreamSendState) -> UpstreamSendState { let observed = escalate_send_state(self.current_send_state(), observed); self.send_state_watermark = observed; @@ -2416,8 +2418,8 @@ fn provider_proved_replay_safe(error: &ProviderError) -> bool { || (error.send_state() != UpstreamSendState::Ambiguous && error.replay_is_safe()) } -/// 发送状态合并档位:`Sent` > `Ambiguous` > `NotSent`。 -/// 只要任一 attempt 达到过高档,请求整体就不允许回落到低档。 +/// 发送状态合并档位:`Sent` > `Ambiguous` > `NotSent` +/// 只要任一 attempt 达到过高档,请求整体就不允许回落到低档 const fn escalate_send_state(a: UpstreamSendState, b: UpstreamSendState) -> UpstreamSendState { match (a, b) { (UpstreamSendState::Sent, _) | (_, UpstreamSendState::Sent) => UpstreamSendState::Sent, diff --git a/backend/crates/gateway-core/src/engine/execution.rs b/backend/crates/gateway-core/src/engine/execution.rs index b49f0d427..f9f515b90 100644 --- a/backend/crates/gateway-core/src/engine/execution.rs +++ b/backend/crates/gateway-core/src/engine/execution.rs @@ -1,4 +1,4 @@ -//! 数据面执行用例:认证、准入、路由、continuation 与会话生命周期。 +//! 数据面执行用例:认证、准入、路由、continuation 与会话生命周期 use std::collections::{BTreeMap, BTreeSet}; use std::fmt; @@ -95,7 +95,7 @@ impl ClientTransport { } } -/// API 解码后交给 Core 的稳定请求元数据。 +/// API 解码后交给 Core 的稳定请求元数据 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ExecutionRequestMetadata { pub protocol: String, @@ -127,7 +127,7 @@ impl AuthenticatedClient { self.settings.as_ref() } - /// 后续仍须重新认证;这里只更新下一次认证使用的冻结配置。 + /// 后续仍须重新认证;这里只更新下一次认证使用的冻结配置 #[must_use] pub fn with_request_settings(mut self, settings: RequestSettings) -> Self { self.settings = Some(settings); @@ -172,10 +172,10 @@ pub struct StartExecution { pub metadata: ExecutionRequestMetadata, } -/// 入口中间件开始前冻结的根请求身份与生命周期。 +/// 入口中间件开始前冻结的根请求身份与生命周期 /// /// 中间件短路时本值直接释放;调用 `next` 后必须原样交回 -/// [`ExecutionService::start_prepared`],不能重新认证为另一个 Key。 +/// [`ExecutionService::start_prepared`],不能重新认证为另一个 Key pub struct PreparedRootExecution { extension_scope: ExtensionCallScope, response_control: super::response_control::ResponseControl, @@ -203,7 +203,7 @@ impl PreparedRootExecution { ) } - /// 入口延续发起实例集合;保留已经交给调用方的取消句柄。 + /// 入口延续发起实例集合;保留已经交给调用方的取消句柄 #[must_use] pub fn with_extension_scope(mut self, scope: ExtensionCallScope) -> Self { self.extension_scope = scope; @@ -215,7 +215,7 @@ impl PreparedRootExecution { self.extension_scope.clone() } - /// 设置已经在改写时完成编译;此处只将同一份结果应用到冻结身份和生命周期。 + /// 设置已经在改写时完成编译;此处只将同一份结果应用到冻结身份和生命周期 pub fn apply_settings(&mut self, settings: &RequestSettings) -> Result<(), GatewayError> { let deadline = settings .execution_deadline(self.client.policy.key_id(), self.started_at) @@ -283,7 +283,7 @@ impl PreparedRootExecution { self.cancellation.clone() } - /// 以冻结的入口身份补齐 Runtime-only binding 事实;调用位置事实由 API/Core 提供。 + /// 以冻结的入口身份补齐 Runtime-only binding 事实;调用位置事实由 API/Core 提供 #[must_use] pub fn middleware_context( &self, @@ -314,18 +314,18 @@ impl PreparedRootExecution { } } -/// 入口中间件调用 `next` 后交给 Core 的已解码执行输入。 +/// 入口中间件调用 `next` 后交给 Core 的已解码执行输入 pub struct PreparedExecutionRequest { pub public_model: PublicModelId, pub operation: Operation, pub metadata: ExecutionRequestMetadata, } -/// 启动一个由协议 adapter 明确绑定到 Provider 自有端点的请求。 +/// 启动一个由协议 adapter 明确绑定到 Provider 自有端点的请求 pub struct StartProviderExecution { pub client: AuthenticatedClient, pub provider: ProviderKind, - /// Provider 自有模型端点可固定上游模型;非模型端点保持 `None`。 + /// Provider 自有模型端点可固定上游模型;非模型端点保持 `None` pub upstream_model: Option, pub operation: Operation, pub metadata: ExecutionRequestMetadata, @@ -489,9 +489,9 @@ struct ActiveRequestLease { authority: Arc, } -/// 管理页或 CLI 一次调用持有的显式模型执行身份。 +/// 管理页或 CLI 一次调用持有的显式模型执行身份 /// -/// 结构不暴露 Key 明文;Runtime 只能把它原样交回 Core 发起模型请求。 +/// 结构不暴露 Key 明文;Runtime 只能把它原样交回 Core 发起模型请求 #[derive(Clone)] pub struct BoundModelExecutionContext { authority: Arc, @@ -582,16 +582,16 @@ pub trait ExecutionSession: Send { &mut self, client_status_code: u16, ) -> BoxFuture<'_, Result<(), EngineError>>; - /// 执行已终结且结算、准入释放均已返回,协议层才可以放弃清理责任。 - /// 结算失败仍由 Store 保留费用重试,释放失败仍按租约 TTL 收敛。 + /// 执行已终结且结算、准入释放均已返回,协议层才可以放弃清理责任 + /// 结算失败仍由 Store 保留费用重试,释放失败仍按租约 TTL 收敛 fn is_finalized(&self) -> bool; fn cancel(&self); - /// 将会话交给宿主持续驱动清理;取消请求事件的等待不会丢弃已开始的结算。 + /// 将会话交给宿主持续驱动清理;取消请求事件的等待不会丢弃已开始的结算 fn detach_finalize(self: Box) -> BoxFuture<'static, ()>; } pub trait ExecutionService: Send + Sync { - /// 未就绪时管理与健康接口仍可运行,模型认证按既有路径报告不可用。 + /// 未就绪时管理与健康接口仍可运行,模型认证按既有路径报告不可用 fn request_settings(&self) -> Option { None } @@ -611,7 +611,7 @@ pub trait ExecutionService: Send + Sync { self.authenticate(plaintext.expose_for_auth()) }) } - /// 只验证入口认证信封,不记录 Key 使用事实或执行推理准入。 + /// 只验证入口认证信封,不记录 Key 使用事实或执行推理准入 fn verify_request( &self, _request: ClientAuthenticationRequest, @@ -629,7 +629,7 @@ pub trait ExecutionService: Send + Sync { } fn contains_public_model(&self, client: &AuthenticatedClient, model: &PublicModelId) -> bool; - /// 重新鉴权并冻结本次根请求身份、请求 ID、deadline 与取消域。 + /// 重新鉴权并冻结本次根请求身份、请求 ID、deadline 与取消域 fn prepare_execution( &self, client: AuthenticatedClient, @@ -637,8 +637,8 @@ pub trait ExecutionService: Send + Sync { Box::pin(async move { PreparedRootExecution::new(client) }) } - /// 由已经验证管理会话与插件 models 域的宿主入口选择 Key。 - /// 不向页面提供明文,也不跳过普通请求的扩展、准入和计量。 + /// 由已经验证管理会话与插件 models 域的宿主入口选择 Key + /// 不向页面提供明文,也不跳过普通请求的扩展、准入和计量 fn prepare_plugin_execution( &self, _client_key_id: &ClientApiKeyId, @@ -651,7 +651,7 @@ pub trait ExecutionService: Send + Sync { }) } - /// 为已经通过 `verify_request` 的只读入口建立中间件生命周期,不重复写 Key 使用事实。 + /// 为已经通过 `verify_request` 的只读入口建立中间件生命周期,不重复写 Key 使用事实 fn prepare_verified_execution( &self, client: AuthenticatedClient, @@ -659,12 +659,12 @@ pub trait ExecutionService: Send + Sync { PreparedRootExecution::new(client) } - /// 解析与准备阶段冻结的发布代次一致的中间件计划。 + /// 解析与准备阶段冻结的发布代次一致的中间件计划 fn middleware_plan(&self, _prepared: &PreparedRootExecution) -> Option { None } - /// 消费一次已准备身份并进入原有 Core 路由、准入、attempt 与结算路径。 + /// 消费一次已准备身份并进入原有 Core 路由、准入、attempt 与结算路径 fn start_prepared( &self, prepared: PreparedRootExecution, @@ -681,7 +681,7 @@ pub trait ExecutionService: Send + Sync { }) } - /// 消费一次已准备身份,并进入 Provider 自有端点的原有准入与结算路径。 + /// 消费一次已准备身份,并进入 Provider 自有端点的原有准入与结算路径 fn start_prepared_provider_endpoint( &self, prepared: PreparedRootExecution, @@ -712,9 +712,9 @@ pub trait ExecutionService: Send + Sync { ) -> BoxFuture<'_, Result>; } -/// 只验证 Client API Key 并返回稳定 Key ID,不产生请求使用事实。 +/// 只验证 Client API Key 并返回稳定 Key ID,不产生请求使用事实 /// -/// 管理侧登录等非数据面场景必须使用该端口,避免把认证本身误记为一次 Key 使用。 +/// 管理侧登录等非数据面场景必须使用该端口,避免把认证本身误记为一次 Key 使用 pub trait ClientKeyVerifier: Send + Sync { fn verify_client_key( &self, @@ -722,16 +722,16 @@ pub trait ClientKeyVerifier: Send + Sync { ) -> Result; } -/// 成功认证后的 API Key 使用事实接收器。 +/// 成功认证后的 API Key 使用事实接收器 /// -/// 认证仍是同步快照读取;实现必须自行异步、去重地持久化,不得阻塞客户端请求。 +/// 认证仍是同步快照读取;实现必须自行异步、去重地持久化,不得阻塞客户端请求 pub trait ClientApiKeyUsageSink: Send + Sync { fn record_used(&self, key_id: &ClientApiKeyId); } pub struct DefaultExecutionService { snapshots: RuntimeSnapshotHandle, - /// probe 自身走 transient store,探测失败仍写入持久 store 的 ops_events。 + /// probe 自身走 transient store,探测失败仍写入持久 store 的 ops_events observations: Arc, providers: ProviderRegistry, admissions: Arc, @@ -819,7 +819,7 @@ impl DefaultExecutionService { self } - /// CLI command-plane 没有后台订阅;每次绑定显式身份前主动编译最新事实。 + /// CLI command-plane 没有后台订阅;每次绑定显式身份前主动编译最新事实 #[must_use] pub fn with_snapshot_refresh(mut self, refresh: Arc) -> Self { self.snapshot_refresh = Some(refresh); @@ -954,7 +954,7 @@ impl DefaultExecutionService { &self, mut client: AuthenticatedClient, ) -> Result { - // 长连接每次执行都重新鉴权并冻结当前策略,确保限额和授权变更对新请求生效。 + // 长连接每次执行都重新鉴权并冻结当前策略,确保限额和授权变更对新请求生效 let expected_key_id = client.policy.key_id().clone(); let authentication = client.authentication.clone().ok_or_else(|| { GatewayError::new( @@ -1081,7 +1081,8 @@ impl DefaultExecutionService { ) }); // 路由插件可以调用 host.model/host.affinity;仅这条扩展路径必须在首次 RPC - // 前取得父 Key 准入。无策略的原生路由仍保持“先路由、后准入”的既有顺序。 + // 前取得父 Key 准入 + // 无策略的原生路由仍保持“先路由、后准入”的既有顺序 let mut start_guard = None; let request_observation = self.request_observers.as_ref().and_then(|observers| { let generation = request.client.snapshot.extensions()?.clone(); @@ -1366,7 +1367,7 @@ impl DefaultExecutionService { client: request.client.clone(), account_scope: Arc::clone(&authorization.account_scope), deadline_at: authorization.deadline_at, - // 回收嵌套调用权限只取消子作用域,父请求仍需完成响应流终态加工。 + // 回收嵌套调用权限只取消子作用域,父请求仍需完成响应流终态加工 cancellation: authorization.cancellation.child_token(), extension_scope: authorization.extension_scope.clone(), graph: Arc::clone(graph), @@ -1600,7 +1601,7 @@ impl DefaultExecutionService { )?; let nested_permit = parent.graph.acquire()?; // Core.start 之后的路由/Provider 可以继续调用外部系统;在进入子执行前即按 - // 可能已出站记水位,不能让父 attempt 随后的 `not_sent` 触发重放。 + // 可能已出站记水位,不能让父 attempt 随后的 `not_sent` 触发重放 parent.graph.effects.observe(); let execution_effects_baseline = parent.graph.effects.epoch(); metadata.endpoint = "host.model".to_owned(); @@ -1750,8 +1751,8 @@ impl DefaultExecutionService { account.as_ref(), None, )?; - // 连接承载多个独立执行;递归预算和副作用只在本次执行的子调用图内共享。 - // 父连接的取消仍通过 authority 传播,不能因新建执行丢失扩展调用链。 + // 连接承载多个独立执行;递归预算和副作用只在本次执行的子调用图内共享 + // 父连接的取消仍通过 authority 传播,不能因新建执行丢失扩展调用链 let graph = Arc::new(NestedExecutionGraph::new(Arc::new( ExecutionEffects::default(), ))); @@ -1873,7 +1874,7 @@ impl DefaultExecutionService { "request deadline elapsed", )); } - // 在发送原子准入之前接管取消清理,覆盖 Redis 已取得租约但返回尚未被观察的窗口。 + // 在发送原子准入之前接管取消清理,覆盖 Redis 已取得租约但返回尚未被观察的窗口 admission.armed = true; let acquire = self .admissions @@ -2046,7 +2047,7 @@ impl DefaultExecutionService { }) } - /// 探测失败先记录脱敏分类事实,再把请求局部的原始上游响应交给认证管理端。 + /// 探测失败先记录脱敏分类事实,再把请求局部的原始上游响应交给认证管理端 async fn observe_probe_failure( &self, observed: &ProbeObservation, @@ -2239,7 +2240,7 @@ impl ExecutionService for DefaultExecutionService { } continue; }; - // 保留上游顺序;映射只选择完整的目标对象,不能跨账号/模型混拼字段。 + // 保留上游顺序;映射只选择完整的目标对象,不能跨账号/模型混拼字段 let by_id = models .iter() .map(|entry| (entry.model.as_str(), entry)) @@ -2254,7 +2255,7 @@ impl ExecutionService for DefaultExecutionService { }; let model = PublicModelId::new(entry.model.as_str().to_owned()) .map_err(|_| ProviderCatalogUnavailable)?; - // 原生目录可能比路由快照更新,不能向客户端公布当前已知不可路由的模型。 + // 原生目录可能比路由快照更新,不能向客户端公布当前已知不可路由的模型 if !client .snapshot .contains_public_model_for_provider(&model, kind) @@ -2575,8 +2576,9 @@ impl DefaultExecutionSession { } let budget = self.budget.take(); let charge = self.core.budget_charge(); - // 在首次 await 前把完整清理责任留在会话内。事件等待被取消后,后续 poll - // 或 detach 继续同一个 future,既不丢失费用,也不重启已完成的结算。 + // 在首次 await 前把完整清理责任留在会话内 + // 事件等待被取消后,后续 poll + // 或 detach 继续同一个 future,既不丢失费用,也不重启已完成的结算 self.cleanup = Some(Box::pin(async move { if let Some(budget) = budget { settle_budget(budget.as_ref(), charge).await; diff --git a/backend/crates/gateway-core/src/engine/extensions.rs b/backend/crates/gateway-core/src/engine/extensions.rs index 62b04b3ad..1d058c3d3 100644 --- a/backend/crates/gateway-core/src/engine/extensions.rs +++ b/backend/crates/gateway-core/src/engine/extensions.rs @@ -1,11 +1,12 @@ -//! 插件嵌套调用的递归作用域。 +//! 插件嵌套调用的递归作用域 use std::{collections::BTreeSet, sync::Arc}; -/// 一次请求已经进入过的插件实例集合。 +/// 一次请求已经进入过的插件实例集合 /// /// Core 把它随子请求传播给策略与观察边界;Runtime 只据此跳过对应 -/// 实例,不能自行扩大授权或改变路由。集合同时用于拒绝 A → B → A 间接递归。 +/// 实例,不能自行扩大授权或改变路由 +/// 集合同时用于拒绝 A → B → A 间接递归 #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct ExtensionCallScope { instance_ids: Arc>, @@ -29,7 +30,7 @@ impl ExtensionCallScope { self.instance_ids.is_empty() } - /// 返回加入当前发起实例后的新作用域;重复实例表示间接递归。 + /// 返回加入当前发起实例后的新作用域;重复实例表示间接递归 #[must_use] pub fn extending(&self, instance_id: String) -> Option { if self.contains(&instance_id) { diff --git a/backend/crates/gateway-core/src/engine/middleware.rs b/backend/crates/gateway-core/src/engine/middleware.rs index 95ea4ab48..5cf19e0f8 100644 --- a/backend/crates/gateway-core/src/engine/middleware.rs +++ b/backend/crates/gateway-core/src/engine/middleware.rs @@ -1,8 +1,9 @@ -//! 数据面请求/响应的统一洋葱中间件端口。 +//! 数据面请求/响应的统一洋葱中间件端口 //! -//! 本模块只承载宿主内部组合合同。插件 RPC 如何表示 `next` 和正文句柄由 +//! 本模块只承载宿主内部组合合同 +//! 插件 RPC 如何表示 `next` 和正文句柄由 //! Runtime/SDK 负责;身份、路由、账号租约、发送事实、重试、计量与结算仍由 Core -//! 持有,不能通过本端口交给插件解释。 +//! 持有,不能通过本端口交给插件解释 use std::collections::{BTreeMap, BTreeSet}; use std::fmt; @@ -29,33 +30,33 @@ use crate::policy::ClientApiKeyId; use crate::runtime::extensions::{ExtensionSetId, ExtensionSetReference}; use crate::settings::RequestSettings; -/// 中间件在一次逻辑请求中的挂载位置。 +/// 中间件在一次逻辑请求中的挂载位置 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum MiddlewareMount { - /// 所有 HTTP 接口的认证、路由和正文解析之前。 + /// 所有 HTTP 接口的认证、路由和正文解析之前 Http, - /// 下游 WebSocket 消息在默认解析和写入之前组合。 + /// 下游 WebSocket 消息在默认解析和写入之前组合 WebSocket, - /// 类型化公开服务在执行默认业务逻辑前组合。 + /// 类型化公开服务在执行默认业务逻辑前组合 Service, - /// 客户端请求外层;一次逻辑请求只执行一次。 + /// 客户端请求外层;一次逻辑请求只执行一次 Request, - /// 受管 Provider attempt 内层;每次真实 retry 都重新建立。 + /// 受管 Provider attempt 内层;每次真实 retry 都重新建立 Attempt, } -/// 中间件正文块的宿主边界。 +/// 中间件正文块的宿主边界 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MiddlewareFraming { - /// 一个完整 JSON document。 + /// 一个完整 JSON document JsonDocument, - /// 一个完整 SSE event,包含终止该事件的空行。 + /// 一个完整 SSE event,包含终止该事件的空行 SseEvent, - /// 不承诺 UTF-8 或消息边界的原始字节。 + /// 不承诺 UTF-8 或消息边界的原始字节 RawBytes, } -/// 允许进入中间件合同的多值 HTTP 头。 +/// 允许进入中间件合同的多值 HTTP 头 #[derive(Clone, PartialEq, Eq)] pub struct MiddlewareHeader { name: String, @@ -97,7 +98,7 @@ impl fmt::Debug for MiddlewareHeader { } } -/// 一次中间件调用可修改的请求事实。 +/// 一次中间件调用可修改的请求事实 #[derive(Clone, PartialEq, Eq)] pub struct MiddlewareRequest { settings: Option, @@ -109,7 +110,7 @@ pub struct MiddlewareRequest { capabilities: Option, } -/// 只声明具体功能的责任归属,实际正文中的需求始终继续生效。 +/// 只声明具体功能的责任归属,实际正文中的需求始终继续生效 #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct MiddlewareCapabilityDeclaration { pub handled: BTreeSet, @@ -162,7 +163,7 @@ impl MiddlewareRequest { (self.protocol, self.headers, self.body) } - /// 保留链入口的语义来源;新增声明必须对应当前这一层实际消除的功能字段。 + /// 保留链入口的语义来源;新增声明必须对应当前这一层实际消除的功能字段 pub fn replace_parts( mut self, protocol: String, @@ -197,7 +198,7 @@ impl MiddlewareRequest { Ok(self) } - /// API 在最终解码后应用;没有声明的透传路径不解析或重新编码正文。 + /// API 在最终解码后应用;没有声明的透传路径不解析或重新编码正文 pub fn apply_capabilities(&self, operation: Operation) -> Result { let Some(declaration) = &self.capabilities else { return Ok(operation); @@ -246,7 +247,7 @@ impl fmt::Debug for MiddlewareRequest { } } -/// 中间件逐次拉取的完整正文边界。 +/// 中间件逐次拉取的完整正文边界 pub struct MiddlewareFrame { bytes: Bytes, framing: MiddlewareFraming, @@ -282,21 +283,21 @@ impl MiddlewareFrame { self.terminal } - /// 返回正文是否经过至少一个中间件的替换、展开或丢弃映射。 + /// 返回正文是否经过至少一个中间件的替换、展开或丢弃映射 /// - /// 插件可读取该事实;对快照的修改不改变宿主确认的来源。 + /// 插件可读取该事实;对快照的修改不改变宿主确认的来源 #[must_use] pub const fn transformed(&self) -> bool { self.transformed } - /// 返回来源事件的只读视图;读取不转移原结算和会话状态的所有权。 + /// 返回来源事件的只读视图;读取不转移原结算和会话状态的所有权 #[must_use] pub fn event(&self) -> Option<&ProviderEvent> { self.envelope.as_ref().map(MiddlewareFrameEnvelope::event) } - /// 继承宿主确认的改写事实;`false` 不能清除内层已经记录的改写。 + /// 继承宿主确认的改写事实;`false` 不能清除内层已经记录的改写 #[must_use] pub fn with_transformed(mut self, transformed: bool) -> Self { self.transformed |= transformed; @@ -308,10 +309,11 @@ impl MiddlewareFrame { self.bytes } - /// 拆出插件可见正文与只能由宿主原样搬运的事实信封。 + /// 拆出插件可见正文与只能由宿主原样搬运的事实信封 /// /// Runtime 在一对多转换时必须把信封附到最后一个输出;这样 `Completed` 不会在 - /// 前置输出交付前提前终结。读取快照不会转移信封的计量所有权。 + /// 前置输出交付前提前终结 + /// 读取快照不会转移信封的计量所有权 #[must_use] pub fn into_parts( self, @@ -324,7 +326,7 @@ impl MiddlewareFrame { (self.bytes, self.framing, self.terminal, self.envelope) } - /// 把宿主持有的事实信封附到改写后的最后一个输出。 + /// 把宿主持有的事实信封附到改写后的最后一个输出 #[must_use] pub fn with_envelope(mut self, envelope: MiddlewareFrameEnvelope) -> Self { self.envelope = Some(envelope); @@ -372,9 +374,9 @@ impl fmt::Debug for MiddlewareFrame { } } -/// 可读取完整事件、只能随正文移动一次的 Core 事实信封。 +/// 可读取完整事件、只能随正文移动一次的 Core 事实信封 /// -/// 该类型故意不实现 `Clone`,防止同一 usage/cost 在一对多响应中被复制。 +/// 该类型故意不实现 `Clone`,防止同一 usage/cost 在一对多响应中被复制 pub struct MiddlewareFrameEnvelope { event: ProviderEvent, } @@ -394,14 +396,16 @@ impl fmt::Debug for MiddlewareFrameEnvelope { } } -/// 洋葱返回路径上的惰性正文。 +/// 洋葱返回路径上的惰性正文 /// -/// 实现必须让 `Drop` 与 [`Self::close`] 都回收底层流。请求 finalization 继续委托 -/// 原有 [`crate::engine::execution::ExecutionSession`],本端口不建立第二套清理器。 +/// 实现必须让 `Drop` 与 [`Self::close`] 都回收底层流 +/// 请求 finalization 继续委托 +/// 原有 [`crate::engine::execution::ExecutionSession`],本端口不建立第二套清理器 pub trait MiddlewareBody: Send { fn next_frame(&mut self) -> BoxFuture<'_, Result, MiddlewareError>>; - /// 在最外层输出准备完成后提交原有 Core delivery barrier。中间件实现只能向内委托。 + /// 在最外层输出准备完成后提交原有 Core delivery barrier + /// 中间件实现只能向内委托 fn commit_downstream( &mut self, _client_status_code: Option, @@ -409,7 +413,8 @@ pub trait MiddlewareBody: Send { Box::pin(async { Ok(()) }) } - /// 首字节前失败时记录最终返回给客户端的状态。中间件实现只能向内委托。 + /// 首字节前失败时记录最终返回给客户端的状态 + /// 中间件实现只能向内委托 fn record_client_status( &mut self, _client_status_code: u16, @@ -417,7 +422,7 @@ pub trait MiddlewareBody: Send { Box::pin(async { Ok(()) }) } - /// 没有调用 `next` 的短路正文视为无需 Core 结算;调用过 `next` 的包装器必须委托。 + /// 没有调用 `next` 的短路正文视为无需 Core 结算;调用过 `next` 的包装器必须委托 fn is_finalized(&self) -> bool { true } @@ -425,7 +430,7 @@ pub trait MiddlewareBody: Send { fn close(self: Box) -> BoxFuture<'static, ()>; } -/// `next` 或短路返回的响应头与惰性正文。 +/// `next` 或短路返回的响应头与惰性正文 pub struct MiddlewareResponse { protocol: String, status_code: u16, @@ -485,7 +490,7 @@ impl MiddlewareResponse { ) } - /// 搬运宿主 terminal 的来源事实;读取快照不转移其所有权。 + /// 搬运宿主 terminal 的来源事实;读取快照不转移其所有权 #[must_use] pub fn with_envelope(mut self, envelope: MiddlewareResponseEnvelope) -> Self { self.envelope = Some(envelope); @@ -511,7 +516,7 @@ impl fmt::Debug for MiddlewareResponse { } } -/// Attempt terminal 的 Core 来源事实;中间件只能随响应原样搬运。 +/// Attempt terminal 的 Core 来源事实;中间件只能随响应原样搬运 pub struct MiddlewareResponseEnvelope { metadata: ProviderCallMetadata, } @@ -535,7 +540,7 @@ impl fmt::Debug for MiddlewareResponseEnvelope { } } -/// Core 冻结的调用事实;完整业务身份可由插件读取。 +/// Core 冻结的调用事实;完整业务身份可由插件读取 #[derive(Clone)] pub struct MiddlewareContext { plan: Option, @@ -648,13 +653,13 @@ impl MiddlewareContext { self.account_id.as_ref() } - /// 返回冻结的业务归属,也用于匹配 binding。 + /// 返回冻结的业务归属,也用于匹配 binding #[must_use] pub const fn client_key_id(&self) -> &ClientApiKeyId { &self.client_key_id } - /// 返回冻结的业务归属,也用于匹配 binding。 + /// 返回冻结的业务归属,也用于匹配 binding #[must_use] pub fn account_group_ids(&self) -> &[AccountGroupId] { &self.account_group_ids @@ -675,14 +680,14 @@ impl MiddlewareContext { &self.extension_scope } - /// 只供受信 Runtime 记录本次中间件调用已经产生的外部副作用,不进入插件 wire。 + /// 只供受信 Runtime 记录本次中间件调用已经产生的外部副作用,不进入插件 wire #[must_use] pub fn execution_effects(&self) -> Option> { self.execution_effects.as_ref().map(Arc::clone) } } -/// 不含授权主体的调用位置与目标事实。 +/// 不含授权主体的调用位置与目标事实 pub struct MiddlewareTarget { pub request_id: ModelRequestId, pub mount: MiddlewareMount, @@ -695,7 +700,7 @@ pub struct MiddlewareTarget { pub account_id: Option, } -/// 冻结的业务主体与生命周期;不用于裁剪插件权限。 +/// 冻结的业务主体与生命周期;不用于裁剪插件权限 pub struct MiddlewareAuthority { pub client_key_id: ClientApiKeyId, pub account_group_ids: Arc<[AccountGroupId]>, @@ -723,12 +728,13 @@ impl fmt::Debug for MiddlewareContext { } } -/// 中间件链调用错误。原有域错误保持原类型,不能被插件故障改写为“未发送”。 +/// 中间件链调用错误 +/// 原有域错误保持原类型,不能被插件故障改写为“未发送” #[derive(Debug)] pub enum MiddlewareError { Rejected, Fault, - /// 保留跨进程失败,供外层插件读取;客户端响应由协议 owner 决定。 + /// 保留跨进程失败,供外层插件读取;客户端响应由协议 owner 决定 Remote { source: Box, rejected: bool, @@ -790,11 +796,11 @@ impl From for MiddlewareError { } } -/// 绑定当前调用且只能消费一次的宿主续体。 +/// 绑定当前调用且只能消费一次的宿主续体 pub type MiddlewareNext = crate::middleware::Next; -/// 一个发布代次冻结的统一数据面中间件计划。 +/// 一个发布代次冻结的统一数据面中间件计划 pub trait MiddlewarePlan: Send + Sync + fmt::Debug { fn has_service(&self) -> bool { false @@ -824,7 +830,7 @@ pub trait MiddlewarePlan: Send + Sync + fmt::Debug { next.run(message) } - /// HTTP 入口在认证、路由与正文解析之前组合,无匹配时保留直接路径。 + /// HTTP 入口在认证、路由与正文解析之前组合,无匹配时保留直接路径 fn has_http(&self) -> bool { false } @@ -846,7 +852,7 @@ pub trait MiddlewarePlan: Send + Sync + fmt::Debug { ) -> BoxFuture<'static, Result>; } -/// 调用期间同时保活发布代次与中间件计划。 +/// 调用期间同时保活发布代次与中间件计划 #[derive(Clone)] pub struct FrozenMiddlewarePlan { plan: Arc, @@ -926,12 +932,12 @@ impl fmt::Debug for FrozenMiddlewarePlan { } } -/// 同一发布代次不能被静默替换。 +/// 同一发布代次不能被静默替换 #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] #[error("middleware generation is already registered")] pub struct MiddlewareRegistrationError; -/// 按请求冻结的发布集合解析中间件计划;索引不延长旧代次寿命。 +/// 按请求冻结的发布集合解析中间件计划;索引不延长旧代次寿命 #[derive(Clone, Default)] pub struct MiddlewareExtensionIndex { sets: Arc>>>, diff --git a/backend/crates/gateway-core/src/engine/mod.rs b/backend/crates/gateway-core/src/engine/mod.rs index 228579967..f9cc316dc 100644 --- a/backend/crates/gateway-core/src/engine/mod.rs +++ b/backend/crates/gateway-core/src/engine/mod.rs @@ -1,4 +1,4 @@ -//! 模型请求生命周期、单行持久化 port 与 commit/send/cancellation 边界。 +//! 模型请求生命周期、单行持久化 port 与 commit/send/cancellation 边界 pub mod admission; pub mod authentication; @@ -48,7 +48,7 @@ use crate::routing::{ConfigRevision, PublicModelId, UpstreamModelId}; use crate::upstream::UpstreamSendState; use crate::validation::{IdentifierError, validate_text}; -/// `model_requests.id`。 +/// `model_requests.id` #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ModelRequestId(String); @@ -71,7 +71,7 @@ impl fmt::Display for ModelRequestId { } } -/// `model_requests.outcome`。 +/// `model_requests.outcome` #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ExecutionOutcome { Running, @@ -94,22 +94,22 @@ impl ExecutionOutcome { } } -/// Request-local attempt 的原因,不对应数据库表。 +/// Request-local attempt 的原因,不对应数据库表 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum AttemptTrigger { Initial, AccountRetry, } -/// Provider 可为当前 attempt 选择的请求局部传输档位。 +/// Provider 可为当前 attempt 选择的请求局部传输档位 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] pub enum AttemptTransport { - /// 使用 Provider 的默认传输策略。 + /// 使用 Provider 的默认传输策略 #[default] Default, - /// 固定账号重试 Provider 的首选传输;序号由 Provider 的独立预算驱动。 + /// 固定账号重试 Provider 的首选传输;序号由 Provider 的独立预算驱动 Retry(NonZeroU32), - /// 使用 Provider 定义的备用传输。 + /// 使用 Provider 定义的备用传输 Fallback, } @@ -123,10 +123,10 @@ impl AttemptTrigger { } } -/// 单次 attempt 的账号选择与账号绑定状态事实。 +/// 单次 attempt 的账号选择与账号绑定状态事实 /// /// `required_account` 用于管理端 connection test,或请求局部的同账号恢复 attempt; -/// 一旦设置,Provider 在本 attempt 内不得换号或切换 target。 +/// 一旦设置,Provider 在本 attempt 内不得换号或切换 target #[derive(Debug, Clone, Default)] pub struct AccountAttemptContext { excluded_accounts: BTreeSet, @@ -154,9 +154,9 @@ impl AccountAttemptContext { } } - /// 为管理端对固定账号的上游诊断创建上下文。 + /// 为管理端对固定账号的上游诊断创建上下文 /// - /// 诊断只绕过本地的被动可用性投影,不能换号,并继续受账号租约保护。 + /// 诊断只绕过本地的被动可用性投影,不能换号,并继续受账号租约保护 #[must_use] pub const fn diagnostic( excluded_accounts: BTreeSet, @@ -173,7 +173,7 @@ impl AccountAttemptContext { } } - /// 附着普通请求认证时冻结的账号范围。 + /// 附着普通请求认证时冻结的账号范围 #[must_use] pub fn with_account_scope( mut self, @@ -183,7 +183,7 @@ impl AccountAttemptContext { self } - /// 标记本请求已对即将选择的固定账号执行过一次凭据恢复。 + /// 标记本请求已对即将选择的固定账号执行过一次凭据恢复 #[must_use] pub const fn with_credential_recovery_attempted(mut self, attempted: bool) -> Self { self.credential_recovery_attempted = attempted; @@ -210,7 +210,7 @@ impl AccountAttemptContext { self.credential_recovery_attempted } - /// 此尝试是否要由真实上游而非本地投影确认固定账号的可用性。 + /// 此尝试是否要由真实上游而非本地投影确认固定账号的可用性 #[must_use] pub const fn is_diagnostic_required_account(&self) -> bool { self.diagnostic_required_account @@ -222,10 +222,10 @@ impl AccountAttemptContext { } } -/// 请求中 Provider 账号绑定状态的唯一归属。 +/// 请求中 Provider 账号绑定状态的唯一归属 /// /// `turn_state` 等 opaque 状态只能发送给创建它的 Provider 与账号; -/// Core 在首次真实选号后冻结该事实,Provider 据此决定是否清理跨账号状态。 +/// Core 在首次真实选号后冻结该事实,Provider 据此决定是否清理跨账号状态 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderAccountStateOwner { provider: ProviderKind, @@ -259,21 +259,21 @@ impl ProviderAccountStateOwner { } } -/// 当前 attempt 对 previous-response 的唯一处理方式。 +/// 当前 attempt 对 previous-response 的唯一处理方式 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ContinuationAttempt { - /// 没有 previous-response。 + /// 没有 previous-response None, - /// 使用 Store 解析出的原生 handle 与账号绑定。 + /// 使用 Store 解析出的原生 handle 与账号绑定 Native, - /// 在原账号上执行 Provider 定义的恢复。 + /// 在原账号上执行 Provider 定义的恢复 /// /// 只有持有可携带 transcript 的 Provider 才能清除 native handle 后重放; - /// 只持有 opaque state 的 Provider 必须保留 continuation 依赖或返回客户端错误。 + /// 只持有 opaque state 的 Provider 必须保留 continuation 依赖或返回客户端错误 ReplayOwner, - /// 允许选择其他账号并执行 Provider 定义的恢复。 + /// 允许选择其他账号并执行 Provider 定义的恢复 /// - /// 该枚举本身不证明客户端 input 完整,也不授权 Provider 清除 previous-response。 + /// 该枚举本身不证明客户端 input 完整,也不授权 Provider 清除 previous-response ReplayAny, } @@ -289,7 +289,7 @@ impl ContinuationAttempt { } } -/// Provider 每次执行可见的 request-local context。 +/// Provider 每次执行可见的 request-local context #[derive(Debug, Clone)] pub struct RequestAttemptContext { response_control: Option, @@ -401,7 +401,7 @@ impl RequestAttemptContext { self } - /// 传递本次请求已经消耗的排队预算,不能在新 attempt 中重置。 + /// 传递本次请求已经消耗的排队预算,不能在新 attempt 中重置 #[must_use] pub fn with_concurrency_wait_budget( mut self, @@ -411,28 +411,28 @@ impl RequestAttemptContext { self } - /// 传递请求内共享的连接恢复预算。 + /// 传递请求内共享的连接恢复预算 #[must_use] pub fn with_connection_budget(mut self, budget: connection::ConnectionBudget) -> Self { self.connection_budget = budget; self } - /// 覆盖本次请求的单调计时原点。 + /// 覆盖本次请求的单调计时原点 #[must_use] pub fn with_timing_started_at(mut self, timing_started_at: Instant) -> Self { self.timing_started_at = timing_started_at; self } - /// 附着与发布视图同代次的请求策略;诊断与无插件路径保持 `None`。 + /// 附着与发布视图同代次的请求策略;诊断与无插件路径保持 `None` #[must_use] pub fn with_request_policy(mut self, policy: Option) -> Self { self.request_policy = policy; self } - /// 附着本次逻辑请求共享的外部副作用水位;不会传给 Provider 或插件 wire。 + /// 附着本次逻辑请求共享的外部副作用水位;不会传给 Provider 或插件 wire #[must_use] pub fn with_execution_effects( mut self, @@ -442,7 +442,7 @@ impl RequestAttemptContext { self } - /// 附着与路由快照同代次的 attempt 中间件及可信绑定事实。 + /// 附着与路由快照同代次的 attempt 中间件及可信绑定事实 #[must_use] pub fn with_middleware( mut self, @@ -474,7 +474,7 @@ impl RequestAttemptContext { &self.client_api_key_ref } - /// 返回贯穿本次请求的单调计时原点。 + /// 返回贯穿本次请求的单调计时原点 #[must_use] pub const fn timing_started_at(&self) -> Instant { self.timing_started_at @@ -511,7 +511,7 @@ impl RequestAttemptContext { } } -/// Provider 每次执行可见的 request-local context。 +/// Provider 每次执行可见的 request-local context #[derive(Debug, Clone)] pub struct AttemptContext { request: RequestAttemptContext, @@ -536,7 +536,7 @@ impl AttemptContext { self.request.requested_model.as_ref() } - /// 从本次请求冻结的发布代次选择上游适配器;不建立连接、不重新选号。 + /// 从本次请求冻结的发布代次选择上游适配器;不建立连接、不重新选号 pub fn upstream_adapter( &self, provider: &ProviderKind, @@ -568,7 +568,7 @@ impl AttemptContext { &self.request.pricing } - /// 本次逻辑请求首次解析的 Provider 身份,换号及传输重试保持不变。 + /// 本次逻辑请求首次解析的 Provider 身份,换号及传输重试保持不变 #[must_use] pub const fn request_profile(&self) -> Option<&crate::account::OpaqueProviderData> { self.request.request_profile.as_ref() @@ -584,7 +584,7 @@ impl AttemptContext { self.request.request_location.as_ref() } - /// 当前 attempt 的诊断关联;克隆后可传给后台 transport 任务。 + /// 当前 attempt 的诊断关联;克隆后可传给后台 transport 任务 #[must_use] pub fn trace(&self) -> crate::diagnostics::TraceContext { self.request.trace.attempt(self.attempt_index.get()) @@ -618,7 +618,7 @@ impl AttemptContext { } } - /// 覆盖本次 attempt 的 continuation 恢复方式。 + /// 覆盖本次 attempt 的 continuation 恢复方式 #[must_use] pub const fn with_continuation_attempt( mut self, @@ -628,7 +628,7 @@ impl AttemptContext { self } - /// 覆盖本次 attempt 的 Provider 传输档位。 + /// 覆盖本次 attempt 的 Provider 传输档位 #[must_use] pub const fn with_transport(mut self, transport: AttemptTransport) -> Self { self.transport = transport; @@ -640,19 +640,19 @@ impl AttemptContext { self.request.request_id() } - /// 返回隔离 Provider 会话与缓存身份的下游租户引用。 + /// 返回隔离 Provider 会话与缓存身份的下游租户引用 #[must_use] pub const fn client_api_key_ref(&self) -> &ClientApiKeyId { self.request.client_api_key_ref() } - /// 返回本次请求统一的单调计时原点。 + /// 返回本次请求统一的单调计时原点 #[must_use] pub const fn timing_started_at(&self) -> Instant { self.request.timing_started_at() } - /// 返回同一次请求冻结的插件策略上下文。 + /// 返回同一次请求冻结的插件策略上下文 #[must_use] pub const fn request_policy_context(&self) -> Option<&policy::RequestPolicyContext> { self.request.request_policy() @@ -663,10 +663,10 @@ impl AttemptContext { self.request.extension_scope() } - /// 在已选账号并持有其 lease 后,以 owned terminal 执行本次 retry 的中间件链。 + /// 在已选账号并持有其 lease 后,以 owned terminal 执行本次 retry 的中间件链 /// /// terminal 返回的 stream 仍须保持 cold;中间件不能取得 credential、发送状态、 - /// canonical usage/cost 或结算所有权。 + /// canonical usage/cost 或结算所有权 pub async fn execute_middleware( &self, operation: crate::operation::Operation, @@ -716,7 +716,7 @@ impl AttemptContext { self.deadline } - /// 返回密钥准入、账号选择与重试共用的排队预算。 + /// 返回密钥准入、账号选择与重试共用的排队预算 #[must_use] pub const fn concurrency_wait_budget(&self) -> &crate::concurrency::ConcurrencyWaitBudget { &self.request.concurrency_wait_budget @@ -732,7 +732,7 @@ impl AttemptContext { self.account.excluded_accounts() } - /// 管理端诊断或请求局部恢复 attempt 强制使用的唯一账号。 + /// 管理端诊断或请求局部恢复 attempt 强制使用的唯一账号 #[must_use] pub const fn required_account(&self) -> Option<&ProviderAccountId> { self.account.required_account() @@ -743,19 +743,19 @@ impl AttemptContext { self.account.state_owner() } - /// 同一请求是否已经为当前固定账号执行过一次 OAuth 恢复。 + /// 同一请求是否已经为当前固定账号执行过一次 OAuth 恢复 #[must_use] pub const fn credential_recovery_attempted(&self) -> bool { self.account.credential_recovery_attempted() } - /// 管理端固定账号诊断会跳过本地可用性投影,保留全部租约约束。 + /// 管理端固定账号诊断会跳过本地可用性投影,保留全部租约约束 #[must_use] pub const fn is_diagnostic_required_account(&self) -> bool { self.account.is_diagnostic_required_account() } - /// 普通请求认证时冻结的账号范围;管理端诊断为 `None`。 + /// 普通请求认证时冻结的账号范围;管理端诊断为 `None` #[must_use] pub const fn account_scope(&self) -> Option<&Arc> { self.account.account_scope() @@ -776,7 +776,7 @@ impl AttemptContext { &self.request.connection_budget } - /// 返回本次 attempt 的 Provider 传输档位。 + /// 返回本次 attempt 的 Provider 传输档位 #[must_use] pub const fn transport(&self) -> AttemptTransport { self.transport @@ -787,7 +787,7 @@ impl AttemptContext { &self.cancellation } - /// 使用冻结策略选择账号;没有匹配策略时委托现有 `AccountSelector`。 + /// 使用冻结策略选择账号;没有匹配策略时委托现有 `AccountSelector` pub async fn select_account<'a>( &self, provider: &ProviderKind, @@ -806,7 +806,7 @@ impl AttemptContext { } } -/// 创建唯一 `model_requests` 行所需的入口事实。 +/// 创建唯一 `model_requests` 行所需的入口事实 #[derive(Debug, Clone)] pub struct NewModelRequest { pub id: ModelRequestId, @@ -828,14 +828,14 @@ pub struct NewModelRequest { pub compact: bool, pub continuation: provider::ContinuationRequestObservation, pub image_generation_requested: bool, - /// Client Key 准入判定的完整耗时;内部探测不经过该阶段。 + /// Client Key 准入判定的完整耗时;内部探测不经过该阶段 pub admission_decision_ms: Option, pub started_at: SystemTime, - /// 可选执行截止;Store 将其投影为可续期的异常回收期限。 + /// 可选执行截止;Store 将其投影为可续期的异常回收期限 pub deadline_at: Deadline, } -/// 每次真实上游发送前对同一 `model_requests` 行的更新。 +/// 每次真实上游发送前对同一 `model_requests` 行的更新 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AttemptRecord { pub request_id: ModelRequestId, @@ -847,14 +847,14 @@ pub struct AttemptRecord { pub upstream_model_id: Option, pub upstream_transport: String, pub http_version: Option, - /// 本 attempt 从进入 Provider 选择器到持有账号 lease 的等待。 + /// 本 attempt 从进入 Provider 选择器到持有账号 lease 的等待 pub account_selection_wait_ms: Option, - /// 选择成功后(含当前请求)的请求级账号池容量快照。 + /// 选择成功后(含当前请求)的请求级账号池容量快照 pub capacity_used_slots: Option, pub capacity_total_slots: Option, } -/// 需要解释换号的中间失败。 +/// 需要解释换号的中间失败 #[derive(Debug)] pub struct IntermediateFailure { pub request_id: ModelRequestId, @@ -869,7 +869,7 @@ pub struct IntermediateFailure { pub latency: Duration, } -/// 已认证有效请求在执行会话建立前的拒绝;不伪造模型执行或上游 attempt。 +/// 已认证有效请求在执行会话建立前的拒绝;不伪造模型执行或上游 attempt #[derive(Debug)] pub struct EntryRejection { pub request_id: ModelRequestId, @@ -878,7 +878,7 @@ pub struct EntryRejection { pub latency: Duration, } -/// 不属于任何 `model_requests` 行的管理端账号探测失败。 +/// 不属于任何 `model_requests` 行的管理端账号探测失败 #[derive(Debug)] pub struct ProbeFailure { pub provider_kind: ProviderKind, @@ -888,7 +888,7 @@ pub struct ProbeFailure { pub latency: Duration, } -/// `model_requests` 可用的毫秒级阶段耗时。 +/// `model_requests` 可用的毫秒级阶段耗时 #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct ModelRequestTimings { pub transport_decision_wait_ms: Option, @@ -902,14 +902,14 @@ pub struct ModelRequestTimings { pub latency_ms: Option, } -/// 终态失败的 Provider 结构化连接与 continuation 观测。 +/// 终态失败的 Provider 结构化连接与 continuation 观测 #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct ModelRequestFailureObservation { pub continuation_unavailable_reason: Option, pub upstream_connection: Option, } -/// 单行模型请求的终态写回。 +/// 单行模型请求的终态写回 #[derive(Debug)] pub struct ModelRequestFinalization { pub request_id: ModelRequestId, @@ -927,13 +927,13 @@ pub struct ModelRequestFinalization { pub websocket_pool: Option, pub service_tier: Option, pub upstream_response_model: Option, - /// Provider 已筛选的专有观测 JSON;Core 不解释字段。 + /// Provider 已筛选的专有观测 JSON;Core 不解释字段 pub provider_metadata_json: Option, - /// 请求全程的有界诊断快照,跨 Provider 与重试保留。 + /// 请求全程的有界诊断快照,跨 Provider 与重试保留 pub diagnostic_trace_json: Option, pub error: Option, pub provider_error_code: Option, - /// Provider 返回的原始错误正文或 WebSocket close/error frame。 + /// Provider 返回的原始错误正文或 WebSocket close/error frame pub raw_upstream_error: Option, pub failure_observation: ModelRequestFailureObservation, pub retry_after_ms: Option, @@ -944,16 +944,16 @@ pub struct ModelRequestFinalization { pub completed_at: SystemTime, } -/// 按过期恢复租约收敛的 running 请求数。 +/// 按过期恢复租约收敛的 running 请求数 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct RecoveryReport { pub requests: u64, } -/// `model_requests` 与必要 `ops_events` 的唯一 Core port。 +/// `model_requests` 与必要 `ops_events` 的唯一 Core port #[async_trait] pub trait ExecutionStore: Send + Sync { - /// 会话持有期间刷新异常回收期限,结束或 Drop 后停止;观测失败不取消执行。 + /// 会话持有期间刷新异常回收期限,结束或 Drop 后停止;观测失败不取消执行 fn maintain_request( &self, _request_id: &ModelRequestId, @@ -965,10 +965,10 @@ pub trait ExecutionStore: Send + Sync { async fn create_model_request(&self, request: NewModelRequest) -> Result<(), StoreError>; async fn record_attempt(&self, attempt: AttemptRecord) -> Result<(), StoreError>; /// 请求插入与首次 attempt 合并持久化;两者写同一行,支持合并写的 - /// store 可覆写为单次往返,缩短首 token 前的关键路径。 + /// store 可覆写为单次往返,缩短首 token 前的关键路径 /// /// 覆写实现应保证原子性:默认的两步实现里第二步失败会留下一条 - /// `running` 请求行,由 deadline 回收器收敛。 + /// `running` 请求行,由 deadline 回收器收敛 async fn create_model_request_with_attempt( &self, request: NewModelRequest, @@ -997,12 +997,12 @@ pub trait ExecutionStore: Send + Sync { &self, failure: IntermediateFailure, ) -> Result<(), StoreError>; - /// 记录路由或准入拒绝;可恢复观测失败不改变客户端结果。 + /// 记录路由或准入拒绝;可恢复观测失败不改变客户端结果 async fn record_entry_rejection(&self, _rejection: EntryRejection) -> Result<(), StoreError> { Ok(()) } - /// 记录不挂在 `model_requests` 上的账号探测失败;默认丢弃。 + /// 记录不挂在 `model_requests` 上的账号探测失败;默认丢弃 async fn record_probe_failure(&self, _failure: ProbeFailure) -> Result<(), StoreError> { Ok(()) } @@ -1014,7 +1014,7 @@ pub trait ExecutionStore: Send + Sync { async fn recover_expired(&self, now: SystemTime) -> Result; } -/// 首事件能否交付客户端的持久化屏障。 +/// 首事件能否交付客户端的持久化屏障 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CommitRequirement { CommitBeforeDelivery, @@ -1036,7 +1036,7 @@ impl CoordinatedEvent { } } - /// 将一批 provider 事件合并为单个提交单元;空批次是无效投递状态。 + /// 将一批 provider 事件合并为单个提交单元;空批次是无效投递状态 pub fn try_batch( events: Vec, commit_requirement: CommitRequirement, @@ -1060,7 +1060,7 @@ impl CoordinatedEvent { self.events } - /// 返回本批次中 Provider 交付的私有会话更新。 + /// 返回本批次中 Provider 交付的私有会话更新 #[must_use] pub fn session_update(&self) -> Option<&ProviderSessionState> { self.events.iter().find_map(ProviderEvent::session_update) @@ -1097,7 +1097,7 @@ pub enum EngineError { InvalidDeliveryState, } -/// Engine 只组合 Store 与 Provider Registry;重试算法完全位于 coordinator。 +/// Engine 只组合 Store 与 Provider Registry;重试算法完全位于 coordinator pub struct GatewayEngine { store: Arc, providers: provider::ProviderRegistry, diff --git a/backend/crates/gateway-core/src/engine/nested.rs b/backend/crates/gateway-core/src/engine/nested.rs index 9c3c5e774..3ef1f35fb 100644 --- a/backend/crates/gateway-core/src/engine/nested.rs +++ b/backend/crates/gateway-core/src/engine/nested.rs @@ -1,8 +1,8 @@ -//! 插件宿主模型调用与亲和查询的中立 Core 端口。 +//! 插件宿主模型调用与亲和查询的中立 Core 端口 //! //! Runtime 只传递宿主签发的父请求关联和业务目标;调用方身份、 //! 快照、准入、续写资格与计费始终由 [`super::execution::DefaultExecutionService`] -//! 持有和复核。 +//! 持有和复核 use std::{ fmt, @@ -22,11 +22,12 @@ use super::{ execution::{BoundModelExecutionContext, ExecutionRequestMetadata, StartedExecution}, }; -/// 同一请求图内不可逆外部副作用的单调水位。 +/// 同一请求图内不可逆外部副作用的单调水位 /// /// 受信 Runtime 只在受管子执行或外部 HTTP 已发送、或无法证明未发送时调用 -/// [`Self::observe`]。它不承载费用或插件输入;Coordinator 用每次执行自己的基线 -/// 阻止把随后 Provider 的 `not_sent` 错当作可重放。 +/// [`Self::observe`] +/// 它不承载费用或插件输入;Coordinator 用每次执行自己的基线 +/// 阻止把随后 Provider 的 `not_sent` 错当作可重放 #[derive(Debug, Default)] pub struct ExecutionEffects(AtomicUsize); @@ -40,10 +41,10 @@ impl ExecutionEffects { } } -/// 插件发起的一个子模型请求。 +/// 插件发起的一个子模型请求 /// /// `parent_request_id` 与 `initiating_plugin_instance_id` 都来自宿主保存的 -/// `CallContext`,不能接受插件自行构造的关联事实。 +/// `CallContext`,不能接受插件自行构造的关联事实 pub struct NestedModelExecutionRequest { pub parent_request_id: ModelRequestId, pub initiating_plugin_instance_id: String, @@ -52,22 +53,24 @@ pub struct NestedModelExecutionRequest { pub metadata: ExecutionRequestMetadata, pub provider: Option, pub account: Option, - /// 发起回调时父 attempt 已持有的账号。该事实由宿主从 `CallContext` 注入, - /// 用于阻止子请求在父请求等待其返回时再次等待同一账号 lease。 + /// 发起回调时父 attempt 已持有的账号 + /// 该事实由宿主从 `CallContext` 注入, + /// 用于阻止子请求在父请求等待其返回时再次等待同一账号 lease pub parent_account: Option, } -/// 插件为当前调用或连接显式绑定的模型执行身份。 +/// 插件为当前调用或连接显式绑定的模型执行身份 /// -/// Key ID 由插件选择,发起实例由宿主确定。Core 在绑定时冻结当前 -/// Key 策略;每个实际模型请求仍独立经过准入、预算、账本与计费。 +/// Key ID 由插件选择,发起实例由宿主确定 +/// Core 在绑定时冻结当前 +/// Key 策略;每个实际模型请求仍独立经过准入、预算、账本与计费 pub struct BoundModelExecutionBinding { pub settings: Option, pub client_key_id: ClientApiKeyId, pub initiating_plugin_instance_id: String, - /// 身份随父调用或连接回收;每个模型请求使用独立的执行期限。 + /// 身份随父调用或连接回收;每个模型请求使用独立的执行期限 pub cancellation: CancellationToken, - /// 宿主保留的扩展调用链,异步观察不能通过新建模型请求清空防递归事实。 + /// 宿主保留的扩展调用链,异步观察不能通过新建模型请求清空防递归事实 pub extension_scope: super::extensions::ExtensionCallScope, } @@ -84,7 +87,7 @@ impl fmt::Debug for BoundModelExecutionBinding { } } -/// 显式身份作用域内的一次模型请求。 +/// 显式身份作用域内的一次模型请求 pub struct BoundModelExecutionRequest { pub context: BoundModelExecutionContext, pub public_model: PublicModelId, @@ -125,7 +128,7 @@ impl fmt::Debug for NestedModelExecutionRequest { } } -/// 嵌套模型执行只进入 Core 的既有执行会话;Runtime 不实现重试、commit 或计费。 +/// 嵌套模型执行只进入 Core 的既有执行会话;Runtime 不实现重试、commit 或计费 pub trait NestedModelExecutionPort: Send + Sync { fn models( &self, @@ -134,7 +137,7 @@ pub trait NestedModelExecutionPort: Send + Sync { client_version: String, ) -> BoxFuture<'_, Result, GatewayError>>; - /// 为插件调用冻结显式执行身份;取消父调用会取消其所有模型执行。 + /// 为插件调用冻结显式执行身份;取消父调用会取消其所有模型执行 fn bind( &self, binding: BoundModelExecutionBinding, @@ -151,7 +154,7 @@ pub trait NestedModelExecutionPort: Send + Sync { ) -> BoxFuture<'_, Result>; } -/// 带父请求身份与授权范围的会话亲和查询。 +/// 带父请求身份与授权范围的会话亲和查询 pub struct AffinityLookupRequest { pub parent_request_id: ModelRequestId, pub provider: ProviderKind, @@ -169,7 +172,7 @@ impl fmt::Debug for AffinityLookupRequest { } } -/// 只公开后续请求可作为偏好提交、但仍须 Core 再次复核的亲和提示。 +/// 只公开后续请求可作为偏好提交、但仍须 Core 再次复核的亲和提示 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AffinityLookupResult { provider: ProviderKind, @@ -193,7 +196,7 @@ impl AffinityLookupResult { } } -/// 会话亲和查询失败沿用公开网关错误分类,不暴露响应句柄或归属细节。 +/// 会话亲和查询失败沿用公开网关错误分类,不暴露响应句柄或归属细节 pub trait AffinityLookupPort: Send + Sync { fn lookup( &self, diff --git a/backend/crates/gateway-core/src/engine/observation.rs b/backend/crates/gateway-core/src/engine/observation.rs index ba9eac365..479c3e5b9 100644 --- a/backend/crates/gateway-core/src/engine/observation.rs +++ b/backend/crates/gateway-core/src/engine/observation.rs @@ -1,4 +1,4 @@ -//! 单次响应事实与请求终态扩展派发;重试丢弃时统一清理。 +//! 单次响应事实与请求终态扩展派发;重试丢弃时统一清理 use std::{ collections::BTreeMap, @@ -27,7 +27,7 @@ use crate::{ upstream::UpstreamSendState, }; -/// 一条 WebSocket 响应事件所属的实际上游 attempt 身份。 +/// 一条 WebSocket 响应事件所属的实际上游 attempt 身份 #[derive(Debug, Clone, PartialEq, Eq)] pub struct WebSocketResponseAttempt { provider: ProviderKind, @@ -36,7 +36,7 @@ pub struct WebSocketResponseAttempt { } impl WebSocketResponseAttempt { - /// 固定一条响应事件所属的实际上游 attempt。 + /// 固定一条响应事件所属的实际上游 attempt #[must_use] pub const fn new( provider: ProviderKind, @@ -66,7 +66,7 @@ impl WebSocketResponseAttempt { } } -/// 请求观察使用的冻结 Client Key 与账号组范围。 +/// 请求观察使用的冻结 Client Key 与账号组范围 #[derive(Debug, Clone, PartialEq, Eq)] pub struct RequestObservationScope { client_key_id: ClientApiKeyId, @@ -95,10 +95,10 @@ impl RequestObservationScope { } } -/// 实际上游 WebSocket 响应事件的只读观察。 +/// 实际上游 WebSocket 响应事件的只读观察 /// /// Key 与账号组只用于 Runtime 匹配冻结绑定,不得进入插件 wire;`wire` 保留策略加工前 -/// 的 Provider 原始事实,由 Runtime 再按正文读取授权投影。 +/// 的 Provider 原始事实,由 Runtime 再按正文读取授权投影 #[derive(Clone, PartialEq)] pub struct WebSocketResponseObservation { event_id: String, @@ -114,7 +114,7 @@ pub struct WebSocketResponseObservation { } impl WebSocketResponseObservation { - /// 创建一个已附加冻结 Key/组范围、尚未附加公开模型范围的实际上游事件。 + /// 创建一个已附加冻结 Key/组范围、尚未附加公开模型范围的实际上游事件 #[must_use] pub fn new( request_id: ModelRequestId, @@ -226,7 +226,7 @@ impl WebSocketResponseObservation { } } -/// 插件可观察的请求终态;拒绝表示请求没有进入 Provider 执行。 +/// 插件可观察的请求终态;拒绝表示请求没有进入 Provider 执行 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RequestObservationOutcome { Succeeded, @@ -236,7 +236,7 @@ pub enum RequestObservationOutcome { Incomplete, } -/// Core 在业务结果确定后产生的一次最终观察,不包含原始正文或凭据。 +/// Core 在业务结果确定后产生的一次最终观察,不包含原始正文或凭据 #[derive(Debug, Clone, PartialEq, Eq)] pub struct RequestObservation { event_id: String, @@ -265,7 +265,7 @@ pub struct RequestObservation { } impl RequestObservation { - /// 创建一个带冻结 Key/组范围、不带 Provider、模型、用量或错误详情的最终事实。 + /// 创建一个带冻结 Key/组范围、不带 Provider、模型、用量或错误详情的最终事实 #[must_use] pub fn new( request_id: ModelRequestId, @@ -495,9 +495,9 @@ impl RequestObservation { } } -/// 一个发布代次的不可变请求观察计划;实现必须自行保证派发有界且不阻塞业务结果。 +/// 一个发布代次的不可变请求观察计划;实现必须自行保证派发有界且不阻塞业务结果 pub trait RequestObserverPlan: Send + Sync { - /// 旁路观察一条实际上游 WebSocket 事件;未启用观察时,默认实现不额外处理事件。 + /// 旁路观察一条实际上游 WebSocket 事件;未启用观察时,默认实现不额外处理事件 fn dispatch_websocket_response( &self, _generation: ExtensionSetReference, @@ -505,23 +505,23 @@ pub trait RequestObserverPlan: Send + Sync { ) { } - /// `generation` 必须由后台工作持有到本次派发结束,避免旧代次提前排空。 + /// `generation` 必须由后台工作持有到本次派发结束,避免旧代次提前排空 fn dispatch(&self, generation: ExtensionSetReference, observation: RequestObservation); } -/// 观察计划注册冲突;同一个集合 ID 不能被静默替换。 +/// 观察计划注册冲突;同一个集合 ID 不能被静默替换 #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] #[error("request observer generation is already registered")] pub struct RequestObserverRegistrationError; -/// 按发布集合解析计划的非拥有索引;旧代次由发布视图和在途派发保活。 +/// 按发布集合解析计划的非拥有索引;旧代次由发布视图和在途派发保活 #[derive(Clone, Default)] pub struct RequestObserverExtensionIndex { sets: Arc>>>, } impl RequestObserverExtensionIndex { - /// 注册候选代次;返回值必须由候选集合强持有。 + /// 注册候选代次;返回值必须由候选集合强持有 pub fn register( &self, id: ExtensionSetId, @@ -539,7 +539,7 @@ impl RequestObserverExtensionIndex { Ok(plan) } - /// 只解析请求已经冻结的集合;缺少计划表示该代次没有观察绑定。 + /// 只解析请求已经冻结的集合;缺少计划表示该代次没有观察绑定 #[must_use] pub fn resolve( &self, @@ -553,7 +553,7 @@ impl RequestObserverExtensionIndex { } } -/// 同一请求的所有终结分支共享这个一次性派发状态。 +/// 同一请求的所有终结分支共享这个一次性派发状态 #[derive(Clone)] pub(super) struct RequestObservationDispatch { state: Arc, @@ -849,7 +849,7 @@ fn observe_event_timing(timings: &mut ModelRequestTimings, event: &GatewayEvent, timings.first_token_ms.get_or_insert(elapsed_ms); } // `response.output_item.added` 会先投影一个空参数的 tool delta;它只是结构帧, - // 不能抢在真实工具参数之前成为首个可消费 token。 + // 不能抢在真实工具参数之前成为首个可消费 token GatewayEvent::ToolCallDelta(delta) if !delta.arguments_delta.is_empty() => { timings.first_token_ms.get_or_insert(elapsed_ms); } diff --git a/backend/crates/gateway-core/src/engine/policy.rs b/backend/crates/gateway-core/src/engine/policy.rs index f7385e781..73d9935bd 100644 --- a/backend/crates/gateway-core/src/engine/policy.rs +++ b/backend/crates/gateway-core/src/engine/policy.rs @@ -1,4 +1,4 @@ -//! 请求级插件路由与账号调度计划;Core 保留目标、资格和租约复核。 +//! 请求级插件路由与账号调度计划;Core 保留目标、资格和租约复核 use std::{ collections::{BTreeMap, BTreeSet}, @@ -23,7 +23,7 @@ use crate::{ runtime::extensions::{ExtensionSetId, ExtensionSetReference}, }; -/// 模型路由插件的一次输入;正文仍保存在 `operation` 中,由 Runtime 按权限投影。 +/// 模型路由插件的一次输入;正文仍保存在 `operation` 中,由 Runtime 按权限投影 #[derive(Clone)] pub struct ModelRouteInput { request_id: super::ModelRequestId, @@ -77,7 +77,7 @@ impl ModelRouteInput { &self.available_providers } - /// 只供受信 Runtime 记录本次策略调用已经产生的外部副作用,不进入插件 wire。 + /// 只供受信 Runtime 记录本次策略调用已经产生的外部副作用,不进入插件 wire #[must_use] pub fn execution_effects(&self) -> Arc { Arc::clone(&self.execution_effects) @@ -98,7 +98,7 @@ impl fmt::Debug for ModelRouteInput { } } -/// 模型路由结果;Runtime 区分未处理、明确拒绝和调用故障。 +/// 模型路由结果;Runtime 区分未处理、明确拒绝和调用故障 #[derive(Debug, Clone, PartialEq, Eq)] pub enum ModelRouteDecision { Unhandled, @@ -109,7 +109,7 @@ pub enum ModelRouteDecision { Reject, } -/// 账号调度插件可见的单个候选事实,不包含凭据或账号资料。 +/// 账号调度插件可见的单个候选事实,不包含凭据或账号资料 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AccountScheduleCandidate { account_id: ProviderAccountId, @@ -170,7 +170,7 @@ impl AccountScheduleCandidate { } } -/// 账号调度输入;Key 与账号组仅用于 Runtime 匹配绑定,不发送给插件。 +/// 账号调度输入;Key 与账号组仅用于 Runtime 匹配绑定,不发送给插件 #[derive(Debug, Clone)] pub struct AccountScheduleInput { request_id: super::ModelRequestId, @@ -230,14 +230,14 @@ impl AccountScheduleInput { &self.candidates } - /// 只供受信 Runtime 记录本次策略调用已经产生的外部副作用,不进入插件 wire。 + /// 只供受信 Runtime 记录本次策略调用已经产生的外部副作用,不进入插件 wire #[must_use] pub fn execution_effects(&self) -> Arc { Arc::clone(&self.execution_effects) } } -/// 账号调度结果;插件选中的账号仍须通过 Core 资格复核并取得 Provider 租约。 +/// 账号调度结果;插件选中的账号仍须通过 Core 资格复核并取得 Provider 租约 #[derive(Debug, Clone, PartialEq, Eq)] pub enum AccountScheduleDecision { Pick(ProviderAccountId), @@ -245,7 +245,7 @@ pub enum AccountScheduleDecision { Reject, } -/// 重试策略只能收窄宿主决定;不能自行创造恢复路径。 +/// 重试策略只能收窄宿主决定;不能自行创造恢复路径 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RetryDecision { Delegate, @@ -253,7 +253,7 @@ pub enum RetryDecision { Retry, } -/// 协调器计算的安全失败事实;无凭据、原始错误或请求正文。 +/// 协调器计算的安全失败事实;无凭据、原始错误或请求正文 #[derive(Debug, Clone)] pub struct RetryFacts { pub attempt_index: NonZeroU32, @@ -276,12 +276,12 @@ pub struct RetryInput { pub facts: RetryFacts, } -/// 策略调用失败不携带插件原始消息,避免跨层泄漏不可信诊断。 +/// 策略调用失败不携带插件原始消息,避免跨层泄漏不可信诊断 #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] #[error("request policy call failed")] pub struct RequestPolicyFault; -/// Provider 选择器需要区分明确拒绝、策略故障和等待期间过期的选择。 +/// Provider 选择器需要区分明确拒绝、策略故障和等待期间过期的选择 #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] pub enum AccountPolicyError { #[error("account scheduling policy rejected the request")] @@ -292,7 +292,7 @@ pub enum AccountPolicyError { StaleCandidate, } -/// 一个发布代次的不可变请求策略计划。 +/// 一个发布代次的不可变请求策略计划 pub trait RequestPolicyPlan: Send + Sync + fmt::Debug { fn retry_decision( &self, @@ -312,7 +312,7 @@ pub trait RequestPolicyPlan: Send + Sync + fmt::Debug { ) -> BoxFuture<'static, Result>; } -/// 同一次请求冻结的策略计划和授权身份;完整集合引用保活所有 RPC 会话。 +/// 同一次请求冻结的策略计划和授权身份;完整集合引用保活所有 RPC 会话 #[derive(Clone)] pub struct RequestPolicyContext { plan: Arc, @@ -376,7 +376,7 @@ impl RequestPolicyContext { } } - /// 附着父子调用图;计划必须跳过集合中的实例,防止直接或间接重入。 + /// 附着父子调用图;计划必须跳过集合中的实例,防止直接或间接重入 #[must_use] pub fn with_extension_scope(mut self, extension_scope: ExtensionCallScope) -> Self { self.extension_scope = extension_scope; @@ -414,7 +414,7 @@ impl RequestPolicyContext { .await } - /// 硬绑定已在候选资格中收窄;插件先决定,委托时再采用内置亲和与排序。 + /// 硬绑定已在候选资格中收窄;插件先决定,委托时再采用内置亲和与排序 pub async fn select_account<'a>( &self, attempt_index: NonZeroU32, @@ -457,7 +457,7 @@ impl RequestPolicyContext { .await .map_err(|_| AccountPolicyError::Fault)?; // 插件调用可能消耗显著时间;无论插件选择还是委托,均用返回时刻重新执行 - // 同一资格判断,不能让调用前尚未过期的候选在调用后被内置策略选中。 + // 同一资格判断,不能让调用前尚未过期的候选在调用后被内置策略选中 let mut current = context.clone(); current.now = SystemTime::now(); match decision { @@ -482,12 +482,12 @@ impl RequestPolicyContext { } } -/// 策略计划注册冲突;同一代次不能被静默替换。 +/// 策略计划注册冲突;同一代次不能被静默替换 #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] #[error("request policy generation is already registered")] pub struct RequestPolicyRegistrationError; -/// 按发布集合解析策略计划的非拥有索引。 +/// 按发布集合解析策略计划的非拥有索引 #[derive(Clone, Default)] pub struct RequestPolicyExtensionIndex { sets: Arc>>>, diff --git a/backend/crates/gateway-core/src/engine/probe.rs b/backend/crates/gateway-core/src/engine/probe.rs index a3e0d0734..c492483b9 100644 --- a/backend/crates/gateway-core/src/engine/probe.rs +++ b/backend/crates/gateway-core/src/engine/probe.rs @@ -1,4 +1,4 @@ -//! 管理端账号连通性测试的真实执行链端口。 +//! 管理端账号连通性测试的真实执行链端口 use std::fmt; @@ -23,9 +23,9 @@ pub struct AccountProbeResult { pub text: Vec, } -/// 仅供当前管理端连接测试展示的原始上游失败响应。 +/// 仅供当前管理端连接测试展示的原始上游失败响应 /// -/// 正文不进入 `Debug`、日志或持久化;Core 只在探测终态从原始 Provider 错误显式复制。 +/// 正文不进入 `Debug`、日志或持久化;Core 只在探测终态从原始 Provider 错误显式复制 #[derive(PartialEq, Eq)] pub struct AccountProbeUpstreamResponse { status: u16, @@ -72,7 +72,7 @@ impl fmt::Debug for AccountProbeUpstreamResponse { } } -/// 连接测试失败发生的稳定责任边界。 +/// 连接测试失败发生的稳定责任边界 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AccountProbeErrorSource { Gateway, @@ -81,7 +81,7 @@ pub enum AccountProbeErrorSource { } impl AccountProbeErrorSource { - /// 返回管理端 wire 使用的稳定值。 + /// 返回管理端 wire 使用的稳定值 #[must_use] pub const fn as_str(self) -> &'static str { match self { @@ -92,9 +92,9 @@ impl AccountProbeErrorSource { } } -/// 管理端连接测试的终态错误。 +/// 管理端连接测试的终态错误 /// -/// `gateway` 保留稳定分类,`upstream_response` 只面向本次认证管理请求展示源响应。 +/// `gateway` 保留稳定分类,`upstream_response` 只面向本次认证管理请求展示源响应 #[derive(Debug)] pub struct AccountProbeError { gateway: GatewayError, @@ -162,7 +162,7 @@ impl From for AccountProbeError { } pub trait AccountProbe: Send + Sync { - /// 插件管理操作传入其冻结快照;其他调用可使用当前发布视图。 + /// 插件管理操作传入其冻结快照;其他调用可使用当前发布视图 fn probe( &self, request: AccountProbeRequest, diff --git a/backend/crates/gateway-core/src/engine/provider.rs b/backend/crates/gateway-core/src/engine/provider.rs index 01bef97ec..e4602b869 100644 --- a/backend/crates/gateway-core/src/engine/provider.rs +++ b/backend/crates/gateway-core/src/engine/provider.rs @@ -1,4 +1,4 @@ -//! 原生 Provider 的执行边界。 +//! 原生 Provider 的执行边界 use std::collections::{BTreeMap, VecDeque}; use std::fmt; @@ -35,11 +35,11 @@ use crate::routing::{ use crate::upstream::OpaqueUpstreamValue; use crate::upstream::{UpstreamSendState, UpstreamTransport}; -/// Box 只出现在 Provider Registry 的统一 event envelope 边界。 +/// Box 只出现在 Provider Registry 的统一 event envelope 边界 pub type EventStream = Pin> + Send + 'static>>; -/// Provider 选定单个 credential 后返回的事实。 +/// Provider 选定单个 credential 后返回的事实 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderCallMetadata { provider: ProviderKind, @@ -50,7 +50,7 @@ pub struct ProviderCallMetadata { selection_observation: Option, } -/// Provider 账号选择阶段输出的中立运行压力事实。 +/// Provider 账号选择阶段输出的中立运行压力事实 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ProviderSelectionObservation { account_selection_wait_ms: u64, @@ -81,7 +81,7 @@ impl ProviderSelectionObservation { } impl ProviderCallMetadata { - /// 创建一次调用的不可变事实。 + /// 创建一次调用的不可变事实 #[must_use] pub const fn new( provider: ProviderKind, @@ -99,7 +99,7 @@ impl ProviderCallMetadata { } } - /// 创建一次不声明模型的 Provider 原生端点调用事实。 + /// 创建一次不声明模型的 Provider 原生端点调用事实 #[must_use] pub const fn for_provider_endpoint( provider: ProviderKind, @@ -116,14 +116,14 @@ impl ProviderCallMetadata { } } - /// 设置 adapter 已分类为非 bearer 的 request ID。 + /// 设置 adapter 已分类为非 bearer 的 request ID #[must_use] pub fn with_upstream_request_id(mut self, request_id: OpaqueUpstreamValue) -> Self { self.upstream_request_id = Some(request_id); self } - /// 附加 Provider 选择账号时测得的等待与容量快照。 + /// 附加 Provider 选择账号时测得的等待与容量快照 #[must_use] pub const fn with_selection_observation( mut self, @@ -133,31 +133,31 @@ impl ProviderCallMetadata { self } - /// 返回 Provider。 + /// 返回 Provider #[must_use] pub const fn provider(&self) -> &ProviderKind { &self.provider } - /// 返回实际模型;Provider 原生端点没有模型事实。 + /// 返回实际模型;Provider 原生端点没有模型事实 #[must_use] pub const fn upstream_model(&self) -> Option<&UpstreamModelId> { self.upstream_model.as_ref() } - /// 返回 live Provider account ID。 + /// 返回 live Provider account ID #[must_use] pub const fn provider_account_id(&self) -> &ProviderAccountId { &self.provider_account_id } - /// 返回安全上游 request ID。 + /// 返回安全上游 request ID #[must_use] pub const fn upstream_request_id(&self) -> Option<&OpaqueUpstreamValue> { self.upstream_request_id.as_ref() } - /// 返回 transport。 + /// 返回 transport #[must_use] pub const fn transport(&self) -> &UpstreamTransport { &self.transport @@ -168,7 +168,7 @@ impl ProviderCallMetadata { self.selection_observation } - /// 确认 metadata 没有替换请求计划中冻结的 Provider 候选。 + /// 确认 metadata 没有替换请求计划中冻结的 Provider 候选 #[must_use] pub fn confirms(&self, candidate: &ProviderCandidate) -> bool { candidate.provider() == &self.provider @@ -176,30 +176,32 @@ impl ProviderCallMetadata { } } -/// Provider 自己持有的 credential/concurrency 租约。 +/// Provider 自己持有的 credential/concurrency 租约 /// -/// 句柄必须通过 `Drop` 释放资源。Core 不读取 credential,也不为 lease 定义 -/// 平台无关字段。 +/// 句柄必须通过 `Drop` 释放资源 +/// Core 不读取 credential,也不为 lease 定义 +/// 平台无关字段 pub trait ResourceLease: Send + Sync + 'static {} impl ResourceLease for T where T: Send + Sync + 'static {} -/// Provider 原生响应格式到统一响应格式的有状态转换边界。 +/// Provider 原生响应格式到统一响应格式的有状态转换边界 /// -/// Core 只负责调用顺序、格式与终态约束,不解释协议正文。实现必须由一次 -/// [`ProviderStream`] 独占,不能在并发请求间共享可变转换状态。 +/// Core 只负责调用顺序、格式与终态约束,不解释协议正文 +/// 实现必须由一次 +/// [`ProviderStream`] 独占,不能在并发请求间共享可变转换状态 pub trait NativeResponseTranslator: Send + 'static { - /// 转换前协议格式。 + /// 转换前协议格式 fn source_protocol(&self) -> &str; - /// 转换后协议格式。 + /// 转换后协议格式 fn target_protocol(&self) -> &str; - /// 把一个原生响应事件转换为零至多个统一响应事件。 + /// 把一个原生响应事件转换为零至多个统一响应事件 /// /// # Errors /// - /// 原生协议事件无效或转换状态不一致时返回已发送的 Provider 错误。 + /// 原生协议事件无效或转换状态不一致时返回已发送的 Provider 错误 fn translate( &mut self, event: &crate::event::ProtocolWireEvent, @@ -208,10 +210,11 @@ pub trait NativeResponseTranslator: Send + 'static { const MAX_NATIVE_RESPONSE_EVENTS_PER_INPUT: usize = 64; -/// Metadata、canonical event stream 与 owned lease 的统一返回值。 +/// Metadata、canonical event stream 与 owned lease 的统一返回值 /// /// 底层 stream 必须是 cold stream:在第一次 poll 前不得发送请求级 handshake -/// 或业务 payload。这样 Coordinator 可以先持久化 attempt,再越过发送屏障。 +/// 或业务 payload +/// 这样 Coordinator 可以先持久化 attempt,再越过发送屏障 pub struct ProviderStream { metadata: ProviderCallMetadata, events: EventStream, @@ -279,7 +282,7 @@ impl ProviderStreamAccountFeedback { fn report_failure(&mut self, error: &ProviderError) { if self.reported // `Ambiguous` 仍须关闭重放边界,但不能证明失败由账号造成;将它计入 - // 账号评分会把传输不确定性错误归因给账号。 + // 账号评分会把传输不确定性错误归因给账号 || error.send_state() != UpstreamSendState::Sent || matches!( error.kind(), @@ -313,7 +316,7 @@ impl ProviderStreamAccountFeedback { } impl ProviderStream { - /// 组装一次、且仅一次可见上游调用。 + /// 组装一次、且仅一次可见上游调用 #[must_use] pub fn new(metadata: ProviderCallMetadata, events: S, lease: impl ResourceLease) -> Self where @@ -331,14 +334,14 @@ impl ProviderStream { } } - /// 让公共 stream 边界统一回灌账号成功率与首个有效输出延迟。 + /// 让公共 stream 边界统一回灌账号成功率与首个有效输出延迟 #[must_use] pub fn with_account_feedback(mut self, stats: Arc) -> Self { self.set_account_feedback(stats, score_all_confirmed_failures); self } - /// 使用 Provider 定义的闭集判断回灌账号成功率与首个有效输出延迟。 + /// 使用 Provider 定义的闭集判断回灌账号成功率与首个有效输出延迟 #[must_use] pub fn with_filtered_account_feedback( mut self, @@ -349,7 +352,7 @@ impl ProviderStream { self } - /// 在原生响应加工的两个策略阶段之间安装 Provider 自有转换器。 + /// 在原生响应加工的两个策略阶段之间安装 Provider 自有转换器 #[must_use] pub fn with_native_response_translator( mut self, @@ -359,12 +362,12 @@ impl ProviderStream { self } - /// 在 Core 已记录原始事实且完成 `BeforeTranslation` 后执行原生转换。 + /// 在 Core 已记录原始事实且完成 `BeforeTranslation` 后执行原生转换 /// /// # Errors /// /// source/target 格式不一致、单事件展开越界、终态被丢弃或 Provider 转换失败时 - /// 返回错误。 + /// 返回错误 pub fn translate_native_response( &mut self, mut event: ProviderEvent, @@ -391,8 +394,9 @@ impl ProviderStream { } let Some(last) = translated.pop() else { // wire 转换器可以吞掉纯协议结构事件,但不能连带丢失 Core 已经 - // 识别出的 usage 等 canonical facts。facts-only 封套仍需交给 - // 客户端 adapter;没有 facts 时才是真正的零输出。 + // 识别出的 usage 等 canonical facts + // facts-only 封套仍需交给 + // 客户端 adapter;没有 facts 时才是真正的零输出 event.replace_wire(None); return Ok(event .has_canonical_facts() @@ -401,7 +405,7 @@ impl ProviderStream { .collect()); }; // 一对多时把 canonical/observation/session 信封放到最后一个 wire;这样 - // `Completed` 不会在同一源事件的前置输出交付前提前终结 Coordinator。 + // `Completed` 不会在同一源事件的前置输出交付前提前终结 Coordinator let mut events = translated .into_iter() .map(|wire| { @@ -433,7 +437,7 @@ impl ProviderStream { }); } - /// 返回调用事实。 + /// 返回调用事实 #[must_use] pub const fn metadata(&self) -> &ProviderCallMetadata { &self.metadata @@ -447,7 +451,7 @@ impl ProviderStream { } } -/// Attempt 中间件消费一次后建立 cold Provider stream 的 owned terminal。 +/// Attempt 中间件消费一次后建立 cold Provider stream 的 owned terminal pub type ProviderMiddlewareTerminal = Box< dyn FnOnce( Operation, @@ -634,7 +638,7 @@ fn encode_sse_wire_event(wire: &ProtocolWireEvent) -> Result, context: MiddlewareContext, @@ -685,8 +689,8 @@ fn middleware_frame_to_provider_event( ) -> Result { let transformed = frame.transformed(); let (bytes, framing, _, mut envelope) = frame.into_provider_parts(); - // 透传不能把已解析的事件降格成 raw bytes,否则 Responses 会丢失终态和 WS 帧。 - // 同时核对正文,避免进程内中间件漏标 transformed 时忽略了实际改写。 + // 透传不能把已解析的事件降格成 raw bytes,否则 Responses 会丢失终态和 WS 帧 + // 同时核对正文,避免进程内中间件漏标 transformed 时忽略了实际改写 if let Some(event) = envelope.as_ref() && let Some(wire) = event .wire_event() @@ -805,7 +809,7 @@ impl Stream for ProviderStream { Poll::Pending => Poll::Pending, Poll::Ready(Some(Ok(event))) => { // 纯 wire 流的 facts 仍只是旁路观测;一旦交付 canonical-only - // 事件,就必须校验完整事实序列,不能漏掉带 wire 的开始或终态。 + // 事件,就必须校验完整事实序列,不能漏掉带 wire 的开始或终态 this.strict_canonical_seen |= event.wire_event().is_none() && !event.canonical_facts().is_empty(); for fact in event.canonical_facts() { @@ -869,19 +873,19 @@ impl Stream for ProviderStream { } } -/// 传给 Provider 的单候选请求。 +/// 传给 Provider 的单候选请求 #[derive(Clone)] pub struct ProviderRequest { operation: Operation, candidate: ProviderCandidate, } -/// Provider 对公共观测表可解释的请求语义;未知字段保持空值。 +/// Provider 对公共观测表可解释的请求语义;未知字段保持空值 #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct ProviderRequestObservation { - /// 独立 Provider 端点声明的请求模型,仅用于观测,不参与文本模型目录路由。 + /// 独立 Provider 端点声明的请求模型,仅用于观测,不参与文本模型目录路由 pub requested_model: Option, - /// 客户端原始请求中的推理强度。 + /// 客户端原始请求中的推理强度 pub reasoning_effort: Option, pub reasoning_preset: Option, pub request_kind: Option, @@ -890,7 +894,7 @@ pub struct ProviderRequestObservation { pub continuation: ContinuationRequestObservation, } -/// 仅用于恢复事件关联的客户端作用域不透明请求事实。 +/// 仅用于恢复事件关联的客户端作用域不透明请求事实 #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct ContinuationRequestObservation { pub affinity_hash: Option, @@ -899,7 +903,7 @@ pub struct ContinuationRequestObservation { } impl ProviderRequest { - /// 绑定 operation 与请求计划中冻结的 Provider 候选。 + /// 绑定 operation 与请求计划中冻结的 Provider 候选 #[must_use] pub const fn new(operation: Operation, candidate: ProviderCandidate) -> Self { Self { @@ -908,13 +912,13 @@ impl ProviderRequest { } } - /// 返回业务 operation。 + /// 返回业务 operation #[must_use] pub const fn operation(&self) -> &Operation { &self.operation } - /// 返回冻结 Provider 候选。 + /// 返回冻结 Provider 候选 #[must_use] pub const fn candidate(&self) -> &ProviderCandidate { &self.candidate @@ -932,14 +936,15 @@ impl fmt::Debug for ProviderRequest { } } -/// Provider 热路径唯一接口。 +/// Provider 热路径唯一接口 /// -/// 每次 `execute` 只能选择一个 credential 并准备一次可见上游调用。实现不得 +/// 每次 `execute` 只能选择一个 credential 并准备一次可见上游调用 +/// 实现不得 /// 在内部轮换 credential 或隐藏业务 retry;失败后由 Attempt Coordinator 使用 -/// 新的 attempt 再次调用。 +/// 新的 attempt 再次调用 #[async_trait] pub trait Provider: Send + Sync { - /// 从已冻结的配置解析请求身份;只读取本地发布资料,不执行网络请求。 + /// 从已冻结的配置解析请求身份;只读取本地发布资料,不执行网络请求 fn resolve_request_profile( &self, configuration: &crate::account::OpaqueProviderData, @@ -947,25 +952,25 @@ pub trait Provider: Send + Sync { Ok(configuration.clone()) } - /// 没有持久选择时返回 Provider 的已准备默认画像;结果仍由 Core 按请求冻结。 + /// 没有持久选择时返回 Provider 的已准备默认画像;结果仍由 Core 按请求冻结 fn default_request_profile( &self, ) -> Result, ProviderError> { Ok(None) } - /// 返回实例生命周期内稳定的注册名称。 + /// 返回实例生命周期内稳定的注册名称 fn name(&self) -> &str; - /// 返回当前进程已经成功发布的目录代次。 + /// 返回当前进程已经成功发布的目录代次 fn catalog_generation(&self) -> ProviderCatalogGeneration; - /// 发现型目录不用于提前拒绝上游可能支持的新模型或别名。 + /// 发现型目录不用于提前拒绝上游可能支持的新模型或别名 fn model_catalog_is_exhaustive(&self) -> bool { true } - /// 解释 Provider 差异化观测字段;不参与路由和传输。 + /// 解释 Provider 差异化观测字段;不参与路由和传输 fn request_observation( &self, _operation: &Operation, @@ -974,17 +979,17 @@ pub trait Provider: Send + Sync { ProviderRequestObservation::default() } - /// 查询当前 Provider 的实时模型目录,并由 Provider 自己编译能力事实。 + /// 查询当前 Provider 的实时模型目录,并由 Provider 自己编译能力事实 /// /// # Errors /// - /// 目录 transport、认证或 Provider 协议失败时返回稳定错误。 + /// 目录 transport、认证或 Provider 协议失败时返回稳定错误 async fn query_model_capabilities( &self, ) -> Result, ProviderError>; - /// 读取当前客户端协议的原生目录,必须限定到认证时冻结的账号范围。 - /// `None` 表示不提供该协议的原生目录;读取失败不能伪装成不支持。 + /// 读取当前客户端协议的原生目录,必须限定到认证时冻结的账号范围 + /// `None` 表示不提供该协议的原生目录;读取失败不能伪装成不支持 async fn query_client_model_catalog( &self, _scope: &crate::account::scope::FrozenAccountScope, @@ -997,16 +1002,17 @@ pub trait Provider: Send + Sync { Ok(None) } - /// 选择一个未被排除的资源并返回 cold [`ProviderStream`]。 + /// 选择一个未被排除的资源并返回 cold [`ProviderStream`] /// /// 返回成功、返回错误或准备 future 被取消前,均不得发送本次请求的上游握手或业务 - /// 载荷,也不得启动可独立完成这些发送的后台任务。发送只在返回的流被 poll 后发生, - /// 保证 Core 能在真实出站前校验账号范围并登记 attempt。 + /// 载荷,也不得启动可独立完成这些发送的后台任务 + /// 发送只在返回的流被 poll 后发生, + /// 保证 Core 能在真实出站前校验账号范围并登记 attempt /// /// # Errors /// /// 没有可用资源、请求无效或准备失败时返回 `NotSent` 错误; - /// 可能已发送的失败必须通过 stream 返回,不得降级发送事实。 + /// 可能已发送的失败必须通过 stream 返回,不得降级发送事实 async fn execute( self: Arc, request: ProviderRequest, @@ -1014,28 +1020,28 @@ pub trait Provider: Send + Sync { ) -> Result; } -/// Provider Registry 构建错误。 +/// Provider Registry 构建错误 #[derive(Debug, Clone, PartialEq, Eq, Error)] pub enum RegistryError { - /// Provider 名称无效。 + /// Provider 名称无效 #[error("invalid provider registry name")] InvalidName, - /// Provider 重复注册。 + /// Provider 重复注册 #[error("provider `{provider}` is already registered")] Duplicate { - /// Provider 名称。 + /// Provider 名称 provider: String, }, } -/// 唯一保存 `Arc` 的异构注册表。 +/// 唯一保存 `Arc` 的异构注册表 #[derive(Default)] pub struct ProviderRegistryBuilder { providers: BTreeMap>, } impl ProviderRegistryBuilder { - /// 创建空 builder。 + /// 创建空 builder #[must_use] pub const fn new() -> Self { Self { @@ -1043,11 +1049,11 @@ impl ProviderRegistryBuilder { } } - /// 注册 Provider。 + /// 注册 Provider /// /// # Errors /// - /// 名称无效或重复时返回错误。 + /// 名称无效或重复时返回错误 pub fn register(&mut self, provider: Arc) -> Result<(), RegistryError> { let kind = ProviderKind::new(provider.name()).map_err(|_| RegistryError::InvalidName)?; if self.providers.contains_key(&kind) { @@ -1059,7 +1065,7 @@ impl ProviderRegistryBuilder { Ok(()) } - /// 冻结注册表。 + /// 冻结注册表 #[must_use] pub fn build(self) -> ProviderRegistry { ProviderRegistry { @@ -1068,14 +1074,14 @@ impl ProviderRegistryBuilder { } } -/// 固定内置 Provider 的不可变注册表。 +/// 固定内置 Provider 的不可变注册表 #[derive(Clone, Default)] pub struct ProviderRegistry { pub(super) providers: Arc>>, } impl ProviderRegistry { - /// 从一组异构 Provider 构造冻结注册表。 + /// 从一组异构 Provider 构造冻结注册表 pub fn new( providers: impl IntoIterator>, ) -> Result { @@ -1086,13 +1092,13 @@ impl ProviderRegistry { Ok(builder.build()) } - /// 创建 builder。 + /// 创建 builder #[must_use] pub const fn builder() -> ProviderRegistryBuilder { ProviderRegistryBuilder::new() } - /// 按 Provider 名称查询 adapter。 + /// 按 Provider 名称查询 adapter #[must_use] pub fn get(&self, provider: &ProviderKind) -> Option<&Arc> { self.providers.get(provider) @@ -1112,19 +1118,19 @@ impl ProviderRegistry { }) } - /// 判断 Provider 是否已注册。 + /// 判断 Provider 是否已注册 #[must_use] pub fn contains(&self, provider: &ProviderKind) -> bool { self.providers.contains_key(provider) } - /// 返回注册数量。 + /// 返回注册数量 #[must_use] pub fn len(&self) -> usize { self.providers.len() } - /// 判断注册表是否为空。 + /// 判断注册表是否为空 #[must_use] pub fn is_empty(&self) -> bool { self.providers.is_empty() diff --git a/backend/crates/gateway-core/src/engine/response_control.rs b/backend/crates/gateway-core/src/engine/response_control.rs index 7878af2d5..14815d82a 100644 --- a/backend/crates/gateway-core/src/engine/response_control.rs +++ b/backend/crates/gateway-core/src/engine/response_control.rs @@ -1,11 +1,11 @@ -//! 当前执行的响应中断信号;只由持有活动响应的 Provider 注册,不查找其他执行或连接。 +//! 当前执行的响应中断信号;只由持有活动响应的 Provider 注册,不查找其他执行或连接 use std::fmt; use std::sync::{Arc, Mutex, Weak}; use crate::lifecycle::CancellationToken; -/// 单次根执行的控制入口;重试不继承已结束响应的控制目标。 +/// 单次根执行的控制入口;重试不继承已结束响应的控制目标 #[derive(Clone, Default)] pub struct ResponseControl { active: Arc>>, @@ -22,10 +22,10 @@ struct InterruptTarget { requested: CancellationToken, } -/// Provider 持有的活动响应控制权;释放后客户端无法再向该响应发出中断。 +/// Provider 持有的活动响应控制权;释放后客户端无法再向该响应发出中断 pub struct ActiveResponseInterrupt(Arc); -/// 控制帧不能作用于当前执行时的稳定原因,不携带其他响应身份。 +/// 控制帧不能作用于当前执行时的稳定原因,不携带其他响应身份 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ResponseInterruptError { Unavailable, @@ -33,7 +33,7 @@ pub enum ResponseInterruptError { } impl ResponseControl { - /// 在上游确认响应 ID 后注册;同一执行只允许一个存活的响应 owner。 + /// 在上游确认响应 ID 后注册;同一执行只允许一个存活的响应 owner pub fn activate(&self, response_id: String) -> Option { let mut active = self .active @@ -50,7 +50,7 @@ impl ResponseControl { Some(ActiveResponseInterrupt(target)) } - /// 重复中断合并为一次信号;ID 必须匹配本执行当前的上游响应。 + /// 重复中断合并为一次信号;ID 必须匹配本执行当前的上游响应 pub fn interrupt(&self, response_id: &str) -> Result<(), ResponseInterruptError> { let active = self .active @@ -73,7 +73,7 @@ impl ActiveResponseInterrupt { } pub fn requested(&self) -> impl Future + Send + 'static { - // 等待者只持有信号,不能延长活动响应 owner 的生命周期。 + // 等待者只持有信号,不能延长活动响应 owner 的生命周期 let requested = self.0.requested.clone(); async move { requested.cancelled().await } } diff --git a/backend/crates/gateway-core/src/engine/upstream_adapter.rs b/backend/crates/gateway-core/src/engine/upstream_adapter.rs index 30e8bdb24..1d1041aba 100644 --- a/backend/crates/gateway-core/src/engine/upstream_adapter.rs +++ b/backend/crates/gateway-core/src/engine/upstream_adapter.rs @@ -1,7 +1,8 @@ -//! 内置 Provider 已选账号上的受管上游适配端口。 +//! 内置 Provider 已选账号上的受管上游适配端口 //! -//! 适配器不注册 Provider 或选择账号。Provider 持有租约、解释凭据并计算费用; -//! Runtime 只实现适配器与受管网络之间的连接,返回值继续经过原生事件和结算链。 +//! 适配器不注册 Provider 或选择账号 +//! Provider 持有租约、解释凭据并计算费用; +//! Runtime 只实现适配器与受管网络之间的连接,返回值继续经过原生事件和结算链 use std::{fmt, sync::Arc}; @@ -23,7 +24,7 @@ use super::{ provider::{EventStream, ProviderCallMetadata}, }; -/// 发布代次内冻结的适配器选择计划;匹配只读宿主身份,不调用插件进程。 +/// 发布代次内冻结的适配器选择计划;匹配只读宿主身份,不调用插件进程 pub trait UpstreamAdapterPlan: fmt::Debug + Send + Sync { fn select( &self, @@ -33,7 +34,7 @@ pub trait UpstreamAdapterPlan: fmt::Debug + Send + Sync { ) -> Result>, ProviderError>; } -/// 请求同时持有计划及发布集合,防止旧集合先排空进程而中断在途调用。 +/// 请求同时持有计划及发布集合,防止旧集合先排空进程而中断在途调用 #[derive(Debug, Clone)] pub struct FrozenUpstreamAdapterPlan { plan: Arc, @@ -62,16 +63,17 @@ impl FrozenUpstreamAdapterPlan { } } -/// 为本次调用选定的适配器,持有对应代次的运行资源。 +/// 为本次调用选定的适配器,持有对应代次的运行资源 pub trait UpstreamAdapter: fmt::Debug + Send + Sync { - /// 返回冷流。首次消费前不得调用插件、建立业务连接或发送请求。 + /// 返回冷流 + /// 首次消费前不得调用插件、建立业务连接或发送请求 fn execute(self: Arc, invocation: UpstreamAdapterInvocation) -> EventStream; - /// 注册时声明的上游传输,用于宿主登记 attempt。 + /// 注册时声明的上游传输,用于宿主登记 attempt fn transport(&self) -> &str; } -/// 一次执行的宿主事实;账号及 Provider 归属不可由插件返回值覆盖。 +/// 一次执行的宿主事实;账号及 Provider 归属不可由插件返回值覆盖 pub struct UpstreamAdapterInvocation { pub operation: Operation, pub headers: Vec, @@ -80,10 +82,11 @@ pub struct UpstreamAdapterInvocation { pub account: Arc, } -/// Provider 针对已选账号租约提供的凭据与反馈端口。 +/// Provider 针对已选账号租约提供的凭据与反馈端口 /// -/// 实现必须持有原账号租约。鉴权头只交给宿主网络边界,不能投影到插件 RPC。 -/// 目标授权由适配器注册与受管网络检查,凭据格式及刷新协议仍归 Provider。 +/// 实现必须持有原账号租约 +/// 鉴权头只交给宿主网络边界,不能投影到插件 RPC +/// 目标授权由适配器注册与受管网络检查,凭据格式及刷新协议仍归 Provider pub trait UpstreamAccountConnection: Send + Sync { fn account_id(&self) -> &ProviderAccountId; fn credential_revision(&self) -> CredentialRevision; @@ -91,9 +94,9 @@ pub trait UpstreamAccountConnection: Send + Sync { fn outbound_proxy(&self) -> Option<&OutboundProxy>; fn authorization(&self) -> Result, ProviderError>; - /// 复用 Provider 的价格来源;未知用量、模型或档位不构造零费用。 + /// 复用 Provider 的价格来源;未知用量、模型或档位不构造零费用 fn calculate_cost(&self, service_tier: Option<&str>, usage: &Usage) -> Option; - /// 失败的刷新、账号状态和冷却反馈仍由 Provider 处理,不由 Runtime 写账号。 + /// 失败的刷新、账号状态和冷却反馈仍由 Provider 处理,不由 Runtime 写账号 fn record_failure(&self, error: ProviderError) -> BoxFuture<'_, ProviderError>; } diff --git a/backend/crates/gateway-core/src/error.rs b/backend/crates/gateway-core/src/error.rs index 56bb201cd..d587e5a8e 100644 --- a/backend/crates/gateway-core/src/error.rs +++ b/backend/crates/gateway-core/src/error.rs @@ -1,4 +1,4 @@ -//! 网关核心使用的稳定错误分类。 +//! 网关核心使用的稳定错误分类 use std::fmt; use std::num::NonZeroU32; @@ -14,55 +14,55 @@ pub use crate::validation::{ IdentifierError, MeteringError, OperationError, PolicyError, RoutingError, }; -/// 跨 Provider 稳定的上游失败分类。 +/// 跨 Provider 稳定的上游失败分类 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum ProviderErrorKind { - /// 请求语义错误。 + /// 请求语义错误 InvalidRequest, - /// previous-response 续接状态已不可用,需要客户端重建请求链。 + /// previous-response 续接状态已不可用,需要客户端重建请求链 ContinuationRecoveryRequired, - /// Provider 不支持所需能力。 + /// Provider 不支持所需能力 Unsupported, - /// Credential 认证失败。 + /// Credential 认证失败 Unauthorized, - /// Credential 没有权限。 + /// Credential 没有权限 PermissionDenied, - /// 冻结的请求策略在 Provider 发送前明确拒绝本次请求。 + /// 冻结的请求策略在 Provider 发送前明确拒绝本次请求 RequestPolicyDenied, - /// Provider 限流。 + /// Provider 限流 RateLimited, - /// Credential 配额耗尽。 + /// Credential 配额耗尽 QuotaExhausted, - /// 仍有符合条件的账号,但它们暂时没有可调度容量。 + /// 仍有符合条件的账号,但它们暂时没有可调度容量 AccountCapacityUnavailable, ConcurrencyQueueFull, ConcurrencyQueueTimeout, - /// Provider 已确认当前请求无法选出可用账号。 + /// Provider 已确认当前请求无法选出可用账号 NoEligibleAccount, - /// Provider 的账号存储、租约协调或本地凭据数据不可用。 + /// Provider 的账号存储、租约协调或本地凭据数据不可用 ProviderInfrastructureUnavailable, - /// 请求超时。 + /// 请求超时 Timeout, - /// 网络或 transport 失败。 + /// 网络或 transport 失败 Transport, - /// 上游协议不合法。 + /// 上游协议不合法 Protocol, - /// 上游以 message too big 拒收当前请求(如 WebSocket close 1009)。 - /// 这是请求自身的问题:换账号或暂停 Provider 都无法让它成功。 + /// 上游以 message too big 拒收当前请求(如 WebSocket close 1009) + /// 这是请求自身的问题:换账号或暂停 Provider 都无法让它成功 MessageTooBig, - /// Provider 暂不可用。 + /// Provider 暂不可用 Unavailable, - /// 上游明确拒绝当前请求的模型容量;不证明整个 Provider 的基础设施故障。 + /// 上游明确拒绝当前请求的模型容量;不证明整个 Provider 的基础设施故障 UpstreamCapacityUnavailable, - /// 请求被取消。 + /// 请求被取消 Cancelled, - /// 进程终止后由恢复流程收敛。 + /// 进程终止后由恢复流程收敛 ProcessTerminated, } impl ProviderErrorKind { - /// 返回可持久化的稳定名称。 + /// 返回可持久化的稳定名称 #[must_use] pub const fn as_str(self) -> &'static str { match self { @@ -91,63 +91,63 @@ impl ProviderErrorKind { } } -/// Provider 对 previous-response 失败给出的稳定恢复语义。 +/// Provider 对 previous-response 失败给出的稳定恢复语义 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ContinuationFailure { - /// 原生 continuation 正被同账号的另一请求占用。 + /// 原生 continuation 正被同账号的另一请求占用 Busy, - /// 原生 history 在当前连接或账号不可用,可由完整输入重放恢复。 + /// 原生 history 在当前连接或账号不可用,可由完整输入重放恢复 HistoryUnavailable, } -/// Provider 对 previous-response 失败给出的显式恢复边界。 +/// Provider 对 previous-response 失败给出的显式恢复边界 /// /// Core 不根据失败分类猜测 continuation 是否可迁移;只有 Provider 明确允许时, -/// 才能放宽 owner 范围。 +/// 才能放宽 owner 范围 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ContinuationRecoveryDisposition { - /// 只能再次取得同账号、同一精确连接,不能改变 continuation scope。 + /// 只能再次取得同账号、同一精确连接,不能改变 continuation scope RetryExactConnection, - /// 代理停止内部恢复,由客户端携带完整输入创建新链。 + /// 代理停止内部恢复,由客户端携带完整输入创建新链 ClientReplayRequired, - /// Provider 已证明可以进入 owner 或其他合法目标的恢复流程。 + /// Provider 已证明可以进入 owner 或其他合法目标的恢复流程 ProviderReplayAllowed, } -/// Provider 在下游尚未提交事件时请求的隐藏恢复方式。 +/// Provider 在下游尚未提交事件时请求的隐藏恢复方式 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum PreDeliveryRetry { - /// 排除本次账号,按普通调度策略重新选号。 + /// 排除本次账号,按普通调度策略重新选号 AccountRotation, - /// 上游明确拒绝后的同账号退避;耗尽后仍由普通安全重放规则决定是否换号。 + /// 上游明确拒绝后的同账号退避;耗尽后仍由普通安全重放规则决定是否换号 SameAccountTransientRetry { - /// 每个账号在本请求中的重试上限,同时受总路由预算约束。 + /// 每个账号在本请求中的重试上限,同时受总路由预算约束 max_retries: NonZeroU32, - /// 指数退避的起始间隔。 + /// 指数退避的起始间隔 initial_delay: Duration, - /// 单次等待上限,避免上游提示把请求拖入长时间等待。 + /// 单次等待上限,避免上游提示把请求拖入长时间等待 max_delay: Duration, }, - /// 固定本次账号,并按 Provider 给出的序号重试当前传输。 + /// 固定本次账号,并按 Provider 给出的序号重试当前传输 SameAccountTransportRetry { - /// Provider-owned 传输重试序号,从 1 开始。 + /// Provider-owned 传输重试序号,从 1 开始 retry_index: NonZeroU32, - /// 发起下一次传输尝试前的退避时长。 + /// 发起下一次传输尝试前的退避时长 delay: Duration, }, - /// 可靠 NotSent 的建连恢复;次数与时间由 Core 的请求级预算裁决。 + /// 可靠 NotSent 的建连恢复;次数与时间由 Core 的请求级预算裁决 SameAccountConnectionRetry { - /// Provider 明确指定实际失败的传输,避免 HTTP 恢复重新进入 WS。 + /// Provider 明确指定实际失败的传输,避免 HTTP 恢复重新进入 WS transport: crate::engine::AttemptTransport, }, - /// 固定本次账号,并要求 Provider 使用备用传输。 + /// 固定本次账号,并要求 Provider 使用备用传输 SameAccountTransportFallback, } -/// 已由 Provider 从结构化上游错误中提取、仅供原客户端协议展示的错误详情。 +/// 已由 Provider 从结构化上游错误中提取、仅供原客户端协议展示的错误详情 /// /// 它不是诊断日志或持久化错误信息:[`Debug`] 会隐藏全部值,调用方应只在 -/// 原请求的协议响应中使用它。 +/// 原请求的协议响应中使用它 #[derive(Clone, PartialEq, Eq)] pub struct ClientVisibleUpstreamError { message: String, @@ -156,7 +156,7 @@ pub struct ClientVisibleUpstreamError { } impl ClientVisibleUpstreamError { - /// 原样保存结构化上游 `message`、`code` 和 `type`。 + /// 原样保存结构化上游 `message`、`code` 和 `type` #[must_use] pub fn new( message: impl Into, @@ -170,19 +170,19 @@ impl ClientVisibleUpstreamError { } } - /// 返回原上游的结构化 message。 + /// 返回原上游的结构化 message #[must_use] pub fn message(&self) -> &str { &self.message } - /// 返回原上游的结构化 code。 + /// 返回原上游的结构化 code #[must_use] pub fn code(&self) -> Option<&str> { self.code.as_ref().map(OpaqueUpstreamValue::as_str) } - /// 返回原上游的结构化 type。 + /// 返回原上游的结构化 type #[must_use] pub fn error_type(&self) -> Option<&str> { self.error_type.as_ref().map(OpaqueUpstreamValue::as_str) @@ -195,10 +195,10 @@ impl fmt::Debug for ClientVisibleUpstreamError { } } -/// Provider 已分类为可安全持久化和展示的诊断摘要。 +/// Provider 已分类为可安全持久化和展示的诊断摘要 /// /// Adapter 只能放入不含凭据、cookie、原始请求正文和 opaque response ID 的有界摘要; -/// 原始上游错误返回由 [`RawUpstreamError`] 单独承载。 +/// 原始上游错误返回由 [`RawUpstreamError`] 单独承载 #[derive(Clone, PartialEq, Eq)] pub struct ProviderDiagnostic { message: String, @@ -216,7 +216,7 @@ impl ProviderDiagnostic { } } - /// 附着本地定义的阶段与原因码;独立于发送状态和重试策略。 + /// 附着本地定义的阶段与原因码;独立于发送状态和重试策略 #[must_use] pub const fn with_classification(mut self, stage: &'static str, code: &'static str) -> Self { self.stage = Some(stage); @@ -224,7 +224,7 @@ impl ProviderDiagnostic { self } - /// 从错误链补充可证明的 I/O 原因;只保留本地原因码,不格式化原始错误。 + /// 从错误链补充可证明的 I/O 原因;只保留本地原因码,不格式化原始错误 #[must_use] pub fn with_io_cause(mut self, error: &(dyn std::error::Error + 'static)) -> Self { let mut source = Some(error); @@ -275,11 +275,12 @@ impl fmt::Debug for ProviderDiagnostic { } } -/// 运维错误详情中按原样展示的上游错误返回。 +/// 运维错误详情中按原样展示的上游错误返回 /// /// 该值只接收响应方向的错误正文或 WebSocket close/error frame;不得放入请求 -/// Authorization、Cookie 或客户端输入。它会被持久化并由管理端显式返回,但普通 -/// `Debug`/`Display` 不输出正文,避免在非运维日志中重复扩散。 +/// Authorization、Cookie 或客户端输入 +/// 它会被持久化并由管理端显式返回,但普通 +/// `Debug`/`Display` 不输出正文,避免在非运维日志中重复扩散 #[derive(Clone, PartialEq, Eq)] pub struct RawUpstreamError(String); @@ -301,10 +302,10 @@ impl fmt::Debug for RawUpstreamError { } } -/// Provider 上游物理连接结束时的低基数观测。 +/// Provider 上游物理连接结束时的低基数观测 /// /// 连接标识必须是随机或单向派生值,不得携带凭据、原始会话 ID -/// 或请求正文。 +/// 或请求正文 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderConnectionObservation { connection_id: String, @@ -350,14 +351,15 @@ impl ProviderConnectionObservation { } } -/// 只供当前客户端请求使用的 HTTP 失败响应。 +/// 只供当前客户端请求使用的 HTTP 失败响应 /// -/// 通常保留原始上游响应;Provider 也可为明确的恢复流程准备客户端投影。 -/// 投影不能覆盖 [`ProviderError`] 中用于诊断和记账的真实上游事实。 +/// 通常保留原始上游响应;Provider 也可为明确的恢复流程准备客户端投影 +/// 投影不能覆盖 [`ProviderError`] 中用于诊断和记账的真实上游事实 /// -/// 该值不属于稳定诊断事实,不能进入日志或持久化。它刻意不实现 [`Clone`]; +/// 该值不属于稳定诊断事实,不能进入日志或持久化 +/// 它刻意不实现 [`Clone`]; /// [`ProviderError::stable_snapshot`] 会丢弃它,只有最终失败的原对象才能把响应交给 -/// 原客户端协议,或由认证管理端的账号连接测试显式复制到请求局部结果。 +/// 原客户端协议,或由认证管理端的账号连接测试显式复制到请求局部结果 #[derive(PartialEq, Eq)] pub struct ClientVisibleUpstreamResponse { status: u16, @@ -367,7 +369,7 @@ pub struct ClientVisibleUpstreamResponse { } impl ClientVisibleUpstreamResponse { - /// 保存客户端响应的状态码、Content-Type 和正文。 + /// 保存客户端响应的状态码、Content-Type 和正文 #[must_use] pub fn new(status: u16, content_type: Option>, body: Bytes) -> Self { Self { @@ -378,7 +380,7 @@ impl ClientVisibleUpstreamResponse { } } - /// 附加 Provider 已剔除账号绑定与 framing 信息的普通响应头。 + /// 附加 Provider 已剔除账号绑定与 framing 信息的普通响应头 #[must_use] pub fn with_headers(mut self, headers: Vec) -> Self { self.headers = headers; @@ -421,15 +423,17 @@ impl fmt::Debug for ClientVisibleUpstreamResponse { } } -/// 单次 Provider 调用的稳定错误。 +/// 单次 Provider 调用的稳定错误 /// /// 稳定诊断字段不接收原始响应正文,也不会在 `Debug` 或 `Display` 中打印上游 -/// code/request ID/response ID。Adapter 若捕获到可能含 secret 的上下文,只能调用 -/// [`ProviderError::redact_sensitive_context`] 丢弃正文并留下脱敏标记。 +/// code/request ID/response ID +/// Adapter 若捕获到可能含 secret 的上下文,只能调用 +/// [`ProviderError::redact_sensitive_context`] 丢弃正文并留下脱敏标记 /// /// 唯一例外是 [`ProviderError::with_atomic_client_events`]:它只承载尚未交付客户端、 -/// 必须与本错误一起完成重试判断的协议事件。Core 会在任何持久化或日志记录前取走 -/// 这些事件,不能把它们当作诊断上下文使用。 +/// 必须与本错误一起完成重试判断的协议事件 +/// Core 会在任何持久化或日志记录前取走 +/// 这些事件,不能把它们当作诊断上下文使用 pub struct ProviderError { kind: ProviderErrorKind, send_state: UpstreamSendState, @@ -467,7 +471,7 @@ struct ProviderErrorFailureObservation { struct AtomicClientEvents(Vec); impl ProviderError { - /// 创建 Provider 错误。 + /// 创建 Provider 错误 #[must_use] pub const fn new(kind: ProviderErrorKind, send_state: UpstreamSendState) -> Self { Self { @@ -492,7 +496,7 @@ impl ProviderError { } } - /// 附加合法上游状态码。 + /// 附加合法上游状态码 #[must_use] pub fn with_status(mut self, status: u16) -> Self { if (100..=599).contains(&status) { @@ -501,14 +505,14 @@ impl ProviderError { self } - /// 附加 adapter 已分类为安全的上游错误 code。 + /// 附加 adapter 已分类为安全的上游错误 code #[must_use] pub fn with_upstream_code(mut self, code: OpaqueUpstreamValue) -> Self { self.upstream_values_mut().code = Some(code); self } - /// 附加 adapter 已分类为非 bearer 的上游 request ID。 + /// 附加 adapter 已分类为非 bearer 的上游 request ID #[must_use] pub fn with_upstream_request_id(mut self, request_id: OpaqueUpstreamValue) -> Self { self.upstream_values_mut().request_id = Some(request_id); @@ -520,21 +524,21 @@ impl ProviderError { .get_or_insert_with(|| Box::new(ProviderErrorUpstreamValues::default())) } - /// 附加 Provider 建议的冷却时间。 + /// 附加 Provider 建议的冷却时间 #[must_use] pub const fn with_retry_after(mut self, retry_after: Duration) -> Self { self.retry_after = Some(retry_after); self } - /// 附加 previous-response 的恢复语义。 + /// 附加 previous-response 的恢复语义 #[must_use] pub const fn with_continuation_failure(mut self, failure: ContinuationFailure) -> Self { self.continuation_failure = Some(failure); self } - /// 附加 Provider 明确选择的 previous-response 恢复边界。 + /// 附加 Provider 明确选择的 previous-response 恢复边界 #[must_use] pub const fn with_continuation_recovery_disposition( mut self, @@ -544,7 +548,7 @@ impl ProviderError { self } - /// 附加 Provider 给出的低基数 continuation 不可用原因。 + /// 附加 Provider 给出的低基数 continuation 不可用原因 #[must_use] pub fn with_continuation_unavailable_reason(mut self, reason: &'static str) -> Self { self.failure_observation_mut() @@ -552,7 +556,7 @@ impl ProviderError { self } - /// 附加上游物理连接结束观测。 + /// 附加上游物理连接结束观测 #[must_use] pub fn with_connection_observation( mut self, @@ -567,46 +571,47 @@ impl ProviderError { .get_or_insert_with(|| Box::new(ProviderErrorFailureObservation::default())) } - /// 标记 Provider 已证明本次失败可在下游提交前安全重放。 + /// 标记 Provider 已证明本次失败可在下游提交前安全重放 /// /// 证明可以来自明确的“未执行”拒绝,也可以来自同 attempt 已交付、可恢复 - /// 同一业务请求的状态检查点;后者仍须由 Core 的同账号恢复门禁单独校验。 + /// 同一业务请求的状态检查点;后者仍须由 Core 的同账号恢复门禁单独校验 #[must_use] pub const fn with_replay_safe(mut self) -> Self { self.replay_safe = true; self } - /// 允许 Core 仅在客户端尚未收到任何事件时执行一次受预算约束的换号恢复。 + /// 允许 Core 仅在客户端尚未收到任何事件时执行一次受预算约束的换号恢复 /// /// 该标记不证明上游未执行,也不等价于 [`Self::with_replay_safe`];它只表达 - /// Provider 选择了“客户端无感恢复优先”的传输策略。Core 在 continuation、 - /// 指定账号或下游已经进入提交状态时必须忽略它。 + /// Provider 选择了“客户端无感恢复优先”的传输策略 + /// Core 在 continuation、 + /// 指定账号或下游已经进入提交状态时必须忽略它 #[must_use] pub fn with_pre_delivery_retry(mut self) -> Self { self.pre_delivery_retry = Some(Box::new(PreDeliveryRetry::AccountRotation)); self } - /// 允许 Core 在首个客户端事件前固定原账号并改用 Provider 备用传输。 + /// 允许 Core 在首个客户端事件前固定原账号并改用 Provider 备用传输 /// /// 与普通换号恢复相同,Core 仍会拒绝 continuation、指定账号、预算耗尽或 - /// 下游已提交后的隐藏重放。 + /// 下游已提交后的隐藏重放 #[must_use] pub fn with_pre_delivery_transport_fallback(mut self) -> Self { self.set_pre_delivery_transport_fallback(); self } - /// 原地设置同账号备用传输恢复,保留当前错误携带的原客户端响应。 + /// 原地设置同账号备用传输恢复,保留当前错误携带的原客户端响应 pub fn set_pre_delivery_transport_fallback(&mut self) { self.pre_delivery_retry = Some(Box::new(PreDeliveryRetry::SameAccountTransportFallback)); } - /// 允许 Core 在首个客户端事件前固定原账号并重试当前 Provider 传输。 + /// 允许 Core 在首个客户端事件前固定原账号并重试当前 Provider 传输 /// /// 重试预算和退避策略由 Provider 所属协议定义;Core 只负责同账号钉选、 - /// deadline/取消边界以及下游 commit barrier。 + /// deadline/取消边界以及下游 commit barrier #[must_use] pub fn with_pre_delivery_transport_retry( mut self, @@ -617,7 +622,7 @@ impl ProviderError { self } - /// 原地设置同账号当前传输重试,保留当前错误携带的原客户端响应。 + /// 原地设置同账号当前传输重试,保留当前错误携带的原客户端响应 pub fn set_pre_delivery_transport_retry(&mut self, retry_index: NonZeroU32, delay: Duration) { self.pre_delivery_retry = Some(Box::new(PreDeliveryRetry::SameAccountTransportRetry { retry_index, @@ -633,14 +638,14 @@ impl ProviderError { self } - /// 要求 Core 在账号凭据已恢复后仅对原账号重放一次。 + /// 要求 Core 在账号凭据已恢复后仅对原账号重放一次 #[must_use] pub const fn with_same_account_retry(mut self) -> Self { self.retry_same_account = true; self } - /// 请求有界同账号退避;该意图本身不提供安全重放证据。 + /// 请求有界同账号退避;该意图本身不提供安全重放证据 #[must_use] pub fn with_transient_retry( mut self, @@ -656,21 +661,21 @@ impl ProviderError { self } - /// 标记上游明确拒绝应先触发 OAuth 恢复,而不是直接废弃账号。 + /// 标记上游明确拒绝应先触发 OAuth 恢复,而不是直接废弃账号 #[must_use] pub const fn with_credential_recovery(mut self) -> Self { self.credential_recovery_required = true; self } - /// 附加只用于原客户端协议响应的结构化上游错误。 + /// 附加只用于原客户端协议响应的结构化上游错误 #[must_use] pub fn with_client_visible_upstream_error(mut self, error: ClientVisibleUpstreamError) -> Self { self.client_visible_upstream_error = Some(Box::new(error)); self } - /// 附加仅可返回给当前请求方的原始上游 HTTP 失败响应。 + /// 附加仅可返回给当前请求方的原始上游 HTTP 失败响应 #[must_use] pub fn with_client_visible_upstream_response( mut self, @@ -680,10 +685,10 @@ impl ProviderError { self } - /// 附加必须先由 Core 判断重试、再决定丢弃或交付的客户端协议事件。 + /// 附加必须先由 Core 判断重试、再决定丢弃或交付的客户端协议事件 /// /// Provider 只能放入当前 attempt 尚未交付的事件;调用方应设置明确的有界缓冲, - /// 并保证失败事件本身位于这批事件的末尾。 + /// 并保证失败事件本身位于这批事件的末尾 #[must_use] pub fn with_atomic_client_events(mut self, events: Vec) -> Self { self.atomic_client_events = @@ -691,46 +696,46 @@ impl ProviderError { self } - /// 丢弃敏感正文,只记录“发生过脱敏”这一事实。 + /// 丢弃敏感正文,只记录“发生过脱敏”这一事实 #[must_use] pub fn redact_sensitive_context(mut self, _sensitive_context: impl AsRef) -> Self { self.sensitive_context_redacted = true; self } - /// 附加 Adapter 已确认可安全持久化和展示的诊断摘要。 + /// 附加 Adapter 已确认可安全持久化和展示的诊断摘要 #[must_use] pub fn with_diagnostic(mut self, diagnostic: ProviderDiagnostic) -> Self { self.diagnostic = Some(Box::new(diagnostic)); self } - /// 附加将由运维错误详情原样展示的上游错误返回。 + /// 附加将由运维错误详情原样展示的上游错误返回 #[must_use] pub fn with_raw_upstream_error(mut self, error: RawUpstreamError) -> Self { self.raw_upstream_error = Some(Box::new(error)); self } - /// 返回稳定错误分类。 + /// 返回稳定错误分类 #[must_use] pub const fn kind(&self) -> ProviderErrorKind { self.kind } - /// 返回本次 payload 的保守发送状态。 + /// 返回本次 payload 的保守发送状态 #[must_use] pub const fn send_state(&self) -> UpstreamSendState { self.send_state } - /// 返回合法上游状态码。 + /// 返回合法上游状态码 #[must_use] pub const fn upstream_status(&self) -> Option { self.upstream_status } - /// 返回安全分类的上游错误 code。 + /// 返回安全分类的上游错误 code #[must_use] pub fn upstream_code(&self) -> Option<&OpaqueUpstreamValue> { self.upstream_values @@ -738,7 +743,7 @@ impl ProviderError { .and_then(|values| values.code.as_ref()) } - /// 返回安全分类的上游 request ID。 + /// 返回安全分类的上游 request ID #[must_use] pub fn upstream_request_id(&self) -> Option<&OpaqueUpstreamValue> { self.upstream_values @@ -746,19 +751,19 @@ impl ProviderError { .and_then(|values| values.request_id.as_ref()) } - /// 返回 Provider 建议的冷却时间。 + /// 返回 Provider 建议的冷却时间 #[must_use] pub const fn retry_after(&self) -> Option { self.retry_after } - /// 返回 previous-response 的恢复语义。 + /// 返回 previous-response 的恢复语义 #[must_use] pub const fn continuation_failure(&self) -> Option { self.continuation_failure } - /// 返回 Provider 明确选择的 previous-response 恢复边界。 + /// 返回 Provider 明确选择的 previous-response 恢复边界 #[must_use] pub const fn continuation_recovery_disposition( &self, @@ -781,19 +786,19 @@ impl ProviderError { .and_then(|observation| observation.connection.as_ref()) } - /// 返回 Provider 是否已证明本次失败可安全重放。 + /// 返回 Provider 是否已证明本次失败可安全重放 #[must_use] pub const fn replay_is_safe(&self) -> bool { self.replay_safe } - /// 返回 Provider 是否允许在首个客户端事件前隐藏恢复传输失败。 + /// 返回 Provider 是否允许在首个客户端事件前隐藏恢复传输失败 #[must_use] pub const fn allows_pre_delivery_retry(&self) -> bool { self.pre_delivery_retry.is_some() } - /// 返回下游提交前由 Provider 选择的隐藏恢复方式。 + /// 返回下游提交前由 Provider 选择的隐藏恢复方式 #[must_use] pub const fn pre_delivery_retry(&self) -> Option { match self.pre_delivery_retry.as_ref() { @@ -802,7 +807,7 @@ impl ProviderError { } } - /// 返回 Provider 是否已完成凭据恢复并要求原账号重放。 + /// 返回 Provider 是否已完成凭据恢复并要求原账号重放 #[must_use] pub const fn retries_same_account(&self) -> bool { self.retry_same_account @@ -813,40 +818,40 @@ impl ProviderError { self.credential_recovery_required } - /// 表示 adapter 是否丢弃过敏感错误正文。 + /// 表示 adapter 是否丢弃过敏感错误正文 #[must_use] pub const fn sensitive_context_was_redacted(&self) -> bool { self.sensitive_context_redacted } - /// 返回可安全持久化和展示的 Provider 诊断摘要。 + /// 返回可安全持久化和展示的 Provider 诊断摘要 #[must_use] pub fn diagnostic(&self) -> Option<&ProviderDiagnostic> { self.diagnostic.as_deref() } - /// 返回将由运维错误详情原样展示的上游错误返回。 + /// 返回将由运维错误详情原样展示的上游错误返回 #[must_use] pub fn raw_upstream_error(&self) -> Option<&RawUpstreamError> { self.raw_upstream_error.as_deref() } - /// 返回仅供原客户端协议展示的结构化上游错误。 + /// 返回仅供原客户端协议展示的结构化上游错误 #[must_use] pub fn client_visible_upstream_error(&self) -> Option<&ClientVisibleUpstreamError> { self.client_visible_upstream_error.as_deref() } - /// 返回只供当前协议响应使用的原始上游 HTTP 失败响应。 + /// 返回只供当前协议响应使用的原始上游 HTTP 失败响应 #[must_use] pub fn client_visible_upstream_response(&self) -> Option<&ClientVisibleUpstreamResponse> { self.client_visible_upstream_response.as_deref() } - /// 取走只供本次重试/提交决策使用的原子客户端事件。 + /// 取走只供本次重试/提交决策使用的原子客户端事件 /// /// Core 必须在创建稳定快照、记录中间失败或构造终态前调用本方法,避免原始 - /// wire 进入诊断或持久化对象。 + /// wire 进入诊断或持久化对象 #[must_use] pub fn take_atomic_client_events(&mut self) -> Vec { self.atomic_client_events @@ -854,15 +859,15 @@ impl ProviderError { .map_or_else(Vec::new, |events| events.0) } - /// 返回是否携带尚未提交的原子客户端事件;不暴露事件内容。 + /// 返回是否携带尚未提交的原子客户端事件;不暴露事件内容 #[must_use] pub fn has_atomic_client_events(&self) -> bool { self.atomic_client_events.is_some() } - /// 复制可安全进入诊断和持久化边界的稳定错误事实。 + /// 复制可安全进入诊断和持久化边界的稳定错误事实 /// - /// 请求局部的原始上游响应与尚未提交的客户端事件不会进入快照。 + /// 请求局部的原始上游响应与尚未提交的客户端事件不会进入快照 #[must_use] pub fn stable_snapshot(&self) -> Self { Self { @@ -966,45 +971,45 @@ impl fmt::Display for ProviderError { impl std::error::Error for ProviderError {} -/// 对客户端协议稳定的网关错误分类。 +/// 对客户端协议稳定的网关错误分类 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum GatewayErrorKind { - /// 请求无效。 + /// 请求无效 InvalidRequest, - /// 请求能力不支持。 + /// 请求能力不支持 Unsupported, - /// 下游认证失败。 + /// 下游认证失败 Unauthorized, - /// 调用方策略拒绝。 + /// 调用方策略拒绝 PolicyDenied, - /// 对外模型或 route 不存在。 + /// 对外模型或 route 不存在 ModelNotFound, - /// 当前没有可用 target。 + /// 当前没有可用 target NoAvailableProvider, - /// 符合条件的上游账号暂时没有调度容量。 + /// 符合条件的上游账号暂时没有调度容量 AccountCapacityUnavailable, ConcurrencyQueueFull, ConcurrencyQueueTimeout, - /// Provider 的本地账号基础设施不可用。 + /// Provider 的本地账号基础设施不可用 ProviderInfrastructureUnavailable, - /// 上游限流。 + /// 上游限流 RateLimited, - /// 上游暂不可用。 + /// 上游暂不可用 UpstreamUnavailable, - /// 上游拒收当前请求:消息过大(如 WebSocket close 1009)。 - /// 客户端必须缩小或重建请求;重试或换账号都无法成功。 + /// 上游拒收当前请求:消息过大(如 WebSocket close 1009) + /// 客户端必须缩小或重建请求;重试或换账号都无法成功 MessageTooBig, - /// 请求超时。 + /// 请求超时 Timeout, - /// 请求取消。 + /// 请求取消 Cancelled, - /// 内部持久化或状态机失败。 + /// 内部持久化或状态机失败 Internal, } impl GatewayErrorKind { - /// 返回适合客户端协议映射的稳定 code。 + /// 返回适合客户端协议映射的稳定 code #[must_use] pub const fn as_str(self) -> &'static str { match self { @@ -1028,7 +1033,7 @@ impl GatewayErrorKind { } } -/// 协议无关、可安全暴露的网关错误。 +/// 协议无关、可安全暴露的网关错误 #[derive(Clone, PartialEq, Eq, Error)] #[error("{message}")] pub struct GatewayError { @@ -1046,7 +1051,7 @@ struct GatewayClientErrorDetails { } impl GatewayError { - /// 使用静态安全消息创建错误。 + /// 使用静态安全消息创建错误 #[must_use] pub const fn new(kind: GatewayErrorKind, message: &'static str) -> Self { Self { @@ -1081,7 +1086,7 @@ impl GatewayError { .and_then(|details| details.retry_after) } - /// 将 Provider 错误归一为客户端无关错误。 + /// 将 Provider 错误归一为客户端无关错误 #[must_use] pub fn from_provider(error: &ProviderError) -> Self { let gateway = match error.kind() { @@ -1157,40 +1162,40 @@ impl GatewayError { } } - /// 附加只供运维观测使用的脱敏 Provider 摘要。 + /// 附加只供运维观测使用的脱敏 Provider 摘要 #[must_use] pub fn with_diagnostic(mut self, diagnostic: ProviderDiagnostic) -> Self { self.diagnostic = Some(Box::new(diagnostic)); self } - /// 附加只供请求方协议展示的结构化上游错误。 + /// 附加只供请求方协议展示的结构化上游错误 #[must_use] pub fn with_client_visible_upstream_error(mut self, error: ClientVisibleUpstreamError) -> Self { self.client_visible_upstream_error = Some(error); self } - /// 返回稳定错误分类。 + /// 返回稳定错误分类 #[must_use] pub const fn kind(&self) -> GatewayErrorKind { self.kind } - /// 返回已经脱敏的静态消息。 + /// 返回已经脱敏的静态消息 #[must_use] pub const fn safe_message(&self) -> &'static str { self.message } - /// 返回可安全持久化和展示的 Provider 诊断摘要。 + /// 返回可安全持久化和展示的 Provider 诊断摘要 #[must_use] pub fn diagnostic(&self) -> Option<&ProviderDiagnostic> { self.diagnostic.as_deref() } /// 返回优先用于原客户端协议响应的 message;持久化和日志必须继续使用 - /// [`Self::safe_message`]。 + /// [`Self::safe_message`] #[must_use] pub fn client_message(&self) -> &str { self.client_visible_upstream_error @@ -1198,7 +1203,7 @@ impl GatewayError { .map_or(self.message, ClientVisibleUpstreamError::message) } - /// 返回原上游结构化 error type;没有时调用方应回退稳定网关 type。 + /// 返回原上游结构化 error type;没有时调用方应回退稳定网关 type #[must_use] pub fn client_error_type(&self) -> Option<&str> { self.client_visible_upstream_error @@ -1206,7 +1211,7 @@ impl GatewayError { .and_then(ClientVisibleUpstreamError::error_type) } - /// 返回原上游结构化 error code;没有时调用方应回退稳定网关 code。 + /// 返回原上游结构化 error code;没有时调用方应回退稳定网关 code #[must_use] pub fn client_error_code(&self) -> Option<&str> { self.client_visible_upstream_error @@ -1241,21 +1246,21 @@ impl fmt::Debug for GatewayError { } } -/// Store adapter 的稳定错误分类。 +/// Store adapter 的稳定错误分类 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum StoreErrorKind { - /// CAS 或 expected revision 冲突。 + /// CAS 或 expected revision 冲突 Conflict, - /// 存储暂不可用。 + /// 存储暂不可用 Unavailable, - /// 生命周期状态转换非法。 + /// 生命周期状态转换非法 InvalidState, - /// 数据无法转换为核心事实。 + /// 数据无法转换为核心事实 InvalidData, } -/// Core port 返回的脱敏存储错误。 +/// Core port 返回的脱敏存储错误 #[derive(Debug, Clone, PartialEq, Eq, Error)] #[error("execution store failed: {kind:?}")] pub struct StoreError { @@ -1263,13 +1268,13 @@ pub struct StoreError { } impl StoreError { - /// 创建不携带数据库正文的 store 错误。 + /// 创建不携带数据库正文的 store 错误 #[must_use] pub const fn new(kind: StoreErrorKind) -> Self { Self { kind } } - /// 返回稳定错误分类。 + /// 返回稳定错误分类 #[must_use] pub const fn kind(&self) -> StoreErrorKind { self.kind diff --git a/backend/crates/gateway-core/src/event.rs b/backend/crates/gateway-core/src/event.rs index 21cfddc00..23cd4cf91 100644 --- a/backend/crates/gateway-core/src/event.rs +++ b/backend/crates/gateway-core/src/event.rs @@ -1,4 +1,4 @@ -//! Provider 与客户端协议之间唯一的 canonical event 边界。 +//! Provider 与客户端协议之间唯一的 canonical event 边界 use std::collections::BTreeMap; use std::fmt; @@ -14,7 +14,7 @@ use crate::upstream::OpaqueUpstreamValue; use crate::upstream::UpstreamTransport; use crate::validation::{IdentifierError, validate_text}; -/// 一次响应的稳定元数据。 +/// 一次响应的稳定元数据 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ResponseMeta { response_id: String, @@ -23,7 +23,7 @@ pub struct ResponseMeta { } impl ResponseMeta { - /// 创建响应元数据。 + /// 创建响应元数据 #[must_use] pub fn new(response_id: impl Into, model: impl Into) -> Self { Self { @@ -33,7 +33,7 @@ impl ResponseMeta { } } - /// 创建不声明模型的 Provider 原生端点响应元数据。 + /// 创建不声明模型的 Provider 原生端点响应元数据 #[must_use] pub fn for_provider_endpoint(response_id: impl Into) -> Self { Self { @@ -43,65 +43,65 @@ impl ResponseMeta { } } - /// 设置终止原因。 + /// 设置终止原因 #[must_use] pub const fn with_finish_reason(mut self, finish_reason: FinishReason) -> Self { self.finish_reason = Some(finish_reason); self } - /// 返回客户端可见的 Provider 原生响应 ID。 + /// 返回客户端可见的 Provider 原生响应 ID #[must_use] pub fn response_id(&self) -> &str { &self.response_id } - /// 返回对客户端公开的模型名。 + /// 返回对客户端公开的模型名 #[must_use] pub fn model(&self) -> Option<&str> { self.model.as_deref() } - /// 返回规范化终止原因。 + /// 返回规范化终止原因 #[must_use] pub const fn finish_reason(&self) -> Option { self.finish_reason } } -/// 规范化终止原因。 +/// 规范化终止原因 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum FinishReason { - /// 正常停止。 + /// 正常停止 Stop, - /// 达到长度限制。 + /// 达到长度限制 Length, - /// 发出了工具调用。 + /// 发出了工具调用 ToolCall, - /// 内容策略停止。 + /// 内容策略停止 ContentFilter, - /// Provider 返回了完整但非上述类别的终态。 + /// Provider 返回了完整但非上述类别的终态 Other, } -/// 输出内容类别。 +/// 输出内容类别 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum ContentKind { - /// 文本。 + /// 文本 Text, - /// 推理摘要或推理内容。 + /// 推理摘要或推理内容 Reasoning, - /// 工具调用。 + /// 工具调用 ToolCall, - /// 图像。 + /// 图像 Image, - /// 音频。 + /// 音频 Audio, } -/// 新增的输出内容项。 +/// 新增的输出内容项 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ContentItem { index: u32, @@ -109,31 +109,31 @@ pub struct ContentItem { } impl ContentItem { - /// 创建内容项。 + /// 创建内容项 #[must_use] pub const fn new(index: u32, kind: ContentKind) -> Self { Self { index, kind } } - /// 返回内容索引。 + /// 返回内容索引 #[must_use] pub const fn index(&self) -> u32 { self.index } - /// 返回内容类别。 + /// 返回内容类别 #[must_use] pub const fn kind(&self) -> ContentKind { self.kind } } -/// 文本增量。 +/// 文本增量 #[derive(Clone, PartialEq, Eq)] pub struct TextDelta { - /// 内容索引。 + /// 内容索引 pub content_index: u32, - /// 增量正文。 + /// 增量正文 pub text: String, } @@ -147,12 +147,12 @@ impl fmt::Debug for TextDelta { } } -/// 推理内容增量。 +/// 推理内容增量 #[derive(Clone, PartialEq, Eq)] pub struct ReasoningDelta { - /// 内容索引。 + /// 内容索引 pub content_index: u32, - /// 增量正文。 + /// 增量正文 pub text: String, } @@ -166,16 +166,16 @@ impl fmt::Debug for ReasoningDelta { } } -/// 工具调用增量。 +/// 工具调用增量 #[derive(Clone, PartialEq, Eq)] pub struct ToolCallDelta { - /// 内容索引。 + /// 内容索引 pub content_index: u32, - /// 稳定 tool call ID。 + /// 稳定 tool call ID pub call_id: String, - /// 首个增量可携带工具名。 + /// 首个增量可携带工具名 pub name: Option, - /// JSON arguments 字符串增量。 + /// JSON arguments 字符串增量 pub arguments_delta: String, } @@ -191,10 +191,11 @@ impl fmt::Debug for ToolCallDelta { } } -/// Provider 返回的协议原生 JSON event。 +/// Provider 返回的协议原生 JSON event /// /// Core 不解释 `data`,只把它与同一上游事件产生的 canonical facts 一起 -/// 交给客户端协议 adapter。Debug 永不输出正文。 +/// 交给客户端协议 adapter +/// Debug 永不输出正文 #[derive(Clone, PartialEq)] pub struct ProtocolWireEvent { protocol: String, @@ -209,11 +210,12 @@ pub struct ProtocolWireEvent { } impl ProtocolWireEvent { - /// 创建协议原生 JSON event。 + /// 创建协议原生 JSON event /// /// # Errors /// - /// 协议名不满足内部路由标识约束时返回错误。上游事件名保持不透明。 + /// 协议名不满足内部路由标识约束时返回错误 + /// 上游事件名保持不透明 pub fn json( protocol: impl Into, event_type: Option, @@ -222,11 +224,12 @@ impl ProtocolWireEvent { Self::json_with_sse_metadata(protocol, event_type, data, None, None) } - /// 创建携带原生 SSE 元数据的协议 JSON event。 + /// 创建携带原生 SSE 元数据的协议 JSON event /// /// # Errors /// - /// 协议名不满足内部路由标识约束时返回错误。SSE 元数据保持不透明。 + /// 协议名不满足内部路由标识约束时返回错误 + /// SSE 元数据保持不透明 pub fn json_with_sse_metadata( protocol: impl Into, event_type: Option, @@ -249,13 +252,13 @@ impl ProtocolWireEvent { }) } - /// 创建携带未经改写 SSE 原始帧的协议 JSON event。 + /// 创建携带未经改写 SSE 原始帧的协议 JSON event /// - /// `data` 只供 Core 旁路观测;客户端 SSE 输出必须优先使用 `raw_sse_frame`。 + /// `data` 只供 Core 旁路观测;客户端 SSE 输出必须优先使用 `raw_sse_frame` /// /// # Errors /// - /// 与 [`Self::json_with_sse_metadata`] 相同。 + /// 与 [`Self::json_with_sse_metadata`] 相同 pub fn json_with_raw_sse_metadata( protocol: impl Into, event_type: Option, @@ -270,14 +273,14 @@ impl ProtocolWireEvent { Ok(event) } - /// 创建只有原始 SSE 字节、没有可解析 JSON 的协议 event。 + /// 创建只有原始 SSE 字节、没有可解析 JSON 的协议 event /// /// 例如 keep-alive 注释和未知的非 JSON SSE 帧仍须原样透传,但不会参与 - /// WebSocket JSON 输出或旁路观测。 + /// WebSocket JSON 输出或旁路观测 /// /// # Errors /// - /// 协议名不满足 wire 安全约束时返回错误。 + /// 协议名不满足 wire 安全约束时返回错误 pub fn raw_sse( protocol: impl Into, raw_sse_frame: Bytes, @@ -297,13 +300,13 @@ impl ProtocolWireEvent { }) } - /// 创建未经改写的完整 JSON 响应正文。 + /// 创建未经改写的完整 JSON 响应正文 /// - /// 该载荷用于非流式协议端点;Core 不解析或重编码其中的值。 + /// 该载荷用于非流式协议端点;Core 不解析或重编码其中的值 /// /// # Errors /// - /// 协议名不满足内部路由标识约束时返回错误。 + /// 协议名不满足内部路由标识约束时返回错误 pub fn raw_json( protocol: impl Into, raw_json_body: Bytes, @@ -323,11 +326,11 @@ impl ProtocolWireEvent { }) } - /// 创建未经改写的 HTTP 响应正文片段;正文不要求为 JSON。 + /// 创建未经改写的 HTTP 响应正文片段;正文不要求为 JSON /// /// # Errors /// - /// 协议名不满足内部路由标识约束时返回错误。 + /// 协议名不满足内部路由标识约束时返回错误 pub fn raw_http_body( protocol: impl Into, raw_http_body: Bytes, @@ -347,73 +350,73 @@ impl ProtocolWireEvent { }) } - /// 返回客户端协议名称。 + /// 返回客户端协议名称 #[must_use] pub fn protocol(&self) -> &str { &self.protocol } - /// 返回可选的协议事件名称。 + /// 返回可选的协议事件名称 #[must_use] pub fn event_type(&self) -> Option<&str> { self.event_type.as_deref() } - /// 返回上游 SSE `id` 字段。 + /// 返回上游 SSE `id` 字段 #[must_use] pub fn sse_id(&self) -> Option<&str> { self.sse_id.as_deref() } - /// 返回上游 SSE `retry` 字段。 + /// 返回上游 SSE `retry` 字段 #[must_use] pub const fn sse_retry(&self) -> Option { self.sse_retry } - /// 返回协议原生 JSON 数据。 + /// 返回协议原生 JSON 数据 #[must_use] pub const fn data(&self) -> &Value { &self.data } - /// 返回该 event 是否携带可供旁路观测的 JSON 数据。 + /// 返回该 event 是否携带可供旁路观测的 JSON 数据 #[must_use] pub const fn has_json_data(&self) -> bool { self.has_json_data } - /// 返回可直接写给 SSE 客户端的未经改写原始帧。 + /// 返回可直接写给 SSE 客户端的未经改写原始帧 #[must_use] pub const fn raw_sse_frame(&self) -> Option<&Bytes> { self.raw_sse_frame.as_ref() } - /// 返回可直接写给 JSON 客户端的未经改写响应正文。 + /// 返回可直接写给 JSON 客户端的未经改写响应正文 #[must_use] pub const fn raw_json_body(&self) -> Option<&Bytes> { self.raw_json_body.as_ref() } - /// 拆出未经改写的完整 JSON 响应正文。 + /// 拆出未经改写的完整 JSON 响应正文 #[must_use] pub fn into_raw_json_body(self) -> Option { self.raw_json_body } - /// 返回可直接交付给 HTTP 客户端的任意响应正文片段。 + /// 返回可直接交付给 HTTP 客户端的任意响应正文片段 #[must_use] pub const fn raw_http_body_bytes(&self) -> Option<&Bytes> { self.raw_http_body.as_ref() } - /// 拆出未经改写的 HTTP 响应正文片段。 + /// 拆出未经改写的 HTTP 响应正文片段 #[must_use] pub fn into_raw_http_body(self) -> Option { self.raw_http_body } - /// 拆出协议原生 JSON 数据。 + /// 拆出协议原生 JSON 数据 #[must_use] pub fn into_data(self) -> Value { self.data @@ -437,7 +440,7 @@ impl fmt::Debug for ProtocolWireEvent { } } -/// 上游响应使用的 HTTP 协议版本。 +/// 上游响应使用的 HTTP 协议版本 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum UpstreamHttpVersion { Unknown, @@ -449,7 +452,7 @@ pub enum UpstreamHttpVersion { } impl UpstreamHttpVersion { - /// 解析 transport 已规范化的协议版本。 + /// 解析 transport 已规范化的协议版本 #[must_use] pub fn parse(value: &str) -> Option { match value.trim().to_ascii_uppercase().as_str() { @@ -462,7 +465,7 @@ impl UpstreamHttpVersion { } } - /// 返回数据库使用的稳定名称。 + /// 返回数据库使用的稳定名称 #[must_use] pub const fn as_str(self) -> &'static str { match self { @@ -476,7 +479,7 @@ impl UpstreamHttpVersion { } } -/// Provider transport 边界测得的阶段耗时。 +/// Provider transport 边界测得的阶段耗时 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct ProviderResponseTimings { pub transport_decision_wait_ms: Option, @@ -489,18 +492,19 @@ pub struct ProviderResponseTimings { pub provider_processing_ms: Option, } -/// Provider 已筛选的响应观测 JSON。 +/// Provider 已筛选的响应观测 JSON /// /// Core 只验证它是有界 JSON object,并在请求终态原样交给存储层;字段语义、 -/// 脱敏和版本演进均由所属 Provider 负责。这样 Provider 专有协议不会渗入路由 -/// 或管理领域。 +/// 脱敏和版本演进均由所属 Provider 负责 +/// 这样 Provider 专有协议不会渗入路由 +/// 或管理领域 #[derive(Clone, PartialEq, Eq)] pub struct ProviderResponseMetadata(String); impl ProviderResponseMetadata { const MAX_BYTES: usize = 32 * 1024; - /// 从 Provider 已筛选的 JSON object 创建观测快照。 + /// 从 Provider 已筛选的 JSON object 创建观测快照 #[must_use] pub fn new(json: String) -> Option { if json.len() > Self::MAX_BYTES { @@ -512,7 +516,7 @@ impl ProviderResponseMetadata { .map(|_| Self(json)) } - /// 返回不透明的 JSON 文本,供持久化 adapter 原样验证和写入。 + /// 返回不透明的 JSON 文本,供持久化 adapter 原样验证和写入 #[must_use] pub fn as_json(&self) -> &str { &self.0 @@ -525,7 +529,7 @@ impl fmt::Debug for ProviderResponseMetadata { } } -/// Provider 已筛选、可由协议 adapter 尝试表达的不透明响应头。 +/// Provider 已筛选、可由协议 adapter 尝试表达的不透明响应头 #[derive(Clone, PartialEq, Eq)] pub struct ProviderResponseHeader { name: String, @@ -533,7 +537,7 @@ pub struct ProviderResponseHeader { } impl ProviderResponseHeader { - /// 保存 Provider 交付的原始名称和值;HTTP 可表达性由最终 adapter 判断。 + /// 保存 Provider 交付的原始名称和值;HTTP 可表达性由最终 adapter 判断 #[must_use] pub fn new(name: impl Into, value: Bytes) -> Self { Self { @@ -563,7 +567,7 @@ impl fmt::Debug for ProviderResponseHeader { } } -/// Core 消费的 Provider 执行观测,不会原样进入客户端响应。 +/// Core 消费的 Provider 执行观测,不会原样进入客户端响应 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderResponseObservation { transport: UpstreamTransport, @@ -619,11 +623,11 @@ impl ProviderResponseObservation { self } - /// 附加 Provider 确定的统计档位,Core 不重新解释其来源。 + /// 附加 Provider 确定的统计档位,Core 不重新解释其来源 /// /// # Errors /// - /// 档位为空、超过 64 字节或包含控制字符时返回错误。 + /// 档位为空、超过 64 字节或包含控制字符时返回错误 pub fn try_with_service_tier( mut self, service_tier: impl Into, @@ -634,7 +638,7 @@ impl ProviderResponseObservation { Ok(self) } - /// 附加 Provider 确定的统计档位;不适合持久化的值会被忽略。 + /// 附加 Provider 确定的统计档位;不适合持久化的值会被忽略 #[must_use] pub fn with_service_tier_if_valid(mut self, service_tier: impl Into) -> Self { let service_tier = service_tier.into(); @@ -644,7 +648,7 @@ impl ProviderResponseObservation { self } - /// 记录 Provider 从上游响应明确取得的模型,不改写发送模型或计费合同。 + /// 记录 Provider 从上游响应明确取得的模型,不改写发送模型或计费合同 #[must_use] pub fn with_upstream_response_model_if_valid(mut self, model: &str) -> Self { let model = model.trim(); @@ -671,7 +675,7 @@ impl ProviderResponseObservation { self } - /// 附加由 Provider 自己定义且已筛选的响应观测快照。 + /// 附加由 Provider 自己定义且已筛选的响应观测快照 #[must_use] pub fn with_provider_metadata(mut self, metadata: ProviderResponseMetadata) -> Self { self.provider_metadata = Some(metadata); @@ -718,14 +722,14 @@ impl ProviderResponseObservation { &self.client_headers } - /// 返回 Provider 专有的安全观测 JSON;Core 不读取其字段。 + /// 返回 Provider 专有的安全观测 JSON;Core 不读取其字段 #[must_use] pub const fn provider_metadata(&self) -> Option<&ProviderResponseMetadata> { self.provider_metadata.as_ref() } } -/// WebSocket 请求使用新连接或复用池中连接的事实。 +/// WebSocket 请求使用新连接或复用池中连接的事实 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum WebSocketPoolKind { New, @@ -742,34 +746,35 @@ impl WebSocketPoolKind { } } -/// 所有 Provider 都必须输出的稳定事件集合。 +/// 所有 Provider 都必须输出的稳定事件集合 #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] pub enum GatewayEvent { - /// 响应开始;必须是首事件。 + /// 响应开始;必须是首事件 Started(ResponseMeta), - /// 声明一个后续可增量写入的内容项。 + /// 声明一个后续可增量写入的内容项 ContentAdded(ContentItem), - /// 文本增量。 + /// 文本增量 TextDelta(TextDelta), - /// 推理增量。 + /// 推理增量 ReasoningDelta(ReasoningDelta), - /// 工具调用增量。 + /// 工具调用增量 ToolCallDelta(ToolCallDelta), - /// 规范化用量。 + /// 规范化用量 Usage(Usage), - /// Provider 域依据实际模型和终态用量计算的费用。 + /// Provider 域依据实际模型和终态用量计算的费用 CalculatedCost(CalculatedCost), - /// Provider 最终 usage chunk 上报的实际已计费总额;同 attempt 最新值覆盖旧值。 + /// Provider 最终 usage chunk 上报的实际已计费总额;同 attempt 最新值覆盖旧值 ProviderCost(ProviderReportedCost), - /// 响应完成;必须是末事件。 + /// 响应完成;必须是末事件 Completed(ResponseMeta), } -/// Provider 单个上游事件产生的事实与可选协议原生表达。 +/// Provider 单个上游事件产生的事实与可选协议原生表达 /// -/// 一个值至少包含一个 canonical fact 或一条 wire event。把同一 wire event -/// 产生的多个 canonical facts 放在同一封套,可避免客户端重复收到该事件。 +/// 一个值至少包含一个 canonical fact 或一条 wire event +/// 把同一 wire event +/// 产生的多个 canonical facts 放在同一封套,可避免客户端重复收到该事件 #[derive(Debug, Clone, PartialEq)] pub struct ProviderEvent { canonical: Vec, @@ -781,7 +786,7 @@ pub struct ProviderEvent { } impl ProviderEvent { - /// 创建只有一个 canonical fact 的事件。 + /// 创建只有一个 canonical fact 的事件 #[must_use] pub fn canonical(event: GatewayEvent) -> Self { Self { @@ -794,14 +799,14 @@ impl ProviderEvent { } } - /// 附加同一上游事件产生的业务事实,不重复生成协议输出。 + /// 附加同一上游事件产生的业务事实,不重复生成协议输出 #[must_use] pub fn with_fact(mut self, fact: GatewayEvent) -> Self { self.canonical.push(fact); self } - /// 创建只有协议原生表达的事件。 + /// 创建只有协议原生表达的事件 #[must_use] pub fn wire(wire: ProtocolWireEvent) -> Self { Self { @@ -814,7 +819,7 @@ impl ProviderEvent { } } - /// 创建同一上游事件的 canonical facts 与协议原生表达。 + /// 创建同一上游事件的 canonical facts 与协议原生表达 #[must_use] pub fn canonical_with_wire(canonical: Vec, wire: ProtocolWireEvent) -> Self { Self { @@ -827,7 +832,7 @@ impl ProviderEvent { } } - /// 创建仅供 Core 消费的上游响应观察;该事件不会进入客户端 adapter。 + /// 创建仅供 Core 消费的上游响应观察;该事件不会进入客户端 adapter #[must_use] pub fn observation(observation: ProviderResponseObservation) -> Self { Self { @@ -840,26 +845,26 @@ impl ProviderEvent { } } - /// 返回协议表达是否由中间件替换、展开或丢弃映射产生。 + /// 返回协议表达是否由中间件替换、展开或丢弃映射产生 /// - /// 宿主最终协议边界据此启用严格复核,插件可读取其快照。 + /// 宿主最终协议边界据此启用严格复核,插件可读取其快照 #[must_use] pub const fn middleware_transformed(&self) -> bool { self.middleware_transformed } - /// 返回中间件第一次改写前的协议表达,供协议复核和插件读取。 + /// 返回中间件第一次改写前的协议表达,供协议复核和插件读取 #[must_use] pub fn middleware_origin_wire(&self) -> Option<&ProtocolWireEvent> { self.middleware_origin_wire.as_deref() } - /// 记录宿主已经确认的中间件改写;后续原生编码和投递必须继续传播。 + /// 记录宿主已经确认的中间件改写;后续原生编码和投递必须继续传播 pub(crate) fn mark_middleware_transformed(&mut self) { self.middleware_transformed = true; } - /// 用中间件输出替换正文,同时冻结第一次改写前的原始 wire。 + /// 用中间件输出替换正文,同时冻结第一次改写前的原始 wire pub(crate) fn replace_middleware_wire(&mut self, wire: Option) { let previous = self.wire.take(); if self.middleware_origin_wire.is_none() { @@ -869,77 +874,77 @@ impl ProviderEvent { self.middleware_transformed = true; } - /// 一对多或内建协议转换生成的新封套继承同一份宿主 provenance。 + /// 一对多或内建协议转换生成的新封套继承同一份宿主 provenance pub(crate) fn inherit_middleware_provenance(&mut self, source: &Self) { self.middleware_transformed = source.middleware_transformed; self.middleware_origin_wire = source.middleware_origin_wire.clone(); } - /// 把响应观察附在同一上游封套,不拆分或重复交付原生表达。 + /// 把响应观察附在同一上游封套,不拆分或重复交付原生表达 pub fn attach_observation(&mut self, observation: ProviderResponseObservation) { self.observation = Some(Box::new(observation)); } /// 附加 Provider 私有状态检查点;Core 可将其用于同请求恢复,协议连接可在 - /// terminal 事件上持有它供下一轮使用。 + /// terminal 事件上持有它供下一轮使用 pub fn attach_session_update(&mut self, state: ProviderSessionState) { self.session_update = Some(Box::new(state)); } - /// 返回 Provider 私有的连接内状态更新。 + /// 返回 Provider 私有的连接内状态更新 #[must_use] pub fn session_update(&self) -> Option<&ProviderSessionState> { self.session_update.as_deref() } - /// 取出 Provider 连接内状态更新,交给协议连接持有。 + /// 取出 Provider 连接内状态更新,交给协议连接持有 #[must_use] pub fn take_session_update(&mut self) -> Option { self.session_update.take().map(|state| *state) } - /// 返回 Core 可解释的全部 facts。 + /// 返回 Core 可解释的全部 facts #[must_use] pub fn canonical_facts(&self) -> &[GatewayEvent] { &self.canonical } - /// 返回协议原生表达。 + /// 返回协议原生表达 #[must_use] pub fn wire_event(&self) -> Option<&ProtocolWireEvent> { self.wire.as_deref() } - /// 仅替换客户端协议表达;canonical facts、响应观察和会话状态保持原始事实。 + /// 仅替换客户端协议表达;canonical facts、响应观察和会话状态保持原始事实 pub fn replace_wire(&mut self, wire: Option) { self.wire = wire.map(Box::new); } - /// 取出仅供 Core 持久化的响应观察。 + /// 取出仅供 Core 持久化的响应观察 #[must_use] pub fn take_observation(&mut self) -> Option { self.observation.take().map(|observation| *observation) } - /// 返回仅供 Core 持久化的响应观察。 + /// 返回仅供 Core 持久化的响应观察 #[must_use] pub fn response_observation(&self) -> Option<&ProviderResponseObservation> { self.observation.as_deref() } - /// 拆分 canonical facts 与协议原生表达。 + /// 拆分 canonical facts 与协议原生表达 #[must_use] pub fn into_parts(self) -> (Vec, Option) { (self.canonical, self.wire.map(|wire| *wire)) } - /// 返回是否含有可用于 commit barrier 的 canonical fact。 + /// 返回是否含有可用于 commit barrier 的 canonical fact #[must_use] pub fn has_canonical_facts(&self) -> bool { !self.canonical.is_empty() } - /// 返回该封套是否仍包含可交付客户端的表达。 + /// 返回该封套是否仍包含可交付客户端的表达 #[must_use] pub fn has_client_event(&self) -> bool { !self.canonical.is_empty() || self.wire.is_some() @@ -952,36 +957,36 @@ impl From for ProviderEvent { } } -/// Canonical event 顺序错误。 +/// Canonical event 顺序错误 #[derive(Debug, Clone, PartialEq, Eq, Error)] pub enum EventSequenceError { - /// 首事件不是 `Started`。 + /// 首事件不是 `Started` #[error("canonical stream must start with Started")] MissingStarted, - /// `Started` 重复。 + /// `Started` 重复 #[error("canonical stream contains more than one Started event")] DuplicateStarted, - /// 内容索引重复。 + /// 内容索引重复 #[error("canonical stream adds content index {index} more than once")] DuplicateContent { - /// 重复索引。 + /// 重复索引 index: u32, }, - /// Delta 引用了不存在或类别错误的内容项。 + /// Delta 引用了不存在或类别错误的内容项 #[error("canonical delta does not match content index {index}")] InvalidDeltaTarget { - /// 内容索引。 + /// 内容索引 index: u32, }, - /// `Completed` 重复或其后仍有事件。 + /// `Completed` 重复或其后仍有事件 #[error("canonical stream emitted an event after Completed")] EventAfterCompleted, - /// Stream 未产生 `Completed` 就结束。 + /// Stream 未产生 `Completed` 就结束 #[error("canonical stream ended before Completed")] MissingCompleted, } -/// 增量校验 canonical event 顺序的轻量状态机。 +/// 增量校验 canonical event 顺序的轻量状态机 #[derive(Debug, Default)] pub struct EventSequenceValidator { started: bool, @@ -990,7 +995,7 @@ pub struct EventSequenceValidator { } impl EventSequenceValidator { - /// 创建空校验器。 + /// 创建空校验器 #[must_use] pub const fn new() -> Self { Self { @@ -1000,11 +1005,11 @@ impl EventSequenceValidator { } } - /// 校验一个事件并推进状态。 + /// 校验一个事件并推进状态 /// /// # Errors /// - /// 事件顺序或 delta 目标不满足 canonical contract 时返回错误。 + /// 事件顺序或 delta 目标不满足 canonical contract 时返回错误 pub fn observe(&mut self, event: &GatewayEvent) -> Result<(), EventSequenceError> { if self.completed { return Err(EventSequenceError::EventAfterCompleted); @@ -1046,11 +1051,11 @@ impl EventSequenceValidator { Ok(()) } - /// 校验 stream 是否以 `Completed` 正常结束。 + /// 校验 stream 是否以 `Completed` 正常结束 /// /// # Errors /// - /// 未开始或未完成时返回错误。 + /// 未开始或未完成时返回错误 pub fn finish(&self) -> Result<(), EventSequenceError> { if !self.started { return Err(EventSequenceError::MissingStarted); diff --git a/backend/crates/gateway-core/src/health.rs b/backend/crates/gateway-core/src/health.rs index 030fb058c..3ff1066f2 100644 --- a/backend/crates/gateway-core/src/health.rs +++ b/backend/crates/gateway-core/src/health.rs @@ -1,4 +1,4 @@ -//! Store、Core 与 Host 向 HTTP 健康探针暴露的中立契约。 +//! Store、Core 与 Host 向 HTTP 健康探针暴露的中立契约 use std::time::SystemTime; @@ -6,7 +6,7 @@ use futures::future::BoxFuture; use crate::task::{WorkerFencingToken, WorkerId, WorkerKind}; -/// 一个健康探针的语义状态。 +/// 一个健康探针的语义状态 #[derive(Debug, Clone, PartialEq, Eq)] pub enum HealthState { Healthy, @@ -14,14 +14,14 @@ pub enum HealthState { Unhealthy(String), } -/// 可被 API 异质聚合的异步健康探针。 +/// 可被 API 异质聚合的异步健康探针 pub trait HealthProbe: Send + Sync { fn name(&self) -> &'static str; fn check(&self) -> BoxFuture<'_, HealthState>; } -/// Host 监督器对一个 worker 观测到的当前状态。 +/// Host 监督器对一个 worker 观测到的当前状态 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WorkerRuntimeState { Starting, @@ -34,14 +34,14 @@ pub enum WorkerRuntimeState { Disabled, } -/// worker 健康快照的稳定 key。 +/// worker 健康快照的稳定 key #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum WorkerHealthKey { Task(WorkerId), Disabled(WorkerKind), } -/// 一个 worker 在某一时刻的完整可观测快照。 +/// 一个 worker 在某一时刻的完整可观测快照 #[derive(Debug, Clone, PartialEq, Eq)] pub struct WorkerHealthSnapshot { pub key: WorkerHealthKey, @@ -54,7 +54,7 @@ pub struct WorkerHealthSnapshot { pub last_error: Option, } -/// Host 提供、API 消费的 worker 健康快照来源。 +/// Host 提供、API 消费的 worker 健康快照来源 pub trait WorkerHealthSource: Send + Sync { fn snapshot(&self) -> Vec; } diff --git a/backend/crates/gateway-core/src/identity.rs b/backend/crates/gateway-core/src/identity.rs index 0d06606b7..088ba5744 100644 --- a/backend/crates/gateway-core/src/identity.rs +++ b/backend/crates/gateway-core/src/identity.rs @@ -1,9 +1,9 @@ -//! 编译期 Provider 身份;账号与路由共同使用,不依赖路由计划。 +//! 编译期 Provider 身份;账号与路由共同使用,不依赖路由计划 use crate::validation::{IdentifierError, validate_text}; use std::fmt; -/// 编译进二进制的 Provider adapter slug。 +/// 编译进二进制的 Provider adapter slug #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize)] #[serde(transparent)] pub struct ProviderKind(String); @@ -16,7 +16,7 @@ impl<'de> serde::Deserialize<'de> for ProviderKind { } impl ProviderKind { - /// 校验 Provider slug。 + /// 校验 Provider slug pub fn new(value: impl Into) -> Result { let value = value.into(); validate_text(&value, 64, true, None)?; diff --git a/backend/crates/gateway-core/src/lib.rs b/backend/crates/gateway-core/src/lib.rs index fe634e979..9afb560c8 100644 --- a/backend/crates/gateway-core/src/lib.rs +++ b/backend/crates/gateway-core/src/lib.rs @@ -1,7 +1,8 @@ -//! 多平台 AI 网关的数据面核心。 +//! 多平台 AI 网关的数据面核心 //! -//! 本 crate 只描述协议与 Provider 无关的业务语义。HTTP、数据库、Redis、 -//! 具体客户端协议和具体 Provider 都通过外层 adapter 接入。 +//! 本 crate 只描述协议与 Provider 无关的业务语义 +//! HTTP、数据库、Redis、 +//! 具体客户端协议和具体 Provider 都通过外层 adapter 接入 pub mod account; pub mod concurrency; @@ -47,7 +48,7 @@ use runtime::{ }; use task::{WorkerContribution, WorkerDefinitionError}; -/// Store 提供给数据面 Core 的封闭能力集合。 +/// Store 提供给数据面 Core 的封闭能力集合 #[derive(Clone)] pub struct CoreStorePorts { execution: Arc, @@ -117,7 +118,7 @@ pub struct CoreBundle { worker_contributions: Vec, } -/// 已构造但尚未恢复准入或发现 Provider 目录的数据面启动对象。 +/// 已构造但尚未恢复准入或发现 Provider 目录的数据面启动对象 pub struct CoreStartup { ports: CoreStorePorts, providers: ProviderRegistry, @@ -139,7 +140,7 @@ impl CoreStartup { self.control.snapshot_control() } - /// 绑定组合根依赖后执行 fail-closed 准入恢复与首次目录发现。 + /// 绑定组合根依赖后执行 fail-closed 准入恢复与首次目录发现 pub async fn activate(self) -> Result { restore_client_admission_startup( self.ports.execution.as_ref(), @@ -266,7 +267,7 @@ impl CoreBundle { } } -/// 首个快照与准入恢复均为监听前 fail-closed 屏障。 +/// 首个快照与准入恢复均为监听前 fail-closed 屏障 pub async fn initialize( ports: CoreStorePorts, providers: ProviderRegistry, @@ -289,7 +290,7 @@ pub async fn initialize( .await } -/// 构造 Core 与快照发布能力;首次 Provider 目录发现延迟到 [`CoreStartup::activate`]。 +/// 构造 Core 与快照发布能力;首次 Provider 目录发现延迟到 [`CoreStartup::activate`] #[must_use] pub fn prepare( ports: CoreStorePorts, @@ -312,13 +313,13 @@ pub fn prepare( } } -/// 管理命令只持有快照读写能力,不恢复数据面准入,也不启动执行/结算 Worker。 +/// 管理命令只持有快照读写能力,不恢复数据面准入,也不启动执行/结算 Worker pub struct CoreControlPlaneBundle { snapshots: RuntimeSnapshotHandle, publisher: Arc, } -/// CLI/组合根可先绑定回调端口,再激活首次目录发现。 +/// CLI/组合根可先绑定回调端口,再激活首次目录发现 pub struct CoreControlPlaneStartup { snapshots: RuntimeSnapshotHandle, publisher: Arc, @@ -338,7 +339,7 @@ impl CoreControlPlaneStartup { pub async fn activate(self) -> Result { match self.publisher.refresh().await { Ok(_) => {} - // 故障插件不能封锁管理修复入口;没有可用快照时数据面仍拒绝新请求。 + // 故障插件不能封锁管理修复入口;没有可用快照时数据面仍拒绝新请求 Err( routing::snapshot::RuntimeSnapshotCompileError::ExtensionsUnavailable | routing::snapshot::RuntimeSnapshotCompileError::InvalidExtensionModels, @@ -365,7 +366,7 @@ impl CoreControlPlaneBundle { } /// CLI 命令按需拥有嵌套模型执行与会话亲和查询端口,但不恢复网关启动时的准入租约, -/// 也不产生任何后台 Worker 定义。 +/// 也不产生任何后台 Worker 定义 pub struct CoreCommandPlaneStartup { ports: CoreStorePorts, providers: ProviderRegistry, @@ -438,7 +439,7 @@ impl CoreCommandPlaneBundle { } } -/// 为单次 CLI 命令构造当前快照与窄执行端口;激活时不恢复全局准入状态。 +/// 为单次 CLI 命令构造当前快照与窄执行端口;激活时不恢复全局准入状态 #[must_use] pub fn prepare_command_plane( ports: CoreStorePorts, @@ -469,7 +470,7 @@ pub async fn initialize_control_plane( .await } -/// 仅构造管理命令所需快照能力,不恢复准入、不发现目录且不启动 Worker。 +/// 仅构造管理命令所需快照能力,不恢复准入、不发现目录且不启动 Worker #[must_use] pub fn prepare_control_plane( ports: CoreStorePorts, diff --git a/backend/crates/gateway-core/src/lifecycle.rs b/backend/crates/gateway-core/src/lifecycle.rs index 2cd43d6eb..c89259b24 100644 --- a/backend/crates/gateway-core/src/lifecycle.rs +++ b/backend/crates/gateway-core/src/lifecycle.rs @@ -1,4 +1,4 @@ -//! 请求与连接的截止、租约、取消和 drain 契约。 +//! 请求与连接的截止、租约、取消和 drain 契约 use std::fmt; use std::sync::Arc; @@ -14,7 +14,7 @@ struct CancellationState { event: Event, } -/// 可克隆的请求、任务与连接取消信号。 +/// 可克隆的请求、任务与连接取消信号 #[derive(Clone)] pub struct CancellationToken { state: Arc, @@ -48,7 +48,7 @@ impl CancellationToken { } } - /// 创建只从父级继承取消的子 token;取消子级不会反向取消父请求。 + /// 创建只从父级继承取消的子 token;取消子级不会反向取消父请求 #[must_use] pub fn child_token(&self) -> Self { let ancestors = std::iter::once(Arc::clone(&self.state)) @@ -95,9 +95,9 @@ fn wait_for_state(state: Arc) -> BoxFuture<'static, ()> { if state.cancelled.load(Ordering::Acquire) { return; } - // listener 随等待 future 丢弃而注销,避免 select 败选分支在自身及祖先状态中积累。 + // listener 随等待 future 丢弃而注销,避免 select 败选分支在自身及祖先状态中积累 let listener = state.event.listen(); - // 注册后复查,覆盖取消发生在首次检查与注册之间的竞态。 + // 注册后复查,覆盖取消发生在首次检查与注册之间的竞态 if state.cancelled.load(Ordering::Acquire) { return; } @@ -105,7 +105,7 @@ fn wait_for_state(state: Arc) -> BoxFuture<'static, ()> { }) } -/// 进程已进入 drain,新连接不得再注册。 +/// 进程已进入 drain,新连接不得再注册 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ConnectionDraining; @@ -117,17 +117,17 @@ impl fmt::Display for ConnectionDraining { impl std::error::Error for ConnectionDraining {} -/// 一次成功的活跃连接注册。 +/// 一次成功的活跃连接注册 /// -/// 实现必须在 guard `Drop` 时原子减少活跃连接计数。 +/// 实现必须在 guard `Drop` 时原子减少活跃连接计数 pub trait ConnectionGuard: Send + 'static {} -/// API 消费、Host 实现的进程连接生命周期。 +/// API 消费、Host 实现的进程连接生命周期 pub trait ConnectionLifecycle: Send + Sync { - /// 原子地检查 drain 状态并注册一个活跃连接。 + /// 原子地检查 drain 状态并注册一个活跃连接 /// /// 当本方法成功时,drain 必须等待返回的 guard 被释放; - /// 当 drain 已经线性化生效时,本方法必须返回 [`ConnectionDraining`]。 + /// 当 drain 已经线性化生效时,本方法必须返回 [`ConnectionDraining`] fn try_register(&self) -> Result, ConnectionDraining>; fn cancellation(&self) -> CancellationToken; @@ -135,11 +135,11 @@ pub trait ConnectionLifecycle: Send + Sync { fn is_draining(&self) -> bool; } -/// 请求默认没有总时长限制;显式截止与用于异常回收的可续期租约分开。 +/// 请求默认没有总时长限制;显式截止与用于异常回收的可续期租约分开 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct Deadline(Option); -/// 租约失去续期后有界回收,不作为模型请求的总执行预算。 +/// 租约失去续期后有界回收,不作为模型请求的总执行预算 pub const REQUEST_LEASE_TTL: Duration = Duration::from_secs(10 * 60); impl Deadline { @@ -198,6 +198,6 @@ impl From for Deadline { } } -/// 持有者释放即停止续期,具体执行器由 Store 提供。 +/// 持有者释放即停止续期,具体执行器由 Store 提供 pub trait LeaseGuard: Send + Sync {} impl LeaseGuard for T {} diff --git a/backend/crates/gateway-core/src/metering/mod.rs b/backend/crates/gateway-core/src/metering/mod.rs index e2beac041..d922e535b 100644 --- a/backend/crates/gateway-core/src/metering/mod.rs +++ b/backend/crates/gateway-core/src/metering/mod.rs @@ -1,4 +1,4 @@ -//! 跨 Provider 的标准化用量与单次请求总费用。 +//! 跨 Provider 的标准化用量与单次请求总费用 use std::fmt; use std::str::FromStr; @@ -13,20 +13,20 @@ pub use pricing::{ const DECIMAL_SCALE: u128 = 10_000_000_000; const MAX_SCALED_DECIMAL: u128 = 99_999_999_999_999_999_999; -/// 与 PostgreSQL `numeric(20, 10)` 对齐的非负十进制定点值。 +/// 与 PostgreSQL `numeric(20, 10)` 对齐的非负十进制定点值 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct Decimal(u128); impl Decimal { pub const ZERO: Self = Self(0); - /// 数据库可表示的最大非负金额。 + /// 数据库可表示的最大非负金额 pub const MAX: Self = Self(MAX_SCALED_DECIMAL); - /// 从按十位小数缩放的整数创建。 + /// 从按十位小数缩放的整数创建 /// /// # Errors /// - /// 超出数据库范围时返回错误。 + /// 超出数据库范围时返回错误 pub const fn from_scaled(value: u128) -> Result { if value > MAX_SCALED_DECIMAL { return Err(MeteringError::InvalidDecimal); @@ -39,7 +39,7 @@ impl Decimal { self.0 } - /// 非负金额相减,超支时返回零。 + /// 非负金额相减,超支时返回零 #[must_use] pub const fn saturating_sub(self, other: Self) -> Self { Self(self.0.saturating_sub(other.0)) @@ -53,7 +53,7 @@ impl Decimal { .map(Self) } - /// 除以非零整数,保留最多十位小数。 + /// 除以非零整数,保留最多十位小数 #[must_use] pub fn checked_div_u64(self, divisor: u64) -> Option { let divisor = u128::from(divisor); @@ -63,7 +63,7 @@ impl Decimal { .and_then(|value| Self::from_scaled(value).ok()) } - /// 去尾零的 canonical 字符串,用于 wire 序列化。 + /// 去尾零的 canonical 字符串,用于 wire 序列化 #[must_use] pub fn canonical(self) -> String { let integer = self.0 / DECIMAL_SCALE; @@ -125,16 +125,16 @@ impl fmt::Display for Decimal { } } -/// 三字符大写货币代码。 +/// 三字符大写货币代码 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct CurrencyCode([u8; 3]); impl CurrencyCode { - /// 校验货币代码。 + /// 校验货币代码 /// /// # Errors /// - /// 输入不是三个大写 ASCII 字符时返回错误。 + /// 输入不是三个大写 ASCII 字符时返回错误 pub fn new(value: &str) -> Result { let bytes = value.as_bytes(); if bytes.len() != 3 || !bytes.iter().all(u8::is_ascii_uppercase) { @@ -155,7 +155,7 @@ impl fmt::Display for CurrencyCode { } } -/// 带货币的非负总金额。 +/// 带货币的非负总金额 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Money { amount: Decimal, @@ -189,7 +189,7 @@ impl Money { } } -/// `model_requests` 的公共 Token 事实。 +/// `model_requests` 的公共 Token 事实 #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct Usage { pub input_tokens: Option, @@ -199,7 +199,7 @@ pub struct Usage { pub reasoning_tokens: Option, pub image_input_tokens: Option, pub image_output_tokens: Option, - /// Provider/协议报告的独立事实,不从其他列相加推导。 + /// Provider/协议报告的独立事实,不从其他列相加推导 pub total_tokens: Option, } @@ -218,7 +218,7 @@ impl Usage { } } - /// 合并同一最终上游结果的增量观测;每个字段以较新的非空值为准。 + /// 合并同一最终上游结果的增量观测;每个字段以较新的非空值为准 pub fn merge(&mut self, newer: &Self) { if newer.input_tokens.is_some() { self.input_tokens = newer.input_tokens; @@ -247,7 +247,7 @@ impl Usage { } } -/// 费用金额的来源。 +/// 费用金额的来源 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum CostSource { ProviderReported, @@ -266,7 +266,7 @@ impl CostSource { } } -/// Provider 或受控代码对当次请求总费用的可信程度。 +/// Provider 或受控代码对当次请求总费用的可信程度 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum CostEstimateStatus { Known, @@ -283,7 +283,7 @@ impl CostEstimateStatus { } } -/// 单次模型请求的总费用及当次确定的本地计算明细。 +/// 单次模型请求的总费用及当次确定的本地计算明细 #[derive(Debug, Clone, PartialEq, Eq)] pub struct CostEstimate { status: CostEstimateStatus, @@ -292,25 +292,25 @@ pub struct CostEstimate { breakdown: Option>, } -/// Provider 在单次请求终态上报的实际已计费总额。 +/// Provider 在单次请求终态上报的实际已计费总额 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct ProviderReportedCost { total: Money, } -/// Provider 域依据公开单价和实际用量算出的单次总额。 +/// Provider 域依据公开单价和实际用量算出的单次总额 #[derive(Debug, Clone, PartialEq, Eq)] pub struct CalculatedCost { total: Money, breakdown: Option>, } -/// Provider 受控价格规则计算出的运行时费用明细。 +/// Provider 受控价格规则计算出的运行时费用明细 /// -/// 随本地费用事件保存,以免后续改价改变历史明细。 +/// 随本地费用事件保存,以免后续改价改变历史明细 #[derive(Debug, Clone, PartialEq, Eq)] pub struct CalculatedCostBreakdown { - // Provider 选中的价格区间事实,与服务档位和自定义倍率独立。 + // Provider 选中的价格区间事实,与服务档位和自定义倍率独立 long_context_billing_applied: bool, image: Option, input_amount: Money, @@ -328,7 +328,7 @@ pub struct CalculatedCostBreakdown { custom_multiplier_bps: u32, } -/// 图像输入是总输入的子集,单独保留其价格,不能伪装成文本平均单价。 +/// 图像输入是总输入的子集,单独保留其价格,不能伪装成文本平均单价 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ImageCostBreakdown { pub input_tokens: u64, @@ -339,7 +339,7 @@ pub struct ImageCostBreakdown { pub cache_read_price_per_million: Money, } -/// 一次请求的费用组成,全部使用同一币种。 +/// 一次请求的费用组成,全部使用同一币种 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct CalculatedCostAmounts { input: Money, @@ -371,7 +371,7 @@ impl CalculatedCostAmounts { } } -/// 每百万 Token 的费率组成,全部使用同一币种。 +/// 每百万 Token 的费率组成,全部使用同一币种 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct CalculatedCostRates { input: Money, @@ -514,7 +514,7 @@ impl CalculatedCostBreakdown { self.custom_multiplier_bps } - /// 统一调整金额和有效单价,保留服务档位倍率的独立含义。 + /// 统一调整金额和有效单价,保留服务档位倍率的独立含义 #[must_use] pub fn with_custom_multiplier(mut self, bps: u32) -> Option { if bps > 1_000_000 { @@ -560,7 +560,7 @@ impl CalculatedCostBreakdown { self.cache_read_amount = scale(self.cache_read_amount)?; self.cache_write_amount = scale(self.cache_write_amount)?; self.standard_amount = scale(self.standard_amount)?; - // 各费用项独立四舍五入后求和,防止明细合计与账本金额相差一个 tick。 + // 各费用项独立四舍五入后求和,防止明细合计与账本金额相差一个 tick let mut total = self .input_amount .amount() @@ -589,11 +589,11 @@ impl CalculatedCostBreakdown { } impl ProviderReportedCost { - /// xAI 等 Provider 的 USD ticks 可直接传入;1 USD = 10^10 ticks。 + /// xAI 等 Provider 的 USD ticks 可直接传入;1 USD = 10^10 ticks /// /// # Errors /// - /// ticks 超出数据库 `numeric(20, 10)` 范围时失败。 + /// ticks 超出数据库 `numeric(20, 10)` 范围时失败 pub fn from_usd_ticks(ticks: u128) -> Result { Ok(Self { total: Money::new(Decimal::from_scaled(ticks)?, CurrencyCode(*b"USD")), @@ -617,11 +617,11 @@ impl ProviderReportedCost { } impl CalculatedCost { - /// 从精确 USD ticks 创建本地计算费用;1 USD = 10^10 ticks。 + /// 从精确 USD ticks 创建本地计算费用;1 USD = 10^10 ticks /// /// # Errors /// - /// ticks 超出数据库 `numeric(20, 10)` 范围时失败。 + /// ticks 超出数据库 `numeric(20, 10)` 范围时失败 pub fn from_usd_ticks(ticks: u128) -> Result { Ok(Self { total: Money::new(Decimal::from_scaled(ticks)?, CurrencyCode(*b"USD")), diff --git a/backend/crates/gateway-core/src/metering/pricing.rs b/backend/crates/gateway-core/src/metering/pricing.rs index ea21a9917..12d790159 100644 --- a/backend/crates/gateway-core/src/metering/pricing.rs +++ b/backend/crates/gateway-core/src/metering/pricing.rs @@ -1,4 +1,4 @@ -//! 请求冻结的本地价格覆盖;默认价目及模型别名继续由 Provider 拥有。 +//! 请求冻结的本地价格覆盖;默认价目及模型别名继续由 Provider 拥有 use std::collections::BTreeMap; use std::str::FromStr; @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use super::Decimal; -/// 每百万 Token 的 USD 价格,保留四位小数以对齐单 Token 的金额精度。 +/// 每百万 Token 的 USD 价格,保留四位小数以对齐单 Token 的金额精度 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(try_from = "String", into = "String")] pub struct TokenPrice(Decimal); @@ -37,7 +37,7 @@ impl From for String { } } -/// 一档完整 Token 单价;零价格与未覆盖的档位有不同含义。 +/// 一档完整 Token 单价;零价格与未覆盖的档位有不同含义 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct TokenPriceOverride { @@ -47,7 +47,7 @@ pub struct TokenPriceOverride { pub cache_write: TokenPrice, } -/// 一个精确模型的覆盖项;倍率使用基点,10000 表示 1 倍。 +/// 一个精确模型的覆盖项;倍率使用基点,10000 表示 1 倍 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct ModelPriceOverride { @@ -56,10 +56,10 @@ pub struct ModelPriceOverride { } impl ModelPriceOverride { - /// 校验存储与管理入口共用的价格约束。 + /// 校验存储与管理入口共用的价格约束 /// /// # Errors - /// 倍率超过 100 倍或档位不受支持时返回错误。 + /// 倍率超过 100 倍或档位不受支持时返回错误 pub fn validate(&self) -> Result<(), &'static str> { if self.multiplier_bps > 1_000_000 { return Err("倍率必须在 0 至 100 倍之间"); @@ -82,10 +82,10 @@ impl ModelPriceOverride { } } -/// 按 Provider、上游模型索引的不可变覆盖事实。 +/// 按 Provider、上游模型索引的不可变覆盖事实 pub type PricingOverrides = BTreeMap>; -/// 按档位叠加配置;倍率仅由人工覆盖控制,同步价目不携带业务倍率。 +/// 按档位叠加配置;倍率仅由人工覆盖控制,同步价目不携带业务倍率 pub fn merge_pricing(mut base: PricingOverrides, overrides: &PricingOverrides) -> PricingOverrides { for (provider, models) in overrides { let target = base.entry(provider.clone()).or_default(); diff --git a/backend/crates/gateway-core/src/middleware/http/mod.rs b/backend/crates/gateway-core/src/middleware/http/mod.rs index f4ce63e85..7743556a9 100644 --- a/backend/crates/gateway-core/src/middleware/http/mod.rs +++ b/backend/crates/gateway-core/src/middleware/http/mod.rs @@ -1,4 +1,4 @@ -//! HTTP 中间件的值与流合同;不包含服务器、客户端或业务路由实现。 +//! HTTP 中间件的值与流合同;不包含服务器、客户端或业务路由实现 pub mod upgrade; @@ -9,13 +9,13 @@ use http_body_util::{BodyExt as _, Empty, combinators::UnsyncBoxBody}; use crate::{engine::middleware::MiddlewareError, lifecycle::CancellationToken}; -/// 正文保留数据帧、trailers 和背压;不要求完整读取或重新编码。 +/// 正文保留数据帧、trailers 和背压;不要求完整读取或重新编码 pub type Body = UnsyncBoxBody>; pub type Request = http::Request; pub type Response = http::Response; pub type Next = super::Next; -/// 路由前调用没有 Client Key;身份在默认认证流程中产生。 +/// 路由前调用没有 Client Key;身份在默认认证流程中产生 #[derive(Clone, Debug)] pub struct Context { pub request_id: String, @@ -27,7 +27,7 @@ pub struct Context { pub cancellation: CancellationToken, } -/// 内部子请求复用 API 总路由;端口不解释业务路径,也不经网络回环。 +/// 内部子请求复用 API 总路由;端口不解释业务路径,也不经网络回环 pub trait Dispatcher: Send + Sync { fn request_settings(&self) -> Option { None @@ -40,7 +40,7 @@ pub trait Dispatcher: Send + Sync { ) -> futures::future::BoxFuture<'static, Result>; } -/// 宿主解析后放入请求 extensions,插件显式覆盖后由终端消费。 +/// 宿主解析后放入请求 extensions,插件显式覆盖后由终端消费 #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct Settings { pub timeout: Option, diff --git a/backend/crates/gateway-core/src/middleware/http/upgrade.rs b/backend/crates/gateway-core/src/middleware/http/upgrade.rs index 2c127b7af..e29829170 100644 --- a/backend/crates/gateway-core/src/middleware/http/upgrade.rs +++ b/backend/crates/gateway-core/src/middleware/http/upgrade.rs @@ -1,4 +1,4 @@ -//! 升级保留 HTTP 返回路径;会话任务由最外层传输在提交握手时接管。 +//! 升级保留 HTTP 返回路径;会话任务由最外层传输在提交握手时接管 use std::sync::{Arc, Mutex}; @@ -10,7 +10,7 @@ pub type PendingSession = Shared, Arc>>>; pub type SessionTask = BoxFuture<'static, Result<(), MiddlewareError>>; -/// 具体服务器负责握手与 socket;Core 不依赖 HTTP 服务器实现。 +/// 具体服务器负责握手与 socket;Core 不依赖 HTTP 服务器实现 pub trait WebSocketUpgrade: Send + Sync { fn accept( &self, @@ -19,7 +19,7 @@ pub trait WebSocketUpgrade: Send + Sync { ) -> BoxFuture<'static, Result<(super::Response, PendingSession), MiddlewareError>>; } -/// 响应被替换或丢弃时任务随之释放,不启动脱离请求的插件调用。 +/// 响应被替换或丢弃时任务随之释放,不启动脱离请求的插件调用 #[derive(Clone)] pub struct Upgraded { pub status: http::StatusCode, diff --git a/backend/crates/gateway-core/src/middleware/mod.rs b/backend/crates/gateway-core/src/middleware/mod.rs index 08da76e45..7e8b3b818 100644 --- a/backend/crates/gateway-core/src/middleware/mod.rs +++ b/backend/crates/gateway-core/src/middleware/mod.rs @@ -1,4 +1,4 @@ -//! 与协议无关的洋葱组合器;输入、结果与错误保留调用方的领域类型。 +//! 与协议无关的洋葱组合器;输入、结果与错误保留调用方的领域类型 pub mod http; pub mod service; @@ -6,7 +6,7 @@ pub mod websocket; use futures::future::BoxFuture; -/// 单次调用中的一层;上下文放在类型化输入中或由闭包捕获。 +/// 单次调用中的一层;上下文放在类型化输入中或由闭包捕获 pub type Middleware = Box< dyn FnOnce(Input, Next) -> BoxFuture<'static, Result> + Send, @@ -15,16 +15,16 @@ pub type Middleware = Box< type Terminal = Box BoxFuture<'static, Result> + Send>; -/// 消费自身后进入下一层,所有权保证同一个续体不能运行两次。 +/// 消费自身后进入下一层,所有权保证同一个续体不能运行两次 /// -/// 返回流或会话时,资源寿命由结果对象持有,组合器不后台驱动或缓冲结果。 +/// 返回流或会话时,资源寿命由结果对象持有,组合器不后台驱动或缓冲结果 pub struct Next { layers: std::vec::IntoIter>, terminal: Terminal, } impl Next { - /// 直接返回下一层 future,不启动与父调用脱离的任务。 + /// 直接返回下一层 future,不启动与父调用脱离的任务 pub fn run(mut self, input: Input) -> BoxFuture<'static, Result> { match self.layers.next() { Some(layer) => layer(input, self), @@ -33,9 +33,9 @@ impl Next { } } -/// 按给定顺序进入中间件,响应与错误沿等待栈逆序返回。 +/// 按给定顺序进入中间件,响应与错误沿等待栈逆序返回 /// -/// 空链直接调用终端;输入不需要 Clone,不经过 JSON 或字符串操作分派。 +/// 空链直接调用终端;输入不需要 Clone,不经过 JSON 或字符串操作分派 pub fn compose( layers: Vec>, terminal: impl FnOnce(Input) -> BoxFuture<'static, Result> + Send + 'static, diff --git a/backend/crates/gateway-core/src/middleware/service.rs b/backend/crates/gateway-core/src/middleware/service.rs index b5c5931c0..8e4d55500 100644 --- a/backend/crates/gateway-core/src/middleware/service.rs +++ b/backend/crates/gateway-core/src/middleware/service.rs @@ -1,11 +1,11 @@ -//! 公开服务的跨进程组合边界;原生调用仍使用所属领域的输入输出类型。 +//! 公开服务的跨进程组合边界;原生调用仍使用所属领域的输入输出类型 use crate::{ engine::{extensions::ExtensionCallScope, middleware::FrozenMiddlewarePlan}, lifecycle::CancellationToken, }; -/// 值只在进入插件链时编码;操作的领域校验与副作用由终端所属服务执行。 +/// 值只在进入插件链时编码;操作的领域校验与副作用由终端所属服务执行 pub type Value = serde_json::Value; pub type Next = super::Next; @@ -38,7 +38,7 @@ impl Error { } } -/// 子服务继承同一发布计划及取消信号;防递归集合不承担字段或权限过滤。 +/// 子服务继承同一发布计划及取消信号;防递归集合不承担字段或权限过滤 #[derive(Clone, Debug)] pub struct Context { pub operation: &'static str, diff --git a/backend/crates/gateway-core/src/middleware/websocket.rs b/backend/crates/gateway-core/src/middleware/websocket.rs index 98e1f5efa..16fc1cb38 100644 --- a/backend/crates/gateway-core/src/middleware/websocket.rs +++ b/backend/crates/gateway-core/src/middleware/websocket.rs @@ -1,4 +1,4 @@ -//! 双工消息边界,不解析消息正文或第三方控制协议。 +//! 双工消息边界,不解析消息正文或第三方控制协议 use crate::{ engine::middleware::{MiddlewareError, MiddlewareHeader}, lifecycle::CancellationToken, @@ -26,11 +26,11 @@ pub struct Message { pub payload: Bytes, } -/// 等待实际 transport 写入;与接收端分开持有,不在插件调用期间锁住 writer。 +/// 等待实际 transport 写入;与接收端分开持有,不在插件调用期间锁住 writer pub trait Sender: Send + Sync { fn send(&self, message: Message) -> BoxFuture<'_, Result<(), MiddlewareError>>; } -/// 一个接收端和独立发送端;接收等待不能阻塞发送。 +/// 一个接收端和独立发送端;接收等待不能阻塞发送 pub trait Session: Sender { fn receive(&self) -> BoxFuture<'_, Result, MiddlewareError>>; } diff --git a/backend/crates/gateway-core/src/operation.rs b/backend/crates/gateway-core/src/operation.rs index b1387d8ac..0fb0fc4aa 100644 --- a/backend/crates/gateway-core/src/operation.rs +++ b/backend/crates/gateway-core/src/operation.rs @@ -1,8 +1,9 @@ -//! 协议无关的业务 operation。 +//! 协议无关的业务 operation //! -//! 这里只保留网关需要解释、路由或结算的稳定语义。协议 adapter 无法写入 +//! 这里只保留网关需要解释、路由或结算的稳定语义 +//! 协议 adapter 无法写入 //! wire body 与连接级事实随 [`ProtocolPayload`] 或 [`RawJsonPayload`] 不透明传递, -//! 由对应 Provider 自己解释。 +//! 由对应 Provider 自己解释 use std::collections::BTreeSet; use std::fmt; @@ -14,24 +15,24 @@ use serde_json::{Map, Value}; use crate::validation::{OperationError, validate_text}; -/// 网关支持的稳定 operation 分类。 +/// 网关支持的稳定 operation 分类 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[non_exhaustive] pub enum OperationKind { - /// 文本、多模态和工具生成。 + /// 文本、多模态和工具生成 Generate, - /// 图像生成。 + /// 图像生成 GenerateImage, - /// Provider 原生 standalone search。 + /// Provider 原生 standalone search Search, - /// 使用目标模型的真实 tokenizer 计数,不允许 Core 估算。 + /// 使用目标模型的真实 tokenizer 计数,不允许 Core 估算 CountTokens, - /// Provider 声明并映射到固定上游目标的账号认证 HTTP 操作。 + /// Provider 声明并映射到固定上游目标的账号认证 HTTP 操作 ProviderHttp, } impl OperationKind { - /// 返回注册表和持久化使用的稳定名称。 + /// 返回注册表和持久化使用的稳定名称 #[must_use] pub const fn as_str(self) -> &'static str { match self { @@ -44,23 +45,23 @@ impl OperationKind { } } -/// Router 理解的稳定能力。 +/// Router 理解的稳定能力 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[non_exhaustive] pub enum Feature { - /// Tool calling。 + /// Tool calling Tools, - /// 图像输入。 + /// 图像输入 Vision, - /// 推理控制或推理输出。 + /// 推理控制或推理输出 Reasoning, - /// JSON Schema 输出。 + /// JSON Schema 输出 JsonSchema, - /// Provider 原生延续。 + /// Provider 原生延续 NativeContinuation, } -/// 从 operation 推导出的请求能力约束。 +/// 从 operation 推导出的请求能力约束 #[derive(Debug, Clone, PartialEq, Eq)] pub struct CapabilityRequirements { operation: OperationKind, @@ -69,7 +70,7 @@ pub struct CapabilityRequirements { } impl CapabilityRequirements { - /// 创建仅要求 operation 的能力约束。 + /// 创建仅要求 operation 的能力约束 #[must_use] pub fn new(operation: OperationKind) -> Self { Self { @@ -79,43 +80,43 @@ impl CapabilityRequirements { } } - /// 增加稳定能力要求。 + /// 增加稳定能力要求 #[must_use] pub fn require(mut self, feature: Feature) -> Self { self.features.insert(feature); self } - /// 设置请求的最大输出 token 数。 + /// 设置请求的最大输出 token 数 #[must_use] pub const fn with_requested_output_tokens(mut self, tokens: Option) -> Self { self.requested_output_tokens = tokens; self } - /// 返回 operation 分类。 + /// 返回 operation 分类 #[must_use] pub const fn operation(&self) -> OperationKind { self.operation } - /// 返回全部稳定能力要求。 + /// 返回全部稳定能力要求 #[must_use] pub fn features(&self) -> &BTreeSet { &self.features } - /// 返回请求的最大输出 token 数。 + /// 返回请求的最大输出 token 数 #[must_use] pub const fn requested_output_tokens(&self) -> Option { self.requested_output_tokens } } -/// 同一客户端连接内由 Provider 生成并解释的不透明会话状态。 +/// 同一客户端连接内由 Provider 生成并解释的不透明会话状态 /// /// Core 只在重试和路由过程中保持该值;协议层只能把 Provider 返回的状态原样带入 -/// 下一轮,不能读取或改写正文。 +/// 下一轮,不能读取或改写正文 #[derive(Clone, PartialEq, Serialize, Deserialize)] pub struct ProviderSessionState { provider: String, @@ -124,7 +125,7 @@ pub struct ProviderSessionState { extension_owner: Option>, } -/// 受管扩展会话的宿主归属,不能把插件私有状态交给原生 Provider 解码。 +/// 受管扩展会话的宿主归属,不能把插件私有状态交给原生 Provider 解码 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ExtensionSessionOwner { @@ -137,11 +138,11 @@ pub struct ExtensionSessionOwner { } impl ProviderSessionState { - /// 创建 Provider 私有会话状态。 + /// 创建 Provider 私有会话状态 /// /// # Errors /// - /// Provider 名称无效时返回错误。 + /// Provider 名称无效时返回错误 pub fn new( provider: impl Into, payload: Map, @@ -189,10 +190,11 @@ impl fmt::Debug for ProviderSessionState { } } -/// 客户端协议交给 Provider 的不透明 JSON object。 +/// 客户端协议交给 Provider 的不透明 JSON object /// -/// Core 只负责在路由和重试过程中保持该值,不读取或改写正文。协议 adapter -/// 与对应 Provider 共同拥有其语义。 +/// Core 只负责在路由和重试过程中保持该值,不读取或改写正文 +/// 协议 adapter +/// 与对应 Provider 共同拥有其语义 #[derive(Clone, PartialEq)] pub struct ProtocolPayload { protocol: String, @@ -201,11 +203,11 @@ pub struct ProtocolPayload { } impl ProtocolPayload { - /// 创建协议不透明正文。 + /// 创建协议不透明正文 /// /// # Errors /// - /// 协议名称为空、过长或含控制字符时返回错误。 + /// 协议名称为空、过长或含控制字符时返回错误 pub fn json_object( protocol: impl Into, body: Map, @@ -221,28 +223,28 @@ impl ProtocolPayload { }) } - /// 返回协议名称。 + /// 返回协议名称 #[must_use] pub fn protocol(&self) -> &str { &self.protocol } - /// 返回仅供对应 Provider 解释的 JSON object。 + /// 返回仅供对应 Provider 解释的 JSON object #[must_use] pub const fn body(&self) -> &Map { &self.body } - /// 附着不写入 wire body 的协议连接上下文。 + /// 附着不写入 wire body 的协议连接上下文 /// - /// Core 只保留该值;对应 Provider 可以读取已知键,未知键必须忽略。 + /// Core 只保留该值;对应 Provider 可以读取已知键,未知键必须忽略 #[must_use] pub fn with_context(mut self, context: Map) -> Self { self.context = context; self } - /// 返回仅供对应 Provider 解释的非 wire 上下文。 + /// 返回仅供对应 Provider 解释的非 wire 上下文 #[must_use] pub const fn context(&self) -> &Map { &self.context @@ -260,10 +262,10 @@ impl fmt::Debug for ProtocolPayload { } } -/// 协议 adapter 与 Provider 之间不透明传递的 JSON 正文。 +/// 协议 adapter 与 Provider 之间不透明传递的 JSON 正文 /// /// Core 保留字节与非 wire 上下文,不验证、解析或改写正文; -/// 请求载荷与原生模型目录均由协议 adapter 和对应 Provider 共同拥有语义。 +/// 请求载荷与原生模型目录均由协议 adapter 和对应 Provider 共同拥有语义 #[derive(Clone, PartialEq, Eq)] pub struct RawJsonPayload { protocol: String, @@ -273,11 +275,11 @@ pub struct RawJsonPayload { } impl RawJsonPayload { - /// 创建不透明的协议 JSON 正文。 + /// 创建不透明的协议 JSON 正文 /// /// # Errors /// - /// 协议名称为空、过长或含控制字符时返回错误。 + /// 协议名称为空、过长或含控制字符时返回错误 pub fn new(protocol: impl Into, body: Bytes) -> Result { let protocol = protocol.into(); validate_text(&protocol, 64, true, None).map_err(|_| OperationError::EmptyField { @@ -291,26 +293,26 @@ impl RawJsonPayload { }) } - /// 返回协议名称。 + /// 返回协议名称 #[must_use] pub fn protocol(&self) -> &str { &self.protocol } - /// 返回协议正文的 JSON 字节。 + /// 返回协议正文的 JSON 字节 #[must_use] pub const fn body(&self) -> &Bytes { &self.body } - /// 附着不写入 wire body 的协议连接上下文。 + /// 附着不写入 wire body 的协议连接上下文 #[must_use] pub fn with_context(mut self, context: Map) -> Self { self.context = context; self } - /// 返回仅供对应 Provider 解释的非 wire 上下文。 + /// 返回仅供对应 Provider 解释的非 wire 上下文 #[must_use] pub const fn context(&self) -> &Map { &self.context @@ -328,10 +330,11 @@ impl fmt::Debug for RawJsonPayload { } } -/// Provider HTTP 端点使用的不透明字节正文。 +/// Provider HTTP 端点使用的不透明字节正文 /// -/// 与 [`RawJsonPayload`] 不同,这里不要求正文是 JSON。Core 只保留字节和 -/// 非 wire 上下文,最终 origin、路径与账号认证仍由 Provider 决定。 +/// 与 [`RawJsonPayload`] 不同,这里不要求正文是 JSON +/// Core 只保留字节和 +/// 非 wire 上下文,最终 origin、路径与账号认证仍由 Provider 决定 #[derive(Clone, PartialEq, Eq)] pub struct RawHttpPayload { protocol: String, @@ -341,11 +344,11 @@ pub struct RawHttpPayload { } impl RawHttpPayload { - /// 创建不透明 HTTP 正文。 + /// 创建不透明 HTTP 正文 /// /// # Errors /// - /// 协议名称为空、过长或含控制字符时返回错误。 + /// 协议名称为空、过长或含控制字符时返回错误 pub fn new(protocol: impl Into, body: Bytes) -> Result { let protocol = protocol.into(); validate_text(&protocol, 64, true, None).map_err(|_| OperationError::EmptyField { @@ -392,7 +395,7 @@ impl fmt::Debug for RawHttpPayload { } } -/// 通用生成请求。 +/// 通用生成请求 #[derive(Clone, PartialEq)] pub struct GenerateRequest { payload: Arc, @@ -407,10 +410,10 @@ struct GeneratePayload { } impl GenerateRequest { - /// 创建携带协议不透明正文的生成请求。 + /// 创建携带协议不透明正文的生成请求 /// /// 完整请求只以原始 wire object 保存;Core 仅局部只读已知字段以推导路由能力, - /// Provider 负责解释与转换正文。 + /// Provider 负责解释与转换正文 #[must_use] pub fn from_protocol_payload(protocol_payload: ProtocolPayload) -> Self { Self { @@ -423,14 +426,15 @@ impl GenerateRequest { } } - /// 写回原生 Provider 编码结果,保留已准入的能力、连接上下文和会话状态。 + /// 写回原生 Provider 编码结果,保留已准入的能力、连接上下文和会话状态 /// /// 原生编码不消耗中间件的一次跨协议改写额度:可以保留协议名,也可以在一次 - /// 中间件改写后编码到 Provider wire。调用方仍须验证保护字段没有被改动。 + /// 中间件改写后编码到 Provider wire + /// 调用方仍须验证保护字段没有被改动 /// /// # Errors /// - /// 目标协议名无效时返回错误。 + /// 目标协议名无效时返回错误 pub fn with_native_encoded_body( mut self, protocol: impl Into, @@ -445,19 +449,19 @@ impl GenerateRequest { Ok(self) } - /// 附着同一客户端连接上一轮由 Provider 返回的不透明状态。 + /// 附着同一客户端连接上一轮由 Provider 返回的不透明状态 #[must_use] pub fn with_provider_session_state(mut self, state: ProviderSessionState) -> Self { self.set_provider_session_state(state); self } - /// 原地附着会话状态;payload 独占时不复制正文。 + /// 原地附着会话状态;payload 独占时不复制正文 pub fn set_provider_session_state(&mut self, state: ProviderSessionState) { Arc::make_mut(&mut self.payload).provider_session_state = Some(state); } - /// 返回最大输出 token 数。 + /// 返回最大输出 token 数 #[must_use] pub fn max_output_tokens(&self) -> Option { self.body() @@ -466,7 +470,7 @@ impl GenerateRequest { .filter(|tokens| *tokens > 0) } - /// 返回客户端提供的 prompt cache 路由键。 + /// 返回客户端提供的 prompt cache 路由键 #[must_use] pub fn prompt_cache_key(&self) -> Option<&str> { self.body() @@ -475,7 +479,7 @@ impl GenerateRequest { .filter(|key| !key.trim().is_empty()) } - /// 返回原始请求是否要求 Provider 原生 continuation。 + /// 返回原始请求是否要求 Provider 原生 continuation #[must_use] pub fn native_continuation_requested(&self) -> bool { self.body() @@ -484,7 +488,7 @@ impl GenerateRequest { .is_some() } - /// 返回客户端是否请求了图片生成工具。 + /// 返回客户端是否请求了图片生成工具 #[must_use] pub fn image_generation_requested(&self) -> bool { self.body() @@ -497,7 +501,7 @@ impl GenerateRequest { }) } - /// 返回指定 Provider 的连接内会话状态。 + /// 返回指定 Provider 的连接内会话状态 #[must_use] pub fn provider_session_state(&self, provider: &str) -> Option<&ProviderSessionState> { self.payload @@ -506,7 +510,7 @@ impl GenerateRequest { .filter(|state| state.provider() == provider) } - /// 返回协议不透明正文。 + /// 返回协议不透明正文 #[must_use] pub fn protocol_payload(&self) -> &ProtocolPayload { &self.payload.protocol_payload @@ -593,16 +597,16 @@ fn contains_type(value: Option<&Value>, expected: &str) -> bool { } } -/// 图像 API 的稳定端点语义。 +/// 图像 API 的稳定端点语义 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ImageRequestKind { - /// 从文本生成图像。 + /// 从文本生成图像 Generation, - /// 使用输入图像执行编辑。 + /// 使用输入图像执行编辑 Edit, } -/// 图像生成或编辑请求。 +/// 图像生成或编辑请求 #[derive(Clone, PartialEq)] pub struct ImageRequest { kind: ImageRequestKind, @@ -610,19 +614,19 @@ pub struct ImageRequest { } impl ImageRequest { - /// 创建携带原始协议 JSON 正文的图像请求。 + /// 创建携带原始协议 JSON 正文的图像请求 #[must_use] pub const fn from_raw_json(kind: ImageRequestKind, payload: RawJsonPayload) -> Self { Self { kind, payload } } - /// 返回图像 API 端点语义。 + /// 返回图像 API 端点语义 #[must_use] pub const fn kind(&self) -> ImageRequestKind { self.kind } - /// 返回未经重编码的协议 JSON 正文。 + /// 返回未经重编码的协议 JSON 正文 #[must_use] pub const fn payload(&self) -> &RawJsonPayload { &self.payload @@ -639,20 +643,20 @@ impl fmt::Debug for ImageRequest { } } -/// Provider 原生 standalone search 请求。 +/// Provider 原生 standalone search 请求 #[derive(Clone, PartialEq, Eq)] pub struct StandaloneSearchRequest { payload: RawJsonPayload, } impl StandaloneSearchRequest { - /// 创建携带原始协议 JSON 正文的 standalone search 请求。 + /// 创建携带原始协议 JSON 正文的 standalone search 请求 #[must_use] pub const fn from_raw_json(payload: RawJsonPayload) -> Self { Self { payload } } - /// 返回未经重编码的协议 JSON 正文。 + /// 返回未经重编码的协议 JSON 正文 #[must_use] pub const fn payload(&self) -> &RawJsonPayload { &self.payload @@ -668,7 +672,7 @@ impl fmt::Debug for StandaloneSearchRequest { } } -/// 使用目标模型的 Provider 原生 Token 计数请求。 +/// 使用目标模型的 Provider 原生 Token 计数请求 #[derive(Clone, PartialEq, Eq)] pub struct TokenCountRequest { payload: RawJsonPayload, @@ -695,7 +699,7 @@ impl fmt::Debug for TokenCountRequest { } } -/// API 可公开的 Provider HTTP method 集合;不自动包含 `HEAD` 或任意 method。 +/// API 可公开的 Provider HTTP method 集合;不自动包含 `HEAD` 或任意 method #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ProviderHttpMethod { Get, @@ -712,7 +716,7 @@ impl ProviderHttpMethod { } } -/// 已由 API 解析的单个 HTTP header;值不会出现在 `Debug`。 +/// 已由 API 解析的单个 HTTP header;值不会出现在 `Debug` #[derive(Clone, PartialEq, Eq)] pub struct ProviderHttpHeader { name: String, @@ -749,7 +753,7 @@ impl fmt::Debug for ProviderHttpHeader { } } -/// Provider 显式声明的账号认证 HTTP 操作。 +/// Provider 显式声明的账号认证 HTTP 操作 #[derive(Clone, PartialEq, Eq)] pub struct ProviderHttpRequest { endpoint: String, @@ -762,7 +766,7 @@ pub struct ProviderHttpRequest { impl ProviderHttpRequest { /// # Errors /// - /// endpoint 不是安全符号名,query 或 headers 超过边界时返回错误。 + /// endpoint 不是安全符号名,query 或 headers 超过边界时返回错误 pub fn new( endpoint: impl Into, method: ProviderHttpMethod, @@ -856,19 +860,19 @@ impl fmt::Debug for ProviderHttpRequest { } } -/// 网关内部业务请求;不包含任何客户端 wire 或 Provider SDK 类型。 +/// 网关内部业务请求;不包含任何客户端 wire 或 Provider SDK 类型 #[derive(Clone, PartialEq)] #[non_exhaustive] pub enum Operation { - /// 生成。 + /// 生成 Generate(GenerateRequest), - /// 图像生成。 + /// 图像生成 GenerateImage(ImageRequest), - /// Provider 原生 standalone search。 + /// Provider 原生 standalone search Search(StandaloneSearchRequest), - /// 使用目标模型进行真实 Token 计数。 + /// 使用目标模型进行真实 Token 计数 CountTokens(TokenCountRequest), - /// Provider 显式登记的账号认证 HTTP 操作。 + /// Provider 显式登记的账号认证 HTTP 操作 ProviderHttp(ProviderHttpRequest), } @@ -886,11 +890,11 @@ impl Operation { Ok(Self::Generate(request)) } - /// 把协议正文编码为中间件可见的原始字节;不包含非 wire context 或会话状态。 + /// 把协议正文编码为中间件可见的原始字节;不包含非 wire context 或会话状态 /// /// # Errors /// - /// 生成请求的 JSON object 无法编码时返回错误。 + /// 生成请求的 JSON object 无法编码时返回错误 pub fn middleware_body(&self) -> Result { match self { Self::Generate(request) => serde_json::to_vec(request.protocol_payload().body()) @@ -905,14 +909,14 @@ impl Operation { } } - /// 写回中间件返回的协议与正文,同时保留宿主持有的非 wire context。 + /// 写回中间件返回的协议与正文,同时保留宿主持有的非 wire context /// /// 同协议是正文替换;跨协议只能使用既有 direct-once 转换边界,不能借中间件 - /// 绕过重复转换保护。 + /// 绕过重复转换保护 /// /// # Errors /// - /// 协议名或正文无效,或请求已经发生过一次跨协议转换时返回错误。 + /// 协议名或正文无效,或请求已经发生过一次跨协议转换时返回错误 pub fn replace_middleware_wire( self, protocol: impl Into, @@ -933,11 +937,12 @@ impl Operation { } /// 替换协议正文和非 wire 上下文,同时保留 operation 类别、Provider 会话状态与 - /// 原协议名称。`body=None` 表示只改上下文,避免无加工路径重新编码正文。 + /// 原协议名称 + /// `body=None` 表示只改上下文,避免无加工路径重新编码正文 /// /// # Errors /// - /// Generate 的替换正文不是 JSON object 时返回错误;原始 JSON 端点继续按字节保存。 + /// Generate 的替换正文不是 JSON object 时返回错误;原始 JSON 端点继续按字节保存 pub fn replace_protocol_wire( self, body: Option, @@ -947,12 +952,13 @@ impl Operation { } /// 替换 Provider HTTP 正文、非 wire 上下文和已解析 headers,同时保持 endpoint、 - /// method 与 query 不变。headers 继续由 [`ProviderHttpRequest::new`] 统一验证; - /// Core 不解析插件传输使用的 base64 表示。 + /// method 与 query 不变 + /// headers 继续由 [`ProviderHttpRequest::new`] 统一验证; + /// Core 不解析插件传输使用的 base64 表示 /// /// # Errors /// - /// 当前 operation 不是 Provider HTTP,或替换后的 headers 超出请求边界时返回错误。 + /// 当前 operation 不是 Provider HTTP,或替换后的 headers 超出请求边界时返回错误 pub fn replace_provider_http_wire( self, body: Option, @@ -981,11 +987,12 @@ impl Operation { } /// 把一次已选择的协议转换结果写回 operation,同时保留非 wire 上下文和 - /// Provider 会话状态。该方法只验证公共载荷形状,不解释目标协议字段。 + /// Provider 会话状态 + /// 该方法只验证公共载荷形状,不解释目标协议字段 /// /// # Errors /// - /// 目标协议名无效,或 Generate 的目标正文不是 JSON object 时返回错误。 + /// 目标协议名无效,或 Generate 的目标正文不是 JSON object 时返回错误 pub fn translate_protocol_wire( self, target_protocol: impl Into, @@ -1110,7 +1117,7 @@ impl Operation { } } - /// 返回协议 adapter 持有的不透明格式标识。 + /// 返回协议 adapter 持有的不透明格式标识 #[must_use] pub fn protocol(&self) -> &str { match self { @@ -1122,7 +1129,7 @@ impl Operation { } } - /// 附着协议连接持有的 Provider 私有状态;非生成 operation 保持不变。 + /// 附着协议连接持有的 Provider 私有状态;非生成 operation 保持不变 #[must_use] pub fn with_provider_session_state(self, state: ProviderSessionState) -> Self { match self { @@ -1131,14 +1138,14 @@ impl Operation { } } - /// 原地附着 Provider 私有状态;payload 独占时不复制正文。 + /// 原地附着 Provider 私有状态;payload 独占时不复制正文 pub fn set_provider_session_state(&mut self, state: ProviderSessionState) { if let Self::Generate(request) = self { request.set_provider_session_state(state); } } - /// 返回稳定 operation 分类。 + /// 返回稳定 operation 分类 #[must_use] pub const fn kind(&self) -> OperationKind { match self { @@ -1150,7 +1157,7 @@ impl Operation { } } - /// 推导 Router 使用的能力要求。 + /// 推导 Router 使用的能力要求 #[must_use] pub fn capability_requirements(&self) -> CapabilityRequirements { match self { @@ -1162,7 +1169,7 @@ impl Operation { } } - /// 返回该 operation 是否代表一次图片生成请求。 + /// 返回该 operation 是否代表一次图片生成请求 #[must_use] pub fn image_generation_requested(&self) -> bool { match self { @@ -1172,7 +1179,7 @@ impl Operation { } } - /// 返回当前 Provider 的连接内私有状态。 + /// 返回当前 Provider 的连接内私有状态 #[must_use] pub fn provider_session_state(&self, provider: &str) -> Option<&ProviderSessionState> { match self { diff --git a/backend/crates/gateway-core/src/policy/client_version.rs b/backend/crates/gateway-core/src/policy/client_version.rs index 1fee156c1..cd27745b9 100644 --- a/backend/crates/gateway-core/src/policy/client_version.rs +++ b/backend/crates/gateway-core/src/policy/client_version.rs @@ -1,4 +1,4 @@ -//! Codex 客户端最低版本策略。 +//! Codex 客户端最低版本策略 use std::fmt; @@ -6,7 +6,7 @@ use semver::Version; const MAXIMUM_VERSION_LENGTH: usize = 64; -/// 可被网关识别并限制最低版本的 Codex 客户端。 +/// 可被网关识别并限制最低版本的 Codex 客户端 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CodexClientKind { Desktop, @@ -23,12 +23,12 @@ impl CodexClientKind { } } -/// 经严格 SemVer 校验的 Codex 客户端版本。 +/// 经严格 SemVer 校验的 Codex 客户端版本 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct CodexClientVersion(Version); impl CodexClientVersion { - /// 仅接受没有前后空白、长度受限的标准 SemVer。 + /// 仅接受没有前后空白、长度受限的标准 SemVer pub fn parse(value: &str) -> Result { if value.is_empty() || value.len() > MAXIMUM_VERSION_LENGTH @@ -49,12 +49,12 @@ impl fmt::Display for CodexClientVersion { } } -/// 客户端版本 wire 值不合法。 +/// 客户端版本 wire 值不合法 #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] #[error("Codex client version must be a valid semantic version")] pub struct CodexClientVersionError; -/// 运行快照中冻结的最低版本要求;`None` 表示该客户端不限制。 +/// 运行快照中冻结的最低版本要求;`None` 表示该客户端不限制 #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct CodexClientMinVersions { desktop: Option, @@ -85,7 +85,7 @@ impl CodexClientMinVersions { } } - /// 校验一个已识别客户端;配置未启用时直接放行。 + /// 校验一个已识别客户端;配置未启用时直接放行 pub fn enforce( &self, kind: CodexClientKind, @@ -111,7 +111,7 @@ impl CodexClientMinVersions { } } -/// 已识别客户端未满足冻结的最低版本要求。 +/// 已识别客户端未满足冻结的最低版本要求 #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] pub enum ClientVersionRejection { #[error("recognized Codex client did not provide a valid version")] diff --git a/backend/crates/gateway-core/src/policy/mod.rs b/backend/crates/gateway-core/src/policy/mod.rs index bb5096014..72c0888c6 100644 --- a/backend/crates/gateway-core/src/policy/mod.rs +++ b/backend/crates/gateway-core/src/policy/mod.rs @@ -1,6 +1,6 @@ -//! 下游 Client API Key 的准入策略。 +//! 下游 Client API Key 的准入策略 //! -//! Client API Key 冻结账号分组权限;模型名称不参与权限判断。 +//! Client API Key 冻结账号分组权限;模型名称不参与权限判断 mod client_version; @@ -16,16 +16,16 @@ use crate::account::{OpaqueProviderData, scope::FrozenAccountScope}; use crate::identity::ProviderKind; use crate::validation::{IdentifierError, PolicyError, validate_text}; -/// `client_api_keys.id` 的核心值对象。 +/// `client_api_keys.id` 的核心值对象 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ClientApiKeyId(String); impl ClientApiKeyId { - /// 校验并创建 Key ID。 + /// 校验并创建 Key ID /// /// # Errors /// - /// ID 为空、过长或包含控制字符时返回错误。 + /// ID 为空、过长或包含控制字符时返回错误 pub fn new(value: impl Into) -> Result { let value = value.into(); validate_text(&value, 128, false, None)?; @@ -44,29 +44,29 @@ impl fmt::Display for ClientApiKeyId { } } -/// RuntimeSnapshot 中用于同步认证的明文 Client API Key。 +/// RuntimeSnapshot 中用于同步认证的明文 Client API Key /// -/// 数据库按产品约束明文保存;该值对象只负责阻止 `Debug`/日志意外输出。 +/// 数据库按产品约束明文保存;该值对象只负责阻止 `Debug`/日志意外输出 #[derive(Clone, PartialEq, Eq)] pub struct PlaintextClientApiKey(String); impl PlaintextClientApiKey { - /// 校验并创建明文 Key。 + /// 校验并创建明文 Key /// /// # Errors /// - /// Key 为空或无法作为 HTTP Bearer 值发送时返回错误。 + /// Key 为空或无法作为 HTTP Bearer 值发送时返回错误 pub fn new(value: impl Into) -> Result { let value = value.into(); Self::validate(&value)?; Ok(Self(value)) } - /// 迁入的 Key 不限定前缀或长度;保持原值,仅校验 HTTP 可传输的非空可见 ASCII。 + /// 迁入的 Key 不限定前缀或长度;保持原值,仅校验 HTTP 可传输的非空可见 ASCII /// /// # Errors /// - /// Key 为空或包含空白、控制字符、非 ASCII 字符时返回错误。 + /// Key 为空或包含空白、控制字符、非 ASCII 字符时返回错误 pub fn validate(value: &str) -> Result<(), IdentifierError> { if value.is_empty() { return Err(IdentifierError::Empty); @@ -77,7 +77,7 @@ impl PlaintextClientApiKey { Ok(()) } - /// 仅借给同步认证器做常量时间比较。 + /// 仅借给同步认证器做常量时间比较 #[must_use] pub fn expose_for_auth(&self) -> &str { &self.0 @@ -90,7 +90,7 @@ impl fmt::Debug for PlaintextClientApiKey { } } -/// 零表示对应维度不额外限制。 +/// 零表示对应维度不额外限制 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields)] pub struct RateLimits { @@ -108,7 +108,7 @@ impl RateLimits { } } -/// Key 自身的设置默认值;请求派生策略保留本值,不能把宿主默认或插件覆盖写回。 +/// Key 自身的设置默认值;请求派生策略保留本值,不能把宿主默认或插件覆盖写回 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ClientSettings { pub request_profiles: BTreeMap, @@ -116,7 +116,7 @@ pub struct ClientSettings { pub limits: RateLimits, } -/// 从 `client_api_keys` 冻结的公开准入事实。 +/// 从 `client_api_keys` 冻结的公开准入事实 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ClientPolicy { defaults: Arc, @@ -201,11 +201,11 @@ impl ClientPolicy { self.limits } - /// 禁用的 Key 不接受新请求。 + /// 禁用的 Key 不接受新请求 /// /// # Errors /// - /// Key 已禁用时返回稳定拒绝原因。 + /// Key 已禁用时返回稳定拒绝原因 pub fn authorize(&self) -> Result<(), PolicyError> { if self.enabled { Ok(()) diff --git a/backend/crates/gateway-core/src/provider_ports.rs b/backend/crates/gateway-core/src/provider_ports.rs index 6de1877c2..65443588d 100644 --- a/backend/crates/gateway-core/src/provider_ports.rs +++ b/backend/crates/gateway-core/src/provider_ports.rs @@ -1,4 +1,4 @@ -//! Provider 运行时所需的中立存储能力。 +//! Provider 运行时所需的中立存储能力 use std::collections::{BTreeMap, BTreeSet}; use std::fmt; @@ -19,7 +19,7 @@ use crate::validation::{IdentifierError, validate_text}; const MAX_PENDING_FLOW_TTL: Duration = Duration::from_secs(30 * 60); -/// Provider 可据此决定是否重试,但看不到 SQL、Redis 或秘密原文。 +/// Provider 可据此决定是否重试,但看不到 SQL、Redis 或秘密原文 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ProviderStoreErrorKind { Unavailable, @@ -27,7 +27,7 @@ pub enum ProviderStoreErrorKind { Conflict, } -/// Provider 存储端口的脱敏错误。 +/// Provider 存储端口的脱敏错误 #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] #[error("provider store {operation} failed: {kind:?}")] pub struct ProviderStoreError { @@ -47,7 +47,7 @@ impl ProviderStoreError { } } -/// 一个 Provider 的完整可重建调度状态。 +/// 一个 Provider 的完整可重建调度状态 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderSchedulingState { signals: BTreeMap, @@ -77,7 +77,7 @@ impl ProviderSchedulingState { } } -/// 请求级账号 lease 的全部中立事实。 +/// 请求级账号 lease 的全部中立事实 #[derive(Debug, Clone)] pub struct ProviderSchedulingLeaseRequest { provider_kind: ProviderKind, @@ -152,7 +152,7 @@ impl ProviderSchedulingLeaseRequest { } } -/// Lease 生命周期由具体 Store guard 管理,Provider 只能持有。 +/// Lease 生命周期由具体 Store guard 管理,Provider 只能持有 pub trait ProviderLeaseGuard: Send + Sync + 'static {} impl ProviderLeaseGuard for T where T: Send + Sync + 'static {} @@ -174,7 +174,7 @@ impl fmt::Debug for ProviderLeaseAcquisition { } } -/// Provider 运行时会持有的三类 lease;刷新必须同时持有全局容量与账号互斥 lease。 +/// Provider 运行时会持有的三类 lease;刷新必须同时持有全局容量与账号互斥 lease #[derive(Debug, Clone)] pub enum ProviderLeaseRequest { Scheduling(ProviderSchedulingLeaseRequest), @@ -196,7 +196,7 @@ pub trait ProviderLeasePort: Send + Sync { ) -> BoxFuture<'_, Result>; /// 读取指定账号当前的在途请求数;只用于容量熔断的峰值证据, - /// 支持租约信号的存储实现覆盖,否则视为不可观测(空映射)。 + /// 支持租约信号的存储实现覆盖,否则视为不可观测(空映射) fn account_in_flight<'a>( &'a self, account_ids: &'a [ProviderAccountId], @@ -206,7 +206,7 @@ pub trait ProviderLeasePort: Send + Sync { } } -/// Provider 从原始会话锚点派生的不可逆亲和键。 +/// Provider 从原始会话锚点派生的不可逆亲和键 #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ProviderSessionAffinityKey(String); @@ -239,7 +239,7 @@ impl fmt::Debug for ProviderSessionAffinityKey { } } -/// 可丢失的会话到账号偏好;Provider 负责先把原始会话标识哈希为不透明键。 +/// 可丢失的会话到账号偏好;Provider 负责先把原始会话标识哈希为不透明键 pub trait ProviderSessionAffinityPort: Send + Sync { fn load<'a>( &'a self, @@ -255,10 +255,11 @@ pub trait ProviderSessionAffinityPort: Send + Sync { ttl: Duration, ) -> BoxFuture<'a, Result<(), ProviderStoreError>>; - /// 仅在亲和键尚未绑定时写入候选账号,并返回原子操作后的实际绑定。 + /// 仅在亲和键尚未绑定时写入候选账号,并返回原子操作后的实际绑定 /// /// 已存在的绑定绝不会被候选账号覆盖;同一根会话的并发首次请求据此收敛到 - /// 单一账号。TTL 只在首次写入时设置,已有绑定由成功反馈负责刷新。 + /// 单一账号 + /// TTL 只在首次写入时设置,已有绑定由成功反馈负责刷新 fn claim_or_load<'a>( &'a self, provider_kind: &'a ProviderKind, @@ -268,10 +269,10 @@ pub trait ProviderSessionAffinityPort: Send + Sync { ) -> BoxFuture<'a, Result>; /// 仅当当前绑定等于 `expected_account_id`(或键已过期)时写入新账号, - /// 并返回原子操作后的实际绑定。 + /// 并返回原子操作后的实际绑定 /// /// Provider 用它迁移不可用账号,以及在成功后以 `expected == replacement` - /// 刷新 TTL;迟到的旧账号成功不能覆盖较新的会话 winner。 + /// 刷新 TTL;迟到的旧账号成功不能覆盖较新的会话 winner fn compare_and_bind<'a>( &'a self, provider_kind: &'a ProviderKind, @@ -288,10 +289,10 @@ pub trait ProviderSessionAffinityPort: Send + Sync { ) -> BoxFuture<'a, Result>; } -/// Provider 会话内已失败账号的可丢失排除集。 +/// Provider 会话内已失败账号的可丢失排除集 /// /// Provider 自行派生会话键并决定何时写入或清理;Core 只承载调度所需的账号 ID -/// 与 compare-and-swap revision,不解释任一 Provider 协议字段。 +/// 与 compare-and-swap revision,不解释任一 Provider 协议字段 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderSessionExclusions { excluded_accounts: BTreeSet, @@ -318,9 +319,9 @@ impl ProviderSessionExclusions { } } -/// 可丢失的 Provider 会话级账号排除状态。 +/// 可丢失的 Provider 会话级账号排除状态 /// -/// 该端口不接收协议正文;Provider 只能以不可逆会话键、账号 ID 和固定 TTL 操作。 +/// 该端口不接收协议正文;Provider 只能以不可逆会话键、账号 ID 和固定 TTL 操作 pub trait ProviderSessionExclusionPort: Send + Sync { fn load<'a>( &'a self, @@ -344,7 +345,7 @@ pub trait ProviderSessionExclusionPort: Send + Sync { ) -> BoxFuture<'a, Result>; } -/// 所有 Provider 共享的 OAuth refresh 并发容量。 +/// 所有 Provider 共享的 OAuth refresh 并发容量 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ProviderRefreshCapacityRequest { max_concurrent: NonZeroU32, @@ -391,32 +392,32 @@ impl ProviderRefreshLeaseRequest { } } -/// Provider 定义的 catalog cache 作用域。 +/// Provider 定义的 catalog cache 作用域 /// -/// Core 不解释其值;例如 Provider 可以使用套餐、区域或产品线作为共享目录边界。 +/// Core 不解释其值;例如 Provider 可以使用套餐、区域或产品线作为共享目录边界 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ProviderCatalogScope(String); impl ProviderCatalogScope { - /// 创建一个稳定且可用于 Redis 隔离键的 Provider-owned 作用域。 + /// 创建一个稳定且可用于 Redis 隔离键的 Provider-owned 作用域 /// /// # Errors /// - /// 空值、过长文本或控制字符会被拒绝。 + /// 空值、过长文本或控制字符会被拒绝 pub fn new(value: impl Into) -> Result { let value = value.into(); validate_text(&value, 128, false, None)?; Ok(Self(value)) } - /// 返回 Provider-owned 作用域文本。 + /// 返回 Provider-owned 作用域文本 #[must_use] pub fn as_str(&self) -> &str { &self.0 } } -/// Opaque catalog cache 的 Provider 与 Provider-owned 作用域。 +/// Opaque catalog cache 的 Provider 与 Provider-owned 作用域 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderCatalogCacheKey { provider_kind: ProviderKind, @@ -457,10 +458,11 @@ pub trait ProviderCatalogCachePort: Send + Sync { ) -> BoxFuture<'a, Result, ProviderStoreError>>; } -/// Provider 从官方制品核验出的可重建请求画像。 +/// Provider 从官方制品核验出的可重建请求画像 /// /// Core 只用单调制品序号约束覆盖顺序;具体版本字段由对应 Provider 放在 -/// `profile` 中解释。每个 Provider 的各制品分别保留一份最新画像。 +/// `profile` 中解释 +/// 每个 Provider 的各制品分别保留一份最新画像 #[derive(Clone, PartialEq)] pub struct ProviderArtifactProfile { provider_kind: ProviderKind, @@ -527,10 +529,10 @@ impl fmt::Debug for ProviderArtifactProfile { } pub trait ProviderArtifactProfileCachePort: Send + Sync { - /// 覆盖同一 Provider、同一制品的固定 cache key。 + /// 覆盖同一 Provider、同一制品的固定 cache key /// /// 返回 `false` 表示 Store 已持有更高的制品序号;相同序号但内容不同必须返回 - /// [`ProviderStoreErrorKind::Conflict`],不能静默改写已核验画像。 + /// [`ProviderStoreErrorKind::Conflict`],不能静默改写已核验画像 fn replace_if_newer( &self, profile: ProviderArtifactProfile, @@ -544,7 +546,7 @@ pub trait ProviderArtifactProfileCachePort: Send + Sync { ) -> BoxFuture<'a, Result, ProviderStoreError>>; } -/// Redis 中可重建的账号状态投影。 +/// Redis 中可重建的账号状态投影 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderCredentialState { account_id: ProviderAccountId, @@ -614,24 +616,24 @@ pub trait ProviderCredentialStatePort: Send + Sync { account_id: &'a ProviderAccountId, ) -> BoxFuture<'a, Result>; - /// 记录一次瞬态刷新失败并返回窗口内累计失败次数;每次失败刷新 TTL。 + /// 记录一次瞬态刷新失败并返回窗口内累计失败次数;每次失败刷新 TTL fn record_refresh_backoff<'a>( &'a self, account_id: &'a ProviderAccountId, window: Duration, ) -> BoxFuture<'a, Result>; - /// 凭据完整成功轮换后清零失败计数,退避窗口重新从 base 起步。 + /// 凭据完整成功轮换后清零失败计数,退避窗口重新从 base 起步 fn clear_refresh_backoff<'a>( &'a self, account_id: &'a ProviderAccountId, ) -> BoxFuture<'a, Result<(), ProviderStoreError>>; } -/// 账号级 cooldown 的来源类别;调度状态统一按 `rate_limited` 处理。 +/// 账号级 cooldown 的来源类别;调度状态统一按 `rate_limited` 处理 pub use crate::account::AccountCooldownKind as ProviderCooldownKind; -/// 可丢失的账号冷却事实,不进入持久状态;探测冻结到期后仍需确认恢复。 +/// 可丢失的账号冷却事实,不进入持久状态;探测冻结到期后仍需确认恢复 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderCooldown { account_id: ProviderAccountId, @@ -699,10 +701,10 @@ impl ProviderCooldown { } } -/// 可丢失 cooldown 的细粒度作用域。 +/// 可丢失 cooldown 的细粒度作用域 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ProviderCooldownScope { - /// 只阻止同一账号调用指定上游模型。 + /// 只阻止同一账号调用指定上游模型 UpstreamModel(UpstreamModelId), } @@ -727,7 +729,7 @@ impl ProviderCooldownScope { } } -/// 不进入账号持久状态的账号+作用域 cooldown。 +/// 不进入账号持久状态的账号+作用域 cooldown #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderScopedCooldown { account_id: ProviderAccountId, @@ -808,16 +810,16 @@ pub trait ProviderCooldownPort: Send + Sync { through_revision: CredentialRevision, ) -> BoxFuture<'a, Result>; - /// 删除账号时清除该账号全部 account/model scope cooldown key。 - /// 生命周期与 credential revision 无关;不要求调用方持有 revision。 + /// 删除账号时清除该账号全部 account/model scope cooldown key + /// 生命周期与 credential revision 无关;不要求调用方持有 revision fn clear_all<'a>( &'a self, account_id: &'a ProviderAccountId, ) -> BoxFuture<'a, Result>; /// 记录一次容量类失败并返回滑动窗口内的累计次数,同时把观测到的账号 - /// 在途并发并入窗口峰值(`in_flight` 为 0 表示本次未观测,跳过峰值更新)。 - /// 只服务容量熔断触发器;调用频率受失败频率约束,不需要批量接口。 + /// 在途并发并入窗口峰值(`in_flight` 为 0 表示本次未观测,跳过峰值更新) + /// 只服务容量熔断触发器;调用频率受失败频率约束,不需要批量接口 fn record_capacity_failure<'a>( &'a self, account_id: &'a ProviderAccountId, @@ -825,14 +827,14 @@ pub trait ProviderCooldownPort: Send + Sync { in_flight: u32, ) -> BoxFuture<'a, Result>; - /// 普通请求成功后原子清除临时限流及失败证据;必须保留任何容量冻结。 + /// 普通请求成功后原子清除临时限流及失败证据;必须保留任何容量冻结 fn clear_after_success<'a>( &'a self, account_id: &'a ProviderAccountId, through_revision: CredentialRevision, ) -> BoxFuture<'a, Result<(), ProviderStoreError>>; - /// 读取窗口内观测到的在途并发峰值;无证据时返回 `None`。 + /// 读取窗口内观测到的在途并发峰值;无证据时返回 `None` fn capacity_peak_in_flight<'a>( &'a self, account_id: &'a ProviderAccountId, @@ -869,10 +871,10 @@ impl ProviderRefreshPolicy { self.concurrency } - /// 判断 AT 是否已经进入当前配置的提前刷新窗口。 + /// 判断 AT 是否已经进入当前配置的提前刷新窗口 /// /// `margin` 是运行时策略,不投影为账号的持久化时间字段;已过期 AT 也需要 - /// 尝试用 RT 恢复,因此视为到期。 + /// 尝试用 RT 恢复,因此视为到期 #[must_use] pub fn is_refresh_due( self, @@ -886,17 +888,18 @@ impl ProviderRefreshPolicy { } } -/// 指数退避基准延迟;attempt=1 即为该值。 +/// 指数退避基准延迟;attempt=1 即为该值 const REFRESH_BACKOFF_BASE_DELAY: Duration = Duration::from_secs(5); -/// 每多一次连续失败,基准延迟乘以该因子。 +/// 每多一次连续失败,基准延迟乘以该因子 const REFRESH_BACKOFF_FACTOR: u32 = 3; -/// 退避延迟上限,避免连续失败时无限增长。 +/// 退避延迟上限,避免连续失败时无限增长 const REFRESH_BACKOFF_CAP: Duration = Duration::from_secs(300); -/// 基于连续失败计数的指数退避重试时刻;复用 `provider_refresh_retry_at` 的稳定扰动。 +/// 基于连续失败计数的指数退避重试时刻;复用 `provider_refresh_retry_at` 的稳定扰动 /// -/// `attempt` 为窗口内累计失败次数(0 与 1 等价,均取基准延迟)。延迟按 -/// `base * factor^(attempt-1)` 增长并封顶到 `REFRESH_BACKOFF_CAP`。 +/// `attempt` 为窗口内累计失败次数(0 与 1 等价,均取基准延迟) +/// 延迟按 +/// `base * factor^(attempt-1)` 增长并封顶到 `REFRESH_BACKOFF_CAP` pub fn provider_refresh_backoff_at( account_id: &ProviderAccountId, observed_at: SystemTime, @@ -913,7 +916,7 @@ pub fn provider_refresh_backoff_at( provider_refresh_retry_at(account_id, observed_at, base_delay, reason) } -/// 临时失败后的持久重试时刻;稳定扰动避免多实例同频重试。 +/// 临时失败后的持久重试时刻;稳定扰动避免多实例同频重试 pub fn provider_refresh_retry_at( account_id: &ProviderAccountId, observed_at: SystemTime, @@ -949,7 +952,7 @@ fn invalid_refresh_policy(operation: &'static str) -> ProviderStoreError { } pub trait ProviderRuntimePolicyPort: Send + Sync { - /// 原子推进预热执行游标,重启或时钟回拨后不重复领取已消费的时刻。 + /// 原子推进预热执行游标,重启或时钟回拨后不重复领取已消费的时刻 fn claim_warmup_slot<'a>( &'a self, _timezone: crate::time::DeploymentTimeZone, @@ -962,7 +965,7 @@ pub trait ProviderRuntimePolicyPort: Send + Sync { )) }) } - /// 仅首次启动写入该 Provider 的默认选择;已保存的管理配置始终优先。 + /// 仅首次启动写入该 Provider 的默认选择;已保存的管理配置始终优先 fn initialize_request_profile<'a>( &'a self, _provider: &'a ProviderKind, @@ -971,10 +974,11 @@ pub trait ProviderRuntimePolicyPort: Send + Sync { Box::pin(async move { Ok(initial) }) } - /// 读取候选配置版本实际引用的全局与 Client Key 画像配置。 + /// 读取候选配置版本实际引用的全局与 Client Key 画像配置 /// /// 实现必须在同一数据库快照内核对 revision,且只返回画像投影,不能读取 Key - /// 明文。Provider 代次据此在发布前拒绝已失效的选择。 + /// 明文 + /// Provider 代次据此在发布前拒绝已失效的选择 fn load_request_profile_configurations<'a>( &'a self, _revision: ConfigRevision, @@ -987,14 +991,14 @@ pub trait ProviderRuntimePolicyPort: Send + Sync { &self, ) -> BoxFuture<'_, Result>; - /// 读取账号容量熔断策略;默认关闭,只有实现运行时设置的存储需要覆盖。 + /// 读取账号容量熔断策略;默认关闭,只有实现运行时设置的存储需要覆盖 fn load_freeze_policy( &self, ) -> BoxFuture<'_, Result> { Box::pin(async move { Ok(ProviderFreezePolicy::disabled()) }) } - /// 读取账号模型预激活策略;默认关闭,只有实现运行时设置的存储需要覆盖。 + /// 读取账号模型预激活策略;默认关闭,只有实现运行时设置的存储需要覆盖 fn load_warmup_policy( &self, ) -> BoxFuture<'_, Result> { @@ -1003,7 +1007,7 @@ pub trait ProviderRuntimePolicyPort: Send + Sync { } /// 账号容量熔断(自动冻结)策略;来源于 `runtime_settings`, -/// 由 Provider 触发路径与恢复 worker 共享同一份配置事实。 +/// 由 Provider 触发路径与恢复 worker 共享同一份配置事实 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderFreezePolicy { enabled: bool, @@ -1017,7 +1021,7 @@ pub struct ProviderFreezePolicy { impl ProviderFreezePolicy { /// 边界与迁移 `0010_account_auto_freeze.sql` 的 check 约束一致; - /// store 层写入前已校验,这里兜底防御越界配置。 + /// store 层写入前已校验,这里兜底防御越界配置 pub fn try_new( enabled: bool, threshold: u32, @@ -1053,7 +1057,7 @@ impl ProviderFreezePolicy { }) } - /// 功能关闭时的全零策略;触发路径与 worker 都以此短路。 + /// 功能关闭时的全零策略;触发路径与 worker 都以此短路 #[must_use] pub const fn disabled() -> Self { Self { @@ -1072,19 +1076,19 @@ impl ProviderFreezePolicy { self.enabled } - /// 窗口内触发冻结的请求级失败次数阈值。 + /// 窗口内触发冻结的请求级失败次数阈值 #[must_use] pub const fn threshold(&self) -> u32 { self.threshold } - /// 失败计数滑动窗口;每次失败都会顺延窗口。 + /// 失败计数滑动窗口;每次失败都会顺延窗口 #[must_use] pub const fn window(&self) -> Duration { self.window } - /// 冻结时长;探测失败后的顺延也使用该值。 + /// 冻结时长;探测失败后的顺延也使用该值 #[must_use] pub const fn freeze_duration(&self) -> Duration { self.freeze_duration @@ -1095,7 +1099,7 @@ impl ProviderFreezePolicy { self.probe_enabled } - /// 探测模型;`None` 表示由 worker 选择账号可用的第一个模型。 + /// 探测模型;`None` 表示由 worker 选择账号可用的第一个模型 #[must_use] pub fn probe_model(&self) -> Option<&str> { self.probe_model.as_deref() @@ -1107,7 +1111,7 @@ impl ProviderFreezePolicy { } } -/// 校验每日预激活时间格式,如 "08:00" 或 "08:00,13:00"。 +/// 校验每日预激活时间格式,如 "08:00" 或 "08:00,13:00" #[must_use] pub fn valid_warmup_schedule_time(value: &str) -> bool { if value.is_empty() @@ -1135,7 +1139,7 @@ pub fn valid_warmup_schedule_time(value: &str) -> bool { true } -/// 账号模型预激活(预热)策略;来源于 `runtime_settings`。 +/// 账号模型预激活(预热)策略;来源于 `runtime_settings` #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderWarmupPolicy { enabled: bool, @@ -1194,7 +1198,7 @@ impl ProviderWarmupPolicy { self.model.as_deref() } - /// 解析每日时间列表,返回如 `vec![(8, 0)]`。 + /// 解析每日时间列表,返回如 `vec![(8, 0)]` #[must_use] pub fn scheduled_times(&self) -> Vec<(u32, u32)> { self.schedule_time @@ -1210,7 +1214,7 @@ impl ProviderWarmupPolicy { } } -/// OAuth pending flow 的原始绑定只在 Provider 与 Store 边界内短暂存在。 +/// OAuth pending flow 的原始绑定只在 Provider 与 Store 边界内短暂存在 #[derive(Clone, PartialEq, Eq)] pub struct OAuthPendingBinding(String); @@ -1315,10 +1319,10 @@ pub enum OAuthPendingPutOutcome { AlreadyExists, } -/// OAuth 回调处理取得临时 flow 的独占权结果。 +/// OAuth 回调处理取得临时 flow 的独占权结果 /// /// 与一次性消费不同,Provider 在上游换取令牌失败时可以释放 claim,让同一 flow -/// 使用新的回调地址重试;只有完整校验成功后才会消费 flow。 +/// 使用新的回调地址重试;只有完整校验成功后才会消费 flow #[derive(Clone, PartialEq)] pub enum OAuthPendingClaimOutcome { Claimed(OpaqueProviderData), @@ -1386,7 +1390,7 @@ pub trait OAuthPendingFlowPort: Send + Sync { ) -> BoxFuture<'a, Result>; } -/// Provider 只能按能力取用端口,无法取得 Redis client 或 repository 集合。 +/// Provider 只能按能力取用端口,无法取得 Redis client 或 repository 集合 #[derive(Clone)] pub struct ProviderStorePorts { accounts: Arc, @@ -1404,7 +1408,7 @@ pub struct ProviderStorePorts { impl ProviderStorePorts { #[must_use] - // 每个参数代表独立能力端口;合并为单一配置对象会隐藏 Provider 能力边界。 + // 每个参数代表独立能力端口;合并为单一配置对象会隐藏 Provider 能力边界 #[expect(clippy::too_many_arguments)] pub fn new( accounts: Arc, diff --git a/backend/crates/gateway-core/src/routing/catalog.rs b/backend/crates/gateway-core/src/routing/catalog.rs index c933653a7..01227edd5 100644 --- a/backend/crates/gateway-core/src/routing/catalog.rs +++ b/backend/crates/gateway-core/src/routing/catalog.rs @@ -1,4 +1,4 @@ -//! 快照编译与客户端读取需要的 Provider 目录合同,不暴露执行或账号选择能力。 +//! 快照编译与客户端读取需要的 Provider 目录合同,不暴露执行或账号选择能力 use std::collections::{BTreeMap, BTreeSet}; @@ -12,7 +12,7 @@ use super::{ ModelCapabilities, ModelPresentation, PublicModelId, PublicModelProfile, UpstreamModelId, }; -/// 扩展集合持有的直接模型别名;执行与元数据仍归属内置 Provider。 +/// 扩展集合持有的直接模型别名;执行与元数据仍归属内置 Provider #[derive(Debug, Clone, PartialEq, Eq)] pub struct ContributedModelAlias { pub owner: String, @@ -21,7 +21,7 @@ pub struct ContributedModelAlias { pub target: UpstreamModelId, } -/// Provider 客户端目录条目;缺少原生协议正文时使用已编译的通用画像。 +/// Provider 客户端目录条目;缺少原生协议正文时使用已编译的通用画像 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderModelDescriptor { pub model: UpstreamModelId, @@ -34,7 +34,7 @@ pub enum ProviderModelContent { Adapted(ModelPresentation), } -/// 对外目录按条目保留原生正文或使用通用画像适配。 +/// 对外目录按条目保留原生正文或使用通用画像适配 #[derive(Debug, Clone)] pub enum PublicModelDescriptor { Native { @@ -58,7 +58,7 @@ impl ProviderModelContent { } } -/// Provider 实时目录编译后的单模型能力。 +/// Provider 实时目录编译后的单模型能力 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderModelCapabilities { upstream_model: UpstreamModelId, @@ -84,14 +84,14 @@ impl ProviderModelCapabilities { self } - /// 声明发现此模型的账号;仅用于目录可见性,不作为推理能力白名单。 + /// 声明发现此模型的账号;仅用于目录可见性,不作为推理能力白名单 #[must_use] pub fn with_catalog_accounts(mut self, accounts: BTreeSet) -> Self { self.catalog_accounts = Some(accounts); self } - /// 未提供账号来源的 Provider 沿用 Provider 级目录;空集合表示无可展示来源。 + /// 未提供账号来源的 Provider 沿用 Provider 级目录;空集合表示无可展示来源 #[must_use] pub const fn catalog_accounts(&self) -> Option<&BTreeSet> { self.catalog_accounts.as_ref() @@ -113,9 +113,9 @@ impl ProviderModelCapabilities { } } -/// Provider 实时目录成功发布后的进程内单调代次。 +/// Provider 实时目录成功发布后的进程内单调代次 /// -/// 代次只表达“目录内容已经变化”,不承载模型、ETag 或 Provider 私有数据。 +/// 代次只表达“目录内容已经变化”,不承载模型、ETag 或 Provider 私有数据 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ProviderCatalogGeneration(u64); @@ -131,24 +131,24 @@ impl ProviderCatalogGeneration { } } -/// 目录未知,不能据此判定某个模型不存在;不携带执行错误或上游协议细节。 +/// 目录未知,不能据此判定某个模型不存在;不携带执行错误或上游协议细节 #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] #[error("provider model catalog is unavailable")] pub struct ProviderCatalogUnavailable; -/// 快照编译与对账使用的对象安全目录端口。 +/// 快照编译与对账使用的对象安全目录端口 pub trait ProviderCatalogPort: Send + Sync { - /// 目录是否完整到足以根据缺项拒绝请求;发现型目录交由上游验证模型名。 + /// 目录是否完整到足以根据缺项拒绝请求;发现型目录交由上游验证模型名 fn model_catalog_is_exhaustive(&self, _provider: &ProviderKind) -> bool { true } - /// 返回当前冻结视图内全部 Provider 的目录代次。 + /// 返回当前冻结视图内全部 Provider 的目录代次 /// - /// 即使某个目录暂时不可读,也必须保留它的 Provider 与最近成功发布的代次。 + /// 即使某个目录暂时不可读,也必须保留它的 Provider 与最近成功发布的代次 fn catalog_generations(&self) -> BTreeMap; - /// 查询模型发现事实;只有完整目录的空列表能证明无模型,失败表示未知。 + /// 查询模型发现事实;只有完整目录的空列表能证明无模型,失败表示未知 fn query_model_capabilities( &self, provider: &ProviderKind, diff --git a/backend/crates/gateway-core/src/routing/mod.rs b/backend/crates/gateway-core/src/routing/mod.rs index e90cdfd2e..9c8fef052 100644 --- a/backend/crates/gateway-core/src/routing/mod.rs +++ b/backend/crates/gateway-core/src/routing/mod.rs @@ -1,4 +1,4 @@ -//! Provider、模型目录、精确模型映射与请求级候选计划。 +//! Provider、模型目录、精确模型映射与请求级候选计划 mod catalog; pub mod snapshot; @@ -26,7 +26,7 @@ use crate::validation::{IdentifierError, RoutingError, validate_text}; const MAX_REQUEST_ATTEMPTS: u32 = 32; -/// 客户端请求中的模型名称。 +/// 客户端请求中的模型名称 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize)] #[serde(transparent)] pub struct PublicModelId(String); @@ -45,14 +45,14 @@ impl PublicModelId { Ok(Self(value)) } - /// 从客户端 OpenAI wire 读取模型名。 + /// 从客户端 OpenAI wire 读取模型名 /// /// 该值只作为路由查询键;具体模型字符串能否被上游接受由 Provider 决定, - /// 因而不能把内部标识长度或控制字符规则当作入站 schema gate。 + /// 因而不能把内部标识长度或控制字符规则当作入站 schema gate /// /// # Errors /// - /// 空模型名无法参与路由时返回错误。 + /// 空模型名无法参与路由时返回错误 pub fn from_client_wire(value: impl Into) -> Result { let value = value.into(); if value.is_empty() { @@ -73,7 +73,7 @@ impl fmt::Display for PublicModelId { } } -/// Provider 实际接收的模型名称。 +/// Provider 实际接收的模型名称 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize)] #[serde(transparent)] pub struct UpstreamModelId(String); @@ -92,10 +92,10 @@ impl UpstreamModelId { Ok(Self(value)) } - /// 从未命中显式映射的客户端模型名构造同名上游模型。 + /// 从未命中显式映射的客户端模型名构造同名上游模型 /// /// 目录和配置仍使用 [`Self::new`] 的内部标识约束;客户端 wire 只要求非空, - /// 具体模型字符串是否可用由绑定 Provider 决定。 + /// 具体模型字符串是否可用由绑定 Provider 决定 pub fn from_client_wire(value: impl Into) -> Result { let value = value.into(); if value.is_empty() { @@ -116,7 +116,7 @@ impl fmt::Display for UpstreamModelId { } } -/// `runtime_settings.config_revision`。 +/// `runtime_settings.config_revision` #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ConfigRevision(NonZeroU64); @@ -133,7 +133,7 @@ impl ConfigRevision { } } -/// Provider 实时目录报告的能力支持等级。 +/// Provider 实时目录报告的能力支持等级 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum SupportLevel { Native, @@ -142,7 +142,7 @@ pub enum SupportLevel { Unknown, } -/// Provider 实时模型目录中的能力事实;不落 PostgreSQL。 +/// Provider 实时模型目录中的能力事实;不落 PostgreSQL #[derive(Debug, Clone, PartialEq, Eq)] pub struct ModelCapabilities { operations: BTreeSet, @@ -151,9 +151,9 @@ pub struct ModelCapabilities { upstream_validates_features: bool, } -/// Provider 为客户端模型目录提供的展示与交互能力。 +/// Provider 为客户端模型目录提供的展示与交互能力 /// -/// 该值不参与路由,也不把任一 Provider 的 wire 类型带入 Core。 +/// 该值不参与路由,也不把任一 Provider 的 wire 类型带入 Core #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct ModelPresentation { display_name: Option, @@ -172,7 +172,7 @@ pub struct ModelPresentation { hidden: bool, } -/// Provider 声明给 Codex 客户端的服务档位。 +/// Provider 声明给 Codex 客户端的服务档位 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ModelServiceTier { id: String, @@ -250,7 +250,7 @@ impl ModelPresentation { self } - /// 目录声明的可覆盖上限;客户端本地 context window 覆盖会被它钳制。 + /// 目录声明的可覆盖上限;客户端本地 context window 覆盖会被它钳制 #[must_use] pub const fn with_max_context_window_tokens( mut self, @@ -374,7 +374,7 @@ impl ModelPresentation { } } -/// 一个公开模型及其 Provider 编译后的客户端画像。 +/// 一个公开模型及其 Provider 编译后的客户端画像 #[derive(Debug, Clone, PartialEq, Eq)] pub struct PublicModelProfile { model: PublicModelId, @@ -418,7 +418,7 @@ impl ModelCapabilities { self } - /// 将请求形态 feature 的最终合法性判断交给上游 wire API。 + /// 将请求形态 feature 的最终合法性判断交给上游 wire API #[must_use] pub const fn with_upstream_feature_validation(mut self) -> Self { self.upstream_validates_features = true; @@ -464,7 +464,7 @@ impl ModelCapabilities { } } -/// 一个 Provider 实时发现的上游模型能力。 +/// 一个 Provider 实时发现的上游模型能力 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderModel { provider: ProviderKind, @@ -510,15 +510,15 @@ impl ProviderModel { } } -/// 本次请求选择 Provider 时使用的动态过滤事实。 +/// 本次请求选择 Provider 时使用的动态过滤事实 #[derive(Debug, Clone, Default)] pub struct RoutingContext { - /// 管理端 connection test 显式限制的 Provider;普通请求留空。 + /// 管理端 connection test 显式限制的 Provider;普通请求留空 pub required_provider: Option, pub blocked_providers: BTreeSet, } -/// 已绑定 Provider 的请求候选;模型端点携带真实上游模型,原生端点不虚构模型。 +/// 已绑定 Provider 的请求候选;模型端点携带真实上游模型,原生端点不虚构模型 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProviderCandidate { provider: ProviderKind, @@ -549,7 +549,7 @@ impl ProviderCandidate { } } -/// 一次请求冻结的 Provider 尝试顺序。 +/// 一次请求冻结的 Provider 尝试顺序 #[derive(Debug, Clone)] pub struct RoutingPlan { pricing: Arc, @@ -573,7 +573,7 @@ impl RoutingPlan { self.account_scope.disable_fast() } - /// 本次请求冻结的全局位置,重试时沿用同一份配置。 + /// 本次请求冻结的全局位置,重试时沿用同一份配置 #[must_use] pub const fn request_location(&self) -> Option<&crate::account::RequestLocation> { self.request_location.as_ref() diff --git a/backend/crates/gateway-core/src/routing/snapshot.rs b/backend/crates/gateway-core/src/routing/snapshot.rs index e0e2d0672..3db4785af 100644 --- a/backend/crates/gateway-core/src/routing/snapshot.rs +++ b/backend/crates/gateway-core/src/routing/snapshot.rs @@ -1,4 +1,4 @@ -//! RuntimeSnapshot 事实、编译、原子发布与版本收敛规则。 +//! RuntimeSnapshot 事实、编译、原子发布与版本收敛规则 use std::collections::{BTreeMap, BTreeSet}; use std::num::NonZeroU32; @@ -26,7 +26,7 @@ const MAXIMUM_CATALOG_STABILITY_ATTEMPTS: usize = 4; type ModelCatalogAccounts = BTreeMap>>; -/// Store 读取到的一个启用 Client API Key 策略事实。 +/// Store 读取到的一个启用 Client API Key 策略事实 #[derive(Debug, Clone, PartialEq, Eq)] pub struct SnapshotClientPolicyFacts { request_profiles: BTreeMap, @@ -63,7 +63,7 @@ impl SnapshotClientPolicyFacts { } } -/// Store 读取到的账号分组事实。 +/// Store 读取到的账号分组事实 #[derive(Debug, Clone, PartialEq, Eq)] pub struct SnapshotAccountGroupFacts { disable_fast: bool, @@ -90,7 +90,7 @@ impl SnapshotAccountGroupFacts { } } -/// Store 读取到的账号及其固有 Provider 事实。 +/// Store 读取到的账号及其固有 Provider 事实 #[derive(Debug, Clone, PartialEq, Eq)] pub struct SnapshotProviderAccountFacts { account_id: ProviderAccountId, @@ -117,7 +117,7 @@ impl SnapshotProviderAccountFacts { } } -/// Store 读取到的一条分组成员关系。 +/// Store 读取到的一条分组成员关系 #[derive(Debug, Clone, PartialEq, Eq)] pub struct SnapshotAccountGroupMemberFacts { group_id: AccountGroupId, @@ -134,7 +134,7 @@ impl SnapshotAccountGroupMemberFacts { } } -/// 一次一致性读取产生的全部 RuntimeSnapshot 持久事实。 +/// 一次一致性读取产生的全部 RuntimeSnapshot 持久事实 #[derive(Debug, Clone, PartialEq, Eq)] pub struct SnapshotFacts { config_revision: ConfigRevision, @@ -179,7 +179,7 @@ impl SnapshotFacts { } } -/// 不泄漏持久化实现细节的 Snapshot store 错误。 +/// 不泄漏持久化实现细节的 Snapshot store 错误 #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] #[error("runtime snapshot store is unavailable")] pub struct SnapshotStoreError; @@ -191,14 +191,14 @@ impl SnapshotStoreError { } } -/// RuntimeSnapshot 持久事实的数据库中立端口。 +/// RuntimeSnapshot 持久事实的数据库中立端口 pub trait SnapshotStorePort: Send + Sync { fn load_snapshot_facts(&self) -> BoxFuture<'_, Result>; fn current_config_revision(&self) -> BoxFuture<'_, Result>; } -/// 快照未发布时可安全记录的稳定错误。 +/// 快照未发布时可安全记录的稳定错误 #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] pub enum RuntimeSnapshotCompileError { #[error("runtime extensions could not be prepared")] @@ -215,7 +215,7 @@ pub enum RuntimeSnapshotCompileError { CatalogChanged, } -/// Store 一致性事实与 Provider 实时目录的唯一快照编译器。 +/// Store 一致性事实与 Provider 实时目录的唯一快照编译器 #[derive(Clone)] pub struct RuntimeSnapshotCompiler { store: Arc, @@ -255,12 +255,12 @@ impl RuntimeSnapshotCompiler { self.catalogs.catalog_generations() } - /// 读取一个 revision,并为已注册 Provider 查询实时模型目录。 + /// 读取一个 revision,并为已注册 Provider 查询实时模型目录 pub async fn compile(&self) -> Result { self.compile_inner(None).await } - /// 配置提交复用同一扩展集合的已发布目录,目录留待对账刷新。 + /// 配置提交复用同一扩展集合的已发布目录,目录留待对账刷新 pub(crate) async fn compile_with_cached_catalog( &self, previous: &RuntimeSnapshot, @@ -272,7 +272,7 @@ impl RuntimeSnapshotCompiler { &self, previous: Option<&RuntimeSnapshot>, ) -> Result { - // 同一配置 revision 的重试复用扩展候选,保留插件策略的发布一致性。 + // 同一配置 revision 的重试复用扩展候选,保留插件策略的发布一致性 let mut prepared_extensions: Option<( ConfigRevision, crate::runtime::extensions::ExtensionSetReference, @@ -349,7 +349,7 @@ async fn compile_runtime_snapshot( provider_kinds: Vec, previous: Option<&RuntimeSnapshot>, ) -> Result { - // 只有成功取得的完整目录能证明模型缺项;发现型目录和查询失败均交由上游验证。 + // 只有成功取得的完整目录能证明模型缺项;发现型目录和查询失败均交由上游验证 let mut provider_models = Vec::new(); let mut catalog_accounts = BTreeMap::new(); let mut exhaustive_provider_catalogs = BTreeSet::new(); @@ -419,7 +419,7 @@ async fn compile_runtime_snapshot( for account in facts.provider_accounts { let provider_kind = ProviderKind::new(account.provider_kind) .map_err(|_| RuntimeSnapshotCompileError::InvalidData)?; - // 账号归属独立于当前执行器集合;Provider 撤下后仍保留账号和分组,路由只选已注册执行器。 + // 账号归属独立于当前执行器集合;Provider 撤下后仍保留账号和分组,路由只选已注册执行器 if accounts .insert( account.account_id.clone(), @@ -473,7 +473,7 @@ async fn compile_runtime_snapshot( let group = groups .get(&group_id) .ok_or(RuntimeSnapshotCompileError::InvalidData)?; - // 禁用分组仅影响选号;Key 仍绑定其 Fast 限制。 + // 禁用分组仅影响选号;Key 仍绑定其 Fast 限制 disable_fast |= group.disable_fast; bound_groups.push(RoutingGroupSnapshot::new( group.id.clone(), @@ -520,7 +520,7 @@ async fn compile_runtime_snapshot( }) } -/// 数据面使用的不可变配置快照。 +/// 数据面使用的不可变配置快照 #[derive(Debug, Clone)] pub struct RuntimeSnapshot { settings: Arc, @@ -538,7 +538,7 @@ pub struct RuntimeSnapshot { } impl RuntimeSnapshot { - /// 设置事实与编译结果共享快照寿命,读取时不从执行策略反向拼装。 + /// 设置事实与编译结果共享快照寿命,读取时不从执行策略反向拼装 #[must_use] pub fn settings(&self) -> &SettingsValues { &self.settings.values @@ -554,7 +554,7 @@ impl RuntimeSnapshot { self.with_settings(values).map(Arc::new) } - /// 只替换设置及其编译结果,目录、发布代次和身份事实共享原快照。 + /// 只替换设置及其编译结果,目录、发布代次和身份事实共享原快照 pub fn with_settings( &self, values: &SettingsValues, @@ -642,7 +642,7 @@ impl RuntimeSnapshot { self.settings.client_queue_policy } - /// 校验 Provider、实时模型目录和 Client API Key,并构建快照。 + /// 校验 Provider、实时模型目录和 Client API Key,并构建快照 pub fn new( revision: ConfigRevision, values: SettingsValues, @@ -771,7 +771,7 @@ impl RuntimeSnapshot { self.revision } - /// 返回目录发现模型与设置映射的并集,仅用于公开模型展示。 + /// 返回目录发现模型与设置映射的并集,仅用于公开模型展示 #[must_use] pub fn public_models_for_provider(&self, provider: &ProviderKind) -> Vec { if !self.providers.contains(provider) { @@ -801,7 +801,7 @@ impl RuntimeSnapshot { models.into_iter().collect() } - /// 返回 Provider 已明确声明画像的公开模型;没有画像时不猜测 Provider 语义。 + /// 返回 Provider 已明确声明画像的公开模型;没有画像时不猜测 Provider 语义 #[must_use] pub fn public_model_profiles_for_provider( &self, @@ -852,7 +852,7 @@ impl RuntimeSnapshot { .collect() } - /// 合并冻结账号范围内实际存在 Provider 的公开模型。 + /// 合并冻结账号范围内实际存在 Provider 的公开模型 #[must_use] pub fn public_models_for_scope(&self, scope: &FrozenAccountScope) -> Vec { scope @@ -868,7 +868,7 @@ impl RuntimeSnapshot { .collect() } - /// 合并冻结账号范围内实际存在 Provider 的公开模型画像。 + /// 合并冻结账号范围内实际存在 Provider 的公开模型画像 #[must_use] pub fn public_model_profiles_for_scope( &self, @@ -891,7 +891,7 @@ impl RuntimeSnapshot { .collect() } - /// 完整目录按映射后的模型判定;发现型或不可用目录不作为能力白名单。 + /// 完整目录按映射后的模型判定;发现型或不可用目录不作为能力白名单 #[must_use] pub fn contains_public_model_for_provider( &self, @@ -983,7 +983,7 @@ impl RuntimeSnapshot { self.client_policies.get(id) } - /// 返回当前 Key 范围、本代次注册表与请求路由限制共同允许的 Provider。 + /// 返回当前 Key 范围、本代次注册表与请求路由限制共同允许的 Provider #[must_use] pub fn available_providers( &self, @@ -1020,7 +1020,7 @@ impl RuntimeSnapshot { self.plan_inner(public_model, operation, account_scope, context, true) } - /// 管理探测已固定目标账号,不使用数据面 Key 的账号模型政策裁决探测资格。 + /// 管理探测已固定目标账号,不使用数据面 Key 的账号模型政策裁决探测资格 pub(crate) fn plan_diagnostic( &self, public_model: &PublicModelId, @@ -1087,7 +1087,7 @@ impl RuntimeSnapshot { UpstreamModelId::from_client_wire(mapped_model) } .map_err(|_| RoutingError::InvalidIdentifier)?; - // 强制 Provider 路由也不能扩大 Key 授权;管理诊断的目标账号由探测入口固定。 + // 强制 Provider 路由也不能扩大 Key 授权;管理诊断的目标账号由探测入口固定 if enforce_account_model_policy && !account_scope.allows_provider_model(provider, upstream_model.as_str()) { @@ -1116,7 +1116,7 @@ impl RuntimeSnapshot { } if candidates.is_empty() { - // 模型存在性必须包含被请求路由限制排除的 Provider;目录未知时不能断言模型不存在。 + // 模型存在性必须包含被请求路由限制排除的 Provider;目录未知时不能断言模型不存在 let mut scoped_providers = providers.intersection(&self.providers).peekable(); if scoped_providers.peek().is_some() && scoped_providers.all(|provider| { @@ -1147,10 +1147,11 @@ impl RuntimeSnapshot { }) } - /// 为 Provider 自有端点冻结请求计划。 + /// 为 Provider 自有端点冻结请求计划 /// - /// 端点 adapter 已经确定 Provider。非模型 HTTP 端点不读取文本模型目录;Token - /// 计数端点仍必须匹配冻结账号范围及该模型声明的计数能力。 + /// 端点 adapter 已经确定 Provider + /// 非模型 HTTP 端点不读取文本模型目录;Token + /// 计数端点仍必须匹配冻结账号范围及该模型声明的计数能力 pub fn plan_provider_endpoint( &self, provider: &ProviderKind, diff --git a/backend/crates/gateway-core/src/runtime/extensions.rs b/backend/crates/gateway-core/src/runtime/extensions.rs index 0e1f52c4a..059cd26cd 100644 --- a/backend/crates/gateway-core/src/runtime/extensions.rs +++ b/backend/crates/gateway-core/src/runtime/extensions.rs @@ -1,4 +1,4 @@ -//! 可发布能力集合的中立身份、静态目录与保活合同;不解释包格式、进程或管理配置。 +//! 可发布能力集合的中立身份、静态目录与保活合同;不解释包格式、进程或管理配置 use std::{fmt, sync::Arc}; @@ -28,23 +28,23 @@ impl ExtensionSetId { } } -/// 发布视图及在途请求持有引用;准备器只能用非拥有索引查找代次。 +/// 发布视图及在途请求持有引用;准备器只能用非拥有索引查找代次 pub trait ExtensionSetLease: Send + Sync { fn is_ready(&self) -> bool; - /// 与发布集合一起冻结的上游适配计划,不能在请求中解析为新代次。 + /// 与发布集合一起冻结的上游适配计划,不能在请求中解析为新代次 fn upstream_adapters( &self, ) -> Option> { None } - /// 与集合一起冻结的目录事实,读取不调用插件进程。 + /// 与集合一起冻结的目录事实,读取不调用插件进程 fn model_aliases(&self) -> &[ContributedModelAlias] { &[] } - /// 局部故障已有请求级处理计划时,集合仍可提供服务。 + /// 局部故障已有请求级处理计划时,集合仍可提供服务 fn can_serve(&self) -> bool { self.is_ready() } @@ -102,7 +102,7 @@ impl fmt::Debug for ExtensionSetReference { #[error("extension preparation is unavailable")] pub struct ExtensionPreparationError; -/// 准备阶段读取与请求快照相同的持久 revision;不得自行发布当前代次。 +/// 准备阶段读取与请求快照相同的持久 revision;不得自行发布当前代次 pub trait ExtensionPreparationPort: Send + Sync { fn prepare( &self, diff --git a/backend/crates/gateway-core/src/runtime/mod.rs b/backend/crates/gateway-core/src/runtime/mod.rs index 2635c7bd9..b60cece46 100644 --- a/backend/crates/gateway-core/src/runtime/mod.rs +++ b/backend/crates/gateway-core/src/runtime/mod.rs @@ -1,4 +1,4 @@ -//! 运行时快照的原子发布、健康状态与跨进程版本收敛。 +//! 运行时快照的原子发布、健康状态与跨进程版本收敛 pub mod extensions; @@ -32,7 +32,7 @@ const MAXIMUM_BACKOFF: Duration = Duration::from_secs(30); const UNUSED_LEASE_TTL: Duration = Duration::from_secs(30); const UNUSED_LEASE_RENEWAL: Duration = Duration::from_secs(10); -/// 不泄漏订阅基础设施细节的通知错误。 +/// 不泄漏订阅基础设施细节的通知错误 #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] #[error("runtime snapshot notification is unavailable")] pub struct SnapshotSubscriptionError; @@ -44,11 +44,11 @@ impl SnapshotSubscriptionError { } } -/// 可丢失的配置 revision 通知流;权威 revision 始终由 Store 端口读取。 +/// 可丢失的配置 revision 通知流;权威 revision 始终由 Store 端口读取 pub type SnapshotRevisionStream = Pin> + Send + 'static>>; -/// 跨进程 revision 通知的基础设施中立端口。 +/// 跨进程 revision 通知的基础设施中立端口 pub trait SnapshotSubscriptionPort: Send + Sync { fn publish_snapshot_revision( &self, @@ -60,12 +60,12 @@ pub trait SnapshotSubscriptionPort: Send + Sync { ) -> BoxFuture<'_, Result>; } -/// 请求级冻结失败;此状态必须 fail closed。 +/// 请求级冻结失败;此状态必须 fail closed #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] #[error("runtime snapshot is unavailable")] pub struct RuntimeSnapshotUnavailable; -/// RuntimeSnapshot 原子发布和请求级冻结句柄。 +/// RuntimeSnapshot 原子发布和请求级冻结句柄 #[derive(Clone, Default)] pub struct RuntimeSnapshotHandle { current: Arc>>>, @@ -91,7 +91,7 @@ impl RuntimeSnapshotHandle { self.notify_publication(); } - /// 合并发布与暂停通知;订阅者收到后读取当前快照,不依赖逐条投递。 + /// 合并发布与暂停通知;订阅者收到后读取当前快照,不依赖逐条投递 pub fn subscribe_publications(&self) -> futures::channel::mpsc::Receiver<()> { let (sender, receiver) = futures::channel::mpsc::channel(0); self.publications @@ -127,13 +127,13 @@ impl RuntimeSnapshotHandle { .map(|snapshot| snapshot.provider_catalog_generations().clone()) } - /// 控制面只读诊断可观察尚未就绪的发布候选;数据面仍必须通过 [`Self::acquire`]。 + /// 控制面只读诊断可观察尚未就绪的发布候选;数据面仍必须通过 [`Self::acquire`] #[must_use] pub fn snapshot_for_diagnostics(&self) -> Option> { read_unpoisoned(&self.current).clone() } - /// 冻结当前 Arc;后续发布不改变已经开始的请求。 + /// 冻结当前 Arc;后续发布不改变已经开始的请求 pub fn acquire(&self) -> Result, RuntimeSnapshotUnavailable> { read_unpoisoned(&self.current) .clone() @@ -158,7 +158,7 @@ impl HealthProbe for RuntimeSnapshotHandle { } } -/// Admin 提交配置后触发本进程刷新与跨进程通知的对象安全端口。 +/// Admin 提交配置后触发本进程刷新与跨进程通知的对象安全端口 pub trait SnapshotControl: Send + Sync { fn publish_committed(&self, committed_revision: ConfigRevision) -> BoxFuture<'_, ()>; } @@ -180,7 +180,7 @@ impl CommittedRefreshGuard { state.pending_commits += 1; state.active_background.clone() }; - // 先登记待发布提交,再取消正在等待目录的后台刷新,避免后者抢先重入。 + // 先登记待发布提交,再取消正在等待目录的后台刷新,避免后者抢先重入 if let Some(active) = active { active.cancel(); } @@ -221,7 +221,7 @@ impl Drop for BackgroundRefreshGuard { } } -// 仅中断未完成的后台读取或编译;配置提交负责发布最新事实并发送通知。 +// 仅中断未完成的后台读取或编译;配置提交负责发布最新事实并发送通知 async fn preempt_for_committed_refresh( cancellation: Option<&CancellationToken>, operation: impl Future, @@ -238,7 +238,7 @@ async fn preempt_for_committed_refresh( } } -/// 配置提交后的本进程快照发布与跨进程失效通知。 +/// 配置提交后的本进程快照发布与跨进程失效通知 #[derive(Clone)] pub struct RuntimeSnapshotPublisher { compiler: Arc, @@ -270,7 +270,7 @@ impl RuntimeSnapshotPublisher { } } - /// 串行重编译并替换本进程快照;无法确认配置时暂停新请求。 + /// 串行重编译并替换本进程快照;无法确认配置时暂停新请求 pub async fn refresh(&self) -> Result { self.refresh_with_mode(RefreshMode::Required).await } @@ -279,7 +279,7 @@ impl RuntimeSnapshotPublisher { &self, mode: RefreshMode, ) -> Result { - // 提交先登记并中断慢后台读取;发布/暂停仍在完整临界区内串行执行。 + // 提交先登记并中断慢后台读取;发布/暂停仍在完整临界区内串行执行 let _committed = matches!(mode, RefreshMode::Committed) .then(|| CommittedRefreshGuard::begin(Arc::clone(&self.refresh_priority))); let _refresh = self.refresh_lock.lock().await; @@ -342,7 +342,7 @@ impl RuntimeSnapshotPublisher { let snapshot = match compiled { Ok(snapshot) => snapshot, Err(error) => { - // 目录对账失败可继续服务旧快照;配置或权限变化无法确认时须暂停。 + // 目录对账失败可继续服务旧快照;配置或权限变化无法确认时须暂停 if configuration_changed { self.snapshots.suspend(); } @@ -367,7 +367,7 @@ impl RuntimeSnapshotPublisher { self.compiler.provider_catalog_generations() != *snapshot.provider_catalog_generations() } - /// 数据库提交不能被目录或通知基础设施的暂时故障伪装成回滚。 + /// 数据库提交不能被目录或通知基础设施的暂时故障伪装成回滚 async fn publish_committed_inner(&self, committed_revision: ConfigRevision) { if let Err(error) = self.refresh_with_mode(RefreshMode::Committed).await { tracing::warn!(?error, "committed runtime snapshot refresh failed"); @@ -389,7 +389,7 @@ impl RuntimeSnapshotPublisher { } } - /// 交给 Host 的周期对账与长驻订阅任务。 + /// 交给 Host 的周期对账与长驻订阅任务 pub fn worker_contributions(&self) -> Result, WorkerDefinitionError> { let reconciliation_id = WorkerId::try_new( WorkerKind::RuntimeSnapshotReconciliation, @@ -514,7 +514,7 @@ async fn wait_or_cancel(cancellation: &CancellationToken, duration: Duration) { } } -/// 当前发布版本与持久版本不一致时必须重载;缺失和回退同样 fail closed。 +/// 当前发布版本与持久版本不一致时必须重载;缺失和回退同样 fail closed #[must_use] pub fn runtime_revision_needs_refresh( published_revision: Option, diff --git a/backend/crates/gateway-core/src/settings/compiled.rs b/backend/crates/gateway-core/src/settings/compiled.rs index 32907b868..b126fc71f 100644 --- a/backend/crates/gateway-core/src/settings/compiled.rs +++ b/backend/crates/gateway-core/src/settings/compiled.rs @@ -1,3 +1,5 @@ +//! 将运行设置校验并编译为账号选择、并发排队与请求限制策略 + use std::time::Duration; use super::{InvalidSettings, SettingsValues}; @@ -7,7 +9,7 @@ use crate::{ policy::{CodexClientMinVersions, CodexClientVersion}, }; -// 持久快照和请求覆盖共用同一编译结果,避免参数校验与派生规则分叉。 +// 持久快照和请求覆盖共用同一编译结果,避免参数校验与派生规则分叉 #[derive(Debug, Clone)] pub(crate) struct CompiledSettings { pub(crate) values: SettingsValues, diff --git a/backend/crates/gateway-core/src/settings/mod.rs b/backend/crates/gateway-core/src/settings/mod.rs index c8e3e2e75..ea5f7b80c 100644 --- a/backend/crates/gateway-core/src/settings/mod.rs +++ b/backend/crates/gateway-core/src/settings/mod.rs @@ -1,4 +1,4 @@ -//! 冻结宿主基线与请求显式改写;派生快照只属于当前调用,不发布全局配置。 +//! 冻结宿主基线与请求显式改写;派生快照只属于当前调用,不发布全局配置 use std::{ collections::BTreeMap, @@ -16,14 +16,14 @@ pub(crate) mod compiled; mod values; pub use values::SettingsValues; -/// 一次模型调用的有效设置;改写只影响当前请求,不发布配置或修改持久化 revision。 +/// 一次模型调用的有效设置;改写只影响当前请求,不发布配置或修改持久化 revision #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ExecutionSettings { pub runtime: SettingsValues, pub disable_fast: bool, pub client_limits: RateLimits, - /// null 表示不限制总时长;显式时限从请求开始计时,改写不重置计时原点。 + /// null 表示不限制总时长;显式时限从请求开始计时,改写不重置计时原点 pub timeout_ms: Option, } @@ -51,7 +51,7 @@ struct HttpTimeout { change: Option>>, } -/// 共享值均不可变;同级子调用的修改不会回写父调用。 +/// 共享值均不可变;同级子调用的修改不会回写父调用 #[derive(Clone, Debug)] pub struct RequestSettings { baseline: Arc, @@ -99,7 +99,7 @@ impl RequestSettings { self.effective.settings() } - /// 来源与有效值分开查询,不允许插件伪造其他实例的写入来源。 + /// 来源与有效值分开查询,不允许插件伪造其他实例的写入来源 pub fn inspect(&self) -> Value { serde_json::json!({ "config_revision": self.baseline.revision().get(), @@ -118,7 +118,7 @@ impl RequestSettings { self.replace_scoped(self.values(), values, instance_id) } - /// Key 画像等隐式作用域结果不记录成插件改写,避免随后切换 Key 时泄漏。 + /// Key 画像等隐式作用域结果不记录成插件改写,避免随后切换 Key 时泄漏 fn replace_scoped( &self, previous: &SettingsValues, @@ -133,7 +133,7 @@ impl RequestSettings { let order = self.order.checked_add(1).ok_or(InvalidSettings)?; let mut overrides = self.overrides.as_ref().clone(); let effective_values = self.values(); - // 每个已声明设置项整体替换,不猜测画像对象、数组或 null 的深合并含义。 + // 每个已声明设置项整体替换,不猜测画像对象、数组或 null 的深合并含义 for (name, value) in next.as_object().ok_or(InvalidSettings)? { if current.get(name) != Some(value) { overrides.insert( @@ -146,7 +146,7 @@ impl RequestSettings { ); } } - // 只把本层实际修改的字段写回宿主层,作用域默认值不参与继承。 + // 只把本层实际修改的字段写回宿主层,作用域默认值不参与继承 let values = if previous == effective_values { values } else { @@ -166,7 +166,7 @@ impl RequestSettings { }) } - /// 持续会话的新请求读取当前宿主快照,只继承明确改写的字段。 + /// 持续会话的新请求读取当前宿主快照,只继承明确改写的字段 pub fn rebase(&self, snapshot: Arc) -> Result { if Arc::ptr_eq(&self.baseline, &snapshot) { return Ok(self.clone()); @@ -188,7 +188,7 @@ impl RequestSettings { }) } - /// 认证、模型入口和插件视图共用解析规则,派生策略不改变 Key 默认值。 + /// 认证、模型入口和插件视图共用解析规则,派生策略不改变 Key 默认值 #[must_use] pub fn apply_policy(&self, policy: ClientPolicy) -> ClientPolicy { let values = self.resolve_execution(policy.key_id().as_str(), policy.defaults(), None); @@ -238,7 +238,7 @@ impl RequestSettings { .as_ref() .filter(|scope| scope.client_key_id == key); let mut runtime = self.values().clone(); - // Key 画像覆盖宿主默认,插件显式覆盖整个设置项;此优先级只在这里解释。 + // Key 画像覆盖宿主默认,插件显式覆盖整个设置项;此优先级只在这里解释 if !self.overrides.contains_key("request_profiles") && !defaults.request_profiles.is_empty() { let mut profiles = runtime.request_profiles().clone(); diff --git a/backend/crates/gateway-core/src/settings/values.rs b/backend/crates/gateway-core/src/settings/values.rs index 0b28b4356..0e6c90e33 100644 --- a/backend/crates/gateway-core/src/settings/values.rs +++ b/backend/crates/gateway-core/src/settings/values.rs @@ -1,8 +1,10 @@ +//! 请求可覆盖的运行设置值,以及共享配置的不可变构造接口 + use std::{collections::BTreeMap, sync::Arc}; use crate::identity::ProviderKind; -/// 请求可覆盖的运行设置事实;编译产物不能反向改写本值。 +/// 请求可覆盖的运行设置事实;编译产物不能反向改写本值 #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields)] pub struct SettingsValues { diff --git a/backend/crates/gateway-core/src/task.rs b/backend/crates/gateway-core/src/task.rs index 613bfbbae..f45d90ffa 100644 --- a/backend/crates/gateway-core/src/task.rs +++ b/backend/crates/gateway-core/src/task.rs @@ -1,7 +1,8 @@ -//! 后台任务、leader lease 与运行计划的中立契约。 +//! 后台任务、leader lease 与运行计划的中立契约 //! -//! 本模块不执行任务,也不启动异步运行时。各业务 owner 只返回 -//! [`WorkerContribution`],由 Host 负责监督、续租、重启与关闭。 +//! 本模块不执行任务,也不启动异步运行时 +//! 各业务 owner 只返回 +//! [`WorkerContribution`],由 Host 负责监督、续租、重启与关闭 use std::fmt; use std::num::NonZeroU64; @@ -11,7 +12,7 @@ use futures::future::BoxFuture; use crate::lifecycle::CancellationToken; -/// 冻结架构中的全部后台任务类别。 +/// 冻结架构中的全部后台任务类别 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum WorkerKind { OAuthRefresh, @@ -63,7 +64,7 @@ impl fmt::Display for WorkerKind { } } -/// 一个后台任务的稳定身份;同类任务可由多个 owner 各自贡献。 +/// 一个后台任务的稳定身份;同类任务可由多个 owner 各自贡献 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct WorkerId { kind: WorkerKind, @@ -71,10 +72,10 @@ pub struct WorkerId { } impl WorkerId { - /// 从任务类别和可用于持久化 key 的 owner 创建身份。 + /// 从任务类别和可用于持久化 key 的 owner 创建身份 /// /// owner 长度为 1..=64,必须以小写 ASCII 字母或数字开头, - /// 其余字符仅允许小写 ASCII 字母、数字、`-`、`_` 和 `.`。 + /// 其余字符仅允许小写 ASCII 字母、数字、`-`、`_` 和 `.` pub fn try_new( kind: WorkerKind, owner: impl Into, @@ -103,7 +104,7 @@ impl fmt::Display for WorkerId { } } -/// 正整数 fencing token;零值无法被构造。 +/// 正整数 fencing token;零值无法被构造 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct WorkerFencingToken(NonZeroU64); @@ -125,7 +126,7 @@ impl From for WorkerFencingToken { } } -/// 一个周期任务的完整监督时序。 +/// 一个周期任务的完整监督时序 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct WorkerSchedule { interval: Duration, @@ -136,10 +137,10 @@ pub struct WorkerSchedule { } impl WorkerSchedule { - /// 构造已校验的时序。 + /// 构造已校验的时序 /// /// 所有时长必须大于零;最大退避不得小于初始退避; - /// lease 续租间隔必须严格小于 lease TTL。 + /// lease 续租间隔必须严格小于 lease TTL pub fn try_new( interval: Duration, initial_backoff: Duration, @@ -192,7 +193,7 @@ impl WorkerSchedule { } } -/// 长驻任务退出或 panic 后的 Host 重启策略。 +/// 长驻任务退出或 panic 后的 Host 重启策略 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct DaemonRestartPolicy { initial_backoff: Duration, @@ -227,7 +228,7 @@ impl DaemonRestartPolicy { } } -/// 一次 leader lease 申请。 +/// 一次 leader lease 申请 #[derive(Debug, Clone, PartialEq, Eq)] pub struct WorkerLeaseRequest { worker: WorkerId, @@ -253,10 +254,10 @@ impl WorkerLeaseRequest { } } -/// 已获取 lease 的可续租句柄。 +/// 已获取 lease 的可续租句柄 /// /// Host 在正常路径必须显式 `await` [`Self::release`];异常退出只依赖 -/// Redis TTL 兜底,实现不得在 `Drop` 中启动异步任务。 +/// Redis TTL 兜底,实现不得在 `Drop` 中启动异步任务 pub trait WorkerLeaderLeaseGuard: Send + Sync { fn fencing_token(&self) -> WorkerFencingToken; @@ -265,7 +266,7 @@ pub trait WorkerLeaderLeaseGuard: Send + Sync { fn release(self: Box) -> BoxFuture<'static, Result<(), WorkerLeaseError>>; } -/// Leader lease 的稳定获取结果。 +/// Leader lease 的稳定获取结果 pub enum WorkerLeaseAcquisition { Acquired(Box), Busy { retry_after: Option }, @@ -283,7 +284,7 @@ impl fmt::Debug for WorkerLeaseAcquisition { } } -/// 多实例 worker leader lease 的中立端口。 +/// 多实例 worker leader lease 的中立端口 pub trait WorkerLeaderLeasePort: Send + Sync { fn try_acquire( &self, @@ -291,7 +292,7 @@ pub trait WorkerLeaderLeasePort: Send + Sync { ) -> BoxFuture<'_, Result>; } -/// 周期任务收到的单周期上下文。 +/// 周期任务收到的单周期上下文 #[derive(Debug, Clone)] pub struct WorkerCycleContext { worker: WorkerId, @@ -329,19 +330,19 @@ impl WorkerCycleContext { } } -/// 由 Host 周期调用的短生命任务。 +/// 由 Host 周期调用的短生命任务 pub trait ScheduledTask: Send + Sync { fn run_cycle(&self, context: WorkerCycleContext) -> BoxFuture<'_, Result<(), WorkerTaskError>>; } -/// 长驻任务。 +/// 长驻任务 /// -/// 实现负责连接级重连退避;Host 负责任务退出或 panic 后的重启退避。 +/// 实现负责连接级重连退避;Host 负责任务退出或 panic 后的重启退避 pub trait DaemonTask: Send + Sync { fn run(&self, cancellation: CancellationToken) -> BoxFuture<'_, Result<(), WorkerTaskError>>; } -/// Host 可执行的两种任务形态,无法组合出“守护任务 + 周期调度”等非法状态。 +/// Host 可执行的两种任务形态,无法组合出“守护任务 + 周期调度”等非法状态 pub enum WorkerRunnable { Scheduled { schedule: WorkerSchedule, @@ -374,7 +375,7 @@ impl fmt::Debug for WorkerRunnable { } } -/// 一个 owner 交给 Host 的任务注册。 +/// 一个 owner 交给 Host 的任务注册 #[derive(Debug)] pub struct WorkerRegistration { pub id: WorkerId, @@ -388,7 +389,7 @@ impl WorkerRegistration { Ok(registration) } - /// 校验公开字段构造出的注册是否保持身份与 lease 时序一致。 + /// 校验公开字段构造出的注册是否保持身份与 lease 时序一致 pub fn validate(&self) -> Result<(), WorkerDefinitionError> { let WorkerRunnable::Scheduled { schedule, @@ -408,7 +409,7 @@ impl WorkerRegistration { } } -/// Final DB 明确没有相应持久化状态时允许的禁用原因。 +/// Final DB 明确没有相应持久化状态时允许的禁用原因 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum WorkerDisabledReason { NoBufferedOpsEvents, @@ -436,7 +437,7 @@ impl fmt::Display for WorkerDisabledReason { } } -/// 各包对 Host 计划的一项贡献。 +/// 各包对 Host 计划的一项贡献 #[derive(Debug)] pub enum WorkerContribution { Registration(WorkerRegistration), @@ -464,7 +465,7 @@ impl WorkerContribution { } } -/// 任务定义在交给 Host 前的稳定校验错误。 +/// 任务定义在交给 Host 前的稳定校验错误 #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] pub enum WorkerDefinitionError { #[error("worker owner is invalid")] @@ -483,7 +484,7 @@ pub enum WorkerDefinitionError { DisabledReasonMismatch, } -/// 不暴露基础设施原文或 lease resource 的错误。 +/// 不暴露基础设施原文或 lease resource 的错误 #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] #[error("{message}")] pub struct WorkerLeaseError { @@ -504,7 +505,7 @@ impl WorkerLeaseError { } } -/// 不暴露 Provider 原文或存储细节的后台任务错误。 +/// 不暴露 Provider 原文或存储细节的后台任务错误 #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] #[error("{message}")] pub struct WorkerTaskError { diff --git a/backend/crates/gateway-core/src/time.rs b/backend/crates/gateway-core/src/time.rs index 05e15eb78..a63e27246 100644 --- a/backend/crates/gateway-core/src/time.rs +++ b/backend/crates/gateway-core/src/time.rs @@ -1,4 +1,4 @@ -//! 部署时区的日历换算,持久化和比较始终使用 UTC 时间点。 +//! 部署时区的日历换算,持久化和比较始终使用 UTC 时间点 use std::str::FromStr; @@ -6,7 +6,7 @@ use chrono::{DateTime, Days, NaiveDate, NaiveDateTime, Offset as _, TimeZone as use chrono_tz::{GapInfo, Tz}; use serde::{Deserialize, Serialize}; -/// 由组合根传递的不可变 IANA 时区。 +/// 由组合根传递的不可变 IANA 时区 #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] #[serde(transparent)] pub struct DeploymentTimeZone(Tz); @@ -41,7 +41,7 @@ impl DeploymentTimeZone { value.with_timezone(&self.0) } - /// 重复本地时刻只取较早时间点,缺失时刻由调用方决定是否跳过。 + /// 重复本地时刻只取较早时间点,缺失时刻由调用方决定是否跳过 #[must_use] pub fn resolve_local(self, value: NaiveDateTime) -> Option> { self.0 @@ -50,7 +50,7 @@ impl DeploymentTimeZone { .map(|value| value.to_utc()) } - /// 午夜跳时取缺口结束点,包含整日跳过的历史时区变更。 + /// 午夜跳时取缺口结束点,包含整日跳过的历史时区变更 #[must_use] pub fn date_start(self, date: NaiveDate) -> Option> { let midnight = date.and_hms_opt(0, 0, 0)?; @@ -66,7 +66,7 @@ impl DeploymentTimeZone { self.date_start(self.local_date(value)?) } - /// 先移动本地日期再解析时间点,避免把自然日写成固定 24 小时。 + /// 先移动本地日期再解析时间点,避免把自然日写成固定 24 小时 #[must_use] pub fn days_before(self, value: DateTime, days: u64) -> Option> { self.date_start(self.local_date(value)?.checked_sub_days(Days::new(days))?) @@ -79,7 +79,7 @@ impl DeploymentTimeZone { fn local_date(self, value: DateTime) -> Option { let local = self.local(value); - // UTC 可表示的端点在叠加时区偏移后仍可能超出本地日历范围。 + // UTC 可表示的端点在叠加时区偏移后仍可能超出本地日历范围 local .naive_utc() .checked_add_offset(local.offset().fix()) diff --git a/backend/crates/gateway-core/src/upstream.rs b/backend/crates/gateway-core/src/upstream.rs index 6b1ebff49..ec4db950c 100644 --- a/backend/crates/gateway-core/src/upstream.rs +++ b/backend/crates/gateway-core/src/upstream.rs @@ -1,10 +1,10 @@ -//! 上游调用跨 Engine、Event、Error 与 Provider 共享的中立边界事实。 +//! 上游调用跨 Engine、Event、Error 与 Provider 共享的中立边界事实 use std::fmt; use crate::validation::{IdentifierError, validate_text}; -/// 上游是否可能已经收到业务 payload。 +/// 上游是否可能已经收到业务 payload #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum UpstreamSendState { NotSent, @@ -23,44 +23,45 @@ impl UpstreamSendState { } } -/// 上游 transport 注册名称。 +/// 上游 transport 注册名称 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct UpstreamTransport(String); impl UpstreamTransport { - /// 校验 transport 名称。 + /// 校验 transport 名称 /// /// # Errors /// - /// 名称无效时返回错误。 + /// 名称无效时返回错误 pub fn new(value: impl Into) -> Result { let value = value.into(); validate_text(&value, 64, true, None)?; Ok(Self(value)) } - /// 返回 transport 名称。 + /// 返回 transport 名称 #[must_use] pub fn as_str(&self) -> &str { &self.0 } } -/// Adapter 已明确分类为非 bearer 的不透明上游值。 +/// Adapter 已明确分类为非 bearer 的不透明上游值 /// -/// Core 不解释或校验内容,只通过自定义 [`Debug`] 避免它意外进入日志。协议 adapter -/// 可在原客户端响应中读取原值。 +/// Core 不解释或校验内容,只通过自定义 [`Debug`] 避免它意外进入日志 +/// 协议 adapter +/// 可在原客户端响应中读取原值 #[derive(Clone, PartialEq, Eq, Hash)] pub struct OpaqueUpstreamValue(String); impl OpaqueUpstreamValue { - /// 原样保存上游值。 + /// 原样保存上游值 #[must_use] pub fn new(value: impl Into) -> Self { Self(value.into()) } - /// 返回已经由 adapter 安全分类的原值。 + /// 返回已经由 adapter 安全分类的原值 #[must_use] pub fn as_str(&self) -> &str { &self.0 diff --git a/backend/crates/gateway-core/src/validation.rs b/backend/crates/gateway-core/src/validation.rs index 58a976436..e1d05fcba 100644 --- a/backend/crates/gateway-core/src/validation.rs +++ b/backend/crates/gateway-core/src/validation.rs @@ -1,143 +1,143 @@ -//! 领域值对象的纯校验错误与文本约束,不依赖执行事件或路由状态。 +//! 领域值对象的纯校验错误与文本约束,不依赖执行事件或路由状态 use thiserror::Error; -/// 应用层标识不满足约束。 +/// 应用层标识不满足约束 #[derive(Debug, Clone, PartialEq, Eq, Error)] pub enum IdentifierError { - /// 标识为空。 + /// 标识为空 #[error("identifier must not be empty")] Empty, - /// 标识超过核心允许的字节数。 + /// 标识超过核心允许的字节数 #[error("identifier exceeds {max_bytes} bytes")] TooLong { - /// 最大字节数。 + /// 最大字节数 max_bytes: usize, }, - /// 标识使用了保留的系统前缀。 + /// 标识使用了保留的系统前缀 #[error("identifier uses the reserved system prefix")] ReservedPrefix, - /// 标识包含控制字符。 + /// 标识包含控制字符 #[error("identifier contains control characters")] ControlCharacter, - /// 标识缺少规定的语义前缀。 + /// 标识缺少规定的语义前缀 #[error("identifier must start with `{expected}`")] MissingPrefix { - /// 规定前缀。 + /// 规定前缀 expected: &'static str, }, - /// 标识不满足该领域值对象的固定格式。 + /// 标识不满足该领域值对象的固定格式 #[error("identifier has an invalid format")] InvalidFormat, } -/// Operation 构造或校验失败。 +/// Operation 构造或校验失败 #[derive(Debug, Clone, PartialEq, Eq, Error)] pub enum OperationError { - /// 必填文本为空。 + /// 必填文本为空 #[error("`{field}` must not be empty")] EmptyField { - /// 字段名。 + /// 字段名 field: &'static str, }, - /// 数量字段为零。 + /// 数量字段为零 #[error("`{field}` must be greater than zero")] ZeroValue { - /// 字段名。 + /// 字段名 field: &'static str, }, - /// JSON 字段必须是 object。 + /// JSON 字段必须是 object #[error("`{field}` must be a JSON object")] JsonObjectRequired { - /// 字段名。 + /// 字段名 field: &'static str, }, } -/// 路由快照或 Route Plan 不满足不变量。 +/// 路由快照或 Route Plan 不满足不变量 #[derive(Debug, Clone, PartialEq, Eq, Error)] pub enum RoutingError { - /// 请求设置无法编译。 + /// 请求设置无法编译 #[error("runtime settings are invalid")] InvalidSettings, - /// Provider/model 路由标识无法构造。 + /// Provider/model 路由标识无法构造 #[error("routing identifier is invalid")] InvalidIdentifier, - /// 配置 revision 必须为正数。 + /// 配置 revision 必须为正数 #[error("config revision must be greater than zero")] InvalidRevision, - /// Restricted scope 必须保留至少一个持久化分组 binding。 + /// Restricted scope 必须保留至少一个持久化分组 binding #[error("account routing scope is invalid")] InvalidAccountScope, - /// Key 的账号范围中当前没有任何可参与路由的账号。 + /// Key 的账号范围中当前没有任何可参与路由的账号 #[error("account routing scope is empty")] EmptyAccountScope, - /// 快照中存在重复实体。 + /// 快照中存在重复实体 #[error("duplicate {entity} `{id}`")] DuplicateEntity { - /// 实体类型。 + /// 实体类型 entity: &'static str, - /// 实体 ID。 + /// 实体 ID id: String, }, - /// 实体引用不存在。 + /// 实体引用不存在 #[error("{entity} `{id}` was not found")] NotFound { - /// 实体类型。 + /// 实体类型 entity: &'static str, - /// 实体 ID。 + /// 实体 ID id: String, }, - /// 账号范围内的 Provider 目录均已知,且均没有请求或映射后的模型。 + /// 账号范围内的 Provider 目录均已知,且均没有请求或映射后的模型 #[error( "model `{model}` (mapped to `{mapped_model}`) was not found in the scoped provider catalogs" )] ModelNotFound { - /// 客户端提交的模型名称。 + /// 客户端提交的模型名称 model: String, - /// 应用模型映射后的名称。 + /// 应用模型映射后的名称 mapped_model: String, }, - /// 固定平台内没有可执行本次请求的 Provider。 + /// 固定平台内没有可执行本次请求的 Provider #[error("no provider can execute model `{model}`")] NoCapableProvider { - /// 客户端提交的模型名称。 + /// 客户端提交的模型名称 model: String, }, - /// 固定 Provider 的原生端点当前不可执行。 + /// 固定 Provider 的原生端点当前不可执行 #[error("provider endpoint `{provider}` is unavailable")] NoCapableProviderEndpoint { - /// adapter 已绑定的 Provider。 + /// adapter 已绑定的 Provider provider: String, }, - /// 固定 Provider 可用,但未声明该原生端点操作所需能力。 + /// 固定 Provider 可用,但未声明该原生端点操作所需能力 #[error("provider endpoint `{provider}` does not support `{operation}`")] UnsupportedProviderEndpoint { - /// adapter 已绑定的 Provider。 + /// adapter 已绑定的 Provider provider: String, - /// 请求的稳定操作名。 + /// 请求的稳定操作名 operation: &'static str, }, } -/// 调用方策略不满足约束或拒绝请求。 +/// 调用方策略不满足约束或拒绝请求 #[derive(Debug, Clone, PartialEq, Eq, Error)] pub enum PolicyError { - /// 请求超过调用方策略。 + /// 请求超过调用方策略 #[error("request was denied by caller policy: {reason}")] Denied { - /// 稳定拒绝原因。 + /// 稳定拒绝原因 reason: &'static str, }, } -/// 用量或价格估算不满足事实约束。 +/// 用量或价格估算不满足事实约束 #[derive(Debug, Clone, PartialEq, Eq, Error)] pub enum MeteringError { - /// 十进制定点值格式无效或超过 `numeric(20, 10)`。 + /// 十进制定点值格式无效或超过 `numeric(20, 10)` #[error("decimal value must fit unsigned numeric(20, 10)")] InvalidDecimal, - /// 货币代码无效。 + /// 货币代码无效 #[error("currency must be a three-letter uppercase ASCII code")] InvalidCurrency, } diff --git a/backend/crates/gateway-core/tests/account/location.rs b/backend/crates/gateway-core/tests/account/location.rs index 505d62748..09265886e 100644 --- a/backend/crates/gateway-core/tests/account/location.rs +++ b/backend/crates/gateway-core/tests/account/location.rs @@ -1,3 +1,5 @@ +//! 验证账号代理位置的规范化、字段约束与代理变更后的清理 + use gateway_core::account::{InvalidRequestLocation, RequestLocation}; use serde_json::json; diff --git a/backend/crates/gateway-core/tests/account/mod.rs b/backend/crates/gateway-core/tests/account/mod.rs index 6d2458101..4c878a378 100644 --- a/backend/crates/gateway-core/tests/account/mod.rs +++ b/backend/crates/gateway-core/tests/account/mod.rs @@ -1,3 +1,5 @@ +//! 账号领域测试入口,以及候选账号与调度输入构造辅助 + mod location; mod model_access; mod proxy; @@ -1107,7 +1109,7 @@ fn reserved_concurrency_only_blocks_requests_that_cannot_use_the_reserved_slots( .capacity_snapshot(&candidates, &normal) .unwrap(); assert_eq!((capacity.used_slots(), capacity.total_slots()), (4, 4)); - // 预留造成的暂满仍是可等待的容量约束,不能被当成不可用账号。 + // 预留造成的暂满仍是可等待的容量约束,不能被当成不可用账号 assert_eq!( AccountSelector.wait_candidates(&candidates, &normal).len(), 1 @@ -1125,7 +1127,7 @@ fn reserved_concurrency_only_blocks_requests_that_cannot_use_the_reserved_slots( let full = [candidate_with_concurrency("acct_reserved", 5, 5)]; assert!(AccountSelector.select(&full, &prioritized).is_none()); - // 预留不能让普通请求失去全部名额,也不改变不限并发。 + // 预留不能让普通请求失去全部名额,也不改变不限并发 let mut oversized = context(RotationStrategy::Smart); oversized.reserved_concurrency = 10; assert_eq!(oversized.concurrency_limit(&candidates[0].account).get(), 1); diff --git a/backend/crates/gateway-core/tests/account/model_access.rs b/backend/crates/gateway-core/tests/account/model_access.rs index 63716981e..6cc377dba 100644 --- a/backend/crates/gateway-core/tests/account/model_access.rs +++ b/backend/crates/gateway-core/tests/account/model_access.rs @@ -1,3 +1,5 @@ +//! 验证账号模型访问设置的边界、去重与模型标识保留 + use gateway_core::account::AccountModelAccess; use serde_json::json; diff --git a/backend/crates/gateway-core/tests/account/proxy.rs b/backend/crates/gateway-core/tests/account/proxy.rs index 21b6d92ce..01feb677f 100644 --- a/backend/crates/gateway-core/tests/account/proxy.rs +++ b/backend/crates/gateway-core/tests/account/proxy.rs @@ -1,3 +1,5 @@ +//! 验证代理端点的协议限制、凭据脱敏与非法配置拒绝 + use gateway_core::account::OutboundProxy; #[test] diff --git a/backend/crates/gateway-core/tests/account/selection.rs b/backend/crates/gateway-core/tests/account/selection.rs index e9fa3cfd1..febcb2005 100644 --- a/backend/crates/gateway-core/tests/account/selection.rs +++ b/backend/crates/gateway-core/tests/account/selection.rs @@ -1,3 +1,5 @@ +//! 验证账号选择、容量限制与失败反馈的衰减和并发更新 + use std::time::{Duration, Instant}; use gateway_core::account::{ diff --git a/backend/crates/gateway-core/tests/account/selection_diagnostics.rs b/backend/crates/gateway-core/tests/account/selection_diagnostics.rs index b28016e18..41b400b5d 100644 --- a/backend/crates/gateway-core/tests/account/selection_diagnostics.rs +++ b/backend/crates/gateway-core/tests/account/selection_diagnostics.rs @@ -1,3 +1,5 @@ +//! 验证账号选择诊断保留选择依据,并限制候选记录数量 + use gateway_core::account::{AccountSelector, RotationStrategy}; use gateway_core::diagnostics::TraceContext; use serde_json::json; @@ -47,7 +49,7 @@ fn selection_trace_should_bound_candidates_and_preserve_the_selected_account() { .collect::>(); candidates.push(candidate("acct_best", 0, Some(10_000))); let trace = TraceContext::new("req_many_candidates"); - // 选号位于首部保留区之外,也应在长流的常规事件淘汰后保留。 + // 选号位于首部保留区之外,也应在长流的常规事件淘汰后保留 for _ in 0..10 { trace.record("test.event", json!({})); } diff --git a/backend/crates/gateway-core/tests/account/smart_scheduling.rs b/backend/crates/gateway-core/tests/account/smart_scheduling.rs index c6aa6cab4..bb8db1c24 100644 --- a/backend/crates/gateway-core/tests/account/smart_scheduling.rs +++ b/backend/crates/gateway-core/tests/account/smart_scheduling.rs @@ -1,3 +1,5 @@ +//! 验证智能调度权重、软亲和与容量约束的组合行为 + use gateway_core::account::{ AccountSelector, PreferredAccountSelection, RotationStrategy, SmartSchedulingConfig, }; @@ -143,7 +145,7 @@ fn queue_weight_balances_live_pressure_and_health_only_when_enabled_for_smart() expects_shortest, "{strategy:?}, {queue_weight}" ); - // 新请求不能绕过已经选定的队列,取消请求同步归还位置。 + // 新请求不能绕过已经选定的队列,取消请求同步归还位置 let newcomer = CapacityWait::new( &queue, policy, @@ -186,7 +188,7 @@ fn queue_weight_does_not_widen_immediate_selection_tolerance() { context.policy = context.policy.with_smart_scheduling( SmartSchedulingConfig::new([0.0, 0.0, 0.0, 0.0, 0.0, 1.0], false).unwrap(), ); - // 仅开启排队系数时,立即可用的同权重账号按既有游标轮换。 + // 仅开启排队系数时,立即可用的同权重账号按既有游标轮换 assert_eq!( AccountSelector .select(&candidates, &context) @@ -249,7 +251,7 @@ fn proportional_coefficients_preserve_near_best_rotation_and_diagnostics() { candidate_with_concurrency("acct_b", 0, 100), candidate_with_concurrency("acct_c", 0, 100), ]; - // 默认额度权重 0.8 下,9375 正好位于容差边界,9374 刚好超出。 + // 默认额度权重 0.8 下,9375 正好位于容差边界,9374 刚好超出 for (candidate, quota) in candidates.iter_mut().zip([10_000, 9_375, 9_374]) { candidate.signals.quota_remaining_rank = Some(quota); } diff --git a/backend/crates/gateway-core/tests/concurrency.rs b/backend/crates/gateway-core/tests/concurrency.rs index 87b02d254..c9a44621c 100644 --- a/backend/crates/gateway-core/tests/concurrency.rs +++ b/backend/crates/gateway-core/tests/concurrency.rs @@ -1,3 +1,5 @@ +//! 验证并发等待队列的容量、顺序、优先级、超时与取消回收 + use std::time::{Duration, Instant, SystemTime}; use futures::{FutureExt, executor::block_on}; @@ -121,7 +123,7 @@ fn new_layers_and_retries_cannot_restart_a_requests_wait_budget() { key_wait.wait(&["key"]).await.unwrap(); drop(key_wait); - // 第一层已取得重试机会,后续准备跨过总时限后不能在账号层重新排队。 + // 第一层已取得重试机会,后续准备跨过总时限后不能在账号层重新排队 futures_timer::Delay::new(policy.timeout).await; let next_attempt_budget = budget.clone(); let mut account_wait = @@ -132,7 +134,7 @@ fn new_layers_and_retries_cannot_restart_a_requests_wait_budget() { ); assert!(!accounts.has_waiters(&"account")); - // 另一个请求仍有完整预算,不能受上一请求超时影响。 + // 另一个请求仍有完整预算,不能受上一请求超时影响 let independent_budget = ConcurrencyWaitBudget::default(); let mut independent = CapacityWait::new(&accounts, policy, request_deadline, &independent_budget); @@ -176,7 +178,7 @@ fn custom_priority_reads_live_counts_and_preserves_limits_and_fifo() { assert_eq!(observed, [("a", 1), ("b", 0)]); assert_eq!(*tail.key(), "a"); assert!(tail.turn().now_or_never().is_none()); - // 满队列不参与评分,不能因偏好而突破上限。 + // 满队列不参与评分,不能因偏好而突破上限 let fallback = queue .enqueue_with_priority(&["a", "b"], 2, deadline, |key, count| { assert_eq!((*key, count), ("b", 0)); @@ -250,11 +252,11 @@ fn high_priority_waiters_go_ahead_of_normal_waiters_and_keep_fifo_among_themselv let first_budget = ConcurrencyWaitBudget::default(); let mut first_high = CapacityWait::new(&queue, policy, request_deadline, &first_budget) .with_priority(WaitPriority::High); - // 只有普通等待者时,高优先级请求可以直接尝试租约,普通请求仍须排队。 + // 只有普通等待者时,高优先级请求可以直接尝试租约,普通请求仍须排队 assert!(first_high.can_try(&"a")); assert!(!normal.can_try(&"a")); - // 普通等待者已占满单队列上限,高优先级仍可入队并直接成为队首。 + // 普通等待者已占满单队列上限,高优先级仍可入队并直接成为队首 first_high.wait(&["a"]).await.unwrap(); let second_budget = ConcurrencyWaitBudget::default(); let mut second_high = CapacityWait::new(&queue, policy, request_deadline, &second_budget) @@ -288,7 +290,7 @@ fn displaced_normal_head_yields_its_lease_attempt_until_high_priority_waiters_le normal.wait(&["account"]).await.unwrap(); assert!(normal.can_try(&"account")); - // 普通队首已经开始重读容量,尚未取得租约时被高优先级请求插队。 + // 普通队首已经开始重读容量,尚未取得租约时被高优先级请求插队 let mut high = CapacityWait::new(&queue, policy, deadline, &high_budget) .with_priority(WaitPriority::High); high.wait(&["account"]).await.unwrap(); @@ -296,7 +298,7 @@ fn displaced_normal_head_yields_its_lease_attempt_until_high_priority_waiters_le assert!(high.can_try(&"account")); assert!(normal.wait(&["account"]).now_or_never().is_none()); - // 审批取消也必须恢复原队首的资格,不能丢失其等待位置。 + // 审批取消也必须恢复原队首的资格,不能丢失其等待位置 drop(high); normal.wait(&["account"]).await.unwrap(); assert!(normal.can_try(&"account")); diff --git a/backend/crates/gateway-core/tests/diagnostics/capture.rs b/backend/crates/gateway-core/tests/diagnostics/capture.rs index 5c3d72ff4..d126c4737 100644 --- a/backend/crates/gateway-core/tests/diagnostics/capture.rs +++ b/backend/crates/gateway-core/tests/diagnostics/capture.rs @@ -1,3 +1,5 @@ +//! 验证诊断采集的敏感信息过滤与结构、大小边界 + use gateway_core::diagnostics::{body_fingerprint, diagnostic_headers, diagnostic_json}; use serde_json::{Value, json}; diff --git a/backend/crates/gateway-core/tests/diagnostics/mod.rs b/backend/crates/gateway-core/tests/diagnostics/mod.rs index 6f8d1e95c..a3d45f334 100644 --- a/backend/crates/gateway-core/tests/diagnostics/mod.rs +++ b/backend/crates/gateway-core/tests/diagnostics/mod.rs @@ -1,3 +1,5 @@ +//! 诊断采集、流重组与调用追踪的测试入口 + mod capture; mod stream; mod trace; diff --git a/backend/crates/gateway-core/tests/diagnostics/stream.rs b/backend/crates/gateway-core/tests/diagnostics/stream.rs index 664349188..3c9840fbe 100644 --- a/backend/crates/gateway-core/tests/diagnostics/stream.rs +++ b/backend/crates/gateway-core/tests/diagnostics/stream.rs @@ -1,3 +1,5 @@ +//! 验证 SSE 诊断流跨分块重组、超限恢复与未完成流识别 + use gateway_core::diagnostics::{StreamCapture, StreamFormat, TraceContext, body_fingerprint}; #[test] @@ -12,7 +14,7 @@ fn sse_capture_reassembles_every_chunk_boundary_and_preserves_unknown_events() { let snapshot = trace.snapshot().unwrap(); let events = snapshot["events"].as_array().unwrap(); assert_eq!(events.len(), 3, "boundary {boundary}"); - // 未知 SSE 名称和伪装成头部的 JSON 仍有摘要,但不能获得协议字段的明文权限。 + // 未知 SSE 名称和伪装成头部的 JSON 仍有摘要,但不能获得协议字段的明文权限 assert_eq!( events[0]["data"]["eventType"], body_fingerprint(b"future.metadata") diff --git a/backend/crates/gateway-core/tests/diagnostics/trace.rs b/backend/crates/gateway-core/tests/diagnostics/trace.rs index c319136c3..e323df27e 100644 --- a/backend/crates/gateway-core/tests/diagnostics/trace.rs +++ b/backend/crates/gateway-core/tests/diagnostics/trace.rs @@ -1,3 +1,5 @@ +//! 验证有界调用追踪保留关键事件并持续执行脱敏规则 + use std::{ fmt::Write as _, sync::{Arc, Mutex}, @@ -284,7 +286,7 @@ fn size_gaps_and_truncated_events_do_not_reintroduce_user_content() { #[test] fn ordinary_trace_logs_and_serialized_snapshots_share_the_sanitized_capture_boundary() { - // tracing 的 callsite 缓存是进程级的;隔离并行测试的注册,不修改生产日志或全局 subscriber。 + // tracing 的 callsite 缓存是进程级的;隔离并行测试的注册,不修改生产日志或全局 subscriber const CHILD: &str = "GATEWAY_CORE_DIAGNOSTICS_LOG_TEST"; if std::env::var_os(CHILD).is_none() { let thread = std::thread::current(); @@ -301,7 +303,7 @@ fn ordinary_trace_logs_and_serialized_snapshots_share_the_sanitized_capture_boun ); return; } - // 仅捕获本线程的普通 trace 事件,不打开 request_dump,也不读写真实日志文件。 + // 仅捕获本线程的普通 trace 事件,不打开 request_dump,也不读写真实日志文件 let logs = Arc::new(Mutex::new(Vec::new())); let snapshot = tracing::subscriber::with_default(TraceLog(Arc::clone(&logs)), || { let trace = TraceContext::new("req_feedback"); @@ -326,7 +328,7 @@ fn ordinary_trace_logs_and_serialized_snapshots_share_the_sanitized_capture_boun assert_eq!(snapshot["wireFrames"], 0); snapshot }); - // API / 反馈导出使用的是这个 Value 的序列化结果,而不只是 Debug 表示。 + // API / 反馈导出使用的是这个 Value 的序列化结果,而不只是 Debug 表示 let exported = String::from_utf8(serde_json::to_vec(&snapshot).unwrap()).unwrap(); assert!(!exported.contains("PRIVATE_"), "{exported}"); let logs = logs.lock().unwrap(); diff --git a/backend/crates/gateway-core/tests/engine/admission.rs b/backend/crates/gateway-core/tests/engine/admission.rs index 43ba32607..ba8808f49 100644 --- a/backend/crates/gateway-core/tests/engine/admission.rs +++ b/backend/crates/gateway-core/tests/engine/admission.rs @@ -1,3 +1,5 @@ +//! 验证启动时准入恢复的精确事实、调用顺序与失败拒绝 + use std::collections::VecDeque; use std::sync::Mutex; use std::time::{Duration, SystemTime}; diff --git a/backend/crates/gateway-core/tests/engine/connection.rs b/backend/crates/gateway-core/tests/engine/connection.rs index fe73ddf95..4a8e4e484 100644 --- a/backend/crates/gateway-core/tests/engine/connection.rs +++ b/backend/crates/gateway-core/tests/engine/connection.rs @@ -1,3 +1,5 @@ +//! 验证各传输共享建连恢复预算,且不限制成功建连后的执行 + use gateway_core::engine::connection::ConnectionBudget; #[test] diff --git a/backend/crates/gateway-core/tests/engine/continuation.rs b/backend/crates/gateway-core/tests/engine/continuation.rs index dc27a7afa..9083c17df 100644 --- a/backend/crates/gateway-core/tests/engine/continuation.rs +++ b/backend/crates/gateway-core/tests/engine/continuation.rs @@ -1,3 +1,5 @@ +//! 验证原生续接标识的脱敏、账号绑定与 Client Key 隔离 + use gateway_core::account::ProviderAccountId; use gateway_core::engine::continuation::{ ContinuationBinding, NativeContinuationPin, PreviousResponseId, diff --git a/backend/crates/gateway-core/tests/engine/coordinator.rs b/backend/crates/gateway-core/tests/engine/coordinator.rs index 81a4791ed..ebf50f2c1 100644 --- a/backend/crates/gateway-core/tests/engine/coordinator.rs +++ b/backend/crates/gateway-core/tests/engine/coordinator.rs @@ -1,4 +1,4 @@ -//! 单行 `model_requests`、账号重试与下游提交屏障测试。 +//! 单行 `model_requests`、账号重试与下游提交屏障测试 use std::collections::{BTreeMap, BTreeSet, VecDeque}; use std::num::NonZeroU32; @@ -278,7 +278,7 @@ enum Script { account_id: &'static str, items: Vec>, }, - /// 产出 `items` 后永久悬挂的流;用于逼出会话级 deadline。 + /// 产出 `items` 后永久悬挂的流;用于逼出会话级 deadline HangingStream { account_id: &'static str, items: Vec>, @@ -368,7 +368,7 @@ impl Provider for ScriptedProvider { .expect("operations lock") .push(request.operation().clone()); self.contexts.lock().expect("contexts lock").push(context); - // 模拟官方发布在每次上游尝试开始后推进,重试应继续使用首次解析版本。 + // 模拟官方发布在每次上游尝试开始后推进,重试应继续使用首次解析版本 self.profile_generation.fetch_add(1, Ordering::SeqCst); let script = self .scripts @@ -2651,7 +2651,7 @@ fn retryable_error_after_credential_recovery_switches_account_instead_of_termina assert_eq!(contexts.len(), 3); assert_eq!(contexts[1].required_account(), Some(&first)); assert!(contexts[1].credential_recovery_attempted()); - // recovery 钉账号只绑定 replay attempt;replay 上的 429 之后必须能换号。 + // recovery 钉账号只绑定 replay attempt;replay 上的 429 之后必须能换号 assert_eq!(contexts[2].required_account(), None); assert!(contexts[2].excluded_accounts().contains(&first)); let state = store.state.lock().expect("store lock"); @@ -2754,7 +2754,7 @@ fn rate_limited_account_exhaustion_survives_a_later_empty_selection() { assert_eq!(state.attempts.len(), 1); assert_eq!(state.intermediate_failures, 1); assert_eq!(state.finalizations.len(), 1); - // attempt-1 已把 sent 落库;attempt-2 空选路终态不得降级回 not_sent。 + // attempt-1 已把 sent 落库;attempt-2 空选路终态不得降级回 not_sent assert_eq!(state.finalizations[0].send_state, UpstreamSendState::Sent); assert_eq!(state.finalizations[0].upstream_status_code, Some(429)); assert_eq!( @@ -4383,7 +4383,7 @@ fn interrupted_first_attempt_write_never_restarts_selection_or_creates_a_zero_at drop(next); assert_eq!(store.state.lock().unwrap().created, 1); assert_eq!(provider.released_leases.load(Ordering::SeqCst), 0); - // 首写属于可丢弃观测;丢失返回确认后不臆测入库成功,也不能退回零次重新创建。 + // 首写属于可丢弃观测;丢失返回确认后不臆测入库成功,也不能退回零次重新创建 assert!(release.send(()).is_err()); if cancel { session.cancel_and_finalize().await.unwrap(); diff --git a/backend/crates/gateway-core/tests/engine/execution.rs b/backend/crates/gateway-core/tests/engine/execution.rs index 3a85680ed..a694046c9 100644 --- a/backend/crates/gateway-core/tests/engine/execution.rs +++ b/backend/crates/gateway-core/tests/engine/execution.rs @@ -1,3 +1,5 @@ +//! 执行服务的准入、预算、请求记录与终结行为测试 + use std::{ collections::{BTreeMap, BTreeSet, VecDeque}, sync::{ @@ -720,7 +722,7 @@ fn detached_early_failure_resumes_cancelled_store_write_and_settles_once_for_all store.finalizes.load(Ordering::SeqCst), usize::from(!suspend_create) ); - // receiver 已从 Store 替身取走;只有延续原 future 才能继续接收此信号。 + // receiver 已从 Store 替身取走;只有延续原 future 才能继续接收此信号 complete_write .send(()) .expect("detached cleanup retains the original store write"); @@ -2831,7 +2833,7 @@ fn settlement_failure_keeps_provider_error_and_releases_concurrency_once() { )); assert!(started.session.is_finalized()); started.session.detach_finalize().await; - // Store 端口已接管精确费用后,结算错误不能改写 Provider 错误或触发第二次结算。 + // Store 端口已接管精确费用后,结算错误不能改写 Provider 错误或触发第二次结算 assert_cleanup_completed(&admissions, &budget, &started.request_id); } }); diff --git a/backend/crates/gateway-core/tests/engine/extensions.rs b/backend/crates/gateway-core/tests/engine/extensions.rs index 1570afeb0..739fbb0da 100644 --- a/backend/crates/gateway-core/tests/engine/extensions.rs +++ b/backend/crates/gateway-core/tests/engine/extensions.rs @@ -1,3 +1,5 @@ +//! 执行测试共用的扩展代次引用与就绪租约替身 + use std::sync::Arc; use gateway_core::runtime::extensions::{ExtensionSetId, ExtensionSetLease, ExtensionSetReference}; diff --git a/backend/crates/gateway-core/tests/engine/middleware.rs b/backend/crates/gateway-core/tests/engine/middleware.rs index a4d881371..24bb2b665 100644 --- a/backend/crates/gateway-core/tests/engine/middleware.rs +++ b/backend/crates/gateway-core/tests/engine/middleware.rs @@ -1,3 +1,5 @@ +//! 请求中间件的改写、交付状态与发布代次持有测试 + use std::num::NonZeroU32; use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -211,7 +213,7 @@ fn capability_declaration_preserves_original_semantics_and_recomputes_upstream_n serde_json::to_vec(&converted).unwrap() ); - // 后续普通正文修改也不能删掉未承担的原始语义,或藏起实际新增的需求。 + // 后续普通正文修改也不能删掉未承担的原始语义,或藏起实际新增的需求 converted.as_object_mut().unwrap().remove("tools"); converted["input"] = json!([{"type":"input_image","image_url":"synthetic"}]); let request = request diff --git a/backend/crates/gateway-core/tests/engine/mod.rs b/backend/crates/gateway-core/tests/engine/mod.rs index ab3d565f7..6553e9cf8 100644 --- a/backend/crates/gateway-core/tests/engine/mod.rs +++ b/backend/crates/gateway-core/tests/engine/mod.rs @@ -1,3 +1,5 @@ +//! 执行引擎各阶段与端口合同的测试入口 + mod admission; mod connection; mod continuation; diff --git a/backend/crates/gateway-core/tests/engine/policy.rs b/backend/crates/gateway-core/tests/engine/policy.rs index 298206835..dfd6a6792 100644 --- a/backend/crates/gateway-core/tests/engine/policy.rs +++ b/backend/crates/gateway-core/tests/engine/policy.rs @@ -1,3 +1,5 @@ +//! 验证插件调度策略对软亲和的覆盖与宿主候选范围限制 + use std::{ collections::BTreeSet, num::NonZeroU32, diff --git a/backend/crates/gateway-core/tests/engine/probe.rs b/backend/crates/gateway-core/tests/engine/probe.rs index f7a8c6c0f..13d5d9d30 100644 --- a/backend/crates/gateway-core/tests/engine/probe.rs +++ b/backend/crates/gateway-core/tests/engine/probe.rs @@ -1,3 +1,5 @@ +//! 验证账号探测可通过对象安全的执行链端口调用 + use gateway_core::engine::probe::AccountProbe; #[test] diff --git a/backend/crates/gateway-core/tests/engine/provider.rs b/backend/crates/gateway-core/tests/engine/provider.rs index b6fb5bca1..ef3722751 100644 --- a/backend/crates/gateway-core/tests/engine/provider.rs +++ b/backend/crates/gateway-core/tests/engine/provider.rs @@ -1,3 +1,5 @@ +//! 验证 Provider 注册唯一性与模型能力目录查询 + use std::collections::BTreeSet; use std::num::NonZeroU32; use std::sync::Arc; diff --git a/backend/crates/gateway-core/tests/engine/response_control.rs b/backend/crates/gateway-core/tests/engine/response_control.rs index b6c2af709..4dc8c352b 100644 --- a/backend/crates/gateway-core/tests/engine/response_control.rs +++ b/backend/crates/gateway-core/tests/engine/response_control.rs @@ -1,3 +1,5 @@ +//! 验证响应中断的当前 owner 校验及独立执行间的控制隔离 + use futures::FutureExt; use gateway_core::engine::response_control::{ResponseControl, ResponseInterruptError}; diff --git a/backend/crates/gateway-core/tests/error.rs b/backend/crates/gateway-core/tests/error.rs index 34e8720a1..2e7aa6d64 100644 --- a/backend/crates/gateway-core/tests/error.rs +++ b/backend/crates/gateway-core/tests/error.rs @@ -1,3 +1,5 @@ +//! 验证执行错误的敏感信息脱敏、稳定快照与请求局部数据边界 + use bytes::Bytes; use gateway_core::error::{ ClientVisibleUpstreamError, ClientVisibleUpstreamResponse, GatewayError, OpaqueUpstreamValue, diff --git a/backend/crates/gateway-core/tests/event.rs b/backend/crates/gateway-core/tests/event.rs index eb2d4c55d..b0432e965 100644 --- a/backend/crates/gateway-core/tests/event.rs +++ b/backend/crates/gateway-core/tests/event.rs @@ -1,3 +1,5 @@ +//! 验证 Provider 事件的协议保留、敏感值保护与事件序列约束 + use bytes::Bytes; use gateway_core::event::{ ContentItem, ContentKind, EventSequenceError, EventSequenceValidator, GatewayEvent, diff --git a/backend/crates/gateway-core/tests/health.rs b/backend/crates/gateway-core/tests/health.rs index 9f176a761..7cfce97c1 100644 --- a/backend/crates/gateway-core/tests/health.rs +++ b/backend/crates/gateway-core/tests/health.rs @@ -1,3 +1,5 @@ +//! 验证健康探针的对象安全接口与 Worker 身份投影 + use futures::future::BoxFuture; use gateway_core::health::{ HealthProbe, HealthState, WorkerHealthKey, WorkerHealthSnapshot, WorkerHealthSource, diff --git a/backend/crates/gateway-core/tests/lifecycle.rs b/backend/crates/gateway-core/tests/lifecycle.rs index 163a44d36..2fbd37f06 100644 --- a/backend/crates/gateway-core/tests/lifecycle.rs +++ b/backend/crates/gateway-core/tests/lifecycle.rs @@ -1,3 +1,5 @@ +//! 验证取消树的唤醒、父子隔离、竞态处理与资源释放 + use std::future::Future as _; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, Barrier}; @@ -9,7 +11,7 @@ use gateway_core::lifecycle::CancellationToken; #[test] fn discarded_cancellation_branches_release_allocations() { let token = CancellationToken::new(); - // 预热一次,允许取消信号保留固定大小的通知状态。 + // 预热一次,允许取消信号保留固定大小的通知状态 discard_cancellation_branch(&token); let allocations = allocation_counter::measure(|| { @@ -62,7 +64,7 @@ fn completing_child_cancellation_releases_ancestor_allocations() { } fn discard_cancellation_branch(token: &CancellationToken) { - // select 先轮询取消分支,再由另一个就绪分支获胜并丢弃取消等待。 + // select 先轮询取消分支,再由另一个就绪分支获胜并丢弃取消等待 let selected = futures::future::select(Box::pin(token.cancelled()), futures::future::ready(())) .now_or_never(); assert!(matches!(selected, Some(futures::future::Either::Right(_)))); @@ -167,7 +169,7 @@ fn cancellation_racing_with_wait_registration_and_drop_does_not_lose_wakeup() { .is_pending(); finished.wait(); - // 取消完成后再检查,既验证就绪结果,也验证 Pending 路径实际收到唤醒。 + // 取消完成后再检查,既验证就绪结果,也验证 Pending 路径实际收到唤醒 if pending { assert!(counter.0.load(Ordering::Relaxed) > 0); assert!(waiting.now_or_never().is_some()); diff --git a/backend/crates/gateway-core/tests/main.rs b/backend/crates/gateway-core/tests/main.rs index 5e5b5d9e5..bc31e960d 100644 --- a/backend/crates/gateway-core/tests/main.rs +++ b/backend/crates/gateway-core/tests/main.rs @@ -1,3 +1,5 @@ +//! 核心领域、执行引擎与运行时模块的测试入口 + mod account; mod concurrency; mod diagnostics; diff --git a/backend/crates/gateway-core/tests/metering/mod.rs b/backend/crates/gateway-core/tests/metering/mod.rs index 2f00bb087..410454528 100644 --- a/backend/crates/gateway-core/tests/metering/mod.rs +++ b/backend/crates/gateway-core/tests/metering/mod.rs @@ -1,3 +1,5 @@ +//! 计量测试入口,以及金额精度、用量与费用来源合同测试 + use std::str::FromStr; mod pricing; diff --git a/backend/crates/gateway-core/tests/metering/pricing.rs b/backend/crates/gateway-core/tests/metering/pricing.rs index 2e4a6389d..16fd1e245 100644 --- a/backend/crates/gateway-core/tests/metering/pricing.rs +++ b/backend/crates/gateway-core/tests/metering/pricing.rs @@ -1,3 +1,5 @@ +//! 验证价格精度、人工覆盖与费用倍率的计算和快照保留 + use gateway_core::metering::{ModelPriceOverride, PricingOverrides, TokenPrice, merge_pricing}; use serde_json::json; diff --git a/backend/crates/gateway-core/tests/middleware/mod.rs b/backend/crates/gateway-core/tests/middleware/mod.rs index fdada259c..40bd79752 100644 --- a/backend/crates/gateway-core/tests/middleware/mod.rs +++ b/backend/crates/gateway-core/tests/middleware/mod.rs @@ -1,3 +1,5 @@ +//! 验证中间件嵌套调用顺序、类型传递与短路后的资源释放 + use std::sync::{Arc, Mutex}; use gateway_core::middleware::{Middleware, compose}; diff --git a/backend/crates/gateway-core/tests/operation.rs b/backend/crates/gateway-core/tests/operation.rs index e9b287c10..ba11b460d 100644 --- a/backend/crates/gateway-core/tests/operation.rs +++ b/backend/crates/gateway-core/tests/operation.rs @@ -1,3 +1,5 @@ +//! 验证生成请求保留不透明正文,并按协议解析能力与转换边界 + use bytes::Bytes; use gateway_core::operation::{ Feature, GenerateRequest, ImageRequest, ImageRequestKind, Operation, OperationKind, diff --git a/backend/crates/gateway-core/tests/policy/mod.rs b/backend/crates/gateway-core/tests/policy/mod.rs index e397b22d9..080e81662 100644 --- a/backend/crates/gateway-core/tests/policy/mod.rs +++ b/backend/crates/gateway-core/tests/policy/mod.rs @@ -1,3 +1,5 @@ +//! 验证 Client Key 准入、限额、凭据保护与客户端版本策略 + use std::sync::Arc; use gateway_core::policy::{ diff --git a/backend/crates/gateway-core/tests/provider_ports.rs b/backend/crates/gateway-core/tests/provider_ports.rs index 21ff4784e..06c3c3b87 100644 --- a/backend/crates/gateway-core/tests/provider_ports.rs +++ b/backend/crates/gateway-core/tests/provider_ports.rs @@ -1,3 +1,5 @@ +//! 验证 Provider 协调端口的敏感值保护、有效期与刷新边界 + use std::collections::BTreeMap; use std::num::NonZeroU32; use std::time::{Duration, SystemTime}; diff --git a/backend/crates/gateway-core/tests/routing/mod.rs b/backend/crates/gateway-core/tests/routing/mod.rs index e53b226a4..a6d66860e 100644 --- a/backend/crates/gateway-core/tests/routing/mod.rs +++ b/backend/crates/gateway-core/tests/routing/mod.rs @@ -1,3 +1,5 @@ +//! 路由领域测试入口,以及版本、账号范围与 Key 策略约束测试 + use std::collections::{BTreeMap, BTreeSet}; use std::sync::Arc; diff --git a/backend/crates/gateway-core/tests/routing/snapshot.rs b/backend/crates/gateway-core/tests/routing/snapshot.rs index 97666dc23..eee666ace 100644 --- a/backend/crates/gateway-core/tests/routing/snapshot.rs +++ b/backend/crates/gateway-core/tests/routing/snapshot.rs @@ -1,3 +1,5 @@ +//! 验证路由快照编译的模型别名、版本一致性与请求事实冻结 + use std::collections::BTreeMap; use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; diff --git a/backend/crates/gateway-core/tests/runtime/extensions.rs b/backend/crates/gateway-core/tests/runtime/extensions.rs index eb9b79044..b8488ebc1 100644 --- a/backend/crates/gateway-core/tests/runtime/extensions.rs +++ b/backend/crates/gateway-core/tests/runtime/extensions.rs @@ -1,3 +1,5 @@ +//! 验证扩展发布代次的在途引用保留与故障隔离 + use std::sync::{ Arc, atomic::{AtomicBool, AtomicUsize, Ordering}, diff --git a/backend/crates/gateway-core/tests/runtime/mod.rs b/backend/crates/gateway-core/tests/runtime/mod.rs index ede4c083b..7915b1dbc 100644 --- a/backend/crates/gateway-core/tests/runtime/mod.rs +++ b/backend/crates/gateway-core/tests/runtime/mod.rs @@ -1,3 +1,5 @@ +//! 验证运行时快照发布、版本对账与并发更新行为 + use std::collections::BTreeMap; mod extensions; use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; @@ -253,7 +255,7 @@ fn committed_account_change_should_preempt_inflight_catalog_reconciliation() { Arc::new(TestSnapshotSubscriptions::default()), ); - // 首次提交复用目录,随后对账停在慢 Provider 查询;下一次提交仍须及时撤权。 + // 首次提交复用目录,随后对账停在慢 Provider 查询;下一次提交仍须及时撤权 *store.facts.lock().expect("facts lock") = Ok(scoped_facts(10, true)); *store.current_revision.lock().expect("revision lock") = Ok(revision(10)); publisher.publish_committed(revision(10)).await; @@ -382,7 +384,7 @@ fn overlapping_refreshes_should_not_restore_revoked_account_scope() { ); let other_publisher = publisher.clone(); - // 旧刷新已读完 revision 10 的完整事实,停在目录查询;不用 sleep 碰调度概率。 + // 旧刷新已读完 revision 10 的完整事实,停在目录查询;不用 sleep 碰调度概率 let mut old = Box::pin(publisher.refresh()); assert!(futures::poll!(old.as_mut()).is_pending()); assert_eq!(catalog.queries.load(Ordering::SeqCst), 1); @@ -511,7 +513,7 @@ fn subscription_refresh_should_wait_for_inflight_compile_and_reload_authoritativ let mut old = Box::pin(publisher.refresh()); assert!(futures::poll!(old.as_mut()).is_pending()); *store.facts.lock().expect("facts lock") = Ok(scoped_facts(11, false)); - // 通知只是提示,即使版本过期也要在取得发布权后重新读取 Store。 + // 通知只是提示,即使版本过期也要在取得发布权后重新读取 Store notifications .unbounded_send(Ok(revision(3))) .expect("notify"); @@ -549,7 +551,7 @@ fn reconciliation_should_fail_closed_on_persisted_revision_rollback_and_recover( task.run_cycle(context.clone()).await.expect("recover"); assert_eq!(handle.revision(), Some(revision(7))); - // 有效的持久回退也必须发布,不能用 revision 数值单调性掩盖竞争。 + // 有效的持久回退也必须发布,不能用 revision 数值单调性掩盖竞争 *store.facts.lock().expect("facts lock") = Ok(facts(6, 6)); *store.current_revision.lock().expect("revision lock") = Ok(revision(6)); task.run_cycle(context).await.expect("publish rollback"); diff --git a/backend/crates/gateway-core/tests/settings/mod.rs b/backend/crates/gateway-core/tests/settings/mod.rs index 712205f4e..4b2fc9189 100644 --- a/backend/crates/gateway-core/tests/settings/mod.rs +++ b/backend/crates/gateway-core/tests/settings/mod.rs @@ -1,3 +1,5 @@ +//! 验证运行设置编译、Key 默认值与显式请求覆盖的解析和继承 + use std::{collections::BTreeMap, sync::Arc}; use gateway_core::{ diff --git a/backend/crates/gateway-core/tests/task.rs b/backend/crates/gateway-core/tests/task.rs index 48a49a132..76f673fc8 100644 --- a/backend/crates/gateway-core/tests/task.rs +++ b/backend/crates/gateway-core/tests/task.rs @@ -1,3 +1,5 @@ +//! 验证后台任务的调度时长、身份与租约配置约束 + use std::num::NonZeroU64; use std::time::Duration; diff --git a/backend/crates/gateway-core/tests/time.rs b/backend/crates/gateway-core/tests/time.rs index acb8cbb08..09edc376e 100644 --- a/backend/crates/gateway-core/tests/time.rs +++ b/backend/crates/gateway-core/tests/time.rs @@ -1,3 +1,5 @@ +//! 验证部署时区的日历边界、夏令时歧义与无效时间处理 + use chrono::{DateTime, NaiveDate, Utc}; use gateway_core::time::DeploymentTimeZone; diff --git a/backend/crates/gateway-core/tests/upstream.rs b/backend/crates/gateway-core/tests/upstream.rs index 12c53f862..6de77bdfb 100644 --- a/backend/crates/gateway-core/tests/upstream.rs +++ b/backend/crates/gateway-core/tests/upstream.rs @@ -1,3 +1,5 @@ +//! 验证上游发送状态保留发送不确定与已发送的区别 + use gateway_core::upstream::UpstreamSendState; #[test] diff --git a/backend/crates/gateway-host/build.rs b/backend/crates/gateway-host/build.rs index 4fbec7ae8..4fcaf2560 100644 --- a/backend/crates/gateway-host/build.rs +++ b/backend/crates/gateway-host/build.rs @@ -1,3 +1,5 @@ +//! 从构建环境与发行配置读取版本信息,注入宿主编译期元数据 + use std::{env, fs}; const RELEASE_VERSION_FILE: &str = "../../../release/version.yaml"; diff --git a/backend/crates/gateway-host/src/client_distribution.rs b/backend/crates/gateway-host/src/client_distribution.rs index 147bd4987..0671bccca 100644 --- a/backend/crates/gateway-host/src/client_distribution.rs +++ b/backend/crates/gateway-host/src/client_distribution.rs @@ -1,4 +1,4 @@ -//! RG-Adguard Store 页面解析与官方 Codex Desktop 下载回退。 +//! RG-Adguard Store 页面解析与官方 Codex Desktop 下载回退 use std::{collections::BTreeMap, time::Duration}; @@ -29,12 +29,12 @@ const OFFICIAL_X64_URL: &str = "https://persistent.oaistatic.com/codex-app-prod/ const OFFICIAL_ARM64_URL: &str = "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-arm64.msix"; -/// 下载页请求或解析失败的稳定分类。 +/// 下载页请求或解析失败的稳定分类 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ClientDistributionResolutionErrorKind { - /// 上游请求不可用。 + /// 上游请求不可用 Unavailable, - /// 上游响应不满足受信合同。 + /// 上游响应不满足受信合同 InvalidResponse, } @@ -174,11 +174,11 @@ impl ClientDistributionResolver for RgAdguardClientDistribution { } } -/// 从 RG-Adguard HTML 中选择每个受支持架构的最新有效主包。 +/// 从 RG-Adguard HTML 中选择每个受支持架构的最新有效主包 /// /// # Errors /// -/// 页面没有任何满足包身份、下载地址和过期时间约束的结果时返回无效响应。 +/// 页面没有任何满足包身份、下载地址和过期时间约束的结果时返回无效响应 pub fn parse_store_packages( html: &str, now: DateTime, @@ -357,7 +357,7 @@ fn official_fallback(now: DateTime) -> CodexDesktopWindowsDownloads { } } -/// 为动态结果中缺失的架构补齐 OpenAI 官方稳定包。 +/// 为动态结果中缺失的架构补齐 OpenAI 官方稳定包 #[must_use] pub fn complete_with_official_fallback( packages: Vec, diff --git a/backend/crates/gateway-host/src/command_line.rs b/backend/crates/gateway-host/src/command_line.rs index 312e0221d..0b700e212 100644 --- a/backend/crates/gateway-host/src/command_line.rs +++ b/backend/crates/gateway-host/src/command_line.rs @@ -1,4 +1,4 @@ -//! CLI 不进入 HTTP serve,也必须接收终止信号;资源退出时停止监听任务。 +//! CLI 不进入 HTTP serve,也必须接收终止信号;资源退出时停止监听任务 use gateway_core::lifecycle::CancellationToken; diff --git a/backend/crates/gateway-host/src/config.rs b/backend/crates/gateway-host/src/config.rs index 269c24fab..54dc9c777 100644 --- a/backend/crates/gateway-host/src/config.rs +++ b/backend/crates/gateway-host/src/config.rs @@ -1,4 +1,4 @@ -//! 配置文件发现、反序列化与 Host-owned 配置校验。 +//! 配置文件发现、反序列化与 Host-owned 配置校验 use std::env; use std::path::{Path, PathBuf}; @@ -14,18 +14,18 @@ const CONFIG_RELATIVE_PATH: &str = "deploy/config.yaml"; const SERVER_HOST_ENV: &str = "CPR_SERVER_HOST"; const SERVER_PORT_ENV: &str = "CPR_SERVER_PORT"; -/// 由组装根实现的顶层配置契约。 +/// 由组装根实现的顶层配置契约 /// /// Host 只负责找到和解析文件;每个包的字段解释与相对路径解析 -/// 由顶层配置委托给对应的包完成。 +/// 由顶层配置委托给对应的包完成 pub trait LoadableConfig: DeserializeOwned { - /// 同一配置文件中由其他工具消费的顶层配置段。 + /// 同一配置文件中由其他工具消费的顶层配置段 const EXTERNAL_SECTIONS: &'static [&'static str] = &[]; fn resolve_and_validate(&mut self, source_dir: &Path) -> Result<(), ConfigError>; } -/// 从当前目录或父目录中的 `deploy/config.yaml` 加载顶层配置。 +/// 从当前目录或父目录中的 `deploy/config.yaml` 加载顶层配置 pub fn load_config() -> Result { let current = env::current_dir().map_err(|_| ConfigError::CurrentDirectory)?; let path = discover_config_path(¤t)?; @@ -50,7 +50,7 @@ pub fn load_config() -> Result { field: error.path().to_string(), }, })?; - // 此时日志尚未初始化;只报告字段路径,不回显可能包含凭据的配置值。 + // 此时日志尚未初始化;只报告字段路径,不回显可能包含凭据的配置值 for field in unused { if !T::EXTERNAL_SECTIONS.contains(&field.as_str()) { eprintln!("[警告] 配置字段 {field:?} 未使用,已忽略"); @@ -71,7 +71,7 @@ fn missing_config_field(error: &config::ConfigError) -> Option { } } -/// Host 唯一拥有的进程配置。 +/// Host 唯一拥有的进程配置 #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] pub struct HostConfig { #[serde(default)] @@ -88,7 +88,7 @@ pub struct HostConfig { } impl HostConfig { - /// 解析 Host 配置;在线更新默认沿用已解析的 API 静态资源目录。 + /// 解析 Host 配置;在线更新默认沿用已解析的 API 静态资源目录 pub fn resolve_and_validate( &mut self, source_dir: &Path, @@ -147,27 +147,27 @@ impl HostConfig { } } -/// HTTP 监听地址。 +/// HTTP 监听地址 #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] pub struct ListenConfig { pub host: String, pub port: u16, } -/// Host 结构化日志配置。 +/// Host 结构化日志配置 #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] pub struct LoggingConfig { pub level: String, pub stdout: bool, pub file: FileLoggingConfig, - /// 将 OAuth 原始 AT/RT 写入独立恢复日志;默认关闭,沿用 file 的目录与保留期。 + /// 将 OAuth 原始 AT/RT 写入独立恢复日志;默认关闭,沿用 file 的目录与保留期 #[serde(default)] pub oauth_recovery: bool, - /// 将完整请求原文写入独立诊断日志;默认关闭,因为内容包含凭据。 - /// 请求报文的留存窗口由 request_dump_retention_days 独立控制。 + /// 将完整请求原文写入独立诊断日志;默认关闭,因为内容包含凭据 + /// 请求报文的留存窗口由 request_dump_retention_days 独立控制 #[serde(default)] pub request_dump: bool, - /// 完整报文的最少保留天数;按 UTC 日期整组清理,默认 1 天。 + /// 完整报文的最少保留天数;按 UTC 日期整组清理,默认 1 天 #[serde(default = "default_request_dump_retention_days")] pub request_dump_retention_days: usize, } @@ -210,12 +210,12 @@ impl LoggingConfig { } } -/// Host 文件日志配置。 +/// Host 文件日志配置 #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] pub struct FileLoggingConfig { pub enabled: bool, pub directory: PathBuf, - /// 至少保留的完整天数,默认 7 天;文件数量不参与清理。 + /// 至少保留的完整天数,默认 7 天;文件数量不参与清理 #[serde(default = "default_file_retention_days")] pub retention_days: usize, pub max_file_size_mb: u64, diff --git a/backend/crates/gateway-host/src/lib.rs b/backend/crates/gateway-host/src/lib.rs index 6970ff317..b409adf35 100644 --- a/backend/crates/gateway-host/src/lib.rs +++ b/backend/crates/gateway-host/src/lib.rs @@ -1,4 +1,4 @@ -//! 网关进程与操作系统能力:配置发现、日志、任务、自更新与 serve/drain。 +//! 网关进程与操作系统能力:配置发现、日志、任务、自更新与 serve/drain pub mod client_distribution; mod command_line; @@ -35,7 +35,7 @@ use self::serve::{ConnectionTracker, serve_router}; use self::system_update::ProcessSystemOperations; use self::workers::WorkerSupervisor; -/// Host 初始化的能力集;字段全部私有,不暴露内部监督器或进程状态。 +/// Host 初始化的能力集;字段全部私有,不暴露内部监督器或进程状态 pub struct HostBundle { config: HostConfig, log_guard: LogGuard, @@ -48,7 +48,7 @@ pub struct HostBundle { command_signal: Option, } -/// 在启动其他包之前初始化进程级能力。 +/// 在启动其他包之前初始化进程级能力 pub async fn initialize(config: HostConfig) -> Result { let official_plugins = Arc::new(official_plugins::FileOfficialPluginRelease::new( config @@ -78,7 +78,7 @@ pub async fn initialize(config: HostConfig) -> Result { }) } -/// CLI 的 stdout/stderr 属于命令结果;宿主诊断只保留已配置的文件日志。 +/// CLI 的 stdout/stderr 属于命令结果;宿主诊断只保留已配置的文件日志 pub async fn initialize_command_line(mut config: HostConfig) -> Result { config.logging.stdout = false; let mut host = initialize(config).await?; @@ -87,7 +87,7 @@ pub async fn initialize_command_line(mut config: HostConfig) -> Result Arc { self.client_distribution.clone() } - /// 返回与当前可执行文件一同部署的只读官方插件发行目录。 + /// 返回与当前可执行文件一同部署的只读官方插件发行目录 #[must_use] pub fn official_plugin_release_files(&self) -> Arc { self.official_plugins.clone() } - /// 身份来自构建脚本写入二进制的常量,不采信运行时配置或环境覆盖。 + /// 身份来自构建脚本写入二进制的常量,不采信运行时配置或环境覆盖 #[must_use] pub fn official_plugin_release_identity( &self, @@ -141,7 +141,7 @@ impl HostBundle { self.connections.clone() } - /// 报告文件日志写入或归档失败,避免将缺失日志误报为完整。 + /// 报告文件日志写入或归档失败,避免将缺失日志误报为完整 #[must_use] pub fn logging_health_probe(&self) -> Arc { self.log_guard.health_probe() @@ -161,7 +161,7 @@ impl HostBundle { Ok(()) } - /// 进程唯一阻塞点;返回前完成 HTTP drain 与 worker join。 + /// 进程唯一阻塞点;返回前完成 HTTP drain 与 worker join pub async fn serve(self, router: Router) -> Result<(), HostError> { let result = serve_router( router, diff --git a/backend/crates/gateway-host/src/logging/mod.rs b/backend/crates/gateway-host/src/logging/mod.rs index 5063428a6..05c02799f 100644 --- a/backend/crates/gateway-host/src/logging/mod.rs +++ b/backend/crates/gateway-host/src/logging/mod.rs @@ -1,4 +1,4 @@ -//! 进程结构化日志与按日期/大小轮转的文件 writer。 +//! 进程结构化日志与按日期/大小轮转的文件 writer use std::env; use std::io; @@ -17,18 +17,18 @@ mod writer; use sink::{FileLogGuard, FileLogSink, LogHealth}; use writer::RotatingLogWriter; -// 文件前缀统一为 codex-proxy-rs-;专用 tracing target 使用 snake_case。 +// 文件前缀统一为 codex-proxy-rs-;专用 tracing target 使用 snake_case const APPLICATION_LOG_FILE_PREFIX: &str = "codex-proxy-rs-application"; -/// 内部分类标识;由 host.logging.oauth_recovery 控制是否写入独立文件。 +/// 内部分类标识;由 host.logging.oauth_recovery 控制是否写入独立文件 const OAUTH_RECOVERY_LOG_TARGET: &str = "oauth_recovery"; const OAUTH_RECOVERY_LOG_FILE_PREFIX: &str = "codex-proxy-rs-oauth-recovery"; -/// 请求转储含完整凭据与正文,只允许在显式开启时写入独立文件。 +/// 请求转储含完整凭据与正文,只允许在显式开启时写入独立文件 const REQUEST_DUMP_LOG_TARGET: &str = "request_dump"; const REQUEST_DUMP_LOG_FILE_PREFIX: &str = "codex-proxy-rs-request-dump"; -/// 文件日志在正常退出时排空队列并等待落盘;控制台日志单独持有守卫。 +/// 文件日志在正常退出时排空队列并等待落盘;控制台日志单独持有守卫 pub struct LogGuard { _stdout: Vec, _files: Vec, @@ -47,7 +47,7 @@ pub enum LogError { SizeOverflow, } -/// 初始化日志;文件按完整日期留存,不以分片数量淘汰窗口内记录。 +/// 初始化日志;文件按完整日期留存,不以分片数量淘汰窗口内记录 pub fn initialize_logging( config: &LoggingConfig, timezone: gateway_core::time::DeploymentTimeZone, diff --git a/backend/crates/gateway-host/src/logging/sink.rs b/backend/crates/gateway-host/src/logging/sink.rs index 47b4a9030..fe093b946 100644 --- a/backend/crates/gateway-host/src/logging/sink.rs +++ b/backend/crates/gateway-host/src/logging/sink.rs @@ -1,4 +1,4 @@ -//! 有界文件日志队列:拥堵时背压,正常退出时排空;写入失败显式影响健康状态。 +//! 有界文件日志队列:拥堵时背压,正常退出时排空;写入失败显式影响健康状态 use std::io::{self, Write}; use std::sync::atomic::{AtomicU64, Ordering}; @@ -133,15 +133,15 @@ impl Write for FileLogSink { } fn flush(&mut self) -> io::Result<()> { - // Files are unbuffered. The guard is the lifecycle owner of queue drain and sync. + // 文件不额外缓冲,由守卫负责队列排空与同步的生命周期 Ok(()) } } impl Drop for FileLogGuard { fn drop(&mut self) { - // Shutdown follows every accepted record. Unlike a timed guard, joining cannot - // silently abandon queued log records during a normal process shutdown. + // 关闭指令排在所有已接收记录之后 + // 等待线程退出,确保正常关闭时不会因超时而丢弃队列中的日志 let _ = self.sender.send(Command::Shutdown); if self .worker diff --git a/backend/crates/gateway-host/src/logging/writer.rs b/backend/crates/gateway-host/src/logging/writer.rs index e33007b0c..c0561fc83 100644 --- a/backend/crates/gateway-host/src/logging/writer.rs +++ b/backend/crates/gateway-host/src/logging/writer.rs @@ -1,4 +1,4 @@ -//! 按大小轮转、压缩已关闭分片、按部署时区自然日期组清理。 +//! 按大小轮转、压缩已关闭分片、按部署时区自然日期组清理 use std::collections::BTreeMap; use std::fs::{self, File, OpenOptions}; @@ -61,7 +61,7 @@ impl RotatingLogWriter { file, health, }; - // Recover unfinished archive work after restart; the active file is never compressed. + // 重启后恢复未完成归档,正在写入的文件不参与压缩 for entry in managed_log_files(&writer.directory, prefix)? { if !entry.compressed && entry.path != path @@ -99,7 +99,7 @@ impl RotatingLogWriter { .directory .join(log_file_name(self.prefix, date, segment)); let file = open_log_segment(&path)?; - // Commit rotation state only after opening the new file succeeds. + // 新文件打开成功后才能提交轮转状态 self.file = file; self.date = date; self.segment = segment; @@ -107,7 +107,7 @@ impl RotatingLogWriter { if let Err(error) = compress_log_file(&previous) { self.health.maintenance_failed(error.kind()); } - // Maintenance errors must not discard the record that triggered rotation. + // 维护失败不能丢弃触发轮转的日志记录 if let Err(error) = cleanup_log_files( &self.directory, self.prefix, @@ -219,7 +219,7 @@ fn cleanup_log_files( if date >= cutoff { continue; } - // 用较近的文件名日期或实际写入日期保护整组,避免时区切换后提前清理。 + // 用较近的文件名日期或实际写入日期保护整组,避免时区切换后提前清理 let mut latest = date; for path in &paths { let modified: chrono::DateTime = path.metadata()?.modified()?.into(); @@ -240,7 +240,7 @@ fn compress_log_file(path: &Path) -> io::Result<()> { let modified = source.metadata()?.modified()?; let archive = path.with_extension("log.gz"); let temporary = path.with_extension("log.gz.tmp"); - // Only a closed, owner-managed segment can have this temporary archive. + // 只有已关闭且由当前写入器管理的分段才能拥有此临时归档 let output = File::create(&temporary)?; let mut encoder = GzEncoder::new(output, Compression::fast()); io::copy(&mut source, &mut encoder)?; @@ -248,6 +248,6 @@ fn compress_log_file(path: &Path) -> io::Result<()> { output.set_modified(modified)?; output.sync_all()?; fs::rename(&temporary, &archive)?; - // The original survives until a complete, synced archive has been published. + // 完整归档同步并发布后才能删除原文件 fs::remove_file(path) } diff --git a/backend/crates/gateway-host/src/official_plugins.rs b/backend/crates/gateway-host/src/official_plugins.rs index 238c9ba4b..ce176a614 100644 --- a/backend/crates/gateway-host/src/official_plugins.rs +++ b/backend/crates/gateway-host/src/official_plugins.rs @@ -1,4 +1,4 @@ -//! 随宿主发行的官方插件文件只读入口。 +//! 随宿主发行的官方插件文件只读入口 use std::{ fs, @@ -16,7 +16,7 @@ const MANIFEST_FILE_NAME: &str = "plugin-release-manifest.json"; const MAXIMUM_MANIFEST_BYTES: usize = 256 * 1024; const MAXIMUM_ARTIFACT_BYTES: usize = 32 * 1024 * 1024; -/// 目录与宿主二进制一同部署和替换;`sealed` 本身不被当作密码学签名。 +/// 目录与宿主二进制一同部署和替换;`sealed` 本身不被当作密码学签名 pub struct FileOfficialPluginRelease { directory: PathBuf, } diff --git a/backend/crates/gateway-host/src/outbound/connector.rs b/backend/crates/gateway-host/src/outbound/connector.rs index bfe85d72d..886e25ed0 100644 --- a/backend/crates/gateway-host/src/outbound/connector.rs +++ b/backend/crates/gateway-host/src/outbound/connector.rs @@ -1,3 +1,5 @@ +//! 受管出站连接器,建立直连或代理隧道并完成 TLS 连接 + use std::{ net::SocketAddr, pin::Pin, diff --git a/backend/crates/gateway-host/src/outbound/http.rs b/backend/crates/gateway-host/src/outbound/http.rs index bb94d0855..6b7534dd4 100644 --- a/backend/crates/gateway-host/src/outbound/http.rs +++ b/backend/crates/gateway-host/src/outbound/http.rs @@ -1,3 +1,5 @@ +//! 受管 HTTP 客户端,约束出站网络、请求时限与响应读取 + use std::{ collections::HashMap, sync::{Arc, Mutex}, @@ -78,7 +80,7 @@ impl HttpError { self.kind } - /// 固定诊断分类,不包含网址、请求头、正文或底层错误中的凭据。 + /// 固定诊断分类,不包含网址、请求头、正文或底层错误中的凭据 pub const fn reason(&self) -> &'static str { self.reason } @@ -146,7 +148,7 @@ impl HttpClient { .http1_max_buf_size(64 * 1024) .retry_canceled_requests(false) .build(connector); - // 淘汰缓存不影响已借出的 Client 或在途 body。 + // 淘汰缓存不影响已借出的 Client 或在途 body if clients.len() >= 64 { clients.clear(); } @@ -165,7 +167,7 @@ impl HttpClient { .await } - /// 为同一代理端点上的不同管理身份隔离连接池;scope 不得包含 secret。 + /// 为同一代理端点上的不同管理身份隔离连接池;scope 不得包含 secret pub async fn open_scoped( &self, request: HttpRequest, @@ -266,7 +268,7 @@ impl HttpClient { if url.scheme() == "http" && let Some(proxy) = proxy.filter(|proxy| proxy.expose_url().starts_with("http")) { - // 正向代理接收数字地址的绝对 URI;Host 保留原始域名,禁止代理再次解析目标。 + // 正向代理接收数字地址的绝对 URI;Host 保留原始域名,禁止代理再次解析目标 let authority = uri .authority() .ok_or_else(|| HttpError::invalid("host"))? @@ -380,7 +382,7 @@ impl HttpBody { return Err(HttpError::sent("read size")); } loop { - // 已缓冲的数据和就绪帧也受同一期限约束,慢读者不能无限续用 HTTP 资源。 + // 已缓冲的数据和就绪帧也受同一期限约束,慢读者不能无限续用 HTTP 资源 if Instant::now() >= self.deadline { return Err(HttpError::sent("response deadline").with_kind(HttpErrorKind::Timeout)); } diff --git a/backend/crates/gateway-host/src/outbound/mod.rs b/backend/crates/gateway-host/src/outbound/mod.rs index eaedeab2b..e8ea55927 100644 --- a/backend/crates/gateway-host/src/outbound/mod.rs +++ b/backend/crates/gateway-host/src/outbound/mod.rs @@ -1,4 +1,4 @@ -//! 受管 HTTP 资源;调用授权属于上层,Host 负责出站、容量、期限和流回收。 +//! 受管 HTTP 资源;调用授权属于上层,Host 负责出站、容量、期限和流回收 mod connector; mod http; diff --git a/backend/crates/gateway-host/src/outbound/network.rs b/backend/crates/gateway-host/src/outbound/network.rs index 65f9aa62f..9d8bc5a85 100644 --- a/backend/crates/gateway-host/src/outbound/network.rs +++ b/backend/crates/gateway-host/src/outbound/network.rs @@ -1,3 +1,5 @@ +//! 受管出站请求的 DNS 解析、地址分类与网络范围校验 + use std::{ net::{IpAddr, SocketAddr}, sync::Arc, @@ -8,7 +10,7 @@ use ipnet::IpNet; use super::{HttpError, HttpErrorKind}; -/// 默认仅允许公网;额外地址段来自明确授权,不由请求或 DNS 回应扩大。 +/// 默认仅允许公网;额外地址段来自明确授权,不由请求或 DNS 回应扩大 #[derive(Clone, Default)] pub struct NetworkPolicy { additional: Vec, @@ -16,7 +18,7 @@ pub struct NetworkPolicy { } impl NetworkPolicy { - /// 完整信任的宿主调用方可以访问任意地址;仍使用相同的 DNS 与传输实现。 + /// 完整信任的宿主调用方可以访问任意地址;仍使用相同的 DNS 与传输实现 #[must_use] pub const fn unrestricted() -> Self { Self { @@ -60,7 +62,7 @@ impl NetworkPolicy { } } -/// DNS 只产出候选地址;连接层只使用通过授权检查的地址,不再次解析目标域名。 +/// DNS 只产出候选地址;连接层只使用通过授权检查的地址,不再次解析目标域名 pub trait DnsResolver: Send + Sync { fn resolve( &self, @@ -137,7 +139,7 @@ fn public_address(address: IpAddr) -> bool { } IpAddr::V6(ip) => { let segments = ip.segments(); - // 只默认开放全球单播;隧道、文档与协议专用地址均需显式授予。 + // 只默认开放全球单播;隧道、文档与协议专用地址均需显式授予 segments[0] & 0xe000 == 0x2000 && !(segments[0] == 0x2001 && segments[1] < 0x0200) && !(segments[0] == 0x2001 && segments[1] == 0x0db8) diff --git a/backend/crates/gateway-host/src/outbound/websocket.rs b/backend/crates/gateway-host/src/outbound/websocket.rs index ad8c897cd..c30c417c4 100644 --- a/backend/crates/gateway-host/src/outbound/websocket.rs +++ b/backend/crates/gateway-host/src/outbound/websocket.rs @@ -1,3 +1,5 @@ +//! 受管 WebSocket 的握手、消息收发、超时与连接关闭 + use std::time::Duration; use bytes::Bytes; @@ -23,7 +25,7 @@ const MAX_MESSAGE_BYTES: usize = 8 * 1024 * 1024; type Socket = WebSocketStream>; -/// 读写分别串行,允许等待输出时发送控制消息;关闭会唤醒在途操作并释放容量。 +/// 读写分别串行,允许等待输出时发送控制消息;关闭会唤醒在途操作并释放容量 pub struct ManagedWebSocket { writer: Mutex, OwnedSemaphorePermit)>>, reader: Mutex>>, @@ -43,7 +45,7 @@ pub struct WebSocketResponse { } impl HttpClient { - /// 使用与 HTTP 相同的 DNS、代理、证书和容量边界执行 HTTP/1.1 Upgrade。 + /// 使用与 HTTP 相同的 DNS、代理、证书和容量边界执行 HTTP/1.1 Upgrade pub async fn open_websocket( &self, request: HttpRequest, @@ -201,7 +203,7 @@ impl ManagedWebSocket { *self.closed.borrow() } - /// 先取消在途读写,再释放两端;返回时连接与容量均已回收。 + /// 先取消在途读写,再释放两端;返回时连接与容量均已回收 pub async fn close(&self) { self.closed.send_replace(true); let mut writer = self.writer.lock().await; diff --git a/backend/crates/gateway-host/src/plugin_distribution/github.rs b/backend/crates/gateway-host/src/plugin_distribution/github.rs index becf902e9..54ac1edb9 100644 --- a/backend/crates/gateway-host/src/plugin_distribution/github.rs +++ b/backend/crates/gateway-host/src/plugin_distribution/github.rs @@ -1,3 +1,5 @@ +//! GitHub 插件发行查询、缓存与制品下载地址及摘要解析 + use std::{ sync::Arc, time::{Duration, Instant}, @@ -92,9 +94,9 @@ impl HttpPluginDistribution { .clone() }; tokio::time::timeout(Duration::from_secs(60), async { - // 同一来源共用锁和结果;取消请求释放锁,下一位查询者可重新发起。 + // 同一来源共用锁和结果;取消请求释放锁,下一位查询者可重新发起 let mut cached = slot.lock().await; - // 显式查询刷新成功结果;等待同一次请求的调用者仍共享结果,失败保留短缓存限流。 + // 显式查询刷新成功结果;等待同一次请求的调用者仍共享结果,失败保留短缓存限流 if let Some(entry) = cached.as_ref().filter(|entry| { entry.until > Instant::now() && (!refresh || entry.fetched_at >= requested_at || entry.result.is_err()) diff --git a/backend/crates/gateway-host/src/plugin_distribution/http.rs b/backend/crates/gateway-host/src/plugin_distribution/http.rs index fa9566fa8..ef0cd0901 100644 --- a/backend/crates/gateway-host/src/plugin_distribution/http.rs +++ b/backend/crates/gateway-host/src/plugin_distribution/http.rs @@ -1,3 +1,5 @@ +//! 插件制品下载的来源校验、凭据匹配与限流错误处理 + use std::{ collections::HashMap, sync::Arc, @@ -104,7 +106,7 @@ impl Downloads { .map_err(|_| source_error())?; if let Some(delay) = rate_delay(response.status, &response.headers) { let mut rates = self.rate_limits.lock().await; - // 限流项有界;超出时拒绝新查询,不淘汰仍然有效的退避。 + // 限流项有界;超出时拒绝新查询,不淘汰仍然有效的退避 if rates.len() < 256 || rates.contains_key(&scope) { rates.insert(scope, Instant::now() + delay); } @@ -124,7 +126,7 @@ impl Downloads { if url.scheme() == "https" && next.scheme() != "https" { return Err(source_error()); } - // 不继承上一次请求的头;下一跳从授权列表重新匹配,包括同域的路径变化。 + // 不继承上一次请求的头;下一跳从授权列表重新匹配,包括同域的路径变化 url = next; continue; } diff --git a/backend/crates/gateway-host/src/plugin_distribution/mod.rs b/backend/crates/gateway-host/src/plugin_distribution/mod.rs index 09945c4a7..82cde084a 100644 --- a/backend/crates/gateway-host/src/plugin_distribution/mod.rs +++ b/backend/crates/gateway-host/src/plugin_distribution/mod.rs @@ -1,4 +1,4 @@ -//! 插件来源查询和受控下载;不解析插件包,不持有数据库或管理事务。 +//! 插件来源查询和受控下载;不解析插件包,不持有数据库或管理事务 mod github; mod http; @@ -29,7 +29,7 @@ use self::{github::CachedRelease, http::Downloads}; const MAX_ARCHIVE_BYTES: usize = 32 * 1024 * 1024; const MAX_METADATA_BYTES: usize = 1024 * 1024; -/// 插件分发服务复用宿主受控 HTTP;缓存与限流不会跨出站身份共享。 +/// 插件分发服务复用宿主受控 HTTP;缓存与限流不会跨出站身份共享 pub struct HttpPluginDistribution { downloads: Downloads, api_base: Url, @@ -42,7 +42,7 @@ impl HttpPluginDistribution { Self::with_transport("https://api.github.com/", http, source_network()?) } - /// 测试可注入受限网络;生产来源由管理员显式配置,因此保持既有内外网可达范围。 + /// 测试可注入受限网络;生产来源由管理员显式配置,因此保持既有内外网可达范围 pub fn with_transport( api_base: &str, http: Arc, diff --git a/backend/crates/gateway-host/src/pricing.rs b/backend/crates/gateway-host/src/pricing.rs index b58af3a43..b7e2fd005 100644 --- a/backend/crates/gateway-host/src/pricing.rs +++ b/backend/crates/gateway-host/src/pricing.rs @@ -1,4 +1,4 @@ -//! models.dev 的只读价目适配;固定地址、禁止跳转,不进入推理请求链路。 +//! models.dev 的只读价目适配;固定地址、禁止跳转,不进入推理请求链路 use async_trait::async_trait; use gateway_admin::{ @@ -46,7 +46,7 @@ fn source_error() -> AdminError { AdminError::bad_gateway("无法读取 models.dev 价目,请稍后重试;现有价格未变更") } -/// 只接受当前网关 Provider 的文本 Token 价格;不猜测缺失字段或模态换算。 +/// 只接受当前网关 Provider 的文本 Token 价格;不猜测缺失字段或模态换算 pub fn decode_catalog(body: &[u8]) -> Result { let catalog: Value = serde_json::from_slice(body).map_err(|_| source_error())?; let mut result = PricingSyncPreview { @@ -90,7 +90,7 @@ fn model_price(provider: &str, model: &Value) -> Option { return None; } let cost = model.get("cost")?; - // 独立 reasoning/audio 价格无法用当前 Token 用量拆分,不能静默吞掉。 + // 独立 reasoning/audio 价格无法用当前 Token 用量拆分,不能静默吞掉 if ["reasoning", "input_audio", "output_audio"] .iter() .any(|key| cost.get(key).is_some()) @@ -130,7 +130,7 @@ fn rates(cost: &Value) -> Option { Some(TokenPriceOverride { input, output: price("output")?, - // 没有缓存折扣的模型按普通输入价;不能把缺项解释成免费。 + // 没有缓存折扣的模型按普通输入价;不能把缺项解释成免费 cache_read: if cost.get("cache_read").is_some() { price("cache_read")? } else { diff --git a/backend/crates/gateway-host/src/process.rs b/backend/crates/gateway-host/src/process.rs index d827f9f13..3fc0dd5fd 100644 --- a/backend/crates/gateway-host/src/process.rs +++ b/backend/crates/gateway-host/src/process.rs @@ -1,4 +1,4 @@ -//! 受管子进程的创建、退出通知与回收,不解释插件或其他业务协议。 +//! 受管子进程的创建、退出通知与回收,不解释插件或其他业务协议 use std::{num::NonZeroUsize, path::PathBuf, process::Stdio, sync::Arc}; @@ -35,7 +35,7 @@ pub enum ProcessStartError { Pipes, } -/// 组合根提供共享监督入口,容量直到子进程被回收后才归还。 +/// 组合根提供共享监督入口,容量直到子进程被回收后才归还 pub struct ProcessSupervisor { slots: Arc, } @@ -66,7 +66,7 @@ impl ProcessSupervisor { } } -/// 通信资源和监督控制分离;最后一个控制引用释放时也必须回收进程。 +/// 通信资源和监督控制分离;最后一个控制引用释放时也必须回收进程 pub struct ProcessConnection { pub input: ChildStdin, pub output: ChildStdout, @@ -97,7 +97,7 @@ impl ProcessControl { } } -/// 可信原生程序仍拥有当前 OS 用户权限;清空环境和独立进程不等于安全沙箱。 +/// 可信原生程序仍拥有当前 OS 用户权限;清空环境和独立进程不等于安全沙箱 fn spawn_process( spec: ProcessSpec, slot: tokio::sync::OwnedSemaphorePermit, @@ -110,7 +110,7 @@ fn spawn_process( .stderr(Stdio::piped()) .kill_on_drop(true) .spawn() - // 只保留错误类别与 OS 编号,不把进程路径、环境或输出带入诊断。 + // 只保留错误类别与 OS 编号,不把进程路径、环境或输出带入诊断 .map_err(|error| ProcessStartError::Spawn { kind: error.kind(), os_code: error.raw_os_error(), @@ -141,7 +141,7 @@ fn spawn_process( } } }; - // stderr 是不可信诊断,不写宿主日志;业务日志必须经过有界脱敏接口。 + // stderr 是不可信诊断,不写宿主日志;业务日志必须经过有界脱敏接口 if !matches!(reason, ProcessExit::Exited(_)) { let _ = child.kill().await; } diff --git a/backend/crates/gateway-host/src/proxy_probe.rs b/backend/crates/gateway-host/src/proxy_probe.rs index 1b678b44c..469bf455e 100644 --- a/backend/crates/gateway-host/src/proxy_probe.rs +++ b/backend/crates/gateway-host/src/proxy_probe.rs @@ -1,4 +1,4 @@ -//! 使用与 Provider 请求一致的显式代理协议,执行有超时和响应大小限制的出口测试。 +//! 使用与 Provider 请求一致的显式代理协议,执行有超时和响应大小限制的出口测试 use std::{ net::{IpAddr, Ipv4Addr, Ipv6Addr}, @@ -33,7 +33,7 @@ type ProxyClientBuilder = impl Default for HttpProxyProbe { fn default() -> Self { - // 分别向 IPv4 和 IPv6 专用端点并发探测,以获取真实的双栈出口地址。 + // 分别向 IPv4 和 IPv6 专用端点并发探测,以获取真实的双栈出口地址 Self::new_dual( "https://api.ipify.org?format=json", "https://api6.ipify.org?format=json", @@ -63,7 +63,7 @@ impl HttpProxyProbe { } } - /// 由组合根注入与 Provider 请求一致的证书信任策略。 + /// 由组合根注入与 Provider 请求一致的证书信任策略 #[must_use] pub fn with_client_builder( mut self, @@ -248,7 +248,7 @@ impl HttpProxyProbe { } impl HttpProxyProbe { - /// 显式注入地理位置服务地址,便于部署适配与隔离网络验证。 + /// 显式注入地理位置服务地址,便于部署适配与隔离网络验证 #[must_use] pub fn with_location_endpoint(mut self, endpoint: impl Into) -> Self { self.location_endpoint = endpoint.into(); @@ -260,8 +260,8 @@ impl HttpProxyProbe { proxy: &OutboundProxy, ip: IpAddr, ) -> Result { - // 出口 IP 已由代理探测确认;按这个固定 IP 从服务端查询位置,避免代理出口屏蔽位置服务。 - // 部署环境无法直连位置服务时仍尝试原代理路径。 + // 出口 IP 已由代理探测确认;按这个固定 IP 从服务端查询位置,避免代理出口屏蔽位置服务 + // 部署环境无法直连位置服务时仍尝试原代理路径 match self.lookup_location(ip, None).await { Ok(location) => Ok(location), Err(direct_error) => self @@ -291,7 +291,7 @@ impl HttpProxyProbe { .proxy(reqwest::Proxy::all(proxy.expose_url()).map_err(|_| "代理地址不合法")?); } let client = (self.build_client)(builder)?; - // 查询已检测到的具体出口,不能再次查询“我的 IP”,轮换代理可能换到另一个出口。 + // 查询已检测到的具体出口,不能再次查询“我的 IP”,轮换代理可能换到另一个出口 let url = format!("{}/{ip}", self.location_endpoint.trim_end_matches('/')); let mut response = client .get(url) diff --git a/backend/crates/gateway-host/src/retention.rs b/backend/crates/gateway-host/src/retention.rs index 93d9651d2..c4620989a 100644 --- a/backend/crates/gateway-host/src/retention.rs +++ b/backend/crates/gateway-host/src/retention.rs @@ -1,4 +1,4 @@ -//! 保留期任务的调度、单轮预算、取消与日志;删除事务由 Store 端口负责。 +//! 保留期任务的调度、单轮预算、取消与日志;删除事务由 Store 端口负责 use std::{num::NonZeroU32, sync::Arc, time::Duration}; @@ -30,7 +30,7 @@ impl Default for RetentionCycleBudget { } pub fn worker(store: Arc) -> Result { - // 保持稳定的租约身份,滚动升级期间仍与已有进程互斥。 + // 保持稳定的租约身份,滚动升级期间仍与已有进程互斥 let id = WorkerId::try_new(WorkerKind::Retention, "postgres")?; let schedule = WorkerSchedule::try_new( Duration::from_secs(60 * 60), diff --git a/backend/crates/gateway-host/src/serve.rs b/backend/crates/gateway-host/src/serve.rs index 4ef3ae10d..599fa6ad7 100644 --- a/backend/crates/gateway-host/src/serve.rs +++ b/backend/crates/gateway-host/src/serve.rs @@ -1,4 +1,4 @@ -//! HTTP 监听、OS signal、原子连接注册与优雅 drain。 +//! HTTP 监听、OS signal、原子连接注册与优雅 drain use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, OnceLock}; @@ -13,7 +13,7 @@ const DRAINING_BIT: usize = 1usize << (usize::BITS - 1); const ACTIVE_MASK: usize = !DRAINING_BIT; /// 自重启交接没有显式握手:替换进程按 CPR_RESTART_DELAY_MS 估算等待, -/// 可能在旧进程释放监听端口前尝试绑定,因此对 AddrInUse 保留有限重试窗口。 +/// 可能在旧进程释放监听端口前尝试绑定,因此对 AddrInUse 保留有限重试窗口 const BIND_RETRY_WINDOW: Duration = Duration::from_secs(10); const BIND_RETRY_MAX_DELAY: Duration = Duration::from_secs(1); @@ -54,7 +54,7 @@ impl ConnectionTracker { tokio::pin!(notified); // notify_waiters 不保存许可:必须先 enable 注册等待者,再读 // 活跃计数;否则最后一个 guard 在读数与首次 poll 之间 drop - // 时唤醒丢失,只能等满整个超时。 + // 时唤醒丢失,只能等满整个超时 notified.as_mut().enable(); if self.state.value.load(Ordering::Acquire) & ACTIVE_MASK == 0 { return; @@ -114,7 +114,7 @@ impl Drop for ActiveConnection { } /// 绑定失败即进程退出、服务彻底离线,因此 AddrInUse(典型为自重启交接 -/// 时旧进程尚未关闭 listener)在窗口内指数退避重试;其他错误立即上抛。 +/// 时旧进程尚未关闭 listener)在窗口内指数退避重试;其他错误立即上抛 pub async fn bind_listener(address: &str) -> std::io::Result { let deadline = tokio::time::Instant::now() + BIND_RETRY_WINDOW; let mut delay = Duration::from_millis(100); @@ -154,9 +154,10 @@ pub(crate) async fn serve_router( shutdown_connections.begin_draining(); }; // axum 的优雅关闭会无限等待存量连接结束,而 SSE 等长响应体不观察 - // 取消信号:慢客户端可以把关闭拖住直到被 SIGKILL。整个 drain(axum + // 取消信号:慢客户端可以把关闭拖住直到被 SIGKILL + // 整个 drain(axum // 优雅关闭 + 游离 guard 等待)共享同一个从取消信号起算的绝对截止点, - // 逾期即放弃等待,存量连接随进程退出终止。 + // 逾期即放弃等待,存量连接随进程退出终止 let drain_deadline_at = Arc::new(OnceLock::new()); let drain_deadline = { let cancellation = cancellation.clone(); diff --git a/backend/crates/gateway-host/src/system_update/archive.rs b/backend/crates/gateway-host/src/system_update/archive.rs index 5b43053e5..8f10689e4 100644 --- a/backend/crates/gateway-host/src/system_update/archive.rs +++ b/backend/crates/gateway-host/src/system_update/archive.rs @@ -1,4 +1,4 @@ -//! Release tar.gz 安全解包与制品归一化。 +//! Release tar.gz 安全解包与制品归一化 use std::collections::BTreeSet; use std::fs; diff --git a/backend/crates/gateway-host/src/system_update/download.rs b/backend/crates/gateway-host/src/system_update/download.rs index 4e7a031ea..c99ed258d 100644 --- a/backend/crates/gateway-host/src/system_update/download.rs +++ b/backend/crates/gateway-host/src/system_update/download.rs @@ -1,4 +1,4 @@ -//! 受限制的 Release 下载、重定向信任链和 SHA-256 校验。 +//! 受限制的 Release 下载、重定向信任链和 SHA-256 校验 use std::fs; use std::io::{Read, Write}; diff --git a/backend/crates/gateway-host/src/system_update/installation.rs b/backend/crates/gateway-host/src/system_update/installation.rs index a009f0a1d..d4d827b81 100644 --- a/backend/crates/gateway-host/src/system_update/installation.rs +++ b/backend/crates/gateway-host/src/system_update/installation.rs @@ -1,4 +1,4 @@ -//! 安装文件指纹用于区分本进程安装的候选与外部重新部署,历史版本号不代表磁盘事实。 +//! 安装文件指纹用于区分本进程安装的候选与外部重新部署,历史版本号不代表磁盘事实 use std::fs; use std::io::{self, Read}; diff --git a/backend/crates/gateway-host/src/system_update/mod.rs b/backend/crates/gateway-host/src/system_update/mod.rs index 7ec33575a..d26757702 100644 --- a/backend/crates/gateway-host/src/system_update/mod.rs +++ b/backend/crates/gateway-host/src/system_update/mod.rs @@ -1,4 +1,4 @@ -//! 版本、安全自更新、回滚与进程重启的 Host-owned 实现。 +//! 版本、安全自更新、回滚与进程重启的 Host-owned 实现 mod archive; mod download; @@ -56,7 +56,7 @@ const MAX_OFFICIAL_PLUGIN_MANIFEST_SIZE: u64 = 256 * 1024; type OperationError = SystemOperationError; -/// 系统更新与重启配置;所有字段只由 Host 解释。 +/// 系统更新与重启配置;所有字段只由 Host 解释 #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] #[serde(default)] pub struct SystemUpdateConfig { @@ -68,7 +68,7 @@ pub struct SystemUpdateConfig { pub update_repository: Option, pub github_api_base: String, pub executable_path: Option, - /// 未显式指定时,由组合根传入 API 实际使用的静态资源目录。 + /// 未显式指定时,由组合根传入 API 实际使用的静态资源目录 pub web_dist_dir: Option, pub update_state_file: PathBuf, pub update_lock_file: PathBuf, @@ -228,7 +228,7 @@ impl SystemUpdateConfig { } } -/// gateway-admin 消费的真实进程/文件系统操作实现。 +/// gateway-admin 消费的真实进程/文件系统操作实现 #[derive(Clone)] pub struct ProcessSystemOperations { cancellation: CancellationToken, @@ -243,7 +243,7 @@ impl ProcessSystemOperations { #[must_use] pub fn new(cancellation: CancellationToken, mut config: SystemUpdateConfig) -> Self { // 组合根在接收请求前固定安装路径和发行文件;旧程序重命名后, - // current_exe 可能指向备份,后续更新、回滚和重启不能再反查运行文件。 + // current_exe 可能指向备份,后续更新、回滚和重启不能再反查运行文件 let running_files = Arc::new(config.executable_path().and_then(|executable| { config.executable_path = Some(executable); ReleaseFiles::installed(&config) @@ -302,7 +302,7 @@ impl ProcessSystemOperations { target_version: target.clone(), }; let service = self.clone(); - // 任务持有互斥锁和落盘守卫,HTTP 断开不会取消更新;Host 关闭仍会收敛终态。 + // 任务持有互斥锁和落盘守卫,HTTP 断开不会取消更新;Host 关闭仍会收敛终态 drop(tokio::spawn(async move { let _operation_lock = operation_lock; let result = tokio::select! { @@ -701,7 +701,7 @@ impl SystemOperations for ProcessSystemOperations { preflight: Arc, ) -> Result { // 与 update/rollback 互斥:更新替换文件期间触发自重启会让新进程 - // 载入半成品产物。 + // 载入半成品产物 let _operation = self .operation_lock .try_lock() diff --git a/backend/crates/gateway-host/src/system_update/process.rs b/backend/crates/gateway-host/src/system_update/process.rs index 475cd4a36..4f6d8f6c5 100644 --- a/backend/crates/gateway-host/src/system_update/process.rs +++ b/backend/crates/gateway-host/src/system_update/process.rs @@ -1,4 +1,4 @@ -//! 宿主进程替换与容器内关闭语义。 +//! 宿主进程替换与容器内关闭语义 use std::env; use std::fs; diff --git a/backend/crates/gateway-host/src/system_update/release.rs b/backend/crates/gateway-host/src/system_update/release.rs index 7c83898b2..69f9114e5 100644 --- a/backend/crates/gateway-host/src/system_update/release.rs +++ b/backend/crates/gateway-host/src/system_update/release.rs @@ -1,4 +1,4 @@ -//! GitHub Release 发现、缓存、版本比较与下载信任边界。 +//! GitHub Release 发现、缓存、版本比较与下载信任边界 use std::env; use std::sync::atomic::{AtomicU64, Ordering}; @@ -92,7 +92,7 @@ impl ReleaseCache { |release| detail_from_release(config, release, channel), ); let mut entry = self.entry.lock().await; - // 较慢的旧请求不能覆盖或清除新检查的结果,包括切走再切回同一通道。 + // 较慢的旧请求不能覆盖或清除新检查的结果,包括切走再切回同一通道 if self.request_sequence.load(Ordering::Relaxed) == sequence { *entry = Some(CachedRelease { key, @@ -103,7 +103,7 @@ impl ReleaseCache { Ok(detail) } Err(error) => { - // 强制检查失败后清掉旧结果,避免后续版本查询再次显示旧的更新标记。 + // 强制检查失败后清掉旧结果,避免后续版本查询再次显示旧的更新标记 let mut entry = self.entry.lock().await; if self.request_sequence.load(Ordering::Relaxed) == sequence && entry.as_ref().is_some_and(|cached| cached.key == key) @@ -126,7 +126,7 @@ impl ReleaseCache { } } -/// 优先返回可升级的最高版本,没有候选时保留当前版本的发布信息。 +/// 优先返回可升级的最高版本,没有候选时保留当前版本的发布信息 pub(crate) async fn fetch_latest( api_base: &str, repository: &str, @@ -144,7 +144,7 @@ pub(crate) async fn fetch_latest( .build() .map_err(|error| upstream(format!("failed to create release client: {error}")))?; - // 所有发行线先过滤再排序,避免更高大版本或其他实验遮住仍可安装的更新。 + // 所有发行线先过滤再排序,避免更高大版本或其他实验遮住仍可安装的更新 let mut latest: Option<(semver::Version, GitHubRelease)> = None; let mut page = 1; loop { @@ -275,7 +275,7 @@ pub(crate) fn select_archive<'a>( }) } -/// 校验 GitHub Release 资产 URL 及其重定向目标是否位于受信任边界内。 +/// 校验 GitHub Release 资产 URL 及其重定向目标是否位于受信任边界内 pub fn validate_download_url(raw: &str, api_base: &str) -> Result<(), OperationError> { let url = reqwest::Url::parse(raw) .map_err(|error| invalid(format!("invalid download URL: {error}")))?; @@ -380,7 +380,7 @@ pub(crate) fn version_channel(version: &str) -> Option { channel_for_version(&version) } -/// 检查更新与执行更新共用同一规则;构建元数据不构成更新。 +/// 检查更新与执行更新共用同一规则;构建元数据不构成更新 fn update_target_allowed( current: &semver::Version, target: &semver::Version, diff --git a/backend/crates/gateway-host/src/system_update/state.rs b/backend/crates/gateway-host/src/system_update/state.rs index 22c56b733..2daa64721 100644 --- a/backend/crates/gateway-host/src/system_update/state.rs +++ b/backend/crates/gateway-host/src/system_update/state.rs @@ -1,4 +1,4 @@ -//! 系统操作状态文件、跨进程锁与临时目录。 +//! 系统操作状态文件、跨进程锁与临时目录 use std::fs; use std::io::{self, Write}; @@ -160,7 +160,7 @@ impl Drop for UpdateTempDir { } } -/// 更新任务拥有持久化终态的责任,Future 在取消或 panic 时析构也必须收尾。 +/// 更新任务拥有持久化终态的责任,Future 在取消或 panic 时析构也必须收尾 pub(crate) struct UpdateOperation { path: PathBuf, operation_id: String, @@ -227,7 +227,7 @@ impl Drop for UpdateOperation { } } -/// 只在拿到进程内锁后调用,旧版本断连遗留且已无执行者的 running 不能永久保留。 +/// 只在拿到进程内锁后调用,旧版本断连遗留且已无执行者的 running 不能永久保留 pub(crate) fn recover_interrupted(path: &Path, lock_path: &Path) -> Result<(), OperationError> { let state = read_persisted(path)?; if !matches!(state.operation.status, PersistedStatus::Running) { @@ -348,7 +348,7 @@ pub(crate) fn read_status( }) } -/// 调用方必须持有进程内锁及文件锁,避免将安装中间态当成手动部署。 +/// 调用方必须持有进程内锁及文件锁,避免将安装中间态当成手动部署 pub(crate) fn reconcile_installation( config: &SystemUpdateConfig, running: &ReleaseFiles, @@ -369,7 +369,7 @@ pub(crate) fn reconcile_installation( && (state.current_files.as_ref() != Some(&installed) || state.current_version.as_deref() != Some(config.version.as_str())) { - // 保留操作历史,但外部部署的文件不能继续使用旧操作推断版本。 + // 保留操作历史,但外部部署的文件不能继续使用旧操作推断版本 state.current_version = Some(config.version.clone()); state.current_files = Some(installed); changed = true; @@ -381,7 +381,7 @@ pub(crate) fn reconcile_installation( .is_ok_and(|actual| actual == expected) }); if !backup_valid { - // 无法证明旧备份完整时撤销回滚资格,不删除用户的备份文件。 + // 无法证明旧备份完整时撤销回滚资格,不删除用户的备份文件 state.previous_version = None; state.previous_files = None; changed = true; @@ -390,7 +390,7 @@ pub(crate) fn reconcile_installation( if changed { write_persisted(path, &state)?; } - // 指纹证明磁盘仍是本进程安装的候选,回滚到旧版本也必须等待重启。 + // 指纹证明磁盘仍是本进程安装的候选,回滚到旧版本也必须等待重启 read_status(path, need_restart) } diff --git a/backend/crates/gateway-host/src/system_update/swap.rs b/backend/crates/gateway-host/src/system_update/swap.rs index 25fe9456b..4c5893ac3 100644 --- a/backend/crates/gateway-host/src/system_update/swap.rs +++ b/backend/crates/gateway-host/src/system_update/swap.rs @@ -1,4 +1,4 @@ -//! 二进制与 Web 制品的交易式替换、跨文件系统移动与回滚。 +//! 二进制与 Web 制品的交易式替换、跨文件系统移动与回滚 use std::fmt; use std::fs; diff --git a/backend/crates/gateway-host/src/workers.rs b/backend/crates/gateway-host/src/workers.rs index c9c3e3476..4d2136f70 100644 --- a/backend/crates/gateway-host/src/workers.rs +++ b/backend/crates/gateway-host/src/workers.rs @@ -1,4 +1,4 @@ -//! Core 任务计划的唯一运行时:注册校验、lease、退避、健康与关闭。 +//! Core 任务计划的唯一运行时:注册校验、lease、退避、健康与关闭 use std::collections::{BTreeMap, BTreeSet}; use std::panic::AssertUnwindSafe; @@ -19,7 +19,7 @@ use gateway_core::task::{ }; use tokio::task::JoinHandle; -/// 进程内所有后台任务的唯一监督器。 +/// 进程内所有后台任务的唯一监督器 pub struct WorkerSupervisor { cancellation: CancellationToken, health: Arc, @@ -43,7 +43,7 @@ impl WorkerSupervisor { self.health.clone() } - /// 校验完整计划并且仅启动一次。 + /// 校验完整计划并且仅启动一次 pub fn start( &self, plan: Vec, @@ -109,8 +109,9 @@ impl WorkerSupervisor { } /// 先协作取消;所有任务共享同一绝对截止点,逾期任务被 abort, - /// lease 依赖 TTL 兜底。每个句柄恰好 join 一次:已在截止点前退出的 - /// 不会被二次 poll,逾期的在 abort 后等待其真正终止。 + /// lease 依赖 TTL 兜底 + /// 每个句柄恰好 join 一次:已在截止点前退出的 + /// 不会被二次 poll,逾期的在 abort 后等待其真正终止 pub async fn shutdown(&self, timeout: Duration) { self.cancellation.cancel(); let handles = std::mem::take(&mut *lock_unpoisoned(&self.handles)); diff --git a/backend/crates/gateway-host/tests/client_distribution.rs b/backend/crates/gateway-host/tests/client_distribution.rs index 5707200c2..53e40bbca 100644 --- a/backend/crates/gateway-host/tests/client_distribution.rs +++ b/backend/crates/gateway-host/tests/client_distribution.rs @@ -1,3 +1,5 @@ +//! 验证客户端下载信息的版本选择、过期处理与官方地址回退 + use chrono::{TimeZone as _, Utc}; use gateway_admin::model::client_distribution::{ ClientArchitecture, ClientDownloadPackage, ClientDownloadSource, diff --git a/backend/crates/gateway-host/tests/config.rs b/backend/crates/gateway-host/tests/config.rs index 33afacb74..dde017fbc 100644 --- a/backend/crates/gateway-host/tests/config.rs +++ b/backend/crates/gateway-host/tests/config.rs @@ -1,3 +1,5 @@ +//! 验证宿主配置默认值、资源路径与系统更新参数解析 + use std::path::PathBuf; use gateway_host::config::{FileLoggingConfig, HostConfig, ListenConfig, LoggingConfig}; diff --git a/backend/crates/gateway-host/tests/logging/mod.rs b/backend/crates/gateway-host/tests/logging/mod.rs index 12c059245..21b046268 100644 --- a/backend/crates/gateway-host/tests/logging/mod.rs +++ b/backend/crates/gateway-host/tests/logging/mod.rs @@ -1,3 +1,5 @@ +//! 日志测试入口,以及输出开关、关闭日志与部署时区测试 + use std::env; use std::fs; use std::io::Read; diff --git a/backend/crates/gateway-host/tests/logging/sink.rs b/backend/crates/gateway-host/tests/logging/sink.rs index 207862be5..edb52cffc 100644 --- a/backend/crates/gateway-host/tests/logging/sink.rs +++ b/backend/crates/gateway-host/tests/logging/sink.rs @@ -1,3 +1,5 @@ +//! 验证文件日志队列排空与写入失败后的完整性状态 + use super::*; use gateway_core::health::HealthState; use std::time::Duration; diff --git a/backend/crates/gateway-host/tests/logging/writer.rs b/backend/crates/gateway-host/tests/logging/writer.rs index 577dde160..3e2b70e9b 100644 --- a/backend/crates/gateway-host/tests/logging/writer.rs +++ b/backend/crates/gateway-host/tests/logging/writer.rs @@ -1,3 +1,5 @@ +//! 验证日志压缩、轮转与重启后按日期保留完整记录 + use super::*; use std::time::SystemTime; @@ -11,7 +13,7 @@ fn retention_preserves_complete_dates_across_compression_rotation_and_restart() true, || { let payload = "x".repeat(1024 * 1024); - // 三条记录已跨过 1 MiB 轮转边界;保留日期的 25 个分段由下方单独构造。 + // 三条记录已跨过 1 MiB 轮转边界;保留日期的 25 个分段由下方单独构造 for sequence in 0..ROTATION_RECORDS { tracing::info!(target: REQUEST_DUMP_LOG_TARGET, sequence, payload, "retention record"); tracing::info!(target: APPLICATION_LOG_TARGET, sequence, payload, "retention record"); @@ -44,7 +46,7 @@ fn retention_preserves_complete_dates_across_compression_rotation_and_restart() let path = directory.path().join(format!("{prefix}{old_date}.log")); seed_log(&path, old_date, "expired date\n"); expired.push(path); - // 时区切换或恢复的文件按较近写入日期保护整个分段组。 + // 时区切换或恢复的文件按较近写入日期保护整个分段组 let restored_date = old_date - chrono::Days::new(1); for segment in 0..=1 { let path = directory @@ -60,7 +62,7 @@ fn retention_preserves_complete_dates_across_compression_rotation_and_restart() } let unrelated = directory.path().join("unmanaged.log"); fs::write(&unrelated, "unmanaged\n").unwrap(); - // A crash before archive publication leaves the original and possibly a partial temporary file. + // 归档发布前崩溃会留下原文件,也可能留下未完成的临时文件 fs::write( retained[0].with_extension("log.gz.tmp"), "unfinished archive", diff --git a/backend/crates/gateway-host/tests/main.rs b/backend/crates/gateway-host/tests/main.rs index ead209dc7..712529fd7 100644 --- a/backend/crates/gateway-host/tests/main.rs +++ b/backend/crates/gateway-host/tests/main.rs @@ -1,3 +1,5 @@ +//! 宿主配置、网络、进程、日志与后台任务的测试入口 + mod client_distribution; mod config; mod logging; diff --git a/backend/crates/gateway-host/tests/official_plugins.rs b/backend/crates/gateway-host/tests/official_plugins.rs index 923dc8e5f..9fc07e636 100644 --- a/backend/crates/gateway-host/tests/official_plugins.rs +++ b/backend/crates/gateway-host/tests/official_plugins.rs @@ -1,3 +1,5 @@ +//! 验证官方发行目录的有界读取与符号链接拒绝 + use std::fs; use gateway_admin::ports::plugin_release::{ diff --git a/backend/crates/gateway-host/tests/outbound/connector.rs b/backend/crates/gateway-host/tests/outbound/connector.rs index 9eef1f5cc..aa04d606a 100644 --- a/backend/crates/gateway-host/tests/outbound/connector.rs +++ b/backend/crates/gateway-host/tests/outbound/connector.rs @@ -1,3 +1,5 @@ +//! 验证固定目标地址的直连和代理连接保留 Host、SNI 与证书校验 + use std::{sync::Arc, time::Duration}; use gateway_core::{account::OutboundProxy, upstream::UpstreamSendState}; diff --git a/backend/crates/gateway-host/tests/outbound/http.rs b/backend/crates/gateway-host/tests/outbound/http.rs index 1366a3e02..cbc3f055f 100644 --- a/backend/crates/gateway-host/tests/outbound/http.rs +++ b/backend/crates/gateway-host/tests/outbound/http.rs @@ -1,3 +1,5 @@ +//! 验证受管 HTTP 的协议、网络约束、时限与正文资源回收 + use std::time::Duration; use gateway_core::{account::OutboundProxy, upstream::UpstreamSendState}; @@ -212,7 +214,7 @@ async fn buffered_body_cannot_extend_the_request_deadline() { response.body.read(1).await.unwrap().unwrap().as_ref(), &[42] ); - // 上游已返回整块数据,插件延迟读取也不能绕过调用期限。 + // 上游已返回整块数据,插件延迟读取也不能绕过调用期限 tokio::time::sleep(Duration::from_millis(320)).await; let error = response.body.read(1).await.unwrap_err(); assert_eq!(error.send_state, UpstreamSendState::Sent); @@ -240,7 +242,7 @@ async fn cancelling_dns_releases_the_request_slot_without_starting_http() { let client = HttpClient::new() .unwrap() .with_resolver(Arc::new(PendingDns)); - // 超过总并发容量,逐次取消仍不能积累占用。 + // 超过总并发容量,逐次取消仍不能积累占用 for _ in 0..129 { assert!( tokio::time::timeout( diff --git a/backend/crates/gateway-host/tests/outbound/mod.rs b/backend/crates/gateway-host/tests/outbound/mod.rs index a5270e370..f099f500b 100644 --- a/backend/crates/gateway-host/tests/outbound/mod.rs +++ b/backend/crates/gateway-host/tests/outbound/mod.rs @@ -1,3 +1,5 @@ +//! 受管出站连接、HTTP、WebSocket 与网络策略的测试入口 + mod connector; mod http; mod network; diff --git a/backend/crates/gateway-host/tests/outbound/network.rs b/backend/crates/gateway-host/tests/outbound/network.rs index 7bdecb04a..f51d3e415 100644 --- a/backend/crates/gateway-host/tests/outbound/network.rs +++ b/backend/crates/gateway-host/tests/outbound/network.rs @@ -1,3 +1,5 @@ +//! 验证 DNS 固定、地址范围校验与解析超时的发送状态 + use std::{ net::SocketAddr, sync::{ diff --git a/backend/crates/gateway-host/tests/outbound/websocket.rs b/backend/crates/gateway-host/tests/outbound/websocket.rs index 9a4622182..58e92f92e 100644 --- a/backend/crates/gateway-host/tests/outbound/websocket.rs +++ b/backend/crates/gateway-host/tests/outbound/websocket.rs @@ -1,3 +1,5 @@ +//! 验证受管 WebSocket 的消息收发、关闭、超时与代理隔离 + use std::time::Duration; use futures::{SinkExt as _, StreamExt as _}; diff --git a/backend/crates/gateway-host/tests/plugin_distribution/github.rs b/backend/crates/gateway-host/tests/plugin_distribution/github.rs index d141759b9..d71c65d41 100644 --- a/backend/crates/gateway-host/tests/plugin_distribution/github.rs +++ b/backend/crates/gateway-host/tests/plugin_distribution/github.rs @@ -1,3 +1,5 @@ +//! 验证 GitHub 发行查询的凭据、缓存合并与固定制品下载 + use gateway_admin::{ model::{ AdminErrorKind, diff --git a/backend/crates/gateway-host/tests/plugin_distribution/http.rs b/backend/crates/gateway-host/tests/plugin_distribution/http.rs index d00c71c62..ffc9813e4 100644 --- a/backend/crates/gateway-host/tests/plugin_distribution/http.rs +++ b/backend/crates/gateway-host/tests/plugin_distribution/http.rs @@ -1,3 +1,5 @@ +//! 验证插件下载的凭据范围、内容摘要与安全错误输出 + use std::sync::Arc; use gateway_admin::{ diff --git a/backend/crates/gateway-host/tests/plugin_distribution/lifecycle.rs b/backend/crates/gateway-host/tests/plugin_distribution/lifecycle.rs index 02d171b37..b38110e49 100644 --- a/backend/crates/gateway-host/tests/plugin_distribution/lifecycle.rs +++ b/backend/crates/gateway-host/tests/plugin_distribution/lifecycle.rs @@ -1,3 +1,5 @@ +//! 验证发行查询取消、并发等待与限流到期后的资源释放 + use std::{ sync::{ Arc, @@ -70,7 +72,7 @@ async fn cancelling_the_fetch_owner_releases_the_shared_slot_for_the_next_query( .query_release(query("example/plugins"), vec![], None) .await }); - // 以来源确实收到请求为屏障,避免只取消尚未执行的任务而误报释放成功。 + // 以来源确实收到请求为屏障,避免只取消尚未执行的任务而误报释放成功 wait_for_requests(&server, 1).await; first.abort(); assert!(first.await.unwrap_err().is_cancelled()); @@ -113,7 +115,7 @@ async fn cancelling_a_cache_waiter_does_not_cancel_or_poison_the_fetch_owner() { }); wait_for_requests(&server, 1).await; let waiter = distribution.query_release(query("example/plugins"), vec![], None); - // timeout 会实际轮询等待者再取消;不会只中止一个未调度的 spawn。 + // timeout 会实际轮询等待者再取消;不会只中止一个未调度的 spawn assert!( tokio::time::timeout(Duration::from_millis(25), waiter) .await @@ -126,7 +128,7 @@ async fn cancelling_a_cache_waiter_does_not_cancel_or_poison_the_fetch_owner() { .unwrap(); assert_eq!(first.tag, "v1.0.0"); assert_eq!(server.received_requests().await.unwrap().len(), 1); - // 等待者取消不影响持有者完成,也不阻止之后的显式查询刷新结果。 + // 等待者取消不影响持有者完成,也不阻止之后的显式查询刷新结果 let refreshed = distribution .query_release(query("example/plugins"), vec![], None) .await @@ -234,7 +236,7 @@ async fn rate_limit_expiry_releases_the_identity_without_reusing_a_failure_cache .query_release(query("authorized/plugin"), vec![authorized], None) .await .unwrap(); - // 同一查询另有失败缓存;改用尚未查询的仓库,只验证共享身份退避的真实到期。 + // 同一查询另有失败缓存;改用尚未查询的仓库,只验证共享身份退避的真实到期 tokio::time::sleep(Duration::from_millis(1100)).await; let recovered = distribution .query_release(query("recovered/plugin"), vec![], None) diff --git a/backend/crates/gateway-host/tests/plugin_distribution/mod.rs b/backend/crates/gateway-host/tests/plugin_distribution/mod.rs index 83e7b1078..d251656ff 100644 --- a/backend/crates/gateway-host/tests/plugin_distribution/mod.rs +++ b/backend/crates/gateway-host/tests/plugin_distribution/mod.rs @@ -1,3 +1,5 @@ +//! 插件分发测试入口,以及共用下载环境与摘要辅助 + mod github; mod http; mod lifecycle; diff --git a/backend/crates/gateway-host/tests/pricing.rs b/backend/crates/gateway-host/tests/pricing.rs index 8d0213ad3..a9ec945e7 100644 --- a/backend/crates/gateway-host/tests/pricing.rs +++ b/backend/crates/gateway-host/tests/pricing.rs @@ -1,3 +1,5 @@ +//! 验证外部价格源的 Provider 选择、免费价格与无效文档拒绝 + use gateway_host::pricing::decode_catalog; use serde_json::json; diff --git a/backend/crates/gateway-host/tests/process.rs b/backend/crates/gateway-host/tests/process.rs index 40e1eda25..5c3a68b88 100644 --- a/backend/crates/gateway-host/tests/process.rs +++ b/backend/crates/gateway-host/tests/process.rs @@ -1,3 +1,5 @@ +//! 验证真实子进程的启动、停止、回收与容量释放 + #[cfg(unix)] use gateway_host::process::{ProcessExit, ProcessSpec, ProcessStartError, ProcessSupervisor}; @@ -82,7 +84,7 @@ async fn busy_and_blocked_children_can_be_stopped_and_reaped() { .unwrap() .unwrap(); assert_eq!(ready, b'R'); - // 就绪后保留输入句柄且不再读取输出,确保测试的是运行中的死循环或阻塞 I/O。 + // 就绪后保留输入句柄且不再读取输出,确保测试的是运行中的死循环或阻塞 I/O tokio::time::sleep(std::time::Duration::from_millis(100)).await; assert!(!directory.path().join("unblocked").exists()); process.control.stop(); @@ -123,7 +125,7 @@ async fn allocation_failure_in_a_bounded_child_returns_process_capacity() { let mut spec = worker(directory.path(), "allocate"); let executable = spec.executable.to_str().unwrap().replace('\'', "'\\''"); let wrapper = directory.path().join("bounded-worker"); - // 地址空间上限和禁用 core dump 仅保护测试机,不宣称生产监督器已施加 OS 内存隔离。 + // 地址空间上限和禁用 core dump 仅保护测试机,不宣称生产监督器已施加 OS 内存隔离 std::fs::write( &wrapper, format!("#!/bin/sh\nset -eu\nulimit -c 0\nulimit -v 65536\nexec '{executable}'\n"), diff --git a/backend/crates/gateway-host/tests/proxy_probe.rs b/backend/crates/gateway-host/tests/proxy_probe.rs index a4081f5c2..1b21820aa 100644 --- a/backend/crates/gateway-host/tests/proxy_probe.rs +++ b/backend/crates/gateway-host/tests/proxy_probe.rs @@ -1,3 +1,5 @@ +//! 验证代理探测的双栈地址、认证、证书与失败边界 + use gateway_admin::ports::proxy::ProxyProbe; use gateway_core::account::OutboundProxy; use gateway_host::proxy_probe::HttpProxyProbe; diff --git a/backend/crates/gateway-host/tests/retention.rs b/backend/crates/gateway-host/tests/retention.rs index 4333d61df..3f33c42d4 100644 --- a/backend/crates/gateway-host/tests/retention.rs +++ b/backend/crates/gateway-host/tests/retention.rs @@ -1,3 +1,5 @@ +//! 验证历史清理的轮转、批次预算、时长限制与取消行为 + use async_trait::async_trait; use chrono::{DateTime, Utc}; use gateway_admin::{ diff --git a/backend/crates/gateway-host/tests/serve.rs b/backend/crates/gateway-host/tests/serve.rs index 5a0a726fe..170d63b30 100644 --- a/backend/crates/gateway-host/tests/serve.rs +++ b/backend/crates/gateway-host/tests/serve.rs @@ -1,3 +1,5 @@ +//! 验证宿主服务关闭时的连接排空、超时与并发唤醒 + use std::sync::Arc; use std::time::Duration; @@ -52,7 +54,8 @@ async fn wait_until_idle_should_wake_when_last_guard_drops_after_first_poll() { async fn wait_until_idle_should_observe_guard_drop_racing_the_idle_check() { // 回归约束:notified 必须在读取活跃计数前完成注册(enable),否则最后 // 一个 guard 在计数检查与首次 poll 之间 drop 时唤醒丢失,只能等满整个 - // 超时。多线程反复交错,任何一次丢唤醒都会撞上 2s 超时并触发断言。 + // 超时 + // 多线程反复交错,任何一次丢唤醒都会撞上 2s 超时并触发断言 for _ in 0..256 { let tracker = Arc::new(ConnectionTracker::new(CancellationToken::new())); let guard = tracker.try_register().expect("register connection"); diff --git a/backend/crates/gateway-host/tests/support/mod.rs b/backend/crates/gateway-host/tests/support/mod.rs index a61610bd4..205a7c466 100644 --- a/backend/crates/gateway-host/tests/support/mod.rs +++ b/backend/crates/gateway-host/tests/support/mod.rs @@ -1 +1,3 @@ +//! 宿主测试共用的网络与服务辅助入口 + pub mod network; diff --git a/backend/crates/gateway-host/tests/support/network.rs b/backend/crates/gateway-host/tests/support/network.rs index 9c065322b..c3ff3ed58 100644 --- a/backend/crates/gateway-host/tests/support/network.rs +++ b/backend/crates/gateway-host/tests/support/network.rs @@ -1,3 +1,5 @@ +//! 宿主网络测试使用的固定 DNS、本地服务器与请求观测辅助 + use std::{ io, net::{IpAddr, SocketAddr}, @@ -174,7 +176,7 @@ async fn serve_http2( ) -> io::Result<()> { let service = hyper::service::service_fn(move |request: http::Request| { - // 模拟 nginx 1.24:URI 已携带 :authority,额外的 Host 仍按重复头拒绝。 + // 模拟 nginx 1.24:URI 已携带 :authority,额外的 Host 仍按重复头拒绝 let status = if request.headers().contains_key(http::header::HOST) { 400 } else { diff --git a/backend/crates/gateway-host/tests/support/process_worker.rs b/backend/crates/gateway-host/tests/support/process_worker.rs index 172e06c23..cb72db5a8 100644 --- a/backend/crates/gateway-host/tests/support/process_worker.rs +++ b/backend/crates/gateway-host/tests/support/process_worker.rs @@ -1,4 +1,4 @@ -//! Host 进程监督测试的 Rust 子进程,不依赖脚本解释器。 +//! Host 进程监督测试的 Rust 子进程,不依赖脚本解释器 use std::{ io::{Read as _, Write as _}, @@ -26,7 +26,7 @@ fn main() { std::fs::write("unblocked", b"stdin").unwrap(); } "stdout" => { - // 测试端不消费此正文,超过管道容量后必须由监督器终止,而不是等待写入完成。 + // 测试端不消费此正文,超过管道容量后必须由监督器终止,而不是等待写入完成 std::io::stdout() .write_all(&vec![b'x'; 8 * 1024 * 1024]) .unwrap(); @@ -34,7 +34,7 @@ fn main() { } "exit" => std::process::exit(23), "allocate" => { - // 仅供测试包装器在严格地址空间上限内触发真实分配失败,不能无保护地运行此模式。 + // 仅供测试包装器在严格地址空间上限内触发真实分配失败,不能无保护地运行此模式 std::hint::black_box(vec![b'x'; 64 * 1024 * 1024]); std::fs::write("allocation-succeeded", b"unexpected").unwrap(); } diff --git a/backend/crates/gateway-host/tests/system_update/channels.rs b/backend/crates/gateway-host/tests/system_update/channels.rs index 413cb76fc..d5bd6784c 100644 --- a/backend/crates/gateway-host/tests/system_update/channels.rs +++ b/backend/crates/gateway-host/tests/system_update/channels.rs @@ -1,3 +1,5 @@ +//! 验证更新通道选择、发行候选缓存与并发查询结果隔离 + use super::*; use gateway_admin::model::system::SystemUpdateChannel::{Alpha, Beta, Experimental, Rc, Stable}; diff --git a/backend/crates/gateway-host/tests/system_update/installation.rs b/backend/crates/gateway-host/tests/system_update/installation.rs index f6f151d0c..3d049df6f 100644 --- a/backend/crates/gateway-host/tests/system_update/installation.rs +++ b/backend/crates/gateway-host/tests/system_update/installation.rs @@ -1,3 +1,5 @@ +//! 验证安装状态恢复、外部文件变化与回滚备份校验 + use super::*; #[tokio::test] @@ -14,7 +16,7 @@ async fn manual_image_replacement_should_clear_pending_state_and_invalid_backup( .await; let service = fixture.service(&server); assert!(complete_update(&service, TARGET_VERSION).await.need_restart); - // 模拟重建镜像:数据卷保留,应用文件和容器内备份被重新替换。 + // 模拟重建镜像:数据卷保留,应用文件和容器内备份被重新替换 fixture.write_executable("manually-installed-binary"); fs::remove_file(fixture.root.path().join("codex-proxy-rs.backup")) .expect("remove image backup"); diff --git a/backend/crates/gateway-host/tests/system_update/mod.rs b/backend/crates/gateway-host/tests/system_update/mod.rs index 542b5c91a..b3cd7142c 100644 --- a/backend/crates/gateway-host/tests/system_update/mod.rs +++ b/backend/crates/gateway-host/tests/system_update/mod.rs @@ -1,3 +1,5 @@ +//! 系统更新测试入口,以及重启准备和操作互斥测试 + mod channels; mod installation; @@ -234,7 +236,7 @@ async fn restart_should_use_startup_path_after_running_executable_is_renamed() { const CHILD_ENV: &str = "CPR_TEST_RESTART_RENAMED_EXECUTABLE_CHILD"; let executable = std::env::current_exe().expect("test executable"); if std::env::var_os(CHILD_ENV).is_none() { - // 用例只移动目录项,不修改旧程序内容;同盘硬链接避免复制整个调试二进制。 + // 用例只移动目录项,不修改旧程序内容;同盘硬链接避免复制整个调试二进制 let directory = tempfile::tempdir_in(executable.parent().unwrap()).expect("isolated test directory"); let child = directory.path().join("restart-test"); @@ -257,7 +259,7 @@ async fn restart_should_use_startup_path_after_running_executable_is_renamed() { return; } if executable.file_name() == Some(std::ffi::OsStr::new("codex-proxy-rs.backup")) { - // 误启动旧程序时留下可观察结果并退出,避免旧副本再次发起重启。 + // 误启动旧程序时留下可观察结果并退出,避免旧副本再次发起重启 fs::write( executable .parent() @@ -303,7 +305,7 @@ async fn restart_should_use_startup_path_after_running_executable_is_renamed() { async fn restart_should_conflict_while_another_system_operation_is_running() { let fixture = Fixture::new(); // 裸 TCP 监听不回包:update 持有操作锁后停在 release 拉取阶段,accept - // 信号保证断言时锁一定已被占用。 + // 信号保证断言时锁一定已被占用 let listener = tokio::net::TcpListener::bind("127.0.0.1:0") .await .expect("release listener"); @@ -1757,7 +1759,7 @@ async fn accepted_update_should_survive_a_lost_http_response() { .await .expect("accepted"); accepted.notify_one(); - // 模拟受理响应丢失,客户端断开会取消此 HTTP handler。 + // 模拟受理响应丢失,客户端断开会取消此 HTTP handler std::future::pending::().await } }), @@ -1851,7 +1853,7 @@ async fn host_shutdown_should_finish_an_accepted_update_and_release_its_lock() { cancellation.clone(), fixture.config(&format!("{}/repos", upstream.uri())), ); - // 在后台任务第一次 poll 之前触发关闭,覆盖受理后的生命周期交接。 + // 在后台任务第一次 poll 之前触发关闭,覆盖受理后的生命周期交接 service .perform_test_update(Some(TARGET_VERSION.to_owned())) .await diff --git a/backend/crates/gateway-host/tests/workers.rs b/backend/crates/gateway-host/tests/workers.rs index 5af943534..91988de7c 100644 --- a/backend/crates/gateway-host/tests/workers.rs +++ b/backend/crates/gateway-host/tests/workers.rs @@ -1,3 +1,5 @@ +//! 验证 Worker 注册、任务组合、禁用原因与监督生命周期 + use std::collections::{BTreeMap, BTreeSet, VecDeque}; use std::future::pending; use std::num::NonZeroU64; @@ -632,7 +634,7 @@ async fn shutdown_timeout_aborts_uncooperative_task_and_drops_guard() { #[tokio::test] async fn shutdown_timeout_must_not_rejoin_workers_that_already_exited() { // 挂死任务放在 join 顺序最末的 kind(Retention),保证超时触发时 - // 排在它前面的合作任务句柄都已被 join 过一次。 + // 排在它前面的合作任务句柄都已被 join 过一次 let hung = HungTask { entered: Arc::new(AtomicBool::new(false)), notification: Arc::new(Notify::new()), diff --git a/backend/crates/gateway-plugin/runtime/build.rs b/backend/crates/gateway-plugin/runtime/build.rs index f8eab7648..92d35582f 100644 --- a/backend/crates/gateway-plugin/runtime/build.rs +++ b/backend/crates/gateway-plugin/runtime/build.rs @@ -1,7 +1,9 @@ +//! 配置测试插件子进程的链接参数,减少归档中的调试段体积 + fn main() { println!("cargo::rerun-if-changed=build.rs"); - // 测试插件会反复装包和解包;调试段不参与协议行为,不能让它放大每个用例的 I/O。 - // 仅调整两个测试子进程,宿主和测试入口仍保留完整调试信息。 + // 测试插件会反复装包和解包;调试段不参与协议行为,不能让它放大每个用例的 I/O + // 仅调整两个测试子进程,宿主和测试入口仍保留完整调试信息 let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); if matches!(target_os.as_str(), "linux" | "macos") { for binary in [ diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/catalog.rs b/backend/crates/gateway-plugin/runtime/src/adapter/catalog.rs index 0fccccd92..614b84137 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/catalog.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/catalog.rs @@ -1,3 +1,5 @@ +//! 编译插件模型目录贡献并校验能力绑定 + use std::{collections::BTreeSet, time::Duration}; use gateway_admin::model::{ @@ -66,7 +68,7 @@ pub(crate) fn validate_bindings( manifest: &Manifest, bindings: &[PluginCapabilityBinding], ) -> Result<(), AdminError> { - // 目录随实例完整发布;不接受请求级绑定,以免展示与执行的可见范围分叉。 + // 目录随实例完整发布;不接受请求级绑定,以免展示与执行的可见范围分叉 if let Some(declaration) = manifest.contributes.get(&Capability::ModelCatalog) && bindings .iter() diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/command_line/duration.rs b/backend/crates/gateway-plugin/runtime/src/adapter/command_line/duration.rs index 17311a40c..2e362ee61 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/command_line/duration.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/command_line/duration.rs @@ -1,4 +1,4 @@ -//! CLI duration 使用精确十进制纳秒,不经浮点转换或平台宽度截断。 +//! CLI duration 使用精确十进制纳秒,不经浮点转换或平台宽度截断 pub(super) fn parse(value: &str) -> Option { let (negative, mut remaining) = match value.as_bytes().first()? { diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/command_line/execution.rs b/backend/crates/gateway-plugin/runtime/src/adapter/command_line/execution.rs index 78e28738f..34af97c85 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/command_line/execution.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/command_line/execution.rs @@ -1,3 +1,5 @@ +//! 执行插件命令行调用,传递取消信号并映射 RPC 结果 + use gateway_admin::model::AdminError; use gateway_core::lifecycle::CancellationToken; use gateway_plugin_sdk::{Stage, call::management::CommandResult}; @@ -56,7 +58,7 @@ impl PluginCommandSession { }; match result { Ok(Ok(output)) => Ok(output), - // 一旦执行已开始,账号/私有状态回调也可能提交;仅 HTTP 未发送不能证明无副作用。 + // 一旦执行已开始,账号/私有状态回调也可能提交;仅 HTTP 未发送不能证明无副作用 Ok(Err(reason)) => Err(PluginCommandError::Incomplete { saved_accounts, reason, diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/command_line/mod.rs b/backend/crates/gateway-plugin/runtime/src/adapter/command_line/mod.rs index 92606a38a..aae868186 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/command_line/mod.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/command_line/mod.rs @@ -1,3 +1,5 @@ +//! 插件命令行能力的会话、命令查找与帮助入口 + mod duration; mod execution; mod parameters; @@ -25,7 +27,7 @@ pub(crate) struct PluginCommand { callbacks: Arc, } -/// 命令列表与执行共同保活已准备集合;这里不维护另一份当前代次。 +/// 命令列表与执行共同保活已准备集合;这里不维护另一份当前代次 pub struct PluginCommandSession { _reference: ExtensionSetReference, commands: Vec>, @@ -34,7 +36,7 @@ pub struct PluginCommandSession { limits: RpcLimits, } -/// 输出只交付显式 CLI 调用方,不作为诊断内容。 +/// 输出只交付显式 CLI 调用方,不作为诊断内容 pub struct PluginCommandOutput { pub stdout: String, pub stderr: String, @@ -72,8 +74,8 @@ impl PluginCommandSession { } } - /// CLI 专属终止入口;调用方不能将该集合同时发布给运行中的 HTTP 网关。 - /// 包括未声明命令、但为候选校验而启动的进程,全部回收后才允许退出主 Runtime。 + /// CLI 专属终止入口;调用方不能将该集合同时发布给运行中的 HTTP 网关 + /// 包括未声明命令、但为候选校验而启动的进程,全部回收后才允许退出主 Runtime pub async fn shutdown(self) { for session in &self.sessions { session.quiesce(); diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/command_line/parameters.rs b/backend/crates/gateway-plugin/runtime/src/adapter/command_line/parameters.rs index 6bd786cff..6675911b9 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/command_line/parameters.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/command_line/parameters.rs @@ -1,3 +1,5 @@ +//! 插件命令行参数定义校验、输入解析与帮助文本生成 + use std::collections::{BTreeMap, BTreeSet}; use gateway_admin::model::AdminError; @@ -173,6 +175,6 @@ fn description(value: &str) -> bool { } fn invalid_argument() -> AdminError { - // 不回显用户参数;即使未声明 sensitive,参数也可能包含凭据。 + // 不回显用户参数;即使未声明 sensitive,参数也可能包含凭据 AdminError::invalid("插件命令参数未知、重复、缺值或类型不正确;请查看 --help") } diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/command_line/registration.rs b/backend/crates/gateway-plugin/runtime/src/adapter/command_line/registration.rs index 24d036482..4e91431d7 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/command_line/registration.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/command_line/registration.rs @@ -1,3 +1,5 @@ +//! 校验插件命令行注册声明并准备可执行命令 + use super::{PluginCommand, parameters}; use crate::{RpcSession, callback::PluginCallbacks}; use gateway_admin::model::{AdminError, plugins::instances::PluginInstance}; diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/frontend_authentication.rs b/backend/crates/gateway-plugin/runtime/src/adapter/frontend_authentication.rs index 074a1e6c4..97d808c77 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/frontend_authentication.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/frontend_authentication.rs @@ -1,3 +1,5 @@ +//! 编译插件客户端认证绑定,并将认证结果映射为宿主身份 + use std::{collections::BTreeMap, sync::Arc, time::Duration}; use futures::future::BoxFuture; @@ -65,7 +67,7 @@ pub(crate) async fn prepare_entry( })) } -/// 认证器恢复失败仍占据认证入口,不能静默改走其他认证路径。 +/// 认证器恢复失败仍占据认证入口,不能静默改走其他认证路径 pub(crate) fn unavailable_entry(instance: &PluginInstance) -> Option { instance .bindings diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/management/callback.rs b/backend/crates/gateway-plugin/runtime/src/adapter/management/callback.rs index 3a9cdf571..ee305af26 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/management/callback.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/management/callback.rs @@ -1,3 +1,5 @@ +//! 插件管理授权回调的临时状态签发、绑定校验与调用分派 + use std::time::Duration; use chrono::Utc; @@ -53,7 +55,7 @@ impl ManagementEntry { } let flow = uuid::Uuid::new_v4().simple().to_string(); let mut owner = Sha256::new(); - // state 中的 owner 为随机且不可反查管理员的摘要;实际身份仅来自已验证的 Admin 上下文。 + // state 中的 owner 为随机且不可反查管理员的摘要;实际身份仅来自已验证的 Admin 上下文 owner.update(uuid::Uuid::new_v4().as_bytes()); match &context.principal { AdminPrincipal::Session { admin_user_id } => { @@ -144,7 +146,7 @@ impl ManagementEntry { OAuthPendingClaimOutcome::Claimed(payload) => payload, _ => return Err(invalid_state()), }; - // 在任何插件执行之前消耗 state;崩溃或响应丢失不得重放登录回调。 + // 在任何插件执行之前消耗 state;崩溃或响应丢失不得重放登录回调 if store .consume_claim(&namespace, &flow, &owner, &claim) .await diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/management/mod.rs b/backend/crates/gateway-plugin/runtime/src/adapter/management/mod.rs index c9534c1a0..e22e85bf4 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/management/mod.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/management/mod.rs @@ -1,3 +1,5 @@ +//! 插件管理页面资源读取、请求转发与响应解码 + mod callback; mod registration; mod validation; @@ -112,7 +114,7 @@ impl ManagementEntry { } } -// 管理 API 与公开回调共用类型和正文预算校验,完整响应头由 HTTP 入口解码。 +// 管理 API 与公开回调共用类型和正文预算校验,完整响应头由 HTTP 入口解码 fn decode_response( reply: RpcReply, allowed_content_types: &[String], diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/management/registration.rs b/backend/crates/gateway-plugin/runtime/src/adapter/management/registration.rs index 5d2a6556d..7bb6e252a 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/management/registration.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/management/registration.rs @@ -1,3 +1,5 @@ +//! 将插件管理声明编译为页面、路由与资源入口 + use std::{collections::BTreeMap, sync::Arc, time::Duration}; use gateway_admin::model::{ diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/management/validation.rs b/backend/crates/gateway-plugin/runtime/src/adapter/management/validation.rs index 05ee8e02c..77edb8bda 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/management/validation.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/management/validation.rs @@ -1,3 +1,5 @@ +//! 校验插件管理声明的路由路径与资源内容类型 + use std::collections::BTreeSet; use gateway_admin::model::AdminError; diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/mod.rs b/backend/crates/gateway-plugin/runtime/src/adapter/mod.rs index 8b36a4f18..2c3dd9dfe 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/mod.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/mod.rs @@ -1,3 +1,5 @@ +//! 将插件贡献适配为宿主认证、目录、策略、管理与执行能力 + pub(super) mod catalog; pub(super) mod command_line; pub(super) mod frontend_authentication; diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/observer.rs b/backend/crates/gateway-plugin/runtime/src/adapter/observer.rs index 43ec30b13..d7809aa07 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/observer.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/observer.rs @@ -1,3 +1,5 @@ +//! 编译插件观察订阅,投影请求事实并调度有界事件通知 + use std::{ collections::BTreeMap, sync::{Arc, Mutex}, diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/policy/http.rs b/backend/crates/gateway-plugin/runtime/src/adapter/policy/http.rs index 51b1322f3..230ad38e1 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/policy/http.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/policy/http.rs @@ -1,3 +1,5 @@ +//! 将宿主 HTTP 调用接入插件中间件,并管理正文与回调作用域 + use std::sync::Arc; use futures::stream; @@ -111,7 +113,7 @@ pub(super) async fn invoke( })).boxed_unsync(); Some(body) } else { - // 返回句柄时不拉取正文,仍消费唯一 End,释放本次 RPC 槽位。 + // 返回句柄时不拉取正文,仍消费唯一 End,释放本次 RPC 槽位 if stream .next() .await diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/policy/middleware.rs b/backend/crates/gateway-plugin/runtime/src/adapter/policy/middleware.rs index 45bf71f78..b1b10bae0 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/policy/middleware.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/policy/middleware.rs @@ -1,3 +1,5 @@ +//! 插件中间件链的执行、失败回退与响应正文生命周期管理 + use std::{ sync::{Arc, Mutex}, time::Duration, @@ -31,7 +33,7 @@ use super::{MiddlewareEntry, PluginRequestPolicyPlan}; const MAXIMUM_MAPPED_FRAMES_PER_SOURCE: usize = 64; const MAXIMUM_MAPPED_BYTES_PER_SOURCE: usize = 8 * 1024 * 1024; -// 协议适配只提供选择条件与单层调用;顺序、上下文捕获和续体拼接共用这一个入口。 +// 协议适配只提供选择条件与单层调用;顺序、上下文捕获和续体拼接共用这一个入口 type Invoke = fn( Arc, Context, @@ -572,7 +574,7 @@ impl MiddlewareBody for PluginMiddlewareBody { continue; } let mut frame = frame.ok_or(MiddlewareError::InvalidState)?; - // 只有宿主来源事件本就没有 wire 时,空 payload 才是事实帧。 + // 只有宿主来源事件本就没有 wire 时,空 payload 才是事实帧 let facts_only = frame.bytes().is_empty() && frame .event() @@ -651,7 +653,7 @@ fn validate_terminal_end( downstream_error: &Mutex>, ) -> Result<(), MiddlewareError> { // 只有插件在终帧后继续输出才属于状态无效;RPC 传输失败保持为调用故障, - // 两条失败路径都不能覆盖已经记录的下游域错误。 + // 两条失败路径都不能覆盖已经记录的下游域错误 match terminal { Ok(None) => Ok(()), Ok(Some(_)) => Err(take_error(downstream_error).unwrap_or(MiddlewareError::InvalidState)), diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/policy/mod.rs b/backend/crates/gateway-plugin/runtime/src/adapter/policy/mod.rs index 4b4f7c134..3a6ba63cc 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/policy/mod.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/policy/mod.rs @@ -1,3 +1,5 @@ +//! 编译插件请求策略与中间件绑定,组合为请求级执行计划 + mod http; mod middleware; mod retry; @@ -197,7 +199,7 @@ pub(crate) fn compile_entries( .collect() } -/// 已保存绑定的阶段是宿主合同;进程不可用时仍保留相同范围和故障策略。 +/// 已保存绑定的阶段是宿主合同;进程不可用时仍保留相同范围和故障策略 pub(crate) fn unavailable_entries( instance: &PluginInstance, ) -> Result, AdminError> { @@ -358,7 +360,7 @@ impl PluginRequestPolicyPlan { &right.instance_id, )) }); - // 发布时固定各挂载位置的有序候选,消息处理不再扫描其他边界的绑定。 + // 发布时固定各挂载位置的有序候选,消息处理不再扫描其他边界的绑定 let mut by_mount = BTreeMap::<_, Vec<_>>::new(); for entry in middleware { by_mount diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/policy/retry.rs b/backend/crates/gateway-plugin/runtime/src/adapter/policy/retry.rs index d204916ff..ca33517ff 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/policy/retry.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/policy/retry.rs @@ -1,3 +1,5 @@ +//! 将宿主允许的恢复路径交给插件重试策略裁决,并处理调用失败 + use std::{sync::Arc, time::Duration}; use futures::future::BoxFuture; @@ -28,7 +30,7 @@ pub(super) fn decide( input.facts.model.as_deref(), ) }) { - // 整条委托链共享预算,不能每个插件重新取得完整等待时间。 + // 整条委托链共享预算,不能每个插件重新取得完整等待时间 let remaining = timeout .min(input.facts.remaining_deadline) .saturating_sub(started.elapsed()); diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/policy/route_schedule.rs b/backend/crates/gateway-plugin/runtime/src/adapter/policy/route_schedule.rs index 57c7d8b7f..9ea4818ee 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/policy/route_schedule.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/policy/route_schedule.rs @@ -1,3 +1,5 @@ +//! 插件模型路由与账号调度调用,以及请求事实的线协议投影 + use std::time::{Duration, SystemTime, UNIX_EPOCH}; use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/policy/service.rs b/backend/crates/gateway-plugin/runtime/src/adapter/policy/service.rs index d031b4a97..3de5fd0e4 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/policy/service.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/policy/service.rs @@ -1,4 +1,4 @@ -//! 服务组合与 HTTP 使用相同续体;不在这里解释操作参数或执行管理业务。 +//! 服务组合与 HTTP 使用相同续体;不在这里解释操作参数或执行管理业务 use std::sync::Arc; diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/policy/websocket.rs b/backend/crates/gateway-plugin/runtime/src/adapter/policy/websocket.rs index c4ee8e901..2a556d5dd 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/policy/websocket.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/policy/websocket.rs @@ -1,3 +1,5 @@ +//! 将宿主 WebSocket 调用接入插件中间件,并管理消息与回调作用域 + use std::sync::Arc; use super::MiddlewareEntry; diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/scope.rs b/backend/crates/gateway-plugin/runtime/src/adapter/scope.rs index 944b8c107..016eeed43 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/scope.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/scope.rs @@ -1,3 +1,5 @@ +//! 编译插件绑定范围,并匹配身份、Provider、模型与调用场景 + use std::collections::BTreeSet; use gateway_admin::model::{AdminError, plugins::instances::PluginCapabilityBinding}; @@ -11,10 +13,10 @@ use gateway_core::{ routing::{AccountGroupId, PublicModelId}, }; -/// 由 Admin 已冻结引用编译出的通用请求绑定范围。 +/// 由 Admin 已冻结引用编译出的通用请求绑定范围 /// /// 组条件采用“任一命中”;同一 Key 可以同时属于多个组,因此不同组集合不能单独证明两个 -/// 调度绑定互斥。 +/// 调度绑定互斥 #[derive(Default)] pub(crate) struct BindingScope { client_keys: BTreeSet, @@ -100,7 +102,7 @@ impl BindingScope { } pub(crate) fn matches_route(&self, input: &ModelRouteInput) -> bool { - // Router 执行时 Provider 尚未产生;带 Provider 条件的绑定在编译时拒绝。 + // Router 执行时 Provider 尚未产生;带 Provider 条件的绑定在编译时拒绝 !self.has_provider_condition() && self.matches_identity(input.client_key_id(), input.account_group_ids()) && self.matches_model(Some(input.requested_model().as_str())) @@ -124,7 +126,7 @@ impl BindingScope { && self.matches_model(model) } - /// 保守判断两个范围能否同时匹配;只有 Key、Provider 或模型的明确不交集可证明互斥。 + /// 保守判断两个范围能否同时匹配;只有 Key、Provider 或模型的明确不交集可证明互斥 pub(crate) fn overlaps(&self, other: &Self) -> bool { !disjoint_when_both_constrained(&self.client_keys, &other.client_keys) && !disjoint_when_both_constrained(&self.providers, &other.providers) diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/upstream_adapter/event.rs b/backend/crates/gateway-plugin/runtime/src/adapter/upstream_adapter/event.rs index 3759fb472..c32b71695 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/upstream_adapter/event.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/upstream_adapter/event.rs @@ -1,3 +1,5 @@ +//! 校验并解码插件上游事件,转换为宿主执行事件与错误 + use std::time::Duration; use bytes::Bytes; @@ -67,7 +69,7 @@ impl EventDecoder { if tier.is_empty() || tier.len() > 64 || tier.chars().any(char::is_control) { return Err(invalid(sent)); } - // 上游可能在完成时才把 auto 解析为实际档位;与原生观测一样保留最新值。 + // 上游可能在完成时才把 auto 解析为实际档位;与原生观测一样保留最新值 self.service_tier = Some(tier); } let mut facts = Vec::new(); diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/upstream_adapter/execution.rs b/backend/crates/gateway-plugin/runtime/src/adapter/upstream_adapter/execution.rs index 7f9f54c52..230fb3fec 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/upstream_adapter/execution.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/upstream_adapter/execution.rs @@ -1,3 +1,5 @@ +//! 执行插件上游适配调用,管理事件流、续接状态与调用生命周期 + use std::sync::Arc; use futures::stream; @@ -64,7 +66,7 @@ pub(super) fn execute( decoder: EventDecoder::default(), ended: false, }; - // try_unfold 首次 poll 才运行闭包;Core 在此前登记 attempt 并持有账号租约。 + // try_unfold 首次 poll 才运行闭包;Core 在此前登记 attempt 并持有账号租约 Box::pin(stream::try_unfold(state, |mut state| async move { if state.ended { return Ok(None); @@ -100,7 +102,7 @@ impl Execution { active.managed.send_state.get(), )?; if self.decoder.completed { - // 先确认 RPC 正常终结,再发布唯一 Completed;否则 Core 不会继续 poll 尾部错误。 + // 先确认 RPC 正常终结,再发布唯一 Completed;否则 Core 不会继续 poll 尾部错误 let terminal = async { let tail = tokio::select! { biased; @@ -114,7 +116,7 @@ impl Execution { attach_continuation(&self.adapter, &self.invocation, active, &mut decoded).await }.await; if let Err(error) = terminal { - // 尾部故障不得交付成功 wire,但已经解析的计量事实仍归 Core 结算。 + // 尾部故障不得交付成功 wire,但已经解析的计量事实仍归 Core 结算 let metering = decoded .event .canonical_facts() diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/upstream_adapter/mod.rs b/backend/crates/gateway-plugin/runtime/src/adapter/upstream_adapter/mod.rs index dbeb90fc7..f225c0abf 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/upstream_adapter/mod.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/upstream_adapter/mod.rs @@ -1,3 +1,5 @@ +//! 将插件上游适配贡献编译为可按请求选择的执行计划 + mod event; mod execution; mod registration; diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/upstream_adapter/registration.rs b/backend/crates/gateway-plugin/runtime/src/adapter/upstream_adapter/registration.rs index 289e9df96..715be1a65 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/upstream_adapter/registration.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/upstream_adapter/registration.rs @@ -1,3 +1,5 @@ +//! 校验并准备插件上游适配声明与能力绑定 + use std::{collections::BTreeSet, sync::Arc, time::Duration}; use gateway_admin::model::{ diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/upstream_adapter/target.rs b/backend/crates/gateway-plugin/runtime/src/adapter/upstream_adapter/target.rs index a79acb7a7..ad7b9cb26 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/upstream_adapter/target.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/upstream_adapter/target.rs @@ -1,3 +1,5 @@ +//! 编译插件上游目标与路径约束,解析受管请求目标 + use std::collections::BTreeMap; use gateway_admin::model::AdminError; diff --git a/backend/crates/gateway-plugin/runtime/src/callback/accounts.rs b/backend/crates/gateway-plugin/runtime/src/callback/accounts.rs index 119c9ea6b..6782dcf84 100644 --- a/backend/crates/gateway-plugin/runtime/src/callback/accounts.rs +++ b/backend/crates/gateway-plugin/runtime/src/callback/accounts.rs @@ -1,4 +1,4 @@ -//! 账号域回调;账号归属与凭据版本由 Admin 复核,不接受插件伪造归属。 +//! 账号域回调;账号归属与凭据版本由 Admin 复核,不接受插件伪造归属 use std::sync::{Arc, OnceLock, Weak}; diff --git a/backend/crates/gateway-plugin/runtime/src/callback/admin.rs b/backend/crates/gateway-plugin/runtime/src/callback/admin.rs index 75c245a25..e3a89d76c 100644 --- a/backend/crates/gateway-plugin/runtime/src/callback/admin.rs +++ b/backend/crates/gateway-plugin/runtime/src/callback/admin.rs @@ -1,4 +1,4 @@ -//! Admin 端口回调共享的审计上下文、结果编码与错误映射。 +//! Admin 端口回调共享的审计上下文、结果编码与错误映射 use gateway_admin::model::{AdminError, AdminErrorKind, MutationActor, MutationContext}; use gateway_plugin_sdk::{CallContext, ErrorCode, PluginFault}; @@ -8,7 +8,7 @@ use crate::RpcReply; pub(super) fn mutation_context(context: &CallContext) -> MutationContext { MutationContext { actor: MutationActor::System, - // 回调沿用宿主签发的调用上下文,插件不能自报审计身份。 + // 回调沿用宿主签发的调用上下文,插件不能自报审计身份 request_id: format!( "plugin:{}:scope:{}:call:{}", context.instance_id, context.resource_scope_id, context.call_id diff --git a/backend/crates/gateway-plugin/runtime/src/callback/affinity.rs b/backend/crates/gateway-plugin/runtime/src/callback/affinity.rs index 88eef65c6..80e156d00 100644 --- a/backend/crates/gateway-plugin/runtime/src/callback/affinity.rs +++ b/backend/crates/gateway-plugin/runtime/src/callback/affinity.rs @@ -1,4 +1,4 @@ -//! Provider 会话亲和读取;只查询既有记录,不推导或写入亲和事实。 +//! Provider 会话亲和读取;只查询既有记录,不推导或写入亲和事实 use std::sync::{Arc, OnceLock, Weak}; diff --git a/backend/crates/gateway-plugin/runtime/src/callback/data.rs b/backend/crates/gateway-plugin/runtime/src/callback/data.rs index 78783eb75..6df0f493a 100644 --- a/backend/crates/gateway-plugin/runtime/src/callback/data.rs +++ b/backend/crates/gateway-plugin/runtime/src/callback/data.rs @@ -1,3 +1,5 @@ +//! 插件非秘密账号、Key 与额度事实查询的宿主回调 + use std::sync::Arc; use gateway_admin::model::{PageSize, provider_credentials::PluginAccountListQuery}; diff --git a/backend/crates/gateway-plugin/runtime/src/callback/error.rs b/backend/crates/gateway-plugin/runtime/src/callback/error.rs index cacb508fd..c91f6d515 100644 --- a/backend/crates/gateway-plugin/runtime/src/callback/error.rs +++ b/backend/crates/gateway-plugin/runtime/src/callback/error.rs @@ -1,4 +1,4 @@ -//! 插件错误保留业务分类与原始详情;诊断日志仍由各 owner 单独生成。 +//! 插件错误保留业务分类与原始详情;诊断日志仍由各 owner 单独生成 use gateway_core::{ engine::{EngineError, execution::gateway_error_from_engine, middleware::MiddlewareError}, diff --git a/backend/crates/gateway-plugin/runtime/src/callback/facts.rs b/backend/crates/gateway-plugin/runtime/src/callback/facts.rs index 6c142b936..91cf267fe 100644 --- a/backend/crates/gateway-plugin/runtime/src/callback/facts.rs +++ b/backend/crates/gateway-plugin/runtime/src/callback/facts.rs @@ -1,3 +1,5 @@ +//! 将宿主执行、会话、费用与观测事实投影为插件线协议值 + use gateway_core::{ engine::provider::ProviderCallMetadata, event::{ @@ -233,7 +235,7 @@ fn project_fact(event: GatewayEvent) -> Option return None, GatewayEvent::Completed(meta) => CanonicalEvent::Completed { id: meta.response_id().to_owned(), diff --git a/backend/crates/gateway-plugin/runtime/src/callback/http.rs b/backend/crates/gateway-plugin/runtime/src/callback/http.rs index 97c3c2570..4b115b9ec 100644 --- a/backend/crates/gateway-plugin/runtime/src/callback/http.rs +++ b/backend/crates/gateway-plugin/runtime/src/callback/http.rs @@ -1,3 +1,5 @@ +//! 插件受管 HTTP 回调的请求解码、执行分派与错误映射 + use std::sync::Arc; use gateway_core::upstream::UpstreamSendState; @@ -8,7 +10,7 @@ use serde::de::DeserializeOwned; use super::{CallResources, CallbackScope, HttpStream, denied, invalid}; use crate::RpcReply; -/// 回调的公共网络外层;账号鉴权只装饰请求,发送、读流和回收共用同一终端。 +/// 回调的公共网络外层;账号鉴权只装饰请求,发送、读流和回收共用同一终端 pub(super) struct HttpCallbacks<'a> { pub(super) client: &'a HttpClient, pub(super) network: &'a NetworkPolicy, diff --git a/backend/crates/gateway-plugin/runtime/src/callback/http_dispatch.rs b/backend/crates/gateway-plugin/runtime/src/callback/http_dispatch.rs index d5180641e..83cb05274 100644 --- a/backend/crates/gateway-plugin/runtime/src/callback/http_dispatch.rs +++ b/backend/crates/gateway-plugin/runtime/src/callback/http_dispatch.rs @@ -1,4 +1,4 @@ -//! 主动 HTTP 子调用只进入 API 分派端口,正文复用当前 RPC 的资源池。 +//! 主动 HTTP 子调用只进入 API 分派端口,正文复用当前 RPC 的资源池 use super::{CallResources, http_middleware, invalid, services::ServicePorts}; use crate::RpcReply; use gateway_core::middleware::http as core; @@ -49,7 +49,7 @@ pub(super) async fn call( |parent| parent.call_id.clone(), )), extensions: call.scope.child_extensions(&context.instance_id)?, - // 句柄可以在父 RPC End 后交给 HTTP 发送端;生命周期属于父调用和响应正文,不能在 RPC End 提前取消。 + // 句柄可以在父 RPC End 后交给 HTTP 发送端;生命周期属于父调用和响应正文,不能在 RPC End 提前取消 cancellation: parent.map_or_else( || call.cancellation.child_token(), |parent| parent.cancellation.child_token(), diff --git a/backend/crates/gateway-plugin/runtime/src/callback/http_middleware/mod.rs b/backend/crates/gateway-plugin/runtime/src/callback/http_middleware/mod.rs index d3f562820..30d2f5ba6 100644 --- a/backend/crates/gateway-plugin/runtime/src/callback/http_middleware/mod.rs +++ b/backend/crates/gateway-plugin/runtime/src/callback/http_middleware/mod.rs @@ -1,4 +1,4 @@ -//! HTTP 边界只搬运类型化请求、响应与正文资源;业务默认实现仍由 API 持有。 +//! HTTP 边界只搬运类型化请求、响应与正文资源;业务默认实现仍由 API 持有 pub(crate) mod resources; mod websocket; @@ -107,7 +107,7 @@ impl Invocation { request: wire::Request, payload: Vec, ) -> Result { - // 先校验完整 HTTP 值,再消费唯一续体;请求 extensions(包括连接升级)原样搬运。 + // 先校验完整 HTTP 值,再消费唯一续体;请求 extensions(包括连接升级)原样搬运 let method = request.method.parse().map_err(|_| invalid())?; let uri = request.uri.parse().map_err(|_| invalid())?; let headers = headers(request.headers)?; diff --git a/backend/crates/gateway-plugin/runtime/src/callback/http_middleware/resources.rs b/backend/crates/gateway-plugin/runtime/src/callback/http_middleware/resources.rs index cad7cfad7..01dfbd5b3 100644 --- a/backend/crates/gateway-plugin/runtime/src/callback/http_middleware/resources.rs +++ b/backend/crates/gateway-plugin/runtime/src/callback/http_middleware/resources.rs @@ -1,4 +1,4 @@ -//! HTTP 正文与响应部件共享同一调用资源池;next 与主动分派复用句柄和背压。 +//! HTTP 正文与响应部件共享同一调用资源池;next 与主动分派复用句柄和背压 use super::*; use bytes::Bytes; use futures::StreamExt as _; @@ -45,7 +45,7 @@ struct PipeReader { impl Drop for PipeReader { fn drop(&mut self) { - // 下游可以提前返回;消费端退出即归还管道名额,并唤醒仍在背压中等待的写入。 + // 下游可以提前返回;消费端退出即归还管道名额,并唤醒仍在背压中等待的写入 self.cancellation.cancel(); if let Some(resources) = self.resources.upgrade() { resources @@ -248,7 +248,7 @@ impl Resources { state.body.as_mut().ok_or_else(unavailable)?.frame().await }; if !matches!(&frame, Some(Ok(_))) { - // EOF 和读取失败均结束读取所有权;原响应部件仍可随替换后的正文返回。 + // EOF 和读取失败均结束读取所有权;原响应部件仍可随替换后的正文返回 state.body.take(); self.entries .lock() diff --git a/backend/crates/gateway-plugin/runtime/src/callback/http_middleware/websocket.rs b/backend/crates/gateway-plugin/runtime/src/callback/http_middleware/websocket.rs index 9d32e15fa..d908bfbb5 100644 --- a/backend/crates/gateway-plugin/runtime/src/callback/http_middleware/websocket.rs +++ b/backend/crates/gateway-plugin/runtime/src/callback/http_middleware/websocket.rs @@ -1,4 +1,4 @@ -//! 握手消费当前 HTTP 续体;收发继续使用同一调用的受管消息资源。 +//! 握手消费当前 HTTP 续体;收发继续使用同一调用的受管消息资源 use super::*; use crate::callback::websocket_middleware::Invocation as Messages; diff --git a/backend/crates/gateway-plugin/runtime/src/callback/keys.rs b/backend/crates/gateway-plugin/runtime/src/callback/keys.rs index ca5a17655..a8fd25bc6 100644 --- a/backend/crates/gateway-plugin/runtime/src/callback/keys.rs +++ b/backend/crates/gateway-plugin/runtime/src/callback/keys.rs @@ -1,4 +1,4 @@ -//! Key 目录与预算回调;实例身份由宿主冻结,写入授权在存储事务复验。 +//! Key 目录与预算回调;实例身份由宿主冻结,写入授权在存储事务复验 use std::sync::{Arc, OnceLock, Weak}; diff --git a/backend/crates/gateway-plugin/runtime/src/callback/log.rs b/backend/crates/gateway-plugin/runtime/src/callback/log.rs index 749b93055..30045d2d3 100644 --- a/backend/crates/gateway-plugin/runtime/src/callback/log.rs +++ b/backend/crates/gateway-plugin/runtime/src/callback/log.rs @@ -1,3 +1,5 @@ +//! 插件日志回调的输入校验、输出限额与宿主日志写入 + use std::{ sync::{Arc, Mutex}, time::Duration, @@ -12,7 +14,7 @@ use tokio::{sync::Semaphore, time::Instant}; use crate::RpcReply; -// 这是保护宿主的有界默认值,不是吞吐保证;预算按实例 incarnation 隔离。 +// 这是保护宿主的有界默认值,不是吞吐保证;预算按实例 incarnation 隔离 const WINDOW: Duration = Duration::from_secs(1); const MAXIMUM_EVENTS: u32 = 64; const MAXIMUM_INPUT_BYTES: usize = 8 * 1024; @@ -93,16 +95,17 @@ impl PluginLog { budget.recorded += 1; (std::mem::take(&mut budget.suppressed), slot) }; - // 使用现有诊断 owner;未知键名、正文和嵌套敏感字段不会原样进入普通日志。 + // 使用现有诊断 owner;未知键名、正文和嵌套敏感字段不会原样进入普通日志 let fields = diagnostic_json(&serde_json::to_value(request.fields).map_err(|_| super::invalid())?); - // 标识的字符形状不能证明内容安全;事件名也只保留摘要。 + // 标识的字符形状不能证明内容安全;事件名也只保留摘要 let event = body_fingerprint(request.event.as_bytes()); let plugin_id = self.plugin_id.clone(); let plugin_version = self.plugin_version.clone(); let context = context.clone(); - // tracing 后端可能同步阻塞。任务容量跨实例与代次共享,并由写入任务持有到完成, - // 父调用取消不能释放容量后继续无限排队;已受理日志可在父调用结束后落盘。 + // tracing 后端可能同步阻塞 + // 任务容量跨实例与代次共享,并由写入任务持有到完成, + // 父调用取消不能释放容量后继续无限排队;已受理日志可在父调用结束后落盘 tokio::task::spawn_blocking(move || { let _slot = slot; macro_rules! record { diff --git a/backend/crates/gateway-plugin/runtime/src/callback/middleware/body.rs b/backend/crates/gateway-plugin/runtime/src/callback/middleware/body.rs index e99030005..43eaebda4 100644 --- a/backend/crates/gateway-plugin/runtime/src/callback/middleware/body.rs +++ b/backend/crates/gateway-plugin/runtime/src/callback/middleware/body.rs @@ -1,3 +1,5 @@ +//! 插件中间件正文句柄的读取、交付事实与关闭生命周期 + use std::sync::{ Arc, Mutex, atomic::{AtomicBool, Ordering}, @@ -175,8 +177,8 @@ impl BodyResource { } pub(super) async fn read(&self, maximum: usize) -> Result, PluginFault> { - // SDK 发出上一源的映射帧后即可请求下一源;消费者可能尚未处理排队帧。 - // 等待归还而非报冲突,仍只保留一个源;父 RPC 的期限与取消约束等待。 + // SDK 发出上一源的映射帧后即可请求下一源;消费者可能尚未处理排队帧 + // 等待归还而非报冲突,仍只保留一个源;父 RPC 的期限与取消约束等待 let mut state = loop { let released = self.source_released.notified(); let state = self.state.lock().await; @@ -210,7 +212,7 @@ impl BodyResource { }; let transformed = frame.transformed(); let (bytes, framing, terminal, envelope) = frame.into_parts(); - // canonical-only 事件没有客户端正文,仍须保留并允许读取其完整事实。 + // canonical-only 事件没有客户端正文,仍须保留并允许读取其完整事实 let facts_only = framing == MiddlewareFraming::RawBytes && bytes.is_empty() && envelope.is_some(); if (!facts_only && framing != self.expected_framing) || bytes.len() > maximum { diff --git a/backend/crates/gateway-plugin/runtime/src/callback/middleware/headers.rs b/backend/crates/gateway-plugin/runtime/src/callback/middleware/headers.rs index f35fa1aea..b79b7e614 100644 --- a/backend/crates/gateway-plugin/runtime/src/callback/middleware/headers.rs +++ b/backend/crates/gateway-plugin/runtime/src/callback/middleware/headers.rs @@ -1,3 +1,5 @@ +//! 校验插件请求头与响应头,并应用显式头部修改 + use bytes::Bytes; use gateway_core::engine::middleware::MiddlewareHeader; use gateway_plugin_sdk::{ @@ -16,7 +18,7 @@ pub(super) fn apply_header_mutations( headers: &mut Vec, mutations: &[MiddlewareHeaderMutation], ) -> Result<(), PluginFault> { - // SDK 的完整替换会先删除原始集合,再追加新的集合。 + // SDK 的完整替换会先删除原始集合,再追加新的集合 if mutations.len() > MAX_HEADERS * 2 { return Err(invalid()); } @@ -39,7 +41,7 @@ pub(super) fn apply_header_mutations( validate_headers(headers) } -/// 保留完整 header 集合,只检查传输格式与资源大小。 +/// 保留完整 header 集合,只检查传输格式与资源大小 pub(super) fn validate_headers(headers: &[MiddlewareHeader]) -> Result<(), PluginFault> { if headers.len() > MAX_HEADERS { return Err(invalid()); diff --git a/backend/crates/gateway-plugin/runtime/src/callback/middleware/mod.rs b/backend/crates/gateway-plugin/runtime/src/callback/middleware/mod.rs index c7f8ca294..01608e7f9 100644 --- a/backend/crates/gateway-plugin/runtime/src/callback/middleware/mod.rs +++ b/backend/crates/gateway-plugin/runtime/src/callback/middleware/mod.rs @@ -1,3 +1,5 @@ +//! 插件中间件回调的调用状态、后续处理委托与资源回收 + mod body; mod headers; @@ -568,7 +570,7 @@ impl MiddlewareInvocation { } MiddlewareResponseBody::Empty => { // 调用过 next 后不能靠关闭下游流伪装成已完成;需要丢弃正文时也必须 - // 通过 preserving mapper 拉取到底,才能保留计量、终态和取消合同。 + // 通过 preserving mapper 拉取到底,才能保留计量、终态和取消合同 if downstream.is_some() { return Err(MiddlewareError::InvalidState); } @@ -660,7 +662,7 @@ fn validate_status(status: u16) -> Result<(), PluginFault> { const fn framing_for_response(transport: ClientTransport, status: u16) -> MiddlewareFraming { match transport { - // WebSocket 的失败也作为 JSON 消息交付,不使用 HTTP 错误正文的字节流边界。 + // WebSocket 的失败也作为 JSON 消息交付,不使用 HTTP 错误正文的字节流边界 ClientTransport::WebSocket => MiddlewareFraming::JsonDocument, _ if status < 200 || status >= 300 => MiddlewareFraming::RawBytes, ClientTransport::HttpSse => MiddlewareFraming::SseEvent, diff --git a/backend/crates/gateway-plugin/runtime/src/callback/mod.rs b/backend/crates/gateway-plugin/runtime/src/callback/mod.rs index 64fcdf2a0..9923bf480 100644 --- a/backend/crates/gateway-plugin/runtime/src/callback/mod.rs +++ b/backend/crates/gateway-plugin/runtime/src/callback/mod.rs @@ -1,3 +1,5 @@ +//! 插件宿主回调端口的组合、授权检查与方法分派 + mod accounts; mod admin; mod affinity; @@ -118,7 +120,7 @@ impl CallResources { middleware.and_then(|middleware| middleware.request_settings()) } - /// 建立受管流后,网络操作各自计时;连接空闲时间不消耗后续操作的预算。 + /// 建立受管流后,网络操作各自计时;连接空闲时间不消耗后续操作的预算 fn timeout(&self) -> Result { let state = self .state @@ -454,9 +456,10 @@ impl CallbackHandler for PluginCallbacks { .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); state.closed = true; - // RPC End 可以先于消费者取完已入队的中间件正文到达。这里只撤销新的 + // RPC End 可以先于消费者取完已入队的中间件正文到达 + // 这里只撤销新的 // callback 入口;正文包装器持有 invocation,最后一个 owner 释放时再关闭 - // 下游,避免提前丢失尚未搬运的计量/终态信封。 + // 下游,避免提前丢失尚未搬运的计量/终态信封 state.middleware.take(); for stream in state.streams.values() { stream.close(); diff --git a/backend/crates/gateway-plugin/runtime/src/callback/model.rs b/backend/crates/gateway-plugin/runtime/src/callback/model.rs index 8ead173b7..99fe13581 100644 --- a/backend/crates/gateway-plugin/runtime/src/callback/model.rs +++ b/backend/crates/gateway-plugin/runtime/src/callback/model.rs @@ -1,4 +1,4 @@ -//! 嵌套模型执行;只驱动 Core 会话,不拥有路由、重试或计费规则。 +//! 嵌套模型执行;只驱动 Core 会话,不拥有路由、重试或计费规则 use std::sync::{Arc, OnceLock, Weak}; @@ -254,7 +254,7 @@ impl PluginModels { .await .map_err(super::error::gateway)?; // 调用持有身份直到 RPC 结束,流式执行不会因单次 callback 返回而提前取消; - // 同一 Key 复用身份快照,每次执行由 Core 创建独立调用图。 + // 同一 Key 复用身份快照,每次执行由 Core 创建独立调用图 bindings.insert(client_key_id, bound.clone()); Ok(bound) } @@ -584,7 +584,7 @@ impl ModelStream { } }; if events == 0 { - // 空批次仍需释放待交付屏障,已经提交的空批次直接继续拉取。 + // 空批次仍需释放待交付屏障,已经提交的空批次直接继续拉取 if commit { let result = state .session diff --git a/backend/crates/gateway-plugin/runtime/src/callback/private_state.rs b/backend/crates/gateway-plugin/runtime/src/callback/private_state.rs index a1c498981..937fc60f2 100644 --- a/backend/crates/gateway-plugin/runtime/src/callback/private_state.rs +++ b/backend/crates/gateway-plugin/runtime/src/callback/private_state.rs @@ -1,3 +1,5 @@ +//! 插件私有状态回调的访问授权、数据校验与存储调用 + use std::{ collections::BTreeMap, sync::{Arc, RwLock}, diff --git a/backend/crates/gateway-plugin/runtime/src/callback/resources.rs b/backend/crates/gateway-plugin/runtime/src/callback/resources.rs index 572e994ee..d7844c6e2 100644 --- a/backend/crates/gateway-plugin/runtime/src/callback/resources.rs +++ b/backend/crates/gateway-plugin/runtime/src/callback/resources.rs @@ -1,3 +1,5 @@ +//! 将插件自有资源回调绑定到宿主管理端口与实例身份 + use super::{ admin::{encode, map_admin_error, mutation_context}, denied, invalid, diff --git a/backend/crates/gateway-plugin/runtime/src/callback/scope.rs b/backend/crates/gateway-plugin/runtime/src/callback/scope.rs index 5c8f7c4ff..5dacacd69 100644 --- a/backend/crates/gateway-plugin/runtime/src/callback/scope.rs +++ b/backend/crates/gateway-plugin/runtime/src/callback/scope.rs @@ -1,3 +1,5 @@ +//! 插件调用的权限作用域、子调用上下文与出站副作用观测 + use std::sync::Arc; use gateway_core::{ @@ -5,7 +7,7 @@ use gateway_core::{ }; use gateway_plugin_sdk::{CallContext, Stage}; -/// 所有中间件共享的父调用身份;不混入协议操作或服务参数。 +/// 所有中间件共享的父调用身份;不混入协议操作或服务参数 #[derive(Clone)] pub(crate) struct InvocationContext { pub request_id: String, @@ -14,7 +16,7 @@ pub(crate) struct InvocationContext { pub plan: gateway_core::engine::middleware::FrozenMiddlewarePlan, } -/// 一次回调的调用链、账号与网络事实,由操作持有到完成。 +/// 一次回调的调用链、账号与网络事实,由操作持有到完成 pub(crate) struct CallbackScope { pub(super) origin: Option, stage: Stage, @@ -28,7 +30,7 @@ pub(crate) struct CallbackScope { impl CallbackScope { pub(super) fn observe_http_dispatch(&self) { - // 子路由及其插件可能执行模型或提交事务,父执行不能再按 Provider 的 not_sent 透明重放。 + // 子路由及其插件可能执行模型或提交事务,父执行不能再按 Provider 的 not_sent 透明重放 if let Some(effects) = &self.execution_effects { effects.observe(); } @@ -39,7 +41,7 @@ impl CallbackScope { instance_id: &str, ) -> Result { - // 上游适配器在进入回调前已登记当前实例;主动子调用沿用该集合。 + // 上游适配器在进入回调前已登记当前实例;主动子调用沿用该集合 if self.extension_scope.contains(instance_id) { return Ok(self.extension_scope.clone()); } @@ -145,7 +147,7 @@ impl HttpAttempt { impl Drop for HttpAttempt { fn drop(&mut self) { - // HTTP future 被取消时无法证明上游未接收;通知 Core 收紧重放判断。 + // HTTP future 被取消时无法证明上游未接收;通知 Core 收紧重放判断 if !self.completed { self.observe(UpstreamSendState::Ambiguous); } diff --git a/backend/crates/gateway-plugin/runtime/src/callback/services.rs b/backend/crates/gateway-plugin/runtime/src/callback/services.rs index 73d6a67a0..9496c779d 100644 --- a/backend/crates/gateway-plugin/runtime/src/callback/services.rs +++ b/backend/crates/gateway-plugin/runtime/src/callback/services.rs @@ -1,4 +1,4 @@ -//! 通用公开服务回调;操作目录与类型校验由业务服务登记。 +//! 通用公开服务回调;操作目录与类型校验由业务服务登记 use super::{CallbackScope, MiddlewareCallback, invalid}; use crate::RpcReply; diff --git a/backend/crates/gateway-plugin/runtime/src/callback/upstream/mod.rs b/backend/crates/gateway-plugin/runtime/src/callback/upstream/mod.rs index d39c5fd8c..c030a0a46 100644 --- a/backend/crates/gateway-plugin/runtime/src/callback/upstream/mod.rs +++ b/backend/crates/gateway-plugin/runtime/src/callback/upstream/mod.rs @@ -1,3 +1,5 @@ +//! 插件受管上游请求与连接回调,跟踪发送状态并约束连接归属 + mod sessions; use std::{ @@ -89,7 +91,7 @@ impl ManagedUpstream { pub(crate) async fn retain_connection(&self) -> Result { let (url, socket) = self.socket.lock().await.take().ok_or_else(denied)?; - // 旧调用仍在收发时不能把同一连接交给下一次执行。 + // 旧调用仍在收发时不能把同一连接交给下一次执行 let socket = match Arc::try_unwrap(socket) { Ok(socket) if !socket.is_closed() => socket, Ok(_) => return Err(denied()), @@ -118,7 +120,7 @@ impl ManagedUpstream { return; } let mut socket = self.socket.lock().await; - // 关闭后的读取结果可能晚于下一次 open,不能移除新连接。 + // 关闭后的读取结果可能晚于下一次 open,不能移除新连接 if socket .as_ref() .is_some_and(|(_, current)| Arc::ptr_eq(current, connection)) @@ -142,7 +144,7 @@ impl ManagedUpstream { "selected account authentication is unavailable", ) })?; - // 账号设置先构成基线;插件同名输入替换默认值,多值头保持原顺序。 + // 账号设置先构成基线;插件同名输入替换默认值,多值头保持原顺序 let mut headers: Vec<_> = authorization .into_iter() .filter(|header| { @@ -252,7 +254,7 @@ pub(super) async fn dispatch( timeout()?, ) .await; - // 握手也已向上游发送账号身份;失败不能伪装成未出站或跳过账号反馈。 + // 握手也已向上游发送账号身份;失败不能伪装成未出站或跳过账号反馈 attempt.finish( response .as_ref() diff --git a/backend/crates/gateway-plugin/runtime/src/callback/upstream/sessions.rs b/backend/crates/gateway-plugin/runtime/src/callback/upstream/sessions.rs index 21a720528..7aa868401 100644 --- a/backend/crates/gateway-plugin/runtime/src/callback/upstream/sessions.rs +++ b/backend/crates/gateway-plugin/runtime/src/callback/upstream/sessions.rs @@ -1,3 +1,5 @@ +//! 按所属会话保留、复用与回收插件受管上游连接 + use std::{ collections::BTreeMap, sync::{Arc, Mutex}, @@ -14,7 +16,7 @@ pub(crate) struct ConnectionOwner { pub credential_revision: u64, } -/// 每个适配器代次独立持有连接;取出即独占,失败或取消不会归还半完成会话。 +/// 每个适配器代次独立持有连接;取出即独占,失败或取消不会归还半完成会话 #[derive(Default)] pub(crate) struct ConnectionPool { idle: Mutex>, diff --git a/backend/crates/gateway-plugin/runtime/src/callback/websocket_middleware.rs b/backend/crates/gateway-plugin/runtime/src/callback/websocket_middleware.rs index 22a363603..4fa7da028 100644 --- a/backend/crates/gateway-plugin/runtime/src/callback/websocket_middleware.rs +++ b/backend/crates/gateway-plugin/runtime/src/callback/websocket_middleware.rs @@ -1,4 +1,4 @@ -//! 每次消息调用拥有自己的续体和正文句柄,发送端由连接 owner 持有。 +//! 每次消息调用拥有自己的续体和正文句柄,发送端由连接 owner 持有 use super::MiddlewareCallback; use crate::RpcReply; use bytes::Bytes; diff --git a/backend/crates/gateway-plugin/runtime/src/contribution.rs b/backend/crates/gateway-plugin/runtime/src/contribution.rs index ca2036096..bf71bd0c8 100644 --- a/backend/crates/gateway-plugin/runtime/src/contribution.rs +++ b/backend/crates/gateway-plugin/runtime/src/contribution.rs @@ -1,3 +1,5 @@ +//! 将实例能力绑定解析为清单中的唯一贡献声明 + use gateway_admin::model::{AdminError, plugins::instances::PluginCapabilityBinding}; use gateway_plugin_sdk::{Capability, ContributionDeclaration, Manifest}; @@ -6,7 +8,7 @@ pub(super) struct ResolvedContribution<'a> { pub(super) declaration: &'a ContributionDeclaration, } -/// 绑定只保存贡献项 ID;实际能力始终来自已检查清单的 map key。 +/// 绑定只保存贡献项 ID;实际能力始终来自已检查清单的 map key pub(super) fn resolve<'a>( manifest: &'a Manifest, binding: &PluginCapabilityBinding, diff --git a/backend/crates/gateway-plugin/runtime/src/generation/configuration.rs b/backend/crates/gateway-plugin/runtime/src/generation/configuration.rs index 404b7e13a..693eb85cd 100644 --- a/backend/crates/gateway-plugin/runtime/src/generation/configuration.rs +++ b/backend/crates/gateway-plugin/runtime/src/generation/configuration.rs @@ -1,3 +1,5 @@ +//! 校验插件实例配置、敏感字段与能力绑定,生成安全的配置错误 + use std::collections::BTreeSet; use gateway_admin::model::{ @@ -106,7 +108,7 @@ fn prepare_configuration( return Err(AdminError::invalid("普通配置不能包含声明的敏感字段")); } let configuration = serde_json::Value::Object(configuration); - // 禁用网络与文件解析,配置 schema 只能引用包内同一 JSON 文档。 + // 禁用网络与文件解析,配置 schema 只能引用包内同一 JSON 文档 let schema = jsonschema::options() .offline() .with_pattern_options(jsonschema::PatternOptions::fancy_regex().backtrack_limit(20_000)) @@ -114,7 +116,7 @@ fn prepare_configuration( .map_err(|_| AdminError::invalid("插件配置 schema 无效或引用外部资源"))?; let mut ready = true; for error in schema.iter_errors(&configuration) { - // 待配置只代表缺少必填值,不能借停用保存类型错误或非法配置。 + // 待配置只代表缺少必填值,不能借停用保存类型错误或非法配置 if matches!( error.kind(), jsonschema::error::ValidationErrorKind::Required { .. } @@ -130,7 +132,7 @@ fn prepare_configuration( fn configuration_error(error: &jsonschema::ValidationError<'_>) -> AdminError { use jsonschema::error::ValidationErrorKind; - // 校验库的 Display 会包含原值,敏感配置也参与校验,只返回字段路径和静态原因。 + // 校验库的 Display 会包含原值,敏感配置也参与校验,只返回字段路径和静态原因 let mut path = error.instance_path().to_string(); let reason = match error.kind() { ValidationErrorKind::AdditionalProperties { unexpected } => { diff --git a/backend/crates/gateway-plugin/runtime/src/generation/mod.rs b/backend/crates/gateway-plugin/runtime/src/generation/mod.rs index 55aab23bf..e2e2ab492 100644 --- a/backend/crates/gateway-plugin/runtime/src/generation/mod.rs +++ b/backend/crates/gateway-plugin/runtime/src/generation/mod.rs @@ -1,3 +1,5 @@ +//! 插件发布代次的配置校验、实例准备与重启熔断入口 + mod configuration; mod prepare; mod restart_circuit; diff --git a/backend/crates/gateway-plugin/runtime/src/generation/prepare/instance.rs b/backend/crates/gateway-plugin/runtime/src/generation/prepare/instance.rs index e7ad891af..6d6c12def 100644 --- a/backend/crates/gateway-plugin/runtime/src/generation/prepare/instance.rs +++ b/backend/crates/gateway-plugin/runtime/src/generation/prepare/instance.rs @@ -1,3 +1,5 @@ +//! 准备单个插件实例的进程、RPC 会话与能力贡献 + use super::{ AdminError, Arc, Duration, Handshake, PluginCallbackPorts, PluginCallbacks, PluginInstance, PluginPrivateState, PluginRuntime, PluginStateStoreErrorKind, PreparedContributions, @@ -65,7 +67,7 @@ impl PluginRuntime { package.manifest(), &instance.bindings, )?; - // 发行清单与当前 Runtime 共用同一份能力声明,不能让二者分别漂移。 + // 发行清单与当前 Runtime 共用同一份能力声明,不能让二者分别漂移 if !crate::package::host_supports(package.manifest())? { return Err(AdminError::invalid("该插件声明的业务能力尚未接入运行时")); } diff --git a/backend/crates/gateway-plugin/runtime/src/generation/prepare/maintenance.rs b/backend/crates/gateway-plugin/runtime/src/generation/prepare/maintenance.rs index 036146533..a38426961 100644 --- a/backend/crates/gateway-plugin/runtime/src/generation/prepare/maintenance.rs +++ b/backend/crates/gateway-plugin/runtime/src/generation/prepare/maintenance.rs @@ -1,4 +1,4 @@ -//! 对账只从已发布集合开始;通知合并,实例串行,失败和停用不阻塞其他实例。 +//! 对账只从已发布集合开始;通知合并,实例串行,失败和停用不阻塞其他实例 use super::{PluginRuntime, PreparedSet, RpcSession}; use futures::{StreamExt as _, future::BoxFuture}; @@ -24,7 +24,7 @@ struct MaintenanceTask { } impl PluginRuntime { - /// 由 Host 监督维护任务;CLI 和只读准备不会启动后台业务。 + /// 由 Host 监督维护任务;CLI 和只读准备不会启动后台业务 pub fn maintenance_worker( self: &Arc, snapshots: RuntimeSnapshotHandle, @@ -90,7 +90,7 @@ impl DaemonTask for MaintenanceTask { } } revision = snapshot.as_ref().map(|s| s.revision()); - // 发布变更优先于下一轮维护;不在快照发布锁中等待插件或数据库。 + // 发布变更优先于下一轮维护;不在快照发布锁中等待插件或数据库 tokio::select! { biased; () = cancellation.cancelled() => break, diff --git a/backend/crates/gateway-plugin/runtime/src/generation/prepare/mod.rs b/backend/crates/gateway-plugin/runtime/src/generation/prepare/mod.rs index d1c5f8d6a..7ef9226bd 100644 --- a/backend/crates/gateway-plugin/runtime/src/generation/prepare/mod.rs +++ b/backend/crates/gateway-plugin/runtime/src/generation/prepare/mod.rs @@ -1,3 +1,5 @@ +//! 组装插件运行时端口,准备并持有可发布的实例与能力集合 + mod instance; mod maintenance; @@ -80,7 +82,7 @@ pub struct PluginRuntimeConfig { pub restart_circuit: PluginRestartCircuitConfig, } -/// 只索引有主人的候选;当前代次仅由 Core 的发布视图持有。 +/// 只索引有主人的候选;当前代次仅由 Core 的发布视图持有 pub struct PluginRuntime { service_ports: Arc, pub(super) store: Arc, @@ -191,7 +193,7 @@ impl ExtensionSetLease for PreparedSet { } fn can_serve(&self) -> bool { - // 发布的是可用能力和故障绑定组成的完整计划;单个进程退出不能使原生转发失效。 + // 发布的是可用能力和故障绑定组成的完整计划;单个进程退出不能使原生转发失效 !self.shutting_down.load(Ordering::Acquire) } } @@ -259,7 +261,7 @@ impl PluginRuntime { self } - /// API 完成组装后绑定唯一内部 HTTP 分派端口;不持有服务端强引用。 + /// API 完成组装后绑定唯一内部 HTTP 分派端口;不持有服务端强引用 pub fn bind_http( &self, dispatcher: &Arc, @@ -281,7 +283,7 @@ impl PluginRuntime { self.account_ports.bind(access) } - /// Key 目录与预算端口由 Admin 组合并保活,完整字段由对应公开合同返回。 + /// Key 目录与预算端口由 Admin 组合并保活,完整字段由对应公开合同返回 pub fn bind_client_key_ports( &self, access: &Arc, @@ -289,7 +291,7 @@ impl PluginRuntime { self.client_key_ports.bind(access) } - /// Core 激活后一次性绑定嵌套执行与亲和查询端口;Runtime 仅保存 Weak,避免组合根强环。 + /// Core 激活后一次性绑定嵌套执行与亲和查询端口;Runtime 仅保存 Weak,避免组合根强环 pub fn bind_model_ports( &self, models: &Arc, @@ -299,7 +301,7 @@ impl PluginRuntime { self.affinity_ports.bind(affinity) } - /// 调用方必须先停止接收 HTTP 与 Worker;这里拒绝新候选并等待全部插件 I/O 退出。 + /// 调用方必须先停止接收 HTTP 与 Worker;这里拒绝新候选并等待全部插件 I/O 退出 pub async fn shutdown(&self) { self.shutting_down.store(true, Ordering::Release); let _shutdown = self.shutdown_lock.lock().await; @@ -406,7 +408,7 @@ impl PluginRuntime { return Err(AdminError::invalid("最多配置 64 个插件实例")); } let fingerprint = fingerprint(&snapshot)?; - // 准备串行化与弱索引分锁;诊断和已发布请求不会等待外部 I/O。 + // 准备串行化与弱索引分锁;诊断和已发布请求不会等待外部 I/O let _prepare = self.prepare_lock.lock().await; if self.shutting_down.load(Ordering::Acquire) { return Err(AdminError::unavailable("插件运行时正在关闭")); @@ -440,7 +442,7 @@ impl PluginRuntime { return Err(AdminError::invalid("插件实例 ID 重复")); } let result = tokio::time::timeout( - // 沿用准备阶段原有的 30 秒预算;打包校验与能力恢复也在预算内。 + // 沿用准备阶段原有的 30 秒预算;打包校验与能力恢复也在预算内 Duration::from_secs(30), self.prepare_instance(instance.clone(), snapshot.config_revision), ) @@ -450,8 +452,8 @@ impl PluginRuntime { Ok(candidate) => contributions.append(candidate), Err(error) if required_revision == Some(instance.revision) => return Err(error), Err(error) => { - // 目录没有拒绝请求用的 binding,不能在恢复失败时悄悄撤销仍启用的别名。 - // 不复制其他候选的注册结果;让发布事务保留调用方持有的旧有效快照。 + // 目录没有拒绝请求用的 binding,不能在恢复失败时悄悄撤销仍启用的别名 + // 不复制其他候选的注册结果;让发布事务保留调用方持有的旧有效快照 let has_retained_catalog = self .prepared .lock() @@ -468,7 +470,7 @@ impl PluginRuntime { "已启用的模型目录插件未就绪,请修复或停用后重试", )); } - // 恢复失败只隔离该实例;绑定的拒绝策略仍保留,不能绕过认证或必需处理。 + // 恢复失败只隔离该实例;绑定的拒绝策略仍保留,不能绕过认证或必需处理 contributions .policy_entries .extend(crate::adapter::policy::unavailable_entries(instance)?); @@ -574,7 +576,7 @@ impl PluginRuntime { Ok(ExtensionSetReference::new(set.id.clone(), set)) } - /// CLI 复用同一候选准备与弱索引;读取帮助不会发布 Core 快照或执行命令。 + /// CLI 复用同一候选准备与弱索引;读取帮助不会发布 Core 快照或执行命令 pub async fn prepare_command_line(&self) -> Result { let snapshot = self .store @@ -849,7 +851,7 @@ impl PluginPreparation for PluginRuntime { if instance.artifact_sha256 != metadata.sha256 { return Err(AdminError::invalid("插件配置与制品不匹配")); } - // 安装时已验证声明;只读列表不重新解包,也不占用安装与启用的校验配额。 + // 安装时已验证声明;只读列表不重新解包,也不占用安装与启用的校验配额 let schema = metadata.configuration_schema.clone(); let secret_fields = metadata.secret_fields.iter().cloned().collect(); tokio::task::spawn_blocking(move || { @@ -1246,7 +1248,7 @@ fn instance_fingerprint( .map(|(name, value)| (name, value.expose_secret())) .collect(); let mut value = serde_json::json!({"id":instance.id,"artifact":instance.artifact_sha256,"revision":instance.revision.get(),"trusted":instance.trusted_process,"configuration":instance.configuration,"secrets":secrets,"bindings":instance.bindings}); - // JSONB 恢复可以改变对象键顺序,不能因此替换已经准备好的同一配置。 + // JSONB 恢复可以改变对象键顺序,不能因此替换已经准备好的同一配置 value.sort_all_objects(); let bytes = serde_json::to_vec(&value).map_err(|_| AdminError::invalid("插件配置无法编码"))?; Ok(hex::encode(Sha256::digest(bytes))) diff --git a/backend/crates/gateway-plugin/runtime/src/generation/restart_circuit.rs b/backend/crates/gateway-plugin/runtime/src/generation/restart_circuit.rs index b7ef54728..48f09d320 100644 --- a/backend/crates/gateway-plugin/runtime/src/generation/restart_circuit.rs +++ b/backend/crates/gateway-plugin/runtime/src/generation/restart_circuit.rs @@ -1,3 +1,5 @@ +//! 按插件实例身份累计重启失败,管理熔断窗口与尝试生命周期 + use std::{ collections::BTreeMap, num::NonZeroUsize, @@ -9,7 +11,7 @@ use crate::rpc::RpcSessionLifecycle; const MAXIMUM_TRACKED_IDENTITIES: usize = 256; -/// 原生插件重启熔断的保守初始参数;数值可调,但不代表已完成性能定标。 +/// 原生插件重启熔断的保守初始参数;数值可调,但不代表已完成性能定标 #[derive(Debug, Clone, Copy)] pub struct PluginRestartCircuitConfig { pub maximum_failures: NonZeroUsize, @@ -192,11 +194,11 @@ impl RestartCircuits { latest_attempt: 0, touched, }); - // 监督事实只向前推进;旧代次迟到故障不能清掉或重复累加更新代次的预算。 + // 监督事实只向前推进;旧代次迟到故障不能清掉或重复累加更新代次的预算 if attempt <= entry.latest_attempt { return; } - // 稳定运行过的 incarnation 先清掉旧预算,本次退出作为新周期的第一次故障。 + // 稳定运行过的 incarnation 先清掉旧预算,本次退出作为新周期的第一次故障 if failure.uptime >= self.config.stability_window { entry.failures = 0; } diff --git a/backend/crates/gateway-plugin/runtime/src/lib.rs b/backend/crates/gateway-plugin/runtime/src/lib.rs index 35031a4f8..1433b93ba 100644 --- a/backend/crates/gateway-plugin/runtime/src/lib.rs +++ b/backend/crates/gateway-plugin/runtime/src/lib.rs @@ -1,4 +1,4 @@ -//! 插件包校验与线协议适配;安装选择和制品接受事实由 Admin/Store 拥有。 +//! 插件包校验与线协议适配;安装选择和制品接受事实由 Admin/Store 拥有 mod adapter; mod callback; diff --git a/backend/crates/gateway-plugin/runtime/src/package/cache.rs b/backend/crates/gateway-plugin/runtime/src/package/cache.rs index 5469071f1..3a0180cd1 100644 --- a/backend/crates/gateway-plugin/runtime/src/package/cache.rs +++ b/backend/crates/gateway-plugin/runtime/src/package/cache.rs @@ -1,3 +1,5 @@ +//! 将已验证插件包解压到独立私有目录,并持有可执行资源 + use std::{ fs, path::{Path, PathBuf}, @@ -6,7 +8,7 @@ use std::{ use super::{PackageError, ValidatedPackage}; -/// 每次准备创建独立私有目录;存储中的包体为权威,缓存不会被原地升级。 +/// 每次准备创建独立私有目录;存储中的包体为权威,缓存不会被原地升级 pub struct PreparedPackage { directory: tempfile::TempDir, executable: PathBuf, diff --git a/backend/crates/gateway-plugin/runtime/src/package/compatibility.rs b/backend/crates/gateway-plugin/runtime/src/package/compatibility.rs index 304dbeb45..dd9aa1c70 100644 --- a/backend/crates/gateway-plugin/runtime/src/package/compatibility.rs +++ b/backend/crates/gateway-plugin/runtime/src/package/compatibility.rs @@ -1,3 +1,5 @@ +//! 读取宿主插件兼容性声明,并校验清单所需协议与能力版本 + use std::sync::OnceLock; use gateway_admin::model::{ @@ -15,7 +17,7 @@ pub(crate) fn host_compatibility() -> Result<&'static PluginHostCompatibility, A let compatibility = serde_json::from_str::(HOST_COMPATIBILITY_JSON) .map_err(|_| ())?; - // 发行声明可以是 SDK 合同的子集,但不能声称支持当前二进制无法解释的合同。 + // 发行声明可以是 SDK 合同的子集,但不能声称支持当前二进制无法解释的合同 let known_contracts = compatibility .manifest_schema_versions .iter() diff --git a/backend/crates/gateway-plugin/runtime/src/package/icon.rs b/backend/crates/gateway-plugin/runtime/src/package/icon.rs index 2067f7d85..f281f0057 100644 --- a/backend/crates/gateway-plugin/runtime/src/package/icon.rs +++ b/backend/crates/gateway-plugin/runtime/src/package/icon.rs @@ -1,3 +1,5 @@ +//! 校验插件图标路径、格式、尺寸与动画帧资源限制 + use std::{collections::BTreeMap, io::Cursor}; use gateway_admin::model::plugins::PluginIconTheme; @@ -53,7 +55,7 @@ fn validate_path( if manifest.resources.get(path).map(String::as_str) == Some("image/svg+xml") { return validate_svg(bytes); } - // 扩展名与 MIME 的对应关系由 SDK 清单校验拥有,这里只核对实际编码与解码预算。 + // 扩展名与 MIME 的对应关系由 SDK 清单校验拥有,这里只核对实际编码与解码预算 let expected = ImageFormat::from_path(path).map_err(|_| PackageError::Archive)?; let mut reader = ImageReader::new(Cursor::new(bytes)) .with_guessed_format() @@ -65,7 +67,7 @@ fn validate_path( header.limits(icon_limits()); let (width, height) = header.into_dimensions().map_err(map_image_error)?; validate_dimensions(width, height)?; - // 动画逐帧检查但不收集帧,避免为了验证小图标保留整段动画的解码内存。 + // 动画逐帧检查但不收集帧,避免为了验证小图标保留整段动画的解码内存 match expected { ImageFormat::Png => { let decoder = PngDecoder::with_limits(Cursor::new(bytes), icon_limits()) @@ -97,7 +99,7 @@ fn validate_path( fn validate_svg(bytes: &[u8]) -> Result<(), PackageError> { let text = std::str::from_utf8(bytes).map_err(|_| PackageError::Archive)?; - // 不解析 DTD 或外部实体;保留正常 SVG 样式与矢量内容,由图像上下文和接口 CSP 隔离主动内容。 + // 不解析 DTD 或外部实体;保留正常 SVG 样式与矢量内容,由图像上下文和接口 CSP 隔离主动内容 let document = roxmltree::Document::parse_with_options( text, roxmltree::ParsingOptions { diff --git a/backend/crates/gateway-plugin/runtime/src/package/inspection.rs b/backend/crates/gateway-plugin/runtime/src/package/inspection.rs index 729f0eb11..e42d92194 100644 --- a/backend/crates/gateway-plugin/runtime/src/package/inspection.rs +++ b/backend/crates/gateway-plugin/runtime/src/package/inspection.rs @@ -1,3 +1,5 @@ +//! 将插件包校验适配为制品元数据、图标与兼容性查询端口 + use std::sync::Arc; use std::time::Duration; @@ -46,7 +48,7 @@ impl PluginPackageInspector for PackageInspector { let limits = self.limits; let host_version = self.host_version.clone(); tokio::task::spawn_blocking(move || { - // blocking 任务取消后仍可能运行,容量引用由任务持有直到真正结束。 + // blocking 任务取消后仍可能运行,容量引用由任务持有直到真正结束 let _permit = permit; let package = ValidatedPackage::read(archive, expected_sha256.as_deref(), limits) .map_err(inspection_error)?; diff --git a/backend/crates/gateway-plugin/runtime/src/package/mod.rs b/backend/crates/gateway-plugin/runtime/src/package/mod.rs index b7895c50b..ce78cc298 100644 --- a/backend/crates/gateway-plugin/runtime/src/package/mod.rs +++ b/backend/crates/gateway-plugin/runtime/src/package/mod.rs @@ -1,3 +1,5 @@ +//! 插件包校验、检查、兼容性与解压资源的公共入口 + mod cache; mod compatibility; mod icon; diff --git a/backend/crates/gateway-plugin/runtime/src/package/validation.rs b/backend/crates/gateway-plugin/runtime/src/package/validation.rs index 3838a669f..4aaad8402 100644 --- a/backend/crates/gateway-plugin/runtime/src/package/validation.rs +++ b/backend/crates/gateway-plugin/runtime/src/package/validation.rs @@ -1,3 +1,5 @@ +//! 有界解码插件归档,校验清单、资源路径与内容摘要 + use std::{collections::BTreeMap, io::Read, sync::Arc}; use flate2::read::MultiGzDecoder; @@ -39,7 +41,7 @@ pub enum PackageError { Cache, } -/// 校验后的不可变字节;无需先把未可信压缩包解压到宿主文件系统。 +/// 校验后的不可变字节;无需先把未可信压缩包解压到宿主文件系统 #[derive(Clone)] pub struct ValidatedPackage { pub(super) manifest: Manifest, @@ -49,7 +51,7 @@ pub struct ValidatedPackage { } impl ValidatedPackage { - /// 仅供已冻结资源声明读取校验后的字节,不暴露缓存路径或任意文件读取。 + /// 仅供已冻结资源声明读取校验后的字节,不暴露缓存路径或任意文件读取 pub(crate) fn resource(&self, path: &str) -> Option<&[u8]> { self.manifest .resources @@ -71,7 +73,7 @@ impl ValidatedPackage { return Err(PackageError::Digest); } - // 先限制整个解压字节流,连 PAX 元数据、目录、padding 和尾部数据也纳入预算。 + // 先限制整个解压字节流,连 PAX 元数据、目录、padding 和尾部数据也纳入预算 let expanded_limit = u64::try_from(limits.expanded_bytes).map_err(|_| PackageError::Limit)?; let mut expanded = Vec::new(); @@ -120,7 +122,7 @@ impl ValidatedPackage { let manifest: Manifest = serde_json::from_slice(&manifest_bytes).map_err(|_| PackageError::Archive)?; manifest.validate()?; - // 源清单可以省略 package,但归档安装入口必须只接受构建后的单平台包。 + // 源清单可以省略 package,但归档安装入口必须只接受构建后的单平台包 let package = manifest.package.as_ref().ok_or(ManifestError::Invalid)?; if files.len() != package.files.len() || files.len() > limits.file_count { return Err(PackageError::Archive); diff --git a/backend/crates/gateway-plugin/runtime/src/rpc/dispatch.rs b/backend/crates/gateway-plugin/runtime/src/rpc/dispatch.rs index 1b3975ca1..9a6388bf8 100644 --- a/backend/crates/gateway-plugin/runtime/src/rpc/dispatch.rs +++ b/backend/crates/gateway-plugin/runtime/src/rpc/dispatch.rs @@ -1,3 +1,5 @@ +//! 插件 RPC 帧的异步读写循环与入站消息分派 + use std::sync::Arc; use gateway_plugin_sdk::{ @@ -47,7 +49,7 @@ async fn write_loop( shared.fail(RpcError::Closed); return; }; - // 撤销尚未发送的调用时丢弃排队帧;已开始写入的 Call 必须先于 Cancel。 + // 撤销尚未发送的调用时丢弃排队帧;已开始写入的 Call 必须先于 Cancel if let Message::Call { id, .. } = &frame.message { if !shared.mark_transmitted(*id) { continue; @@ -120,7 +122,7 @@ async fn read_loop( let response = Arc::clone(&shared); let (ready, started) = oneshot::channel(); let task = tokio::spawn(async move { - // 先把任务归属登记到父调用,再允许执行宿主操作。 + // 先把任务归属登记到父调用,再允许执行宿主操作 if started.await.is_err() { return; } @@ -135,7 +137,7 @@ async fn read_loop( }, Err(error) => Frame::control(Message::Error { id, error }), }; - // 宿主回调的局部编码错误只能结束该调用,不能关闭共享写通道。 + // 宿主回调的局部编码错误只能结束该调用,不能关闭共享写通道 if validate_frame(&reply).is_err() { reply = Frame::control(Message::Error { id, diff --git a/backend/crates/gateway-plugin/runtime/src/rpc/mod.rs b/backend/crates/gateway-plugin/runtime/src/rpc/mod.rs index 53da11e68..11f48e43d 100644 --- a/backend/crates/gateway-plugin/runtime/src/rpc/mod.rs +++ b/backend/crates/gateway-plugin/runtime/src/rpc/mod.rs @@ -1,3 +1,5 @@ +//! 插件双向 RPC 会话、调用结果与生命周期的公共入口 + mod dispatch; mod session; diff --git a/backend/crates/gateway-plugin/runtime/src/rpc/session.rs b/backend/crates/gateway-plugin/runtime/src/rpc/session.rs index 44973fed6..1a10c2899 100644 --- a/backend/crates/gateway-plugin/runtime/src/rpc/session.rs +++ b/backend/crates/gateway-plugin/runtime/src/rpc/session.rs @@ -1,3 +1,5 @@ +//! 插件 RPC 会话的调用登记、超时取消、故障传播与关闭管理 + use std::{ collections::BTreeMap, sync::{ @@ -21,7 +23,7 @@ use super::dispatch; #[derive(Debug, Clone, Copy)] pub struct RpcLimits { - /// 受管回调与观察队列的缓冲预算,不是请求正文或 IPC 消息总量上限。 + /// 受管回调与观察队列的缓冲预算,不是请求正文或 IPC 消息总量上限 pub maximum_buffered_body_bytes: usize, pub maximum_calls: usize, pub maximum_callbacks: usize, @@ -104,21 +106,21 @@ impl Drop for RpcSessionLifecycle { } } -/// 不派生 Debug,防止原始请求、凭据或模型输出进入宿主诊断。 +/// 不派生 Debug,防止原始请求、凭据或模型输出进入宿主诊断 pub struct RpcReply { pub result: serde_json::Value, pub payload: Vec, } -/// 具体回调在领域适配器校验账号、出站与执行身份;上下文只取自宿主在途调用表。 +/// 具体回调在领域适配器校验账号、出站与执行身份;上下文只取自宿主在途调用表 pub trait CallbackHandler: Send + Sync { - /// 在 Call 可见之前建立资源范围;不得执行 I/O 或调用 RPC。 + /// 在 Call 可见之前建立资源范围;不得执行 I/O 或调用 RPC fn begin(&self, _context: &CallContext) {} - /// 调用结束或取消时立即撤销其句柄;实现必须幂等且不能阻塞。 + /// 调用结束或取消时立即撤销其句柄;实现必须幂等且不能阻塞 fn finish(&self, _context: &CallContext) {} - /// 首个流式结果已到达;资源仍绑定原调用并在终态统一回收。 + /// 首个流式结果已到达;资源仍绑定原调用并在终态统一回收 fn streaming(&self, _context: &CallContext) {} fn call( @@ -284,7 +286,7 @@ impl Shared { pub fn release_stream_credit(&self, id: u64, bytes: u32) -> Result<(), RpcError> { let mut state = self.state(); - // 完成后仍可消费已排队数据,无须再给已关闭的流发信用。 + // 完成后仍可消费已排队数据,无须再给已关闭的流发信用 let Some(call) = state.calls.get_mut(&id) else { return Ok(()); }; @@ -328,7 +330,7 @@ impl Shared { true } - /// 先撤销调用权限,再等待对端确认;只有无响应对端才终止整个 incarnation。 + /// 先撤销调用权限,再等待对端确认;只有无响应对端才终止整个 incarnation pub fn cancel(self: &Arc, id: u64, error: RpcError) { let mut state = self.state(); let Some(call) = state.calls.get_mut(&id) else { @@ -385,7 +387,7 @@ impl Shared { .get_mut(&parent_id) .filter(|call| call.cancellation.is_none()) { - // 已完成回调的句柄不继续占用请求预算。 + // 已完成回调的句柄不继续占用请求预算 call.callbacks.retain(|handle| !handle.is_finished()); call.callbacks.push(callback); true @@ -504,8 +506,8 @@ impl RpcSession { .. }, ) => { - // Unix 并发 fork 可能短暂继承解包时的写句柄(rust-lang/rust#114554)。 - // 仅重试尚未执行的文件忙错误,并与握手共享启动期限。 + // Unix 并发 fork 可能短暂继承解包时的写句柄(rust-lang/rust#114554) + // 仅重试尚未执行的文件忙错误,并与握手共享启动期限 let retry_at = tokio::time::Instant::now() + Duration::from_millis(10); if retry_at >= deadline { return Err(RpcError::Start(error)); @@ -550,7 +552,7 @@ impl RpcSession { .and_then(|result| result); if let Err(error) = ready { if let Some(lifecycle) = &lifecycle { - // 只有 Ready 之后的存活才可复位预算;长时间卡在握手不等于稳定运行。 + // 只有 Ready 之后的存活才可复位预算;长时间卡在握手不等于稳定运行 lifecycle.fail(Duration::ZERO); } process.stop(); @@ -593,7 +595,7 @@ impl RpcSession { tracing::warn!(instance_id, exit = ?reason, "插件进程意外停止"); } monitor.fail(RpcError::Closed); - // 等进程退出后再回收其文件,避免 Windows 上删除正在执行的制品。 + // 等进程退出后再回收其文件,避免 Windows 上删除正在执行的制品 drop(package); }); Ok(Self { @@ -618,7 +620,7 @@ impl RpcSession { .is_none() } - /// 仅返回稳定分类与固定文案,绝不展开插件 fault、stderr 或请求内容。 + /// 仅返回稳定分类与固定文案,绝不展开插件 fault、stderr 或请求内容 pub(crate) fn diagnostic(&self) -> RpcSessionDiagnostic { let state = self .shared @@ -648,7 +650,7 @@ impl RpcSession { #[must_use] pub fn context(&self, stage: gateway_plugin_sdk::Stage, timeout: Duration) -> CallContext { - // 宿主生成的上下文受本会话预算约束;固定注册期限不能使较小的总预算无法启动。 + // 宿主生成的上下文受本会话预算约束;固定注册期限不能使较小的总预算无法启动 let timeout = timeout.min(self.limits.maximum_call_timeout); CallContext { call_id: 0, @@ -730,7 +732,7 @@ impl RpcSession { ) -> Result { let deadline = self.call_deadline(method, &context)?; // FIFO 锁同时拥有 ID 与 data 入队顺序;Call/初始 Credit 入队后必须立即释放, - // 不能把插件回复或重入回调纳入串行区,也不能为等待锁重置调用期限。 + // 不能把插件回复或重入回调纳入串行区,也不能为等待锁重置调用期限 let mut next_call = tokio::time::timeout_at(deadline, self.next_call.lock()) .await .map_err(|_| RpcError::Timeout)?; @@ -747,7 +749,7 @@ impl RpcSession { }, payload, }; - // 本地元数据错误不能进入写队列并关闭整个会话。 + // 本地元数据错误不能进入写队列并关闭整个会话 validate_frame(&frame).map_err(|_| RpcError::Context)?; let started = self.begin_call(context, deadline, stream)?; let mut guard = CallGuard { @@ -758,7 +760,7 @@ impl RpcSession { let sent = tokio::time::timeout_at(started.deadline, async { self.data.send(frame).await.map_err(|_| RpcError::Closed)?; if let Some((bytes, frames)) = credit { - // 初始信用跟在 Call 后走同一有序队列,不能被控制队列提前发送。 + // 初始信用跟在 Call 后走同一有序队列,不能被控制队列提前发送 self.data .send(Frame::control(Message::Credit { id: started.id, @@ -877,14 +879,14 @@ impl RpcSession { .is_err() { self.shared.fail(RpcError::Closed); - // 强制停止没有第二段宽限;退出和回调析构是返回前必须确认的事实。 + // 强制停止没有第二段宽限;退出和回调析构是返回前必须确认的事实 self.settle_shutdown().await; } } async fn settle_shutdown(&self) { self.shared.process.exited().await; - // 不等待监控任务调度;先撤销父调用并中止其回调,再确认回调 future 已析构。 + // 不等待监控任务调度;先撤销父调用并中止其回调,再确认回调 future 已析构 self.shared.fail(RpcError::Closed); self.shared .wait_for_callbacks(self.limits.maximum_callbacks) diff --git a/backend/crates/gateway-plugin/runtime/src/stream/bridge.rs b/backend/crates/gateway-plugin/runtime/src/stream/bridge.rs index 1415178d5..b942b7bb7 100644 --- a/backend/crates/gateway-plugin/runtime/src/stream/bridge.rs +++ b/backend/crates/gateway-plugin/runtime/src/stream/bridge.rs @@ -1,3 +1,5 @@ +//! 将插件 RPC 流消息桥接为有界接收流,并归还消费信用 + use std::sync::Arc; use tokio::{ @@ -53,7 +55,7 @@ impl StreamIngress { } } -/// 消费者读取后才补充窗口;终态走独立通道,不会排在满数据队列后等待。 +/// 消费者读取后才补充窗口;终态走独立通道,不会排在满数据队列后等待 pub struct RpcStream { pub initial: RpcReply, pub(crate) chunks: mpsc::Receiver>, diff --git a/backend/crates/gateway-plugin/runtime/src/stream/flow_control.rs b/backend/crates/gateway-plugin/runtime/src/stream/flow_control.rs index 724139367..79d274d46 100644 --- a/backend/crates/gateway-plugin/runtime/src/stream/flow_control.rs +++ b/backend/crates/gateway-plugin/runtime/src/stream/flow_control.rs @@ -1,6 +1,8 @@ +//! 插件流的字节与帧数窗口校验、序号推进及信用归还 + use crate::RpcError; -/// 字节与帧数一起授予,避免攻击者用大量单字节分块突破队列上限。 +/// 字节与帧数一起授予,避免攻击者用大量单字节分块突破队列上限 pub(super) struct ReceiveWindow { bytes: u32, frames: u32, diff --git a/backend/crates/gateway-plugin/runtime/src/stream/mod.rs b/backend/crates/gateway-plugin/runtime/src/stream/mod.rs index 71c0bd00d..cdc083b17 100644 --- a/backend/crates/gateway-plugin/runtime/src/stream/mod.rs +++ b/backend/crates/gateway-plugin/runtime/src/stream/mod.rs @@ -1,3 +1,5 @@ +//! 插件 RPC 流桥接与接收流量控制的公共入口 + mod bridge; mod flow_control; diff --git a/backend/crates/gateway-plugin/runtime/tests/adapter/command_line/execution.rs b/backend/crates/gateway-plugin/runtime/tests/adapter/command_line/execution.rs index 37b54d7cb..7bfbc479e 100644 --- a/backend/crates/gateway-plugin/runtime/tests/adapter/command_line/execution.rs +++ b/backend/crates/gateway-plugin/runtime/tests/adapter/command_line/execution.rs @@ -1,3 +1,5 @@ +//! 验证插件命令复用宿主身份、计量、账号事务与审计边界 + use gateway_core::policy::{ClientApiKeyId, RateLimits}; use gateway_plugin_runtime::PluginCommandError; use serde_json::json; diff --git a/backend/crates/gateway-plugin/runtime/tests/adapter/command_line/mod.rs b/backend/crates/gateway-plugin/runtime/tests/adapter/command_line/mod.rs index 3eb326824..c16434bd0 100644 --- a/backend/crates/gateway-plugin/runtime/tests/adapter/command_line/mod.rs +++ b/backend/crates/gateway-plugin/runtime/tests/adapter/command_line/mod.rs @@ -1,3 +1,5 @@ +//! 验证插件命令帮助、调用授权、输出与失败取消后的进程回收 + mod execution; mod parameters; @@ -167,7 +169,7 @@ async fn failed_or_cancelled_commands_are_not_replayed_and_children_are_reaped() if failure == "cancel" { cancellation.cancel(); } else { - // 正常完成注册并确认命令已进入子进程后,只推进执行期限。 + // 正常完成注册并确认命令已进入子进程后,只推进执行期限 tokio::time::pause(); tokio::time::advance(limits.maximum_call_timeout + Duration::from_millis(1)) .await; diff --git a/backend/crates/gateway-plugin/runtime/tests/adapter/command_line/parameters.rs b/backend/crates/gateway-plugin/runtime/tests/adapter/command_line/parameters.rs index d870fe026..75bed8a6e 100644 --- a/backend/crates/gateway-plugin/runtime/tests/adapter/command_line/parameters.rs +++ b/backend/crates/gateway-plugin/runtime/tests/adapter/command_line/parameters.rs @@ -1,3 +1,5 @@ +//! 验证插件命令参数的类型、默认值、数值边界与非法声明拒绝 + use serde_json::{Value, json}; #[tokio::test] diff --git a/backend/crates/gateway-plugin/runtime/tests/adapter/management/callback.rs b/backend/crates/gateway-plugin/runtime/tests/adapter/management/callback.rs index 2e24309f2..77b546a3c 100644 --- a/backend/crates/gateway-plugin/runtime/tests/adapter/management/callback.rs +++ b/backend/crates/gateway-plugin/runtime/tests/adapter/management/callback.rs @@ -1,3 +1,5 @@ +//! 验证插件授权回调状态的一次性消费、身份绑定与过期拒绝 + use gateway_admin::{ PluginManagementService, model::{ diff --git a/backend/crates/gateway-plugin/runtime/tests/adapter/management/mod.rs b/backend/crates/gateway-plugin/runtime/tests/adapter/management/mod.rs index 84d51ee8e..37f0e1079 100644 --- a/backend/crates/gateway-plugin/runtime/tests/adapter/management/mod.rs +++ b/backend/crates/gateway-plugin/runtime/tests/adapter/management/mod.rs @@ -1,3 +1,5 @@ +//! 验证插件管理调用的身份持有、版本绑定与权限撤销 + mod callback; mod registration; @@ -382,7 +384,7 @@ async fn management_resources_and_raw_calls_are_version_bound_and_revocation_rem .save_instance(instance, snapshot.config_revision, &mutation()) .await .unwrap(); - // 模拟持久撤销已提交但旧发布视图仍存在;页面与资源都不能继续使用旧权限。 + // 模拟持久撤销已提交但旧发布视图仍存在;页面与资源都不能继续使用旧权限 assert!(service.views().await.unwrap().is_empty()); assert!(service.handle(target, request()).await.is_err()); assert!( diff --git a/backend/crates/gateway-plugin/runtime/tests/adapter/management/registration.rs b/backend/crates/gateway-plugin/runtime/tests/adapter/management/registration.rs index c19a6166f..160b7894e 100644 --- a/backend/crates/gateway-plugin/runtime/tests/adapter/management/registration.rs +++ b/backend/crates/gateway-plugin/runtime/tests/adapter/management/registration.rs @@ -1,3 +1,5 @@ +//! 验证插件管理注册拒绝未声明资源、非法路由与缺失授权 + use gateway_admin::ports::plugins::PluginPreparation; use serde_json::json; diff --git a/backend/crates/gateway-plugin/runtime/tests/adapter/mod.rs b/backend/crates/gateway-plugin/runtime/tests/adapter/mod.rs index fb7a5644a..9de1e5a76 100644 --- a/backend/crates/gateway-plugin/runtime/tests/adapter/mod.rs +++ b/backend/crates/gateway-plugin/runtime/tests/adapter/mod.rs @@ -1,2 +1,4 @@ +//! 插件命令行与管理能力适配的测试入口 + mod command_line; mod management; diff --git a/backend/crates/gateway-plugin/runtime/tests/callback/key_facts.rs b/backend/crates/gateway-plugin/runtime/tests/callback/key_facts.rs index 19d750bb3..6f6864e0f 100644 --- a/backend/crates/gateway-plugin/runtime/tests/callback/key_facts.rs +++ b/backend/crates/gateway-plugin/runtime/tests/callback/key_facts.rs @@ -1,3 +1,5 @@ +//! 验证插件 Key 事实查询读取当前分组且不暴露密钥 + use gateway_admin::{ model::{ client_keys::{SetClientKeyEnabled, UpdateClientKey}, diff --git a/backend/crates/gateway-plugin/runtime/tests/callback/keys.rs b/backend/crates/gateway-plugin/runtime/tests/callback/keys.rs index 9a5bbf25e..6968767b4 100644 --- a/backend/crates/gateway-plugin/runtime/tests/callback/keys.rs +++ b/backend/crates/gateway-plugin/runtime/tests/callback/keys.rs @@ -1,3 +1,5 @@ +//! 验证插件 Key 预算回调复用宿主服务、权限与提交审计 + use std::{sync::Arc, time::Duration}; use gateway_admin::{ @@ -165,7 +167,7 @@ async fn seed_budget(environment: &Environment) -> ClientApiKeyId { } fn reset_queries() -> Value { - // 重置无需先查询目录;两个基础接口由测试插件自行选择调用顺序。 + // 重置无需先查询目录;两个基础接口由测试插件自行选择调用顺序 json!([ {"method":"host.keys.reset_budget","query":{"client_key_id":"key_budget","period":"weekly"}}, {"method":"host.keys.list","query":{"limit":10}}, @@ -344,7 +346,7 @@ impl PluginClientKeyAccess for HoldCommittedReply { context: &MutationContext, ) -> Result { self.inner.reset_budget(owner, command, context).await?; - // 在真实事务提交后挡住宿主回包,稳定复现插件未收到提交结果的窗口。 + // 在真实事务提交后挡住宿主回包,稳定复现插件未收到提交结果的窗口 self.committed.notify_one(); std::future::pending().await } @@ -415,7 +417,7 @@ async fn losing_callback_reply_after_commit_keeps_budget_and_audit_without_repla .await .expect("native budget transaction committed"); assert!(!request.is_finished()); - // 只断开插件,宿主保持运行,确保连接丢失不会触发透明重放。 + // 只断开插件,宿主保持运行,确保连接丢失不会触发透明重放 std::fs::write(&disconnect, b"").unwrap(); assert!( timeout(Duration::from_secs(5), request) diff --git a/backend/crates/gateway-plugin/runtime/tests/callback/log.rs b/backend/crates/gateway-plugin/runtime/tests/callback/log.rs index 277b74320..b473f6e94 100644 --- a/backend/crates/gateway-plugin/runtime/tests/callback/log.rs +++ b/backend/crates/gateway-plugin/runtime/tests/callback/log.rs @@ -1,3 +1,5 @@ +//! 验证真实插件日志的调用关联、输入校验、脱敏与数量限制 + use std::{ collections::BTreeMap, sync::{Arc, Condvar, Mutex}, @@ -27,7 +29,7 @@ struct Gate { impl Gate { fn wait(&self) { - // 测试回归为同步阻塞时也能自行退出,不能把测试运行器永久挂住。 + // 测试回归为同步阻塞时也能自行退出,不能把测试运行器永久挂住 let (mut released, _) = self .changed .wait_timeout_while( @@ -271,7 +273,7 @@ async fn real_worker_logs_are_correlated_sanitized_validated_and_bounded() { "log_method":"plugin.register", "log_entries":[{"params":{"event":"fixture.blocked"},"repeat":32}], "log_marker":blocked_marker, }); - // 日志后端停住时,注册的业务结果和父调用结束仍须及时完成。 + // 日志后端停住时,注册的业务结果和父调用结束仍须及时完成 let blocked_generation = tokio::time::timeout( Duration::from_secs(10), PluginPreparation::prepare(&runtime, blocked), diff --git a/backend/crates/gateway-plugin/runtime/tests/callback/mod.rs b/backend/crates/gateway-plugin/runtime/tests/callback/mod.rs index e765df1e1..52484946b 100644 --- a/backend/crates/gateway-plugin/runtime/tests/callback/mod.rs +++ b/backend/crates/gateway-plugin/runtime/tests/callback/mod.rs @@ -1,3 +1,5 @@ +//! 插件宿主回调的测试入口 + mod key_facts; mod keys; mod log; diff --git a/backend/crates/gateway-plugin/runtime/tests/generation/authentication.rs b/backend/crates/gateway-plugin/runtime/tests/generation/authentication.rs index f170223a2..20fa10139 100644 --- a/backend/crates/gateway-plugin/runtime/tests/generation/authentication.rs +++ b/backend/crates/gateway-plugin/runtime/tests/generation/authentication.rs @@ -1,3 +1,5 @@ +//! 验证插件认证的身份映射、回退条件与发布后的权限撤销 + use gateway_admin::model::client_keys::SetClientKeyEnabled; use gateway_core::{ engine::{authentication::ClientAuthenticationRequest, execution::ClientAuthenticationError}, diff --git a/backend/crates/gateway-plugin/runtime/tests/generation/configuration.rs b/backend/crates/gateway-plugin/runtime/tests/generation/configuration.rs index ee8d34e95..0e8063bd1 100644 --- a/backend/crates/gateway-plugin/runtime/tests/generation/configuration.rs +++ b/backend/crates/gateway-plugin/runtime/tests/generation/configuration.rs @@ -1,3 +1,5 @@ +//! 验证插件信任、配置 schema、敏感字段与贡献绑定约束 + use gateway_admin::{ model::plugins::instances::{PluginCapabilityBinding, PluginFailurePolicy}, ports::plugins::PluginPreparation, @@ -9,7 +11,7 @@ async fn readiness_uses_validated_metadata_without_loading_the_archive() { let (_cache, store, runtime) = super::setup().await; let mut instance = store.snapshot.lock().unwrap().instances[0].clone(); let mut metadata = store.artifacts[&instance.artifact_sha256].metadata.clone(); - // Store 中没有这个摘要;只读校验只能消费传入的声明,不能退回整包加载。 + // Store 中没有这个摘要;只读校验只能消费传入的声明,不能退回整包加载 metadata.sha256 = "0".repeat(64); instance.artifact_sha256 = metadata.sha256.clone(); metadata.configuration_schema = serde_json::json!({ diff --git a/backend/crates/gateway-plugin/runtime/tests/generation/data.rs b/backend/crates/gateway-plugin/runtime/tests/generation/data.rs index 81eec737c..a3dfa1baa 100644 --- a/backend/crates/gateway-plugin/runtime/tests/generation/data.rs +++ b/backend/crates/gateway-plugin/runtime/tests/generation/data.rs @@ -1,3 +1,5 @@ +//! 验证插件事实查询与额度刷新回调的参数和能力边界 + use std::sync::{ Arc, atomic::{AtomicUsize, Ordering}, diff --git a/backend/crates/gateway-plugin/runtime/tests/generation/maintenance.rs b/backend/crates/gateway-plugin/runtime/tests/generation/maintenance.rs index 07655d291..ceb2ceef2 100644 --- a/backend/crates/gateway-plugin/runtime/tests/generation/maintenance.rs +++ b/backend/crates/gateway-plugin/runtime/tests/generation/maintenance.rs @@ -1,3 +1,5 @@ +//! 验证插件维护任务的资源创建、重试及事务归属和幂等性 + use crate::support::environment::{Environment, mutation}; use gateway_admin::{ model::{ @@ -237,7 +239,7 @@ async fn published_reconciliation_provisions_retries_imports_and_restores_withou drop(resources); drop(core); drop(runtime); - // 离线新增账号后重新启动真实插件子进程;稳定资源键必须复用原分组与 Key。 + // 离线新增账号后重新启动真实插件子进程;稳定资源键必须复用原分组与 Key let offline = environment.account(None).await; std::fs::write(&marker, "").unwrap(); let (runtime, core, resources, stop, task) = start(&environment).await; diff --git a/backend/crates/gateway-plugin/runtime/tests/generation/mod.rs b/backend/crates/gateway-plugin/runtime/tests/generation/mod.rs index 7192eee81..a1bfbddcd 100644 --- a/backend/crates/gateway-plugin/runtime/tests/generation/mod.rs +++ b/backend/crates/gateway-plugin/runtime/tests/generation/mod.rs @@ -1,3 +1,5 @@ +//! 插件发布代次、能力执行与生命周期的集成测试入口 + mod authentication; mod configuration; mod data; diff --git a/backend/crates/gateway-plugin/runtime/tests/generation/observer.rs b/backend/crates/gateway-plugin/runtime/tests/generation/observer.rs index 39c5d36ce..98e2dcd2d 100644 --- a/backend/crates/gateway-plugin/runtime/tests/generation/observer.rs +++ b/backend/crates/gateway-plugin/runtime/tests/generation/observer.rs @@ -1,3 +1,5 @@ +//! 验证插件观察计划的范围、顺序、去重与有界异步通知 + use std::{collections::BTreeMap, num::NonZeroU32, sync::Arc, time::Duration}; use gateway_admin::{ @@ -211,7 +213,7 @@ async fn wait_for_lines(path: &std::path::Path, count: usize) -> Vec Option { - // 插件专用服务优先;CI 的标准测试服务同样通过随机 schema 隔离数据库。 + // 插件专用服务优先;CI 的标准测试服务同样通过随机 schema 隔离数据库 let (database, redis) = if let Ok(database) = std::env::var("CPR_PLUGIN_TEST_DATABASE_URL") { ( @@ -695,7 +697,7 @@ impl Environment { } else { gateway_store::initialize(config).await.unwrap() }; - // 迁移可能已创建默认行;该场景只测租约释放,明确关闭相邻请求的间隔限制。 + // 迁移可能已创建默认行;该场景只测租约释放,明确关闭相邻请求的间隔限制 sqlx::raw_sql(sqlx::AssertSqlSafe(format!("insert into {schema}.runtime_settings(id, config_revision, request_interval_ms, updated_at) values (1,1,0,now()) on conflict (id) do update set request_interval_ms=0"))) .execute(&admin).await.unwrap(); Some(Self { diff --git a/backend/crates/gateway-plugin/runtime/tests/support/middleware/http_resources.rs b/backend/crates/gateway-plugin/runtime/tests/support/middleware/http_resources.rs index 0c5933cd8..35a443df3 100644 --- a/backend/crates/gateway-plugin/runtime/tests/support/middleware/http_resources.rs +++ b/backend/crates/gateway-plugin/runtime/tests/support/middleware/http_resources.rs @@ -1,3 +1,5 @@ +//! 测试插件使用的 HTTP 资源回调与上传生命周期检查 + use gateway_plugin_sdk::{ ErrorCode, PluginFault, call::middleware::http as wire, @@ -24,7 +26,7 @@ pub async fn check_upload_lifecycle(host: &HostClient) -> Result<(), PluginFault } assert_eq!(create(host).await.unwrap_err().code, ErrorCode::Capacity); - // 关闭立即归还名额;重复关闭不能误伤后续分配。 + // 关闭立即归还名额;重复关闭不能误伤后续分配 let closed = handles.remove(0); for _ in 0..2 { call(host, wire::BODY_CLOSE_METHOD, json!({"handle":closed})).await?; @@ -32,7 +34,7 @@ pub async fn check_upload_lifecycle(host: &HostClient) -> Result<(), PluginFault handles.push(create(host).await?); assert_eq!(create(host).await.unwrap_err().code, ErrorCode::Capacity); - // 写完后读端仍能取得 EOF,但不再占用活动上传名额。 + // 写完后读端仍能取得 EOF,但不再占用活动上传名额 let finished = handles.remove(0); call( host, @@ -49,7 +51,7 @@ pub async fn check_upload_lifecycle(host: &HostClient) -> Result<(), PluginFault .await?; assert_eq!(eof.result["eof"], true); - // 正文已转交给子请求,关闭返回正文必须唤醒被单帧背压阻塞的上传。 + // 正文已转交给子请求,关闭返回正文必须唤醒被单帧背压阻塞的上传 let active = handles.remove(0); let reply = call( host, diff --git a/backend/crates/gateway-plugin/runtime/tests/support/middleware/mod.rs b/backend/crates/gateway-plugin/runtime/tests/support/middleware/mod.rs index 65857bc4e..18bf5138c 100644 --- a/backend/crates/gateway-plugin/runtime/tests/support/middleware/mod.rs +++ b/backend/crates/gateway-plugin/runtime/tests/support/middleware/mod.rs @@ -1,3 +1,5 @@ +//! 中间件测试插件子进程入口及异常退出清理辅助 + mod http_resources; use gateway_plugin_sdk::{ @@ -182,7 +184,7 @@ async fn main() -> Result<(), Box> { } if call.is::() { let call = call.into_typed::()?; - // 同一插件再次读取服务不会递归调用自身;回调仍经过宿主服务端口。 + // 同一插件再次读取服务不会递归调用自身;回调仍经过宿主服务端口 let nested = call.host.service::(()).await.unwrap(); let mut output = call.next.run(()).await.unwrap(); assert_eq!(output.config_revision, nested.config_revision); @@ -358,7 +360,7 @@ async fn main() -> Result<(), Box> { if index == 0 { sender.send(WebSocketMessage::new(WebSocketKind::Text, b"ready".to_vec())).await?; } - // 模拟模型任务先完成导致 select 丢弃 receive;下一次等待必须继续取同一条消息。 + // 模拟模型任务先完成导致 select 丢弃 receive;下一次等待必须继续取同一条消息 } if let Some(message) = session.receive().await? { sender.send(message).await?; } for index in 0..32 { @@ -492,7 +494,7 @@ async fn main() -> Result<(), Box> { } } if mode == "inspect" && !call.request.body.is_empty() { - // 读取和解析只能作用于副本,不能把解析结果写回未改写的正文。 + // 读取和解析只能作用于副本,不能把解析结果写回未改写的正文 assert!(serde_json::from_slice::(&call.request.body).is_ok()); } if mode == "rejected" { @@ -538,7 +540,7 @@ async fn main() -> Result<(), Box> { format!("{}\n", serde_json::to_string(&response.metadata).unwrap()), ) .unwrap(); - // 修改快照不能替换原执行的账号或费用。 + // 修改快照不能替换原执行的账号或费用 response.metadata.as_mut().unwrap().provider_account_id = "changed-snapshot".into(); response.body = response.body.with_facts()?.map_frames(move |mut frame| { diff --git a/backend/crates/gateway-plugin/runtime/tests/support/mod.rs b/backend/crates/gateway-plugin/runtime/tests/support/mod.rs index 5b118877d..13766d983 100644 --- a/backend/crates/gateway-plugin/runtime/tests/support/mod.rs +++ b/backend/crates/gateway-plugin/runtime/tests/support/mod.rs @@ -1,3 +1,5 @@ +//! 插件运行时测试共用的制品、实例与端口构造辅助 + use std::{ collections::{BTreeMap, BTreeSet}, io::Write as _, @@ -160,8 +162,8 @@ fn package_with_identity_and_state( }; let manifest = serde_json::to_vec(&manifest).unwrap(); let key = hex::encode(Sha256::digest(&manifest)); - // nextest 的用例分属独立进程;清单含 worker 摘要,只共享不可变归档。 - // 私有解包目录、会话、进程及 Store 仍由每个用例独立创建。 + // nextest 的用例分属独立进程;清单含 worker 摘要,只共享不可变归档 + // 私有解包目录、会话、进程及 Store 仍由每个用例独立创建 let cache = std::path::Path::new(env!("CARGO_TARGET_TMPDIR")).join("plugin-packages-v1"); std::fs::create_dir_all(&cache).unwrap(); let path = cache.join(format!("{key}.tar.gz")); @@ -182,7 +184,7 @@ fn package_with_identity_and_state( ("plugin.json".into(), manifest), ("bin/worker".into(), worker.to_vec()), ])); - // 原子发布避免构建中断后其他进程读到不完整的压缩包,文件锁随进程退出释放。 + // 原子发布避免构建中断后其他进程读到不完整的压缩包,文件锁随进程退出释放 let mut temporary = tempfile::NamedTempFile::new_in(cache).unwrap(); temporary.write_all(&package).unwrap(); temporary.persist(path).unwrap(); diff --git a/backend/crates/gateway-plugin/runtime/tests/support/native.rs b/backend/crates/gateway-plugin/runtime/tests/support/native.rs index 54ddbcbd9..1b7c85f05 100644 --- a/backend/crates/gateway-plugin/runtime/tests/support/native.rs +++ b/backend/crates/gateway-plugin/runtime/tests/support/native.rs @@ -1,3 +1,5 @@ +//! 插件集成测试使用的原生 Provider 与执行事件替身 + use std::{collections::BTreeSet, sync::Arc}; use async_trait::async_trait; @@ -127,7 +129,7 @@ impl Provider for NativeProvider { } } -// 仅测试 Core / Runtime / Store 组合;真实 Provider 的凭据解释由各自合同测试验证。 +// 仅测试 Core / Runtime / Store 组合;真实 Provider 的凭据解释由各自合同测试验证 struct FixtureConnection(ProviderAccountId); impl gateway_core::engine::upstream_adapter::UpstreamAccountConnection for FixtureConnection { diff --git a/backend/crates/gateway-plugin/runtime/tests/support/store.rs b/backend/crates/gateway-plugin/runtime/tests/support/store.rs index 6f7c1f93e..f41ebf63e 100644 --- a/backend/crates/gateway-plugin/runtime/tests/support/store.rs +++ b/backend/crates/gateway-plugin/runtime/tests/support/store.rs @@ -1,3 +1,5 @@ +//! 插件运行时测试使用的内存存储与状态迁移替身 + use std::{collections::BTreeMap, sync::Mutex}; use async_trait::async_trait; diff --git a/backend/crates/gateway-plugin/runtime/tests/support/worker.rs b/backend/crates/gateway-plugin/runtime/tests/support/worker.rs index 2b97f892e..d75df8f4b 100644 --- a/backend/crates/gateway-plugin/runtime/tests/support/worker.rs +++ b/backend/crates/gateway-plugin/runtime/tests/support/worker.rs @@ -1,4 +1,4 @@ -//! 真实子进程协议对端;只依赖公开 SDK,Cargo 为集成测试构建此辅助二进制。 +//! 真实子进程协议对端;只依赖公开 SDK,Cargo 为集成测试构建此辅助二进制 use std::{ collections::BTreeMap, @@ -934,9 +934,9 @@ impl Peer { .await; let mut mapped = Vec::with_capacity(14 + source.len()); mapped.extend_from_slice(b"GMB1"); - mapped.push(1); // Only: 一个输出消费完整源 frame。 + mapped.push(1); // Only: 一个输出消费完整源 frame mapped.extend_from_slice(&read.source_id.to_be_bytes()); - mapped.push(0); // mapped frame 的 terminal 由 Runtime 从源事实恢复。 + mapped.push(0); // mapped frame 的 terminal 由 Runtime 从源事实恢复 mapped.extend_from_slice(&source); let credits = self.streams.lock().await.get(&id).unwrap().clone(); credits.take(mapped.len() as u64).await; @@ -1179,7 +1179,7 @@ impl Peer { } else { "host.log".into() }; - // 分配与入队保持同一顺序,避免并发回调制造不合法 ID 序列。 + // 分配与入队保持同一顺序,避免并发回调制造不合法 ID 序列 let mut callbacks = self.callbacks.lock().await; let callback = self.next_callback.fetch_add(2, Ordering::Relaxed); callbacks.insert( @@ -1343,10 +1343,10 @@ async fn main() { "malformed_truncated_frame" | "malformed_frame_length" ) { let bytes: &[u8] = if method == "malformed_truncated_frame" { - // 声明 16 字节元数据,却只写入一个字节后退出。 + // 声明 16 字节元数据,却只写入一个字节后退出 &[0, 0, 0, 16, 0, 0, 0, 0, b'{'] } else { - // 元数据长度超过公开的 64 KiB 上限,读取端必须在分配前拒绝。 + // 元数据长度超过公开的 64 KiB 上限,读取端必须在分配前拒绝 &[0, 1, 0, 1, 0, 0, 0, 0] }; let mut output = tokio::io::stdout(); @@ -1444,7 +1444,7 @@ async fn main() { impl Peer { async fn upstream_adapter(&self, id: u64, payload: Vec) -> Result<(), PluginFault> { let (request, body) = UpstreamAdapterRequest::decode(&payload).unwrap(); - // 假凭据用于检测宿主是否把已选账号令牌放入了插件输入。 + // 假凭据用于检测宿主是否把已选账号令牌放入了插件输入 assert!(!String::from_utf8_lossy(&payload).contains("fixture-native-token")); self.append_observation_marker("upstream_marker", &json!({"key":request.client_key_id,"account":request.account_id,"continuation":request.continuation})); self.send( diff --git a/backend/crates/gateway-plugin/sdk/src/call/catalog.rs b/backend/crates/gateway-plugin/sdk/src/call/catalog.rs index 9affd10d5..a0502c78c 100644 --- a/backend/crates/gateway-plugin/sdk/src/call/catalog.rs +++ b/backend/crates/gateway-plugin/sdk/src/call/catalog.rs @@ -1,15 +1,15 @@ -//! 固定 Provider 的模型别名贡献;目标功能与协议元数据由宿主目录提供。 +//! 固定 Provider 的模型别名贡献;目标功能与协议元数据由宿主目录提供 use serde::{Deserialize, Serialize}; -/// 一次实例准备产生的完整目录,随实例配置原子发布。 +/// 一次实例准备产生的完整目录,随实例配置原子发布 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ModelCatalogRegistration { pub models: Vec, } -/// 指向内置 Provider 上游模型的直接别名,不创建新的 Provider 执行器。 +/// 指向内置 Provider 上游模型的直接别名,不创建新的 Provider 执行器 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ModelAlias { diff --git a/backend/crates/gateway-plugin/sdk/src/call/data.rs b/backend/crates/gateway-plugin/sdk/src/call/data.rs index e3b2bea46..48927ec1b 100644 --- a/backend/crates/gateway-plugin/sdk/src/call/data.rs +++ b/backend/crates/gateway-plugin/sdk/src/call/data.rs @@ -1,5 +1,5 @@ -//! 基础事实查询与主动额度刷新。 -//! 响应忽略未知字段,以兼容宿主新增事实;查询仍严格校验字段。 +//! 基础事实查询与主动额度刷新 +//! 响应忽略未知字段,以兼容宿主新增事实;查询仍严格校验字段 use serde::{Deserialize, Serialize}; @@ -14,7 +14,7 @@ pub struct ClientKeyFactsQuery { pub client_key_id: String, } -/// 当前显式分组绑定;空列表不是单账号范围,不包含密钥或凭据。 +/// 当前显式分组绑定;空列表不是单账号范围,不包含密钥或凭据 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ClientKeyFacts { pub schema_version: u32, @@ -55,7 +55,7 @@ pub struct QuotaFactsQuery { pub account_id: String, } -/// Provider 已有快照的必要投影;空观测时间表示没有可用样本,不代表额度为零。 +/// Provider 已有快照的必要投影;空观测时间表示没有可用样本,不代表额度为零 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct QuotaFacts { pub schema_version: u32, @@ -68,7 +68,7 @@ pub struct QuotaFacts { pub struct QuotaWindowFacts { pub key: String, pub window_seconds: Option, - /// 百分比而非 0~1 比率;未知值为 null。 + /// 百分比而非 0~1 比率;未知值为 null pub used_percent: Option, pub reset_at_ms: Option, } diff --git a/backend/crates/gateway-plugin/sdk/src/call/frontend_authentication.rs b/backend/crates/gateway-plugin/sdk/src/call/frontend_authentication.rs index 0b944ff9a..f823191c1 100644 --- a/backend/crates/gateway-plugin/sdk/src/call/frontend_authentication.rs +++ b/backend/crates/gateway-plugin/sdk/src/call/frontend_authentication.rs @@ -1,17 +1,17 @@ -//! 数据面入口认证合同;宿主拥有 principal 到执行身份的映射。 +//! 数据面入口认证合同;宿主拥有 principal 到执行身份的映射 use std::fmt; use serde::{Deserialize, Serialize}; -/// 插件在注册阶段返回的稳定认证器标识。 +/// 插件在注册阶段返回的稳定认证器标识 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct FrontendAuthenticationIdentifier { pub identifier: String, } -/// API 提取的有界认证信封;Cookie、管理会话及客户端画像不进入插件调用。 +/// API 提取的有界认证信封;Cookie、管理会话及客户端画像不进入插件调用 #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct FrontendAuthenticationRequest { @@ -27,7 +27,7 @@ impl fmt::Debug for FrontendAuthenticationRequest { } } -/// 插件只声明外部 principal,不得选择宿主 Client Key。 +/// 插件只声明外部 principal,不得选择宿主 Client Key #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "outcome", rename_all = "snake_case", deny_unknown_fields)] pub enum FrontendAuthenticationResult { diff --git a/backend/crates/gateway-plugin/sdk/src/call/host.rs b/backend/crates/gateway-plugin/sdk/src/call/host.rs index 34fab2c89..72923d3ff 100644 --- a/backend/crates/gateway-plugin/sdk/src/call/host.rs +++ b/backend/crates/gateway-plugin/sdk/src/call/host.rs @@ -1,4 +1,4 @@ -//! 宿主回调的数据合同;这里只传递数据,不持有宿主服务或执行资源。 +//! 宿主回调的数据合同;这里只传递数据,不持有宿主服务或执行资源 use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; @@ -8,7 +8,7 @@ use super::model::ExecutionEvent; const MODEL_EVENT_BATCH_PREFIX: [u8; 4] = *b"HME1"; const MAX_MODEL_EVENT_BATCH_BYTES: usize = 8 * 1024 * 1024; -// 凭据文档与账号资料不可通过 Debug 写入诊断。 +// 凭据文档与账号资料不可通过 Debug 写入诊断 #[derive(Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct CredentialFacts { @@ -35,9 +35,9 @@ pub enum LogLevel { Error, } -/// event 必须是代码中固定的事件标识,不能填入凭据、URL 或请求内容。 -/// 宿主只记录 event 的长度和摘要,不因标识字符合法而放行明文。 -/// fields 由宿主按诊断规则脱敏;不能通过自定义字段声明内容可信。 +/// event 必须是代码中固定的事件标识,不能填入凭据、URL 或请求内容 +/// 宿主只记录 event 的长度和摘要,不因标识字符合法而放行明文 +/// fields 由宿主按诊断规则脱敏;不能通过自定义字段声明内容可信 #[derive(Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct LogRequest { @@ -48,15 +48,15 @@ pub struct LogRequest { pub fields: std::collections::BTreeMap, } -/// 超出日志预算时直接丢弃,不等待、不排队,插件不应重试。 +/// 超出日志预算时直接丢弃,不等待、不排队,插件不应重试 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct LogResult { - /// 是否交给有界异步日志写入;不保证立即落盘,最终级别过滤仍由宿主决定。 + /// 是否交给有界异步日志写入;不保证立即落盘,最终级别过滤仍由宿主决定 pub recorded: bool, } -/// URL、头和正文可能携带敏感材料,不提供内容型 Debug。 +/// URL、头和正文可能携带敏感材料,不提供内容型 Debug #[derive(Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct HttpRequest { @@ -74,7 +74,7 @@ pub struct HttpResponse { pub stream: Option, } -/// 宿主账号列表查询;cursor 是上次结果返回的稳定账号 ID。 +/// 宿主账号列表查询;cursor 是上次结果返回的稳定账号 ID #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct AuthListRequest { @@ -83,7 +83,7 @@ pub struct AuthListRequest { pub limit: u16, } -/// 不含原始凭据的账号运行投影。 +/// 不含原始凭据的账号运行投影 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct AuthRuntimeAccount { @@ -116,7 +116,7 @@ pub struct AuthGetRequest { pub account_id: String, } -/// 原始凭据只在 RPC 二进制载荷中传输,故意不实现 `Debug`。 +/// 原始凭据只在 RPC 二进制载荷中传输,故意不实现 `Debug` #[derive(Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct AuthCredential { @@ -126,8 +126,8 @@ pub struct AuthCredential { pub facts: CredentialFacts, } -/// 新建账号由宿主生成 ID;替换账号必须提交精确 credential revision。 -/// 凭据只在 RPC 二进制载荷中传输,故意不实现 `Debug`。 +/// 新建账号由宿主生成 ID;替换账号必须提交精确 credential revision +/// 凭据只在 RPC 二进制载荷中传输,故意不实现 `Debug` #[derive(Clone, Serialize, Deserialize)] #[serde(tag = "action", rename_all = "snake_case", deny_unknown_fields)] pub enum AuthSaveRequest { @@ -162,7 +162,7 @@ pub struct StreamClose { pub stream: String, } -/// 模型执行回调可交给 Core 的稳定操作;Provider 私有端点不属于模型回调。 +/// 模型执行回调可交给 Core 的稳定操作;Provider 私有端点不属于模型回调 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ModelOperation { @@ -172,13 +172,13 @@ pub enum ModelOperation { Search, } -/// 模型执行回调的请求元数据;正文始终使用 RPC 的独立二进制载荷。 +/// 模型执行回调的请求元数据;正文始终使用 RPC 的独立二进制载荷 /// -/// Provider/账号用于指定执行目标;显式 Key 决定本次模型执行身份。 +/// Provider/账号用于指定执行目标;显式 Key 决定本次模型执行身份 #[derive(Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ModelExecuteRequest { - /// 管理、命令、维护、认证与观察调用显式选择 Key;数据请求内的子调用继承父请求身份。 + /// 管理、命令、维护、认证与观察调用显式选择 Key;数据请求内的子调用继承父请求身份 #[serde(default, skip_serializing_if = "Option::is_none")] pub client_key_id: Option, pub model: String, @@ -192,7 +192,7 @@ pub struct ModelExecuteRequest { pub previous_response_id: Option, } -/// 按本次选择的 Key 查询可见模型,不读取或返回 Key 明文。 +/// 按本次选择的 Key 查询可见模型,不读取或返回 Key 明文 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ModelListRequest { @@ -207,7 +207,7 @@ pub struct ModelListResult { pub models: Vec, } -/// 分页读取 Key 的非秘密信息,用于管理页和命令行选择执行身份。 +/// 分页读取 Key 的非秘密信息,用于管理页和命令行选择执行身份 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct KeyListRequest { @@ -230,7 +230,7 @@ pub struct KeyListResult { pub next_cursor: Option, } -/// 非流式子请求已经完成并由 Core 提交;事件位于二进制载荷中的一个 HME1 批次。 +/// 非流式子请求已经完成并由 Core 提交;事件位于二进制载荷中的一个 HME1 批次 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ModelExecuteResult { @@ -238,7 +238,7 @@ pub struct ModelExecuteResult { pub events: u32, } -/// 流式子请求句柄只能在创建它的父调用上下文中使用。 +/// 流式子请求句柄只能在创建它的父调用上下文中使用 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ModelStreamResult { @@ -253,7 +253,7 @@ pub struct ModelStreamReadRequest { pub maximum_bytes: u32, } -/// `end=true` 表示 Core 已完成终态与结算,载荷可以为空。 +/// `end=true` 表示 Core 已完成终态与结算,载荷可以为空 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ModelStreamReadResult { @@ -267,7 +267,7 @@ pub struct ModelStreamCloseRequest { pub stream: String, } -/// 查询 Provider 已经保存的不可逆亲和键;宿主不会从任意会话文本推导假命中。 +/// 查询 Provider 已经保存的不可逆亲和键;宿主不会从任意会话文本推导假命中 #[derive(Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct AffinityLookupRequest { @@ -275,15 +275,16 @@ pub struct AffinityLookupRequest { pub key: String, } -/// 命中只返回账号偏好;后续模型调用仍由 Core 复核账号资格并取得租约。 +/// 命中只返回账号偏好;后续模型调用仍由 Core 复核账号资格并取得租约 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct AffinityLookupResult { pub account_id: Option, } -/// 模型执行回调的有界事件批次。单个事件使用 GPE2 封套,批次只增加 -/// 长度索引,不把原生正文改写成 JSON/base64。 +/// 模型执行回调的有界事件批次 +/// 单个事件使用 GPE2 封套,批次只增加 +/// 长度索引,不把原生正文改写成 JSON/base64 #[derive(Clone, Default)] pub struct ModelEventBatch { pub events: Vec, @@ -292,7 +293,7 @@ pub struct ModelEventBatch { impl ModelEventBatch { /// # Errors /// - /// 事件编码无效、数量/长度溢出或总批次超过 8 MiB 时返回错误。 + /// 事件编码无效、数量/长度溢出或总批次超过 8 MiB 时返回错误 pub fn encode(self) -> Result, ModelEventBatchError> { let count = u32::try_from(self.events.len()).map_err(|_| ModelEventBatchError)?; let mut encoded = Vec::with_capacity(self.events.len()); @@ -331,7 +332,7 @@ impl ModelEventBatch { /// # Errors /// - /// 版本、长度索引、事件封套或总批次无效时返回错误。 + /// 版本、长度索引、事件封套或总批次无效时返回错误 pub fn decode(bytes: &[u8]) -> Result { if bytes.len() < 8 || bytes.len() > MAX_MODEL_EVENT_BATCH_BYTES @@ -372,7 +373,7 @@ impl ModelEventBatch { #[error("host model event batch is invalid or exceeds its limit")] pub struct ModelEventBatchError; -/// 私有状态读取;值只通过宿主绑定的实例与命名空间解析。 +/// 私有状态读取;值只通过宿主绑定的实例与命名空间解析 #[derive(Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct StateGetRequest { @@ -380,7 +381,7 @@ pub struct StateGetRequest { pub key: String, } -/// 状态值可能包含敏感插件数据,故意不实现 `Debug`。 +/// 状态值可能包含敏感插件数据,故意不实现 `Debug` #[derive(Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct StateRecord { @@ -395,7 +396,7 @@ pub struct StateGetResult { pub record: Option, } -/// `expected_version = None` 表示仅当键不存在时创建;更新必须提交精确版本。 +/// `expected_version = None` 表示仅当键不存在时创建;更新必须提交精确版本 #[derive(Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct StatePutRequest { @@ -425,7 +426,7 @@ pub struct StateDeleteResult { pub deleted: bool, } -/// 宿主控制的迁移批次;插件不能选择源记录、目标实例或提交边界。 +/// 宿主控制的迁移批次;插件不能选择源记录、目标实例或提交边界 #[derive(Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct StateMigrationRequest { diff --git a/backend/crates/gateway-plugin/sdk/src/call/key_budgets.rs b/backend/crates/gateway-plugin/sdk/src/call/key_budgets.rs index 6e745ece2..c5b7d5a93 100644 --- a/backend/crates/gateway-plugin/sdk/src/call/key_budgets.rs +++ b/backend/crates/gateway-plugin/sdk/src/call/key_budgets.rs @@ -1,4 +1,4 @@ -//! Client Key 预算查询、上限更新与用量重置。 +//! Client Key 预算查询、上限更新与用量重置 use serde::{Deserialize, Serialize}; diff --git a/backend/crates/gateway-plugin/sdk/src/call/management.rs b/backend/crates/gateway-plugin/sdk/src/call/management.rs index 1acc84310..7b7d7bda8 100644 --- a/backend/crates/gateway-plugin/sdk/src/call/management.rs +++ b/backend/crates/gateway-plugin/sdk/src/call/management.rs @@ -1,4 +1,4 @@ -//! 管理与 CLI 的跨进程数据;参数和命令结果只通过有界二进制载荷传输。 +//! 管理与 CLI 的跨进程数据;参数和命令结果只通过有界二进制载荷传输 use std::collections::BTreeMap; @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use super::host::AuthSaveRequest; -/// `management.register` 冻结的路由与页面;路径均相对插件实例命名空间。 +/// `management.register` 冻结的路由与页面;路径均相对插件实例命名空间 #[derive(Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ManagementRegistration { @@ -20,7 +20,7 @@ pub struct ManagementRegistration { pub callbacks: Vec, } -/// 公开登录回调;宿主签发的一次性 state 用于关联已发起的流程。 +/// 公开登录回调;宿主签发的一次性 state 用于关联已发起的流程 #[derive(Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ManagementCallback { @@ -28,7 +28,7 @@ pub struct ManagementCallback { pub response_content_types: Vec, } -/// 管理 handler 默认要求管理员身份;原始请求头完整传入插件。 +/// 管理 handler 默认要求管理员身份;原始请求头完整传入插件 #[derive(Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ManagementRoute { @@ -39,7 +39,7 @@ pub struct ManagementRoute { pub response_content_types: Vec, } -/// 资源须在包清单 resources 中声明并校验摘要;`public` 决定是否允许未登录访问。 +/// 资源须在包清单 resources 中声明并校验摘要;`public` 决定是否允许未登录访问 #[derive(Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ManagementResource { @@ -53,14 +53,14 @@ pub struct ManagementResource { pub struct ManagementPage { pub id: String, pub title: String, - /// 页面目录直接提供副标题,避免加载静态资源后再替换宿主标题区。 + /// 页面目录直接提供副标题,避免加载静态资源后再替换宿主标题区 #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, pub entry: String, pub icon: Option, } -/// `management.handle` 元数据;原始请求体独立放在帧 payload,不进行 JSON 二次编码。 +/// `management.handle` 元数据;原始请求体独立放在帧 payload,不进行 JSON 二次编码 #[derive(Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ManagementRequest { @@ -72,7 +72,7 @@ pub struct ManagementRequest { pub headers: Vec, } -/// 原始响应体独立放在帧 payload;显式 headers 可覆盖宿主的默认响应头。 +/// 原始响应体独立放在帧 payload;显式 headers 可覆盖宿主的默认响应头 #[derive(Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ManagementResponse { @@ -82,14 +82,14 @@ pub struct ManagementResponse { pub headers: Vec, } -/// `command_line.register` 的只读结果;注册和帮助查询不能执行命令或登录。 +/// `command_line.register` 的只读结果;注册和帮助查询不能执行命令或登录 #[derive(Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct CommandRegistration { pub commands: Vec, } -/// 命令名只在插件实例的命名空间内生效,不注册宿主全局启动参数。 +/// 命令名只在插件实例的命名空间内生效,不注册宿主全局启动参数 #[derive(Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct CommandDescriptor { @@ -112,7 +112,7 @@ pub struct CommandParameter { pub default: Option, } -/// `int` 固定为 32 位,duration 固定为有符号纳秒,避免随部署平台改变合同。 +/// `int` 固定为 32 位,duration 固定为有符号纳秒,避免随部署平台改变合同 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum CommandParameterType { @@ -124,7 +124,7 @@ pub enum CommandParameterType { Duration, } -/// 参数可能含登录材料,故意不实现内容型 Debug。 +/// 参数可能含登录材料,故意不实现内容型 Debug #[derive(Clone, PartialEq, Serialize, Deserialize)] #[serde( tag = "type", @@ -148,8 +148,8 @@ pub struct CommandInvocation { pub arguments: BTreeMap, } -/// 输出由宿主原样交付 CLI 调用方,不进入普通诊断。 -/// 待保存账号仅在退出码为零时按顺序经 Admin 提交;每项独立 CAS,失败不自动重试。 +/// 输出由宿主原样交付 CLI 调用方,不进入普通诊断 +/// 待保存账号仅在退出码为零时按顺序经 Admin 提交;每项独立 CAS,失败不自动重试 #[derive(Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct CommandResult { diff --git a/backend/crates/gateway-plugin/sdk/src/call/middleware/http.rs b/backend/crates/gateway-plugin/sdk/src/call/middleware/http.rs index 8c66a055a..7664dc2a3 100644 --- a/backend/crates/gateway-plugin/sdk/src/call/middleware/http.rs +++ b/backend/crates/gateway-plugin/sdk/src/call/middleware/http.rs @@ -1,9 +1,9 @@ -//! 路由前 HTTP 视图;复用 middleware.handle、next 与正文资源调用。 +//! 路由前 HTTP 视图;复用 middleware.handle、next 与正文资源调用 use super::MiddlewareHeader; use serde::{Deserialize, Serialize}; -/// 正文句柄消费一次;Bytes 使用当前 RPC 的 binary payload,Stream 使用响应流。 +/// 正文句柄消费一次;Bytes 使用当前 RPC 的 binary payload,Stream 使用响应流 #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] pub enum Body { @@ -16,13 +16,13 @@ pub enum Body { #[derive(Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Request { - /// 认证与解压前的运行设置;null 表示主动调用继承宿主或父调用基线。 + /// 认证与解压前的运行设置;null 表示主动调用继承宿主或父调用基线 pub settings: serde_json::Value, pub method: String, pub uri: String, pub version: Version, pub headers: Vec, - /// 缺省超时由宿主形成基线;None 显式取消本次 HTTP 处理期限。 + /// 缺省超时由宿主形成基线;None 显式取消本次 HTTP 处理期限 pub timeout_ms: Option, pub body: Body, } @@ -34,9 +34,9 @@ pub struct Response { pub version: Version, pub headers: Vec, pub body: Body, - /// 返还 next 的原响应时保留该关联,使宿主继续持有传输扩展和资源。 + /// 返还 next 的原响应时保留该关联,使宿主继续持有传输扩展和资源 pub response: Option, - /// 响应流承载会话处理的完成与错误,不承载 HTTP 正文。 + /// 响应流承载会话处理的完成与错误,不承载 HTTP 正文 pub session: bool, } @@ -64,7 +64,7 @@ pub struct Call { pub request: Request, } -/// 数据在 binary payload,trailers 保留多值 header;EOF 不携带正文或 trailers。 +/// 数据在 binary payload,trailers 保留多值 header;EOF 不携带正文或 trailers #[derive(Clone, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct BodyRead { diff --git a/backend/crates/gateway-plugin/sdk/src/call/middleware/mod.rs b/backend/crates/gateway-plugin/sdk/src/call/middleware/mod.rs index f192cf25b..777f750b2 100644 --- a/backend/crates/gateway-plugin/sdk/src/call/middleware/mod.rs +++ b/backend/crates/gateway-plugin/sdk/src/call/middleware/mod.rs @@ -1,4 +1,4 @@ -//! 统一请求/响应中间件的跨进程数据合同。 +//! 统一请求/响应中间件的跨进程数据合同 pub mod http; pub mod websocket; @@ -13,7 +13,7 @@ pub const BODY_CLOSE_METHOD: &str = "host.middleware.body_close"; const BODY_FRAME_PREFIX: [u8; 4] = *b"GMB1"; -/// 中间件在逻辑请求外层或单次 Provider attempt 内层执行。 +/// 中间件在逻辑请求外层或单次 Provider attempt 内层执行 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum MiddlewareMount { @@ -21,7 +21,7 @@ pub enum MiddlewareMount { Attempt, } -/// 客户端交付形态;它不授予插件访问底层 HTTP 或 WebSocket 连接。 +/// 客户端交付形态;它不授予插件访问底层 HTTP 或 WebSocket 连接 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum MiddlewareTransport { @@ -31,7 +31,8 @@ pub enum MiddlewareTransport { Internal, } -/// 正文读取边界。结构化正文只按完整文档或完整 SSE 事件交付。 +/// 正文读取边界 +/// 结构化正文只按完整文档或完整 SSE 事件交付 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum MiddlewareBodyFraming { @@ -40,9 +41,9 @@ pub enum MiddlewareBodyFraming { RawBytes, } -/// 一个保留顺序与重复项的 HTTP header。 +/// 一个保留顺序与重复项的 HTTP header /// -/// Header 值可能包含不适合日志的客户端数据,因此故意不实现 `Debug`。 +/// Header 值可能包含不适合日志的客户端数据,因此故意不实现 `Debug` #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct MiddlewareHeader { @@ -50,9 +51,9 @@ pub struct MiddlewareHeader { pub value: Vec, } -/// 对宿主保存的原始 header 集合做增量修改;未列出的项保持原样。 +/// 对宿主保存的原始 header 集合做增量修改;未列出的项保持原样 /// -/// Header 值可能包含不适合日志的客户端数据,因此故意不实现 `Debug`。 +/// Header 值可能包含不适合日志的客户端数据,因此故意不实现 `Debug` #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "operation", rename_all = "snake_case", deny_unknown_fields)] pub enum MiddlewareHeaderMutation { @@ -60,12 +61,13 @@ pub enum MiddlewareHeaderMutation { Append { name: String, value: Vec }, } -/// `middleware.handle` 的请求头。完整正文位于 RPC binary payload。 +/// `middleware.handle` 的请求头 +/// 完整正文位于 RPC binary payload #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct MiddlewareRequestHead { pub settings_sources: serde_json::Value, - /// 当前请求的完整有效执行设置;尚未建立可改写设置的边界使用 null。 + /// 当前请求的完整有效执行设置;尚未建立可改写设置的边界使用 null #[serde(default)] pub settings: serde_json::Value, pub request_id: String, @@ -88,7 +90,7 @@ pub struct MiddlewareRequestHead { pub headers: Vec, } -/// 调用 next 时如何处理宿主保存的原始请求正文。 +/// 调用 next 时如何处理宿主保存的原始请求正文 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum MiddlewareRequestBody { @@ -96,7 +98,7 @@ pub enum MiddlewareRequestBody { Replace, } -/// 可声明的请求功能;原生续接始终由宿主与 Provider 管理,不能由声明豁免。 +/// 可声明的请求功能;原生续接始终由宿主与 Provider 管理,不能由声明豁免 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum RequestFeature { @@ -106,7 +108,7 @@ pub enum RequestFeature { JsonSchema, } -/// 插件承担的功能转换与额外上游需求,均不覆盖正文推导的事实。 +/// 插件承担的功能转换与额外上游需求,均不覆盖正文推导的事实 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct CapabilityDeclaration { @@ -114,11 +116,12 @@ pub struct CapabilityDeclaration { pub required: Vec, } -/// 单次 `host.middleware.next` 调用。`Replace` 的完整正文位于 binary payload。 +/// 单次 `host.middleware.next` 调用 +/// `Replace` 的完整正文位于 binary payload #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct MiddlewareNextRequest { - /// None 保留原设置;Some 是完整替换,字段删除必须符合对应设置类型。 + /// None 保留原设置;Some 是完整替换,字段删除必须符合对应设置类型 #[serde(default, skip_serializing_if = "Option::is_none")] pub settings: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -130,7 +133,7 @@ pub struct MiddlewareNextRequest { pub capabilities: Option, } -/// 受父调用约束的不透明响应正文句柄。 +/// 受父调用约束的不透明响应正文句柄 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct MiddlewareBodyHandle { @@ -138,11 +141,12 @@ pub struct MiddlewareBodyHandle { pub framing: MiddlewareBodyFraming, } -/// `host.middleware.next` 返回的响应头。正文必须通过句柄惰性读取或原样返还。 +/// `host.middleware.next` 返回的响应头 +/// 正文必须通过句柄惰性读取或原样返还 #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct MiddlewareNextResponse { - /// 绑定本次 next 结果的宿主响应身份;只能在当前父调用内返还。 + /// 绑定本次 next 结果的宿主响应身份;只能在当前父调用内返还 pub response: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub metadata: Option>, @@ -154,7 +158,7 @@ pub struct MiddlewareNextResponse { pub body: Option, } -/// 最终正文来源;`PassThrough` 直接转交下游正文,不触发读取。 +/// 最终正文来源;`PassThrough` 直接转交下游正文,不触发读取 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] pub enum MiddlewareResponseBody { @@ -163,17 +167,17 @@ pub enum MiddlewareResponseBody { Stream { framing: MiddlewareBodyFraming }, } -/// `middleware.handle` 的最终响应头;插件输出流仍使用既有 RPC Stream/Credit/End。 +/// `middleware.handle` 的最终响应头;插件输出流仍使用既有 RPC Stream/Credit/End #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct MiddlewareResponseHead { - /// `Some` 表示基于本层 next 返回的原响应;`None` 表示中间件短路响应。 + /// `Some` 表示基于本层 next 返回的原响应;`None` 表示中间件短路响应 #[serde(default, skip_serializing_if = "Option::is_none")] pub response: Option, - /// 缺省时保留原响应协议;短路响应必须填写。 + /// 缺省时保留原响应协议;短路响应必须填写 #[serde(default, skip_serializing_if = "Option::is_none")] pub protocol: Option, - /// 缺省时保留原状态;短路响应必须填写。 + /// 缺省时保留原状态;短路响应必须填写 #[serde(default, skip_serializing_if = "Option::is_none")] pub status: Option, #[serde(default)] @@ -188,7 +192,7 @@ pub struct MiddlewareBodyRead { pub maximum_bytes: u32, } -/// 读取当前源 frame 的宿主事实,不消费或复制原结算所有权。 +/// 读取当前源 frame 的宿主事实,不消费或复制原结算所有权 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct MiddlewareBodyFacts { @@ -202,12 +206,12 @@ pub struct MiddlewareBodyFactsResult { pub present: bool, } -/// `eof` 只在空 payload 时成立;结构化 frame 不拆成任意网络分块。 +/// `eof` 只在空 payload 时成立;结构化 frame 不拆成任意网络分块 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct MiddlewareBodyReadResult { pub framing: MiddlewareBodyFraming, - /// Runtime 为每个输入 frame 分配的非零单调标识;EOF 时必须为零。 + /// Runtime 为每个输入 frame 分配的非零单调标识;EOF 时必须为零 pub source_id: u64, pub eof: bool, pub terminal: bool, @@ -223,7 +227,7 @@ pub struct MiddlewareBodyClose { #[serde(deny_unknown_fields)] pub struct MiddlewareBodyCloseResult {} -/// 一个输出 frame 与其来源的 wire disposition。 +/// 一个输出 frame 与其来源的 wire disposition #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum MiddlewareBodyDisposition { @@ -235,10 +239,10 @@ pub enum MiddlewareBodyDisposition { Drop = 5, } -/// 插件替换正文流中的一个完整 frame;正文不进入 JSON metadata。 +/// 插件替换正文流中的一个完整 frame;正文不进入 JSON metadata #[derive(Clone, PartialEq)] pub struct MiddlewareBodyFrame { - /// 按需读取的来源快照,不进入插件返回的正文编码。 + /// 按需读取的来源快照,不进入插件返回的正文编码 pub facts: Option>, pub payload: Vec, pub terminal: bool, @@ -290,7 +294,7 @@ impl MiddlewareBodyFrame { } self.source_id = source_id; self.disposition = disposition; - // Mapped frame 的终态只由 Runtime 保存的源 frame 派生。 + // Mapped frame 的终态只由 Runtime 保存的源 frame 派生 self.terminal = false; Ok(self) } @@ -305,7 +309,7 @@ impl MiddlewareBodyFrame { self.disposition } - /// 将 frame 元数据与原始正文编码到一个 RPC stream chunk。 + /// 将 frame 元数据与原始正文编码到一个 RPC stream chunk #[must_use] pub fn encode(self) -> Vec { let mut encoded = Vec::with_capacity(14 + self.payload.len()); @@ -317,11 +321,11 @@ impl MiddlewareBodyFrame { encoded } - /// 解码一个完整的中间件正文 frame。 + /// 解码一个完整的中间件正文 frame /// /// # Errors /// - /// 魔数或 flags 无效时返回错误。 + /// 魔数或 flags 无效时返回错误 pub fn decode(bytes: &[u8]) -> Result { if bytes.len() < 14 || bytes[..4] != BODY_FRAME_PREFIX || bytes[13] > 1 { return Err(MiddlewareBodyFrameError); diff --git a/backend/crates/gateway-plugin/sdk/src/call/middleware/websocket.rs b/backend/crates/gateway-plugin/sdk/src/call/middleware/websocket.rs index 6ae90b2c7..66275a3dc 100644 --- a/backend/crates/gateway-plugin/sdk/src/call/middleware/websocket.rs +++ b/backend/crates/gateway-plugin/sdk/src/call/middleware/websocket.rs @@ -1,4 +1,4 @@ -//! 双工消息使用二进制 payload 或惰性句柄,避免强制 JSON/UTF-8 解码。 +//! 双工消息使用二进制 payload 或惰性句柄,避免强制 JSON/UTF-8 解码 use super::MiddlewareHeader; use serde::{Deserialize, Serialize}; pub const SEND_METHOD: &str = "host.middleware.send"; diff --git a/backend/crates/gateway-plugin/sdk/src/call/mod.rs b/backend/crates/gateway-plugin/sdk/src/call/mod.rs index 2177bebd0..45d8fa332 100644 --- a/backend/crates/gateway-plugin/sdk/src/call/mod.rs +++ b/backend/crates/gateway-plugin/sdk/src/call/mod.rs @@ -1,4 +1,4 @@ -//! 按业务职责组织的跨进程调用数据,不依赖网关领域类型。 +//! 按业务职责组织的跨进程调用数据,不依赖网关领域类型 pub mod catalog; pub mod data; diff --git a/backend/crates/gateway-plugin/sdk/src/call/model/codec.rs b/backend/crates/gateway-plugin/sdk/src/call/model/codec.rs index 73a023e5b..4398d1836 100644 --- a/backend/crates/gateway-plugin/sdk/src/call/model/codec.rs +++ b/backend/crates/gateway-plugin/sdk/src/call/model/codec.rs @@ -1,8 +1,10 @@ +//! 模型执行事件的元数据与原始载荷分离编码及有界解码 + use serde::{Serialize, de::DeserializeOwned}; use super::{ExecutionEvent, WirePayload}; -/// 宿主与插件还会施加更低的帧、队列及可用信用上限。 +/// 宿主与插件还会施加更低的帧、队列及可用信用上限 pub const MAX_EXECUTION_PAYLOAD_BYTES: usize = 8 * 1024 * 1024; #[derive(Debug, thiserror::Error)] @@ -88,7 +90,7 @@ pub(in crate::call) fn unpack( let mut cursor = header_bytes; let mut metadata = &[][..]; let mut payloads = [&[][..]; N]; - // 先验证全部分段边界,不能根据对端的长度声明直接分配。 + // 先验证全部分段边界,不能根据对端的长度声明直接分配 for (chunk, target) in bytes[4..header_bytes] .chunks_exact(4) .zip(std::iter::once(&mut metadata).chain(payloads.iter_mut())) diff --git a/backend/crates/gateway-plugin/sdk/src/call/model/facts.rs b/backend/crates/gateway-plugin/sdk/src/call/model/facts.rs index dbe50e24b..3eaad56d5 100644 --- a/backend/crates/gateway-plugin/sdk/src/call/model/facts.rs +++ b/backend/crates/gateway-plugin/sdk/src/call/model/facts.rs @@ -1,4 +1,5 @@ -//! 宿主确认的执行事实快照。读取快照不会取得原执行的计费或资源所有权。 +//! 宿主确认的执行事实快照 +//! 读取快照不会取得原执行的计费或资源所有权 use serde::{Deserialize, Serialize}; @@ -77,7 +78,7 @@ pub struct ResponseObservation { pub upstream_response_model: Option, pub timings: RequestTimings, pub client_headers: Vec, - /// Provider 已产生的完整观测 JSON 文本,保留原始表示。 + /// Provider 已产生的完整观测 JSON 文本,保留原始表示 pub provider_metadata: Option, } diff --git a/backend/crates/gateway-plugin/sdk/src/call/model/mod.rs b/backend/crates/gateway-plugin/sdk/src/call/model/mod.rs index 74b23c593..7171bb751 100644 --- a/backend/crates/gateway-plugin/sdk/src/call/model/mod.rs +++ b/backend/crates/gateway-plugin/sdk/src/call/model/mod.rs @@ -1,4 +1,4 @@ -//! 宿主模型调用的流事件合同。 +//! 宿主模型调用的流事件合同 use serde::{Deserialize, Serialize}; @@ -39,8 +39,8 @@ pub struct Usage { pub total_tokens: Option, } -/// 每个流分块是一个完整封套;同一 wire 的事实必须合并,避免重复交付客户端。 -/// 载荷及私有状态不实现 Debug,且不能用拆分封套绕过宿主大小和序列检查。 +/// 每个流分块是一个完整封套;同一 wire 的事实必须合并,避免重复交付客户端 +/// 载荷及私有状态不实现 Debug,且不能用拆分封套绕过宿主大小和序列检查 #[derive(Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ExecutionEvent { @@ -48,24 +48,24 @@ pub struct ExecutionEvent { pub facts: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub wire: Option, - /// 宿主确认的来源事实;回传快照不会触发计量、续接或发送状态更新。 + /// 宿主确认的来源事实;回传快照不会触发计量、续接或发送状态更新 #[serde(default, skip_serializing_if = "Option::is_none")] pub host: Option>, } impl ExecutionEvent { - /// 编码为带长度与版本的事件载荷;原始 wire/错误正文不经过 JSON 数组或 base64。 + /// 编码为带长度与版本的事件载荷;原始 wire/错误正文不经过 JSON 数组或 base64 /// /// # Errors - /// 总长度超过事件上限或元数据无法序列化时返回错误。 + /// 总长度超过事件上限或元数据无法序列化时返回错误 pub fn encode(self) -> Result, ExecutionEncodingError> { codec::encode_event(self) } - /// 从完整的执行流载荷解码,先验证各段长度再解析元数据。 + /// 从完整的执行流载荷解码,先验证各段长度再解析元数据 /// /// # Errors - /// 版本、长度、元数据或二进制段声明不一致时返回错误。 + /// 版本、长度、元数据或二进制段声明不一致时返回错误 pub fn decode(bytes: &[u8]) -> Result { codec::decode_event(bytes) } @@ -145,15 +145,15 @@ pub enum WirePayload { id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] retry: Option, - /// 一个完整 SSE 帧的原字节;存在时必须与解析后的 data 和元数据一致。 + /// 一个完整 SSE 帧的原字节;存在时必须与解析后的 data 和元数据一致 #[serde(default, skip_serializing_if = "Option::is_none")] raw_sse: Option>, }, - /// 注释、非 JSON data 等原生 SSE 表达,不能夹带第二个帧。 + /// 注释、非 JSON data 等原生 SSE 表达,不能夹带第二个帧 RawSse { frame: Vec }, - /// 一个完整的非流式 JSON 响应正文,保留空白与字段顺序。 + /// 一个完整的非流式 JSON 响应正文,保留空白与字段顺序 RawJson { body: Vec }, - /// 非流式 HTTP 响应正文的一个有序片段。 - /// 客户端 adapter 必须同时限制单片与聚合后的总字节数。 + /// 非流式 HTTP 响应正文的一个有序片段 + /// 客户端 adapter 必须同时限制单片与聚合后的总字节数 RawBody { body: Vec }, } diff --git a/backend/crates/gateway-plugin/sdk/src/call/observation.rs b/backend/crates/gateway-plugin/sdk/src/call/observation.rs index fc0e7cef8..9b7a8a2c8 100644 --- a/backend/crates/gateway-plugin/sdk/src/call/observation.rs +++ b/backend/crates/gateway-plugin/sdk/src/call/observation.rs @@ -1,10 +1,10 @@ -//! 请求完成与实际上游 WebSocket 事件的统一观察合同。 +//! 请求完成与实际上游 WebSocket 事件的统一观察合同 use serde::{Deserialize, Serialize}; use crate::SendState; -/// 订阅决定投递哪些事件,不裁剪事件中的字段。 +/// 订阅决定投递哪些事件,不裁剪事件中的字段 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum EventKind { @@ -13,7 +13,7 @@ pub enum EventKind { WebSocketResponse, } -/// 元数据在 RPC 参数中传递;WebSocket 原始帧独立放在二进制载荷中。 +/// 元数据在 RPC 参数中传递;WebSocket 原始帧独立放在二进制载荷中 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde( tag = "event", @@ -37,7 +37,7 @@ impl Event { } } -/// 请求终态分类;`rejected` 表示请求在进入 Provider 执行前被宿主拒绝。 +/// 请求终态分类;`rejected` 表示请求在进入 Provider 执行前被宿主拒绝 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum RequestOutcome { @@ -48,7 +48,7 @@ pub enum RequestOutcome { Incomplete, } -/// Core 对单次请求最终费用的可信程度。 +/// Core 对单次请求最终费用的可信程度 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum RequestCostStatus { @@ -56,7 +56,7 @@ pub enum RequestCostStatus { Unknown, } -/// 最终费用事实的来源;观察插件只消费结果,不重新计价。 +/// 最终费用事实的来源;观察插件只消费结果,不重新计价 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum RequestCostSource { @@ -65,7 +65,7 @@ pub enum RequestCostSource { Unavailable, } -/// 精确十进制金额;字符串保留宿主的定点精度。 +/// 精确十进制金额;字符串保留宿主的定点精度 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct RequestMoney { @@ -73,7 +73,7 @@ pub struct RequestMoney { pub currency: String, } -/// 用量观察取得的最终费用;未知费用没有 `total`,不得解释为零。 +/// 用量观察取得的最终费用;未知费用没有 `total`,不得解释为零 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct RequestCost { @@ -83,7 +83,7 @@ pub struct RequestCost { pub total: Option, } -/// 用量观察取得的毫秒级阶段耗时;缺失字段表示宿主没有该项事实。 +/// 用量观察取得的毫秒级阶段耗时;缺失字段表示宿主没有该项事实 #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct RequestTimings { @@ -107,7 +107,7 @@ pub struct RequestTimings { pub latency_ms: Option, } -/// 非成功请求的最终分类、发送状态与重试事实。 +/// 非成功请求的最终分类、发送状态与重试事实 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct RequestFailure { @@ -124,7 +124,7 @@ pub struct RequestFailure { pub retry_after_ms: Option, } -/// 请求生命周期观察取得的最终宿主事实。 +/// 请求生命周期观察取得的最终宿主事实 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct RequestTerminal { @@ -137,7 +137,7 @@ pub struct RequestTerminal { pub error_code: Option, } -/// 用量观察取得的最终标准化用量;缺失字段表示宿主没有该项事实。 +/// 用量观察取得的最终标准化用量;缺失字段表示宿主没有该项事实 #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct RequestUsage { @@ -165,11 +165,11 @@ pub struct RequestUsage { pub failure: Option, } -/// 一次完成事件包含宿主已确认的全部终态、用量、费用与耗时。 +/// 一次完成事件包含宿主已确认的全部终态、用量、费用与耗时 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct RequestCompleted { - /// 不重投时仍保持稳定,供插件自行去重和关联日志。 + /// 不重投时仍保持稳定,供插件自行去重和关联日志 pub event_id: String, pub request_id: String, pub config_revision: u64, @@ -178,10 +178,10 @@ pub struct RequestCompleted { pub client_key_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub account_id: Option, - /// 本次实际选中账号使用的上游模型,未进入上游时可能不存在。 + /// 本次实际选中账号使用的上游模型,未进入上游时可能不存在 #[serde(default, skip_serializing_if = "Option::is_none")] pub upstream_model: Option, - /// 上游响应报告的模型,与请求中的模型名称分开保存。 + /// 上游响应报告的模型,与请求中的模型名称分开保存 #[serde(default, skip_serializing_if = "Option::is_none")] pub response_model: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -195,10 +195,10 @@ pub struct RequestCompleted { pub usage: RequestUsage, } -/// 实际上游 WebSocket 响应事件。 +/// 实际上游 WebSocket 响应事件 /// /// 原始 JSON 在 RPC 二进制载荷中独立传递;`payload_included` 表示本次 -/// 事件是否包含原始字节,元数据不能替代原始载荷。 +/// 事件是否包含原始字节,元数据不能替代原始载荷 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct WebSocketResponse { @@ -209,15 +209,15 @@ pub struct WebSocketResponse { pub protocol: String, pub provider: String, pub attempt_index: u32, - /// 请求内从 1 开始单调递增,切换 attempt 时不重置;有界投递可能产生间隔。 + /// 请求内从 1 开始单调递增,切换 attempt 时不重置;有界投递可能产生间隔 pub sequence: u64, pub payload_included: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub requested_model: Option, - /// 实际 attempt 选择的账号。 + /// 实际 attempt 选择的账号 #[serde(default, skip_serializing_if = "Option::is_none")] pub account_id: Option, - /// 事件名属于原始响应内容,只在 `payload_included=true` 时提供。 + /// 事件名属于原始响应内容,只在 `payload_included=true` 时提供 #[serde(default, skip_serializing_if = "Option::is_none")] pub event_type: Option, } diff --git a/backend/crates/gateway-plugin/sdk/src/call/policy.rs b/backend/crates/gateway-plugin/sdk/src/call/policy.rs index f9ada5b3c..b1ee4932f 100644 --- a/backend/crates/gateway-plugin/sdk/src/call/policy.rs +++ b/backend/crates/gateway-plugin/sdk/src/call/policy.rs @@ -1,10 +1,10 @@ -//! 路由、调度与重试调用的跨进程数据合同。 +//! 路由、调度与重试调用的跨进程数据合同 use serde::{Deserialize, Serialize}; use crate::SendState; -/// `retry` 只继续宿主已选定的恢复路径,不能改账号、延迟或预算。 +/// `retry` 只继续宿主已选定的恢复路径,不能改账号、延迟或预算 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum RetryAction { @@ -12,7 +12,7 @@ pub enum RetryAction { Retry, } -/// 重试输入只包含安全事实,不携带上游错误正文、凭据或请求内容。 +/// 重试输入只包含安全事实,不携带上游错误正文、凭据或请求内容 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct RetryDecisionRequest { @@ -36,7 +36,7 @@ pub enum RetryDecision { Retry, } -/// 策略调用的完整 HTTP 头;值使用 base64 保留非 UTF-8 字节。 +/// 策略调用的完整 HTTP 头;值使用 base64 保留非 UTF-8 字节 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct PolicyHeader { @@ -44,7 +44,7 @@ pub struct PolicyHeader { pub value_base64: String, } -/// 模型路由调用;原始正文在 RPC 二进制载荷中独立传递。 +/// 模型路由调用;原始正文在 RPC 二进制载荷中独立传递 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ModelRouteRequest { @@ -58,7 +58,7 @@ pub struct ModelRouteRequest { pub headers: Vec, } -/// 模型路由决定;宿主仍会复核目标、Key 权限和模型能力。 +/// 模型路由决定;宿主仍会复核目标、Key 权限和模型能力 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "decision", rename_all = "snake_case", deny_unknown_fields)] pub enum ModelRouteDecision { @@ -72,7 +72,7 @@ pub enum ModelRouteDecision { Reject, } -/// 候选账号的即时调度指标;凭据和账号资料通过宿主账号接口读取。 +/// 候选账号的即时调度指标;凭据和账号资料通过宿主账号接口读取 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct AccountScheduleCandidate { @@ -92,7 +92,7 @@ pub struct AccountScheduleCandidate { pub first_output_latency_ms: Option, } -/// 账号调度输入;候选账号及即时调度指标由宿主冻结。 +/// 账号调度输入;候选账号及即时调度指标由宿主冻结 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct AccountScheduleRequest { @@ -104,7 +104,7 @@ pub struct AccountScheduleRequest { pub candidates: Vec, } -/// 账号调度结果;账号租约始终由宿主在决定返回后取得。 +/// 账号调度结果;账号租约始终由宿主在决定返回后取得 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "decision", rename_all = "snake_case", deny_unknown_fields)] pub enum AccountScheduleDecision { diff --git a/backend/crates/gateway-plugin/sdk/src/call/registration.rs b/backend/crates/gateway-plugin/sdk/src/call/registration.rs index 1f9596a7c..8c26731a1 100644 --- a/backend/crates/gateway-plugin/sdk/src/call/registration.rs +++ b/backend/crates/gateway-plugin/sdk/src/call/registration.rs @@ -1,4 +1,4 @@ -//! 插件注册只回传规范化的能力声明。 +//! 插件注册只回传规范化的能力声明 use serde::{Deserialize, Serialize}; diff --git a/backend/crates/gateway-plugin/sdk/src/call/resources.rs b/backend/crates/gateway-plugin/sdk/src/call/resources.rs index 77515c902..11490d1c5 100644 --- a/backend/crates/gateway-plugin/sdk/src/call/resources.rs +++ b/backend/crates/gateway-plugin/sdk/src/call/resources.rs @@ -1,4 +1,4 @@ -//! 实例自有资源:稳定 resource_key 用于重试,名称只在首次创建时使用。 +//! 实例自有资源:稳定 resource_key 用于重试,名称只在首次创建时使用 use serde::{Deserialize, Serialize}; @@ -31,7 +31,7 @@ pub struct GroupMembersChange { pub struct KeyEnsureRequest { pub resource_key: String, pub name: String, - /// 必须包含至少一个本实例分组,不能创建不受分组约束的 Key。 + /// 必须包含至少一个本实例分组,不能创建不受分组约束的 Key pub group_resource_keys: Vec, #[serde(default)] pub max_concurrency: u64, @@ -41,7 +41,7 @@ pub struct KeyEnsureRequest { pub weekly_limit_usd: String, } -/// 返回非秘密身份;调用模型时使用 id,密钥明文仍由管理员管理。 +/// 返回非秘密身份;调用模型时使用 id,密钥明文仍由管理员管理 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ManagedResource { diff --git a/backend/crates/gateway-plugin/sdk/src/call/services/mod.rs b/backend/crates/gateway-plugin/sdk/src/call/services/mod.rs index d531eb744..1142c75e0 100644 --- a/backend/crates/gateway-plugin/sdk/src/call/services/mod.rs +++ b/backend/crates/gateway-plugin/sdk/src/call/services/mod.rs @@ -1,4 +1,4 @@ -//! 公开服务的类型化操作合同;名称用于分派,不承担授权。 +//! 公开服务的类型化操作合同;名称用于分派,不承担授权 pub mod settings; @@ -37,7 +37,7 @@ impl std::error::Error for ServiceError {} pub type Response = Result; -/// 洋葱层保留父子调用标识;input 由 Operation 对应的类型解释。 +/// 洋葱层保留父子调用标识;input 由 Operation 对应的类型解释 #[derive(Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Call { diff --git a/backend/crates/gateway-plugin/sdk/src/call/services/settings.rs b/backend/crates/gateway-plugin/sdk/src/call/services/settings.rs index 14d282235..39b3edb02 100644 --- a/backend/crates/gateway-plugin/sdk/src/call/services/settings.rs +++ b/backend/crates/gateway-plugin/sdk/src/call/services/settings.rs @@ -1,4 +1,7 @@ -// 从宿主设置类型与 service/settings.rs 生成;更新命令见 SDK 维护说明。 +//! 宿主设置服务的操作标识与请求、响应数据合同 +//! +//! 从宿主设置类型与 service/settings.rs 生成;更新命令见 SDK 维护说明 + use super::Operation; use serde::{Deserialize, Serialize}; use std::{ @@ -51,9 +54,9 @@ pub struct RuntimeSettings { #[derive(Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ReplaceRuntimeSettings { - /// 读取设置时的版本;与写入在同一事务内比较,防止覆盖并发更新。 + /// 读取设置时的版本;与写入在同一事务内比较,防止覆盖并发更新 pub expected_revision: Revision, - /// 只覆盖提交的 Provider;未提交项保留当前持久值。 + /// 只覆盖提交的 Provider;未提交项保留当前持久值 pub request_profile_updates: ProviderRequestProfileUpdates, pub request_location_enabled: bool, pub request_location: RequestLocation, diff --git a/backend/crates/gateway-plugin/sdk/src/call/upstream_adapter/codec.rs b/backend/crates/gateway-plugin/sdk/src/call/upstream_adapter/codec.rs index add9502ea..a8f0c47f1 100644 --- a/backend/crates/gateway-plugin/sdk/src/call/upstream_adapter/codec.rs +++ b/backend/crates/gateway-plugin/sdk/src/call/upstream_adapter/codec.rs @@ -1,3 +1,5 @@ +//! 上游适配请求与事件的元数据、正文及续接信息编解码 + use serde::{Deserialize, Serialize}; use super::{UpstreamAdapterEvent, UpstreamAdapterRequest, UpstreamContinuation, UpstreamFailure}; diff --git a/backend/crates/gateway-plugin/sdk/src/call/upstream_adapter/mod.rs b/backend/crates/gateway-plugin/sdk/src/call/upstream_adapter/mod.rs index 796f64f91..654f1a941 100644 --- a/backend/crates/gateway-plugin/sdk/src/call/upstream_adapter/mod.rs +++ b/backend/crates/gateway-plugin/sdk/src/call/upstream_adapter/mod.rs @@ -1,4 +1,4 @@ -//! 内置 OpenAI / xAI 账号上的上游转换合同,凭据不进入此线协议。 +//! 内置 OpenAI / xAI 账号上的上游转换合同,凭据不进入此线协议 use serde::{Deserialize, Serialize}; @@ -46,7 +46,7 @@ impl UpstreamTransport { } } -/// 注册结果随实例配置冻结;同一实例可为两个内置 Provider 分别声明适配器。 +/// 注册结果随实例配置冻结;同一实例可为两个内置 Provider 分别声明适配器 #[derive(Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct UpstreamAdapterRegistration { @@ -59,13 +59,13 @@ pub struct UpstreamAdapterDeclaration { pub id: String, pub provider: BuiltinProvider, pub base_url: String, - /// 相对 Base URL 的精确业务路径,不接受绝对 URL、查询串或通配符。 + /// 相对 Base URL 的精确业务路径,不接受绝对 URL、查询串或通配符 pub paths: Vec, pub authentication_kinds: Vec, pub transport: UpstreamTransport, - /// 客户端请求及适配完成后的 wire 协议;标准事实独立于 wire 传递。 + /// 客户端请求及适配完成后的 wire 协议;标准事实独立于 wire 传递 pub protocol: String, - /// 公开请求模型的匹配范围;空集合沿用功能绑定范围。 + /// 公开请求模型的匹配范围;空集合沿用功能绑定范围 #[serde(default)] pub models: Vec, } @@ -84,7 +84,7 @@ pub struct UpstreamPath { pub purpose: UpstreamPathPurpose, } -/// 元数据为宿主只读投影;请求正文独立置于调用二进制载荷。 +/// 元数据为宿主只读投影;请求正文独立置于调用二进制载荷 #[derive(Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct UpstreamAdapterRequest { @@ -96,15 +96,15 @@ pub struct UpstreamAdapterRequest { pub credential_revision: u64, pub protocol: String, pub client_transport: String, - /// 宿主冻结的强制策略;自定义上游编码同样不得请求 Fast / priority 档位。 + /// 宿主冻结的强制策略;自定义上游编码同样不得请求 Fast / priority 档位 pub disable_fast: bool, pub headers: Vec<(String, Vec)>, - /// 只能来自同账号、同插件代次的宿主续接记录,不从客户端正文信任此值。 + /// 只能来自同账号、同插件代次的宿主续接记录,不从客户端正文信任此值 pub continuation: Option, } impl UpstreamAdapterRequest { - /// 请求身份、业务头与私有续接材料同原始正文一起放入二进制载荷。 + /// 请求身份、业务头与私有续接材料同原始正文一起放入二进制载荷 pub fn encode(self, body: Vec) -> Result, super::model::ExecutionEncodingError> { codec::encode_request(self, body) } @@ -126,14 +126,14 @@ pub enum ContinuationScope { pub struct UpstreamContinuation { pub scope: ContinuationScope, pub upstream_response_id: String, - /// 适配器私有的续接材料,不包含账号凭据。 + /// 适配器私有的续接材料,不包含账号凭据 pub state: serde_json::Map, } -/// 一个完整 GPE2 事件与可选的结算/续接事实;见 [`Self::encode`]。 +/// 一个完整 GPE2 事件与可选的结算/续接事实;见 [`Self::encode`] pub struct UpstreamAdapterEvent { pub event: ExecutionEvent, - /// 上游回显的最新服务档位,可在终态确定;是否参与计价由内置 Provider 决定。 + /// 上游回显的最新服务档位,可在终态确定;是否参与计价由内置 Provider 决定 pub service_tier: Option, pub continuation: Option, pub failure: Option, @@ -150,12 +150,12 @@ impl UpstreamAdapterEvent { } } - /// 编码有界事件,wire 原字节仍使用 GPE2 二进制段。 + /// 编码有界事件,wire 原字节仍使用 GPE2 二进制段 pub fn encode(self) -> Result, super::model::ExecutionEncodingError> { codec::encode(self) } - /// 校验版本、长度及元数据,再解码事件载荷。 + /// 校验版本、长度及元数据,再解码事件载荷 pub fn decode(bytes: &[u8]) -> Result { codec::decode(bytes) } @@ -175,7 +175,7 @@ pub enum UpstreamFailureKind { Protocol, } -/// 解析后的上游拒绝事实;发送状态始终由宿主网络观测,不接受插件声明。 +/// 解析后的上游拒绝事实;发送状态始终由宿主网络观测,不接受插件声明 #[derive(Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct UpstreamFailure { @@ -212,7 +212,7 @@ pub enum WebSocketMessageKind { Binary, } -/// 消息数据独立放入二进制载荷,宿主处理 Ping/Pong 与关闭握手。 +/// 消息数据独立放入二进制载荷,宿主处理 Ping/Pong 与关闭握手 #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct UpstreamWebSocketMessage { diff --git a/backend/crates/gateway-plugin/sdk/src/capability.rs b/backend/crates/gateway-plugin/sdk/src/capability.rs index 980eb0ad1..b1356f2b0 100644 --- a/backend/crates/gateway-plugin/sdk/src/capability.rs +++ b/backend/crates/gateway-plugin/sdk/src/capability.rs @@ -1,8 +1,10 @@ +//! 插件能力、调用阶段、失败策略与贡献声明的线协议定义 + use std::{collections::BTreeMap, fmt}; use serde::{Deserialize, Deserializer, Serialize, de}; -/// 能力描述插件提供的处理器,不承担宿主资源授权。 +/// 能力描述插件提供的处理器,不承担宿主资源授权 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum Capability { @@ -20,7 +22,7 @@ pub enum Capability { } impl Capability { - /// SDK 能描述的行为合同版本;具体宿主可以只开放其中一部分。 + /// SDK 能描述的行为合同版本;具体宿主可以只开放其中一部分 #[must_use] pub const fn contract_versions(self) -> &'static [u32] { match self { @@ -38,7 +40,7 @@ impl Capability { } } - /// 稳定能力标识;默认扩展项 ID 由它派生,不受显示名称影响。 + /// 稳定能力标识;默认扩展项 ID 由它派生,不受显示名称影响 #[must_use] pub const fn identifier(self) -> &'static str { match self { @@ -56,7 +58,7 @@ impl Capability { } } - /// 固定调用阶段;只有中间件需要作者显式选择挂载边界。 + /// 固定调用阶段;只有中间件需要作者显式选择挂载边界 #[must_use] pub const fn fixed_stages(self) -> &'static [Stage] { match self { @@ -75,7 +77,7 @@ impl Capability { } } -/// 调用阶段描述当前处理器位置,不限定插件访问宿主资源。 +/// 调用阶段描述当前处理器位置,不限定插件访问宿主资源 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum Stage { @@ -91,14 +93,14 @@ pub enum Stage { WebSocket, Request, Attempt, - /// Core 登记已选账号的 attempt 后才启动的受管上游执行。 + /// Core 登记已选账号的 attempt 后才启动的受管上游执行 Upstream, Observation, Management, CommandLine, - /// 未登录客户端触发的插件管理调用。 + /// 未登录客户端触发的插件管理调用 PublicManagement, - /// 宿主针对已发布实例签发的幂等维护调用。 + /// 宿主针对已发布实例签发的幂等维护调用 Maintenance, } @@ -129,7 +131,7 @@ const fn default_capability_version() -> u32 { 1 } -/// 插件按能力标识索引的扩展项声明;每种能力至多声明一个处理器。 +/// 插件按能力标识索引的扩展项声明;每种能力至多声明一个处理器 pub type Contributions = BTreeMap; pub(crate) fn deserialize_contributions<'de, D>(deserializer: D) -> Result diff --git a/backend/crates/gateway-plugin/sdk/src/client/data.rs b/backend/crates/gateway-plugin/sdk/src/client/data.rs index 2a9a4c11b..fea28d541 100644 --- a/backend/crates/gateway-plugin/sdk/src/client/data.rs +++ b/backend/crates/gateway-plugin/sdk/src/client/data.rs @@ -1,13 +1,15 @@ +//! 插件查询账号、Key 与额度事实的类型化宿主客户端 + use crate::{PluginFault, call::data}; use super::{HostClient, payload_call}; impl HostClient { - /// 读取 Key 当前启用状态与显式分组绑定,不返回密钥。 + /// 读取 Key 当前启用状态与显式分组绑定,不返回密钥 /// /// # Errors /// - /// Key 不存在或宿主读取失败时返回错误。 + /// Key 不存在或宿主读取失败时返回错误 pub async fn key_facts( &self, query: data::ClientKeyFactsQuery, @@ -15,11 +17,11 @@ impl HostClient { payload_call(self, data::KEYS_GET, query).await } - /// 通过宿主刷新账号额度观测,返回与 quota_facts 相同的非秘密投影。 - /// 不修改上游额度,也不自动重置任何 Key。 + /// 通过宿主刷新账号额度观测,返回与 quota_facts 相同的非秘密投影 + /// 不修改上游额度,也不自动重置任何 Key /// /// # Errors - /// 账号不支持刷新或 Provider 查询失败时返回错误。 + /// 账号不支持刷新或 Provider 查询失败时返回错误 pub async fn refresh_account_quota( &self, query: data::QuotaFactsQuery, @@ -27,11 +29,11 @@ impl HostClient { payload_call(self, data::QUOTA_REFRESH, query).await } - /// 分页读取账号基础事实。 + /// 分页读取账号基础事实 /// /// # Errors /// - /// 参数无效或宿主读取失败时返回错误。 + /// 参数无效或宿主读取失败时返回错误 pub async fn account_facts( &self, query: data::AccountFactsQuery, @@ -39,11 +41,11 @@ impl HostClient { payload_call(self, data::ACCOUNTS_LIST, query).await } - /// 读取已有额度观测,不触发上游刷新,也不返回宿主预测结果。 + /// 读取已有额度观测,不触发上游刷新,也不返回宿主预测结果 /// /// # Errors /// - /// 账号不存在或宿主读取失败时返回错误。 + /// 账号不存在或宿主读取失败时返回错误 pub async fn quota_facts( &self, query: data::QuotaFactsQuery, diff --git a/backend/crates/gateway-plugin/sdk/src/client/frame.rs b/backend/crates/gateway-plugin/sdk/src/client/frame.rs index c355597f5..e7e666216 100644 --- a/backend/crates/gateway-plugin/sdk/src/client/frame.rs +++ b/backend/crates/gateway-plugin/sdk/src/client/frame.rs @@ -1,3 +1,5 @@ +//! 插件消息帧的异步读写与元数据、载荷长度校验 + use tokio::io::{AsyncRead, AsyncReadExt as _, AsyncWrite, AsyncWriteExt as _}; use crate::{Frame, FrameError}; @@ -5,7 +7,7 @@ use crate::{Frame, FrameError}; const MAX_METADATA_BYTES: usize = 64 * 1024; const IO_CHUNK_BYTES: usize = 256 * 1024; -/// 读取完整消息;I/O 分块是传输细节,不限制业务正文总量。 +/// 读取完整消息;I/O 分块是传输细节,不限制业务正文总量 pub async fn read_frame(reader: &mut R) -> Result { let metadata_len = reader.read_u32().await? as usize; let payload_len = usize::try_from(reader.read_u64().await?).map_err(|_| FrameError::Length)?; @@ -14,7 +16,7 @@ pub async fn read_frame(reader: &mut R) -> Result( Ok(()) } -/// 入队前校验本地元数据,避免单次编码失败中断共享传输。 +/// 入队前校验本地元数据,避免单次编码失败中断共享传输 pub fn validate_frame(frame: &Frame) -> Result<(), FrameError> { let metadata = serde_json::to_vec(&frame.message).map_err(|_| FrameError::Metadata)?; check_metadata_length(metadata.len()) diff --git a/backend/crates/gateway-plugin/sdk/src/client/http.rs b/backend/crates/gateway-plugin/sdk/src/client/http.rs index fde747614..10d78dfb1 100644 --- a/backend/crates/gateway-plugin/sdk/src/client/http.rs +++ b/backend/crates/gateway-plugin/sdk/src/client/http.rs @@ -1,4 +1,4 @@ -//! HTTP 正文句柄封装;普通出站和已选账号出站共用惰性读取与提前关闭语义。 +//! HTTP 正文句柄封装;普通出站和已选账号出站共用惰性读取与提前关闭语义 use serde::{Serialize, de::DeserializeOwned}; @@ -8,14 +8,14 @@ use crate::{ call::{host as wire, upstream_adapter::UpstreamHttpRequest}, }; -/// 响应头与惰性正文;不包含宿主的内部流句柄。 +/// 响应头与惰性正文;不包含宿主的内部流句柄 pub struct HostHttpResponse { pub status: u16, pub headers: Vec<(String, String)>, pub body: HostHttpBody, } -/// 父调用内的单消费者正文;不后台预读,父调用结束或取消时宿主兜底回收。 +/// 父调用内的单消费者正文;不后台预读,父调用结束或取消时宿主兜底回收 pub struct HostHttpBody { host: HostClient, stream: Option, @@ -24,10 +24,10 @@ pub struct HostHttpBody { } impl HostClient { - /// 使用受管 HTTP 发送请求;账号上游使用 [`Self::upstream_http`]。 + /// 使用受管 HTTP 发送请求;账号上游使用 [`Self::upstream_http`] /// /// # Errors - /// 期限、请求或网络无效时失败。 + /// 期限、请求或网络无效时失败 pub async fn http( &self, request: wire::HttpRequest, @@ -37,10 +37,10 @@ impl HostClient { .await } - /// 由宿主附加已选账号的认证与代理,返回统一的惰性正文。 + /// 由宿主附加已选账号的认证与代理,返回统一的惰性正文 /// /// # Errors - /// 路径、期限、网络或宿主响应无效时失败。 + /// 路径、期限、网络或宿主响应无效时失败 pub async fn upstream_http( &self, request: UpstreamHttpRequest, @@ -76,11 +76,11 @@ impl HostClient { } impl HostHttpBody { - /// 按需读取至多 64 KiB,EOF 后再次读取不再调用宿主。 - /// 取消一次等待后,再次调用会继续同一次读取,不丢弃分块。 + /// 按需读取至多 64 KiB,EOF 后再次读取不再调用宿主 + /// 取消一次等待后,再次调用会继续同一次读取,不丢弃分块 /// /// # Errors - /// 父调用结束、流无效或网络失败时返回错误。 + /// 父调用结束、流无效或网络失败时返回错误 pub async fn read(&mut self) -> Result>, PluginFault> { let Some(stream) = &self.stream else { return Ok(None); @@ -120,10 +120,10 @@ impl HostHttpBody { } } - /// 收集有限正文;超出调用方预算或读取失败时提前关闭,不把流无限缓冲到内存。 + /// 收集有限正文;超出调用方预算或读取失败时提前关闭,不把流无限缓冲到内存 /// /// # Errors - /// 读取失败或超出 `maximum_bytes` 时返回错误。 + /// 读取失败或超出 `maximum_bytes` 时返回错误 pub async fn collect(mut self, maximum_bytes: usize) -> Result, PluginFault> { let mut body = Vec::new(); loop { @@ -145,10 +145,10 @@ impl HostHttpBody { } } - /// 提前释放正文,重复关闭或 EOF 后关闭不再调用宿主。 + /// 提前释放正文,重复关闭或 EOF 后关闭不再调用宿主 /// /// # Errors - /// 父调用已结束或宿主拒绝释放时失败。 + /// 父调用已结束或宿主拒绝释放时失败 pub async fn close(&mut self) -> Result<(), PluginFault> { let Some(stream) = &self.stream else { return Ok(()); diff --git a/backend/crates/gateway-plugin/sdk/src/client/keys.rs b/backend/crates/gateway-plugin/sdk/src/client/keys.rs index c912a0118..bc3feb2d8 100644 --- a/backend/crates/gateway-plugin/sdk/src/client/keys.rs +++ b/backend/crates/gateway-plugin/sdk/src/client/keys.rs @@ -1,4 +1,4 @@ -//! 非秘密 Key 目录与预算管理的类型化调用。 +//! 非秘密 Key 目录与预算管理的类型化调用 use crate::{ ErrorCode, PluginFault, @@ -11,10 +11,10 @@ use crate::{ use super::{HostClient, SessionError, payload_call}; impl HostClient { - /// 只读查询单个 Key 的预算,不开启或重置窗口。 + /// 只读查询单个 Key 的预算,不开启或重置窗口 /// /// # Errors - /// Key 不存在或宿主读取失败时返回错误。 + /// Key 不存在或宿主读取失败时返回错误 pub async fn get_key_budget( &self, request: key_budgets::GetKeyBudgetRequest, @@ -22,10 +22,10 @@ impl HostClient { payload_call(self, key_budgets::GET, request).await } - /// 更新指定日/周上限;省略项不变,零表示不限,不清零用量。 + /// 更新指定日/周上限;省略项不变,零表示不限,不清零用量 /// /// # Errors - /// 参数无效、Key 不存在或宿主写入失败时返回错误。 + /// 参数无效、Key 不存在或宿主写入失败时返回错误 pub async fn update_key_budget_limits( &self, request: key_budgets::UpdateKeyBudgetLimitsRequest, @@ -33,10 +33,10 @@ impl HostClient { payload_call(self, key_budgets::UPDATE_LIMITS, request).await } - /// 查询 Key 的非秘密身份;调用与父资源保持关联。 + /// 查询 Key 的非秘密身份;调用与父资源保持关联 /// /// # Errors - /// 分页参数不合法,或者宿主读取失败时返回错误。 + /// 分页参数不合法,或者宿主读取失败时返回错误 pub async fn list_keys(&self, query: KeyListRequest) -> Result { let invalid = || PluginFault::new(ErrorCode::InvalidInput, "invalid key list payload"); let reply = self @@ -53,10 +53,10 @@ impl HostClient { serde_json::from_value(reply.result).map_err(|_| invalid()) } - /// 清零并关闭指定周期,保留限额,下次使用时重新开启;结果未知时不能盲目重试。 + /// 清零并关闭指定周期,保留限额,下次使用时重新开启;结果未知时不能盲目重试 /// /// # Errors - /// 实例过期、Key 不存在、宿主写入失败时返回错误。 + /// 实例过期、Key 不存在、宿主写入失败时返回错误 pub async fn reset_key_budget( &self, request: key_budgets::ResetKeyBudgetRequest, diff --git a/backend/crates/gateway-plugin/sdk/src/client/middleware/body.rs b/backend/crates/gateway-plugin/sdk/src/client/middleware/body.rs index 7bf03d16f..c2097cc55 100644 --- a/backend/crates/gateway-plugin/sdk/src/client/middleware/body.rs +++ b/backend/crates/gateway-plugin/sdk/src/client/middleware/body.rs @@ -1,3 +1,5 @@ +//! 插件中间件正文的读取、转换、发送与宿主句柄回收 + use std::{collections::VecDeque, num::NonZeroUsize}; use crate::{ @@ -19,7 +21,7 @@ use crate::client::read::PendingRead; const MAXIMUM_MAPPED_FRAMES_PER_SOURCE: usize = 64; const MAXIMUM_MAPPED_BYTES_PER_SOURCE: usize = 8 * 1024 * 1024; -/// 惰性正文的一次完整 frame。 +/// 惰性正文的一次完整 frame pub struct MiddlewareBody { source: MiddlewareBodySource, } @@ -48,7 +50,7 @@ impl MiddlewareBody { } } - /// 从已编码完整 frame 构造有界插件输出流。 + /// 从已编码完整 frame 构造有界插件输出流 #[must_use] pub fn from_frames(framing: MiddlewareBodyFraming, frames: Vec) -> Self { Self { @@ -64,7 +66,7 @@ impl MiddlewareBody { } } - /// 创建插件生产的有界正文流。 + /// 创建插件生产的有界正文流 #[must_use] pub fn channel( framing: MiddlewareBodyFraming, @@ -88,9 +90,10 @@ impl MiddlewareBody { } } - /// 拉取下一完整 frame。调用后该 opaque handle 不能再作为未读直通返回。 - /// 只读观察并原样交付时使用 [`Self::inspect_frames`],由 SDK 保留源帧。 - /// 取消等待后可继续同一次读取,包括尚未完成的事实快照读取。 + /// 拉取下一完整 frame + /// 调用后该 opaque handle 不能再作为未读直通返回 + /// 只读观察并原样交付时使用 [`Self::inspect_frames`],由 SDK 保留源帧 + /// 取消等待后可继续同一次读取,包括尚未完成的事实快照读取 pub async fn read(&mut self) -> Result, PluginFault> { let MiddlewareBodySource::Host { body, @@ -116,8 +119,8 @@ impl MiddlewareBody { Ok(frame) } - /// 在读取或映射每个源帧时取得完整宿主事实,普通透传不产生额外快照开销。 - /// 必须在 `map_frames` / `inspect_frames` 前调用。 + /// 在读取或映射每个源帧时取得完整宿主事实,普通透传不产生额外快照开销 + /// 必须在 `map_frames` / `inspect_frames` 前调用 pub fn with_facts(mut self) -> Result { match &mut self.source { MiddlewareBodySource::Host { include_facts, .. } => *include_facts = true, @@ -127,7 +130,8 @@ impl MiddlewareBody { Ok(self) } - /// 提前关闭宿主正文句柄。父调用取消或结束也会由 Runtime 兜底回收。 + /// 提前关闭宿主正文句柄 + /// 父调用取消或结束也会由 Runtime 兜底回收 pub async fn close(&mut self) -> Result<(), PluginFault> { let MiddlewareBodySource::Host { body, host, .. } = &self.source else { self.source = MiddlewareBodySource::Empty; @@ -153,14 +157,14 @@ impl MiddlewareBody { Ok(()) } - /// 按下游 Credit 只读观察完整 frame,并原样交付其字节、顺序和源终态。 + /// 按下游 Credit 只读观察完整 frame,并原样交付其字节、顺序和源终态 /// - /// 不预读或重编码正文;闭包只取得借用,不能改写正在交付的帧。 - /// 与映射共用取消和背压边界,观察工作应保持有界。 + /// 不预读或重编码正文;闭包只取得借用,不能改写正在交付的帧 + /// 与映射共用取消和背压边界,观察工作应保持有界 /// /// # Errors /// - /// 正文来自插件自建流时返回错误;空正文直接保留。 + /// 正文来自插件自建流时返回错误;空正文直接保留 pub fn inspect_frames(self, mut inspect: F) -> Result where F: FnMut(&MiddlewareBodyFrame) + Send + 'static, @@ -171,10 +175,11 @@ impl MiddlewareBody { }) } - /// 按下游 Credit 惰性拉取并映射完整 frame;闭包可在单次调用内持有状态。 + /// 按下游 Credit 惰性拉取并映射完整 frame;闭包可在单次调用内持有状态 /// - /// 每个输入可生成零个、一个或多个输出。SDK 不预读或另起后台任务;Runtime - /// 继续复核 frame 大小、源终态与输出终态的一致性。 + /// 每个输入可生成零个、一个或多个输出 + /// SDK 不预读或另起后台任务;Runtime + /// 继续复核 frame 大小、源终态与输出终态的一致性 pub fn map_frames(self, transform: F) -> Result where F: FnMut(MiddlewareBodyFrame) -> Result, PluginFault> @@ -399,7 +404,7 @@ where } } -/// 插件输出正文的有界生产端;每个发送项必须是完整 frame。 +/// 插件输出正文的有界生产端;每个发送项必须是完整 frame #[derive(Clone)] pub struct MiddlewareBodySender { sender: StreamSender, diff --git a/backend/crates/gateway-plugin/sdk/src/client/middleware/call.rs b/backend/crates/gateway-plugin/sdk/src/client/middleware/call.rs index 4bc9bd60f..9e3df564a 100644 --- a/backend/crates/gateway-plugin/sdk/src/client/middleware/call.rs +++ b/backend/crates/gateway-plugin/sdk/src/client/middleware/call.rs @@ -1,4 +1,4 @@ -//! 多边界插件只注册一次处理器,按类型选择当前调用视图。 +//! 多边界插件只注册一次处理器,按类型选择当前调用视图 use super::super::session::{CallReply, PluginCall}; use super::{ HttpCall, HttpResponse, MiddlewareInput, MiddlewareOutput, MiddlewareResponse, RequestCall, @@ -28,7 +28,7 @@ impl MiddlewareCall { Self::Request(call) => &call.context, } } - /// 原样进入下游,供当前处理器不感兴趣的调用复用。 + /// 原样进入下游,供当前处理器不感兴趣的调用复用 pub async fn forward(self) -> Result { match self { Self::Service(call) => call.forward().await.map(MiddlewareResult::Service), diff --git a/backend/crates/gateway-plugin/sdk/src/client/middleware/http.rs b/backend/crates/gateway-plugin/sdk/src/client/middleware/http.rs index 47adeb213..fd4a1a29a 100644 --- a/backend/crates/gateway-plugin/sdk/src/client/middleware/http.rs +++ b/backend/crates/gateway-plugin/sdk/src/client/middleware/http.rs @@ -1,3 +1,5 @@ +//! 插件 HTTP 中间件的类型化请求、响应、正文流与后续调用接口 + use std::{collections::VecDeque, future::Future, pin::Pin}; use super::super::session::{ @@ -14,7 +16,7 @@ use crate::{ client::{HostReply, read::PendingRead}, }; -/// HTTP 数据帧与 trailers 分开表达;结束由流完成表示,不插入协议正文。 +/// HTTP 数据帧与 trailers 分开表达;结束由流完成表示,不插入协议正文 pub enum HttpFrame { Data(Vec), Trailers(Vec), @@ -38,7 +40,7 @@ impl HttpFrame { } } -/// HTTP 正文惰性读取;透传直接返还句柄,完整收集由插件自行决定。 +/// HTTP 正文惰性读取;透传直接返还句柄,完整收集由插件自行决定 pub struct HttpBody(Source); type Producer = Pin> + Send>>; @@ -158,7 +160,7 @@ impl HttpBody { Some(headers) if reply.payload.is_empty() => Ok(Some(HttpFrame::Trailers(headers))), Some(_) => Err(invalid_input()), None => { - // 读取可能早于初始 Credit 发出;返回路径仍须遵守实际窗口,并为帧标记留一字节。 + // 读取可能早于初始 Credit 发出;返回路径仍须遵守实际窗口,并为帧标记留一字节 let maximum = host.maximum_stream_chunk_bytes().saturating_sub(1); if maximum == 0 { return Err(invalid_input()); @@ -196,7 +198,8 @@ impl HttpBody { Ok(()) } - /// 按输出背压逐帧转换;None 丢弃一帧。需要展开时可使用自定义 ResponseStream。 + /// 按输出背压逐帧转换;None 丢弃一帧 + /// 需要展开时可使用自定义 ResponseStream pub fn map_frames(self, transform: F) -> Self where F: FnMut(HttpFrame) -> Result, PluginFault> + Send + 'static, @@ -328,7 +331,7 @@ async fn write_body( .await { Ok(reply) if reply.result == serde_json::json!({}) && reply.payload.is_empty() => Ok(true), - // 下游允许在读取完整请求前返回,例如拒绝认证或直接响应。 + // 下游允许在读取完整请求前返回,例如拒绝认证或直接响应 Err(SessionError::Remote(fault)) if fault.code == crate::ErrorCode::Conflict => Ok(false), Err(error) => Err(error.into_plugin_fault()), _ => Err(invalid_input()), @@ -379,7 +382,7 @@ impl HttpNext { } impl HostClient { - /// 主动调用宿主相对路径,经过完整 HTTP 洋葱链并继承父调用的取消与防递归上下文。 + /// 主动调用宿主相对路径,经过完整 HTTP 洋葱链并继承父调用的取消与防递归上下文 pub async fn dispatch_http(&self, request: HttpRequest) -> Result { send_request(self, wire::DISPATCH_METHOD, request).await } @@ -517,7 +520,7 @@ impl PullResponseStream for SessionTask { } impl HttpCall { - /// 在当前 HTTP 返回路径选择握手;处理函数随真实连接运行,完成或失败时回收连接。 + /// 在当前 HTTP 返回路径选择握手;处理函数随真实连接运行,完成或失败时回收连接 pub async fn upgrade( self, protocols: Vec, diff --git a/backend/crates/gateway-plugin/sdk/src/client/middleware/mod.rs b/backend/crates/gateway-plugin/sdk/src/client/middleware/mod.rs index 87d0e429a..f72700bfb 100644 --- a/backend/crates/gateway-plugin/sdk/src/client/middleware/mod.rs +++ b/backend/crates/gateway-plugin/sdk/src/client/middleware/mod.rs @@ -1,19 +1,21 @@ -mod service; -pub use service::{ServiceCall, ServiceNext, ServiceResponse, TypedServiceCall}; +//! 插件中间件的请求与响应视图,以及各调用边界的类型化接口 + mod body; mod call; -pub use call::{MiddlewareCall, MiddlewareResult}; mod http; +mod plugin; +mod service; mod websocket; + +pub use body::{MiddlewareBody, MiddlewareBodySender}; +pub use call::{MiddlewareCall, MiddlewareResult}; +pub use http::{HttpBody, HttpCall, HttpFrame, HttpNext, HttpRequest, HttpResponse}; +pub use plugin::MiddlewarePlugin; +pub use service::{ServiceCall, ServiceNext, ServiceResponse, TypedServiceCall}; pub use websocket::{ WebSocketCall, WebSocketDirection, WebSocketKind, WebSocketMessage, WebSocketNext, WebSocketPayload, WebSocketSender, WebSocketSession, }; -mod plugin; -pub use http::{HttpBody, HttpCall, HttpFrame, HttpNext, HttpRequest, HttpResponse}; - -pub use body::{MiddlewareBody, MiddlewareBodySender}; -pub use plugin::MiddlewarePlugin; use crate::{ CallContext, ErrorCode, PluginFault, Stage, @@ -26,10 +28,10 @@ use crate::{ use super::session::{CallCancellation, CallReply, HostClient, PluginCall, SessionError}; -/// 作者可读取并修改的一次中间件请求。 +/// 作者可读取并修改的一次中间件请求 /// /// 直接修改公开的 `head`/`body` 会由 [`MiddlewareNext::run`] 与原始输入比较; -/// `replace_body` 还可明确表达“替换为空正文”。 +/// `replace_body` 还可明确表达“替换为空正文” pub struct MiddlewareRequest { pub head: MiddlewareRequestHead, pub body: Vec, @@ -43,7 +45,7 @@ pub struct MiddlewareRequest { } impl MiddlewareRequest { - /// 仅 request 阶段可声明转换;须同时替换正文并负责响应还原。 + /// 仅 request 阶段可声明转换;须同时替换正文并负责响应还原 pub fn declare_capabilities( &mut self, declaration: crate::call::middleware::CapabilityDeclaration, @@ -51,13 +53,13 @@ impl MiddlewareRequest { self.capabilities = Some(declaration); } - /// 明确替换请求正文;空 Vec 也表示 replace-empty,而不是保留原正文。 + /// 明确替换请求正文;空 Vec 也表示 replace-empty,而不是保留原正文 pub fn replace_body(&mut self, body: Vec) { self.body = body; self.body_replaced = true; } - /// 删除原始集合中同名的全部 header。 + /// 删除原始集合中同名的全部 header pub fn remove_header(&mut self, name: impl Into) { let name = name.into(); self.head @@ -67,7 +69,7 @@ impl MiddlewareRequest { .push(MiddlewareHeaderMutation::Remove { name }); } - /// 在原始集合之后追加一个 header,保留同名多值。 + /// 在原始集合之后追加一个 header,保留同名多值 pub fn append_header(&mut self, name: impl Into, value: Vec) { let name = name.into(); self.head.headers.push(MiddlewareHeader { @@ -158,14 +160,15 @@ fn finish_header_mutations( mutations } -/// 绑定当前父调用的 single-use continuation。 +/// 绑定当前父调用的 single-use continuation pub struct MiddlewareNext { host: HostClient, } impl MiddlewareNext { - /// 消费 continuation 并进入链中下一层。Rust 所有权阻止安全代码重复调用; - /// Runtime 仍会拒绝恶意手写 RPC 的第二次调用。 + /// 消费 continuation 并进入链中下一层 + /// Rust 所有权阻止安全代码重复调用; + /// Runtime 仍会拒绝恶意手写 RPC 的第二次调用 pub async fn run(self, request: MiddlewareRequest) -> Result { let (request, payload) = request.into_next_parts(); let reply = self @@ -186,7 +189,7 @@ impl MiddlewareNext { } } -/// 作者返回的响应;完整 next 响应头以增量方式修改,未改写的字节保持原样。 +/// 作者返回的响应;完整 next 响应头以增量方式修改,未改写的字节保持原样 pub struct MiddlewareResponse { pub metadata: Option>, pub protocol: String, @@ -229,7 +232,7 @@ impl MiddlewareResponse { }) } - /// 创建不调用 next 的短路响应。 + /// 创建不调用 next 的短路响应 #[must_use] pub fn direct( protocol: impl Into, @@ -269,7 +272,8 @@ impl MiddlewareResponse { .push(MiddlewareHeaderMutation::Append { name, value }); } - /// 转为现有 RPC reply。即使正文 opaque 直通或为空也使用空 stream,确保单一 End。 + /// 转为现有 RPC reply + /// 即使正文 opaque 直通或为空也使用空 stream,确保单一 End pub fn into_reply(self) -> Result { if self.protocol.is_empty() || !(100..=599).contains(&self.status) { return Err(invalid_input()); @@ -292,7 +296,7 @@ impl MiddlewareResponse { } } -/// 交给中间件作者的一次真实调用。 +/// 交给中间件作者的一次真实调用 pub struct RequestCall { pub context: CallContext, pub request: MiddlewareRequest, @@ -302,7 +306,7 @@ pub struct RequestCall { } impl RequestCall { - /// 从 `middleware.handle` RPC 解码并交叉检查宿主签发的 mount/身份。 + /// 从 `middleware.handle` RPC 解码并交叉检查宿主签发的 mount/身份 pub fn try_from(call: PluginCall) -> Result { if call.method != HANDLE_METHOD { return Err(PluginFault::new( @@ -354,7 +358,7 @@ fn invalid_input() -> PluginFault { PluginFault::new(ErrorCode::InvalidInput, "middleware input is invalid") } -/// 不同公开边界提供类型化视图,注册与组合仍使用同一个 middleware 函数。 +/// 不同公开边界提供类型化视图,注册与组合仍使用同一个 middleware 函数 pub trait MiddlewareInput: Sized + Send + 'static { type Output: MiddlewareOutput; fn accepts(stage: Stage) -> bool; diff --git a/backend/crates/gateway-plugin/sdk/src/client/middleware/plugin.rs b/backend/crates/gateway-plugin/sdk/src/client/middleware/plugin.rs index e3acfea4b..6d6ef7441 100644 --- a/backend/crates/gateway-plugin/sdk/src/client/middleware/plugin.rs +++ b/backend/crates/gateway-plugin/sdk/src/client/middleware/plugin.rs @@ -1,3 +1,5 @@ +//! 将中间件处理函数适配为可注册的插件处理器 + use std::future::Future; use crate::{ @@ -10,11 +12,12 @@ use super::{ MiddlewareInput, MiddlewareOutput, RequestCall, }; -/// 单个中间件插件的作者入口;自动注册并将 RPC 分派到类型化业务函数。 +/// 单个中间件插件的作者入口;自动注册并将 RPC 分派到类型化业务函数 /// -/// 传入作者清单的完整能力列表,不从宿主握手照抄未实现的能力。此入口仅承载 -/// 一个 middleware 处理器;复合插件仍使用 [`PluginHandler`],不能虚报其他能力。 -/// 会话和生命周期分别由既有 SDK 会话与宿主负责,不在此建立第二套状态。 +/// 传入作者清单的完整能力列表,不从宿主握手照抄未实现的能力 +/// 此入口仅承载 +/// 一个 middleware 处理器;复合插件仍使用 [`PluginHandler`],不能虚报其他能力 +/// 会话和生命周期分别由既有 SDK 会话与宿主负责,不在此建立第二套状态 /// /// # Examples /// @@ -34,7 +37,7 @@ use super::{ /// request.append_header("x-team", b"research".to_vec()); /// next.run(request).await /// })?; -/// // 将 plugin 交给 PluginSession::run;请求头修改直接作用于当前调用。 +/// // 将 plugin 交给 PluginSession::run;请求头修改直接作用于当前调用 /// # Ok::<(), gateway_plugin_sdk::client::SessionError>(()) /// ``` pub struct MiddlewarePlugin { @@ -49,12 +52,12 @@ where F: Fn(C) -> Fut + Send + Sync + 'static, Fut: Future> + Send + 'static, { - /// 绑定清单中唯一的中间件声明与业务处理函数。 + /// 绑定清单中唯一的中间件声明与业务处理函数 /// /// # Errors /// - /// 空声明、其他能力、重复能力、不支持的能力版本或挂载阶段返回配置错误。 - /// 包格式与资源生命周期由宿主校验;入口不根据处理器代码推断清单。 + /// 空声明、其他能力、重复能力、不支持的能力版本或挂载阶段返回配置错误 + /// 包格式与资源生命周期由宿主校验;入口不根据处理器代码推断清单 pub fn new(contributes: &Contributions, handler: F) -> Result { let Some(declaration) = contributes.get(&Capability::Middleware) else { return Err(SessionError::Configuration); diff --git a/backend/crates/gateway-plugin/sdk/src/client/middleware/service.rs b/backend/crates/gateway-plugin/sdk/src/client/middleware/service.rs index 67eccfb93..759c49354 100644 --- a/backend/crates/gateway-plugin/sdk/src/client/middleware/service.rs +++ b/backend/crates/gateway-plugin/sdk/src/client/middleware/service.rs @@ -1,4 +1,4 @@ -//! 公开服务中间件;Operation 关联输入输出类型,无需逐操作增加 hook。 +//! 公开服务中间件;Operation 关联输入输出类型,无需逐操作增加 hook use super::{MiddlewareInput, MiddlewareOutput, invalid_input}; use crate::client::session::{CallCancellation, CallReply, HostClient, PluginCall}; @@ -42,7 +42,7 @@ pub struct ServiceNext { pub struct ServiceResponse(wire::Response); impl ServiceCall { - /// 不消费调用即可匹配其公开合同,再取得类型化视图。 + /// 不消费调用即可匹配其公开合同,再取得类型化视图 #[must_use] pub fn is(&self) -> bool { self.operation == O::NAME diff --git a/backend/crates/gateway-plugin/sdk/src/client/middleware/websocket.rs b/backend/crates/gateway-plugin/sdk/src/client/middleware/websocket.rs index 41fb5e2dd..9b02de2a1 100644 --- a/backend/crates/gateway-plugin/sdk/src/client/middleware/websocket.rs +++ b/backend/crates/gateway-plugin/sdk/src/client/middleware/websocket.rs @@ -1,3 +1,5 @@ +//! 插件 WebSocket 中间件的消息、载荷与会话收发接口 + use super::super::session::{CallCancellation, CallReply, HostClient, PluginCall, SessionError}; use super::{MiddlewareInput, MiddlewareOutput, invalid_input}; use crate::client::{HostReply, read::PendingRead}; @@ -10,7 +12,7 @@ use crate::{ }; pub use wire::{Direction as WebSocketDirection, Kind as WebSocketKind}; -/// 原始消息正文按需读取;直接传给 next 或返回时不读取、不复制。 +/// 原始消息正文按需读取;直接传给 next 或返回时不读取、不复制 pub struct WebSocketPayload { source: wire::Payload, bytes: Vec, @@ -68,7 +70,7 @@ impl WebSocketPayload { Ok(Some(reply.payload)) } } - /// 不再需要正文时释放句柄;会话结束也会回收所有未释放资源。 + /// 不再需要正文时释放句柄;会话结束也会回收所有未释放资源 pub async fn close(&mut self) -> Result<(), PluginFault> { if let wire::Payload::Handle { handle } = &self.source { let host = self.host.as_ref().ok_or_else(invalid_input)?; @@ -92,7 +94,7 @@ impl WebSocketPayload { self.read.clear(); Ok(()) } - /// 收集大小由插件决定;透传不需要调用此方法。 + /// 收集大小由插件决定;透传不需要调用此方法 pub async fn collect(mut self) -> Result, PluginFault> { let mut bytes = Vec::new(); while let Some(chunk) = self.read().await? { @@ -132,7 +134,7 @@ impl WebSocketMessage { }) } fn into_wire(self) -> Result<(wire::Message, Vec), PluginFault> { - // 尚在读取的消息不能直接归还句柄,否则宿主稍后返回的数据会被丢弃。 + // 尚在读取的消息不能直接归还句柄,否则宿主稍后返回的数据会被丢弃 if self.payload.read.is_pending() { return Err(PluginFault::new( ErrorCode::InvalidInput, @@ -183,7 +185,7 @@ impl WebSocketSender { pub(crate) fn new(host: HostClient) -> Self { Self { host } } - /// 在当前连接直接发送消息并等待实际写入,不伪装成默认协议的输出。 + /// 在当前连接直接发送消息并等待实际写入,不伪装成默认协议的输出 pub async fn send(&self, message: WebSocketMessage) -> Result<(), PluginFault> { let (head, bytes) = message.into_wire()?; let reply = self @@ -255,7 +257,7 @@ impl MiddlewareOutput for Option { } } -/// 由 HTTP 中间件接管的连接;接收权独占,发送端可以复制后并发使用。 +/// 由 HTTP 中间件接管的连接;接收权独占,发送端可以复制后并发使用 pub struct WebSocketSession { pub sender: WebSocketSender, pub cancellation: CallCancellation, diff --git a/backend/crates/gateway-plugin/sdk/src/client/mod.rs b/backend/crates/gateway-plugin/sdk/src/client/mod.rs index a21bb737e..53ba30375 100644 --- a/backend/crates/gateway-plugin/sdk/src/client/mod.rs +++ b/backend/crates/gateway-plugin/sdk/src/client/mod.rs @@ -1,4 +1,4 @@ -//! 可选的插件侧异步传输与会话辅助;SDK 不启动网关服务。 +//! 可选的插件侧异步传输与会话辅助;SDK 不启动网关服务 mod data; mod frame; diff --git a/backend/crates/gateway-plugin/sdk/src/client/plugin/methods.rs b/backend/crates/gateway-plugin/sdk/src/client/plugin/methods.rs index 0565185bb..9fad10973 100644 --- a/backend/crates/gateway-plugin/sdk/src/client/plugin/methods.rs +++ b/backend/crates/gateway-plugin/sdk/src/client/plugin/methods.rs @@ -1,4 +1,4 @@ -//! 类型化业务方法目录;控制参数、敏感载荷、阶段与响应流在此固定。 +//! 类型化业务方法目录;控制参数、敏感载荷、阶段与响应流在此固定 use crate::{ Capability as C, Stage as S, @@ -141,7 +141,7 @@ pub const STATE_MIGRATE: Method = Method::new( "plugin.reconcile", &[C::Maintenance], diff --git a/backend/crates/gateway-plugin/sdk/src/client/plugin/mod.rs b/backend/crates/gateway-plugin/sdk/src/client/plugin/mod.rs index b9498545b..90cdbb029 100644 --- a/backend/crates/gateway-plugin/sdk/src/client/plugin/mod.rs +++ b/backend/crates/gateway-plugin/sdk/src/client/plugin/mod.rs @@ -1,4 +1,4 @@ -//! 可组合的类型化作者入口,注册事实始终来自同一份规范化清单。 +//! 可组合的类型化作者入口,注册事实始终来自同一份规范化清单 pub mod methods; mod typed; @@ -23,7 +23,7 @@ pub use typed::{Empty, Method, TypedCall, TypedReply}; type Handler = Arc CallFuture<'static> + Send + Sync>; -/// 作者配置错误只包含方法与能力标识,不输出清单中的敏感业务数据。 +/// 作者配置错误只包含方法与能力标识,不输出清单中的敏感业务数据 #[derive(Debug, thiserror::Error)] pub enum AuthorError { #[error("plugin author manifest is invalid: {0}")] @@ -34,27 +34,28 @@ pub enum AuthorError { MissingMethod(&'static str), } -/// 从作者清单组合处理器;不手写 `plugin.register` 或复制宿主握手的能力。 +/// 从作者清单组合处理器;不手写 `plugin.register` 或复制宿主握手的能力 pub struct PluginBuilder { manifest: Manifest, handlers: BTreeMap<&'static str, Handler>, } impl PluginBuilder { - /// 读取与 `cpr-plugin package --manifest` 相同的作者清单。 + /// 读取与 `cpr-plugin package --manifest` 相同的作者清单 /// /// # Errors /// - /// 作者清单无效、含构建元数据或未显式选择中间件阶段时失败。 + /// 作者清单无效、含构建元数据或未显式选择中间件阶段时失败 pub fn from_json(source: &[u8]) -> Result { Self::from_manifest(Manifest::from_author_slice(source)?) } - /// 使用已经规范化的清单。通常优先使用 `from_json(include_bytes!(...))`。 + /// 使用已经规范化的清单 + /// 通常优先使用 `from_json(include_bytes!(...))` /// /// # Errors /// - /// 清单校验失败时返回错误。 + /// 清单校验失败时返回错误 pub fn from_manifest(manifest: Manifest) -> Result { manifest.validate()?; Ok(Self { @@ -63,11 +64,11 @@ impl PluginBuilder { }) } - /// 注册 SDK 定义的类型化方法,参数/载荷编码由方法合同决定。 + /// 注册 SDK 定义的类型化方法,参数/载荷编码由方法合同决定 /// /// # Errors /// - /// 重复注册或对应能力未在作者清单声明时失败。 + /// 重复注册或对应能力未在作者清单声明时失败 pub fn on(mut self, method: Method, handler: F) -> Result where P: Send + 'static, @@ -105,11 +106,11 @@ impl PluginBuilder { Ok(self) } - /// 组合洋葱中间件,沿用 single-use next 与惰性正文,不建立另一套流式机制。 + /// 组合洋葱中间件,沿用 single-use next 与惰性正文,不建立另一套流式机制 /// /// # Errors /// - /// 未声明中间件或重复注册时失败。 + /// 未声明中间件或重复注册时失败 pub fn middleware(mut self, handler: F) -> Result where C: MiddlewareInput, @@ -142,11 +143,11 @@ impl PluginBuilder { Ok(self) } - /// 冻结模型别名目录;目标校验与发布由宿主负责。 + /// 冻结模型别名目录;目标校验与发布由宿主负责 /// /// # Errors /// - /// 未声明模型目录能力或重复注册时失败。 + /// 未声明模型目录能力或重复注册时失败 pub fn model_catalog( self, registration: crate::call::catalog::ModelCatalogRegistration, @@ -157,11 +158,11 @@ impl PluginBuilder { }) } - /// 冻结管理页面/路由描述并绑定类型化业务函数。 + /// 冻结管理页面/路由描述并绑定类型化业务函数 /// /// # Errors /// - /// 未声明管理能力或重复注册时失败。 + /// 未声明管理能力或重复注册时失败 pub fn management( self, registration: ManagementRegistration, @@ -178,11 +179,11 @@ impl PluginBuilder { .on(methods::MANAGEMENT_HANDLE, handler) } - /// 冻结命令帮助并绑定类型化命令执行函数。 + /// 冻结命令帮助并绑定类型化命令执行函数 /// /// # Errors /// - /// 未声明命令行能力或重复注册时失败。 + /// 未声明命令行能力或重复注册时失败 pub fn command_line( self, registration: CommandRegistration, @@ -199,11 +200,11 @@ impl PluginBuilder { .on(methods::COMMAND_LINE_EXECUTE, handler) } - /// 检查声明与实际处理器后生成可交给 `PluginSession::run` 的插件。 + /// 检查声明与实际处理器后生成可交给 `PluginSession::run` 的插件 /// /// # Errors /// - /// 声明缺少处理器时失败。 + /// 声明缺少处理器时失败 pub fn build(self) -> Result { validation::validate(&self.manifest, &self.handlers)?; Ok(ComposedPlugin { @@ -223,7 +224,7 @@ impl PluginBuilder { } } -/// 已校验的业务处理器集合;会话仍负责取消、回调和流控。 +/// 已校验的业务处理器集合;会话仍负责取消、回调和流控 pub struct ComposedPlugin { registration: Registration, handlers: BTreeMap<&'static str, Handler>, diff --git a/backend/crates/gateway-plugin/sdk/src/client/plugin/typed.rs b/backend/crates/gateway-plugin/sdk/src/client/plugin/typed.rs index 277bfa371..47d7bc650 100644 --- a/backend/crates/gateway-plugin/sdk/src/client/plugin/typed.rs +++ b/backend/crates/gateway-plugin/sdk/src/client/plugin/typed.rs @@ -1,3 +1,5 @@ +//! 插件方法的类型化调用与响应编解码,以及调用阶段校验 + use std::marker::PhantomData; use serde::{Deserialize, Serialize, de::DeserializeOwned}; @@ -6,7 +8,7 @@ use crate::{CallContext, Capability, ErrorCode, PluginFault, Stage}; use super::super::{CallCancellation, CallReply, HostClient, PluginCall, ResponseStream}; -/// RPC 的空控制对象,不使用会编码为 `null` 的 Rust 单元类型。 +/// RPC 的空控制对象,不使用会编码为 `null` 的 Rust 单元类型 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Empty {} @@ -14,7 +16,7 @@ pub struct Empty {} pub(super) type Decoder

= fn(PluginCall, &'static [Stage]) -> Result, PluginFault>; pub(super) type Encoder = fn(TypedReply) -> Result; -/// SDK 方法标识与请求/响应类型;只能使用 [`super::methods`] 中的合同常量。 +/// SDK 方法标识与请求/响应类型;只能使用 [`super::methods`] 中的合同常量 pub struct Method { pub(super) name: &'static str, pub(super) capabilities: &'static [Capability], @@ -51,17 +53,17 @@ impl Method { } } -/// 业务请求、宿主上下文与当前父调用内的服务句柄;不实现内容型 `Debug`。 +/// 业务请求、宿主上下文与当前父调用内的服务句柄;不实现内容型 `Debug` pub struct TypedCall

{ pub request: P, - /// 只有合同明确保留原始正文的方法才会在此提供字节。 + /// 只有合同明确保留原始正文的方法才会在此提供字节 pub payload: Vec, pub context: CallContext, pub host: HostClient, pub cancellation: CallCancellation, } -/// 类型化结果,可复用现有有界响应流;敏感 JSON 按方法合同写入二进制载荷。 +/// 类型化结果,可复用现有有界响应流;敏感 JSON 按方法合同写入二进制载荷 pub struct TypedReply { pub result: R, pub payload: Vec, diff --git a/backend/crates/gateway-plugin/sdk/src/client/plugin/validation.rs b/backend/crates/gateway-plugin/sdk/src/client/plugin/validation.rs index 2185070ac..d5d7212fc 100644 --- a/backend/crates/gateway-plugin/sdk/src/client/plugin/validation.rs +++ b/backend/crates/gateway-plugin/sdk/src/client/plugin/validation.rs @@ -1,3 +1,5 @@ +//! 校验插件清单中的能力声明与实际注册处理器是否一致 + use std::collections::BTreeMap; use crate::{Capability as C, Manifest}; diff --git a/backend/crates/gateway-plugin/sdk/src/client/read.rs b/backend/crates/gateway-plugin/sdk/src/client/read.rs index e1f49c59f..7a049f876 100644 --- a/backend/crates/gateway-plugin/sdk/src/client/read.rs +++ b/backend/crates/gateway-plugin/sdk/src/client/read.rs @@ -1,4 +1,4 @@ -//! 读取由资源持有,取消等待不会丢弃已发出的 RPC 或已收到的部分结果。 +//! 读取由资源持有,取消等待不会丢弃已发出的 RPC 或已收到的部分结果 use std::{future::Future, pin::Pin}; @@ -15,7 +15,7 @@ impl PendingRead { &mut self, start: impl FnOnce() -> F, ) -> T { - // 不后台预读;只有消费者再次等待时才继续驱动同一次读取。 + // 不后台预读;只有消费者再次等待时才继续驱动同一次读取 let result = self.0.get_or_insert_with(|| Box::pin(start())).await; self.0 = None; result diff --git a/backend/crates/gateway-plugin/sdk/src/client/resources.rs b/backend/crates/gateway-plugin/sdk/src/client/resources.rs index eba6b6f65..11147411b 100644 --- a/backend/crates/gateway-plugin/sdk/src/client/resources.rs +++ b/backend/crates/gateway-plugin/sdk/src/client/resources.rs @@ -1,12 +1,14 @@ +//! 插件创建自有分组、Key 与修改成员关系的类型化宿主客户端 + use crate::{PluginFault, call::resources}; use super::{HostClient, payload_call}; impl HostClient { - /// 幂等创建或读取本实例分组,不覆盖管理员修改的现有属性。 + /// 幂等创建或读取本实例分组,不覆盖管理员修改的现有属性 /// /// # Errors - /// 实例版本或输入不合法,或者宿主写入失败时返回错误。 + /// 实例版本或输入不合法,或者宿主写入失败时返回错误 pub async fn ensure_group( &self, request: resources::GroupEnsureRequest, @@ -14,10 +16,10 @@ impl HostClient { payload_call(self, resources::GROUP_ENSURE, request).await } - /// 增量修改本实例分组的成员;保留账号在其他分组中的成员关系。 + /// 增量修改本实例分组的成员;保留账号在其他分组中的成员关系 /// /// # Errors - /// 实例版本、资源归属或输入不合法,或者宿主写入失败时返回错误。 + /// 实例版本、资源归属或输入不合法,或者宿主写入失败时返回错误 pub async fn change_group_members( &self, request: resources::GroupMembersChange, @@ -25,10 +27,10 @@ impl HostClient { payload_call(self, resources::GROUP_MEMBERS, request).await } - /// 幂等创建或读取仅绑定本实例分组的 Key;不返回密钥明文。 + /// 幂等创建或读取仅绑定本实例分组的 Key;不返回密钥明文 /// /// # Errors - /// 实例版本、资源归属或输入不合法,或者宿主写入失败时返回错误。 + /// 实例版本、资源归属或输入不合法,或者宿主写入失败时返回错误 pub async fn ensure_key( &self, request: resources::KeyEnsureRequest, diff --git a/backend/crates/gateway-plugin/sdk/src/client/session.rs b/backend/crates/gateway-plugin/sdk/src/client/session.rs index 85e733aec..6f7e22b44 100644 --- a/backend/crates/gateway-plugin/sdk/src/client/session.rs +++ b/backend/crates/gateway-plugin/sdk/src/client/session.rs @@ -1,3 +1,5 @@ +//! 插件侧双向 RPC 会话的握手、调用分派、流控与取消生命周期 + use std::{ collections::{BTreeMap, BTreeSet, VecDeque}, future::Future, @@ -25,10 +27,10 @@ const MAXIMUM_STREAM_CHUNK_BYTES: usize = 16 * 1024 * 1024; const MAXIMUM_CONCURRENCY: usize = 256; const MAXIMUM_BUFFERED_STREAM_CHUNKS: usize = 65_536; -/// 插件侧会话的内存、并发与期限边界。 +/// 插件侧会话的内存、并发与期限边界 #[derive(Debug, Clone, Copy)] pub struct SessionConfig { - /// 单个业务流分块的预算,不限制普通调用正文的总长度。 + /// 单个业务流分块的预算,不限制普通调用正文的总长度 pub maximum_stream_chunk_bytes: usize, pub maximum_calls: usize, pub maximum_callbacks: usize, @@ -40,7 +42,7 @@ pub struct SessionConfig { impl Default for SessionConfig { fn default() -> Self { Self { - // 流分块还需匹配宿主授予的信用窗口。 + // 流分块还需匹配宿主授予的信用窗口 maximum_stream_chunk_bytes: 1024 * 1024, maximum_calls: 32, maximum_callbacks: 32, @@ -70,7 +72,7 @@ impl SessionConfig { } } -/// 插件侧传输和会话错误。 +/// 插件侧传输和会话错误 #[derive(Debug, thiserror::Error)] pub enum SessionError { #[error("plugin session configuration is invalid")] @@ -96,7 +98,7 @@ pub enum SessionError { } impl SessionError { - /// 将本地会话失败收敛为可返回宿主的稳定插件错误。 + /// 将本地会话失败收敛为可返回宿主的稳定插件错误 #[must_use] pub fn into_plugin_fault(self) -> PluginFault { match self { @@ -114,7 +116,7 @@ impl SessionError { } } -/// 一次宿主调用的可克隆取消信号。 +/// 一次宿主调用的可克隆取消信号 #[derive(Clone)] pub struct CallCancellation { receiver: watch::Receiver, @@ -154,13 +156,13 @@ impl CancellationSource { } } -/// 宿主回调的成功结果;不实现 `Debug`,避免载荷进入普通诊断。 +/// 宿主回调的成功结果;不实现 `Debug`,避免载荷进入普通诊断 pub struct HostReply { pub result: Value, pub payload: Vec, } -/// 绑定父调用、期限和取消信号的宿主回调客户端。 +/// 绑定父调用、期限和取消信号的宿主回调客户端 #[derive(Clone)] pub struct HostClient { parent_id: u64, @@ -183,11 +185,11 @@ impl HostClient { } } - /// 发起一次与父调用关联的宿主回调。 + /// 发起一次与父调用关联的宿主回调 /// /// # Errors /// - /// 方法或帧无效、容量耗尽、父调用取消、期限到达、连接关闭或宿主返回错误时失败。 + /// 方法或帧无效、容量耗尽、父调用取消、期限到达、连接关闭或宿主返回错误时失败 pub async fn call( &self, method: impl Into, @@ -239,7 +241,7 @@ impl HostClient { } } -/// 交给插件业务处理器的一次宿主调用;不实现 `Debug`,避免敏感载荷泄露。 +/// 交给插件业务处理器的一次宿主调用;不实现 `Debug`,避免敏感载荷泄露 pub struct PluginCall { pub method: String, pub context: CallContext, @@ -249,24 +251,27 @@ pub struct PluginCall { pub cancellation: CallCancellation, } -/// 插件业务调用 future。 +/// 插件业务调用 future pub type CallFuture<'a> = Pin> + Send + 'a>>; -/// 插件只实现业务调用;会话负责握手、关联、流控、取消和关闭。 +/// 插件只实现业务调用;会话负责握手、关联、流控、取消和关闭 pub trait PluginHandler: Send + Sync + 'static { fn call(&self, call: PluginCall) -> CallFuture<'_>; - /// 取消时释放只属于该调用的临时业务状态。实现必须幂等、快速且不得执行阻塞 I/O。 + /// 取消时释放只属于该调用的临时业务状态 + /// 实现必须幂等、快速且不得执行阻塞 I/O fn cancel(&self, _context: &CallContext) {} - /// 会话停止接收新调用时触发。实现必须幂等、快速且不得执行阻塞 I/O。 + /// 会话停止接收新调用时触发 + /// 实现必须幂等、快速且不得执行阻塞 I/O fn quiesce(&self) {} - /// 会话关闭时释放插件自有临时状态。实现必须幂等、快速且不得执行阻塞 I/O。 + /// 会话关闭时释放插件自有临时状态 + /// 实现必须幂等、快速且不得执行阻塞 I/O fn shutdown(&self) {} } -/// 一次业务成功结果;`stream` 存在时 SDK 自动产生单一流终态。 +/// 一次业务成功结果;`stream` 存在时 SDK 自动产生单一流终态 pub struct CallReply { result: Value, payload: Vec, @@ -306,14 +311,14 @@ pub trait PullResponseStream: Send { fn next(&mut self) -> PullResponseFuture<'_>; } -/// SDK 管理 sequence、Credit 和终态的响应流。 +/// SDK 管理 sequence、Credit 和终态的响应流 pub struct ResponseStream { source: StreamSource, declared_capacity: usize, } impl ResponseStream { - /// 从已完成业务校验的有限分块构造流;SDK 会在发送 `Result` 前预检全部分块。 + /// 从已完成业务校验的有限分块构造流;SDK 会在发送 `Result` 前预检全部分块 #[must_use] pub fn from_chunks(chunks: Vec>) -> Self { let declared_capacity = chunks.len().max(1); @@ -323,7 +328,7 @@ impl ResponseStream { } } - /// 创建由生产者驱动的有界流队列。 + /// 创建由生产者驱动的有界流队列 #[must_use] pub fn channel(capacity: NonZeroUsize) -> (StreamSender, Self) { let (sender, receiver) = mpsc::channel(capacity.get()); @@ -336,7 +341,7 @@ impl ResponseStream { ) } - /// 按消费进度拉取下一帧;返回 None 表示完成,丢弃流会丢弃生产者。 + /// 按消费进度拉取下一帧;返回 None 表示完成,丢弃流会丢弃生产者 pub fn pull(source: Box) -> Self { Self { source: StreamSource::Pull(source), @@ -372,14 +377,14 @@ impl ResponseStream { } } -/// 动态响应流的有界生产端;最后一个 sender 被释放表示成功终态。 +/// 动态响应流的有界生产端;最后一个 sender 被释放表示成功终态 #[derive(Clone)] pub struct StreamSender { sender: mpsc::Sender, PluginFault>>, } impl StreamSender { - /// 排队一个业务分块;实际线协议流控由会话完成。 + /// 排队一个业务分块;实际线协议流控由会话完成 pub async fn send(&self, payload: Vec) -> Result<(), SessionError> { self.sender .send(Ok(payload)) @@ -387,7 +392,7 @@ impl StreamSender { .map_err(|_| SessionError::Closed) } - /// 以业务错误结束动态流。 + /// 以业务错误结束动态流 pub async fn fail(&self, fault: PluginFault) -> Result<(), SessionError> { self.sender .send(Err(fault)) @@ -396,7 +401,7 @@ impl StreamSender { } } -/// 已读取并校验 Hello、尚未向宿主发送 Ready 的插件会话。 +/// 已读取并校验 Hello、尚未向宿主发送 Ready 的插件会话 pub struct PluginSession { reader: R, writer: W, @@ -409,11 +414,12 @@ where R: AsyncRead + Unpin + Send + 'static, W: AsyncWrite + Unpin + Send + 'static, { - /// 读取并校验宿主握手。业务处理器可以在 `run` 前读取不可变 Handshake 构造注册描述。 + /// 读取并校验宿主握手 + /// 业务处理器可以在 `run` 前读取不可变 Handshake 构造注册描述 /// /// # Errors /// - /// 配置无效、握手超时、传输关闭或 Hello 不符合协议时失败。 + /// 配置无效、握手超时、传输关闭或 Hello 不符合协议时失败 pub async fn accept( mut reader: R, writer: W, @@ -448,11 +454,11 @@ where &self.handshake } - /// 发送 Ready 并持续处理调用、回调、流控、取消与关闭。 + /// 发送 Ready 并持续处理调用、回调、流控、取消与关闭 /// /// # Errors /// - /// 帧损坏、协议违规、处理器异常停止或传输关闭时失败。 + /// 帧损坏、协议违规、处理器异常停止或传输关闭时失败 pub async fn run(mut self, handler: H) -> Result<(), SessionError> where H: PluginHandler, @@ -833,7 +839,7 @@ struct SessionDriver { impl SessionDriver { async fn drive(&mut self, mut reader: R) -> Result<(), SessionError> { loop { - // read_exact 不是取消安全的;完成通知可以穿插处理,但必须保留同一个半帧读取 future。 + // read_exact 不是取消安全的;完成通知可以穿插处理,但必须保留同一个半帧读取 future let incoming = read_frame(&mut reader); tokio::pin!(incoming); let frame = loop { @@ -884,8 +890,9 @@ impl SessionDriver { return Err(SessionError::Protocol); } // End/Cancelled 已进入同一 FIFO 后,宿主先前为已消费分块排队的 - // Credit 仍可能稍晚抵达。此时调用资源已经回收,忽略迟到信用即可; - // 未来或回调 ID 仍按协议错误处理。 + // Credit 仍可能稍晚抵达 + // 此时调用资源已经回收,忽略迟到信用即可; + // 未来或回调 ID 仍按协议错误处理 if let Some(active) = self.active.get(&id) { active.credits.grant(bytes, frames)?; } @@ -1325,7 +1332,7 @@ impl CreditWindow { state.maximum_bytes = state.maximum_bytes.max(state.bytes); drop(state); // 每个调用只有一个顺序消费 Credit 的任务;notify_one 会保留许可,避免 - // grant 恰好发生在状态检查和等待注册之间时丢失唤醒。 + // grant 恰好发生在状态检查和等待注册之间时丢失唤醒 self.changed.notify_one(); Ok(()) } diff --git a/backend/crates/gateway-plugin/sdk/src/client/upstream_adapter.rs b/backend/crates/gateway-plugin/sdk/src/client/upstream_adapter.rs index 18c8b73cb..7290e0759 100644 --- a/backend/crates/gateway-plugin/sdk/src/client/upstream_adapter.rs +++ b/backend/crates/gateway-plugin/sdk/src/client/upstream_adapter.rs @@ -1,4 +1,4 @@ -//! 已选账号的受管 WebSocket;连接归属与跨轮续接由宿主管理。 +//! 已选账号的受管 WebSocket;连接归属与跨轮续接由宿主管理 use tokio::sync::{Mutex, OnceCell}; @@ -18,7 +18,7 @@ use crate::{ }, }; -/// 握手拒绝仍保留 HTTP 状态及正文,供适配器解析上游错误。 +/// 握手拒绝仍保留 HTTP 状态及正文,供适配器解析上游错误 pub enum UpstreamWebSocketUpgrade { Connected { headers: Vec<(String, String)>, @@ -31,10 +31,10 @@ pub enum UpstreamWebSocketUpgrade { }, } -/// 本次调用的连接入口;读写可同时进行,有先后依赖的发送须按顺序等待。 +/// 本次调用的连接入口;读写可同时进行,有先后依赖的发送须按顺序等待 /// -/// 结束调用时宿主关闭连接;只有成功终态携带连接内续接状态时才保留连接。 -/// 因此丢弃此对象不会提前关闭宿主尚待确认的续接连接。 +/// 结束调用时宿主关闭连接;只有成功终态携带连接内续接状态时才保留连接 +/// 因此丢弃此对象不会提前关闭宿主尚待确认的续接连接 pub struct UpstreamWebSocket { host: HostClient, closed: OnceCell<()>, @@ -42,10 +42,10 @@ pub struct UpstreamWebSocket { } impl HostClient { - /// 建立连接,续接时复用宿主恢复的同一连接。 + /// 建立连接,续接时复用宿主恢复的同一连接 /// /// # Errors - /// 目标、握手或续接归属无效时失败。 + /// 目标、握手或续接归属无效时失败 pub async fn upstream_websocket( &self, request: UpstreamWebSocketRequest, @@ -75,10 +75,10 @@ impl HostClient { } impl UpstreamWebSocket { - /// 发送完整文本或二进制消息。 + /// 发送完整文本或二进制消息 /// /// # Errors - /// 连接关闭、消息无效、网络失败或期限到达时失败。 + /// 连接关闭、消息无效、网络失败或期限到达时失败 pub async fn send(&self, kind: WebSocketMessageKind, body: Vec) -> Result<(), PluginFault> { if self.closed.get().is_some() { return Err(PluginFault::new( @@ -95,11 +95,11 @@ impl UpstreamWebSocket { .await } - /// 按需读取完整消息,EOF 后再次读取返回 None;Ping/Pong 由宿主处理。 - /// 取消等待不丢弃消息;同时只允许一个读取者,发送和关闭不受读取等待阻塞。 + /// 按需读取完整消息,EOF 后再次读取返回 None;Ping/Pong 由宿主处理 + /// 取消等待不丢弃消息;同时只允许一个读取者,发送和关闭不受读取等待阻塞 /// /// # Errors - /// 父调用结束、网络失败或期限到达时失败。 + /// 父调用结束、网络失败或期限到达时失败 pub async fn read(&self) -> Result)>, PluginFault> { if self.closed.get().is_some() { return Ok(None); @@ -135,10 +135,10 @@ impl UpstreamWebSocket { } } - /// 提前关闭并放弃连接内续接;并发关闭等待同一次回调,失败或取消后可重试。 + /// 提前关闭并放弃连接内续接;并发关闭等待同一次回调,失败或取消后可重试 /// /// # Errors - /// 父调用已结束或宿主拒绝释放时失败。 + /// 父调用已结束或宿主拒绝释放时失败 pub async fn close(&self) -> Result<(), PluginFault> { self.closed .get_or_try_init(|| async { @@ -151,7 +151,7 @@ impl UpstreamWebSocket { .await }) .await?; - // 活跃读取由宿主关闭唤醒;不等待读锁,避免 close 与 read 互相等待。 + // 活跃读取由宿主关闭唤醒;不等待读锁,避免 close 与 read 互相等待 if let Ok(mut pending) = self.read.try_lock() { pending.clear(); } diff --git a/backend/crates/gateway-plugin/sdk/src/context.rs b/backend/crates/gateway-plugin/sdk/src/context.rs index 10728f90f..4417f9ca4 100644 --- a/backend/crates/gateway-plugin/sdk/src/context.rs +++ b/backend/crates/gateway-plugin/sdk/src/context.rs @@ -1,8 +1,10 @@ +//! 插件会话握手与单次调用上下文,绑定制品、配置及调用身份 + use serde::{Deserialize, Serialize}; use crate::{Contributions, Stage}; -/// 一次会话绑定不可变制品与配置;重启必须使用新的 incarnation。 +/// 一次会话绑定不可变制品与配置;重启必须使用新的 incarnation #[derive(Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Handshake { @@ -30,7 +32,7 @@ impl std::fmt::Debug for Handshake { } } -/// 仅携带安全关联信息;这些标识本身不是宿主资源的访问凭证。 +/// 仅携带安全关联信息;这些标识本身不是宿主资源的访问凭证 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct CallContext { @@ -40,7 +42,7 @@ pub struct CallContext { pub incarnation: String, pub stage: Stage, pub timeout_ms: u64, - /// 首个流式结果返回后由资源 owner 控制取消,不再沿用初始调用期限。 + /// 首个流式结果返回后由资源 owner 控制取消,不再沿用初始调用期限 pub resource_stream: bool, pub resource_scope_id: String, #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/backend/crates/gateway-plugin/sdk/src/error.rs b/backend/crates/gateway-plugin/sdk/src/error.rs index 0d57a1b10..16e5bdbcb 100644 --- a/backend/crates/gateway-plugin/sdk/src/error.rs +++ b/backend/crates/gateway-plugin/sdk/src/error.rs @@ -1,3 +1,5 @@ +//! 插件线协议错误码、上游发送状态与安全错误对象 + use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] @@ -16,7 +18,7 @@ pub enum ErrorCode { Conflict, } -/// 发送事实由宿主取单调上界,不能由插件降回未发送。 +/// 发送事实由宿主取单调上界,不能由插件降回未发送 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum SendState { @@ -33,7 +35,7 @@ pub struct PluginFault { pub send_state: SendState, #[serde(default, skip_serializing_if = "Option::is_none")] pub http_status: Option, - /// 原始边界错误详情;按合同完整传递,不作为诊断日志输出。 + /// 原始边界错误详情;按合同完整传递,不作为诊断日志输出 #[serde(default, skip_serializing_if = "Option::is_none")] pub details: Option, } diff --git a/backend/crates/gateway-plugin/sdk/src/lib.rs b/backend/crates/gateway-plugin/sdk/src/lib.rs index 81ff0900a..4739b3791 100644 --- a/backend/crates/gateway-plugin/sdk/src/lib.rs +++ b/backend/crates/gateway-plugin/sdk/src/lib.rs @@ -1,4 +1,4 @@ -//! 实验性插件线协议;不依赖网关领域或基础设施类型。 +//! 实验性插件线协议;不依赖网关领域或基础设施类型 pub mod call; mod capability; diff --git a/backend/crates/gateway-plugin/sdk/src/manifest.rs b/backend/crates/gateway-plugin/sdk/src/manifest.rs index 5dbaae476..d621f37ae 100644 --- a/backend/crates/gateway-plugin/sdk/src/manifest.rs +++ b/backend/crates/gateway-plugin/sdk/src/manifest.rs @@ -1,3 +1,5 @@ +//! 插件作者清单与平台包声明的解析、规范化及完整性校验 + use std::{ collections::{BTreeMap, BTreeSet}, fmt, @@ -12,7 +14,7 @@ use serde_json::Value; use crate::{Capability, Contributions, PROTOCOL_VERSION, Stage}; -/// 当前插件清单格式版本。 +/// 当前插件清单格式版本 pub const MANIFEST_VERSION: u32 = 2; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] @@ -21,7 +23,7 @@ pub enum RuntimeKind { TrustedProcess, } -/// 插件展示图标;路径始终引用同一清单中声明的包资源。 +/// 插件展示图标;路径始终引用同一清单中声明的包资源 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(untagged)] pub enum PluginIcon { @@ -29,7 +31,7 @@ pub enum PluginIcon { Themed(PluginIconVariants), } -/// 为浅色与深色界面分别声明的插件图标。 +/// 为浅色与深色界面分别声明的插件图标 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct PluginIconVariants { @@ -37,7 +39,7 @@ pub struct PluginIconVariants { pub dark: String, } -/// 宿主版本兼容范围。 +/// 宿主版本兼容范围 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Engines { @@ -45,7 +47,7 @@ pub struct Engines { pub codex_proxy_rs: VersionReq, } -/// 安装包的唯一目标平台。 +/// 安装包的唯一目标平台 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct PackageTarget { @@ -53,7 +55,7 @@ pub struct PackageTarget { pub architecture: String, } -/// 构建生成的安装包元数据;作者源清单可以省略。 +/// 构建生成的安装包元数据;作者源清单可以省略 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct Package { @@ -62,7 +64,7 @@ pub struct Package { pub files: BTreeMap, } -/// 插件私有状态的单个命名空间声明;宿主仍会施加更严格的全局上限。 +/// 插件私有状态的单个命名空间声明;宿主仍会施加更严格的全局上限 #[derive(Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct StateNamespace { @@ -91,7 +93,8 @@ impl fmt::Debug for StateNamespace { } } -/// 插件作者清单。`package` 缺省时表示尚未构建的源清单。 +/// 插件作者清单 +/// `package` 缺省时表示尚未构建的源清单 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct Manifest { @@ -138,11 +141,11 @@ pub enum ManifestError { } impl Manifest { - /// 读取作者清单并生成与 CLI、运行注册一致的完整声明。 + /// 读取作者清单并生成与 CLI、运行注册一致的完整声明 /// /// # Errors /// - /// JSON、版本、能力声明或安装包字段不符合作者合同时返回错误。 + /// JSON、版本、能力声明或安装包字段不符合作者合同时返回错误 pub fn from_author_slice(bytes: &[u8]) -> Result { let UniqueValue(source) = serde_json::from_slice(bytes).map_err(|_| ManifestError::Invalid)?; @@ -152,11 +155,11 @@ impl Manifest { Ok(manifest) } - /// 作者清单允许省略扩展项 ID、能力版本和固定阶段,安装清单不隐式补全。 + /// 作者清单允许省略扩展项 ID、能力版本和固定阶段,安装清单不隐式补全 /// /// # Errors /// - /// 已含构建元数据、阶段与固定合同冲突或规范化后清单无效时返回错误。 + /// 已含构建元数据、阶段与固定合同冲突或规范化后清单无效时返回错误 pub fn normalize_author(&mut self) -> Result<(), ManifestError> { if self.package.is_some() { return Err(ManifestError::Invalid); @@ -178,11 +181,11 @@ impl Manifest { self.validate() } - /// 派生并校验稳定插件 ID。 + /// 派生并校验稳定插件 ID /// /// # Errors /// - /// 发布者、机器短名或派生后的完整 ID 不符合命名边界时返回错误。 + /// 发布者、机器短名或派生后的完整 ID 不符合命名边界时返回错误 pub fn plugin_id(&self) -> Result { if !valid_plugin_segment(&self.publisher) || !valid_plugin_segment(&self.name) { return Err(ManifestError::Invalid); @@ -194,11 +197,11 @@ impl Manifest { Ok(plugin_id) } - /// 校验源清单,并在 `package` 存在时同时执行严格安装包结构校验。 + /// 校验源清单,并在 `package` 存在时同时执行严格安装包结构校验 /// /// # Errors /// - /// 清单版本、身份、声明、路径、摘要、状态或安装包元数据不符合合同时返回错误。 + /// 清单版本、身份、声明、路径、摘要、状态或安装包元数据不符合合同时返回错误 pub fn validate(&self) -> Result<(), ManifestError> { if self.manifest_version != MANIFEST_VERSION { return Err(ManifestError::Incompatible); @@ -227,11 +230,11 @@ impl Manifest { Ok(()) } - /// 返回与当前宿主及平台匹配的安装包元数据。 + /// 返回与当前宿主及平台匹配的安装包元数据 /// /// # Errors /// - /// 源清单尚无安装包、清单无效、宿主版本不匹配或目标平台不同时返回错误。 + /// 源清单尚无安装包、清单无效、宿主版本不匹配或目标平台不同时返回错误 pub fn package_for( &self, host: &Version, @@ -381,7 +384,7 @@ impl Manifest { if package.protocol_version != PROTOCOL_VERSION { return Err(ManifestError::Incompatible); } - // semver 将全版本 `*` 表示为空 comparator;`1.*` 仍有明确的主版本边界。 + // semver 将全版本 `*` 表示为空 comparator;`1.*` 仍有明确的主版本边界 if self.engines.codex_proxy_rs.comparators.is_empty() || !platform_component(&package.target.os) || !platform_component(&package.target.architecture) @@ -405,7 +408,7 @@ impl Manifest { { return Err(ManifestError::Invalid); } - // 文件不能同时充当父目录,避免跨平台路径覆盖与解压顺序歧义。 + // 文件不能同时充当父目录,避免跨平台路径覆盖与解压顺序歧义 if path .split('/') .scan(String::new(), |parent, part| { @@ -424,7 +427,7 @@ impl Manifest { } } -/// `serde_json::Value` 默认保留重复对象键的最后一个值;作者清单不能让同一声明因解析入口而改变含义。 +/// `serde_json::Value` 默认保留重复对象键的最后一个值;作者清单不能让同一声明因解析入口而改变含义 struct UniqueValue(Value); impl<'de> Deserialize<'de> for UniqueValue { @@ -550,7 +553,7 @@ fn platform_component(value: &str) -> bool { .all(|byte| byte.is_ascii_alphanumeric() || b"-_.".contains(&byte)) } -/// 逐个检查原始路径段,不能让操作系统先规范化掉 `.`、空段或反斜线。 +/// 逐个检查原始路径段,不能让操作系统先规范化掉 `.`、空段或反斜线 #[must_use] pub fn valid_package_path(path: &str) -> bool { !path.is_empty() diff --git a/backend/crates/gateway-plugin/sdk/src/message.rs b/backend/crates/gateway-plugin/sdk/src/message.rs index c9bfe46d3..dc1693ae6 100644 --- a/backend/crates/gateway-plugin/sdk/src/message.rs +++ b/backend/crates/gateway-plugin/sdk/src/message.rs @@ -1,10 +1,12 @@ +//! 插件双向 RPC 控制消息、流消息与二进制载荷帧的线协议定义 + use serde::{Deserialize, Serialize}; use crate::{CallContext, Handshake, PluginFault}; pub const PROTOCOL_VERSION: u32 = 2; -/// 元数据与二进制载荷分开,流分块不经过 JSON/base64。 +/// 元数据与二进制载荷分开,流分块不经过 JSON/base64 #[derive(Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)] pub enum Message { @@ -66,7 +68,7 @@ pub struct Frame { impl std::fmt::Debug for Message { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - // 配置、调用参数和结果都可能包含凭据;诊断仅暴露消息类别及关联 ID。 + // 配置、调用参数和结果都可能包含凭据;诊断仅暴露消息类别及关联 ID let (kind, id) = match self { Self::Hello { .. } => ("Hello", None), Self::Ready { .. } => ("Ready", None), diff --git a/backend/crates/gateway-plugin/sdk/tests/call/data.rs b/backend/crates/gateway-plugin/sdk/tests/call/data.rs index 77f051c45..38b6970f0 100644 --- a/backend/crates/gateway-plugin/sdk/tests/call/data.rs +++ b/backend/crates/gateway-plugin/sdk/tests/call/data.rs @@ -1,3 +1,5 @@ +//! 验证插件事实查询兼容新增响应字段并拒绝无效已知值 + use gateway_plugin_sdk::call::data::{ AccountFactsPage, AccountFactsQuery, ClientKeyFacts, ClientKeyFactsQuery, QuotaFacts, QuotaFactsQuery, diff --git a/backend/crates/gateway-plugin/sdk/tests/call/frontend_authentication.rs b/backend/crates/gateway-plugin/sdk/tests/call/frontend_authentication.rs index fbf1d09d8..332865aec 100644 --- a/backend/crates/gateway-plugin/sdk/tests/call/frontend_authentication.rs +++ b/backend/crates/gateway-plugin/sdk/tests/call/frontend_authentication.rs @@ -1,3 +1,5 @@ +//! 验证插件认证合同限制身份选择并脱敏凭据和主体 + use gateway_plugin_sdk::call::frontend_authentication::{ FrontendAuthenticationRequest, FrontendAuthenticationResult, }; diff --git a/backend/crates/gateway-plugin/sdk/tests/call/host.rs b/backend/crates/gateway-plugin/sdk/tests/call/host.rs index 52b8f2b6a..16e7e61a4 100644 --- a/backend/crates/gateway-plugin/sdk/tests/call/host.rs +++ b/backend/crates/gateway-plugin/sdk/tests/call/host.rs @@ -1,3 +1,5 @@ +//! 验证宿主回调的状态、账号、模型与二进制载荷合同 + use gateway_plugin_sdk::call::host::{ AffinityLookupRequest, AuthCredential, AuthListRequest, AuthSaveRequest, LogLevel, LogRequest, LogResult, ModelEventBatch, ModelExecuteRequest, ModelOperation, StateDeleteRequest, diff --git a/backend/crates/gateway-plugin/sdk/tests/call/key_budgets.rs b/backend/crates/gateway-plugin/sdk/tests/call/key_budgets.rs index 751d7143c..a13e42c67 100644 --- a/backend/crates/gateway-plugin/sdk/tests/call/key_budgets.rs +++ b/backend/crates/gateway-plugin/sdk/tests/call/key_budgets.rs @@ -1,3 +1,5 @@ +//! 验证 Key 预算重置要求显式周期并拒绝额外权限字段 + use gateway_plugin_sdk::call::key_budgets::{BudgetPeriod, ResetKeyBudgetRequest}; use serde_json::json; diff --git a/backend/crates/gateway-plugin/sdk/tests/call/management.rs b/backend/crates/gateway-plugin/sdk/tests/call/management.rs index 641cb4175..bfcc5bec2 100644 --- a/backend/crates/gateway-plugin/sdk/tests/call/management.rs +++ b/backend/crates/gateway-plugin/sdk/tests/call/management.rs @@ -1,3 +1,5 @@ +//! 验证插件管理与命令行合同的正文分离、类型及权限字段约束 + use gateway_plugin_sdk::call::management::{CommandRegistration, CommandResult, CommandValue}; use serde_json::json; diff --git a/backend/crates/gateway-plugin/sdk/tests/call/middleware/mod.rs b/backend/crates/gateway-plugin/sdk/tests/call/middleware/mod.rs index aab255125..76df76167 100644 --- a/backend/crates/gateway-plugin/sdk/tests/call/middleware/mod.rs +++ b/backend/crates/gateway-plugin/sdk/tests/call/middleware/mod.rs @@ -1,3 +1,5 @@ +//! 验证中间件挂载、正文模式、增量头部与二进制帧的线协议合同 + use gateway_plugin_sdk::{ Capability, Stage, call::middleware::{ diff --git a/backend/crates/gateway-plugin/sdk/tests/call/mod.rs b/backend/crates/gateway-plugin/sdk/tests/call/mod.rs index fcdc6f2f4..03301607e 100644 --- a/backend/crates/gateway-plugin/sdk/tests/call/mod.rs +++ b/backend/crates/gateway-plugin/sdk/tests/call/mod.rs @@ -1,3 +1,5 @@ +//! 插件各类调用数据合同的测试入口 + mod data; mod frontend_authentication; mod host; diff --git a/backend/crates/gateway-plugin/sdk/tests/call/observation.rs b/backend/crates/gateway-plugin/sdk/tests/call/observation.rs index aedaaec84..11f6224f7 100644 --- a/backend/crates/gateway-plugin/sdk/tests/call/observation.rs +++ b/backend/crates/gateway-plugin/sdk/tests/call/observation.rs @@ -1,3 +1,5 @@ +//! 验证观察事件保留已知事实、未知值与独立二进制载荷 + use gateway_plugin_sdk::{ SendState, call::observation::{ diff --git a/backend/crates/gateway-plugin/sdk/tests/call/policy.rs b/backend/crates/gateway-plugin/sdk/tests/call/policy.rs index fd2af7989..e38f4d1de 100644 --- a/backend/crates/gateway-plugin/sdk/tests/call/policy.rs +++ b/backend/crates/gateway-plugin/sdk/tests/call/policy.rs @@ -1,3 +1,5 @@ +//! 验证插件路由和账号调度决策使用显式且严格的线协议值 + use gateway_plugin_sdk::call::policy::{ AccountScheduleCandidate, AccountScheduleDecision, AccountScheduleRequest, ModelRouteDecision, ModelRouteRequest, PolicyHeader, diff --git a/backend/crates/gateway-plugin/sdk/tests/client/http.rs b/backend/crates/gateway-plugin/sdk/tests/client/http.rs index 0631a6ae4..5c8e2fee8 100644 --- a/backend/crates/gateway-plugin/sdk/tests/client/http.rs +++ b/backend/crates/gateway-plugin/sdk/tests/client/http.rs @@ -1,3 +1,5 @@ +//! 验证插件受管 HTTP 与 WebSocket 客户端的读取、关闭和取消行为 + use super::session::{HostPeer, receive, send_call, shutdown, start_session}; use gateway_plugin_sdk::client::{CallFuture, CallReply, PluginCall, PluginHandler, write_frame}; use gateway_plugin_sdk::{ErrorCode, Frame, Message}; @@ -331,7 +333,7 @@ async fn websocket_close_can_be_retried_after_cancelling_its_future() { vec![], ) .await; - // 第一条关闭已发送,但等待其结果的 future 已取消;迟到回包不应中断重试。 + // 第一条关闭已发送,但等待其结果的 future 已取消;迟到回包不应中断重试 for _ in 0..2 { reply_callback( &mut host, diff --git a/backend/crates/gateway-plugin/sdk/tests/client/mod.rs b/backend/crates/gateway-plugin/sdk/tests/client/mod.rs index 52cd2dfea..e239f0e45 100644 --- a/backend/crates/gateway-plugin/sdk/tests/client/mod.rs +++ b/backend/crates/gateway-plugin/sdk/tests/client/mod.rs @@ -1,3 +1,5 @@ +//! 插件客户端、处理器注册与会话的测试入口 + mod http; mod plugin; mod read; diff --git a/backend/crates/gateway-plugin/sdk/tests/client/plugin/mod.rs b/backend/crates/gateway-plugin/sdk/tests/client/plugin/mod.rs index 2b0815313..2f83d6c75 100644 --- a/backend/crates/gateway-plugin/sdk/tests/client/plugin/mod.rs +++ b/backend/crates/gateway-plugin/sdk/tests/client/plugin/mod.rs @@ -1,3 +1,5 @@ +//! 验证类型化插件处理器的组合注册、分派与能力声明一致性 + use std::{ collections::BTreeMap, sync::{ diff --git a/backend/crates/gateway-plugin/sdk/tests/client/read.rs b/backend/crates/gateway-plugin/sdk/tests/client/read.rs index 16e12d321..6ea609be8 100644 --- a/backend/crates/gateway-plugin/sdk/tests/client/read.rs +++ b/backend/crates/gateway-plugin/sdk/tests/client/read.rs @@ -1,3 +1,5 @@ +//! 验证异步读取在取消和背压后保留尚未交付的分块与消息 + use super::session; use gateway_plugin_sdk::{ ErrorCode, Frame, Message, Stage, @@ -110,7 +112,7 @@ async fn verify_cancelled_read(method: &str) { let first = callback_id(&mut host, read).await; signals.cancel.notify_one(); signals.dropped.notified().await; - // 已取消的读取随后成功,下一次 read 应恢复同一读取结果。 + // 已取消的读取随后成功,下一次 read 应恢复同一读取结果 reply(&mut host, first, data.clone(), b"first").await; signals.resume.notify_one(); let next = session::receive(&mut host).await; diff --git a/backend/crates/gateway-plugin/sdk/tests/client/session.rs b/backend/crates/gateway-plugin/sdk/tests/client/session.rs index 35811c751..bbb0a0c94 100644 --- a/backend/crates/gateway-plugin/sdk/tests/client/session.rs +++ b/backend/crates/gateway-plugin/sdk/tests/client/session.rs @@ -1,3 +1,5 @@ +//! 验证插件会话的二进制传输、帧长度限制与不完整消息拒绝 + use std::{ future::pending, num::NonZeroUsize, @@ -780,7 +782,7 @@ async fn completed_call_does_not_discard_a_partially_read_next_frame() { let payload_start = encoded.len() - next.payload.len(); for split in [2, 6, 10, payload_start + 2] { tokio::time::timeout(Duration::from_secs(2), async { - // 单字节缓冲保证前缀已经进入读取 future,而不是仍滞留在传输队列。 + // 单字节缓冲保证前缀已经进入读取 future,而不是仍滞留在传输队列 let (mut host, task) = start_session_with_capacity(TestHandler::default(), 1).await; send_call(&mut host, 1, "slow", json!("first"), Vec::new()).await; host.writer.write_all(&encoded[..split]).await.unwrap(); diff --git a/backend/crates/gateway-plugin/sdk/tests/main.rs b/backend/crates/gateway-plugin/sdk/tests/main.rs index 8d18b3e02..1be68c88f 100644 --- a/backend/crates/gateway-plugin/sdk/tests/main.rs +++ b/backend/crates/gateway-plugin/sdk/tests/main.rs @@ -1,3 +1,5 @@ +//! 插件 SDK 线协议、清单与客户端的测试入口 + mod call; #[cfg(feature = "io")] mod client; diff --git a/backend/crates/gateway-plugin/sdk/tests/manifest.rs b/backend/crates/gateway-plugin/sdk/tests/manifest.rs index e3038eff6..e24c626b8 100644 --- a/backend/crates/gateway-plugin/sdk/tests/manifest.rs +++ b/backend/crates/gateway-plugin/sdk/tests/manifest.rs @@ -1,3 +1,5 @@ +//! 验证插件清单的默认值、路径安全、平台与能力版本约束 + use gateway_plugin_sdk::{ Capability, ContributionDeclaration, Engines, Handshake, MANIFEST_VERSION, Manifest, ManifestError, PROTOCOL_VERSION, Package, PackageTarget, PluginIcon, PluginIconVariants, Stage, diff --git a/backend/crates/gateway-plugin/sdk/tests/message.rs b/backend/crates/gateway-plugin/sdk/tests/message.rs index f48c28ec4..6265d0028 100644 --- a/backend/crates/gateway-plugin/sdk/tests/message.rs +++ b/backend/crates/gateway-plugin/sdk/tests/message.rs @@ -1,3 +1,5 @@ +//! 验证协议消息的诊断输出不泄漏对端载荷与错误详情 + use gateway_plugin_sdk::{Frame, Message}; #[test] diff --git a/backend/crates/gateway-protocol/src/lib.rs b/backend/crates/gateway-protocol/src/lib.rs index 9dec66599..e3866300d 100644 --- a/backend/crates/gateway-protocol/src/lib.rs +++ b/backend/crates/gateway-protocol/src/lib.rs @@ -1,6 +1,6 @@ -//! 网关协议编解码器。 +//! 网关协议编解码器 //! -//! 本 crate 只处理 wire contract,不包含 Provider 认证、路由、重试或传输策略。 +//! 本 crate 只处理 wire contract,不包含 Provider 认证、路由、重试或传输策略 -/// OpenAI wire contract。 +/// OpenAI wire contract pub mod openai; diff --git a/backend/crates/gateway-protocol/src/openai/codex.rs b/backend/crates/gateway-protocol/src/openai/codex.rs index c191be21f..1fb38ff88 100644 --- a/backend/crates/gateway-protocol/src/openai/codex.rs +++ b/backend/crates/gateway-protocol/src/openai/codex.rs @@ -1,17 +1,20 @@ +//! 解析 Codex 请求的会话身份、轮次元数据与多代理模式语义 + use serde_json::{Map, Value}; const MULTI_AGENT_MODE_OPEN_TAG: &str = ""; const MULTI_AGENT_MODE_CLOSE_TAG: &str = ""; const PROACTIVE_MULTI_AGENT_MODE_PREFIX: &str = "Proactive multi-agent delegation is active."; -/// 各 Codex 端点共用的显式根会话身份。连接边界的 session_id 优先, -/// 正文及官方 metadata 只做回退;不从 turn、window 或请求内容猜测归属。 +/// 各 Codex 端点共用的显式根会话身份 +/// 连接边界的 session_id 优先, +/// 正文及官方 metadata 只做回退;不从 turn、window 或请求内容猜测归属 #[must_use] pub fn codex_session_id(body: &Map, context: &Map) -> Option { codex_identity_field(body, context, "session_id") } -/// 各 Codex 端点共用的显式线程身份,与根会话采用相同的来源优先级。 +/// 各 Codex 端点共用的显式线程身份,与根会话采用相同的来源优先级 #[must_use] pub fn codex_thread_id(body: &Map, context: &Map) -> Option { codex_identity_field(body, context, "thread_id") @@ -39,22 +42,22 @@ fn codex_identity_field( }) } -/// 从 OpenAI Responses 请求中提取的稳定 Codex 请求语义。 +/// 从 OpenAI Responses 请求中提取的稳定 Codex 请求语义 #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct CodexResponsesRequestSemantics { - /// 客户端原始选择的推理强度。 + /// 客户端原始选择的推理强度 pub reasoning_effort: Option, - /// Codex turn metadata 中的请求类型。 + /// Codex turn metadata 中的请求类型 pub request_kind: Option, - /// Codex turn metadata 中的子代理类型。 + /// Codex turn metadata 中的子代理类型 pub subagent_kind: Option, - /// 网关侧派生出的推理预设。 + /// 网关侧派生出的推理预设 pub reasoning_preset: Option<&'static str>, - /// 请求是否为 Codex 压缩请求。 + /// 请求是否为 Codex 压缩请求 pub compact: bool, } -/// 从 Responses body 与非 wire 协议上下文中提取 Codex 语义。 +/// 从 Responses body 与非 wire 协议上下文中提取 Codex 语义 #[must_use] pub fn codex_responses_request_semantics( body: &Map, @@ -66,7 +69,7 @@ pub fn codex_responses_request_semantics( ) } -/// 调用方已解析权威 turn metadata 时,从 Responses body 中提取 Codex 语义。 +/// 调用方已解析权威 turn metadata 时,从 Responses body 中提取 Codex 语义 #[must_use] pub fn codex_responses_request_semantics_with_turn_metadata( body: &Map, diff --git a/backend/crates/gateway-protocol/src/openai/events.rs b/backend/crates/gateway-protocol/src/openai/events.rs index 6cccaec87..90fdb1e5c 100644 --- a/backend/crates/gateway-protocol/src/openai/events.rs +++ b/backend/crates/gateway-protocol/src/openai/events.rs @@ -1,3 +1,5 @@ +//! 从 OpenAI 事件与响应头提取模型、用量、额度和重试等待事实 + use std::{collections::BTreeMap, time::Duration}; use serde::{Deserialize, Serialize}; @@ -5,7 +7,7 @@ use serde_json::Value; use super::sse::{SseError, parse_sse_events}; -/// 单次 Responses 流明确声明的模型;终态优先,缺失时不使用请求模型补齐。 +/// 单次 Responses 流明确声明的模型;终态优先,缺失时不使用请求模型补齐 #[derive(Debug, Default)] pub struct ResponseModelObservation { model: Option, @@ -13,7 +15,7 @@ pub struct ResponseModelObservation { } impl ResponseModelObservation { - /// 从原始事件读取响应模型,避免工具转换或 canonical 兜底改写观测事实。 + /// 从原始事件读取响应模型,避免工具转换或 canonical 兜底改写观测事实 pub fn observe(&mut self, event_type: Option<&str>, value: &Value) { let terminal = matches!( event_type, @@ -40,14 +42,14 @@ impl ResponseModelObservation { self.terminal = terminal; } - /// 返回原始响应声明,未声明时保持未知。 + /// 返回原始响应声明,未声明时保持未知 #[must_use] pub fn model(&self) -> Option<&str> { self.model.as_deref() } } -/// 规范化可展示的上游模型名,非法值不影响响应交付。 +/// 规范化可展示的上游模型名,非法值不影响响应交付 #[must_use] pub fn observed_model_name(value: &str) -> Option<&str> { let value = value.trim(); @@ -55,31 +57,31 @@ pub fn observed_model_name(value: &str) -> Option<&str> { .then_some(value) } -/// 从 Codex/OpenAI usage 结构中提取出的标准化 token 用量。 +/// 从 Codex/OpenAI usage 结构中提取出的标准化 token 用量 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TokenUsage { - /// 输入 token 数。 + /// 输入 token 数 pub input_tokens: u64, - /// 输出 token 数。 + /// 输出 token 数 pub output_tokens: u64, - /// 命中缓存的输入 token 数。 + /// 命中缓存的输入 token 数 pub cached_tokens: u64, - /// 写入 prompt cache 的输入 token 数。 + /// 写入 prompt cache 的输入 token 数 pub cache_write_tokens: u64, - /// 输出 token 中的 reasoning token 数。 + /// 输出 token 中的 reasoning token 数 pub reasoning_tokens: u64, - /// 图片工具输入 token 数。 + /// 图片工具输入 token 数 pub image_input_tokens: u64, - /// 图片工具输出 token 数。 + /// 图片工具输出 token 数 pub image_output_tokens: u64, - /// 主模型总 token 数,不包含图片工具 token。 + /// 主模型总 token 数,不包含图片工具 token pub total_tokens: u64, } -/// 从单个 JSON 响应体中提取用量。 +/// 从单个 JSON 响应体中提取用量 /// -/// 该函数同时支持 Codex usage 结构和 OpenAI usage 结构。 +/// 该函数同时支持 Codex usage 结构和 OpenAI usage 结构 pub fn extract_usage(body: &Value) -> Option { let usage = body.get("usage").unwrap_or(body); if !usage.is_object() { @@ -149,9 +151,9 @@ pub fn extract_usage(body: &Value) -> Option { }) } -/// 校验计价所需的 token 字段,拒绝缺失、非法、溢出或不一致的数值。 +/// 校验计价所需的 token 字段,拒绝缺失、非法、溢出或不一致的数值 /// -/// 用量提取允许缺失字段补零;计价必须额外确认这些零值有明确的上游事实。 +/// 用量提取允许缺失字段补零;计价必须额外确认这些零值有明确的上游事实 #[must_use] pub fn billable_usage_is_complete(response: &Value, usage: TokenUsage) -> bool { let Some(raw) = response.get("usage").filter(|value| value.is_object()) else { @@ -197,14 +199,14 @@ pub fn billable_usage_is_complete(response: &Value, usage: TokenUsage) -> bool { .is_none_or(|value| value.as_u64() == total) } -/// 从完整 SSE 文本中提取最终可见用量。 +/// 从完整 SSE 文本中提取最终可见用量 /// /// 如果存在 `response.completed` 的 usage,则优先返回它;否则回退到最后一条 -/// 可见 usage 事件。 +/// 可见 usage 事件 /// /// # Errors /// -/// 当输入不是合法 SSE 流时,返回 [`SseError`]。 +/// 当输入不是合法 SSE 流时,返回 [`SseError`] pub fn extract_sse_usage(body: &str) -> Result, SseError> { let events = parse_sse_events(body)?; let mut fallback_usage: Option = None; @@ -231,16 +233,16 @@ pub fn extract_sse_usage(body: &str) -> Result, SseError> { Ok(fallback_usage) } -/// 从上游错误响应体中提取 retry-after 秒数。 +/// 从上游错误响应体中提取 retry-after 秒数 /// /// 支持结构化的 `resets_in_seconds` / `resets_at`,也支持官方错误消息里的 -/// `try again in 11.054s` / `try again in 28ms` 文本。 +/// `try again in 11.054s` / `try again in 28ms` 文本 pub fn retry_after_seconds_from_body(body: &str) -> Option { let value = serde_json::from_str::(body).ok()?; retry_after_seconds_from_value(&value) } -/// 从已解析的上游错误事件中提取重试秒数,不做业务错误分类。 +/// 从已解析的上游错误事件中提取重试秒数,不做业务错误分类 pub fn retry_after_seconds_from_value(value: &Value) -> Option { let error = value .pointer("/response/error") @@ -296,48 +298,48 @@ fn json_value_as_positive_u64(value: &Value) -> Option { (seconds > 0).then_some(seconds) } -/// 标准化的单个限流窗口。 +/// 标准化的单个限流窗口 #[derive(Debug, Clone, Copy, PartialEq)] pub struct RateLimitWindow { - /// 已使用百分比。 + /// 已使用百分比 pub used_percent: f64, - /// 窗口分钟数。 + /// 窗口分钟数 pub window_minutes: Option, - /// 重置时间戳。 + /// 重置时间戳 pub reset_at: Option, } -/// 限流项键的来源。 +/// 限流项键的来源 /// /// HTTP headers 和事件里的 `metered_limit_name` 提供稳定 `limit_id`; /// WebSocket `additional_rate_limits` 在部分协议版本里只以可读名称为 map key, -/// 此时必须由持久化层用既有名称元数据解析,不能把名称误当成稳定 ID。 +/// 此时必须由持久化层用既有名称元数据解析,不能把名称误当成稳定 ID #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RateLimitKeySource { LimitId, LimitName, } -/// 单个计量项的限流信息。 +/// 单个计量项的限流信息 #[derive(Debug, Clone, PartialEq)] pub struct RateLimitDetails { - /// 稳定的计量项 ID。 + /// 稳定的计量项 ID pub limit_id: String, - /// [`limit_id`](Self::limit_id) 是稳定 ID,还是名称生成的临时解析键。 + /// [`limit_id`](Self::limit_id) 是稳定 ID,还是名称生成的临时解析键 pub key_source: RateLimitKeySource, - /// 上游提供的可读名称。 + /// 上游提供的可读名称 pub limit_name: Option, - /// 当前请求是否被允许。 + /// 当前请求是否被允许 pub allowed: Option, - /// 当前窗口是否已经触顶。 + /// 当前窗口是否已经触顶 pub limit_reached: Option, - /// 主限流窗口。 + /// 主限流窗口 pub primary: Option, - /// 次级限流窗口。 + /// 次级限流窗口 pub secondary: Option, } -/// 上游账户的 credits 快照。 +/// 上游账户的 credits 快照 #[derive(Debug, Clone, PartialEq, Eq)] pub struct CreditsSnapshot { pub has_credits: bool, @@ -345,12 +347,12 @@ pub struct CreditsSnapshot { pub balance: Option, } -/// 从 header 或内部事件中解析出的完整限流状态。 +/// 从 header 或内部事件中解析出的完整限流状态 #[derive(Debug, Clone, PartialEq)] pub struct ParsedRateLimits { - /// 以标准化 limit ID 为键的全部计量项。 + /// 以标准化 limit ID 为键的全部计量项 pub limits: BTreeMap, - /// 当前响应对应的计量项 ID。 + /// 当前响应对应的计量项 ID pub active_limit: Option, pub credits: Option, pub plan_type: Option, @@ -358,7 +360,7 @@ pub struct ParsedRateLimits { pub rate_limit_reached_type: Option, } -/// 从响应头对中解析限流信息。 +/// 从响应头对中解析限流信息 pub fn parse_rate_limit_headers(headers: &[(String, String)]) -> Option { let mut normalized = BTreeMap::new(); for (name, value) in headers { @@ -408,7 +410,7 @@ pub fn parse_rate_limit_headers(headers: &[(String, String)]) -> Option bool { let normalized = name.to_ascii_lowercase(); normalized == "retry-after" @@ -427,10 +429,10 @@ pub fn is_rate_limit_header_name(name: &str) -> bool { || rate_limit_id_from_header_name(&normalized).is_some() } -/// 判断响应头是否为仅供代理本地观测的 Codex 额度信号。 +/// 判断响应头是否为仅供代理本地观测的 Codex 额度信号 /// -/// 这类头会让下游官方 Codex 客户端显示账户额度提示,因此不能穿过账号隔离边界。 -/// 通用限流头(例如 `retry-after`)和非额度的 Codex 头仍允许下发。 +/// 这类头会让下游官方 Codex 客户端显示账户额度提示,因此不能穿过账号隔离边界 +/// 通用限流头(例如 `retry-after`)和非额度的 Codex 头仍允许下发 pub fn is_codex_quota_header_name(name: &str) -> bool { let normalized = name.trim().to_ascii_lowercase(); matches!( @@ -447,7 +449,7 @@ pub fn is_codex_quota_header_name(name: &str) -> bool { .is_some_and(|limit_id| limit_id == "codex" || limit_id.starts_with("codex_")) } -/// 从内部 `codex.rate_limits` 事件中解析限流信息。 +/// 从内部 `codex.rate_limits` 事件中解析限流信息 pub fn parse_rate_limits_event(value: &Value) -> Option { if value.get("type").and_then(Value::as_str) != Some("codex.rate_limits") { return None; @@ -479,7 +481,8 @@ pub fn parse_rate_limits_event(value: &Value) -> Option { .unwrap_or_default(); // 当前 WebSocket 协议会把活动具名桶同时镜像到顶层 `rate_limits`,但不附 - // `metered_limit_name`。只要 additional 中存在相同事实,顶层就不是 core 桶。 + // `metered_limit_name` + // 只要 additional 中存在相同事实,顶层就不是 core 桶 let default_is_additional_alias = explicit_limit_id.is_none() && !matching_additional_ids.is_empty(); if let Some(details) = default_details @@ -524,14 +527,14 @@ pub fn parse_rate_limits_event(value: &Value) -> Option { }) } -/// 从原始 JSON 文本中解析内部 `codex.rate_limits` 事件。 +/// 从原始 JSON 文本中解析内部 `codex.rate_limits` 事件 pub fn parse_rate_limits_event_raw(raw: &str) -> Option { serde_json::from_str::(raw) .ok() .and_then(|value| parse_rate_limits_event(&value)) } -/// 将限流状态转换回内部传输头键值对。 +/// 将限流状态转换回内部传输头键值对 pub fn rate_limits_to_header_pairs(rate_limits: &ParsedRateLimits) -> Vec<(String, String)> { let mut headers = Vec::new(); if let Some(active_limit) = &rate_limits.active_limit { @@ -766,7 +769,7 @@ fn parse_credits_from_lookup(headers: &BTreeMap) -> Option Option { Some(CreditsSnapshot { has_credits: value.get("has_credits")?.as_bool()?, diff --git a/backend/crates/gateway-protocol/src/openai/headers.rs b/backend/crates/gateway-protocol/src/openai/headers.rs index 8cf4419a4..8a89b0229 100644 --- a/backend/crates/gateway-protocol/src/openai/headers.rs +++ b/backend/crates/gateway-protocol/src/openai/headers.rs @@ -1,9 +1,9 @@ -//! OpenAI 请求头的 HTTP 传输边界。 +//! OpenAI 请求头的 HTTP 传输边界 -/// 判断小写请求头是否属于传输层管理的字段,不得作为业务扩展头透传。 +/// 判断小写请求头是否属于传输层管理的字段,不得作为业务扩展头透传 /// -/// API 入站和 Provider 编码共用此分类;`Connection` 动态声明的逐跳头由入站额外剥离。 -/// 只用于请求方向,不影响上游响应中的代理诊断信息。 +/// API 入站和 Provider 编码共用此分类;`Connection` 动态声明的逐跳头由入站额外剥离 +/// 只用于请求方向,不影响上游响应中的代理诊断信息 #[must_use] pub fn is_transport_managed_request_header(name: &str) -> bool { name.starts_with("sec-websocket-") @@ -20,19 +20,20 @@ pub fn is_transport_managed_request_header(name: &str) -> bool { | "upgrade" | "host" | "content-length" - // 请求实体与响应压缩能力属于各段 transport,不能继承下游协商。 + // 请求实体与响应压缩能力属于各段 transport,不能继承下游协商 | "content-encoding" | "accept-encoding" - // 网关中间件也会生成该链路诊断 ID,不作为业务上下文转发。 + // 网关中间件也会生成该链路诊断 ID,不作为业务上下文转发 | "x-request-id" ) } -/// 判断上游响应头是否可由客户端 HTTP adapter 或插件读取/改写。 +/// 判断上游响应头是否可由客户端 HTTP adapter 或插件读取/改写 /// /// `connection_options` 来自同一响应的 `Connection` 字段,调用方应先按逗号拆分并 -/// 转成小写。此函数同时拒绝逐跳头、实体 framing、认证材料和账号身份字段;所有 -/// 响应边界共用这一份分类,避免观测、加工和最终转发出现不同的泄漏面。 +/// 转成小写 +/// 此函数同时拒绝逐跳头、实体 framing、认证材料和账号身份字段;所有 +/// 响应边界共用这一份分类,避免观测、加工和最终转发出现不同的泄漏面 #[must_use] pub fn response_header_is_forwardable(name: &str, connection_options: &[String]) -> bool { let name = name.trim().to_ascii_lowercase(); diff --git a/backend/crates/gateway-protocol/src/openai/mod.rs b/backend/crates/gateway-protocol/src/openai/mod.rs index 60963e5b5..c207ee6f6 100644 --- a/backend/crates/gateway-protocol/src/openai/mod.rs +++ b/backend/crates/gateway-protocol/src/openai/mod.rs @@ -1,20 +1,20 @@ -//! OpenAI wire contract。 +//! 共享的 OpenAI 线协议类型、解析与编码入口 -/// Codex Responses Lite 的 HTTP 请求头。 +/// Codex Responses Lite 的 HTTP 请求头 pub const X_OPENAI_INTERNAL_CODEX_RESPONSES_LITE_HEADER: &str = "x-openai-internal-codex-responses-lite"; -/// Codex Responses Lite 在 WebSocket `client_metadata` 中的投影键。 +/// Codex Responses Lite 在 WebSocket `client_metadata` 中的投影键 pub const WS_REQUEST_HEADER_RESPONSES_LITE_CLIENT_METADATA_KEY: &str = "ws_request_header_x_openai_internal_codex_responses_lite"; -/// Codex memory consolidation 请求标记。 +/// Codex memory consolidation 请求标记 pub const X_OPENAI_MEMGEN_REQUEST_HEADER: &str = "x-openai-memgen-request"; -/// OpenAI Responses 请求中携带的 Codex 专属语义。 +/// OpenAI Responses 请求中携带的 Codex 专属语义 pub mod codex; -/// OpenAI/Codex 事件语义、用量与限流字段编解码。 +/// OpenAI/Codex 事件语义、用量与限流字段编解码 pub mod events; mod headers; -/// Server-Sent Events 帧的解析与编码。 +/// Server-Sent Events 帧的解析与编码 pub mod sse; pub use codex::{ diff --git a/backend/crates/gateway-protocol/src/openai/sse.rs b/backend/crates/gateway-protocol/src/openai/sse.rs index 02d04ed87..e5e151f59 100644 --- a/backend/crates/gateway-protocol/src/openai/sse.rs +++ b/backend/crates/gateway-protocol/src/openai/sse.rs @@ -1,28 +1,29 @@ -//! SSE 事件解析与编码。 +//! SSE 事件解析与编码 use std::fmt; use serde_json::{Value, json}; use thiserror::Error; -/// 单条 SSE 事件。 +/// 单条 SSE 事件 #[derive(Debug, Clone, PartialEq, Eq)] pub struct SseEvent { - /// 事件名。 + /// 事件名 pub event: Option, - /// 数据体。 + /// 数据体 pub data: String, - /// 可选 ID。 + /// 可选 ID pub id: Option, - /// 可选 retry。 + /// 可选 retry pub retry: Option, } -/// 保留原始字节与旁路解析结果的 SSE wire 片段。 +/// 保留原始字节与旁路解析结果的 SSE wire 片段 /// -/// 通常一个片段就是完整 SSE 帧。单帧超过旁路观测预算时会拆成多个原始片段, +/// 通常一个片段就是完整 SSE 帧 +/// 单帧超过旁路观测预算时会拆成多个原始片段, /// 此时 `events` 为空;透明 transport 必须按顺序交付 `raw`,不能从 `events` -/// 反向重建或否决原始数据。 +/// 反向重建或否决原始数据 #[derive(Clone, PartialEq, Eq)] pub struct SseFrame { raw: Vec, @@ -34,19 +35,19 @@ impl SseFrame { Self { raw, events } } - /// 返回未经改写的完整 SSE 帧字节。 + /// 返回未经改写的完整 SSE 帧字节 #[must_use] pub fn raw(&self) -> &[u8] { &self.raw } - /// 返回从同一原始帧旁路解析出的事件。 + /// 返回从同一原始帧旁路解析出的事件 #[must_use] pub fn events(&self) -> &[SseEvent] { &self.events } - /// 拆出原始帧与旁路解析事件。 + /// 拆出原始帧与旁路解析事件 #[must_use] pub fn into_parts(self) -> (Vec, Vec) { (self.raw, self.events) @@ -63,23 +64,24 @@ impl fmt::Debug for SseFrame { } } -/// 单事件旁路解析缓冲上限。 +/// 单事件旁路解析缓冲上限 pub const MAX_SSE_EVENT_BUFFER_BYTES: usize = 64 * 1024 * 1024; -/// SSE 流结束标记帧。 +/// SSE 流结束标记帧 pub const DONE_SSE_FRAME: &str = "data: [DONE]\n\n"; -/// 用于处理任意分块边界的增量 SSE 解码器。 +/// 用于处理任意分块边界的增量 SSE 解码器 #[derive(Debug)] pub struct SseEventDecoder { pending: Vec, - /// `pending` 中已确认不含帧分隔符的前缀长度。新数据到达时从该边界 - /// 回退分隔符最大跨界宽度继续扫描,大帧跨多个 chunk 不会被反复重扫。 + /// `pending` 中已确认不含帧分隔符的前缀长度 + /// 新数据到达时从该边界 + /// 回退分隔符最大跨界宽度继续扫描,大帧跨多个 chunk 不会被反复重扫 scanned: usize, /// 超大未完成帧已开始直接交付;在遇到帧分隔符前只保留跨 chunk 扫描尾部, - /// 不再尝试解析该帧。 + /// 不再尝试解析该帧 opaque_frame: bool, - /// 只有整个 SSE 流的第一条物理行允许携带 UTF-8 BOM。 + /// 只有整个 SSE 流的第一条物理行允许携带 UTF-8 BOM stream_start: bool, } @@ -95,9 +97,9 @@ impl Default for SseEventDecoder { } impl SseEventDecoder { - /// 返回从 `consumed` 起的下一个完整帧结束位置(含分隔符),并推进扫描边界。 + /// 返回从 `consumed` 起的下一个完整帧结束位置(含分隔符),并推进扫描边界 fn next_frame_end(&mut self, consumed: usize) -> Option { - // `\r\n\r\n` 分隔符可能跨 chunk 边界,扫描起点回退 3 字节。 + // `\r\n\r\n` 分隔符可能跨 chunk 边界,扫描起点回退 3 字节 let from = self.scanned.saturating_sub(3).max(consumed); match sse_frame_separator_bytes(&self.pending[from..]) { Some((position, separator_len)) => { @@ -119,7 +121,7 @@ impl SseEventDecoder { } } - /// 追加一个字节块并返回其中已经完整的事件。 + /// 追加一个字节块并返回其中已经完整的事件 pub fn push(&mut self, chunk: &[u8]) -> Result, SseError> { self.pending.extend_from_slice(chunk); let mut events = Vec::new(); @@ -142,11 +144,12 @@ impl SseEventDecoder { Ok(events) } - /// 追加一个字节块,保留每个完整 SSE 帧的原始字节及其旁路解析结果。 + /// 追加一个字节块,保留每个完整 SSE 帧的原始字节及其旁路解析结果 /// /// 与 [`Self::push`] 相比,此方法仅应由需要原样转发的 transport 使用;普通 - /// 解析路径无需为原始帧额外分配内存。旁路解析失败时仍保留原始帧,并以空 - /// event 列表表示不可观测,避免观测失败截断客户端字节流。 + /// 解析路径无需为原始帧额外分配内存 + /// 旁路解析失败时仍保留原始帧,并以空 + /// event 列表表示不可观测,避免观测失败截断客户端字节流 pub fn push_frames(&mut self, chunk: &[u8]) -> Vec { self.pending.extend_from_slice(chunk); let mut frames = Vec::new(); @@ -179,7 +182,7 @@ impl SseEventDecoder { frames } - /// 流结束时解析尚未带空行分隔符的最后一帧。 + /// 流结束时解析尚未带空行分隔符的最后一帧 pub fn finish(&mut self) -> Result, SseError> { self.scanned = 0; if self.pending.is_empty() { @@ -193,9 +196,9 @@ impl SseEventDecoder { Ok(events) } - /// 在流结束时返回未带空行分隔符的最后一帧及其旁路解析结果。 + /// 在流结束时返回未带空行分隔符的最后一帧及其旁路解析结果 /// - /// 与 [`Self::push_frames`] 一致,旁路解析失败不会丢弃原始帧。 + /// 与 [`Self::push_frames`] 一致,旁路解析失败不会丢弃原始帧 pub fn finish_frames(&mut self) -> Vec { self.scanned = 0; if self.pending.is_empty() { @@ -216,7 +219,7 @@ impl SseEventDecoder { vec![SseFrame::new(raw, events)] } - /// 继续透明交付一个已超出观测预算的帧,并在分隔符后恢复普通解析。 + /// 继续透明交付一个已超出观测预算的帧,并在分隔符后恢复普通解析 fn finish_opaque_frame(&mut self, frames: &mut Vec) -> bool { let Some((position, separator_len)) = sse_frame_separator_bytes(&self.pending) else { return false; @@ -231,7 +234,7 @@ impl SseEventDecoder { true } - /// 仅保留分隔符可能跨 chunk 的最大前缀宽度,其余原始字节立即释放给下游。 + /// 仅保留分隔符可能跨 chunk 的最大前缀宽度,其余原始字节立即释放给下游 fn flush_opaque_prefix(&mut self, frames: &mut Vec) { const SEPARATOR_CROSS_CHUNK_BYTES: usize = 3; let emit_len = self @@ -249,12 +252,12 @@ impl SseEventDecoder { } } -/// 编码 OpenAI Responses `response.failed` SSE 事件。 +/// 编码 OpenAI Responses `response.failed` SSE 事件 pub fn response_failed_sse_event(error_type: &str, code: &str, message: &str) -> String { response_failed_sse_event_with_id(None, error_type, code, message) } -/// 使用指定 response id 编码 OpenAI Responses `response.failed` SSE 事件。 +/// 使用指定 response id 编码 OpenAI Responses `response.failed` SSE 事件 pub fn response_failed_sse_event_with_id( response_id: Option<&str>, error_type: &str, @@ -265,7 +268,7 @@ pub fn response_failed_sse_event_with_id( encode_sse_event("response.failed", &data.to_string()) } -/// 构造 OpenAI Responses `response.failed` 的 JSON 数据。 +/// 构造 OpenAI Responses `response.failed` 的 JSON 数据 pub fn response_failed_sse_data_with_id( response_id: Option<&str>, error_type: &str, @@ -289,10 +292,11 @@ pub fn response_failed_sse_data_with_id( }) } -/// 将携带嵌套错误的上游事件投影为 OpenAI Responses `response.failed` 数据。 +/// 将携带嵌套错误的上游事件投影为 OpenAI Responses `response.failed` 数据 /// -/// 该投影供客户端 SSE 兼容转换使用。原始错误对象和事件级元数据保持不透明; -/// 已有 response 快照只更新失败终态字段。 +/// 该投影供客户端 SSE 兼容转换使用 +/// 原始错误对象和事件级元数据保持不透明; +/// 已有 response 快照只更新失败终态字段 pub fn response_failed_sse_data_from_error_event( response: Option<&Value>, response_id: Option<&str>, @@ -333,12 +337,12 @@ fn response_id_or_generated(response_id: Option<&str>) -> String { ) } -/// 返回下一个完整 SSE 帧结束位置(含分隔符)。 +/// 返回下一个完整 SSE 帧结束位置(含分隔符) pub fn sse_frame_end(bytes: &[u8]) -> Option { sse_frame_separator_bytes(bytes).map(|(position, separator_len)| position + separator_len) } -/// 判断完整 SSE 帧是否是单个 `[DONE]` 控制帧。 +/// 判断完整 SSE 帧是否是单个 `[DONE]` 控制帧 #[must_use] pub fn sse_frame_is_done(frame: &str) -> bool { let mut data = frame.lines().filter_map(|raw_line| { @@ -349,19 +353,19 @@ pub fn sse_frame_is_done(frame: &str) -> bool { matches!((data.next(), data.next()), (Some("[DONE]"), None)) } -/// SSE 解析错误。 +/// SSE 解析错误 #[derive(Debug, Error, PartialEq, Eq)] pub enum SseError { - /// retry 字段不是合法整数。 + /// retry 字段不是合法整数 #[error("invalid SSE retry value: {0}")] InvalidRetry(String), - /// 单个事件缓冲超过上限。 + /// 单个事件缓冲超过上限 #[error("SSE buffer exceeded {max_bytes} bytes — aborting stream")] BufferExceeded { - /// 上限字节数。 + /// 上限字节数 max_bytes: usize, }, - /// 解析错误。 + /// 解析错误 #[error("SSE parse error: {0}")] ParseError(String), } @@ -408,7 +412,7 @@ impl EventBuilder { } } -/// 解析 SSE 事件流。 +/// 解析 SSE 事件流 pub fn parse_sse_events(input: &str) -> Result, SseError> { parse_sse_events_inner(input, true) } @@ -468,12 +472,12 @@ fn parse_sse_events_inner(input: &str, strip_leading_bom: bool) -> Result String { encode_sse_event_with_metadata(event, data, None, None) } -/// 编码一条完整的 SSE 事件并保留上游 `id` 与 `retry` 元数据。 +/// 编码一条完整的 SSE 事件并保留上游 `id` 与 `retry` 元数据 pub fn encode_sse_event_with_metadata( event: &str, data: &str, diff --git a/backend/crates/gateway-protocol/tests/main.rs b/backend/crates/gateway-protocol/tests/main.rs index 98e725f43..0469625b4 100644 --- a/backend/crates/gateway-protocol/tests/main.rs +++ b/backend/crates/gateway-protocol/tests/main.rs @@ -1 +1,3 @@ +//! 共享协议解析与编码的测试入口 + mod openai; diff --git a/backend/crates/gateway-protocol/tests/openai/codex.rs b/backend/crates/gateway-protocol/tests/openai/codex.rs index f20b939f3..179683125 100644 --- a/backend/crates/gateway-protocol/tests/openai/codex.rs +++ b/backend/crates/gateway-protocol/tests/openai/codex.rs @@ -1,3 +1,5 @@ +//! 验证 Codex 会话身份优先级、轮次元数据与多代理模式解析 + use gateway_protocol::openai::{ codex_responses_request_semantics, codex_session_id, codex_thread_id, }; diff --git a/backend/crates/gateway-protocol/tests/openai/events.rs b/backend/crates/gateway-protocol/tests/openai/events.rs index 518f53fbe..fec469526 100644 --- a/backend/crates/gateway-protocol/tests/openai/events.rs +++ b/backend/crates/gateway-protocol/tests/openai/events.rs @@ -1,3 +1,5 @@ +//! 验证 OpenAI 事件的模型、用量、额度与重试信息提取 + use gateway_protocol::openai::events::{ RateLimitKeySource, RateLimitWindow, TokenUsage, billable_usage_is_complete, extract_sse_usage, extract_usage, is_codex_quota_header_name, is_rate_limit_header_name, parse_rate_limit_headers, diff --git a/backend/crates/gateway-protocol/tests/openai/headers.rs b/backend/crates/gateway-protocol/tests/openai/headers.rs index ede8f6bde..a03dfe1df 100644 --- a/backend/crates/gateway-protocol/tests/openai/headers.rs +++ b/backend/crates/gateway-protocol/tests/openai/headers.rs @@ -1,3 +1,5 @@ +//! 验证传输头部分类,以及响应中的连接级和身份字段过滤 + use gateway_protocol::openai::{ is_transport_managed_request_header, response_header_is_forwardable, }; diff --git a/backend/crates/gateway-protocol/tests/openai/mod.rs b/backend/crates/gateway-protocol/tests/openai/mod.rs index 611686b26..2386245d2 100644 --- a/backend/crates/gateway-protocol/tests/openai/mod.rs +++ b/backend/crates/gateway-protocol/tests/openai/mod.rs @@ -1,3 +1,5 @@ +//! OpenAI 请求语义、事件、头部与 SSE 的测试入口 + mod codex; mod events; mod headers; diff --git a/backend/crates/gateway-protocol/tests/openai/sse.rs b/backend/crates/gateway-protocol/tests/openai/sse.rs index 5889c78a6..649db3028 100644 --- a/backend/crates/gateway-protocol/tests/openai/sse.rs +++ b/backend/crates/gateway-protocol/tests/openai/sse.rs @@ -1,3 +1,5 @@ +//! 验证 SSE 编解码的多行数据、分块、BOM 与控制字段处理 + use gateway_protocol::openai::sse::{ DONE_SSE_FRAME, MAX_SSE_EVENT_BUFFER_BYTES, SseError, SseEvent, SseEventDecoder, encode_sse_event, encode_sse_event_with_metadata, parse_sse_events, @@ -155,7 +157,7 @@ fn incremental_decoder_should_decode_across_arbitrary_chunk_boundaries() { #[test] fn incremental_decoder_should_find_separators_split_at_every_byte_boundary() { - // CRLF 分隔符按单字节到达时必须仍被识别;帧内容跨任意多次 push 不丢失。 + // CRLF 分隔符按单字节到达时必须仍被识别;帧内容跨任意多次 push 不丢失 let body = b"event: one\r\ndata: first\r\n\r\nevent: two\ndata: second\n\ndata: third\r\n\r\n"; let mut decoder = SseEventDecoder::default(); let mut events = Vec::new(); diff --git a/backend/crates/gateway-store/src/admin_adapter.rs b/backend/crates/gateway-store/src/admin_adapter.rs index 200202d6d..8c9ff2106 100644 --- a/backend/crates/gateway-store/src/admin_adapter.rs +++ b/backend/crates/gateway-store/src/admin_adapter.rs @@ -1,4 +1,4 @@ -//! Admin 认证与设置 adapter。 +//! Admin 认证与设置 adapter use super::*; use gateway_admin::model::audit::MutationAuditOperation; diff --git a/backend/crates/gateway-store/src/backup/mod.rs b/backend/crates/gateway-store/src/backup/mod.rs index 38dd212ad..54366e62c 100644 --- a/backend/crates/gateway-store/src/backup/mod.rs +++ b/backend/crates/gateway-store/src/backup/mod.rs @@ -1,4 +1,4 @@ -//! S3 对象存储与 `pg_dump` 基础设施适配器。 +//! S3 对象存储与 `pg_dump` 基础设施适配器 pub mod pg_dump; pub mod s3; diff --git a/backend/crates/gateway-store/src/backup/pg_dump.rs b/backend/crates/gateway-store/src/backup/pg_dump.rs index 31a49a03e..c4a37385f 100644 --- a/backend/crates/gateway-store/src/backup/pg_dump.rs +++ b/backend/crates/gateway-store/src/backup/pg_dump.rs @@ -1,7 +1,7 @@ -//! `pg_dump` 子进程与本地暂存适配器。 +//! `pg_dump` 子进程与本地暂存适配器 //! -//! 数据库密码只通过 `PGPASSWORD` 环境变量传给子进程,绝不进入命令行参数或日志。 -//! 导出以有界内存流式写入暂存文件并计算 SHA-256;取消时终止子进程并清理部分文件。 +//! 数据库密码只通过 `PGPASSWORD` 环境变量传给子进程,绝不进入命令行参数或日志 +//! 导出以有界内存流式写入暂存文件并计算 SHA-256;取消时终止子进程并清理部分文件 use std::process::Stdio; use std::sync::Arc; @@ -16,17 +16,17 @@ use gateway_admin::ports::backup::{DatabaseDumpPort, DumpArtifact, DumpRequest, use super::staging::StagingArea; -/// `pg_dump` 导出适配器。 +/// `pg_dump` 导出适配器 pub struct PgDumpAdapter { staging: Arc, - /// 不含密码的 PostgreSQL URL,用于 `--dbname`。 + /// 不含密码的 PostgreSQL URL,用于 `--dbname` database_url: String, - /// 数据库密码,仅通过 `PGPASSWORD` 注入。 + /// 数据库密码,仅通过 `PGPASSWORD` 注入 database_password: String, } impl PgDumpAdapter { - /// 组合暂存区与数据库连接事实。 + /// 组合暂存区与数据库连接事实 #[must_use] pub fn new( staging: Arc, @@ -169,7 +169,7 @@ impl DatabaseDumpPort for PgDumpAdapter { } } -/// 以有界内存读取文件并计算 SHA-256。 +/// 以有界内存读取文件并计算 SHA-256 async fn hash_file(path: &std::path::Path) -> Result { let mut file = tokio::fs::File::open(path) .await diff --git a/backend/crates/gateway-store/src/backup/s3.rs b/backend/crates/gateway-store/src/backup/s3.rs index 26433cded..0cbba7772 100644 --- a/backend/crates/gateway-store/src/backup/s3.rs +++ b/backend/crates/gateway-store/src/backup/s3.rs @@ -1,8 +1,9 @@ -//! S3 兼容对象存储适配器(含 Cloudflare R2)。 +//! S3 兼容对象存储适配器(含 Cloudflare R2) //! //! 单个 `BackupObjectStorePort` 实现:方法接受已经校验的存储配置,内部按 -//! `storage_revision` 缓存 SDK client。上传采用分片流式读取,内存占用有界; -//! 取消时主动 `AbortMultipartUpload`。 +//! `storage_revision` 缓存 SDK client +//! 上传采用分片流式读取,内存占用有界; +//! 取消时主动 `AbortMultipartUpload` use std::collections::HashMap; use std::path::Path; @@ -34,18 +35,18 @@ use gateway_admin::model::backup::{ }; use gateway_admin::ports::backup::{BackupObjectStorePort, UploadObjectRequest}; -/// 分片大小:16 MiB。 +/// 分片大小:16 MiB const PART_SIZE: usize = 16 * 1024 * 1024; -/// 并发上传分片数;最大额外内存约为 `PART_SIZE * 并发数`。 +/// 并发上传分片数;最大额外内存约为 `PART_SIZE * 并发数` const CONCURRENCY: usize = 4; -/// SDK 单次请求超时。 +/// SDK 单次请求超时 const OPERATION_TIMEOUT: Duration = Duration::from_secs(300); -/// 探针对象的固定 metadata 值。 +/// 探针对象的固定 metadata 值 const PROBE_METADATA_KEY: &str = "probe"; const PROBE_METADATA_VALUE: &str = "backup-connection-test"; -/// S3 兼容对象存储适配器。 +/// S3 兼容对象存储适配器 pub struct S3ObjectStoreAdapter { clients: Mutex>, } @@ -57,7 +58,7 @@ impl Default for S3ObjectStoreAdapter { } impl S3ObjectStoreAdapter { - /// 创建适配器;client 按 `storage_revision` 惰性构建并缓存。 + /// 创建适配器;client 按 `storage_revision` 惰性构建并缓存 #[must_use] pub fn new() -> Self { Self { @@ -352,7 +353,7 @@ impl BackupObjectStorePort for S3ObjectStoreAdapter { } } -/// 并发分片上传 + CompleteMultipartUpload。 +/// 并发分片上传 + CompleteMultipartUpload async fn upload_parts( client: &S3Client, config: &BackupStorageConfig, @@ -456,7 +457,7 @@ async fn upload_parts( .map_err(map_s3_error) } -/// 从文件中读取指定长度的完整分片。 +/// 从文件中读取指定长度的完整分片 async fn read_part(source: &Path, offset: u64, len: usize) -> Result, BackupError> { let mut file = tokio::fs::File::open(source) .await @@ -465,7 +466,7 @@ async fn read_part(source: &Path, offset: u64, len: usize) -> Result, Ba .await .map_err(|_| BackupError::new(code::S3_UPLOAD_FAILED, "定位暂存归档失败".to_owned()))?; let mut buffer = vec![0_u8; len]; - // Tokio 默认单次文件读取最多 2 MiB;分片必须读取完整,不能把短读当作末尾分片上传。 + // Tokio 默认单次文件读取最多 2 MiB;分片必须读取完整,不能把短读当作末尾分片上传 file.read_exact(&mut buffer) .await .map_err(|_| BackupError::new(code::S3_UPLOAD_FAILED, "读取暂存归档失败".to_owned()))?; @@ -487,7 +488,7 @@ async fn best_effort_delete( .map_err(map_s3_error) } -/// 探测失败结果。 +/// 探测失败结果 fn failed( stage: ConnectionTestStage, error: &SdkError, @@ -507,7 +508,7 @@ fn failed( result } -/// 稳定错误码。 +/// 稳定错误码 fn s3_error_code(error: &SdkError) -> &'static str { match status_of(error) { Some(401) => code::S3_AUTH_FAILED, @@ -520,7 +521,7 @@ fn s3_error_code(error: &SdkError) -> fn s3_error_message(error: &SdkError) -> String { // 只取稳定的 S3 错误码(如 AccessDenied / NoSuchBucket / SignatureDoesNotMatch), - // 不拼入原始 message,避免泄露请求细节。 + // 不拼入原始 message,避免泄露请求细节 let detail = error.code().unwrap_or("unknown"); match status_of(error) { Some(401) => format!("当前凭据认证失败({detail})"), @@ -531,7 +532,7 @@ fn s3_error_message(error: &SdkError) } } -/// 提取 HTTP 状态码;非 ServiceError 返回 `None`。 +/// 提取 HTTP 状态码;非 ServiceError 返回 `None` fn status_of(error: &SdkError) -> Option { match error { SdkError::ServiceError(service) => Some(service.raw().status().as_u16()), @@ -539,7 +540,7 @@ fn status_of(error: &SdkError) -> Option { } } -/// 对象不存在的判定(HeadObject 的 404)。 +/// 对象不存在的判定(HeadObject 的 404) fn is_not_found(error: &SdkError) -> bool { status_of(error) == Some(404) } diff --git a/backend/crates/gateway-store/src/backup/staging.rs b/backend/crates/gateway-store/src/backup/staging.rs index fe8be28b1..53161f0ac 100644 --- a/backend/crates/gateway-store/src/backup/staging.rs +++ b/backend/crates/gateway-store/src/backup/staging.rs @@ -1,15 +1,16 @@ -//! 受控暂存目录:权限校验、磁盘空间检查与原子改名。 +//! 受控暂存目录:权限校验、磁盘空间检查与原子改名 use std::path::{Path, PathBuf}; use crate::{StoreError, StoreResult}; -/// 单任务暂存归档上限(默认 64 GiB)。 +/// 单任务暂存归档上限(默认 64 GiB) pub const DEFAULT_MAX_ARCHIVE_BYTES: u64 = 64 * 1024 * 1024 * 1024; -/// 暂存目录所需的最小剩余空间;与单归档上限解耦,避免小磁盘环境被 64 GiB 门槛误拒。 +/// 暂存目录所需的最小剩余空间;与单归档上限解耦,避免小磁盘环境被 64 GiB 门槛误拒 const MIN_STAGING_FREE_BYTES: u64 = 1024 * 1024 * 1024; -/// 备份暂存区。只负责文件系统;归档校验与上传由调用方负责。 +/// 备份暂存区 +/// 只负责文件系统;归档校验与上传由调用方负责 #[derive(Debug, Clone)] pub struct StagingArea { base_dir: PathBuf, @@ -17,11 +18,11 @@ pub struct StagingArea { } impl StagingArea { - /// 创建暂存区并确保目录存在、权限为仅运行用户可读写(0700)。 + /// 创建暂存区并确保目录存在、权限为仅运行用户可读写(0700) /// /// # Errors /// - /// 目录创建或权限设置失败时返回 [`StoreError`]。 + /// 目录创建或权限设置失败时返回 [`StoreError`] pub fn open(base_dir: PathBuf, max_archive_bytes: u64) -> StoreResult { std::fs::create_dir_all(&base_dir).map_err(|_| unavailable("create staging directory"))?; #[cfg(unix)] @@ -41,32 +42,32 @@ impl StagingArea { }) } - /// 未完成归档的暂存路径。 + /// 未完成归档的暂存路径 #[must_use] pub fn partial_path(&self, backup_id: &str) -> PathBuf { self.base_dir.join(format!("{backup_id}.partial")) } - /// 完成归档的暂存路径。 + /// 完成归档的暂存路径 #[must_use] pub fn final_path(&self, backup_id: &str) -> PathBuf { self.base_dir.join(format!("{backup_id}.dump")) } - /// 单任务暂存归档上限。 + /// 单任务暂存归档上限 #[must_use] pub const fn max_archive_bytes(&self) -> u64 { self.max_archive_bytes } - /// 检查剩余磁盘空间是否足够暂存一个归档。 + /// 检查剩余磁盘空间是否足够暂存一个归档 /// /// 只要求保留一个基本工作余量;单个归档的硬上限由 - /// [`Self::max_archive_bytes`] 在写入时兜底。 + /// [`Self::max_archive_bytes`] 在写入时兜底 /// /// # Errors /// - /// 剩余空间不足或读取失败时返回 [`crate::StoreError`]。 + /// 剩余空间不足或读取失败时返回 [`crate::StoreError`] pub fn ensure_capacity(&self) -> StoreResult<()> { let free = fs2::available_space(&self.base_dir) .map_err(|_| unavailable("read staging free space"))?; @@ -79,13 +80,13 @@ impl StagingArea { Ok(()) } - /// 清理该备份在暂存区的全部文件;不存在视为成功。 + /// 清理该备份在暂存区的全部文件;不存在视为成功 pub fn cleanup(&self, backup_id: &str) { let _ = std::fs::remove_file(self.partial_path(backup_id)); let _ = std::fs::remove_file(self.final_path(backup_id)); } - /// 校验一个已完成归档的暂存路径归属本暂存区。 + /// 校验一个已完成归档的暂存路径归属本暂存区 #[must_use] pub fn owns(&self, path: &Path) -> bool { path.parent() == Some(self.base_dir.as_path()) diff --git a/backend/crates/gateway-store/src/bundle.rs b/backend/crates/gateway-store/src/bundle.rs index e903ffc4b..3fb23b57f 100644 --- a/backend/crates/gateway-store/src/bundle.rs +++ b/backend/crates/gateway-store/src/bundle.rs @@ -1,11 +1,11 @@ -//! 完成连接、迁移与 hydration 的 Store 能力集合与启动屏障。 +//! 完成连接、迁移与 hydration 的 Store 能力集合与启动屏障 use gateway_core::account::ProviderAccountStore; use gateway_core::provider_ports::ProviderCooldownPort; use super::*; -/// 已完成连接、迁移与 hydration 的 Store 能力集合。 +/// 已完成连接、迁移与 hydration 的 Store 能力集合 pub struct StoreBundle { admin_ports: AdminStorePorts, core_ports: CoreStorePorts, @@ -53,7 +53,7 @@ impl StoreBundle { std::mem::take(&mut self.worker_contributions) } - /// 在插件命令真正执行前启动四个必要写泵;不注册任何后台业务 Worker。 + /// 在插件命令真正执行前启动四个必要写泵;不注册任何后台业务 Worker pub fn start_command_line_writes(&mut self) -> Result<(), CommandStoreDrainError> { if self.command_drain.is_some() { return Err(CommandStoreDrainError); @@ -63,7 +63,7 @@ impl StoreBundle { Ok(()) } - /// 命令成功、失败或取消后有界排空账本、Key 使用与准入释放。 + /// 命令成功、失败或取消后有界排空账本、Key 使用与准入释放 pub async fn shutdown_command_line_writes(&mut self) -> Result<(), CommandStoreDrainError> { match self.command_drain.take() { Some(drain) => drain.shutdown().await, @@ -79,17 +79,17 @@ enum StoreMode { CommandLine, } -/// 在返回 Bundle 前完成全部 Store 启动屏障。 +/// 在返回 Bundle 前完成全部 Store 启动屏障 pub async fn initialize(config: StoreConfig) -> StoreResult { connect(config, false, StoreMode::Runtime).await } -/// CLI 命令使用同一有界写队列,但不贡献恢复、保留、leader 或维护 Worker。 +/// CLI 命令使用同一有界写队列,但不贡献恢复、保留、leader 或维护 Worker pub async fn initialize_command_line(config: StoreConfig) -> StoreResult { connect(config, false, StoreMode::CommandLine).await } -/// 读取现有安装描述,不迁移数据库;该 Bundle 的 PostgreSQL 连接默认只读。 +/// 读取现有安装描述,不迁移数据库;该 Bundle 的 PostgreSQL 连接默认只读 pub async fn initialize_read_only(config: StoreConfig) -> StoreResult { connect(config, true, StoreMode::Runtime).await } @@ -205,7 +205,7 @@ async fn connect( let admissions: Arc = Arc::new( redis::RedisClientAdmissionRepository::new(redis_connection.clone(), REDIS_NAMESPACE)?, ); - // Continuation affinity 是下一轮请求的路由事实,Core 必须直接等待 Redis 确认。 + // Continuation affinity 是下一轮请求的路由事实,Core 必须直接等待 Redis 确认 let continuation: Arc = Arc::new(redis::RedisNativeContinuationRepository::new( redis_connection.clone(), @@ -295,7 +295,7 @@ async fn connect( }) } -/// 构造备份控制面的仓储、导出器与对象存储适配器。 +/// 构造备份控制面的仓储、导出器与对象存储适配器 pub(crate) fn backup_ports( pool: sqlx::PgPool, config: &StoreConfig, diff --git a/backend/crates/gateway-store/src/config.rs b/backend/crates/gateway-store/src/config.rs index 83e262238..f772b3739 100644 --- a/backend/crates/gateway-store/src/config.rs +++ b/backend/crates/gateway-store/src/config.rs @@ -1,4 +1,4 @@ -//! Store 启动配置、环境变量解析与校验。 +//! Store 启动配置、环境变量解析与校验 use std::{ path::{Path, PathBuf}, @@ -17,7 +17,7 @@ pub(crate) const POSTGRES_IDLE_TRANSACTION_TIMEOUT: Duration = Duration::from_se pub(crate) const POSTGRES_HEALTH_ATTEMPT_TIMEOUT: Duration = Duration::from_millis(450); pub(crate) const POSTGRES_HEALTH_RETRY_DELAY: Duration = Duration::from_millis(50); -/// Store 自己拥有并校验的启动配置。 +/// Store 自己拥有并校验的启动配置 #[derive(Clone, Deserialize)] pub struct StoreConfig { #[serde(skip)] @@ -30,7 +30,7 @@ pub struct StoreConfig { backup_staging_dir: PathBuf, } -/// PostgreSQL 连接池预算;acquire 超时决定池耗尽时快速失败而非排队积压。 +/// PostgreSQL 连接池预算;acquire 超时决定池耗尽时快速失败而非排队积压 #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] #[serde(default)] pub struct StorePoolConfig { @@ -60,7 +60,7 @@ impl StorePoolConfig { Ok(()) } - /// 管理观测查询可并发占用的连接数;始终为数据面保留约 20% 的池容量。 + /// 管理观测查询可并发占用的连接数;始终为数据面保留约 20% 的池容量 #[must_use] pub const fn observability_max_connections(self) -> u32 { self.max_connections - self.max_connections.div_ceil(5) @@ -122,7 +122,7 @@ impl StoreConfig { self.redis.connection_url("redis") } - /// 返回由统一运行数据根目录派生的备份暂存目录。 + /// 返回由统一运行数据根目录派生的备份暂存目录 #[must_use] pub fn backup_staging_dir(&self) -> &Path { &self.backup_staging_dir diff --git a/backend/crates/gateway-store/src/lease_renewal.rs b/backend/crates/gateway-store/src/lease_renewal.rs index 8e4a7c2a3..a184574ce 100644 --- a/backend/crates/gateway-store/src/lease_renewal.rs +++ b/backend/crates/gateway-store/src/lease_renewal.rs @@ -1,4 +1,4 @@ -//! 请求资源的续期任务随 guard 回收;失败不能让仍在执行的请求失去并发约束。 +//! 请求资源的续期任务随 guard 回收;失败不能让仍在执行的请求失去并发约束 use std::time::Duration; @@ -53,7 +53,7 @@ impl LeaseRenewal { () = tokio::time::sleep(delay) => {} } } - // 显式执行截止由 Core 归类为 timeout,租约不把它抢先改成 cancelled。 + // 显式执行截止由 Core 归类为 timeout,租约不把它抢先改成 cancelled if !deadline.is_elapsed() && let Some(cancellation) = &cancellation { diff --git a/backend/crates/gateway-store/src/lib.rs b/backend/crates/gateway-store/src/lib.rs index 97ce94064..4139823bf 100644 --- a/backend/crates/gateway-store/src/lib.rs +++ b/backend/crates/gateway-store/src/lib.rs @@ -1,7 +1,8 @@ -//! 多 Provider 网关的 PostgreSQL 持久化与 Redis 协调 adapter。 +//! 多 Provider 网关的 PostgreSQL 持久化与 Redis 协调 adapter //! -//! 业务规则与 port 由 `gateway-core` / `gateway-admin` 拥有。本 crate 只负责把 PostgreSQL 业务表 -//! 和可丢失 Redis 状态映射为明确的基础设施操作。 +//! 业务规则与 port 由 `gateway-core` / `gateway-admin` 拥有 +//! 本 crate 只负责把 PostgreSQL 业务表 +//! 和可丢失 Redis 状态映射为明确的基础设施操作 use std::sync::Arc; use std::time::{Duration, SystemTime}; diff --git a/backend/crates/gateway-store/src/postgres/account_groups.rs b/backend/crates/gateway-store/src/postgres/account_groups.rs index 3374b6aaa..c821b5e3d 100644 --- a/backend/crates/gateway-store/src/postgres/account_groups.rs +++ b/backend/crates/gateway-store/src/postgres/account_groups.rs @@ -1,4 +1,4 @@ -//! PostgreSQL owner for provider-neutral account groups and memberships. +//! 跨 Provider 账号分组及成员关系的 PostgreSQL 持久化 use gateway_admin::model::audit::MutationAuditOperation; use std::{collections::BTreeMap, str::FromStr as _}; @@ -34,7 +34,7 @@ use super::{ const ENTITY: &str = "account group"; -/// Account group store with transactional revision and audit ownership. +/// 在同一事务中维护版本与审计的账号分组存储 #[derive(Clone)] pub struct PgAccountGroupRepository { timezone: gateway_core::time::DeploymentTimeZone, @@ -533,7 +533,7 @@ async fn group_costs( } let completed_usage = completed_usage_fact_predicate("mr"); // 用量按实际完成请求的账号统计:账号属于多个分组时计入每个所属分组, - // 不再按 Client Key 绑定分组快照归属,避免多分组 Key 的费用重复出现在未承接请求的分组上。 + // 不再按 Client Key 绑定分组快照归属,避免多分组 Key 的费用重复出现在未承接请求的分组上 let statement = format!( "with requested_groups(group_id) as ( select unnest($1::text[]) @@ -556,7 +556,7 @@ async fn group_costs( where settings.id = 1 group by requested_groups.group_id" ); - // 动态片段仅为共享的固定 usage-fact predicate;group IDs 仍使用 bind。 + // 动态片段仅为共享的固定 usage-fact predicate;group IDs 仍使用 bind let rows = sqlx::query(sqlx::AssertSqlSafe(statement)) .bind(group_ids) .bind( @@ -677,7 +677,7 @@ fn unavailable(message: &'static str) -> StoreError { postgres_unavailable(message) } -/// 原生管理与插件自有分组共用字段校验和写入规则。 +/// 原生管理与插件自有分组共用字段校验和写入规则 pub(crate) async fn insert_account_group_in_transaction( transaction: &mut Transaction<'_, Postgres>, command: &NewAccountGroup, diff --git a/backend/crates/gateway-store/src/postgres/admin_security_audit.rs b/backend/crates/gateway-store/src/postgres/admin_security_audit.rs index a403ce901..79244379c 100644 --- a/backend/crates/gateway-store/src/postgres/admin_security_audit.rs +++ b/backend/crates/gateway-store/src/postgres/admin_security_audit.rs @@ -1,4 +1,4 @@ -//! `admin_users` 与 `admin_audit_events` 的唯一 PostgreSQL owner。 +//! `admin_users` 与 `admin_audit_events` 的唯一 PostgreSQL owner use async_trait::async_trait; use chrono::{DateTime, Utc}; diff --git a/backend/crates/gateway-store/src/postgres/admission_recovery.rs b/backend/crates/gateway-store/src/postgres/admission_recovery.rs index f9f8a7c17..5db74e3d8 100644 --- a/backend/crates/gateway-store/src/postgres/admission_recovery.rs +++ b/backend/crates/gateway-store/src/postgres/admission_recovery.rs @@ -1,4 +1,4 @@ -//! Redis 丢失后从 `model_requests` 恢复客户端准入热状态。 +//! Redis 丢失后从 `model_requests` 恢复客户端准入热状态 use std::collections::BTreeMap; diff --git a/backend/crates/gateway-store/src/postgres/backup.rs b/backend/crates/gateway-store/src/postgres/backup.rs index f7b04587a..31c0041ae 100644 --- a/backend/crates/gateway-store/src/postgres/backup.rs +++ b/backend/crates/gateway-store/src/postgres/backup.rs @@ -1,7 +1,8 @@ -//! `backup_settings` 与 `backup_records` 的 PostgreSQL owner。 +//! `backup_settings` 与 `backup_records` 的 PostgreSQL owner //! -//! 实现 `gateway-admin::ports::backup::BackupRepository`。本模块只执行事务与 -//! 条件更新,不决定保留策略或状态机语义。 +//! 实现 `gateway-admin::ports::backup::BackupRepository` +//! 本模块只执行事务与 +//! 条件更新,不决定保留策略或状态机语义 use async_trait::async_trait; use chrono::{DateTime, Utc}; @@ -26,17 +27,17 @@ use crate::{ }, }; -/// `backup_records` 的最大分页大小。 +/// `backup_records` 的最大分页大小 const BACKUP_PAGE_LIMIT: u32 = 200; -/// 备份任务/配置仓储。 +/// 备份任务/配置仓储 #[derive(Clone)] pub struct PgBackupRepository { pool: PgPool, } impl PgBackupRepository { - /// 包装连接池。 + /// 包装连接池 #[must_use] pub const fn new(pool: PgPool) -> Self { Self { pool } @@ -280,7 +281,7 @@ impl BackupRepository for PgBackupRepository { .begin() .await .map_err(|_| store_unavailable("begin scheduled insert"))?; - // 先锁定并核对计划,防止配置变更后仍排入旧计划任务。 + // 先锁定并核对计划,防止配置变更后仍排入旧计划任务 let advanced = sqlx::query( "update backup_settings set next_run_at = $1 where id = 1 and schedule_enabled and cron_expression = $2 @@ -302,7 +303,7 @@ impl BackupRepository for PgBackupRepository { .map_err(|_| store_unavailable("rollback stale schedule"))?; return Ok(false); } - // 唯一约束冲突只跳过本次任务;保存点避免连带撤销游标推进。 + // 唯一约束冲突只跳过本次任务;保存点避免连带撤销游标推进 let mut insertion = transaction .begin() .await @@ -629,14 +630,14 @@ impl BackupRepository for PgBackupRepository { } } -/// Store 内部 Revision → Admin Revision 转换。 +/// Store 内部 Revision → Admin Revision 转换 fn admin_revision(revision: crate::Revision) -> AdminStoreResult { AdminRevision::new(revision.get()).map_err(|_| { AdminStoreError::new(AdminStoreErrorKind::Invalid, "revision", "zero revision") }) } -/// 锁定并读取单例配置行。 +/// 锁定并读取单例配置行 async fn lock_settings_in_transaction( transaction: &mut Transaction<'_, Postgres>, ) -> AdminStoreResult { @@ -686,7 +687,7 @@ async fn load_backup_settings(pool: &PgPool) -> AdminStoreResult .ok_or_else(|| store_not_found("backup settings")) } -/// 插入 queued 记录;返回记录或唯一约束冲突。 +/// 插入 queued 记录;返回记录或唯一约束冲突 async fn insert_queued_in_transaction( transaction: &mut Transaction<'_, Postgres>, seed: &BackupRecordSeed, @@ -710,7 +711,7 @@ async fn insert_queued_in_transaction( backup_record_from_row(&row) } -/// 把唯一约束冲突映射为活跃任务/计划冲突。 +/// 把唯一约束冲突映射为活跃任务/计划冲突 fn map_insert_error(error: sqlx::Error) -> AdminStoreError { match &error { sqlx::Error::Database(database_error) @@ -743,7 +744,7 @@ fn is_active_or_scheduled_conflict(error: &AdminStoreError) -> bool { error.kind() == AdminStoreErrorKind::Conflict } -/// 已有记录时锁定存储身份:endpoint/region/bucket/path-style 不允许变化。 +/// 已有记录时锁定存储身份:endpoint/region/bucket/path-style 不允许变化 async fn ensure_storage_identity_stable( current: &BackupSettings, command: &UpdateBackupStorageCommand, @@ -806,7 +807,7 @@ fn storage_changed_fields( fields } -/// 在同事务追加审计事件;`revision` 为空时不写 config_revision。 +/// 在同事务追加审计事件;`revision` 为空时不写 config_revision async fn append_audit_in_transaction( transaction: &mut Transaction<'_, Postgres>, mut event: AdminAuditEvent, @@ -897,7 +898,7 @@ fn backup_settings_from_row(row: &sqlx::postgres::PgRow) -> AdminStoreResult(result: Result) -> AdminStoreResult { result.map_err(|_| { AdminStoreError::new( @@ -908,7 +909,7 @@ fn decode(result: Result) -> AdminStoreResult { }) } -/// StoreError → AdminStoreError 的显式映射。 +/// StoreError → AdminStoreError 的显式映射 fn map_admin_error(error: StoreError) -> AdminStoreError { match error { StoreError::Unavailable { .. } => AdminStoreError::new( diff --git a/backend/crates/gateway-store/src/postgres/client_budgets.rs b/backend/crates/gateway-store/src/postgres/client_budgets.rs index d604d24eb..91c76df52 100644 --- a/backend/crates/gateway-store/src/postgres/client_budgets.rs +++ b/backend/crates/gateway-store/src/postgres/client_budgets.rs @@ -1,4 +1,4 @@ -//! 按 Key 串行检查限额,并幂等累计已取得的 USD 费用。 +//! 按 Key 串行检查限额,并幂等累计已取得的 USD 费用 use gateway_admin::model::audit::MutationAuditOperation; use std::{collections::BTreeMap, sync::Mutex, time::Duration}; @@ -56,7 +56,7 @@ async fn reset_client_key_budget_in_transaction( command: &ResetClientKeyBudget, context: &MutationContext, ) -> StoreResult<()> { - // 与准入、结算共用 Key 行锁,重置边界必须在取得锁之后确定。 + // 与准入、结算共用 Key 行锁,重置边界必须在取得锁之后确定 let exists = sqlx::query_scalar::<_, String>("select id from client_api_keys where id = $1 for update") .bind(command.id.as_str()) @@ -78,7 +78,7 @@ async fn reset_client_key_budget_in_transaction( ClientKeyBudgetPeriod::Weekly | ClientKeyBudgetPeriod::All ); let reset_at = Utc::now(); - // 起止时间收拢到重置边界,窗口保持未开启,同时排除重置前完成的迟到费用。 + // 起止时间收拢到重置边界,窗口保持未开启,同时排除重置前完成的迟到费用 sqlx::query( "update client_key_budget_windows set daily_used_usd = case when $2 then 0 else daily_used_usd end, @@ -148,7 +148,7 @@ impl PgClientBudgetStore { } async fn admit_inner(&self, key_id: ClientApiKeyId) -> Result<(), GatewayError> { - // 短暂存储故障后按原金额重试;进程退出丢失的费用不转成人工核账或阻断 Key。 + // 短暂存储故障后按原金额重试;进程退出丢失的费用不转成人工核账或阻断 Key let retries = self .retry .lock() @@ -240,7 +240,7 @@ impl PgClientBudgetStore { async fn settle_inner(&self, charge: &ClientBudgetCharge) -> Result<(), ClientBudgetError> { let mut tx = self.pool.begin().await.map_err(|_| ClientBudgetError)?; - // 与准入统一先锁 Key,再写窗口和费用,串行化同一 Key 的并发结算。 + // 与准入统一先锁 Key,再写窗口和费用,串行化同一 Key 的并发结算 let key = sqlx::query_scalar::<_, String>( "select id from client_api_keys where id = $1 for update", ) @@ -248,7 +248,7 @@ impl PgClientBudgetStore { .fetch_optional(&mut *tx) .await .map_err(|_| ClientBudgetError)?; - let Some(key) = key else { return Ok(()) }; // 删除 Key 时也会删除其费用记录。 + let Some(key) = key else { return Ok(()) }; // 删除 Key 时也会删除其费用记录 settle_in_transaction(&mut tx, &key, charge, self.timezone) .await .map_err(|_| ClientBudgetError)?; @@ -263,7 +263,7 @@ async fn settle_in_transaction( timezone: gateway_core::time::DeploymentTimeZone, ) -> Result<(), sqlx::Error> { let completed_at = DateTime::::from(charge.completed_at); - // 仅在请求结束时写入费用;请求 ID 冲突时不重复累计。 + // 仅在请求结束时写入费用;请求 ID 冲突时不重复累计 let changed = sqlx::query( "insert into client_key_charge_events (request_id, client_api_key_id, amount_usd, completed_at) values ($1, $2, $3::text::numeric, $4) @@ -314,7 +314,7 @@ async fn advance_windows( used_at: DateTime, timezone: gateway_core::time::DeploymentTimeZone, ) -> Result<(), sqlx::Error> { - // 已打开窗口不因部署时区变化清零;续接起点不能早于旧窗口末端或人工重置边界。 + // 已打开窗口不因部署时区变化清零;续接起点不能早于旧窗口末端或人工重置边界 let day = timezone .day_start(now) .ok_or_else(|| sqlx::Error::Protocol("invalid budget day".to_owned()))?; diff --git a/backend/crates/gateway-store/src/postgres/client_keys.rs b/backend/crates/gateway-store/src/postgres/client_keys.rs index 1fb37bf11..824345a8f 100644 --- a/backend/crates/gateway-store/src/postgres/client_keys.rs +++ b/backend/crates/gateway-store/src/postgres/client_keys.rs @@ -1,4 +1,4 @@ -//! 明文 `client_api_keys` 的 PostgreSQL owner。 +//! 明文 `client_api_keys` 的 PostgreSQL owner use gateway_admin::model::audit::MutationAuditOperation; use std::{ @@ -508,9 +508,9 @@ impl ClientApiKeyRepository for PgClientApiKeyRepository { } } -/// 认证成功后按一秒窗口合并写回 API Key 最后使用时间。 +/// 认证成功后按一秒窗口合并写回 API Key 最后使用时间 /// -/// 该 adapter 仅记录稳定 Key ID;认证材料从不进入异步队列或日志。 +/// 该 adapter 仅记录稳定 Key ID;认证材料从不进入异步队列或日志 #[derive(Clone)] pub struct PgClientApiKeyUsageSink { state: Arc, @@ -654,7 +654,7 @@ fn lock_unpoisoned(mutex: &Mutex) -> std::sync::MutexGuard<'_, T> { .unwrap_or_else(std::sync::PoisonError::into_inner) } -/// Admin 用例所需的 Client Key 事务能力。 +/// Admin 用例所需的 Client Key 事务能力 #[derive(Clone)] pub struct PgAdminClientKeyStore { keys: PgClientApiKeyRepository, @@ -670,7 +670,7 @@ impl PgAdminClientKeyStore { } } - /// 会话恢复只读启用状态,不加载明文凭据或其他 Key 的资料。 + /// 会话恢复只读启用状态,不加载明文凭据或其他 Key 的资料 pub async fn is_enabled(&self, id: &ClientApiKeyId) -> AdminStoreResult { sqlx::query_scalar::<_, bool>( "SELECT EXISTS(SELECT 1 FROM client_api_keys WHERE id = $1 AND enabled)", @@ -728,7 +728,7 @@ impl ClientKeyStore for PgAdminClientKeyStore { super::plugins::begin_plugin_mutation(&self.keys.pool, owner).await? } }; - // 保持与完整 Key 编辑相同的锁顺序;绝不读出整份配置再覆盖写回。 + // 保持与完整 Key 编辑相同的锁顺序;绝不读出整份配置再覆盖写回 sqlx::query("select config_revision from runtime_settings where id=1 for update") .execute(&mut *tx) .await @@ -1213,8 +1213,8 @@ async fn ensure_client_key_name_available( id: &str, name: &str, ) -> StoreResult<()> { - // 调用方已通过递增配置版本持有控制面行锁,查重与写入在同一事务内串行执行。 - // 不回填历史重名数据;创建和保存时统一校验,更新排除当前记录。 + // 调用方已通过递增配置版本持有控制面行锁,查重与写入在同一事务内串行执行 + // 不回填历史重名数据;创建和保存时统一校验,更新排除当前记录 let duplicate: bool = sqlx::query_scalar( "select exists(select 1 from client_api_keys where lower(btrim(name)) = lower($1) and id <> $2)", diff --git a/backend/crates/gateway-store/src/postgres/execution.rs b/backend/crates/gateway-store/src/postgres/execution.rs index 5a1ad4d73..7105a847e 100644 --- a/backend/crates/gateway-store/src/postgres/execution.rs +++ b/backend/crates/gateway-store/src/postgres/execution.rs @@ -1,4 +1,4 @@ -//! 单行 `model_requests` 生命周期与最终 usage/cost 的 PostgreSQL owner。 +//! 单行 `model_requests` 生命周期与最终 usage/cost 的 PostgreSQL owner use async_trait::async_trait; use chrono::{DateTime, Utc}; @@ -388,7 +388,7 @@ pub struct ModelRequestRecoveryReport { #[async_trait] pub trait ModelRequestRepository: Send + Sync { async fn insert_model_request(&self, request: NewModelRequest) -> StoreResult<()>; - /// 请求行与首次 attempt 列一次插入;`attempt.attempt_count` 必须为 1。 + /// 请求行与首次 attempt 列一次插入;`attempt.attempt_count` 必须为 1 async fn insert_model_request_with_first_attempt( &self, request: NewModelRequest, @@ -592,7 +592,7 @@ impl ModelRequestRepository for PgExecutionStore { attempt.validate()?; // upstream_send_state 是请求级单调水位(sent > ambiguous > not_sent), // 由 mark/finalize 抬升;开启新 attempt 不得把已持久化的 sent 重置回 - // not_sent——崩溃恢复的终态写回会原样继承本列。 + // not_sent——崩溃恢复的终态写回会原样继承本列 let count = sqlx::query_scalar::<_, i32>( "update model_requests set provider_kind = $2, @@ -717,7 +717,7 @@ impl ModelRequestRepository for PgExecutionStore { finalization: ModelRequestFinalization, ) -> StoreResult { finalization.validate()?; - // 墙上时间可能回拨;终态必须可落盘,实际耗时仍保留 Core 的单调时钟观测。 + // 墙上时间可能回拨;终态必须可落盘,实际耗时仍保留 Core 的单调时钟观测 let finalized = sqlx::query_scalar::<_, i64>( "with finalized as ( update model_requests @@ -980,7 +980,7 @@ impl ExecutionStore for PgExecutionStore { let request_id = request_id.as_str().to_owned(); Box::new(crate::lease_renewal::LeaseRenewal::spawn( deadline, - // 请求记录为 best-effort 观测,续期失败不能取消客户端执行。 + // 请求记录为 best-effort 观测,续期失败不能取消客户端执行 None, move |ttl| { let pool = pool.clone(); @@ -992,7 +992,7 @@ impl ExecutionStore for PgExecutionStore { message: "lease TTL is invalid".to_owned(), } })?; - // 首次观测可能尚在队列中;缺行不创建记录,已终结行不改写。 + // 首次观测可能尚在队列中;缺行不创建记录,已终结行不改写 sqlx::query_scalar::<_, bool>( "with renewed as ( update model_requests set deadline_at = now() + $2 * interval '1 millisecond' @@ -1039,7 +1039,7 @@ impl ExecutionStore for PgExecutionStore { request: CoreNewModelRequest, attempt: CoreAttemptRecord, ) -> Result<(), CoreStoreError> { - // 合并写只对首次 attempt 成立(插入即带 attempt 列);其余走基础两步。 + // 合并写只对首次 attempt 成立(插入即带 attempt 列);其余走基础两步 if attempt.attempt_count.get() != 1 { self.create_model_request(request).await?; return self.record_attempt(attempt).await; @@ -1177,7 +1177,7 @@ impl ExecutionStore for PgExecutionStore { .and_then(|delay| u64::try_from(delay.as_millis()).ok()), upstream_request_id: None, latency_ms: u64::try_from(rejection.latency.as_millis()).ok(), - // 入口尚无 model_requests 行,关联 ID 放在安全消息中,不能伪造外键。 + // 入口尚无 model_requests 行,关联 ID 放在安全消息中,不能伪造外键 message: serde_json::json!({ "requestId": rejection.request_id.as_str(), "clientKeyId": rejection.client_key_id.as_str(), diff --git a/backend/crates/gateway-store/src/postgres/execution_buffer.rs b/backend/crates/gateway-store/src/postgres/execution_buffer.rs index b41c9025b..86b10ad64 100644 --- a/backend/crates/gateway-store/src/postgres/execution_buffer.rs +++ b/backend/crates/gateway-store/src/postgres/execution_buffer.rs @@ -1,4 +1,4 @@ -//! 数据面执行观测的非阻塞 PostgreSQL 写入队列。 +//! 数据面执行观测的非阻塞 PostgreSQL 写入队列 use std::mem::size_of; use std::num::NonZeroUsize; @@ -23,20 +23,20 @@ const DEFAULT_QUEUE_BYTE_CAPACITY: usize = 64 * 1024 * 1024; const PERSISTENCE_LANES: usize = 4; const SHUTDOWN_DRAIN_TIMEOUT: Duration = Duration::from_secs(2); -/// 执行观测缓冲区的进程内累计状态。 +/// 执行观测缓冲区的进程内累计状态 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ExecutionBufferStats { - /// 尚未完成落库的队列项和当前写入项数量。 + /// 尚未完成落库的队列项和当前写入项数量 pub queued_items: usize, - /// 尚未完成落库的观测对象估算字节数。 + /// 尚未完成落库的观测对象估算字节数 pub queued_bytes: usize, - /// 进程启动后成功入队的累计数量。 + /// 进程启动后成功入队的累计数量 pub enqueued_total: u64, - /// 因队列、字节预算或关闭排空超时丢弃的累计数量。 + /// 因队列、字节预算或关闭排空超时丢弃的累计数量 pub dropped_total: u64, - /// 已成功写入底层 Store 的累计数量。 + /// 已成功写入底层 Store 的累计数量 pub persisted_total: u64, - /// 底层 Store 返回失败的累计数量。 + /// 底层 Store 返回失败的累计数量 pub write_failure_total: u64, } @@ -153,14 +153,15 @@ fn saturating_increment(counter: &AtomicU64, increment: u64) { }); } -/// 将数据面观测写入转换为有界、非阻塞的进程内命令。 +/// 将数据面观测写入转换为有界、非阻塞的进程内命令 /// /// 队列满、worker 尚未启动或已经退出时只丢弃观测并记录告警;协议数据面不会 -/// 等待 PostgreSQL,也不会看到 Store 错误。启动恢复仍直接访问底层 Store。 +/// 等待 PostgreSQL,也不会看到 Store 错误 +/// 启动恢复仍直接访问底层 Store pub struct BufferedExecutionStore { inner: Arc, // lane transport 本身没有独立容量;所有发送只能经 `enqueue` 的全局 item/byte - // 预留进入,`QueuedExecutionObservation::drop` 负责归还,不能增加旁路发送入口。 + // 预留进入,`QueuedExecutionObservation::drop` 负责归还,不能增加旁路发送入口 senders: Box<[mpsc::UnboundedSender]>, next_unkeyed_lane: AtomicUsize, state: Arc, @@ -275,7 +276,7 @@ impl BufferedExecutionStore { } fn request_lane(request_id: &str, lane_count: usize) -> usize { - // request ID 由 Core 生成,不含用户选择的散列输入;固定散列只用于进程内顺序亲和。 + // request ID 由 Core 生成,不含用户选择的散列输入;固定散列只用于进程内顺序亲和 let hash = request_id .bytes() .fold(0xcbf2_9ce4_8422_2325_u64, |hash, byte| { @@ -391,9 +392,9 @@ where } } -/// 由 Host 监督的固定并行写泵;同一 request ID 固定落在一个 lane 并按入队顺序落库。 +/// 由 Host 监督的固定并行写泵;同一 request ID 固定落在一个 lane 并按入队顺序落库 /// -/// lane transport 共享 Store 的全局 item/byte 预留,正在写入的项目同样计入总上限。 +/// lane transport 共享 Store 的全局 item/byte 预留,正在写入的项目同样计入总上限 pub struct ExecutionObservationWriter { inner: Arc, receivers: Box<[Mutex>]>, @@ -410,10 +411,10 @@ impl ExecutionObservationWriter { self.state.snapshot() } - /// 等待所有已接收写入结束一次持久化尝试。 + /// 等待所有已接收写入结束一次持久化尝试 /// - /// 队列计数包含正在写入的项目;失败沿用现有 fail-open 计数且不会由本方法重试。 - /// 返回 `false` 表示到达截止时间时仍有排队或正在写入的项目。 + /// 队列计数包含正在写入的项目;失败沿用现有 fail-open 计数且不会由本方法重试 + /// 返回 `false` 表示到达截止时间时仍有排队或正在写入的项目 pub async fn wait_until_idle(&self, deadline: Instant) -> bool { self.idle().wait_until(deadline).await } @@ -427,7 +428,7 @@ impl ExecutionObservationWriter { impl ExecutionBufferIdle { /// 等待所有已接收写入结束一次持久化尝试;队列计数包含正在写入的项目, - /// 写入失败沿用现有 fail-open 计数且不在关闭路径重试。 + /// 写入失败沿用现有 fail-open 计数且不在关闭路径重试 pub(crate) async fn wait_until(&self, deadline: Instant) -> bool { loop { if self.state.queued_items.load(Ordering::Acquire) == 0 { @@ -436,7 +437,7 @@ impl ExecutionBufferIdle { let idle = self.state.idle.notified(); tokio::pin!(idle); let _ = idle.as_mut().enable(); - // 在订阅前后各检查一次,覆盖最后一个写入恰好在注册等待时结束的竞态。 + // 在订阅前后各检查一次,覆盖最后一个写入恰好在注册等待时结束的竞态 if self.state.queued_items.load(Ordering::Acquire) == 0 { return true; } @@ -562,7 +563,8 @@ where return; }; // Store 写入没有统一幂等键;超时可能表示已提交,不能在这里盲目重试并 - // 制造重复 ops_events。失败会被计数并丢弃,数据面始终不等待补偿。 + // 制造重复 ops_events + // 失败会被计数并丢弃,数据面始终不等待补偿 match write.persist(store).await { Ok(()) => state.record_persisted(), Err(error) => { diff --git a/backend/crates/gateway-store/src/postgres/mod.rs b/backend/crates/gateway-store/src/postgres/mod.rs index 6e658315d..3b9c38afc 100644 --- a/backend/crates/gateway-store/src/postgres/mod.rs +++ b/backend/crates/gateway-store/src/postgres/mod.rs @@ -1,4 +1,4 @@ -//! PostgreSQL 业务表的 adapters。 +//! PostgreSQL 业务表的 adapters use async_trait::async_trait; use sqlx::{ @@ -53,7 +53,7 @@ pub(crate) use usage_facts::{ static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!("../../migrations"); -/// 建立 PostgreSQL pool 并只执行冻结的 migration 集。 +/// 建立 PostgreSQL pool 并只执行冻结的 migration 集 pub async fn connect_and_migrate( database_url: &str, pool_config: StorePoolConfig, @@ -86,7 +86,7 @@ pub async fn connect_and_migrate( connect_pool(connect_options, pool_config, false).await } -/// 帮助查询不执行迁移,并用连接默认只读事务阻止意外业务写入。 +/// 帮助查询不执行迁移,并用连接默认只读事务阻止意外业务写入 pub(crate) async fn connect_read_only( database_url: &str, pool_config: StorePoolConfig, @@ -164,7 +164,7 @@ pub trait ControlPlaneRepository: Send + Sync { replacement: ControlPlaneReplacement, ) -> StoreResult; - /// 更新 admin_api_key 字段并推进 config revision。 + /// 更新 admin_api_key 字段并推进 config revision async fn replace_admin_api_key( &self, admin_api_key: Option, @@ -227,7 +227,7 @@ impl ControlPlaneRepository for PgControlPlaneRepository { .await .map_err(|_| postgres_unavailable("begin control plane replacement"))?; let result = async { - // 锁住所有配置写入共同使用的行,版本检查与修改不能被其他事务穿插。 + // 锁住所有配置写入共同使用的行,版本检查与修改不能被其他事务穿插 let current = sqlx::query_scalar::<_, i64>( "select config_revision from runtime_settings where id = 1 for update", ) diff --git a/backend/crates/gateway-store/src/postgres/observability/admin_adapter.rs b/backend/crates/gateway-store/src/postgres/observability/admin_adapter.rs index 90a883454..e20ec571d 100644 --- a/backend/crates/gateway-store/src/postgres/observability/admin_adapter.rs +++ b/backend/crates/gateway-store/src/postgres/observability/admin_adapter.rs @@ -1,4 +1,4 @@ -//! Pg 观测 adapter:实现 `ObservabilityRepository` 与 `AdminObservabilityStore`。 +//! Pg 观测 adapter:实现 `ObservabilityRepository` 与 `AdminObservabilityStore` use futures::{StreamExt, TryStreamExt, stream::BoxStream}; use gateway_admin::ports::store::UsageCalculatedBillingStream; @@ -43,7 +43,7 @@ impl PgObservabilityRepository { self } - /// 从账号事实和当前冷却一次性派生 Dashboard 五态;不在 SQL 中复制状态机。 + /// 从账号事实和当前冷却一次性派生 Dashboard 五态;不在 SQL 中复制状态机 async fn account_status_snapshot( &self, observed_at: DateTime, @@ -89,10 +89,10 @@ impl PgObservabilityRepository { } } -/// `gateway-admin` 观测端口的 PostgreSQL adapter。 +/// `gateway-admin` 观测端口的 PostgreSQL adapter /// /// SQL 查询与内部投影继续由 [`PgObservabilityRepository`] 唯一拥有;本类型只负责 -/// Admin UTC 领域模型与持久化投影之间的无格式化转换。 +/// Admin UTC 领域模型与持久化投影之间的无格式化转换 #[derive(Clone)] pub struct PgAdminObservabilityStore { repository: PgObservabilityRepository, @@ -139,7 +139,7 @@ impl ObservabilityRepository for PgObservabilityRepository { current_page: 1, page_size: ObservabilityPageSize::new(10)?, }; - // 每条 SQL 独立取一个全局观测槽位,避免整包预留造成队头阻塞。 + // 每条 SQL 独立取一个全局观测槽位,避免整包预留造成队头阻塞 let (totals, (provider_accounts, _)) = futures::try_join!( self.query_budget.run( "load dashboard lifetime totals", diff --git a/backend/crates/gateway-store/src/postgres/observability/mapping.rs b/backend/crates/gateway-store/src/postgres/observability/mapping.rs index 388d93f3c..d678efebb 100644 --- a/backend/crates/gateway-store/src/postgres/observability/mapping.rs +++ b/backend/crates/gateway-store/src/postgres/observability/mapping.rs @@ -1,11 +1,11 @@ -//! Store 与 Admin 领域之间的无格式化转换及 row 解析。 +//! Store 与 Admin 领域之间的无格式化转换及 row 解析 use super::*; pub(crate) fn store_range( range: admin_observability::TimeRange, ) -> AdminStoreResult { - // 显式外部范围已经校验;自然日零点的空快照仍须返回零计数。 + // 显式外部范围已经校验;自然日零点的空快照仍须返回零计数 if range.start == range.end { return Ok(ObservabilityRange { start: range.start, diff --git a/backend/crates/gateway-store/src/postgres/observability/mod.rs b/backend/crates/gateway-store/src/postgres/observability/mod.rs index 84e50c524..957971b4b 100644 --- a/backend/crates/gateway-store/src/postgres/observability/mod.rs +++ b/backend/crates/gateway-store/src/postgres/observability/mod.rs @@ -1,4 +1,4 @@ -//! 从 `model_requests`、`ops_events` 与账号公共投影读取观测事实。 +//! 从 `model_requests`、`ops_events` 与账号公共投影读取观测事实 use std::{ collections::{BTreeMap, BTreeSet, HashMap}, diff --git a/backend/crates/gateway-store/src/postgres/observability/model.rs b/backend/crates/gateway-store/src/postgres/observability/model.rs index e81ed8403..550a32aea 100644 --- a/backend/crates/gateway-store/src/postgres/observability/model.rs +++ b/backend/crates/gateway-store/src/postgres/observability/model.rs @@ -1,4 +1,4 @@ -//! 查询模型、校验与观测端口契约。 +//! 查询模型、校验与观测端口契约 use super::*; use futures::stream::BoxStream; @@ -7,7 +7,7 @@ pub(crate) const MAX_PAGE_SIZE: u16 = 100; pub(crate) const MAX_FILTER_BYTES: usize = 256; pub(crate) const MAX_SEARCH_BYTES: usize = 512; pub(crate) const MAX_ACCOUNT_IDS: usize = 200; -/// 概览卡只展示最近使用的四个账号;完整账号用量由账号管理页单独查询。 +/// 概览卡只展示最近使用的四个账号;完整账号用量由账号管理页单独查询 pub(crate) const DASHBOARD_ACCOUNT_LIMIT: u16 = 4; pub(crate) const DIAGNOSTIC_LIMIT: i64 = 100; pub(crate) const ACCOUNT_USAGE_TIMELINE_HOURS: i64 = 24; @@ -225,9 +225,9 @@ pub struct RequestMetrics { pub latency_count: u64, pub max_latency_ms: Option, pub min_latency_ms: Option, - /// 分母:`input_tokens is not null`,即上游确实报告过 input token 事实的请求。 + /// 分母:`input_tokens is not null`,即上游确实报告过 input token 事实的请求 pub cache_eligible_request_count: u64, - /// 分子:分母集合中 `cached_tokens > 0` 的请求。 + /// 分子:分母集合中 `cached_tokens > 0` 的请求 pub cache_hit_request_count: u64, pub latency_percentiles: LatencyPercentiles, pub first_token_latency_percentiles: LatencyPercentiles, @@ -244,7 +244,7 @@ pub struct RequestMetrics { } impl RequestMetrics { - /// 请求级 cache hit rate;没有 input token 事实时返回 `None`。 + /// 请求级 cache hit rate;没有 input token 事实时返回 `None` #[must_use] pub fn cache_hit_request_rate(&self) -> Option { (self.cache_eligible_request_count > 0) @@ -252,7 +252,7 @@ impl RequestMetrics { } } -/// PostgreSQL `percentile_cont` 的非负、有限毫秒值;bits 保留插值小数且可安全比较。 +/// PostgreSQL `percentile_cont` 的非负、有限毫秒值;bits 保留插值小数且可安全比较 #[derive(Clone, Copy, PartialEq, Eq, Hash)] pub struct PercentileMilliseconds(u64); @@ -332,7 +332,7 @@ pub struct RequestMetricPoint { pub costs: Vec, } -/// 已完整交付且由 Provider 计算费用的请求事实。 +/// 已完整交付且由 Provider 计算费用的请求事实 #[derive(Debug, Clone, PartialEq, Eq)] pub struct CalculatedUsageBillingFact { pub billing_snapshot_json: Option, @@ -448,7 +448,7 @@ impl ProviderAccountUsageQuery { } pub fn with_hourly_request_buckets(mut self) -> StoreResult { - // 小时图独立展示最近 24 个 UTC 小时桶,自然日统计不能限制为 24 小时。 + // 小时图独立展示最近 24 个 UTC 小时桶,自然日统计不能限制为 24 小时 let current_hour = DateTime::from_timestamp(self.range.end.timestamp().div_euclid(3600) * 3600, 0) .ok_or_else(|| invalid("account request timeline exceeds supported timestamps"))?; @@ -482,7 +482,7 @@ pub struct DashboardObservation { pub recent_requests: Vec, } -/// 使用记录列表所需的窄投影;完整执行、路由和客户端详情按 ID 单独读取。 +/// 使用记录列表所需的窄投影;完整执行、路由和客户端详情按 ID 单独读取 #[derive(Debug, Clone, PartialEq, Eq)] pub struct UsageListRecord { pub client_api_key_name: Option, diff --git a/backend/crates/gateway-store/src/postgres/observability/queries/accounts.rs b/backend/crates/gateway-store/src/postgres/observability/queries/accounts.rs index dc90ef6c2..c3ef4a628 100644 --- a/backend/crates/gateway-store/src/postgres/observability/queries/accounts.rs +++ b/backend/crates/gateway-store/src/postgres/observability/queries/accounts.rs @@ -1,4 +1,4 @@ -//! Provider 账号用量查询族。 +//! Provider 账号用量查询族 use super::super::*; diff --git a/backend/crates/gateway-store/src/postgres/observability/queries/dashboard.rs b/backend/crates/gateway-store/src/postgres/observability/queries/dashboard.rs index 46c70b28a..48563b045 100644 --- a/backend/crates/gateway-store/src/postgres/observability/queries/dashboard.rs +++ b/backend/crates/gateway-store/src/postgres/observability/queries/dashboard.rs @@ -1,4 +1,4 @@ -//! Dashboard 与趋势查询族。 +//! Dashboard 与趋势查询族 use super::super::*; @@ -9,7 +9,7 @@ pub(crate) async fn request_metrics( ) -> StoreResult { filter.validate()?; // 结果计数覆盖范围内全部请求(成功率/失败率的分母分子);用量、缓存、 - // 延迟与成本聚合仅统计用量事实(已完整交付客户端的成功响应)。 + // 延迟与成本聚合仅统计用量事实(已完整交付客户端的成功响应) let fact = completed_usage_fact_predicate("mr"); let mut query = QueryBuilder::::new(format!( "select count(*)::bigint as request_count, @@ -105,7 +105,7 @@ pub(crate) async fn request_metrics( pub(crate) async fn dashboard_totals(pool: &PgPool) -> StoreResult { // 卡片脚注的“总计”覆盖全部历史;只聚合页面实际展示的字段,避免重复计算 - // 当前区间指标所需的延迟分位数。 + // 当前区间指标所需的延迟分位数 let fact = completed_usage_fact_predicate("mr"); let mut query = QueryBuilder::::new(format!( "select count(*)::bigint as request_count, @@ -159,7 +159,7 @@ async fn request_metric_series_inner( filter.validate()?; let granularity = granularity_for(range); // 与 request_metrics 同一契约:结果计数覆盖全部请求,用量/延迟/成本 - // 聚合仅统计用量事实。 + // 聚合仅统计用量事实 let fact = completed_usage_fact_predicate("mr"); let mut query = QueryBuilder::::new("select "); push_metric_bucket(&mut query, range, granularity, timezone)?; @@ -318,7 +318,7 @@ pub(crate) fn calculated_usage_billing_facts( ); push_completed_usage_fact_filter(&mut query, "mr"); push_usage_filter(&mut query, &filter, "mr"); - // 费用逐条累加与行顺序无关,避免全量物化和不必要的数据库排序。 + // 费用逐条累加与行顺序无关,避免全量物化和不必要的数据库排序 let mut rows = query.build().fetch(pool); while let Some(row) = rows.try_next().await .map_err(|_| postgres_unavailable("load calculated usage billing facts"))? @@ -372,8 +372,9 @@ pub(crate) async fn attempt_metrics( ) -> StoreResult { filter.validate()?; let completed_usage = completed_usage_fact_predicate("mr"); - // PERF: `model_requests` 包含宽请求元数据。这里只物化两个聚合族会复用的列, - // 避免生产基数下 `mr.*` 与多次标量子查询把 CTE 反复写入临时文件。 + // PERF: `model_requests` 包含宽请求元数据 + // 这里只物化两个聚合族会复用的列, + // 避免生产基数下 `mr.*` 与多次标量子查询把 CTE 反复写入临时文件 let mut query = QueryBuilder::::new(format!( "with selected_requests as materialized ( select mr.id, mr.attempt_count, mr.outcome, mr.error_kind, @@ -516,7 +517,7 @@ pub(crate) async fn provider_observations( filter: &UsageRecordFilter, ) -> StoreResult> { let fact = completed_usage_fact_predicate("mr"); - // 请求、attempt 与失败覆盖执行审计;token 只累计完整交付的用量事实。 + // 请求、attempt 与失败覆盖执行审计;token 只累计完整交付的用量事实 let mut query = QueryBuilder::::new(format!( "select coalesce(mr.provider_kind, 'unrouted') as provider_kind, count(*)::bigint as request_count, @@ -679,7 +680,7 @@ pub(crate) fn fill_metric_gaps( Ok(result) } -// 将同一组 UTC 日界交给请求、费用和空桶补齐,避免数据库另用一套时区规则。 +// 将同一组 UTC 日界交给请求、费用和空桶补齐,避免数据库另用一套时区规则 fn push_metric_bucket( query: &mut QueryBuilder, range: ObservabilityRange, diff --git a/backend/crates/gateway-store/src/postgres/observability/queries/mod.rs b/backend/crates/gateway-store/src/postgres/observability/queries/mod.rs index b78061658..e9f6bb1b4 100644 --- a/backend/crates/gateway-store/src/postgres/observability/queries/mod.rs +++ b/backend/crates/gateway-store/src/postgres/observability/queries/mod.rs @@ -1,4 +1,4 @@ -//! 观测查询族:dashboard / usage / ops / accounts。 +//! 观测查询族:dashboard / usage / ops / accounts mod accounts; mod dashboard; diff --git a/backend/crates/gateway-store/src/postgres/observability/queries/ops.rs b/backend/crates/gateway-store/src/postgres/observability/queries/ops.rs index 75cb55c6e..c1364f335 100644 --- a/backend/crates/gateway-store/src/postgres/observability/queries/ops.rs +++ b/backend/crates/gateway-store/src/postgres/observability/queries/ops.rs @@ -1,4 +1,4 @@ -//! Ops 错误查询族。 +//! Ops 错误查询族 use super::super::*; @@ -133,8 +133,8 @@ fn push_request_error_predicates( range: ObservabilityRange, filter: &OpsErrorFilter, ) { - // 错误事实独立于请求结束状态;主动取消不属于需要排查的错误。 - // 列表和总数共用此条件,避免流式响应中的错误因 outcome 被漏掉。 + // 错误事实独立于请求结束状态;主动取消不属于需要排查的错误 + // 列表和总数共用此条件,避免流式响应中的错误因 outcome 被漏掉 statement.push(" and mr.error_kind is not null and mr.error_kind <> 'cancelled'"); push_range(statement, "mr.completed_at", range); for (column, value) in [ @@ -189,8 +189,8 @@ fn push_ops_event_predicates( ] { push_text_equality(statement, column, value); } - // Ops events do not persist upstream transport. A transport filter therefore - // intentionally excludes this source instead of matching an unrelated request fact. + // 运维事件不持久化上游传输类型 + // 按传输类型筛选时排除此来源,避免借用无关的请求事实 if filter.transport.is_some() { statement.push(" and false"); } diff --git a/backend/crates/gateway-store/src/postgres/observability/queries/usage.rs b/backend/crates/gateway-store/src/postgres/observability/queries/usage.rs index 4d901b1d4..b688a3096 100644 --- a/backend/crates/gateway-store/src/postgres/observability/queries/usage.rs +++ b/backend/crates/gateway-store/src/postgres/observability/queries/usage.rs @@ -1,4 +1,4 @@ -//! Usage 明细、诊断与过滤查询族。 +//! Usage 明细、诊断与过滤查询族 use super::super::*; use serde_json::Value; diff --git a/backend/crates/gateway-store/src/postgres/observability/query_budget.rs b/backend/crates/gateway-store/src/postgres/observability/query_budget.rs index 7d3d637bc..022d800c9 100644 --- a/backend/crates/gateway-store/src/postgres/observability/query_budget.rs +++ b/backend/crates/gateway-store/src/postgres/observability/query_budget.rs @@ -1,4 +1,4 @@ -//! 管理观测查询的 PostgreSQL 连接槽位预算。 +//! 管理观测查询的 PostgreSQL 连接槽位预算 use std::{ future::Future, @@ -11,7 +11,7 @@ use tokio::sync::{OwnedSemaphorePermit, Semaphore}; use crate::{StoreBackend, StoreError, StoreResult}; -/// 所有管理观测入口共享的 PostgreSQL 连接槽位预算。 +/// 所有管理观测入口共享的 PostgreSQL 连接槽位预算 #[derive(Clone)] pub struct ObservabilityQueryBudget { slots: Arc, @@ -19,7 +19,7 @@ pub struct ObservabilityQueryBudget { } impl ObservabilityQueryBudget { - /// 预算表示观测类 SQL 最多可同时占用的连接数。 + /// 预算表示观测类 SQL 最多可同时占用的连接数 pub fn try_new(max_connections: u32, wait_timeout: Duration) -> StoreResult { if max_connections == 0 || wait_timeout.is_zero() { return Err(StoreError::InvalidData { @@ -37,11 +37,11 @@ impl ObservabilityQueryBudget { }) } - /// 在共享管理观测预算内执行一条 PostgreSQL 查询。 + /// 在共享管理观测预算内执行一条 PostgreSQL 查询 /// /// # Errors /// - /// 预算在等待期限内没有空闲槽位、预算已关闭,或查询本身失败时返回错误。 + /// 预算在等待期限内没有空闲槽位、预算已关闭,或查询本身失败时返回错误 pub async fn run(&self, operation: &'static str, query: F) -> StoreResult where F: Future>, @@ -50,7 +50,7 @@ impl ObservabilityQueryBudget { query.await } - /// 将槽位持有到查询流消费结束、报错或被丢弃,而不是仅持有到流创建完成。 + /// 将槽位持有到查询流消费结束、报错或被丢弃,而不是仅持有到流创建完成 pub fn run_stream<'a, T, S>( &'a self, operation: &'static str, @@ -75,8 +75,8 @@ impl ObservabilityQueryBudget { Ok(Some(item)) => yield Ok(item), Ok(None) => break, Err(error) => { - // 返回错误后调用方可能不再 poll,但仍保留流对象。 - // 必须在交付错误之前释放数据库流和预算。 + // 返回错误后调用方可能不再 poll,但仍保留流对象 + // 必须在交付错误之前释放数据库流和预算 drop(query); drop(slot); yield Err(error); diff --git a/backend/crates/gateway-store/src/postgres/ops_events.rs b/backend/crates/gateway-store/src/postgres/ops_events.rs index 0818c4e9e..f9c659003 100644 --- a/backend/crates/gateway-store/src/postgres/ops_events.rs +++ b/backend/crates/gateway-store/src/postgres/ops_events.rs @@ -1,4 +1,4 @@ -//! `ops_events` 中间失败与后台故障事实的 PostgreSQL owner。 +//! `ops_events` 中间失败与后台故障事实的 PostgreSQL owner use async_trait::async_trait; use chrono::{DateTime, Utc}; diff --git a/backend/crates/gateway-store/src/postgres/plugins/artifacts.rs b/backend/crates/gateway-store/src/postgres/plugins/artifacts.rs index 03dd1fcfa..0a235055c 100644 --- a/backend/crates/gateway-store/src/postgres/plugins/artifacts.rs +++ b/backend/crates/gateway-store/src/postgres/plugins/artifacts.rs @@ -1,3 +1,5 @@ +//! 插件制品的 PostgreSQL 存储,以及实例、来源和凭据端口的统一适配 + use async_trait::async_trait; use gateway_admin::model::audit::MutationAuditOperation; use gateway_admin::model::plugins::distribution::{SourceCredential, SourceCredentialInfo}; @@ -233,8 +235,9 @@ impl PluginStore for PgPluginStore { .map_err(|_| unavailable())? { let artifact = decode(&row)?; - // 当前允许来源已由 bind 校验。同一包换用已授权的下载入口时保留首次出处, - // 但不能借重复安装把自定义包提升为内置包,或反向改写内置包的信任身份。 + // 当前允许来源已由 bind 校验 + // 同一包换用已授权的下载入口时保留首次出处, + // 但不能借重复安装把自定义包提升为内置包,或反向改写内置包的信任身份 let same_trust_origin = matches!(&artifact.source, PluginSource::Builtin { .. }) == matches!(&source, PluginSource::Builtin { .. }); if artifact.metadata != metadata || !same_trust_origin { @@ -349,7 +352,7 @@ impl PluginStore for PgPluginStore { let revision = bump_config_revision_in_transaction(&mut transaction) .await .map_err(|error| admin_store_error("plugin", error))?; - // 制品删除会级联移除下载引用,先固定本次涉及的凭据,不能清扫无关的未使用凭据。 + // 制品删除会级联移除下载引用,先固定本次涉及的凭据,不能清扫无关的未使用凭据 let credential_ids: Vec = sqlx::query_scalar( "select credential_id from plugin_artifact_credentials where artifact_sha256=$1", ) diff --git a/backend/crates/gateway-store/src/postgres/plugins/credentials.rs b/backend/crates/gateway-store/src/postgres/plugins/credentials.rs index 9c81e85e7..d1913e4f2 100644 --- a/backend/crates/gateway-store/src/postgres/plugins/credentials.rs +++ b/backend/crates/gateway-store/src/postgres/plugins/credentials.rs @@ -1,3 +1,5 @@ +//! 插件下载凭据的加密持久化、读取与引用清理 + use gateway_admin::model::audit::MutationAuditOperation; use gateway_admin::{ model::{ @@ -47,7 +49,7 @@ pub(super) async fn save( context: &MutationContext, ) -> AdminStoreResult { let id = uuid::Uuid::parse_str(&credential.info.id).map_err(|_| unavailable())?; - // 只有持久化边界显式展开 secret,不给敏感领域类型实现通用 Serialize。 + // 只有持久化边界显式展开 secret,不给敏感领域类型实现通用 Serialize let secret = match &credential.authentication { SourceAuthentication::Github { token } => { serde_json::json!({"kind":"github", "token":token.expose_secret()}) @@ -91,7 +93,7 @@ pub(super) async fn save( admin_revision(revision) } -/// 只回收本次删除制品使用过且已无引用的凭据,保留其他插件共享的下载认证。 +/// 只回收本次删除制品使用过且已无引用的凭据,保留其他插件共享的下载认证 pub(super) async fn delete_unused( tx: &mut Transaction<'_, Postgres>, ids: &[uuid::Uuid], diff --git a/backend/crates/gateway-store/src/postgres/plugins/instances.rs b/backend/crates/gateway-store/src/postgres/plugins/instances.rs index 0fd310ecc..54dc9a0e7 100644 --- a/backend/crates/gateway-store/src/postgres/plugins/instances.rs +++ b/backend/crates/gateway-store/src/postgres/plugins/instances.rs @@ -1,3 +1,5 @@ +//! 插件实例配置与版本的事务写入、绑定校验和发布状态读取 + use gateway_admin::model::audit::MutationAuditOperation; use std::collections::{BTreeMap, BTreeSet}; @@ -254,7 +256,7 @@ async fn save_inner( let mut tx = pool.begin().await.map_err(|_| unavailable())?; check_revision(&mut tx, expected).await?; validate_artifact_acceptance(&mut tx, &instance).await?; - // 停用是损坏配置的恢复路径,必须原样保留绑定;再次启用时才要求引用仍存在。 + // 停用是损坏配置的恢复路径,必须原样保留绑定;再次启用时才要求引用仍存在 if instance.enabled { validate_binding_references(&mut tx, &instance).await?; } @@ -272,14 +274,14 @@ async fn save_inner( .collect(); sqlx::query("insert into plugin_instance_secrets(instance_id,secrets_json) values ($1,$2) on conflict (instance_id) do update set secrets_json=excluded.secrets_json") .bind(id).bind(sqlx::types::Json(secrets)).execute(&mut *tx).await.map_err(|_| unavailable())?; - // 与当前配置及状态一同提交,准备失败或事务冲突不能污染恢复点。 - // 停用草稿可能缺少必填参数,不能覆盖此版本最近的启用配置。 + // 与当前配置及状态一同提交,准备失败或事务冲突不能污染恢复点 + // 停用草稿可能缺少必填参数,不能覆盖此版本最近的启用配置 if instance.enabled { sqlx::query("insert into plugin_version_configurations (instance_id,artifact_sha256,configuration_json,secrets_json,bindings_json) select i.id,i.artifact_sha256,i.configuration_json,s.secrets_json,i.bindings_json from plugin_instances i join plugin_instance_secrets s on s.instance_id=i.id where i.id=$1 on conflict (instance_id,artifact_sha256) do update set configuration_json=excluded.configuration_json,secrets_json=excluded.secrets_json,bindings_json=excluded.bindings_json") .bind(id).execute(&mut *tx).await.map_err(|_| unavailable())?; } let committed_revision = admin_revision(revision)?; - // 旧配置与新配置共享事务,任一版本检查或状态提交失败都不留下半次切换。 + // 旧配置与新配置共享事务,任一版本检查或状态提交失败都不留下半次切换 for replacement in replacements { let previous_id = uuid::Uuid::parse_str(&replacement.id).map_err(|_| conflict())?; if previous_id == id || !instance.enabled { @@ -400,7 +402,7 @@ fn revision_from_i64(value: i64) -> AdminStoreResult { Revision::new(u64::try_from(value).map_err(|_| unavailable())?).map_err(|_| unavailable()) } -/// 所有停用共享全局 CAS 和审计事务,不能留下只停用一部分插件的状态。 +/// 所有停用共享全局 CAS 和审计事务,不能留下只停用一部分插件的状态 pub(super) async fn disable( pool: &PgPool, ids: &[String], diff --git a/backend/crates/gateway-store/src/postgres/plugins/mod.rs b/backend/crates/gateway-store/src/postgres/plugins/mod.rs index 90ae9ed03..72973876e 100644 --- a/backend/crates/gateway-store/src/postgres/plugins/mod.rs +++ b/backend/crates/gateway-store/src/postgres/plugins/mod.rs @@ -1,3 +1,5 @@ +//! 插件制品、实例、来源、自有资源与私有状态的 PostgreSQL 适配入口 + mod artifacts; mod credentials; mod instances; diff --git a/backend/crates/gateway-store/src/postgres/plugins/mutation.rs b/backend/crates/gateway-store/src/postgres/plugins/mutation.rs index 4e2694c25..910a2ecb3 100644 --- a/backend/crates/gateway-store/src/postgres/plugins/mutation.rs +++ b/backend/crates/gateway-store/src/postgres/plugins/mutation.rs @@ -1,4 +1,4 @@ -//! 插件写入与实例发布共用控制面锁,避免一个事务混用不同配置版本。 +//! 插件写入与实例发布共用控制面锁,避免一个事务混用不同配置版本 use gateway_admin::{ model::plugin_resources::PluginResourceOwner, @@ -27,7 +27,7 @@ pub(in crate::postgres) async fn begin_plugin_mutation<'a>( owner: &PluginResourceOwner, ) -> AdminStoreResult> { let mut tx = pool.begin().await.map_err(|_| mutation_unavailable())?; - // 与全部管理写入保持相同锁顺序;无变化的对账只锁定,不递增 revision。 + // 与全部管理写入保持相同锁顺序;无变化的对账只锁定,不递增 revision sqlx::query("select config_revision from runtime_settings where id=1 for update") .execute(&mut *tx) .await diff --git a/backend/crates/gateway-store/src/postgres/plugins/resources.rs b/backend/crates/gateway-store/src/postgres/plugins/resources.rs index c6996ed23..24b997739 100644 --- a/backend/crates/gateway-store/src/postgres/plugins/resources.rs +++ b/backend/crates/gateway-store/src/postgres/plugins/resources.rs @@ -1,4 +1,4 @@ -//! 插件资源写入在同一事务中验证当前实例、访问域与资源归属。 +//! 插件资源写入在同一事务中验证当前实例、访问域与资源归属 use super::super::{ account_groups::insert_account_group_in_transaction, @@ -268,7 +268,7 @@ impl PluginResourceStore for PgPluginStore { .await .map_err(|_| unavailable())? .ok_or_else(denied)?; - // 已删除账号自然不再需要加入;外键负责与并发账号删除保持一致。 + // 已删除账号自然不再需要加入;外键负责与并发账号删除保持一致 let added = sqlx::query( "insert into account_group_accounts(account_group_id,provider_account_id,created_at) select $1,id,now() from provider_accounts where id=any($2::text[]) diff --git a/backend/crates/gateway-store/src/postgres/plugins/sources.rs b/backend/crates/gateway-store/src/postgres/plugins/sources.rs index 3072b88c7..1a0fd1a6d 100644 --- a/backend/crates/gateway-store/src/postgres/plugins/sources.rs +++ b/backend/crates/gateway-store/src/postgres/plugins/sources.rs @@ -1,3 +1,5 @@ +//! 插件更新来源的持久化、制品绑定与出站代理引用校验 + use gateway_admin::model::audit::MutationAuditOperation; use gateway_admin::{ model::{ @@ -82,7 +84,7 @@ pub(super) async fn change( admin_revision(revision) } -/// 调用方先锁全局 revision,来源确认与制品写入共用同一个事务。 +/// 调用方先锁全局 revision,来源确认与制品写入共用同一个事务 pub(super) async fn bind( tx: &mut Transaction<'_, Postgres>, plugin_id: &str, @@ -108,7 +110,7 @@ pub(super) async fn bind( Ok(()) } -/// 与制品删除共用全局 revision 锁,最后一个版本删除后不保留重装限制。 +/// 与制品删除共用全局 revision 锁,最后一个版本删除后不保留重装限制 pub(super) async fn delete_if_unused( tx: &mut Transaction<'_, Postgres>, plugin_id: &str, diff --git a/backend/crates/gateway-store/src/postgres/plugins/state.rs b/backend/crates/gateway-store/src/postgres/plugins/state.rs index 8e74021ec..abda3a9b1 100644 --- a/backend/crates/gateway-store/src/postgres/plugins/state.rs +++ b/backend/crates/gateway-store/src/postgres/plugins/state.rs @@ -1,3 +1,5 @@ +//! 插件私有状态的 PostgreSQL 读写、代次隔离、配额与迁移事务 + use std::collections::{BTreeMap, BTreeSet}; use async_trait::async_trait; @@ -926,7 +928,7 @@ async fn writable_generation( }) } -/// 实例与状态代次在同一事务绑定;只有这里会晋升 staging generation。 +/// 实例与状态代次在同一事务绑定;只有这里会晋升 staging generation pub(super) async fn commit_configuration( transaction: &mut Transaction<'_, Postgres>, instance: &PluginInstance, @@ -1041,7 +1043,7 @@ pub(super) async fn commit_configuration( return Err(admin_conflict()); } let staging_id: uuid::Uuid = staging.try_get("id").map_err(|_| admin_unavailable())?; - // 先解除迁移来源,再替换活动代次;任一步失败都会连同配置和旧记录一起回滚。 + // 先解除迁移来源,再替换活动代次;任一步失败都会连同配置和旧记录一起回滚 sqlx::query("update plugin_state_generations set source_generation_id=null where id=$1") .bind(staging_id) .execute(&mut **transaction) @@ -1115,7 +1117,7 @@ pub(super) async fn commit_configuration( Ok(()) } -/// 紧急停用不依赖损坏包的 schema 解析;只允许原制品续绑并立即轮换 fence。 +/// 紧急停用不依赖损坏包的 schema 解析;只允许原制品续绑并立即轮换 fence pub(super) async fn rebind_existing_configuration( transaction: &mut Transaction<'_, Postgres>, instance_id: &str, diff --git a/backend/crates/gateway-store/src/postgres/pricing.rs b/backend/crates/gateway-store/src/postgres/pricing.rs index 3432e2a86..c0afb01a2 100644 --- a/backend/crates/gateway-store/src/postgres/pricing.rs +++ b/backend/crates/gateway-store/src/postgres/pricing.rs @@ -1,4 +1,4 @@ -//! 价格覆盖与请求费用明细的持久化。 +//! 价格覆盖与请求费用明细的持久化 use gateway_admin::model::pricing::{PricingChange, PricingSyncChanges, UpdatePricing}; use gateway_core::{ @@ -59,7 +59,7 @@ pub(crate) fn decode_billing_snapshot( }) }; Some(CalculatedBillingBreakdown { - // 旧快照未记录长上下文计费事实,不根据当前价格倒推历史标识。 + // 旧快照未记录长上下文计费事实,不根据当前价格倒推历史标识 long_context_billing_applied: value .get("longContextBillingApplied") .and_then(serde_json::Value::as_bool) @@ -202,7 +202,7 @@ impl PgControlPlaneRepository { .begin() .await .map_err(|_| postgres_unavailable("begin model pricing update"))?; - // 锁定当前配置再更新选中项,避免批量操作覆盖其他管理员已提交的模型。 + // 锁定当前配置再更新选中项,避免批量操作覆盖其他管理员已提交的模型 let (Json(mut pricing), Json(mut synced)) = sqlx::query_as::<_, (Json, Json)>( "select pricing_overrides_json, pricing_synced_json from runtime_settings where id = 1 for update", ) diff --git a/backend/crates/gateway-store/src/postgres/provider_accounts/admin_adapter.rs b/backend/crates/gateway-store/src/postgres/provider_accounts/admin_adapter.rs index bddbf454e..605164e4e 100644 --- a/backend/crates/gateway-store/src/postgres/provider_accounts/admin_adapter.rs +++ b/backend/crates/gateway-store/src/postgres/provider_accounts/admin_adapter.rs @@ -1,4 +1,4 @@ -//! `gateway-admin` 账号端口的 PostgreSQL adapter。 +//! `gateway-admin` 账号端口的 PostgreSQL adapter use gateway_admin::model::audit::MutationAuditOperation; use std::collections::BTreeMap; @@ -23,9 +23,9 @@ fn account_capacity( } } -/// Admin 账号用例所需的公共账号、留存观测与 revision 事务能力。 +/// Admin 账号用例所需的公共账号、留存观测与 revision 事务能力 /// -/// 三个 PostgreSQL adapter 都保持私有,调用方只能取得 [`AccountStore`] 暴露的领域能力。 +/// 三个 PostgreSQL adapter 都保持私有,调用方只能取得 [`AccountStore`] 暴露的领域能力 #[derive(Clone)] pub struct PgAdminAccountStore { pub(super) pool: PgPool, @@ -706,7 +706,7 @@ impl AccountStore for PgAdminAccountStore { })?; let result = async { - // 与管理写入采用相同锁顺序;锁住默认值后再检查账号最新设置,避免把旧快照写回。 + // 与管理写入采用相同锁顺序;锁住默认值后再检查账号最新设置,避免把旧快照写回 let default_limit: i64 = sqlx::query_scalar( "select max_concurrent_per_account from runtime_settings where id = 1 for update" ).fetch_one(&mut *transaction).await diff --git a/backend/crates/gateway-store/src/postgres/provider_accounts/admin_queries.rs b/backend/crates/gateway-store/src/postgres/provider_accounts/admin_queries.rs index 17cd58913..3e2eb4e80 100644 --- a/backend/crates/gateway-store/src/postgres/provider_accounts/admin_queries.rs +++ b/backend/crates/gateway-store/src/postgres/provider_accounts/admin_queries.rs @@ -1,4 +1,4 @@ -//! 管理账号目录的数据库分页 read model。 +//! 管理账号目录的数据库分页 read model use super::*; @@ -147,7 +147,7 @@ pub(crate) async fn load_admin_account_page( left join outbound_proxies p on p.id = a.outbound_proxy_id order by page.page_position" ); - // 动态片段只来自上面的封闭排序枚举与固定 usage predicate;所有请求值仍使用 bind。 + // 动态片段只来自上面的封闭排序枚举与固定 usage predicate;所有请求值仍使用 bind let rows = sqlx::query(sqlx::AssertSqlSafe(statement)) .bind(active_rate_limited_ids) .bind(now) diff --git a/backend/crates/gateway-store/src/postgres/provider_accounts/authorization.rs b/backend/crates/gateway-store/src/postgres/provider_accounts/authorization.rs index 8c5a7631b..bd93fc841 100644 --- a/backend/crates/gateway-store/src/postgres/provider_accounts/authorization.rs +++ b/backend/crates/gateway-store/src/postgres/provider_accounts/authorization.rs @@ -1,4 +1,4 @@ -//! 完成回执与账号、审计共享提交边界;Redis claim 只负责上游准备阶段的互斥。 +//! 完成回执与账号、审计共享提交边界;Redis claim 只负责上游准备阶段的互斥 use gateway_admin::model::provider_credentials::{ AuthorizationCommitResult, AuthorizationMutationTarget, AuthorizationReceiptKey, @@ -61,7 +61,7 @@ async fn commit_authorization_in_transaction( context: &MutationContext, ) -> AdminStoreResult { let key = &command.key; - // 同一 flow 的竞争者先等待事务结果;Redis 租约过期不能造成第二次账号提交。 + // 同一 flow 的竞争者先等待事务结果;Redis 租约过期不能造成第二次账号提交 let lock = format!( "authorization:{}:{}", key.provider_kind(), @@ -143,7 +143,7 @@ async fn commit_authorization_in_transaction( .bind(i64::try_from(result.config_revision.get()).map_err(|_| invalid_receipt())?) .bind(serde_json::to_value(accounts).map_err(|_| invalid_receipt())?) .execute(&mut **transaction).await.map_err(|_| unavailable_receipt())?; - // 保留期长于 OAuth pending 上限;每次提交有界回收,避免引入第二套维护任务。 + // 保留期长于 OAuth pending 上限;每次提交有界回收,避免引入第二套维护任务 sqlx::query("delete from authorization_receipts where ctid in (select ctid from authorization_receipts where expires_at <= now() order by expires_at limit 128 for update skip locked)") .execute(&mut **transaction).await.map_err(|_| unavailable_receipt())?; Ok(AuthorizationCommitResult { diff --git a/backend/crates/gateway-store/src/postgres/provider_accounts/core_adapter.rs b/backend/crates/gateway-store/src/postgres/provider_accounts/core_adapter.rs index 78ea89839..6edfc8a45 100644 --- a/backend/crates/gateway-store/src/postgres/provider_accounts/core_adapter.rs +++ b/backend/crates/gateway-store/src/postgres/provider_accounts/core_adapter.rs @@ -1,4 +1,4 @@ -//! Core `ProviderAccountStore` 端口适配与 core 投影映射。 +//! Core `ProviderAccountStore` 端口适配与 core 投影映射 use super::*; diff --git a/backend/crates/gateway-store/src/postgres/provider_accounts/mapping.rs b/backend/crates/gateway-store/src/postgres/provider_accounts/mapping.rs index af716415a..2b05ca61c 100644 --- a/backend/crates/gateway-store/src/postgres/provider_accounts/mapping.rs +++ b/backend/crates/gateway-store/src/postgres/provider_accounts/mapping.rs @@ -1,4 +1,4 @@ -//! Admin 账号列表、用量与成本的领域映射。 +//! Admin 账号列表、用量与成本的领域映射 use super::*; diff --git a/backend/crates/gateway-store/src/postgres/provider_accounts/mod.rs b/backend/crates/gateway-store/src/postgres/provider_accounts/mod.rs index b29d2d077..89bb4317a 100644 --- a/backend/crates/gateway-store/src/postgres/provider_accounts/mod.rs +++ b/backend/crates/gateway-store/src/postgres/provider_accounts/mod.rs @@ -1,4 +1,4 @@ -//! 明文 `provider_accounts` 与凭证 revision CAS 的唯一 PostgreSQL owner。 +//! 明文 `provider_accounts` 与凭证 revision CAS 的唯一 PostgreSQL owner use std::{collections::BTreeSet, fmt, str::FromStr}; diff --git a/backend/crates/gateway-store/src/postgres/provider_accounts/quota_forecast.rs b/backend/crates/gateway-store/src/postgres/provider_accounts/quota_forecast.rs index b244d18ae..67ad1a665 100644 --- a/backend/crates/gateway-store/src/postgres/provider_accounts/quota_forecast.rs +++ b/backend/crates/gateway-store/src/postgres/provider_accounts/quota_forecast.rs @@ -1,4 +1,4 @@ -//! 预测只读采样:先聚合数值、再读取有界的 Provider 文档,不扫描模型排行维度。 +//! 预测只读采样:先聚合数值、再读取有界的 Provider 文档,不扫描模型排行维度 use gateway_admin::model::quota_forecast_sampling::{ MAX_FORECAST_HISTORY_POINTS, QuotaForecastHistory, QuotaForecastHistoryPoint, @@ -70,8 +70,9 @@ pub(super) async fn load_history( fn history_sql() -> String { let completed_usage = completed_usage_fact_predicate("mr"); - // 一个语句共享 MVCC 快照。用 RANGE 帧让相同完成时间的点拥有相同累计值, - // 避免并发请求的任意行顺序制造不同分子;未完成请求只进入待决计数。 + // 一个语句共享 MVCC 快照 + // 用 RANGE 帧让相同完成时间的点拥有相同累计值, + // 避免并发请求的任意行顺序制造不同分子;未完成请求只进入待决计数 format!( "with scoped as materialized ( select mr.id, mr.started_at, mr.completed_at, diff --git a/backend/crates/gateway-store/src/postgres/provider_accounts/repository.rs b/backend/crates/gateway-store/src/postgres/provider_accounts/repository.rs index bc12a5673..0b47acbda 100644 --- a/backend/crates/gateway-store/src/postgres/provider_accounts/repository.rs +++ b/backend/crates/gateway-store/src/postgres/provider_accounts/repository.rs @@ -1,4 +1,4 @@ -//! Pg 账号 repository:Core/Admin 端口实现与 admin 事务。 +//! Pg 账号 repository:Core/Admin 端口实现与 admin 事务 use super::*; @@ -808,7 +808,7 @@ pub(crate) async fn rotate_provider_account_in_transaction( let upstream_user_id = replacement_identity.map(ProviderAccountIdentity::upstream_user_id); let upstream_account_id = replacement_identity.and_then(ProviderAccountIdentity::upstream_account_id); - // 事务时间可能早于应用写入的额度观测,保留既有时间下界,避免轮换破坏时间约束。 + // 事务时间可能早于应用写入的额度观测,保留既有时间下界,避免轮换破坏时间约束 let next = sqlx::query_scalar::<_, i64>( "update provider_accounts set name = case when $14 then name else $4 end, @@ -1035,7 +1035,7 @@ pub(crate) async fn finish_admin_transaction( } } -// 授权回执与账号写入需要共享事务;常规导入、刷新和轮换复用同一写入合同。 +// 授权回执与账号写入需要共享事务;常规导入、刷新和轮换复用同一写入合同 pub(super) async fn import_provider_accounts_in_transaction( transaction: &mut Transaction<'_, Postgres>, command: ImportProviderAccounts, @@ -1132,7 +1132,7 @@ pub(super) async fn rotate_provider_account_admin_in_transaction( &command.credential, ) .await?; - // 凭据 CAS、普通设置和审计共享事务,任何设置失败都回滚凭据更新。 + // 凭据 CAS、普通设置和审计共享事务,任何设置失败都回滚凭据更新 if let Some(settings) = &command.settings { let ids = std::slice::from_ref(&settings.account_id); update_provider_accounts_scheduling_in_transaction( diff --git a/backend/crates/gateway-store/src/postgres/provider_accounts/rows.rs b/backend/crates/gateway-store/src/postgres/provider_accounts/rows.rs index f240ab71b..a2cb5fbfe 100644 --- a/backend/crates/gateway-store/src/postgres/provider_accounts/rows.rs +++ b/backend/crates/gateway-store/src/postgres/provider_accounts/rows.rs @@ -1,4 +1,4 @@ -//! `provider_accounts` row 模型、解码与校验。 +//! `provider_accounts` row 模型、解码与校验 use super::*; use gateway_core::routing::AccountGroupId; @@ -9,8 +9,9 @@ pub(crate) const QUOTA_MAX_BYTES: usize = 128 * 1024; pub(crate) const MAX_ADMIN_IMPORT_BATCH: usize = 200; pub(crate) const ADMIN_USAGE_CHUNK_SIZE: usize = 200; -/// 四种窗口投影通过一组 `GROUPING SETS` 从同一批匹配请求派生。`grouping_* = 1` -/// 表示对应维度未参与该行聚合,调用方据此区分窗口、模型与成本行。 +/// 四种窗口投影通过一组 `GROUPING SETS` 从同一批匹配请求派生 +/// `grouping_* = 1` +/// 表示对应维度未参与该行聚合,调用方据此区分窗口、模型与成本行 pub(crate) fn account_usage_by_windows_sql() -> String { let completed_usage = completed_usage_fact_predicate("mr"); format!( @@ -247,7 +248,7 @@ pub struct ImportProviderAccounts { pub struct ProviderAccountAdminImport { pub config_revision: Revision, pub account_ids: Vec, - /// 与导入同一事务返回的最终凭据版本,避免提交后再查到另一轮写入。 + /// 与导入同一事务返回的最终凭据版本,避免提交后再查到另一轮写入 pub credential_revisions: std::collections::BTreeMap, } diff --git a/backend/crates/gateway-store/src/postgres/provider_accounts/runtime.rs b/backend/crates/gateway-store/src/postgres/provider_accounts/runtime.rs index 318b64f3a..be59c6c1d 100644 --- a/backend/crates/gateway-store/src/postgres/provider_accounts/runtime.rs +++ b/backend/crates/gateway-store/src/postgres/provider_accounts/runtime.rs @@ -1,4 +1,4 @@ -//! 持久账号事实与可丢失冷却事实的统一状态投影。 +//! 持久账号事实与可丢失冷却事实的统一状态投影 use std::{collections::BTreeMap, time::SystemTime}; @@ -32,10 +32,11 @@ pub(crate) fn account_status_projection( ) } -/// 尽力读取账号当前有效的 429 冷却。 +/// 尽力读取账号当前有效的 429 冷却 /// /// 冷却属于可丢失的账号级运行时事实:端口不可用、单条读取失败或脏账号 ID -/// 都视为没有冷却,不污染持久账号状态。凭据轮换不会解除上游对同一账号的限流。 +/// 都视为没有冷却,不污染持久账号状态 +/// 凭据轮换不会解除上游对同一账号的限流 pub(crate) async fn load_cooldown( cooldowns: Option<&dyn ProviderCooldownPort>, accounts: &[ProviderAccountSummary], diff --git a/backend/crates/gateway-store/src/postgres/proxies.rs b/backend/crates/gateway-store/src/postgres/proxies.rs index b520cc026..d70b28465 100644 --- a/backend/crates/gateway-store/src/postgres/proxies.rs +++ b/backend/crates/gateway-store/src/postgres/proxies.rs @@ -1,4 +1,4 @@ -//! 命名代理持有认证信息,账号保留解析后的 URL 供 Provider 传输使用。 +//! 命名代理持有认证信息,账号保留解析后的 URL 供 Provider 传输使用 use gateway_admin::model::audit::MutationAuditOperation; use std::{collections::BTreeMap, sync::Arc}; @@ -50,8 +50,8 @@ impl PgProxyRepository { } struct PgProxyImportGuard { - // 独立会话持有咨询锁,避免空闲事务超时,也不给提交事务占用的连接池制造死锁。 - // 每个仓储最多增加四个会话;取消请求或进程退出时,关闭连接会自动释放锁。 + // 独立会话持有咨询锁,避免空闲事务超时,也不给提交事务占用的连接池制造死锁 + // 每个仓储最多增加四个会话;取消请求或进程退出时,关闭连接会自动释放锁 _connection: PgConnection, _slot: OwnedSemaphorePermit, } @@ -268,7 +268,7 @@ async fn lock_url( Ok(()) } -/// 导入和旧版 URL 写入在账号事务内登记到共享代理目录。 +/// 导入和旧版 URL 写入在账号事务内登记到共享代理目录 pub(crate) async fn ensure_proxy_for_url( transaction: &mut Transaction<'_, Postgres>, proxy: &OutboundProxy, @@ -358,7 +358,7 @@ impl ProxyStore for PgProxyRepository { let revision = bump_config_revision_in_transaction(&mut transaction) .await .map_err(store_error)?; - // 在同一条更新中校验绑定,避免旧弹窗清除账号后来选择的其他代理。 + // 在同一条更新中校验绑定,避免旧弹窗清除账号后来选择的其他代理 let updated = sqlx::query( "update provider_accounts set outbound_proxy_id = null, outbound_proxy_url = null, @@ -404,7 +404,7 @@ impl ProxyStore for PgProxyRepository { .begin() .await .map_err(|_| store_error(unavailable()))?; - // 数量和当前页共用只读快照,避免绑定变化使同一次响应的分页事实不一致。 + // 数量和当前页共用只读快照,避免绑定变化使同一次响应的分页事实不一致 sqlx::query("set transaction isolation level repeatable read, read only") .execute(&mut *transaction) .await @@ -457,7 +457,7 @@ impl ProxyStore for PgProxyRepository { .collect::>>() .map_err(store_error)?; if !items.is_empty() { - // 仅批量加载当前页的分组,并沿用同一快照,避免逐账号查询。 + // 仅批量加载当前页的分组,并沿用同一快照,避免逐账号查询 let account_ids: Vec<&str> = items.iter().map(|account| account.id.as_str()).collect(); let rows = sqlx::query_as::<_, (String, String, String, String, bool)>( "select m.provider_account_id, g.id, g.name, g.color, g.enabled @@ -519,7 +519,7 @@ impl ProxyStore for PgProxyRepository { let record = match self.get(id).await { Ok(record) => record, Err(error) => { - // 拒绝预留时先等待数据库释放锁,避免连接关闭尚未生效就误挡后续代理操作。 + // 拒绝预留时先等待数据库释放锁,避免连接关闭尚未生效就误挡后续代理操作 sqlx::query("select pg_advisory_unlock_shared(hashtextextended($1, 739219))") .bind(id) .execute(&mut connection) @@ -759,7 +759,7 @@ impl ProxyStore for PgProxyRepository { .execute(&mut *transaction) .await .map_err(|error| { - // PostgreSQL 18 为 RESTRICT 返回不同于普通外键违规的错误码。 + // PostgreSQL 18 为 RESTRICT 返回不同于普通外键违规的错误码 if error.as_database_error().is_some_and(|error| { error.is_foreign_key_violation() || error.code().as_deref() == Some("23001") }) { @@ -803,7 +803,7 @@ impl ProxyStore for PgProxyRepository { exclude_active_imports(&mut transaction, id) .await .map_err(store_error)?; - // 与配置编辑保持一致的锁顺序,检测结果只有生效位置变化时才推进全局版本。 + // 与配置编辑保持一致的锁顺序,检测结果只有生效位置变化时才推进全局版本 let config_revision: i64 = sqlx::query_scalar( "select config_revision from runtime_settings where id = 1 for update", ) @@ -814,7 +814,7 @@ impl ProxyStore for PgProxyRepository { .await .map_err(store_error)?; if current.revision != revision { - // Drop 只排队回滚,返回冲突前需释放事务锁,避免误挡紧接着的编辑。 + // Drop 只排队回滚,返回冲突前需释放事务锁,避免误挡紧接着的编辑 transaction .rollback() .await diff --git a/backend/crates/gateway-store/src/postgres/retention.rs b/backend/crates/gateway-store/src/postgres/retention.rs index 4fb8d528d..3dd447dc0 100644 --- a/backend/crates/gateway-store/src/postgres/retention.rs +++ b/backend/crates/gateway-store/src/postgres/retention.rs @@ -1,4 +1,4 @@ -//! PostgreSQL 保留期投影与单批删除适配器。 +//! PostgreSQL 保留期投影与单批删除适配器 use crate::{admin_store_error, postgres_unavailable}; use async_trait::async_trait; diff --git a/backend/crates/gateway-store/src/postgres/runtime_settings.rs b/backend/crates/gateway-store/src/postgres/runtime_settings.rs index 922171f4e..d4b1ce55d 100644 --- a/backend/crates/gateway-store/src/postgres/runtime_settings.rs +++ b/backend/crates/gateway-store/src/postgres/runtime_settings.rs @@ -1,4 +1,4 @@ -//! `runtime_settings` 单例与 config revision 的 PostgreSQL owner。 +//! `runtime_settings` 单例与 config revision 的 PostgreSQL owner use std::num::NonZeroU32; use std::time::Duration; @@ -292,7 +292,7 @@ impl ProviderRuntimePolicyPort for PgRuntimeSettingsRepository { let slot = timezone.resolve_local(slot).ok_or_else(|| { ProviderStoreError::new(ProviderStoreErrorKind::InvalidData, "resolve warmup slot") })?; - // 执行游标只向前推进;设置保存不覆盖它,领取也不发布新的配置版本。 + // 执行游标只向前推进;设置保存不覆盖它,领取也不发布新的配置版本 let claimed = sqlx::query( "update runtime_settings set account_warmup_cursor = $1 where id = 1 and (account_warmup_cursor is null or account_warmup_cursor < $1)", @@ -340,7 +340,7 @@ impl ProviderRuntimePolicyPort for PgRuntimeSettingsRepository { > { Box::pin(async move { // 单条语句共享一个 MVCC 快照:先核对候选 revision,再只投影配置对象; - // Client Key 明文与其它策略字段不会进入插件准备边界。 + // Client Key 明文与其它策略字段不会进入插件准备边界 let rows = sqlx::query_as::<_, (i64, Option>)>( "select settings.config_revision, profiles.profile from runtime_settings settings @@ -591,7 +591,7 @@ pub(crate) async fn bump_config_revision_in_transaction( Revision::new(u64::try_from(next).map_err(|_| invalid_numeric())?) } -/// 更新 admin_api_key 字段,config revision 由调用方 bump。 +/// 更新 admin_api_key 字段,config revision 由调用方 bump pub(crate) async fn update_admin_api_key_in_transaction( transaction: &mut Transaction<'_, Postgres>, admin_api_key: Option, diff --git a/backend/crates/gateway-store/src/postgres/snapshot.rs b/backend/crates/gateway-store/src/postgres/snapshot.rs index 477ea5bee..c63158c0f 100644 --- a/backend/crates/gateway-store/src/postgres/snapshot.rs +++ b/backend/crates/gateway-store/src/postgres/snapshot.rs @@ -1,4 +1,4 @@ -//! 终态配置表的一致性 `RuntimeSnapshot` 输入读取。 +//! 终态配置表的一致性 `RuntimeSnapshot` 输入读取 use std::collections::BTreeMap; diff --git a/backend/crates/gateway-store/src/postgres/usage_facts.rs b/backend/crates/gateway-store/src/postgres/usage_facts.rs index 3176493f4..211ab174d 100644 --- a/backend/crates/gateway-store/src/postgres/usage_facts.rs +++ b/backend/crates/gateway-store/src/postgres/usage_facts.rs @@ -1,18 +1,20 @@ -//! `model_requests` 完成用量事实的 PostgreSQL 语义。 +//! `model_requests` 完成用量事实的 PostgreSQL 语义 use sqlx::{Postgres, QueryBuilder}; -/// 只把已完整交付给客户端、且存在用量证据的成功响应投影为用量事实。 +/// 只把已完整交付给客户端、且存在用量证据的成功响应投影为用量事实 /// /// `model_requests` 同时承担执行审计:包括上游已发送、但尚未收到首个事件就断开的 -/// WebSocket 请求。这些失败仍必须留给 Ops Errors 和调度健康度分析,不能混入用量、 -/// 成本、账号使用次数或请求明细。 +/// WebSocket 请求 +/// 这些失败仍必须留给 Ops Errors 和调度健康度分析,不能混入用量、 +/// 成本、账号使用次数或请求明细 /// Provider 原生端点也会写入执行审计;standalone search 没有模型、Token 或费用事实, -/// 因此不属于用量记录。图片端点由请求意图证明其用量语义,不依赖文本模型字段。 +/// 因此不属于用量记录 +/// 图片端点由请求意图证明其用量语义,不依赖文本模型字段 /// 客户端 WebSocket 的每个 `response.create` 没有独立 HTTP 状态码;成功终态与下游 -/// 提交边界已足以证明交付,连接握手的 101 不能冒充单次请求状态。 +/// 提交边界已足以证明交付,连接握手的 101 不能冒充单次请求状态 /// OpenAI Provider 将 `generate=false` 归类为 `prewarm`:它只准备连接与上下文, -/// 即使响应带有模型和输入 Token,也不是推理用量;原始审计和费用事实仍保留。 +/// 即使响应带有模型和输入 Token,也不是推理用量;原始审计和费用事实仍保留 pub(crate) fn completed_usage_fact_predicate(alias: &str) -> String { let evidence = usage_evidence_predicate(alias); format!( @@ -47,9 +49,9 @@ pub(crate) fn push_completed_usage_fact_filter(query: &mut QueryBuilder, alias: &str) { query.push(format!(" and {alias}.recovered_at is null")); } diff --git a/backend/crates/gateway-store/src/redis/admin_account_runtime.rs b/backend/crates/gateway-store/src/redis/admin_account_runtime.rs index 5014698db..19a18a470 100644 --- a/backend/crates/gateway-store/src/redis/admin_account_runtime.rs +++ b/backend/crates/gateway-store/src/redis/admin_account_runtime.rs @@ -1,4 +1,4 @@ -//! Admin query service 使用的账号运行态组合 adapter。 +//! Admin query service 使用的账号运行态组合 adapter use std::collections::BTreeMap; @@ -15,7 +15,7 @@ use super::{ RedisCredentialCooldownRepository, RedisCredentialLeaseRepository, }; -/// 只组合可丢失 Redis 事实;不持有 PostgreSQL,也不执行状态投影。 +/// 只组合可丢失 Redis 事实;不持有 PostgreSQL,也不执行状态投影 #[derive(Clone)] pub struct RedisAdminAccountRuntimeStore { cooldowns: RedisCredentialCooldownRepository, diff --git a/backend/crates/gateway-store/src/redis/artifact_profile.rs b/backend/crates/gateway-store/src/redis/artifact_profile.rs index dc45ae920..ee05c7cd9 100644 --- a/backend/crates/gateway-store/src/redis/artifact_profile.rs +++ b/backend/crates/gateway-store/src/redis/artifact_profile.rs @@ -1,4 +1,4 @@ -//! Provider 官方制品画像的单键 Redis cache。 +//! Provider 官方制品画像的单键 Redis cache use std::time::{Duration, SystemTime}; @@ -35,7 +35,7 @@ redis.call('PEXPIRE', KEYS[1], ARGV[4]) return 1 "#; -/// 每个 Provider 的各制品使用独立覆盖写 key;TTL 负责进程长期停机后的最终清理。 +/// 每个 Provider 的各制品使用独立覆盖写 key;TTL 负责进程长期停机后的最终清理 #[derive(Clone)] pub struct RedisProviderArtifactProfileRepository { connection: ConnectionManager, diff --git a/backend/crates/gateway-store/src/redis/auth.rs b/backend/crates/gateway-store/src/redis/auth.rs index 86c9fbada..33511395b 100644 --- a/backend/crates/gateway-store/src/redis/auth.rs +++ b/backend/crates/gateway-store/src/redis/auth.rs @@ -1,4 +1,4 @@ -//! 控制面统一会话与双层固定窗口登录限流。 +//! 控制面统一会话与双层固定窗口登录限流 use std::time::Duration; @@ -21,13 +21,13 @@ end return 0 "#; -/// Redis 身份标签只由认证服务写入,不能从请求中的角色声明构造。 +/// Redis 身份标签只由认证服务写入,不能从请求中的角色声明构造 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)] pub enum SessionSubjectRecord { Admin { admin_user_id: String, - // 旧会话没有指纹,按未认证处理并要求重新登录。 + // 旧会话没有指纹,按未认证处理并要求重新登录 #[serde(default)] credential_fingerprint: String, }, @@ -49,7 +49,7 @@ impl SessionSubjectRecord { } } -/// Redis 中不含密码或原始 API Key 的统一会话事实。 +/// Redis 中不含密码或原始 API Key 的统一会话事实 #[derive(Debug, Clone, PartialEq, Eq)] pub struct AuthSessionRecord { pub subject: SessionSubjectRecord, diff --git a/backend/crates/gateway-store/src/redis/client_admission.rs b/backend/crates/gateway-store/src/redis/client_admission.rs index 013721c04..9166e5f4f 100644 --- a/backend/crates/gateway-store/src/redis/client_admission.rs +++ b/backend/crates/gateway-store/src/redis/client_admission.rs @@ -1,4 +1,4 @@ -//! Client API Key 的 Redis RPM/并发原子准入与热状态恢复。 +//! Client API Key 的 Redis RPM/并发原子准入与热状态恢复 use std::{collections::HashSet, time::Duration}; @@ -17,7 +17,7 @@ use crate::{StoreError, StoreResult, redis_unavailable, require_nonempty}; use super::{MAX_REDIS_EXACT_INTEGER, namespace, resource_fingerprint}; -// 只刷新既有并发成员,不写 RPM 窗口,也不复活已过期或已释放的租约。 +// 只刷新既有并发成员,不写 RPM 窗口,也不复活已过期或已释放的租约 const RENEW_SCRIPT: &str = r#" local clock = redis.call('TIME') local now_ms = (tonumber(clock[1]) * 1000) + math.floor(tonumber(clock[2]) / 1000) diff --git a/backend/crates/gateway-store/src/redis/coordination_buffer.rs b/backend/crates/gateway-store/src/redis/coordination_buffer.rs index 59e74bcaf..aa138d672 100644 --- a/backend/crates/gateway-store/src/redis/coordination_buffer.rs +++ b/backend/crates/gateway-store/src/redis/coordination_buffer.rs @@ -1,7 +1,7 @@ -//! 可由 TTL 或后续反馈收敛的 Redis 写副作用队列。 +//! 可由 TTL 或后续反馈收敛的 Redis 写副作用队列 //! //! Continuation affinity 决定下一轮 Provider 与账号,必须在响应 ID 可复用前 -//! 获得 Redis 确认,因此不属于本模块的可丢失副作用。 +//! 获得 Redis 确认,因此不属于本模块的可丢失副作用 use std::sync::Arc; use std::{ @@ -23,7 +23,7 @@ use tokio::sync::{Mutex, mpsc}; const DEFAULT_QUEUE_CAPACITY: usize = 4_096; const SHUTDOWN_DRAIN_TIMEOUT: Duration = Duration::from_secs(2); -/// 准入读取保持强一致;终态释放只做有界入队,Redis 失败由 worker 吸收。 +/// 准入读取保持强一致;终态释放只做有界入队,Redis 失败由 worker 吸收 #[derive(Clone)] pub struct BufferedClientAdmissionPort { inner: Arc, diff --git a/backend/crates/gateway-store/src/redis/credential_cooldown.rs b/backend/crates/gateway-store/src/redis/credential_cooldown.rs index 354d0a307..9cd9bdbcb 100644 --- a/backend/crates/gateway-store/src/redis/credential_cooldown.rs +++ b/backend/crates/gateway-store/src/redis/credential_cooldown.rs @@ -1,4 +1,4 @@ -//! 请求调度用的可丢失 Provider 级 cooldown Redis 存储。 +//! 请求调度用的可丢失 Provider 级 cooldown Redis 存储 use async_trait::async_trait; use chrono::{DateTime, Utc}; @@ -65,7 +65,7 @@ if #KEYS > 1 then redis.call('ZREM', KEYS[2], ARGV[2]) end return 1 "#; -// 普通推理成功只能清除临时限流和未形成冻结的证据,判断与删除必须原子执行。 +// 普通推理成功只能清除临时限流和未形成冻结的证据,判断与删除必须原子执行 const SUCCESS_SCRIPT: &str = r#" local current = tonumber(redis.call('HGET', KEYS[1], 'revision') or '0') local kind = redis.call('HGET', KEYS[1], 'kind') or 'rate_limit' @@ -75,7 +75,7 @@ redis.call('ZREM', KEYS[2], ARGV[2]) return 1 "#; -// 探测结果只能修改读到的这一代冻结;删除后重建同 revision 的冻结也不匹配。 +// 探测结果只能修改读到的这一代冻结;删除后重建同 revision 的冻结也不匹配 const FINISH_FREEZE_SCRIPT: &str = r#" if redis.call('HGET', KEYS[1], 'generation') ~= ARGV[2] or redis.call('HGET', KEYS[1], 'revision') ~= ARGV[1] then return 0 end @@ -100,7 +100,7 @@ return 1 "#; // 每次容量失败都顺延窗口 TTL(与刷新退避计数同语义),并把本次观测到的 -// 在途并发并入峰值证据;峰值与计数共享同一窗口生命周期。 +// 在途并发并入峰值证据;峰值与计数共享同一窗口生命周期 const RECORD_CAPACITY_FAILURE_SCRIPT: &str = r#" local count = redis.call('INCR', KEYS[1]) local ttl_ms = tonumber(ARGV[1]) @@ -137,7 +137,7 @@ pub trait CredentialCooldownRepository: Send + Sync { provider_account_id: &str, through_revision: Revision, ) -> StoreResult; - /// 删除账号时清除该账号全部 account/model scope cooldown key。 + /// 删除账号时清除该账号全部 account/model scope cooldown key async fn delete_account_cooldowns(&self, provider_account_id: &str) -> StoreResult; } @@ -335,7 +335,7 @@ impl RedisCredentialCooldownRepository { }) } - /// 包含已到探测时间但尚未确认恢复的冻结;到期不能从 worker 工作集中移除。 + /// 包含已到探测时间但尚未确认恢复的冻结;到期不能从 worker 工作集中移除 pub(crate) async fn active_freezes( &self, ) -> StoreResult> { @@ -388,7 +388,7 @@ impl RedisCredentialCooldownRepository { Ok(changed == 1) } - /// 读取窗口内观测到的在途并发峰值;key 随窗口 TTL 过期,无需额外清理。 + /// 读取窗口内观测到的在途并发峰值;key 随窗口 TTL 过期,无需额外清理 pub(crate) async fn read_capacity_peak( &self, provider_account_id: &str, @@ -470,8 +470,8 @@ impl CredentialCooldownRepository for RedisCredentialCooldownRepository { "provider_account_id", provider_account_id, )?; - // 账号删除:清除该账号的 account key 与全部 model-scoped key。 - // 用 SCAN 精确匹配命名空间内该账号前缀,避免 KEYS 阻塞。 + // 账号删除:清除该账号的 account key 与全部 model-scoped key + // 用 SCAN 精确匹配命名空间内该账号前缀,避免 KEYS 阻塞 let mut connection = self.connection.clone(); let mut keys = vec![ self.key(provider_account_id)?, @@ -500,7 +500,7 @@ impl CredentialCooldownRepository for RedisCredentialCooldownRepository { break; } } - // 删除与索引移除同属一个原子边界;否则新冻结可能在两步之间写入后丢失索引。 + // 删除与索引移除同属一个原子边界;否则新冻结可能在两步之间写入后丢失索引 let removed: i64 = Script::new( r#" local removed = 0 diff --git a/backend/crates/gateway-store/src/redis/credential_leases.rs b/backend/crates/gateway-store/src/redis/credential_leases.rs index 7448c44dc..40ebcfd7f 100644 --- a/backend/crates/gateway-store/src/redis/credential_leases.rs +++ b/backend/crates/gateway-store/src/redis/credential_leases.rs @@ -1,4 +1,4 @@ -//! Provider、账号与 OAuth refresh 的 Redis lease/fencing。 +//! Provider、账号与 OAuth refresh 的 Redis lease/fencing use std::collections::BTreeMap; use std::fmt; @@ -162,7 +162,7 @@ impl CredentialLeaseRequest { } } -/// 对任意明确资源施加并发数和启动间隔约束的计数 lease。 +/// 对任意明确资源施加并发数和启动间隔约束的计数 lease #[derive(Debug, Clone, PartialEq, Eq)] pub struct CredentialBoundedLeaseRequest { pub scope: CredentialLeaseScope, @@ -202,7 +202,7 @@ pub struct CredentialLeaseGrant { pub expires_at: DateTime, } -/// Redis 可丢失的账号调度信号。 +/// Redis 可丢失的账号调度信号 #[derive(Debug, Clone, PartialEq, Eq)] pub struct CredentialRuntimeSignal { pub resource_id: String, @@ -227,7 +227,7 @@ impl fmt::Debug for CredentialBoundedLeaseAcquisition { } } -/// Drop 时在当前 Tokio runtime 上尽力释放;进程崩溃由 Redis TTL 回收。 +/// Drop 时在当前 Tokio runtime 上尽力释放;进程崩溃由 Redis TTL 回收 pub struct CredentialLeaseGuard { repository: RedisCredentialLeaseRepository, request: CredentialLeaseRequest, @@ -235,7 +235,7 @@ pub struct CredentialLeaseGuard { } impl CredentialLeaseGuard { - /// 自动续期仅属于返回 guard;丢失租约会取消依赖该槽位的请求。 + /// 自动续期仅属于返回 guard;丢失租约会取消依赖该槽位的请求 pub fn maintain( self, deadline: gateway_core::lifecycle::Deadline, @@ -285,7 +285,7 @@ impl CredentialLeaseGuard { } struct RenewingSchedulingLease { - // 先停止续期再释放 Redis 成员,防止释放与续期交错。 + // 先停止续期再释放 Redis 成员,防止释放与续期交错 _renewal: crate::lease_renewal::LeaseRenewal, _guard: CredentialLeaseGuard, } @@ -357,7 +357,7 @@ impl RedisCredentialLeaseRepository { }) } - /// 获取带 Drop 释放语义的通用 lease guard,供 Provider refresh/task 组合器使用。 + /// 获取带 Drop 释放语义的通用 lease guard,供 Provider refresh/task 组合器使用 pub async fn try_acquire_guard( &self, request: CredentialLeaseRequest, @@ -370,7 +370,7 @@ impl RedisCredentialLeaseRepository { })) } - /// 原子推进跨进程共享、按 Client Key 与 Provider 隔离的调度游标。 + /// 原子推进跨进程共享、按 Client Key 与 Provider 隔离的调度游标 pub async fn advance_scheduling_cursor( &self, client_api_key_id: &ClientApiKeyId, @@ -502,7 +502,7 @@ impl RedisCredentialLeaseRepository { } } -/// Store-owned 的通用 Provider lease 能力;具体 Provider 不感知 Redis。 +/// Store-owned 的通用 Provider lease 能力;具体 Provider 不感知 Redis pub(crate) struct RedisProviderLeaseCoordinator { repository: RedisCredentialLeaseRepository, process_id: String, diff --git a/backend/crates/gateway-store/src/redis/credential_state.rs b/backend/crates/gateway-store/src/redis/credential_state.rs index dda7a1ce0..438e6eae6 100644 --- a/backend/crates/gateway-store/src/redis/credential_state.rs +++ b/backend/crates/gateway-store/src/redis/credential_state.rs @@ -1,4 +1,4 @@ -//! Provider credential state 的可重建 Redis cache fence。 +//! Provider credential state 的可重建 Redis cache fence use std::time::Duration; @@ -76,7 +76,8 @@ pub trait CredentialStateRepository: Send + Sync { async fn clear_credential_state(&self, provider_account_id: &str) -> StoreResult; } -/// Provider 目录 cache 的隔离键。具体作用域由对应 Provider 决定。 +/// Provider 目录 cache 的隔离键 +/// 具体作用域由对应 Provider 决定 #[derive(Debug, Clone, PartialEq, Eq)] pub struct RedisProviderCatalogCacheKey { pub provider_kind: String, diff --git a/backend/crates/gateway-store/src/redis/mod.rs b/backend/crates/gateway-store/src/redis/mod.rs index 5c71dad53..9e09ab0fa 100644 --- a/backend/crates/gateway-store/src/redis/mod.rs +++ b/backend/crates/gateway-store/src/redis/mod.rs @@ -1,4 +1,4 @@ -//! 可丢失、可从 PostgreSQL 或 Provider 重建的 Redis 协调状态。 +//! 可丢失、可从 PostgreSQL 或 Provider 重建的 Redis 协调状态 use sha2::{Digest, Sha256}; diff --git a/backend/crates/gateway-store/src/redis/native_continuation.rs b/backend/crates/gateway-store/src/redis/native_continuation.rs index d144c959f..4fa563549 100644 --- a/backend/crates/gateway-store/src/redis/native_continuation.rs +++ b/backend/crates/gateway-store/src/redis/native_continuation.rs @@ -1,9 +1,10 @@ -//! Responses continuation 的 Redis 会话亲和记录。 +//! Responses continuation 的 Redis 会话亲和记录 //! //! 该存储只保存可丢失的 response → Provider/account pin;PostgreSQL 审计记录 -//! 不是 continuation 的可用性前提。键只含 response ID 的不可逆指纹;Client Key -//! 所有权写入 payload 并在读取时严格校验,使跨 Key 复用能够 fail closed。 -//! Provider 私有状态作为不透明 JSON 保存,Store 不解释其内容。 +//! 不是 continuation 的可用性前提 +//! 键只含 response ID 的不可逆指纹;Client Key +//! 所有权写入 payload 并在读取时严格校验,使跨 Key 复用能够 fail closed +//! Provider 私有状态作为不透明 JSON 保存,Store 不解释其内容 use std::time::{Duration, SystemTime, UNIX_EPOCH}; @@ -66,7 +67,7 @@ end return 1 "#; -/// 基于 Redis 的 best-effort 原生响应 continuation 亲和。 +/// 基于 Redis 的 best-effort 原生响应 continuation 亲和 #[derive(Clone)] pub struct RedisNativeContinuationRepository { connection: ConnectionManager, diff --git a/backend/crates/gateway-store/src/redis/oauth_pending.rs b/backend/crates/gateway-store/src/redis/oauth_pending.rs index 78c17c5a3..6685490c5 100644 --- a/backend/crates/gateway-store/src/redis/oauth_pending.rs +++ b/backend/crates/gateway-store/src/redis/oauth_pending.rs @@ -1,4 +1,4 @@ -//! OAuth 授权临时状态的 Redis 原子持久化。 +//! OAuth 授权临时状态的 Redis 原子持久化 use std::time::Duration; @@ -128,7 +128,7 @@ redis.call('DEL', KEYS[1]) return 1 "#; -/// OAuth pending flow 的 Redis 原子持久化能力。 +/// OAuth pending flow 的 Redis 原子持久化能力 #[derive(Clone)] pub struct RedisOAuthPendingFlowRepository { connection: ConnectionManager, diff --git a/backend/crates/gateway-store/src/redis/provider_session_affinity.rs b/backend/crates/gateway-store/src/redis/provider_session_affinity.rs index 0ae3d6c9e..ceabaed5c 100644 --- a/backend/crates/gateway-store/src/redis/provider_session_affinity.rs +++ b/backend/crates/gateway-store/src/redis/provider_session_affinity.rs @@ -1,4 +1,4 @@ -//! Provider 会话亲和性的可丢失 Redis 映射。 +//! Provider 会话亲和性的可丢失 Redis 映射 use std::time::Duration; diff --git a/backend/crates/gateway-store/src/redis/provider_session_exclusion.rs b/backend/crates/gateway-store/src/redis/provider_session_exclusion.rs index 7d3d6a5e2..cbbdc5590 100644 --- a/backend/crates/gateway-store/src/redis/provider_session_exclusion.rs +++ b/backend/crates/gateway-store/src/redis/provider_session_exclusion.rs @@ -1,7 +1,7 @@ -//! Provider 会话级账号排除的可丢失 Redis 状态。 +//! Provider 会话级账号排除的可丢失 Redis 状态 //! //! Redis 只保存 Provider 派生的不可逆会话键对应的账号 ID 集合及 revision; -//! 不读取或保存任何 Provider 协议正文。 +//! 不读取或保存任何 Provider 协议正文 use std::collections::BTreeSet; use std::time::Duration; diff --git a/backend/crates/gateway-store/src/redis/runtime_change.rs b/backend/crates/gateway-store/src/redis/runtime_change.rs index be6ae3cba..fb92e8bd8 100644 --- a/backend/crates/gateway-store/src/redis/runtime_change.rs +++ b/backend/crates/gateway-store/src/redis/runtime_change.rs @@ -1,4 +1,4 @@ -//! PostgreSQL revision 提交后的可丢失 Redis Pub/Sub 通知。 +//! PostgreSQL revision 提交后的可丢失 Redis Pub/Sub 通知 use std::pin::Pin; diff --git a/backend/crates/gateway-store/src/redis/worker_lease.rs b/backend/crates/gateway-store/src/redis/worker_lease.rs index 5f5868126..d37db1beb 100644 --- a/backend/crates/gateway-store/src/redis/worker_lease.rs +++ b/backend/crates/gateway-store/src/redis/worker_lease.rs @@ -1,4 +1,4 @@ -//! 多实例 worker leader lease 的 Redis 实现。 +//! 多实例 worker leader lease 的 Redis 实现 use std::num::NonZeroU64; diff --git a/backend/crates/gateway-store/src/value.rs b/backend/crates/gateway-store/src/value.rs index d934a562c..e8d422046 100644 --- a/backend/crates/gateway-store/src/value.rs +++ b/backend/crates/gateway-store/src/value.rs @@ -1,15 +1,15 @@ -//! Store 值类型、错误与跨层映射。 +//! Store 值类型、错误与跨层映射 use super::*; -/// 发生错误的基础设施边界。 +/// 发生错误的基础设施边界 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum StoreBackend { PostgreSql, Redis, } -/// 上层状态机需要区分的稳定冲突类型。 +/// 上层状态机需要区分的稳定冲突类型 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ConflictKind { StaleRevision, @@ -22,7 +22,7 @@ pub enum ConflictKind { FencingTokenStale, } -/// Store adapter 的稳定错误边界。 +/// Store adapter 的稳定错误边界 #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] pub enum StoreError { #[error("{backend:?} store is unavailable: {message}")] @@ -123,7 +123,7 @@ impl Revision { } } -/// `numeric(20,10)` 可无损表达的非负金额。 +/// `numeric(20,10)` 可无损表达的非负金额 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct DecimalAmount(String); @@ -176,7 +176,8 @@ impl FromStr for DecimalAmount { } } -/// Provider-owned JSON object。Store 只验证 object 与大小,不解释内部 key。 +/// Provider-owned JSON object +/// Store 只验证 object 与大小,不解释内部 key #[derive(Clone, PartialEq)] pub struct JsonObject(Map); diff --git a/backend/crates/gateway-store/src/workers.rs b/backend/crates/gateway-store/src/workers.rs index 12af9a7a6..df7463b58 100644 --- a/backend/crates/gateway-store/src/workers.rs +++ b/backend/crates/gateway-store/src/workers.rs @@ -1,4 +1,4 @@ -//! Worker 贡献、调度定义与健康探针。 +//! Worker 贡献、调度定义与健康探针 use super::*; use gateway_core::task::DaemonTask; @@ -11,7 +11,7 @@ pub(crate) struct CommandStoreWriters { pub(crate) admission_release: redis::ClientAdmissionReleaseWriter, } -/// 短生命周期 CLI 只运行数据面必需的三个写泵,不注册恢复、保留或维护 Worker。 +/// 短生命周期 CLI 只运行数据面必需的三个写泵,不注册恢复、保留或维护 Worker pub struct CommandStoreDrain { cancellation: gateway_core::lifecycle::CancellationToken, tasks: Vec>>, @@ -58,7 +58,7 @@ impl CommandStoreDrain { pub(crate) async fn shutdown(mut self) -> Result<(), CommandStoreDrainError> { let deadline = std::time::Instant::now() + COMMAND_DRAIN_TIMEOUT; // CLI 调用方已结束数据面会话;先让已接收的 execution 写入在正常 writer - // 路径完成,避免立即取消后落入更短的常驻进程关闭丢弃窗口。 + // 路径完成,避免立即取消后落入更短的常驻进程关闭丢弃窗口 let mut failed = !self.execution_idle.wait_until(deadline).await; self.cancellation.cancel(); for mut task in self.tasks.drain(..) { diff --git a/backend/crates/gateway-store/tests/backup/mod.rs b/backend/crates/gateway-store/tests/backup/mod.rs index 67039598d..756ff7d0c 100644 --- a/backend/crates/gateway-store/tests/backup/mod.rs +++ b/backend/crates/gateway-store/tests/backup/mod.rs @@ -1 +1,3 @@ +//! 备份存储适配器的测试入口 + mod s3; diff --git a/backend/crates/gateway-store/tests/backup/s3.rs b/backend/crates/gateway-store/tests/backup/s3.rs index c662ab23f..cf7efee07 100644 --- a/backend/crates/gateway-store/tests/backup/s3.rs +++ b/backend/crates/gateway-store/tests/backup/s3.rs @@ -1,3 +1,5 @@ +//! 验证 S3 上传完整发送超过异步文件默认缓冲区的分块 + use std::io; use chrono::Utc; diff --git a/backend/crates/gateway-store/tests/bundle.rs b/backend/crates/gateway-store/tests/bundle.rs index f7b37ae68..8be9e20f0 100644 --- a/backend/crates/gateway-store/tests/bundle.rs +++ b/backend/crates/gateway-store/tests/bundle.rs @@ -1,3 +1,5 @@ +//! 验证只读检查 Bundle 不执行迁移且拒绝业务写入 + use gateway_core::{ account::{ CredentialRevision, NewProviderAccount, PlaintextCredential, ProviderAccount, diff --git a/backend/crates/gateway-store/tests/config.rs b/backend/crates/gateway-store/tests/config.rs index dc699a47a..83e81890b 100644 --- a/backend/crates/gateway-store/tests/config.rs +++ b/backend/crates/gateway-store/tests/config.rs @@ -1,3 +1,5 @@ +//! 验证存储配置的备份路径与连接池容量约束 + use gateway_store::{StoreConfig, StoreError, StorePoolConfig}; const PASSWORD: &str = "111111111111111111111111111111111111111111111111"; diff --git a/backend/crates/gateway-store/tests/main.rs b/backend/crates/gateway-store/tests/main.rs index 10559529c..ce7b14283 100644 --- a/backend/crates/gateway-store/tests/main.rs +++ b/backend/crates/gateway-store/tests/main.rs @@ -1,3 +1,5 @@ +//! PostgreSQL、Redis 与备份存储的测试入口 + mod backup; mod bundle; mod config; diff --git a/backend/crates/gateway-store/tests/postgres/account_groups.rs b/backend/crates/gateway-store/tests/postgres/account_groups.rs index 34863d481..a4612867e 100644 --- a/backend/crates/gateway-store/tests/postgres/account_groups.rs +++ b/backend/crates/gateway-store/tests/postgres/account_groups.rs @@ -1,3 +1,5 @@ +//! 验证账号分组聚合、Key 绑定与分组策略的持久化 + use std::collections::BTreeMap; use gateway_admin::{ @@ -89,7 +91,7 @@ async fn groups_aggregate_cross_provider_members_and_key_bindings_without_multip .await .expect("create scoped client key"); } - // 多分组 Key 的请求费用只计入实际承接账号所属的分组,Key 绑定的其他分组不重复计费。 + // 多分组 Key 的请求费用只计入实际承接账号所属的分组,Key 绑定的其他分组不重复计费 seed_group_cost_snapshot( &database.pool, "req_dual_group_key", @@ -98,7 +100,7 @@ async fn groups_aggregate_cross_provider_members_and_key_bindings_without_multip "1.5", ) .await; - // 归属跟随完成请求的账号,而不是 Client Key 绑定的分组快照。 + // 归属跟随完成请求的账号,而不是 Client Key 绑定的分组快照 seed_group_cost_snapshot( &database.pool, "req_account_attribution", @@ -142,7 +144,7 @@ async fn groups_aggregate_cross_provider_members_and_key_bindings_without_multip BTreeMap::from([("openai".to_owned(), 1), ("xai".to_owned(), 1)]) ); assert_eq!(mixed.client_key_count, 2); - // PostgreSQL 返回持久页与 member facts;实时状态/容量由 Admin query service 投影。 + // PostgreSQL 返回持久页与 member facts;实时状态/容量由 Admin query service 投影 assert_eq!(mixed.account_summary.available, 0); assert_eq!(mixed.account_summary.limited, 0); assert_eq!(mixed.account_summary.total, 0); diff --git a/backend/crates/gateway-store/tests/postgres/admin_security_audit.rs b/backend/crates/gateway-store/tests/postgres/admin_security_audit.rs index ffc9dd098..837e00d89 100644 --- a/backend/crates/gateway-store/tests/postgres/admin_security_audit.rs +++ b/backend/crates/gateway-store/tests/postgres/admin_security_audit.rs @@ -1,3 +1,5 @@ +//! 验证密码变更与安全审计原子提交及审计字段限制 + use chrono::Utc; use gateway_store::postgres::{AdminAuditActorKind, AdminAuditEvent}; @@ -48,7 +50,7 @@ async fn password_change_commits_audit_atomically_and_rejects_concurrent_old_has .unwrap(); assert_eq!(count, 1); - // 制造真实审计约束失败,确认密码更新随同事务回滚。 + // 制造真实审计约束失败,确认密码更新随同事务回滚 let mut invalid_audit = audit("invalid-change"); invalid_audit.actor_admin_user_id = Some("missing-admin".into()); assert!( diff --git a/backend/crates/gateway-store/tests/postgres/admission_recovery.rs b/backend/crates/gateway-store/tests/postgres/admission_recovery.rs index 56d7e5d27..fc0d9b794 100644 --- a/backend/crates/gateway-store/tests/postgres/admission_recovery.rs +++ b/backend/crates/gateway-store/tests/postgres/admission_recovery.rs @@ -1,3 +1,5 @@ +//! 验证准入恢复读取精确时间窗口与运行中请求事实 + use chrono::{DateTime, Duration, Utc}; use gateway_store::postgres::{ ClientAdmissionRecentRequest, ClientAdmissionRecovery, ClientAdmissionRecoveryRepository, diff --git a/backend/crates/gateway-store/tests/postgres/backup.rs b/backend/crates/gateway-store/tests/postgres/backup.rs index 48a29948e..778a5fd92 100644 --- a/backend/crates/gateway-store/tests/postgres/backup.rs +++ b/backend/crates/gateway-store/tests/postgres/backup.rs @@ -1,4 +1,4 @@ -//! `PgBackupRepository` 的真实 PostgreSQL 集成测试。 +//! `PgBackupRepository` 的真实 PostgreSQL 集成测试 use chrono::Utc; use secrecy::SecretString; @@ -254,7 +254,7 @@ async fn active_task_unique_index_blocks_second_queued() { .expect_err("second queued must conflict"); assert_eq!(error.kind(), AdminStoreErrorKind::Conflict); - // 第一个任务完成释放名额后,第二个可以插入。 + // 第一个任务完成释放名额后,第二个可以插入 let claimed = repository .claim_next_queued(Utc::now()) .await @@ -352,7 +352,7 @@ async fn storage_identity_is_locked_once_records_exist() { command.endpoint = "https://two.example.com".to_owned(); command }; - // 尚无记录时允许切换 endpoint。 + // 尚无记录时允许切换 endpoint repository .update_storage_settings(changed_endpoint, &context()) .await @@ -369,7 +369,7 @@ async fn storage_identity_is_locked_once_records_exist() { .expect_err("endpoint change must be rejected with records"); assert_eq!(locked.kind(), AdminStoreErrorKind::Conflict); - // 只轮换凭据与修改 prefix 仍然允许。 + // 只轮换凭据与修改 prefix 仍然允许 let rotation = { let mut command = storage_command("https://two.example.com"); command.access_key_id = "new-ak".to_owned(); @@ -392,7 +392,7 @@ async fn schedule_update_persists_cursor_and_clears_on_disable() { }; let repository = PgBackupRepository::new(db.pool.clone()); - // 启用计划前必须先配置存储并通过连接测试(DB 约束要求)。 + // 启用计划前必须先配置存储并通过连接测试(DB 约束要求) repository .update_storage_settings(storage_command("https://one.example.com"), &context()) .await diff --git a/backend/crates/gateway-store/tests/postgres/client_budgets.rs b/backend/crates/gateway-store/tests/postgres/client_budgets.rs index 365c7b554..03334f105 100644 --- a/backend/crates/gateway-store/tests/postgres/client_budgets.rs +++ b/backend/crates/gateway-store/tests/postgres/client_budgets.rs @@ -1,3 +1,5 @@ +//! 验证 Key 预算重置、在途结算、时间窗口与审计事务 + use std::{collections::BTreeMap, time::SystemTime}; use chrono::{DateTime, Utc}; @@ -124,7 +126,7 @@ async fn manual_reset_clears_selected_windows_and_preserves_policy_and_history() .then_some(before.weekly_resets_at) .flatten() ); - // 同一费用重试既不重复扣额,也不能重新开启已重置的窗口。 + // 同一费用重试既不重复扣额,也不能重新开启已重置的窗口 store.settle(billed).await.unwrap(); assert_eq!(status(&database, key).await, after); assert_eq!( @@ -173,7 +175,7 @@ async fn manual_reset_restarts_selected_windows_only_after_new_usage() { .await .unwrap(); let reset = status(&database, key).await; - // 重置前完成的迟到费用保留历史,但不能开启窗口或重新扣入所选周期。 + // 重置前完成的迟到费用保留历史,但不能开启窗口或重新扣入所选周期 store.settle(delayed).await.unwrap(); let delayed = status(&database, key).await; assert_eq!(delayed.daily_resets_at, reset.daily_resets_at); @@ -207,7 +209,7 @@ async fn manual_reset_restarts_selected_windows_only_after_new_usage() { ); assert_ne!(restarted.weekly_resets_at, before.weekly_resets_at); } - // 开启窗口后仍保留重置边界,另一笔重置前的迟到费用也不能重新扣额。 + // 开启窗口后仍保留重置边界,另一笔重置前的迟到费用也不能重新扣额 store.settle(old).await.unwrap(); let after_old = status(&database, key).await; assert_eq!(after_old.daily_resets_at, restarted.daily_resets_at); @@ -268,7 +270,7 @@ async fn manual_reset_excludes_old_completions_but_counts_inflight_requests_afte .unwrap(); let delayed = charge("key", "delayed", "0.3"); let reset_context = context(); - // 无论迟到结算还是重置先取得锁,重置前完成的费用都不应重新进入日额度。 + // 无论迟到结算还是重置先取得锁,重置前完成的费用都不应重新进入日额度 let (reset, settled) = tokio::join!( admin.reset_client_key_budget( ResetClientKeyBudget { @@ -434,7 +436,7 @@ async fn budgets_settle_exactly_once_and_enforce_each_threshold_across_store_ins let second = PgClientBudgetStore::new(database.pool.clone()); for (key, prefix, amount) in [("day", "d", "0.1"), ("week", "w", "0.1")] { for _ in 0..3 { - // 已准入请求可完成并超过限额,不预占估算费用。 + // 已准入请求可完成并超过限额,不预占估算费用 first.admit(key_id(key)).await.unwrap(); } for index in 0..3 { @@ -524,7 +526,7 @@ async fn zero_cost_and_interrupted_requests_never_block_limited_keys() { let store = PgClientBudgetStore::new(database.pool.clone()); store.admit(key_id("key")).await.unwrap(); store.settle(charge("key", "no-cost", "0")).await.unwrap(); - // 模拟准入后进程退出,没有费用可结算;重启后仍应允许同一 Key 使用。 + // 模拟准入后进程退出,没有费用可结算;重启后仍应允许同一 Key 使用 store.admit(key_id("key")).await.unwrap(); let restarted = PgClientBudgetStore::new(database.pool.clone()); restarted.admit(key_id("key")).await.unwrap(); @@ -556,7 +558,7 @@ async fn transient_settlement_failure_rolls_back_and_retries_exact_cost_before_a seed(&database, "key", "1", "5").await; let store = PgClientBudgetStore::new(database.pool.clone()); store.admit(key_id("key")).await.unwrap(); - // 在费用事件插入后让窗口写入失败,验证整个事务回滚。 + // 在费用事件插入后让窗口写入失败,验证整个事务回滚 sqlx::raw_sql( "create function reject_budget_update() returns trigger language plpgsql as $$ begin @@ -874,7 +876,7 @@ async fn plugin_reset_revalidates_current_revision_without_changing_the_ledger() .kind(), AdminStoreErrorKind::NotFound ); - // 验证拒绝后的事务和锁已释放,当前身份仍可正常执行。 + // 验证拒绝后的事务和锁已释放,当前身份仍可正常执行 store .reset_client_key_budget( ResetClientKeyBudget { @@ -927,7 +929,7 @@ async fn plugin_reset_rolls_back_with_audit_and_serializes_with_late_settlement( .await .unwrap(); let mutation = context(); - // 完成时间在重置前,无论谁先拿到行锁,周用量都不能被迟到结算重新扣回。 + // 完成时间在重置前,无论谁先拿到行锁,周用量都不能被迟到结算重新扣回 let delayed = charge("key", "delayed", "1"); let (reset, settlement) = tokio::join!( store.reset_client_key_budget( @@ -1071,7 +1073,7 @@ async fn plugin_budget_limits_revalidate_authority_and_rollback_with_audit() { seed(&database, "key", "10", "20").await; let store = PgAdminClientKeyStore::new(database.pool.clone()); let before = status(&database, "key").await; - // 读取和配置赋值都不能开启未使用 Key 的窗口。 + // 读取和配置赋值都不能开启未使用 Key 的窗口 assert_eq!(before.daily_resets_at, None); let mut stale_owner = owner.clone(); stale_owner.revision = Revision::new(owner.revision.get() + 1).unwrap(); diff --git a/backend/crates/gateway-store/tests/postgres/client_keys.rs b/backend/crates/gateway-store/tests/postgres/client_keys.rs index 2d5feba57..687278983 100644 --- a/backend/crates/gateway-store/tests/postgres/client_keys.rs +++ b/backend/crates/gateway-store/tests/postgres/client_keys.rs @@ -1,3 +1,5 @@ +//! 验证 Client Key 持久化的唯一性、原值保留与搜索脱敏 + use std::{collections::BTreeMap, sync::Arc, time::Duration}; use chrono::{TimeZone as _, Utc}; diff --git a/backend/crates/gateway-store/tests/postgres/execution.rs b/backend/crates/gateway-store/tests/postgres/execution.rs index 6aefa6a39..3c1d7e3ab 100644 --- a/backend/crates/gateway-store/tests/postgres/execution.rs +++ b/backend/crates/gateway-store/tests/postgres/execution.rs @@ -1,3 +1,5 @@ +//! 验证执行记录端口的请求、尝试、恢复租约与快照持久化 + use std::time::{Duration as StdDuration, SystemTime}; use chrono::{DateTime, Duration, Utc}; @@ -214,7 +216,7 @@ async fn merged_model_less_first_attempt_should_match_sequential_semantics() { (1, "not_sent", "openai", "running", None, None) ); - // 后续 attempt 沿用常规 CAS 递增路径;已持久化的 sent 水位不被重试重置。 + // 后续 attempt 沿用常规 CAS 递增路径;已持久化的 sent 水位不被重试重置 repository .mark_upstream_send_state( "req_merged", @@ -1453,7 +1455,7 @@ async fn diagnostic_trace_is_finalized_atomically_and_available_for_failed_reque database.close().await; } -// 只构造已接纳的入口事实;未选择账号、未出站,也没有上游用量。 +// 只构造已接纳的入口事实;未选择账号、未出站,也没有上游用量 pub(super) fn accepted_request(id: &str) -> CoreNewModelRequest { let started_at = SystemTime::from( DateTime::from_timestamp_micros(Utc::now().timestamp_micros()) @@ -1488,7 +1490,7 @@ pub(super) fn accepted_request(id: &str) -> CoreNewModelRequest { pub(super) fn early_failure(request: &CoreNewModelRequest) -> CoreModelRequestFinalization { let trace = TraceContext::new(request.id.as_str()); trace.record("request.started", json!({"operation": "generate"})); - // trace 的 index 是预备阶段关联,不证明已经拿到账号、建流或实际发送。 + // trace 的 index 是预备阶段关联,不证明已经拿到账号、建流或实际发送 let preparation = trace.attempt(1); preparation.record( "attempt.started", @@ -1675,7 +1677,7 @@ async fn zero_attempt_failure_is_queryable_without_fabricating_upstream_facts() assert_eq!(error.upstream_request_id, None); assert_eq!(error.raw_upstream_error, None); - // 路由入口不等于已选择 Provider;没有平台事实时不能被平台/attempt 筛选命中。 + // 路由入口不等于已选择 Provider;没有平台事实时不能被平台/attempt 筛选命中 for filter in [ OpsErrorFilter { provider_kind: Some("openai".to_owned()), diff --git a/backend/crates/gateway-store/tests/postgres/execution_buffer.rs b/backend/crates/gateway-store/tests/postgres/execution_buffer.rs index 2c2fb2d6d..ce2fac4d3 100644 --- a/backend/crates/gateway-store/tests/postgres/execution_buffer.rs +++ b/backend/crates/gateway-store/tests/postgres/execution_buffer.rs @@ -1,3 +1,5 @@ +//! 验证执行写入队列的容量、请求内顺序与排空行为 + use std::num::NonZeroUsize; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; @@ -769,7 +771,7 @@ async fn zero_attempt_dropped_finalize_is_fail_open_and_recoverable_at_deadline( 0 ); - // 已关闭队列同样不把观测失败返回客户端,也不能覆盖已经恢复的终态。 + // 已关闭队列同样不把观测失败返回客户端,也不能覆盖已经恢复的终态 store .finalize_model_request(early_failure(&request)) .await @@ -798,7 +800,7 @@ async fn zero_attempt_dropped_create_does_not_make_finalize_an_upsert() { NonZeroUsize::new(8_192).expect("byte budget"), ); let mut request = accepted_request("req_zero_attempt_create_dropped"); - // 合成大 header 只触发 Create 字节预算;Finalize 不携带 user_agent,仍可入队。 + // 合成大 header 只触发 Create 字节预算;Finalize 不携带 user_agent,仍可入队 request.user_agent = Some("x".repeat(16_384)); store .create_model_request(request.clone()) @@ -840,7 +842,7 @@ async fn zero_attempt_postgres_write_failures_are_not_retried_and_do_not_stop_th let Some(database) = TestDatabase::create("zero_attempt_write_failure").await else { return; }; - // 只在随机测试 schema 注入语句失败;序列不随语句回滚,用于核对实际写入次数。 + // 只在随机测试 schema 注入语句失败;序列不随语句回滚,用于核对实际写入次数 sqlx::raw_sql( "create sequence zero_attempt_rejected_writes; create function reject_zero_attempt_observation() returns trigger language plpgsql as $$ diff --git a/backend/crates/gateway-store/tests/postgres/health.rs b/backend/crates/gateway-store/tests/postgres/health.rs index 4ce45eed0..4f9424f6e 100644 --- a/backend/crates/gateway-store/tests/postgres/health.rs +++ b/backend/crates/gateway-store/tests/postgres/health.rs @@ -1,3 +1,5 @@ +//! 验证 PostgreSQL 健康探测区分短暂与持续连接池饱和 + use std::time::Duration; use gateway_core::health::{HealthProbe as _, HealthState}; diff --git a/backend/crates/gateway-store/tests/postgres/mod.rs b/backend/crates/gateway-store/tests/postgres/mod.rs index e8773f573..6e5477449 100644 --- a/backend/crates/gateway-store/tests/postgres/mod.rs +++ b/backend/crates/gateway-store/tests/postgres/mod.rs @@ -1,3 +1,5 @@ +//! PostgreSQL 适配测试入口,以及迁移应用和事务归属测试 + use std::{str::FromStr, time::Duration}; use gateway_store::postgres::{ @@ -65,8 +67,8 @@ impl TestDatabase { pub(super) async fn create_through(label: &str, migration_version: i64) -> Option { let database_url = crate::support::test_env("CPR_TEST_DATABASE_URL")?; let schema = format!("cpr_store_{label}_{}", Uuid::new_v4().simple()); - // 临时 schema 验证事务可见性与回滚,不模拟 PostgreSQL 掉电恢复。 - // 仅这些测试连接异步刷 WAL,保持服务端配置和生产连接行为不变。 + // 临时 schema 验证事务可见性与回滚,不模拟 PostgreSQL 掉电恢复 + // 仅这些测试连接异步刷 WAL,保持服务端配置和生产连接行为不变 let options = PgConnectOptions::from_str(&database_url) .expect("parse test PostgreSQL URL") .options([("synchronous_commit", "off")]); diff --git a/backend/crates/gateway-store/tests/postgres/observability/mod.rs b/backend/crates/gateway-store/tests/postgres/observability/mod.rs index e360f600e..ed7ad3e50 100644 --- a/backend/crates/gateway-store/tests/postgres/observability/mod.rs +++ b/backend/crates/gateway-store/tests/postgres/observability/mod.rs @@ -1,3 +1,5 @@ +//! 观测存储测试入口,以及查询范围、过滤与端口合同测试 + use futures::TryStreamExt; use std::{ collections::BTreeMap, @@ -409,7 +411,7 @@ async fn usage_search_should_preserve_account_snapshots_after_account_changes() let range = ObservabilityRange::new(now - TimeDelta::hours(1), now + TimeDelta::hours(1)) .expect("observability range"); - // 搜索与列表都使用请求发生时的快照,不能随当前账号资料变化或删除而改变。 + // 搜索与列表都使用请求发生时的快照,不能随当前账号资料变化或删除而改变 for mutation in [ "update provider_accounts set name = 'renamed', email = 'renamed@example.invalid' where id = 'acct_observe'", @@ -2232,7 +2234,7 @@ async fn calendar_trends_share_exact_day_boundaries_for_requests_costs_and_empty .try_collect::>() .await .expect("calendar cost buckets"); - // 只有已完整交付的 calculated 费用参与趋势事实;其他费用仍按相同边界聚合。 + // 只有已完整交付的 calculated 费用参与趋势事实;其他费用仍按相同边界聚合 assert_eq!(facts.len(), 1); assert!( facts diff --git a/backend/crates/gateway-store/tests/postgres/ops_events.rs b/backend/crates/gateway-store/tests/postgres/ops_events.rs index 3b9111043..3d8da0c95 100644 --- a/backend/crates/gateway-store/tests/postgres/ops_events.rs +++ b/backend/crates/gateway-store/tests/postgres/ops_events.rs @@ -1,3 +1,5 @@ +//! 验证请求级运维事件必须关联明确的执行尝试 + use chrono::Utc; use gateway_store::postgres::{OpsEvent, OpsEventLevel}; diff --git a/backend/crates/gateway-store/tests/postgres/plugins/artifacts.rs b/backend/crates/gateway-store/tests/postgres/plugins/artifacts.rs index ef67de6eb..0602c070b 100644 --- a/backend/crates/gateway-store/tests/postgres/plugins/artifacts.rs +++ b/backend/crates/gateway-store/tests/postgres/plugins/artifacts.rs @@ -1,3 +1,5 @@ +//! 验证插件制品身份、信任接受、迁移与安装幂等性 + use std::collections::BTreeMap; use gateway_admin::{ @@ -153,7 +155,7 @@ async fn trust_migration_removes_only_obsolete_metadata_fields() { ), ('d', serde_json::json!({})), ] { - // 每个制品使用独立平台,保留同一版本的平台不可变约束。 + // 每个制品使用独立平台,保留同一版本的平台不可变约束 let fixture = artifact(digest, &[&format!("test-{digest}")]); let installed = store .install_artifact(fixture.clone(), PluginSource::Upload, &context()) diff --git a/backend/crates/gateway-store/tests/postgres/plugins/credentials.rs b/backend/crates/gateway-store/tests/postgres/plugins/credentials.rs index b97129ba2..017c2c52c 100644 --- a/backend/crates/gateway-store/tests/postgres/plugins/credentials.rs +++ b/backend/crates/gateway-store/tests/postgres/plugins/credentials.rs @@ -1,3 +1,5 @@ +//! 验证插件下载凭据的保护、引用清理与事务回滚 + use gateway_admin::{ model::plugins::{ PluginSource, diff --git a/backend/crates/gateway-store/tests/postgres/plugins/instances.rs b/backend/crates/gateway-store/tests/postgres/plugins/instances.rs index 0fb5f5649..8465d57a2 100644 --- a/backend/crates/gateway-store/tests/postgres/plugins/instances.rs +++ b/backend/crates/gateway-store/tests/postgres/plugins/instances.rs @@ -1,3 +1,5 @@ +//! 验证插件实例的精确授权、版本替换与配置和敏感值原子提交 + use std::collections::BTreeMap; use gateway_admin::{ @@ -73,7 +75,7 @@ async fn management_authorization_checks_exact_target_without_loading_secrets() artifact_sha256: saved.instance.artifact_sha256.clone(), revision: saved.instance.revision.get(), }; - // 故意留下不能解码为密钥映射的测试数据,授权查询不得读取或解释它。 + // 故意留下不能解码为密钥映射的测试数据,授权查询不得读取或解释它 sqlx::query("update plugin_instance_secrets set secrets_json=$2 where instance_id=$1") .bind(uuid::Uuid::parse_str(&target.instance_id).unwrap()) .bind(serde_json::json!({"token": false})) diff --git a/backend/crates/gateway-store/tests/postgres/plugins/mod.rs b/backend/crates/gateway-store/tests/postgres/plugins/mod.rs index 381dd8f55..9e0508ff6 100644 --- a/backend/crates/gateway-store/tests/postgres/plugins/mod.rs +++ b/backend/crates/gateway-store/tests/postgres/plugins/mod.rs @@ -1,3 +1,5 @@ +//! 插件 PostgreSQL 持久化的测试入口 + pub(super) mod artifacts; mod credentials; mod instances; diff --git a/backend/crates/gateway-store/tests/postgres/plugins/sources.rs b/backend/crates/gateway-store/tests/postgres/plugins/sources.rs index 0ebbc5221..8ca8dd7af 100644 --- a/backend/crates/gateway-store/tests/postgres/plugins/sources.rs +++ b/backend/crates/gateway-store/tests/postgres/plugins/sources.rs @@ -1,3 +1,5 @@ +//! 验证插件更新来源、制品引用、并发安装与代理版本约束 + use gateway_admin::{ model::{ plugins::{ diff --git a/backend/crates/gateway-store/tests/postgres/plugins/state.rs b/backend/crates/gateway-store/tests/postgres/plugins/state.rs index 1024e194c..9f257f8ee 100644 --- a/backend/crates/gateway-store/tests/postgres/plugins/state.rs +++ b/backend/crates/gateway-store/tests/postgres/plugins/state.rs @@ -1,3 +1,5 @@ +//! 验证插件私有状态迁移的顺序、配额与代次资源回收 + use std::collections::BTreeMap; use gateway_admin::{ @@ -408,7 +410,7 @@ async fn failed_multi_namespace_promotion_restores_cleaned_source_generations() .err() .unwrap(); assert_eq!(error.kind(), AdminStoreErrorKind::Conflict); - // cache 已走过清理步骤,settings 未完成导致整笔事务回滚,旧身份和两份数据仍有效。 + // cache 已走过清理步骤,settings 未完成导致整笔事务回滚,旧身份和两份数据仍有效 for namespace in ["cache", "settings"] { assert_eq!( store @@ -523,7 +525,7 @@ async fn state_cas_quota_and_fence_are_independent_from_global_revision() { .unwrap() .unwrap(); - // 无关制品安装会推进全局配置 revision,但不能撤销实例自己的状态 fence。 + // 无关制品安装会推进全局配置 revision,但不能撤销实例自己的状态 fence let unrelated = install_version(&store, 'b', "2.0.0").await; let first = store .put( @@ -637,7 +639,7 @@ async fn state_cas_quota_and_fence_are_independent_from_global_revision() { "配额收紧不能让已有记录超过单值上限" ); - // 同一实例的配置提交会轮换 fence;旧进程即使仍持有 owner 也不能继续写。 + // 同一实例的配置提交会轮换 fence;旧进程即使仍持有 owner 也不能继续写 let rebound = store .save_instance_with_state( saved.instance.clone(), diff --git a/backend/crates/gateway-store/tests/postgres/pricing.rs b/backend/crates/gateway-store/tests/postgres/pricing.rs index c0c8e3439..541d9b4d7 100644 --- a/backend/crates/gateway-store/tests/postgres/pricing.rs +++ b/backend/crates/gateway-store/tests/postgres/pricing.rs @@ -1,3 +1,5 @@ +//! 验证并发价格变更与同步保留其他模型、人工覆盖和冻结快照 + use gateway_admin::model::{ MutationActor, MutationContext, pricing::{PricingChange, UpdatePricing}, diff --git a/backend/crates/gateway-store/tests/postgres/provider_accounts/admin_adapter.rs b/backend/crates/gateway-store/tests/postgres/provider_accounts/admin_adapter.rs index 578e49230..d9f09c0a8 100644 --- a/backend/crates/gateway-store/tests/postgres/provider_accounts/admin_adapter.rs +++ b/backend/crates/gateway-store/tests/postgres/provider_accounts/admin_adapter.rs @@ -1,3 +1,5 @@ +//! 验证管理账号查询的容量投影、分页筛选与授权提交 + use super::*; #[tokio::test] @@ -185,7 +187,7 @@ async fn plugin_account_provider_filter_is_optional_and_applied_before_cursor_pa Some("acct_plugin_b") ); - // 未指定 Provider 时按全局账号 ID 跨 Provider 分页,Provider 归属来自持久记录。 + // 未指定 Provider 时按全局账号 ID 跨 Provider 分页,Provider 归属来自持久记录 let all = store .list_plugin_accounts(PluginAccountListQuery { provider_kind: None, diff --git a/backend/crates/gateway-store/tests/postgres/provider_accounts/authorization.rs b/backend/crates/gateway-store/tests/postgres/provider_accounts/authorization.rs index 084355b2f..2009706f0 100644 --- a/backend/crates/gateway-store/tests/postgres/provider_accounts/authorization.rs +++ b/backend/crates/gateway-store/tests/postgres/provider_accounts/authorization.rs @@ -1,3 +1,5 @@ +//! 验证账号授权回执的并发幂等、事务回滚与过期清理 + use gateway_admin::model::provider_credentials::{ AuthorizationReceiptKey, PreparedCredentialRotationFacts, }; @@ -35,7 +37,7 @@ async fn concurrent_authorization_retries_share_one_account_write_and_one_receip actor: MutationActor::AdminApiKey, request_id: "different-owner".into(), }; - // 相同 flow 的另一身份不能读取账号结果,也不能另行提交同一个授权。 + // 相同 flow 的另一身份不能读取账号结果,也不能另行提交同一个授权 let (mut foreign, _) = command(credential("acct_foreign", "foreign-user")); foreign.key = key.clone(); assert!( diff --git a/backend/crates/gateway-store/tests/postgres/provider_accounts/mod.rs b/backend/crates/gateway-store/tests/postgres/provider_accounts/mod.rs index 5feaefb0c..52b9ecd79 100644 --- a/backend/crates/gateway-store/tests/postgres/provider_accounts/mod.rs +++ b/backend/crates/gateway-store/tests/postgres/provider_accounts/mod.rs @@ -1,3 +1,5 @@ +//! Provider 账号存储测试入口,以及凭据与额度端口测试 + use std::{ collections::BTreeMap, num::NonZeroU32, @@ -491,7 +493,7 @@ async fn quota_plan_changes_survive_inflight_background_and_manual_token_refresh assert_eq!(upgraded.revision(), original.revision()); assert_eq!(current_revision(&database.pool).await, config_revision); - // 刷新在额度更新前已准备好旧资料,CAS 提交仍须保留数据库中的新套餐。 + // 刷新在额度更新前已准备好旧资料,CAS 提交仍须保留数据库中的新套餐 let refresh = CredentialCasUpdate::new( id.clone(), original.revision(), @@ -2821,7 +2823,7 @@ async fn verified_credential_rotation_preserves_quota_exhaustion() { }) .await .expect("import exhausted account"); - // 模拟应用与数据库的微小时钟偏差,轮换不能把更新时间倒退到已有额度观测之前。 + // 模拟应用与数据库的微小时钟偏差,轮换不能把更新时间倒退到已有额度观测之前 let observed_at = SystemTime::now() + Duration::from_secs(3); repository .apply_quota_access(QuotaAccessChange { @@ -2988,7 +2990,7 @@ async fn core_refresh_cas_updates_credentials_after_scheduling_is_disabled() { .unwrap() .preserving_profile() .with_account_state(CredentialState::Ready, SystemTime::now(), None, None); - // 模拟刷新在途时停用调度;提交新凭据不能重新启用账号,也不能丢弃刷新结果。 + // 模拟刷新在途时停用调度;提交新凭据不能重新启用账号,也不能丢弃刷新结果 repository.set_enabled(&account_id, false).await.unwrap(); assert!(matches!( repository @@ -3604,7 +3606,7 @@ async fn adaptive_concurrency_handles_unlimited_and_latest_locked_settings_witho .await .expect("atomic adaptation") }); - // 管理员持有设置锁时提交新的账号事实,worker 必须依据提交后的值判断。 + // 管理员持有设置锁时提交新的账号事实,worker 必须依据提交后的值判断 sqlx::query("update provider_accounts set enabled = $2, concurrency_limit = $3, weight = 7, notes = 'administrator edit' where id = $1") .bind(id.as_str()).bind(enabled).bind(account_limit).execute(&mut *admin).await.expect("concurrent admin edit"); admin.commit().await.expect("commit admin edit"); diff --git a/backend/crates/gateway-store/tests/postgres/provider_accounts/quota_forecast.rs b/backend/crates/gateway-store/tests/postgres/provider_accounts/quota_forecast.rs index 33620582f..8e2c83321 100644 --- a/backend/crates/gateway-store/tests/postgres/provider_accounts/quota_forecast.rs +++ b/backend/crates/gateway-store/tests/postgres/provider_accounts/quota_forecast.rs @@ -1,3 +1,5 @@ +//! 验证额度预测查询的请求筛选、配对采样与文档读取限制 + use super::*; use gateway_admin::model::quota_forecast_sampling::MAX_FORECAST_HISTORY_POINTS; use gateway_store::postgres::{ @@ -17,7 +19,7 @@ async fn quota_forecast_excludes_openai_prewarm_but_preserves_inference_and_audi let start = Utc::now() - TimeDelta::minutes(30); let end = start + TimeDelta::minutes(20); for (id, minute, tokens) in [("normal", 1, 100), ("review", 2, 200), ("prewarm", 3, 400)] { - // 普通成功推理也可以只有输入 Token,不能用 output_tokens = 0 判定预热。 + // 普通成功推理也可以只有输入 Token,不能用 output_tokens = 0 判定预热 seed_model_request( &database.pool, ModelRequestSeed { @@ -62,7 +64,7 @@ async fn quota_forecast_excludes_openai_prewarm_but_preserves_inference_and_audi assert_eq!(history.usage.usd, 2.0); assert_eq!(history.usage.excluded_request_count, 1); assert_eq!(history.pending_request_count, 0); - // 预热响应携带的额度观测仍可配对,但它不增加累计推理用量。 + // 预热响应携带的额度观测仍可配对,但它不增加累计推理用量 assert_eq!(history.points.len(), 1); assert_eq!(history.points[0].usage, history.usage); @@ -95,7 +97,7 @@ async fn quota_forecast_excludes_openai_prewarm_but_preserves_inference_and_audi assert_eq!(dashboard.totals.request_count, 3); assert_eq!(dashboard.totals.total_tokens, 300); - // 真正推理的未知费用仍必须阻止费用外推,不能靠放宽覆盖门槛掩盖缺失。 + // 真正推理的未知费用仍必须阻止费用外推,不能靠放宽覆盖门槛掩盖缺失 sqlx::query( "update model_requests set cost_source = 'unavailable', cost_amount = null, cost_currency = null, service_tier = 'auto' where id = 'review'", diff --git a/backend/crates/gateway-store/tests/postgres/provider_accounts/timestamps.rs b/backend/crates/gateway-store/tests/postgres/provider_accounts/timestamps.rs index 57c6961ee..831cd986b 100644 --- a/backend/crates/gateway-store/tests/postgres/provider_accounts/timestamps.rs +++ b/backend/crates/gateway-store/tests/postgres/provider_accounts/timestamps.rs @@ -1,3 +1,5 @@ +//! 验证账号观测与凭据写入在乱序和时钟回退时保持时间边界 + use super::*; use gateway_core::account::AccountConcurrencyLimit; use gateway_store::postgres::RecoverProviderAccount; @@ -10,7 +12,7 @@ async fn cross_kind_observations_preserve_time_bounds_and_stale_observation_fenc let repository = PgProviderAccountRepository::new(database.pool.clone()); let account_id = ProviderAccountId::new("acct_observation_time_bounds").expect("account id"); let mut seed = account(account_id.as_str(), "user-observation-time-bounds"); - // 合成应用时钟领先数据库的已有事实,避免修改宿主时钟或依赖测试执行速度。 + // 合成应用时钟领先数据库的已有事实,避免修改宿主时钟或依赖测试执行速度 let anchor = Utc::now() + TimeDelta::hours(1); seed.credential_observed_at = anchor; repository @@ -144,7 +146,7 @@ async fn credential_and_admin_writes_preserve_existing_update_time_after_clock_r .insert_provider_account(seed) .await .expect("seed account"); - // 账号在回拨前创建,管理恢复和重新导入也必须保留这一时间下界。 + // 账号在回拨前创建,管理恢复和重新导入也必须保留这一时间下界 sqlx::query("update provider_accounts set created_at = updated_at where id = $1") .bind(account_id.as_str()) .execute(&database.pool) diff --git a/backend/crates/gateway-store/tests/postgres/proxies.rs b/backend/crates/gateway-store/tests/postgres/proxies.rs index aa5309549..a2ee4f292 100644 --- a/backend/crates/gateway-store/tests/postgres/proxies.rs +++ b/backend/crates/gateway-store/tests/postgres/proxies.rs @@ -1,3 +1,5 @@ +//! 验证代理位置、账号绑定、分页查询与事务锁释放 + use gateway_admin::{ model::{ MutationActor, MutationContext, PageSize, @@ -373,7 +375,7 @@ async fn proxy_accounts_paginate_thousands_of_accounts_and_search_without_loadin .await .unwrap() .record; - // 相同名称验证稳定排序;账号仅写入隔离测试数据库,不接触真实账号池。 + // 相同名称验证稳定排序;账号仅写入隔离测试数据库,不接触真实账号池 sqlx::query( "insert into provider_accounts (id, provider_kind, name, email, authentication_kind, provider_credentials_json, @@ -500,7 +502,7 @@ async fn rejected_import_reservations_release_proxy_lock_before_returning() { let store = PgProxyRepository::new(database.pool.clone()); let id = "proxy_missing"; - // 读取不存在的代理失败后,应立即允许其他事务取得同一资源的独占锁。 + // 读取不存在的代理失败后,应立即允许其他事务取得同一资源的独占锁 for _ in 0..64 { let error = store.reserve_import(id).await.err().unwrap(); assert_eq!(error.kind(), AdminStoreErrorKind::NotFound); @@ -545,7 +547,7 @@ async fn stale_proxy_test_releases_lock_before_next_update() { .unwrap() .record; - // 暂停连接归还时的后台清理,确保锁由返回错误前的显式回滚释放。 + // 暂停连接归还时的后台清理,确保锁由返回错误前的显式回滚释放 let release_gate = std::sync::Arc::new(tokio::sync::Semaphore::new(0)); let rejected_pool = database .pool @@ -668,7 +670,7 @@ async fn import_reservation_blocks_proxy_mutations_until_rotated_credentials_are .is_err() ); - // 模拟上游已轮换的新凭据;持有保护时,另一个连接仍能完成导入事务。 + // 模拟上游已轮换的新凭据;持有保护时,另一个连接仍能完成导入事务 let mut candidate = account("acct_reserved_import", "reserved-import-user"); candidate.outbound_proxy = Some(reservation.binding.proxy.clone()); let repository = PgProviderAccountRepository::new(database.pool.clone()); @@ -966,7 +968,7 @@ async fn migration_backfills_shared_proxies_without_changing_credentials() { let Some(database) = TestDatabase::create_through("proxy_backfill", 4).await else { return; }; - // 从真实旧 schema 正向升级,不拆卸最新结构,避免遗漏后续增加的引用约束。 + // 从真实旧 schema 正向升级,不拆卸最新结构,避免遗漏后续增加的引用约束 for id in ["acct_one", "acct_two", "acct_direct"] { let proxy_url = (id != "acct_direct").then_some("http://user:secret@127.0.0.1:8080/"); sqlx::query( diff --git a/backend/crates/gateway-store/tests/postgres/query_budget.rs b/backend/crates/gateway-store/tests/postgres/query_budget.rs index 0094b3247..4c18a90fa 100644 --- a/backend/crates/gateway-store/tests/postgres/query_budget.rs +++ b/backend/crates/gateway-store/tests/postgres/query_budget.rs @@ -1,3 +1,5 @@ +//! 验证数据库查询预算的容量、排队超时与流释放 + use std::time::Duration; use futures::{StreamExt, TryStreamExt, stream}; @@ -34,7 +36,7 @@ async fn query_stream_holds_budget_until_completion_error_or_drop() { Err(StoreError::InvalidData { .. }) => {} other => panic!("unexpected stream result: {other:?}"), } - // The finished stream may remain alive; its connection slot must already be free. + // 已结束的流可以继续存活,但其连接槽位必须已经释放 budget .run("after terminal", async { Ok(()) }) .await diff --git a/backend/crates/gateway-store/tests/postgres/retention.rs b/backend/crates/gateway-store/tests/postgres/retention.rs index 4a872ac1f..8e39bb3a3 100644 --- a/backend/crates/gateway-store/tests/postgres/retention.rs +++ b/backend/crates/gateway-store/tests/postgres/retention.rs @@ -1,3 +1,5 @@ +//! 验证历史清理仅按批次上限删除已过期记录 + use std::num::NonZeroU32; use chrono::{Duration as ChronoDuration, Utc}; diff --git a/backend/crates/gateway-store/tests/postgres/runtime_settings.rs b/backend/crates/gateway-store/tests/postgres/runtime_settings.rs index 59c0c9b45..22c664ded 100644 --- a/backend/crates/gateway-store/tests/postgres/runtime_settings.rs +++ b/backend/crates/gateway-store/tests/postgres/runtime_settings.rs @@ -1,3 +1,5 @@ +//! 验证运行设置的数据库约束、升级保留与快照发布 + use std::collections::BTreeMap; use chrono::{DateTime, TimeDelta, Utc}; @@ -58,7 +60,7 @@ async fn smart_settings_upgrade_preserves_selection_and_publishes_custom_config( let Some(database) = TestDatabase::create_through("smart_config", 18).await else { return; }; - // 升级前不能用包含新列的 Repository,直接写入旧版本已有字段。 + // 升级前不能用包含新列的 Repository,直接写入旧版本已有字段 sqlx::query("update runtime_settings set rotation_strategy = 'sticky', refresh_margin_seconds = 3600 where id = 1") .execute(&database.pool).await.unwrap(); super::TEST_MIGRATOR.run(&database.pool).await.unwrap(); @@ -444,7 +446,7 @@ async fn request_location_defaults_and_updates_reach_the_runtime_snapshot() { .unwrap(); assert!(!disabled_snapshot.settings.request_location_enabled); assert_eq!(disabled_snapshot.settings.request_location, expected); - // 位置开关与自动冻结共用设置写入,切换位置不能覆盖冻结参数。 + // 位置开关与自动冻结共用设置写入,切换位置不能覆盖冻结参数 for saved in [&settings, &disabled_settings] { assert!(saved.account_auto_freeze_enabled); assert_eq!(saved.account_auto_freeze_threshold, 17); @@ -866,7 +868,7 @@ async fn control_plane_replacement_commits_one_writer_per_revision_and_preserves .await .unwrap(); assert_eq!(audit_count, 1); - // API Key 更新也推进相同版本,旧设置快照不能复活已经替换的 Key。 + // API Key 更新也推进相同版本,旧设置快照不能复活已经替换的 Key let mut key_audit = replacement("key", 3600).audit; key_audit.action = "settings.admin_key".into(); repository @@ -1021,7 +1023,7 @@ async fn warmup_cursor_resolves_dst_and_deduplicates_across_timezones() { "2026-11-01T05:30:00Z".parse::>().unwrap() ); assert!(!repository.claim_warmup_slot(zone, slot).await.unwrap()); - // 回拨中的重复本地时刻取较早一次,不能误挡后一分的正常调度。 + // 回拨中的重复本地时刻取较早一次,不能误挡后一分的正常调度 let next = slot + TimeDelta::minutes(1); assert!(repository.claim_warmup_slot(zone, next).await.unwrap()); let utc: DeploymentTimeZone = "UTC".parse().unwrap(); diff --git a/backend/crates/gateway-store/tests/postgres/schema_integrity.rs b/backend/crates/gateway-store/tests/postgres/schema_integrity.rs index 50b6973eb..0ff34fd94 100644 --- a/backend/crates/gateway-store/tests/postgres/schema_integrity.rs +++ b/backend/crates/gateway-store/tests/postgres/schema_integrity.rs @@ -1,3 +1,5 @@ +//! 验证数据库拒绝不完整请求事实、缺失快照与无效时间关系 + use super::TestDatabase; async fn seed_request(pool: &sqlx::PgPool) { diff --git a/backend/crates/gateway-store/tests/postgres/snapshot.rs b/backend/crates/gateway-store/tests/postgres/snapshot.rs index 7041dfbf0..0e2c42130 100644 --- a/backend/crates/gateway-store/tests/postgres/snapshot.rs +++ b/backend/crates/gateway-store/tests/postgres/snapshot.rs @@ -1,3 +1,5 @@ +//! 验证运行时快照读取当前 Key 策略并保护密钥诊断输出 + use gateway_core::policy::{ClientApiKeyId, PlaintextClientApiKey, RateLimits}; use gateway_store::postgres::{ ClientApiKeySnapshot, PgRuntimeSnapshotRepository, RuntimeSnapshotRepository, diff --git a/backend/crates/gateway-store/tests/postgres/snapshots.rs b/backend/crates/gateway-store/tests/postgres/snapshots.rs index 40c800fc4..94cd53992 100644 --- a/backend/crates/gateway-store/tests/postgres/snapshots.rs +++ b/backend/crates/gateway-store/tests/postgres/snapshots.rs @@ -1,3 +1,5 @@ +//! 验证账号删除后请求、尝试与运维事件仍保留各自历史快照 + use chrono::{DateTime, TimeDelta, Utc}; use gateway_admin::ports::store::ObservabilityStore as _; use gateway_store::postgres::{ diff --git a/backend/crates/gateway-store/tests/redis/artifact_profile.rs b/backend/crates/gateway-store/tests/redis/artifact_profile.rs index c9969e3db..aa377d42e 100644 --- a/backend/crates/gateway-store/tests/redis/artifact_profile.rs +++ b/backend/crates/gateway-store/tests/redis/artifact_profile.rs @@ -1,3 +1,5 @@ +//! 验证 Provider 制品画像缓存的有效期、版本回退与冲突拒绝 + use std::time::{Duration, SystemTime}; use gateway_core::account::OpaqueProviderData; diff --git a/backend/crates/gateway-store/tests/redis/auth.rs b/backend/crates/gateway-store/tests/redis/auth.rs index 74c068fb7..b1f2b8f66 100644 --- a/backend/crates/gateway-store/tests/redis/auth.rs +++ b/backend/crates/gateway-store/tests/redis/auth.rs @@ -1,3 +1,5 @@ +//! 验证 Redis 认证会话的固定有效期、敏感键保护与原子限流 + use std::time::Duration; use chrono::Utc; @@ -275,7 +277,7 @@ async fn unified_session_rejects_invalid_identities_and_loads_legacy_admin_paylo assert!(repository.load_session(token).await.is_err()); } - // 旧版会话仍能解码,空指纹交由认证服务判定失效,避免升级后返回存储故障。 + // 旧版会话仍能解码,空指纹交由认证服务判定失效,避免升级后返回存储故障 let legacy = serde_json::json!({ "subject": {"type": "admin", "admin_user_id": "admin"}, "expires_at": (Utc::now() + chrono::Duration::seconds(60)).to_rfc3339(), diff --git a/backend/crates/gateway-store/tests/redis/client_admission.rs b/backend/crates/gateway-store/tests/redis/client_admission.rs index a6634d804..e68cef516 100644 --- a/backend/crates/gateway-store/tests/redis/client_admission.rs +++ b/backend/crates/gateway-store/tests/redis/client_admission.rs @@ -1,3 +1,5 @@ +//! 验证 Redis 准入的参数边界、并发容量与租约恢复 + use std::time::Duration; use chrono::{DateTime, Utc}; diff --git a/backend/crates/gateway-store/tests/redis/coordination_buffer.rs b/backend/crates/gateway-store/tests/redis/coordination_buffer.rs index f7260f57f..d65ccd38a 100644 --- a/backend/crates/gateway-store/tests/redis/coordination_buffer.rs +++ b/backend/crates/gateway-store/tests/redis/coordination_buffer.rs @@ -1,3 +1,5 @@ +//! 验证 Redis 协调写入队列的有界丢弃、刷写与容量释放顺序 + use std::collections::BTreeSet; use std::num::NonZeroUsize; use std::sync::{Arc, Mutex}; diff --git a/backend/crates/gateway-store/tests/redis/credential_cooldown.rs b/backend/crates/gateway-store/tests/redis/credential_cooldown.rs index c5a1b2317..318b013de 100644 --- a/backend/crates/gateway-store/tests/redis/credential_cooldown.rs +++ b/backend/crates/gateway-store/tests/redis/credential_cooldown.rs @@ -1,3 +1,5 @@ +//! 验证账号冷却缓存的版本隔离、期限延长与到期清理 + use std::time::{Duration as StdDuration, SystemTime}; use chrono::{DateTime, Duration, Utc}; @@ -427,7 +429,7 @@ async fn stale_probe_cannot_recreate_manual_recovery_or_clear_or_extend_new_free .expect("read") .is_none() ); - // 同凭据版本、相同截止时间的新冻结仍必须视为不同代次。 + // 同凭据版本、相同截止时间的新冻结仍必须视为不同代次 repository .cache_credential_cooldown(&frozen) .await diff --git a/backend/crates/gateway-store/tests/redis/credential_leases.rs b/backend/crates/gateway-store/tests/redis/credential_leases.rs index d9202e428..866e03148 100644 --- a/backend/crates/gateway-store/tests/redis/credential_leases.rs +++ b/backend/crates/gateway-store/tests/redis/credential_leases.rs @@ -1,3 +1,5 @@ +//! 验证账号调度租约的容量、续租、失效与释放 + use std::time::Duration; use gateway_store::redis::{ diff --git a/backend/crates/gateway-store/tests/redis/credential_state.rs b/backend/crates/gateway-store/tests/redis/credential_state.rs index 71c27c0ca..d84d7ee23 100644 --- a/backend/crates/gateway-store/tests/redis/credential_state.rs +++ b/backend/crates/gateway-store/tests/redis/credential_state.rs @@ -1,3 +1,5 @@ +//! 验证凭据状态与目录缓存保持 Provider 中立的键和值合同 + use chrono::Utc; use gateway_store::{ Revision, diff --git a/backend/crates/gateway-store/tests/redis/mod.rs b/backend/crates/gateway-store/tests/redis/mod.rs index 94ca46141..daa4a95da 100644 --- a/backend/crates/gateway-store/tests/redis/mod.rs +++ b/backend/crates/gateway-store/tests/redis/mod.rs @@ -1,3 +1,5 @@ +//! Redis 协调、缓存、会话与租约适配器的测试入口 + mod artifact_profile; mod auth; mod client_admission; diff --git a/backend/crates/gateway-store/tests/redis/native_continuation.rs b/backend/crates/gateway-store/tests/redis/native_continuation.rs index e7d501dbc..aa5d8ee7e 100644 --- a/backend/crates/gateway-store/tests/redis/native_continuation.rs +++ b/backend/crates/gateway-store/tests/redis/native_continuation.rs @@ -1,3 +1,5 @@ +//! 验证原生续接记录的隔离、敏感键保护与有界索引清理 + use gateway_core::account::ProviderAccountId; use gateway_core::engine::continuation::{ NativeContinuationPin, NativeContinuationPort, NativeContinuationScope, diff --git a/backend/crates/gateway-store/tests/redis/oauth_pending.rs b/backend/crates/gateway-store/tests/redis/oauth_pending.rs index 634486742..c88131374 100644 --- a/backend/crates/gateway-store/tests/redis/oauth_pending.rs +++ b/backend/crates/gateway-store/tests/redis/oauth_pending.rs @@ -1,3 +1,5 @@ +//! 验证 OAuth 待授权状态按 Provider 与所属身份隔离 + use std::{sync::Arc, time::Duration}; use gateway_core::{ diff --git a/backend/crates/gateway-store/tests/redis/provider_session_affinity.rs b/backend/crates/gateway-store/tests/redis/provider_session_affinity.rs index 493fe9f56..29957021c 100644 --- a/backend/crates/gateway-store/tests/redis/provider_session_affinity.rs +++ b/backend/crates/gateway-store/tests/redis/provider_session_affinity.rs @@ -1,3 +1,5 @@ +//! 验证会话亲和记录的原子认领、条件替换、有效期与清理 + use std::time::Duration; use gateway_core::account::ProviderAccountId; diff --git a/backend/crates/gateway-store/tests/redis/provider_session_exclusion.rs b/backend/crates/gateway-store/tests/redis/provider_session_exclusion.rs index 3f0946130..1265bba7b 100644 --- a/backend/crates/gateway-store/tests/redis/provider_session_exclusion.rs +++ b/backend/crates/gateway-store/tests/redis/provider_session_exclusion.rs @@ -1,3 +1,5 @@ +//! 验证会话账号排除的有效期与按已观测状态清理 + use std::time::Duration; use gateway_core::account::ProviderAccountId; diff --git a/backend/crates/gateway-store/tests/redis/worker_lease.rs b/backend/crates/gateway-store/tests/redis/worker_lease.rs index affbc240f..aa74d99d6 100644 --- a/backend/crates/gateway-store/tests/redis/worker_lease.rs +++ b/backend/crates/gateway-store/tests/redis/worker_lease.rs @@ -1,4 +1,4 @@ -//! Worker leader lease 通过 StoreBundle 的中立能力进行集成验证。 +//! Worker leader lease 通过 StoreBundle 的中立能力进行集成验证 use std::collections::BTreeSet; use std::str::FromStr; diff --git a/backend/crates/gateway-store/tests/support.rs b/backend/crates/gateway-store/tests/support.rs index 7912ca27b..dd812ea28 100644 --- a/backend/crates/gateway-store/tests/support.rs +++ b/backend/crates/gateway-store/tests/support.rs @@ -1,7 +1,9 @@ -/// 读取集成测试基础设施地址(PostgreSQL / Redis)。 +//! 存储集成测试的基础设施地址读取与 CI 环境判定辅助 + +/// 读取集成测试基础设施地址(PostgreSQL / Redis) /// /// 本地未配置时返回 `None`,测试跳过;CI 中缺失则直接失败—— -/// 静默跳过产生的绿灯与真实回归不可区分。 +/// 静默跳过产生的绿灯与真实回归不可区分 pub(crate) fn test_env(name: &str) -> Option { let value = std::env::var(name).ok(); assert!( @@ -11,8 +13,8 @@ pub(crate) fn test_env(name: &str) -> Option { value } -/// 按 is-ci 约定判定 CI:`CI` 存在且非 falsey(`0`/`false`/空)。 -/// 开发者用 `CI=false` 显式声明「非 CI」时不应把跳过变成 panic。 +/// 按 is-ci 约定判定 CI:`CI` 存在且非 falsey(`0`/`false`/空) +/// 开发者用 `CI=false` 显式声明「非 CI」时不应把跳过变成 panic fn running_in_ci() -> bool { std::env::var("CI") .ok() diff --git a/backend/crates/providers/openai/src/admin.rs b/backend/crates/providers/openai/src/admin.rs index 34a7714ed..1cbc71cc3 100644 --- a/backend/crates/providers/openai/src/admin.rs +++ b/backend/crates/providers/openai/src/admin.rs @@ -1,4 +1,4 @@ -//! OpenAI 管理边界:Provider preparation 与 Redis OAuth pending 适配。 +//! OpenAI 管理边界:Provider preparation 与 Redis OAuth pending 适配 use std::sync::Arc; use std::time::SystemTime; @@ -72,7 +72,7 @@ use crate::transport::{ const PROVIDER_NAME: &str = "openai"; const PENDING_DOCUMENT_SCHEMA_VERSION: u64 = 3; -/// OpenAI 对终态 Admin port 的唯一实现。 +/// OpenAI 对终态 Admin port 的唯一实现 pub(crate) struct OpenAiAdminProvider { provider_kind: ProviderKind, profile: CodexWireProfileState, @@ -207,7 +207,7 @@ impl ProviderAdmin for OpenAiAdminProvider { } fn plan_type_display(&self, plan_type: &str) -> String { - // 与官方 Desktop 的套餐名称映射一致;原始子类型继续由 plan_type 保留。 + // 与官方 Desktop 的套餐名称映射一致;原始子类型继续由 plan_type 保留 match plan_type.to_ascii_lowercase().as_str() { "free" | "free_workspace" | "guest" => "Free", "go" => "Go", @@ -391,7 +391,7 @@ impl ProviderAdmin for OpenAiAdminProvider { return Ok(None); } Ok(Some(CalculatedBillingBreakdown { - // 历史总额只能核对费用拆分,不能证明当时保存过长上下文标记。 + // 历史总额只能核对费用拆分,不能证明当时保存过长上下文标记 long_context_billing_applied: false, image: None, custom_multiplier_bps: breakdown.custom_multiplier_bps(), @@ -855,7 +855,7 @@ impl ProviderAdmin for OpenAiAdminProvider { .await .map_err(map_catalog_error)?; // 只有 Codex 原生对象带齐推理强度、上下文窗口等元数据;API 目录只有模型 ID, - // 拼出来的文件不满足 Codex `model_catalog_json` 的加载要求,这里直接拒绝而不降格。 + // 拼出来的文件不满足 Codex `model_catalog_json` 的加载要求,这里直接拒绝而不降格 let mut entries = Vec::with_capacity(models.len()); for model in &models { if model.document().protocol() != "codex" { @@ -1168,7 +1168,7 @@ fn project_quota_snapshot(snapshot: CodexAccountQuotaSnapshot) -> ProviderQuota }) .collect(); // 快照级 limit_reached 只看滚动后的窗口触顶:顶层标记是观测事实,不能 - // 在窗口全部过期后继续维持限流。 + // 在窗口全部过期后继续维持限流 let limit_reached = quota_windows_limit_reached(&windows); ProviderQuota { plan_type: snapshot.plan_type().map(str::to_owned), @@ -1267,9 +1267,9 @@ fn project_reset_credit( } } -/// `secondary_window` 有时只是上游的空占位:没有时长、重置时间,也没有用量。 +/// `secondary_window` 有时只是上游的空占位:没有时长、重置时间,也没有用量 /// 它不能被可靠翻译成 5 小时或周额度,因此不应占用账号面板;带有实际事实的 -/// 次级窗口(时长、重置、非零用量或触顶)仍完整保留。 +/// 次级窗口(时长、重置、非零用量或触顶)仍完整保留 fn should_project_quota_window(window: &CodexQuotaWindow) -> bool { window.role() != CodexQuotaWindowRole::Secondary || window.window_seconds().is_some() @@ -1298,7 +1298,7 @@ const fn quota_role(role: CodexQuotaWindowRole) -> ProviderQuotaWindowRole { } } -/// 按窗口时长显示汉化额度名;额度桶名称通过独立字段投影。 +/// 按窗口时长显示汉化额度名;额度桶名称通过独立字段投影 fn codex_quota_window_label( kind: CodexQuotaWindowKind, role: CodexQuotaWindowRole, @@ -1315,7 +1315,7 @@ fn codex_quota_window_label( fn custom_quota_window_label(window_seconds: Option, role: CodexQuotaWindowRole) -> String { let Some(seconds) = window_seconds.filter(|seconds| *seconds > 0) else { // 与官方客户端一致:没有时长时不臆测为 5 小时或周额度;保留 - // primary/secondary 语义,让用户知道这是上游未标明时长的独立窗口。 + // primary/secondary 语义,让用户知道这是上游未标明时长的独立窗口 return match role { CodexQuotaWindowRole::Primary => "主额度".to_owned(), CodexQuotaWindowRole::Secondary => "次级额度".to_owned(), @@ -1331,7 +1331,7 @@ fn custom_quota_window_label(window_seconds: Option, role: CodexQuotaWindow } } -/// 将 Provider-owned PKCE/OIDC 状态保存到 Store 提供的 Redis 原子端口。 +/// 将 Provider-owned PKCE/OIDC 状态保存到 Store 提供的 Redis 原子端口 pub(crate) struct OpenAiOAuthPendingStore { port: Arc, provider_kind: ProviderKind, @@ -1698,12 +1698,12 @@ fn map_credential_admin_error(error: CodexCredentialAdminError) -> ProviderAdmin } fn refresh_rejection_message(code: Option<&str>) -> Option<&'static str> { - // 与官方 Codex 的失败原因对齐;只解释管理提示,不改动 Worker 的终态/退避判定。 + // 与官方 Codex 的失败原因对齐;只解释管理提示,不改动 Worker 的终态/退避判定 match code.map(str::to_ascii_lowercase).as_deref() { Some("refresh_token_expired") => Some("刷新令牌已过期,请重新授权"), Some("refresh_token_reused") => Some("刷新令牌已被使用,请重新授权"), Some("refresh_token_invalidated") => Some("刷新令牌已被撤销,请重新授权"), - // token_expired 也用于 RT 校验失败,不据此断言具体到期原因。 + // token_expired 也用于 RT 校验失败,不据此断言具体到期原因 Some("token_expired") => Some("刷新令牌不可用,请重新授权"), Some("invalid_grant") => Some("刷新令牌无效或已失效,请重新授权"), _ => None, @@ -1823,7 +1823,7 @@ fn map_quota_error(error: CodexCredentialQuotaError) -> ProviderAdminError { detail, } => { // 公开文案只解释已知状态和错误码;原始上游材料留在内部诊断, - // 额度查询拒绝不作为凭据失效证据,也不写入账号的凭据错误字段。 + // 额度查询拒绝不作为凭据失效证据,也不写入账号的凭据错误字段 let (kind, public_message) = match (status, code.as_deref()) { (Some(401), Some("token_revoked")) => ( Kind::BadGateway, diff --git a/backend/crates/providers/openai/src/config.rs b/backend/crates/providers/openai/src/config.rs index 7b92173ce..9e907bbf1 100644 --- a/backend/crates/providers/openai/src/config.rs +++ b/backend/crates/providers/openai/src/config.rs @@ -1,4 +1,4 @@ -//! OpenAI Provider 启动配置;客户端身份由管理端设置持久化。 +//! OpenAI Provider 启动配置;客户端身份由管理端设置持久化 use std::path::{Path, PathBuf}; use std::time::Duration; @@ -17,16 +17,16 @@ use crate::{ }, }; -/// 本服务的流式请求默认重试次数。 +/// 本服务的流式请求默认重试次数 pub const DEFAULT_STREAM_MAX_RETRIES: u64 = 5; -/// 防止错误配置产生无界隐藏重放;与官方 Codex 的硬上限一致。 +/// 防止错误配置产生无界隐藏重放;与官方 Codex 的硬上限一致 pub const MAX_STREAM_MAX_RETRIES: u64 = 100; const fn default_stream_max_retries() -> u64 { DEFAULT_STREAM_MAX_RETRIES } -/// OpenAI Provider 唯一启动配置。 +/// OpenAI Provider 唯一启动配置 #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] pub struct OpenAiConfig { #[serde(skip)] @@ -54,7 +54,7 @@ impl OpenAiConfig { self } - /// 校验 Provider-owned 字段,并从统一运行数据目录定位会话身份密钥。 + /// 校验 Provider-owned 字段,并从统一运行数据目录定位会话身份密钥 pub fn resolve_and_validate( &mut self, runtime_data_dir: &Path, @@ -100,7 +100,7 @@ impl OpenAiConfig { self.auth.refresh_enabled } - /// 返回经官方同款硬上限约束后的上游流重试预算。 + /// 返回经官方同款硬上限约束后的上游流重试预算 #[must_use] pub fn stream_max_retries(&self) -> u32 { u32::try_from(self.stream_max_retries.min(MAX_STREAM_MAX_RETRIES)) @@ -129,7 +129,7 @@ impl Default for OpenAiConfig { } } -/// Codex 上游 API 的 Provider-owned 地址配置。 +/// Codex 上游 API 的 Provider-owned 地址配置 #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] pub struct CodexApiConfig { pub base_url: String, @@ -152,7 +152,7 @@ impl CodexApiConfig { } } -/// Codex Responses WebSocket pool 的 Provider-owned 启动设置。 +/// Codex Responses WebSocket pool 的 Provider-owned 启动设置 #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] pub struct CodexWebSocketPoolSettings { pub enabled: bool, @@ -192,10 +192,10 @@ impl CodexWebSocketPoolSettings { } } -/// OpenAI Provider 的额度刷新策略。 +/// OpenAI Provider 的额度刷新策略 #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] pub struct CodexQuotaSettings { - /// 保留模型目录的刷新周期;额度独立每 30 秒检查周期复核和 reset 到期条件。 + /// 保留模型目录的刷新周期;额度独立每 30 秒检查周期复核和 reset 到期条件 pub refresh_interval_minutes: u64, } @@ -224,7 +224,7 @@ impl CodexQuotaSettings { } } -/// OpenAI OAuth 的 Provider-owned 运行开关和端点。 +/// OpenAI OAuth 的 Provider-owned 运行开关和端点 #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] pub struct CodexAuthSettings { pub refresh_enabled: bool, diff --git a/backend/crates/providers/openai/src/credential/admin.rs b/backend/crates/providers/openai/src/credential/admin.rs index 951273259..0353eec8b 100644 --- a/backend/crates/providers/openai/src/credential/admin.rs +++ b/backend/crates/providers/openai/src/credential/admin.rs @@ -1,7 +1,7 @@ -//! Codex Admin 输入的 Provider-owned 验证与明文 command preparation。 +//! Codex Admin 输入的 Provider-owned 验证与明文 command preparation //! //! 本模块不读写 Store;应用层负责把已验证的 Core command 映射到 -//! 持久层的原子配置 revision + audit 事务。 +//! 持久层的原子配置 revision + audit 事务 use std::collections::BTreeSet; use std::fmt; @@ -49,10 +49,11 @@ pub struct ImportCodexOAuthCredential { pub enabled: bool, } -/// OAuth credential 的最小创建输入。 +/// OAuth credential 的最小创建输入 /// /// OAuth metadata 来自 ID token/access token 的本地 payload 解析;PAT metadata -/// 来自 auth whoami 验证。两者均不从导入文档信任身份字段。 +/// 来自 auth whoami 验证 +/// 两者均不从导入文档信任身份字段 pub(crate) struct UnresolvedCodexOAuthCredential { pub(crate) account_id: String, pub(crate) name: String, @@ -94,7 +95,7 @@ impl std::fmt::Debug for ImportCodexOAuthCredential { } } -/// Provider-owned 文档归一后的唯一 Core 写入批次。 +/// Provider-owned 文档归一后的唯一 Core 写入批次 pub struct PreparedCodexAccountImport { accounts: Vec, } @@ -169,9 +170,9 @@ impl fmt::Debug for ParsedCodexImportAccount { } } -/// Store 公共行事实与 Core 明文 credential 的导出输入。 +/// Store 公共行事实与 Core 明文 credential 的导出输入 /// -/// 时间必须由 App 从 `provider_accounts` 原行机械传入;Provider 不伪造时间。 +/// 时间必须由 App 从 `provider_accounts` 原行机械传入;Provider 不伪造时间 pub struct ExportManagedCodexCredential { pub current: LoadedCredential, pub added_at: DateTime, @@ -190,7 +191,7 @@ impl fmt::Debug for ExportManagedCodexCredential { } } -/// CPR canonical 账号导出文档;只允许显式序列化,Debug 永不输出 credential secret。 +/// CPR canonical 账号导出文档;只允许显式序列化,Debug 永不输出 credential secret #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct CodexCprExportDocument { @@ -271,7 +272,7 @@ struct CodexCprOAuthExportAccount { access_token_expires_at: Option, } -/// App 已从 Store 读取的当前账号、revision 与明文 Provider JSON。 +/// App 已从 Store 读取的当前账号、revision 与明文 Provider JSON pub struct RotateManagedCodexCredential { pub current: LoadedCredential, pub secret: CodexOAuthSecret, @@ -291,7 +292,7 @@ impl std::fmt::Debug for RotateManagedCodexCredential { } } -/// Provider 验证后的 rotation;App 只做 Core -> Store command 的机械映射。 +/// Provider 验证后的 rotation;App 只做 Core -> Store command 的机械映射 pub struct PreparedCodexCredentialRotation { pub profile: ProviderAccountUpdate, pub credential: CredentialCasUpdate, @@ -320,7 +321,7 @@ impl std::fmt::Debug for PreparedCodexCredentialRotation { } impl PreparedCodexCredentialRotation { - /// 将 command 与 lease 一起交给 App;App 必须让返回的 guard 活到 CAS 提交结束。 + /// 将 command 与 lease 一起交给 App;App 必须让返回的 guard 活到 CAS 提交结束 #[must_use] pub fn into_parts( self, @@ -339,7 +340,7 @@ impl PreparedCodexCredentialRotation { } } -/// 手工刷新从 token exchange 到数据库 CAS 完成期间持有的 Redis lease。 +/// 手工刷新从 token exchange 到数据库 CAS 完成期间持有的 Redis lease pub struct PreparedCodexCredentialRotationGuard(Option); impl fmt::Debug for PreparedCodexCredentialRotationGuard { @@ -403,7 +404,7 @@ impl CodexCredentialAdminError { } } -/// 无状态的 Codex Admin command preparer。 +/// 无状态的 Codex Admin command preparer #[derive(Debug, Default, Clone, Copy)] pub struct CodexCredentialAdmin; @@ -590,9 +591,9 @@ impl CodexCredentialAdmin { }) } - /// 为已取得 OAuth access token、但资料尚未补全的账号创建最小记录。 + /// 为已取得 OAuth access token、但资料尚未补全的账号创建最小记录 /// - /// 接受调用方已取得的身份投影;本方法不发起 usage/profile 请求。 + /// 接受调用方已取得的身份投影;本方法不发起 usage/profile 请求 pub(crate) fn prepare_unresolved_oauth( &self, input: UnresolvedCodexOAuthCredential, @@ -651,7 +652,7 @@ impl CodexCredentialAdmin { }) } - /// 严格输出可被 CPR 导入逻辑直接读取的 canonical 文档。 + /// 严格输出可被 CPR 导入逻辑直接读取的 canonical 文档 pub fn format_cpr_export( &self, items: Vec, @@ -734,10 +735,10 @@ impl CodexCredentialAdmin { self.prepare_oauth_rotation(input, false) } - /// 构建一次成功 RT exchange 的 CAS 写入,保留已存账号身份资料。 + /// 构建一次成功 RT exchange 的 CAS 写入,保留已存账号身份资料 /// /// Refresh endpoint 已经完成本次 token 交换的授权;这里不对新 access - /// token 重新执行身份验证。 + /// token 重新执行身份验证 pub(crate) fn prepare_refreshed_oauth_rotation( &self, current: LoadedCredential, @@ -857,7 +858,7 @@ impl CodexCredentialAdmin { } } -/// 有状态的 Codex 手工刷新边界;消费调用方刚读取的当前 credential 并准备 CAS。 +/// 有状态的 Codex 手工刷新边界;消费调用方刚读取的当前 credential 并准备 CAS pub struct CodexCredentialAdminService { refresher: Arc, personal_access_token_client: Option>, @@ -894,14 +895,14 @@ impl CodexCredentialAdminService { } } - /// 在生产组装时复用 OAuth auth client,为 at- 导入启用上游身份验证。 + /// 在生产组装时复用 OAuth auth client,为 at- 导入启用上游身份验证 #[must_use] pub fn with_personal_access_token_client(mut self, client: Arc) -> Self { self.personal_access_token_client = Some(client); self } - /// 官方 RT exchange;结果由 App 在同一 revision/audit 事务中提交。 + /// 官方 RT exchange;结果由 App 在同一 revision/audit 事务中提交 pub async fn manual_refresh( &self, current: LoadedCredential, @@ -988,7 +989,7 @@ impl CodexCredentialAdminService { &secret, ); // 正常预刷新由 worker 读取当时的 runtime policy 动态判断;这里仅清除 - // 之前瞬态失败留下的 retry-not-before。 + // 之前瞬态失败留下的 retry-not-before let next_refresh_at = None; let mut prepared = CodexCredentialAdmin.prepare_refreshed_oauth_rotation( current, @@ -1003,11 +1004,11 @@ impl CodexCredentialAdminService { Ok(prepared) } - /// 归一导入 OAuth 凭据到唯一 `NewProviderAccount` 写入路径。 + /// 归一导入 OAuth 凭据到唯一 `NewProviderAccount` 写入路径 /// /// OAuth 导入先取得 access token(直接提供或 RT exchange),再按官方 - /// `parse_chatgpt_jwt_claims` 从 ID token/access token 本地投影账号资料。 - /// at- PAT 使用 whoami 取得身份,并丢弃不适用的 RT、ID token 和刷新计划。 + /// `parse_chatgpt_jwt_claims` 从 ID token/access token 本地投影账号资料 + /// at- PAT 使用 whoami 取得身份,并丢弃不适用的 RT、ID token 和刷新计划 pub async fn prepare_import_document( &self, payload: Value, @@ -1084,7 +1085,7 @@ impl CodexCredentialAdminService { access_token_expires_at = None; metadata } else { - // 普通 OAuth 仍按 ID token、access token 的顺序投影,不调用 whoami。 + // 普通 OAuth 仍按 ID token、access token 的顺序投影,不调用 whoami let id_metadata = secret .id_token .as_ref() @@ -1168,7 +1169,7 @@ impl CodexCredentialAdminService { let access_token_expires_at = parse_access_token_expiration(&access_token); let secret = CodexOAuthSecret { access_token: SecretString::from(access_token), - // RT 未轮换时仍保留导入提供的 RT,保证本次补全不会丢失后续刷新能力。 + // RT 未轮换时仍保留导入提供的 RT,保证本次补全不会丢失后续刷新能力 refresh_token: tokens .refresh_token .map(SecretString::from) @@ -1234,7 +1235,7 @@ fn map_refresh_failure(error: RefreshFailure) -> CodexCredentialAdminError { } RefreshFailure::RetryableTransport { .. } => CodexCredentialAdminError::RefreshUnavailable, // Worker 的 Transport 分类还承担 401 退避;管理提示只按已收到的响应事实细分, - // 不改变后台刷新策略,也不把明确失败响应误报为租约冲突或执行结果未知。 + // 不改变后台刷新策略,也不把明确失败响应误报为租约冲突或执行结果未知 RefreshFailure::Transport { message, upstream } => match upstream { Some(upstream) => CodexCredentialAdminError::RefreshUpstream { status: upstream.status(), @@ -1358,7 +1359,7 @@ fn import_proxy( } let text = |field| proxy.get(field).and_then(Value::as_str).ok_or_else(invalid); let scheme = text("protocol")?; - // sub2api resolves SOCKS targets at the proxy. + // sub2api 的 SOCKS 目标地址由代理解析 let scheme = if scheme == "socks5" { "socks5h" } else { @@ -1491,14 +1492,14 @@ fn parse_api_key_import(value: &Value) -> Result, @@ -34,7 +34,7 @@ impl CodexSessionAffinity { self.root_key.as_ref() } - /// 子线程首次选号继承根会话偏好,之后仅更新自己的绑定。 + /// 子线程首次选号继承根会话偏好,之后仅更新自己的绑定 fn with_thread(mut self, thread_id: Option<&str>) -> Option { if let Some(thread_id) = non_empty(thread_id) && self @@ -59,7 +59,7 @@ impl CodexSessionAffinity { &self.key_hash } - /// 返回可持久化的客户端作用域不透明会话关联值。 + /// 返回可持久化的客户端作用域不透明会话关联值 #[must_use] pub(crate) fn persistence_hash(&self) -> &str { self.key.expose_to_store() @@ -91,7 +91,7 @@ impl CodexSessionAffinity { } } -/// 将原始 response ID 投影为客户端作用域的不可逆关联值。 +/// 将原始 response ID 投影为客户端作用域的不可逆关联值 #[must_use] pub(crate) fn derive_previous_response_id_hash( previous_response_id: &str, @@ -115,8 +115,9 @@ pub(crate) fn derive_codex_session_affinity( .with_thread(request.client_thread_id.as_deref()) } -/// 原始 JSON 端点只读取会话身份,发送时仍保留原始字节。Search 的 `id` 是官方 -/// 根 session_id,必须与 Responses 共用命名空间,不能另建一份账号亲和。 +/// 原始 JSON 端点只读取会话身份,发送时仍保留原始字节 +/// Search 的 `id` 是官方 +/// 根 session_id,必须与 Responses 共用命名空间,不能另建一份账号亲和 pub(crate) fn derive_codex_endpoint_session_affinity( payload: &RawJsonPayload, client_api_key_id: &ClientApiKeyId, @@ -163,14 +164,14 @@ fn session_affinity( } fn short_key_hash(key: &ProviderSessionAffinityKey) -> String { - // 亲和键本身已经是 SHA-256;日志沿用 WebSocket 诊断的 12 位短哈希长度。 + // 亲和键本身已经是 SHA-256;日志沿用 WebSocket 诊断的 12 位短哈希长度 key.expose_to_store() .chars() .take(AFFINITY_KEY_HASH_LENGTH) .collect() } -/// 先确定根会话锚点,显式子线程在此基础上派生自己的绑定。 +/// 先确定根会话锚点,显式子线程在此基础上派生自己的绑定 fn derive_account_affinity_anchor( request: &CodexResponsesRequest, ) -> Option<(&'static str, String)> { @@ -204,10 +205,10 @@ fn opaque_affinity_key(domain: &str, value: &str) -> Option Result { if account.authentication_kind() == super::CODEX_AUTHENTICATION_KIND_API_KEY { return ProviderCatalogScope::new(format!( @@ -63,14 +63,14 @@ impl CodexCatalogScope { .map_err(|_| CodexCredentialCatalogError::InvalidCredentialData) } - /// 返回稳定的 Provider-owned 作用域。 + /// 返回稳定的 Provider-owned 作用域 #[must_use] pub fn as_str(&self) -> &str { self.0.as_str() } } -/// Redis TTL cache 中一条可重建的套餐模型目录。 +/// Redis TTL cache 中一条可重建的套餐模型目录 #[derive(Clone, Debug, PartialEq, Eq)] pub struct CodexPlanCatalog { scope: CodexCatalogScope, @@ -143,7 +143,7 @@ impl CodexCredentialCatalogSnapshot { .map(|catalog| catalog.models.as_slice())) } - /// 将 Provider 私有目录作用域投影为模型来源账号,供 Core 结合冻结政策过滤。 + /// 将 Provider 私有目录作用域投影为模型来源账号,供 Core 结合冻结政策过滤 #[must_use] pub fn model_catalog_accounts(&self) -> BTreeMap<&str, BTreeSet> { let mut accounts = BTreeMap::<&str, BTreeSet>::new(); @@ -213,7 +213,7 @@ struct ClientCatalogKey { type ClientCatalogResult = Result, CodexCredentialCatalogError>; struct CachedClientCatalog { - // 负缓存从请求结束计时,不能让一次慢失败耗尽后续请求的退避窗口。 + // 负缓存从请求结束计时,不能让一次慢失败耗尽后续请求的退避窗口 completed_at: Instant, result: ClientCatalogResult, } @@ -288,9 +288,9 @@ impl CodexCredentialCatalogService { } } - /// OAuth 目录使用 Key 范围内排序稳定的首个成功账号;API Key 聚合按完整对象优先稳定选源。 - /// 客户端原生对象不复用套餐级画像。 - /// 同一账号/凭据版本/客户端版本的并发读取合并;原生正文仅保留在有界进程缓存。 + /// OAuth 目录使用 Key 范围内排序稳定的首个成功账号;API Key 聚合按完整对象优先稳定选源 + /// 客户端原生对象不复用套餐级画像 + /// 同一账号/凭据版本/客户端版本的并发读取合并;原生正文仅保留在有界进程缓存 pub async fn client_model_catalog( &self, scope: &FrozenAccountScope, @@ -327,7 +327,7 @@ impl CodexCredentialCatalogService { } }) .buffer_unordered(MAX_CONCURRENT_API_CATALOGS); - // 所有 API 账号共享等待预算;慢上游不能逐个耗尽超时或阻塞 OAuth 目录。 + // 所有 API 账号共享等待预算;慢上游不能逐个耗尽超时或阻塞 OAuth 目录 let deadline = Instant::now() + MODEL_CATALOG_TIMEOUT; while let Ok(Some((account, result))) = tokio::time::timeout_at(deadline, catalogs.next()).await @@ -338,7 +338,7 @@ impl CodexCredentialCatalogService { } } drop(catalogs); - // 完整对象优先于普通 ID;同类目录按账号顺序选来源,并保留上游条目顺序。 + // 完整对象优先于普通 ID;同类目录按账号顺序选来源,并保留上游条目顺序 let (native, adapted): (Vec<_>, Vec<_>) = api_catalogs .into_values() .flatten() @@ -405,7 +405,7 @@ impl CodexCredentialCatalogService { if let Some(entry) = cache.iter().find(|entry| entry.key == key) { Arc::clone(&entry.value) } else { - // 不驱逐正在读取的目录,避免并发请求绕过缓存容量限制制造额外出站。 + // 不驱逐正在读取的目录,避免并发请求绕过缓存容量限制制造额外出站 if cache.len() >= MAX_CLIENT_CATALOGS { let Some(index) = cache.iter().position(|entry| entry.value.initialized()) else { @@ -424,7 +424,7 @@ impl CodexCredentialCatalogService { }; let result = value .get_or_init(|| async { - // 冻结画像,确保实际请求与缓存身份一致;不传递客户端 Bearer 给上游。 + // 冻结画像,确保实际请求与缓存身份一致;不传递客户端 Bearer 给上游 let client = CodexBackendClient::new( self.http.clone(), self.base_url.clone(), @@ -483,7 +483,7 @@ impl CodexCredentialCatalogService { .cloned()) } - /// 读取账号当前已验证套餐的模型 entitlement;不触发网络。 + /// 读取账号当前已验证套餐的模型 entitlement;不触发网络 pub fn cached_account_models( &self, account: &ProviderAccount, @@ -496,7 +496,7 @@ impl CodexCredentialCatalogService { .map(|models| models.map(<[String]>::to_vec)) } - /// 优先读取套餐目录 cache;缺失时才用当前账号所属套餐的有限候选集实时填充。 + /// 优先读取套餐目录 cache;缺失时才用当前账号所属套餐的有限候选集实时填充 pub async fn cached_or_refresh_account_catalog( &self, account: &ProviderAccount, @@ -508,7 +508,7 @@ impl CodexCredentialCatalogService { self.refresh_account_catalog(account.id()).await } - /// 实时刷新指定账号所属套餐的模型集合,并覆盖可重建 Redis cache。 + /// 实时刷新指定账号所属套餐的模型集合,并覆盖可重建 Redis cache pub async fn refresh_account_catalog( &self, account_id: &ProviderAccountId, @@ -525,12 +525,12 @@ impl CodexCredentialCatalogService { let mut candidates = match catalog_candidates_by_scope(self.repository.list_for_provider().await?) { Ok(mut groups) => groups.remove(&scope).unwrap_or_default(), - // 全部账号都被调度列表过滤时按空候选处理,让下方目标账号补回继续生效。 + // 全部账号都被调度列表过滤时按空候选处理,让下方目标账号补回继续生效 Err(CodexCredentialCatalogError::NoEligibleCredential) => Vec::new(), Err(error) => return Err(error), }; // 常规调度列表不含停用账号;管理端按账号查询模型要对停用账号返回真实上游 - // 结果,这里把不在候选里的目标账号本身补回,仍按优先顺序先试目标账号。 + // 结果,这里把不在候选里的目标账号本身补回,仍按优先顺序先试目标账号 if !candidates .iter() .any(|candidate| candidate.id() == account_id) @@ -558,11 +558,11 @@ impl CodexCredentialCatalogService { Ok(catalog) } - /// 读取目标账号的原生目录条目;供管理端按账号导出目录文件使用。 + /// 读取目标账号的原生目录条目;供管理端按账号导出目录文件使用 /// /// 按账号直接请求上游,避免进程快照的跨套餐同名模型合并替换目标账号的原生对象; - /// 停用账号也不在常规快照候选中,仍允许管理员显式导出。 - /// 返回顺序沿用上游目录顺序,条目内容保持上游原样,不做别名改写或字段裁剪。 + /// 停用账号也不在常规快照候选中,仍允许管理员显式导出 + /// 返回顺序沿用上游目录顺序,条目内容保持上游原样,不做别名改写或字段裁剪 pub async fn account_catalog_documents( &self, account: &ProviderAccount, @@ -579,7 +579,7 @@ impl CodexCredentialCatalogService { Ok((fetched.models, SystemTime::now())) } - /// 读取当前账号所属套餐的目录 cache,不触发上游请求。 + /// 读取当前账号所属套餐的目录 cache,不触发上游请求 pub async fn read_account_catalog( &self, account: &ProviderAccount, @@ -639,7 +639,7 @@ impl CodexCredentialCatalogService { entry.insert(model.clone()); } Entry::Occupied(mut entry) => { - // 原生目录含完整能力证据,同名 API ID 不能覆盖它,也不能跨来源拼字段。 + // 原生目录含完整能力证据,同名 API ID 不能覆盖它,也不能跨来源拼字段 if model.document().protocol() == "codex" && entry.get().document().protocol() != "codex" { @@ -792,7 +792,7 @@ impl CodexCredentialCatalogService { Ok(()) } - /// 记录普通 Responses 响应声明的目录版本;相同版本只触发一次。 + /// 记录普通 Responses 响应声明的目录版本;相同版本只触发一次 pub fn observe_response_etag(&self, etag: &str) -> Result { validate_response_etag(etag)?; let changed = { @@ -817,7 +817,7 @@ impl CodexCredentialCatalogService { Ok(changed) } - /// 等待并认领一次需要强制刷新的 Provider 目录。 + /// 等待并认领一次需要强制刷新的 Provider 目录 pub async fn wait_for_etag_refresh(&self) { loop { if self.begin_pending_etag_refresh() { @@ -827,17 +827,17 @@ impl CodexCredentialCatalogService { } } - /// 立即刷新所有套餐目录,但不接管 ETag daemon 已认领的刷新状态。 + /// 立即刷新所有套餐目录,但不接管 ETag daemon 已认领的刷新状态 /// /// 周期 worker 与 ETag daemon 可以同时请求目录;只有后者可以完成 - /// `inflight` ETag 的状态转换,避免周期刷新错误地确认另一个请求的版本。 + /// `inflight` ETag 的状态转换,避免周期刷新错误地确认另一个请求的版本 pub async fn refresh_catalogs( &self, ) -> Result { self.refresh_inner().await } - /// 忽略当前 cache,按已认领的 ETag 变化强制生成一份完整新快照。 + /// 忽略当前 cache,按已认领的 ETag 变化强制生成一份完整新快照 pub async fn refresh( &self, ) -> Result { diff --git a/backend/crates/providers/openai/src/credential/cookie.rs b/backend/crates/providers/openai/src/credential/cookie.rs index ba38b0540..fc95bf2af 100644 --- a/backend/crates/providers/openai/src/credential/cookie.rs +++ b/backend/crates/providers/openai/src/credential/cookie.rs @@ -1,3 +1,5 @@ +//! Codex Cookie 的接收、作用域校验、过期解析与回放限制 + use std::collections::HashSet; use chrono::{DateTime, TimeDelta, Utc}; diff --git a/backend/crates/providers/openai/src/credential/mod.rs b/backend/crates/providers/openai/src/credential/mod.rs index b8cc9db69..beab34eab 100644 --- a/backend/crates/providers/openai/src/credential/mod.rs +++ b/backend/crates/providers/openai/src/credential/mod.rs @@ -1,4 +1,4 @@ -//! Codex credential 领域导出。 +//! Codex credential 领域导出 mod admin; mod affinity; diff --git a/backend/crates/providers/openai/src/credential/oauth.rs b/backend/crates/providers/openai/src/credential/oauth.rs index 3d7dfe782..b41f3d6df 100644 --- a/backend/crates/providers/openai/src/credential/oauth.rs +++ b/backend/crates/providers/openai/src/credential/oauth.rs @@ -1,4 +1,4 @@ -//! Codex Authorization Code + PKCE/OIDC 管理流。 +//! Codex Authorization Code + PKCE/OIDC 管理流 use std::{fmt, sync::Arc, time::Duration}; @@ -76,14 +76,14 @@ pub struct CompleteCodexOAuthAuthorization { pub callback_url: SecretString, } -/// OAuth exchange 后返回的 Provider prepared credential 及其原始事务信封。 +/// OAuth exchange 后返回的 Provider prepared credential 及其原始事务信封 pub struct CompletedCodexOAuthAuthorization { pub mutation: PendingAuthorizationMutation, pub credential: T, authorization_guard: Box, } -/// OAuth exchange 后唯一的 credential preparation 结果。 +/// OAuth exchange 后唯一的 credential preparation 结果 pub enum CompletedCodexOAuthCredential { Create(NewProviderAccount), Reauthorize(PreparedCodexCredentialRotation), @@ -806,7 +806,7 @@ fn callback_parts(value: &str) -> Result<(SecretString, SecretString), CodexOAut "code" => set_unique_callback_parameter(&mut code, value.into_owned())?, "state" => set_unique_callback_parameter(&mut state, value.into_owned())?, // 回调地址只承载 query 参数;安全绑定由唯一的 code/state、 - // 服务端保存的 state 和 PKCE token exchange 共同完成。 + // 服务端保存的 state 和 PKCE token exchange 共同完成 _ => {} } } diff --git a/backend/crates/providers/openai/src/credential/profile_statistics.rs b/backend/crates/providers/openai/src/credential/profile_statistics.rs index 1cf52f651..73e2ea8f4 100644 --- a/backend/crates/providers/openai/src/credential/profile_statistics.rs +++ b/backend/crates/providers/openai/src/credential/profile_statistics.rs @@ -1,4 +1,4 @@ -//! OpenAI 当前账号个人资料统计与官方头像查询编排。 +//! OpenAI 当前账号个人资料统计与官方头像查询编排 use std::{ collections::HashMap, @@ -130,7 +130,7 @@ impl CodexCredentialProfileService { } } - /// 订阅只按需读取;不缓存、不刷新凭据,也不更新额度状态。 + /// 订阅只按需读取;不缓存、不刷新凭据,也不更新额度状态 pub async fn subscription( &self, account_id: &ProviderAccountId, @@ -158,7 +158,7 @@ impl CodexCredentialProfileService { &upstream_account_id, ) .await; - // 请求期间重新授权、换绑或删除账号时,丢弃旧身份的结果。 + // 请求期间重新授权、换绑或删除账号时,丢弃旧身份的结果 let current = self .repository .store() @@ -203,7 +203,7 @@ impl CodexCredentialProfileService { Ok(statistics) } - /// URL 缓存不缓存认证;每次下载都重新读取所属账号当前的 token。 + /// URL 缓存不缓存认证;每次下载都重新读取所属账号当前的 token async fn account_authentication( &self, account_id: &ProviderAccountId, diff --git a/backend/crates/providers/openai/src/credential/quota/document.rs b/backend/crates/providers/openai/src/credential/quota/document.rs index d28b29bb8..8301a07e8 100644 --- a/backend/crates/providers/openai/src/credential/quota/document.rs +++ b/backend/crates/providers/openai/src/credential/quota/document.rs @@ -1,4 +1,4 @@ -//! OpenAI quota 文档的规范化 map 与上游 `/usage` 输入适配。 +//! OpenAI quota 文档的规范化 map 与上游 `/usage` 输入适配 use std::collections::BTreeMap; @@ -8,14 +8,14 @@ use serde_json::{Map, Value}; pub(super) const DEFAULT_CODEX_LIMIT_ID: &str = "codex"; pub(super) const RATE_LIMITS_BY_LIMIT_ID: &str = "rate_limits_by_limit_id"; -/// 官方新协议的多桶视图:每个 `limit_id` 对应一个独立快照。 +/// 官方新协议的多桶视图:每个 `limit_id` 对应一个独立快照 #[derive(Debug, Default)] pub(super) struct RateLimitSnapshotsByLimitId { snapshots: BTreeMap>, } impl RateLimitSnapshotsByLimitId { - /// 接收上游 `/usage` 形状或已经规范化的 map;输出始终只保留 map。 + /// 接收上游 `/usage` 形状或已经规范化的 map;输出始终只保留 map pub(super) fn take_from_document(quota: &mut Map) -> Self { let mut result = Self::default(); @@ -77,7 +77,7 @@ impl RateLimitSnapshotsByLimitId { Value::String(limit_name.to_owned()), ); } - // map 协议中一个 ID 只能有一个快照;规范化 map 与顶层账号桶优先。 + // map 协议中一个 ID 只能有一个快照;规范化 map 与顶层账号桶优先 self.snapshots.entry(limit_id).or_insert(snapshot); } @@ -104,11 +104,12 @@ impl RateLimitSnapshotsByLimitId { self.snapshots.insert(limit_id.to_owned(), snapshot); } - /// 把 wire 观察解析到稳定 `limit_id`。 + /// 把 wire 观察解析到稳定 `limit_id` /// /// WebSocket 的 `additional_rate_limits` 可能只返回可读名称;这时只接受 - /// `/usage` 或 HTTP headers 已建立的唯一名称映射。无法唯一解析就不落库, - /// 避免凭名称猜 ID 或污染默认 `codex` 桶。 + /// `/usage` 或 HTTP headers 已建立的唯一名称映射 + /// 无法唯一解析就不落库, + /// 避免凭名称猜 ID 或污染默认 `codex` 桶 pub(super) fn resolve_limit_id(&self, details: &RateLimitDetails) -> Option { if details.key_source == RateLimitKeySource::LimitId { return normalize_limit_id(&details.limit_id); diff --git a/backend/crates/providers/openai/src/credential/quota/evidence.rs b/backend/crates/providers/openai/src/credential/quota/evidence.rs index 76a37379f..46509c95f 100644 --- a/backend/crates/providers/openai/src/credential/quota/evidence.rs +++ b/backend/crates/providers/openai/src/credential/quota/evidence.rs @@ -1,4 +1,4 @@ -//! 额度接口失败到规范化账号事实的唯一分类边界。 +//! 额度接口失败到规范化账号事实的唯一分类边界 use gateway_core::account::{AccountErrorReason, CredentialState, QuotaEvidence}; use reqwest::StatusCode; @@ -15,8 +15,9 @@ pub(crate) enum QuotaEndpointFailure { Exhausted(QuotaEvidence), } -/// 额度 endpoint 只有明确 402 能改变账号事实。401/403、429、5xx 与传输失败 -/// 均不足以判断凭据或账号状态,留给 OAuth 与真实推理请求确认。 +/// 额度 endpoint 只有明确 402 能改变账号事实 +/// 401/403、429、5xx 与传输失败 +/// 均不足以判断凭据或账号状态,留给 OAuth 与真实推理请求确认 pub(crate) fn classify_quota_endpoint_failure( error: &CodexClientError, ) -> Option { diff --git a/backend/crates/providers/openai/src/credential/quota/mod.rs b/backend/crates/providers/openai/src/credential/quota/mod.rs index ad934a9d0..1dd22fd26 100644 --- a/backend/crates/providers/openai/src/credential/quota/mod.rs +++ b/backend/crates/providers/openai/src/credential/quota/mod.rs @@ -1,9 +1,9 @@ -//! Codex quota 服务编排:权威额度事实、展示投影、主动/被动同步与 429 冷却。 +//! Codex quota 服务编排:权威额度事实、展示投影、主动/被动同步与 429 冷却 //! -//! - [`document`]:上游 `/usage` 输入到多桶 map 的单向规范化。 -//! - [`snapshot`]:快照/窗口解析、聚合、O(1) 滚动与调度信号。 -//! - [`evidence`]:额度接口错误到凭据/额度事实的分类。 -//! - [`recovery`]:各额度窗口独立的恢复基准与访问结论。 +//! - [`document`]:上游 `/usage` 输入到多桶 map 的单向规范化 +//! - [`snapshot`]:快照/窗口解析、聚合、O(1) 滚动与调度信号 +//! - [`evidence`]:额度接口错误到凭据/额度事实的分类 +//! - [`recovery`]:各额度窗口独立的恢复基准与访问结论 mod document; pub(crate) mod evidence; @@ -65,19 +65,19 @@ pub(crate) const QUOTA_SCHEDULING_TTL: Duration = Duration::from_secs(10 * 60); const QUOTA_HYDRATION_FAILURE_TTL: Duration = Duration::from_secs(5); const PERIODIC_QUOTA_REFRESH_RETRY_INTERVAL: Duration = Duration::from_secs(30 * 60); const QUOTA_RESET_GRACE: Duration = Duration::from_secs(2 * 60); -/// 首次 OAuth 异步观察失败时,由既有 quota worker 兜底重试的单轮上限。 +/// 首次 OAuth 异步观察失败时,由既有 quota worker 兜底重试的单轮上限 const INITIAL_QUOTA_SYNC_BATCH: usize = 100; -// 5xx 上游拒绝的短退避重试预算;指数退避 1s/2s,吞掉瞬时抖动。 +// 5xx 上游拒绝的短退避重试预算;指数退避 1s/2s,吞掉瞬时抖动 const QUOTA_FETCH_5XX_MAX_RETRIES: u32 = 2; const QUOTA_FETCH_5XX_BASE_DELAY: Duration = Duration::from_secs(1); const WARMUP_REQUEST_TIMEOUT: Duration = Duration::from_secs(30); const WARMUP_STREAM_MAX_BYTES: usize = 256 * 1024; -/// OpenAI Provider 主动额度刷新的调度策略。 +/// OpenAI Provider 主动额度刷新的调度策略 /// -/// 正常账号依赖请求响应的被动额度同步;周期 worker 仅复核已耗尽账号。 +/// 正常账号依赖请求响应的被动额度同步;周期 worker 仅复核已耗尽账号 /// 该策略保留模型目录的周期刷新频率;额度到期检查使用独立的短周期, -/// 避免到达 reset 后还要等待完整的目录刷新周期。 +/// 避免到达 reset 后还要等待完整的目录刷新周期 #[derive(Debug, Clone, Copy)] pub struct CodexQuotaRefreshPolicy { interval: Duration, @@ -136,7 +136,7 @@ fn observe_warmup_events(events: Vec, terminal: &mut WarmupTerminal) { .or(event.event.as_deref()); match kind { Some("response.completed") => { - // 只有明确完成的响应才算预热成功;失败事件即使随后出现完成帧也优先。 + // 只有明确完成的响应才算预热成功;失败事件即使随后出现完成帧也优先 if *terminal != WarmupTerminal::Failed { *terminal = if parsed.is_some() && parsed @@ -204,14 +204,14 @@ pub enum CodexCredentialQuotaError { #[error("Codex quota upstream query failed: {detail}")] Upstream { detail: String, - /// 上游 HTTP 状态码;传输失败等无响应场景为 `None`。 + /// 上游 HTTP 状态码;传输失败等无响应场景为 `None` status: Option, - /// 有界错误码,供内部诊断和已知码映射使用,不直接拼入公开提示。 + /// 有界错误码,供内部诊断和已知码映射使用,不直接拼入公开提示 code: Option, }, } -/// 主动额度重置卡查询/消费失败。 +/// 主动额度重置卡查询/消费失败 #[derive(Error)] pub enum CodexResetCreditsError { #[error("Codex reset-credit credential data is invalid")] @@ -267,7 +267,7 @@ impl From for CodexCredentialQuotaError { } } -/// 客户端错误携带的 HTTP 状态码;传输失败等为 `None`。 +/// 客户端错误携带的 HTTP 状态码;传输失败等为 `None` fn upstream_error_status(error: &CodexClientError) -> Option { match error { CodexClientError::Upstream { status, .. } => Some(status.as_u16()), @@ -275,9 +275,9 @@ fn upstream_error_status(error: &CodexClientError) -> Option { } } -/// 从上游错误体提取稳定错误码:优先 `/error/code`,其次 `/code`。 +/// 从上游错误体提取稳定错误码:优先 `/error/code`,其次 `/code` /// -/// 只接受有界的 ASCII 标识,避免把自由文本作为错误码写入诊断。 +/// 只接受有界的 ASCII 标识,避免把自由文本作为错误码写入诊断 fn upstream_error_code(error: &CodexClientError) -> Option { let CodexClientError::Upstream { body, .. } = error else { return None; @@ -308,13 +308,13 @@ pub struct CodexCredentialQuotaService { cooldowns: Arc, leases: Arc, runtime_policy: Arc, - /// 冻结策略的短 TTL 缓存:失败路径热读,避免每个容量错误都查询设置。 + /// 冻结策略的短 TTL 缓存:失败路径热读,避免每个容量错误都查询设置 freeze_policy_cache: Mutex>, scheduling: CodexQuotaSchedulingProjection, reset_consume_locks: Mutex>>>, } -/// 冻结策略缓存活跃期;过期后下一次容量错误重新读取运行时设置。 +/// 冻结策略缓存活跃期;过期后下一次容量错误重新读取运行时设置 const FREEZE_POLICY_CACHE_TTL: Duration = Duration::from_secs(30); #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -536,9 +536,9 @@ impl CodexQuotaSchedulingProjection { .last_periodic_refresh_at .retain(|account_id, _| candidate_ids.contains(account_id)); - // 已耗尽账号首次立即复核,之后每 30 分钟复核,以发现官方提前重置。 - // reset + 2 分钟额外触发一次复核,给上游重置留出传播时间。 - // 正常账号仅在非零用量窗口经过宽限期后参与,未更新时复用周期节流。 + // 已耗尽账号首次立即复核,之后每 30 分钟复核,以发现官方提前重置 + // reset + 2 分钟额外触发一次复核,给上游重置留出传播时间 + // 正常账号仅在非零用量窗口经过宽限期后参与,未更新时复用周期节流 let mut reserved = Vec::new(); for (account, target_reset) in candidates { if !periodic_quota_refresh_due(&state, account.id(), target_reset, now, refreshed_at) { @@ -569,7 +569,7 @@ fn quota_refresh_candidate( let reset_at = account.quota().reset_at(); return Some((account, reset_at)); } - // 正常账号首次进入周期候选也要等待宽限期,不能由“无刷新历史”绕过。 + // 正常账号首次进入周期候选也要等待宽限期,不能由“无刷新历史”绕过 let snapshot = snapshot?; let expired_window_reset = snapshot .windows() @@ -603,7 +603,7 @@ fn periodic_quota_refresh_due( >= PERIODIC_QUOTA_REFRESH_RETRY_INTERVAL || reset_at .and_then(|reset| reset.checked_add(QUOTA_RESET_GRACE)) - // 已在该边界之后复核过时回到周期重试,避免过期 reset 每轮触发。 + // 已在该边界之后复核过时回到周期重试,避免过期 reset 每轮触发 .is_some_and(|due_at| last.wall_at < due_at && due_at <= now) }) } @@ -652,7 +652,7 @@ impl CodexCredentialQuotaService { } } - /// 读取容量熔断策略(带短 TTL 缓存);读取失败退化为关闭,熔断不得放大失败。 + /// 读取容量熔断策略(带短 TTL 缓存);读取失败退化为关闭,熔断不得放大失败 async fn freeze_policy(&self) -> ProviderFreezePolicy { { let cache = self.freeze_policy_cache.lock().await; @@ -675,8 +675,9 @@ impl CodexCredentialQuotaService { } /// 容量熔断入口:滑动窗口内累计容量类失败,达到阈值即写入带 - /// `CapacityFreeze` 类别的账号级冷却。调度侧立即屏蔽该账号,恢复由 - /// freeze-recovery worker 处理;本路径只依赖 Redis 可丢失事实。 + /// `CapacityFreeze` 类别的账号级冷却 + /// 调度侧立即屏蔽该账号,恢复由 + /// freeze-recovery worker 处理;本路径只依赖 Redis 可丢失事实 pub async fn apply_capacity_failure(&self, account: &ProviderAccount, observed_at: SystemTime) { let policy = self.freeze_policy().await; if !policy.enabled() { @@ -727,7 +728,7 @@ impl CodexCredentialQuotaService { .unwrap_or(0) } - /// 查询当前账号由 Codex Desktop 暴露的主动额度重置卡。 + /// 查询当前账号由 Codex Desktop 暴露的主动额度重置卡 pub async fn list_reset_credits( &self, account_id: &ProviderAccountId, @@ -753,7 +754,8 @@ impl CodexCredentialQuotaService { .map_err(|error| map_reset_credit_attempt_error(error, false)) } - /// 消费一张主动额度重置卡。相同账号在本进程内串行,且不做传输重试。 + /// 消费一张主动额度重置卡 + /// 相同账号在本进程内串行,且不做传输重试 pub async fn consume_reset_credit( &self, account_id: &ProviderAccountId, @@ -817,7 +819,7 @@ impl CodexCredentialQuotaService { Ok(account) } - /// 真实推理错误只更新额度访问事实,不伪造 Provider JSON 或展示百分比。 + /// 真实推理错误只更新额度访问事实,不伪造 Provider JSON 或展示百分比 pub(crate) async fn record_confirmed_exhaustion( &self, account: &ProviderAccount, @@ -839,14 +841,14 @@ impl CodexCredentialQuotaService { Ok(()) } - /// 成功推理是额度可访问的权威证据,同时解除账号级 429 冷却。 + /// 成功推理是额度可访问的权威证据,同时解除账号级 429 冷却 pub async fn record_successful_inference( &self, account: &ProviderAccount, observed_at: SystemTime, ) -> Result<(), CodexCredentialQuotaError> { // 已经发出的并发请求可能晚于耗尽事实成功返回;真实成功仍是独立的 - // Allowed 权威证据,但调度不会为了试探恢复而放行耗尽账号。 + // Allowed 权威证据,但调度不会为了试探恢复而放行耗尽账号 if account.quota().access() == QuotaAccessState::Exhausted { let outcome = self .store @@ -869,7 +871,7 @@ impl CodexCredentialQuotaService { Ok(()) } - /// 批量预热请求级额度投影;持久层或 Provider JSON 异常只退化为未知额度。 + /// 批量预热请求级额度投影;持久层或 Provider JSON 异常只退化为未知额度 pub async fn prepare_scheduling(&self, accounts: &[ProviderAccount]) { if self.scheduling.hydration_targets(accounts).is_empty() { return; @@ -921,7 +923,7 @@ impl CodexCredentialQuotaService { self.synchronize_at(SystemTime::now()).await } - /// 按本轮调度时刻选择到期账号;实际 HTTP 观察与落库仍使用发生时刻。 + /// 按本轮调度时刻选择到期账号;实际 HTTP 观察与落库仍使用发生时刻 pub async fn synchronize_at( &self, now: SystemTime, @@ -972,7 +974,7 @@ impl CodexCredentialQuotaService { let observed_at = SystemTime::now(); match self.fetch_usage(&client, &account).await { Ok(FetchedCodexQuota { account, value }) => { - // 单账号解析或落库失败只影响该账号;其余账号继续同步。 + // 单账号解析或落库失败只影响该账号;其余账号继续同步 if let Err(error) = self .apply_fetched_quota(&account, &value, observed_at, &mut summary) .await @@ -1031,7 +1033,7 @@ impl CodexCredentialQuotaService { &self.runtime_policy } - /// 批量预激活 OAuth 账号的 5h 配额滑动窗口。 + /// 批量预激活 OAuth 账号的 5h 配额滑动窗口 pub async fn execute_warmup( &self, model: &str, @@ -1147,7 +1149,7 @@ impl CodexCredentialQuotaService { None, ); - // HTTP 200 只确认响应头;必须消费 SSE 直到终态才能确认预热结果和额度事件。 + // HTTP 200 只确认响应头;必须消费 SSE 直到终态才能确认预热结果和额度事件 let attempt = tokio::time::timeout(WARMUP_REQUEST_TIMEOUT, async { let mut response = client_for_account .create_response_stream_http_sse(&upstream_request, context) @@ -1165,7 +1167,7 @@ impl CodexCredentialQuotaService { Ok(Ok((terminal, headers, rate_limit_updates))) => { match terminal { Ok(()) => { - // 被动额度同步会把成功请求的窗口事实标为可用,失败流不能复用该结论。 + // 被动额度同步会把成功请求的窗口事实标为可用,失败流不能复用该结论 if !headers.is_empty() && let Err(error) = self.synchronize_passive_headers(&account, &headers).await @@ -1218,7 +1220,7 @@ impl CodexCredentialQuotaService { Ok(summary) } - /// `quota_observed_at` 为空代表首次异步观察尚未成功;不另建同步状态表。 + /// `quota_observed_at` 为空代表首次异步观察尚未成功;不另建同步状态表 fn initial_quota_sync_accounts( accounts: &[ProviderAccount], observed_ids: &BTreeSet, @@ -1234,7 +1236,7 @@ impl CodexCredentialQuotaService { .collect() } - /// 解析并 revision-fenced 落库单账号的 Provider quota JSON。 + /// 解析并 revision-fenced 落库单账号的 Provider quota JSON async fn apply_fetched_quota( &self, account: &ProviderAccount, @@ -1291,7 +1293,7 @@ impl CodexCredentialQuotaService { Ok(()) } - /// 把正常推理响应携带的限流事实合并进 Provider 原始 quota JSON。 + /// 把正常推理响应携带的限流事实合并进 Provider 原始 quota JSON pub async fn synchronize_passive_headers( &self, account: &ProviderAccount, @@ -1304,7 +1306,7 @@ impl CodexCredentialQuotaService { .await } - /// 把一次推理响应中采集的结构化限流观察合并后单次落库。 + /// 把一次推理响应中采集的结构化限流观察合并后单次落库 pub async fn synchronize_passive_rate_limits( &self, account: &ProviderAccount, @@ -1334,7 +1336,7 @@ impl CodexCredentialQuotaService { let existing = existing .map(|observation| observation.quota.into_inner()) .unwrap_or_default(); - // 只用本次响应明确携带的套餐更新账号,不能把合并前的旧快照重新当作新证据。 + // 只用本次响应明确携带的套餐更新账号,不能把合并前的旧快照重新当作新证据 let observed_plan = rate_limits .iter() .rev() @@ -1346,7 +1348,7 @@ impl CodexCredentialQuotaService { .next(); let plan_type = observed_account_plan(account.plan_type(), observed_plan); // 套餐、credits 等元数据可以更新,但没有额度窗口事实时必须保留旧观察时刻, - // 也不能借旧快照重新推导 quota state。 + // 也不能借旧快照重新推导 quota state if !has_quota_facts { let Some(state) = existing_state else { return Ok(false); @@ -1372,7 +1374,7 @@ impl CodexCredentialQuotaService { observed_at, &Value::Object(quota.clone()), )?; - // 这些 headers 来自一次成功推理,访问结论优先于可能滞后的百分比。 + // 这些 headers 来自一次成功推理,访问结论优先于可能滞后的百分比 let state = QuotaState::allowed(observed_at); let outcome = self .store @@ -1392,10 +1394,10 @@ impl CodexCredentialQuotaService { Ok(true) } - /// 真实 429 的单一事实入口:写入 Redis 临时限流冷却(`until = now + retry_after`)。 + /// 真实 429 的单一事实入口:写入 Redis 临时限流冷却(`until = now + retry_after`) /// 凭据与额度主窗口(额度重置时间)都不改变——临时限流是独立维度, - /// 到期由 Redis key 过期自动解除,不污染配额耗尽状态。 - /// 已有更晚的冷却不会被缩短(put_if_later)。 + /// 到期由 Redis key 过期自动解除,不污染配额耗尽状态 + /// 已有更晚的冷却不会被缩短(put_if_later) pub async fn apply_rate_limit_429( &self, account: &ProviderAccount, @@ -1418,7 +1420,7 @@ impl CodexCredentialQuotaService { Ok(()) } - /// 读取有效的账号冷却事实;等待恢复探测的冻结到期后仍有效。 + /// 读取有效的账号冷却事实;等待恢复探测的冻结到期后仍有效 pub async fn cooldown( &self, account_id: &ProviderAccountId, @@ -1435,7 +1437,7 @@ impl CodexCredentialQuotaService { Ok(state.is_active(SystemTime::now()).then_some(state)) } - /// 读取单账号最后一次落库的 Provider quota,并由 Codex 域解析展示窗口。 + /// 读取单账号最后一次落库的 Provider quota,并由 Codex 域解析展示窗口 pub async fn read_account( &self, account_id: &ProviderAccountId, @@ -1486,7 +1488,7 @@ impl CodexCredentialQuotaService { Ok(Some(snapshot)) } - /// 只刷新指定账号,revision-fenced 写入动态 Provider JSON 后返回解析快照。 + /// 只刷新指定账号,revision-fenced 写入动态 Provider JSON 后返回解析快照 pub async fn refresh_account( &self, account_id: &ProviderAccountId, @@ -1495,7 +1497,7 @@ impl CodexCredentialQuotaService { .await } - /// 真实限额失败后的异步刷新只补齐展示快照,不允许 usage 快照撤销已确认的失败状态。 + /// 真实限额失败后的异步刷新只补齐展示快照,不允许 usage 快照撤销已确认的失败状态 pub(crate) async fn refresh_account_after_failure( &self, account_id: &ProviderAccountId, @@ -1763,8 +1765,9 @@ fn map_reset_credit_client_error(error: CodexClientError, consume: bool) -> Code .. } => { // 2xx 后发生的解码/响应体上限错误使用 synthetic 502 表示,但额度卡 - // 可能已经消费。只有 transport 记录的真实非成功状态与错误状态一致时, - // 才能把它当作确定的上游拒绝并允许前端清除 pending 幂等键。 + // 可能已经消费 + // 只有 transport 记录的真实非成功状态与错误状态一致时, + // 才能把它当作确定的上游拒绝并允许前端清除 pending 幂等键 if consume && !reset_credit_response_was_explicit_rejection(status, &diagnostics) { CodexResetCreditsError::ConsumeResultUnknown } else { @@ -1810,9 +1813,9 @@ async fn fetch_usage_once( .map_err(CodexQuotaFetchAttemptError::Upstream) } -/// 对 5xx 上游拒绝做有限次指数退避重试(1s/2s),吞掉瞬时抖动。 +/// 对 5xx 上游拒绝做有限次指数退避重试(1s/2s),吞掉瞬时抖动 /// -/// 4xx(含 402/429)不重试:它们已经走额度状态转换,重试只会放大上游负载。 +/// 4xx(含 402/429)不重试:它们已经走额度状态转换,重试只会放大上游负载 async fn fetch_usage_with_5xx_retry( client: &CodexBackendClient, prepared: &PreparedCodexRuntimeCredential, @@ -1843,13 +1846,13 @@ async fn fetch_usage_with_5xx_retry( } } -/// 上游额度可确认套餐变更;同族泛化值不能丢弃 JWT 已给出的具体 SKU。 +/// 上游额度可确认套餐变更;同族泛化值不能丢弃 JWT 已给出的具体 SKU fn observed_account_plan(current: Option<&str>, observed: Option<&str>) -> Option { let plan = observed?.trim().to_ascii_lowercase(); if plan.is_empty() || plan == "unknown" { return None; } - // 套餐族沿用官方 codex_protocol::account::PlanType 的分类。 + // 套餐族沿用官方 codex_protocol::account::PlanType 的分类 let current = current.unwrap_or_default().trim().to_ascii_lowercase(); let generalized = matches!( (plan.as_str(), current.as_str()), @@ -1869,8 +1872,9 @@ fn eligible_periodic_quota_refresh(account: &ProviderAccount, now: SystemTime) - } fn eligible_initial_quota_sync(account: &ProviderAccount, now: SystemTime) -> bool { - // 首次观察只兜底刚入库、尚无 quota 快照的账号。已耗尽等运行时状态必须由 - // periodic 路径处理,才能保留同一账号的最小复核间隔。 + // 首次观察只兜底刚入库、尚无 quota 快照的账号 + // 已耗尽等运行时状态必须由 + // periodic 路径处理,才能保留同一账号的最小复核间隔 account.credential_state() == CredentialState::Ready && eligible_periodic_quota_refresh(account, now) } @@ -1890,8 +1894,8 @@ fn merge_passive_quota( let default_is_named_alias = default_limit_is_named_alias(rate_limits); let mut resolved_limit_ids = BTreeMap::new(); for (wire_limit_id, details) in &rate_limits.limits { - // HTTP 与 WebSocket 都可能把活动具名桶镜像为默认 `codex` 窗口。 - // 同一 wire 观察里存在相同具名事实时丢弃镜像,不触碰 core 桶。 + // HTTP 与 WebSocket 都可能把活动具名桶镜像为默认 `codex` 窗口 + // 同一 wire 观察里存在相同具名事实时丢弃镜像,不触碰 core 桶 if wire_limit_id == DEFAULT_CODEX_LIMIT_ID && default_is_named_alias { continue; } @@ -1950,7 +1954,7 @@ fn merge_passive_metadata(quota: &mut Map, rate_limits: &ParsedRa if let Some(balance) = credits.balance.as_ref() { value.insert("balance".to_owned(), Value::String(balance.clone())); } else { - // 点数对象明确更新但未提供余额时,不能把旧余额继续作为当前余额展示。 + // 点数对象明确更新但未提供余额时,不能把旧余额继续作为当前余额展示 value.remove("balance"); } quota.insert("credits".to_owned(), Value::Object(value)); @@ -1970,14 +1974,17 @@ fn default_limit_is_named_alias(rate_limits: &ParsedRateLimits) -> bool { }) } -/// 丢弃 core 限额中上游给出的无事实 `secondary_window` 占位。 +/// 丢弃 core 限额中上游给出的无事实 `secondary_window` 占位 /// /// 生产响应头可能携带 `used_percent=0`、零时长和空 reset 的占位,`/usage` 也会 -/// 返回 `secondary_window: null`。正常被动同步保留该响应事实,Admin 展示会将其 +/// 返回 `secondary_window: null` +/// 正常被动同步保留该响应事实,Admin 展示会将其 /// 隐藏;但主动 `/usage` 刷新或 402 确认投影会把一个存在但无事实的字段写成 -/// 100%,从而显示并不存在的“次级额度”。因此这些非被动写入口在落库前移除 -/// 无事实值。带 reset、时长、正用量、触顶、未知或非法字段的次级窗口均完全按 -/// 原有额度逻辑保留。 +/// 100%,从而显示并不存在的“次级额度” +/// 因此这些非被动写入口在落库前移除 +/// 无事实值 +/// 带 reset、时长、正用量、触顶、未知或非法字段的次级窗口均完全按 +/// 原有额度逻辑保留 fn normalize_quota_window_placeholders(mut quota: Map) -> Map { quota = canonicalize_rate_limit_document(quota); if let Some(rate_limit) = quota @@ -2014,7 +2021,7 @@ fn secondary_window_is_placeholder(window: &Map) -> bool { } fn passive_rate_limit_snapshot(details: &RateLimitDetails) -> Option> { - // 响应头的窗口属于同一次上游观测;跨响应合并会制造不存在的额度窗口。 + // 响应头的窗口属于同一次上游观测;跨响应合并会制造不存在的额度窗口 let mut snapshot = Map::new(); if let Some(allowed) = details.allowed { snapshot.insert("allowed".to_owned(), Value::Bool(allowed)); diff --git a/backend/crates/providers/openai/src/credential/quota/recovery.rs b/backend/crates/providers/openai/src/credential/quota/recovery.rs index 2ec068bd1..703ec1ac3 100644 --- a/backend/crates/providers/openai/src/credential/quota/recovery.rs +++ b/backend/crates/providers/openai/src/credential/quota/recovery.rs @@ -1,4 +1,4 @@ -//! 将耗尽窗口的恢复基准与最新用量分开持久化,允许各窗口分批恢复。 +//! 将耗尽窗口的恢复基准与最新用量分开持久化,允许各窗口分批恢复 use std::collections::BTreeMap; use std::time::SystemTime; @@ -15,11 +15,12 @@ const RESET_RECOVERY_MAX_USED_PERCENT: f64 = 10.0; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub(super) struct QuotaRecovery { - // 绑定本次耗尽事实,避免真实推理成功后再次耗尽时复用上次的恢复进度。 + // 绑定本次耗尽事实,避免真实推理成功后再次耗尽时复用上次的恢复进度 exhausted_at_micros: i64, pending: BTreeMap>>, - // 已连续观测到"未触顶"的待解除窗口;第二次确认后解除。旧文档没有该 - // 字段时反序列化为空集,从当前耗尽事实重新积累证据。 + // 已连续观测到"未触顶"的待解除窗口;第二次确认后解除 + // 旧文档没有该 + // 字段时反序列化为空集,从当前耗尽事实重新积累证据 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] candidates: BTreeMap, } @@ -32,12 +33,12 @@ impl QuotaRecovery { pending.insert(window.key().to_owned(), window.reset_at()); } } - // 旧版从 primary 派生的账号 reset 不能覆盖明确的周窗口耗尽事实。 - // 真实 usage_limit_reached 错误则可以补充尚未反映到百分比的耗尽窗口。 + // 旧版从 primary 派生的账号 reset 不能覆盖明确的周窗口耗尽事实 + // 真实 usage_limit_reached 错误则可以补充尚未反映到百分比的耗尽窗口 if pending.is_empty() || state.evidence() == Some(QuotaEvidence::UsageLimitReached) { for window in baseline.windows().iter().filter(|w| w.is_account_wide()) { // 错误与 usage 的秒级 reset 可能相差一秒;只用它识别窗口, - // 后续推进比较始终使用该窗口自己的 reset。 + // 后续推进比较始终使用该窗口自己的 reset let matches_error_reset = window.reset_at().zip(state.reset_at()).is_some_and( |(window_reset, error_reset)| { (window_reset.timestamp() - DateTime::::from(error_reset).timestamp()) @@ -50,7 +51,7 @@ impl QuotaRecovery { } } } - // 上游未指出耗尽窗口时保留所有候选,不能任意把错误归给 primary。 + // 上游未指出耗尽窗口时保留所有候选,不能任意把错误归给 primary if pending.is_empty() { pending.extend( baseline @@ -78,14 +79,15 @@ impl QuotaRecovery { continue; } if previous_reset.is_none() { - // 首次观测到该窗口只锚定 reset 基线,不构成解除证据。 + // 首次观测到该窗口只锚定 reset 基线,不构成解除证据 self.pending.insert(key.to_owned(), window.reset_at()); continue; } // 同一窗口内用量回落(reset 不滚动)时"reset 前进"永远不会 // 成立,滑动窗口的正常用量也长期高于低用量阈值;以上游连续 - // 两次观测都报告未触顶为解除证据。单次观测无法排除耗尽后 - // 立刻拉到的旧快照(reset 未滚动、用量尚未触顶)。 + // 两次观测都报告未触顶为解除证据 + // 单次观测无法排除耗尽后 + // 立刻拉到的旧快照(reset 未滚动、用量尚未触顶) let rolled_forward = window .reset_at() .is_some_and(|current| Some(current) > previous_reset); @@ -110,7 +112,7 @@ fn exhaustion_time(state: QuotaState) -> i64 { } fn window_reset_recovered(window: &CodexQuotaWindow, previous: Option>) -> bool { - // reset 必须前进:防止耗尽后立刻拉到的旧快照(reset 未滚动)误解锁。 + // reset 必须前进:防止耗尽后立刻拉到的旧快照(reset 未滚动)误解锁 if !previous .zip(window.reset_at()) .is_some_and(|(previous, current)| current > previous) @@ -118,16 +120,18 @@ fn window_reset_recovered(window: &CodexQuotaWindow, previous: Option, kind: CodexQuotaWindowKind, role: CodexQuotaWindowRole, @@ -66,7 +66,7 @@ impl CodexQuotaWindow { &self.source } - /// 该窗口是否覆盖账号全部 Codex 请求,可使用账号级本地用量聚合。 + /// 该窗口是否覆盖账号全部 Codex 请求,可使用账号级本地用量聚合 #[must_use] pub const fn is_account_wide(&self) -> bool { self.account_wide @@ -102,7 +102,7 @@ impl CodexQuotaWindow { self.reset_at } - /// 该窗口是否已触顶(`limit_reached` 或 `used_percent >= 100`)。 + /// 该窗口是否已触顶(`limit_reached` 或 `used_percent >= 100`) #[must_use] pub fn limit_reached(&self) -> bool { self.limit_reached || self.used_percent.is_some_and(|used| used >= 100.0) @@ -138,13 +138,13 @@ impl CodexAccountQuotaSnapshot { self.observed_at } - /// 上游额度响应明确返回的套餐;缺失时不推断账号级别。 + /// 上游额度响应明确返回的套餐;缺失时不推断账号级别 #[must_use] pub fn plan_type(&self) -> Option<&str> { self.plan_type.as_deref() } - /// 点数只用于展示,不改变套餐限额或账号可用性。 + /// 点数只用于展示,不改变套餐限额或账号可用性 #[must_use] pub fn credits(&self) -> Option<&CreditsSnapshot> { self.credits.as_ref() @@ -165,14 +165,14 @@ impl CodexAccountQuotaSnapshot { self.quota } - /// 将已持久化的规范化事实覆盖到 raw JSON 展示快照上。 + /// 将已持久化的规范化事实覆盖到 raw JSON 展示快照上 #[must_use] pub(crate) const fn with_quota_state(mut self, quota: QuotaState) -> Self { self.quota = quota; self } - /// 只更新展示与调度使用的最后成功查询时间,保留既有额度事实。 + /// 只更新展示与调度使用的最后成功查询时间,保留既有额度事实 #[must_use] pub(crate) const fn with_observed_at(mut self, observed_at: SystemTime) -> Self { self.observed_at = observed_at; @@ -209,7 +209,7 @@ fn parse_codex_quota_object( for limit in canonical_rate_limits(object)? { aggregate.observe_rate_limit(limit.rate_limit)?; } - // credits / spend_control 只证明文档结构已识别,不参与账号级访问结论。 + // credits / spend_control 只证明文档结构已识别,不参与账号级访问结论 if let Some(spend_control) = object.get("spend_control") { aggregate.observe_metadata_object(spend_control, "reached")?; } @@ -246,13 +246,13 @@ pub(crate) fn quota_snapshot_from_observation( &Value::Object(quota.expose_to_provider().clone()), ) .ok()?; - // 归一化列是持久化权威事实;raw JSON 只负责 Provider 展示结构。 + // 归一化列是持久化权威事实;raw JSON 只负责 Provider 展示结构 snapshot.quota = observation.state; Some(snapshot) } -/// 缓存定期重读持久化事实,额度窗口独立按 reset 到期。 -/// 正常账号依赖被动同步,空闲超过缓存 TTL 不能把尚未重置的额度抹成未知。 +/// 缓存定期重读持久化事实,额度窗口独立按 reset 到期 +/// 正常账号依赖被动同步,空闲超过缓存 TTL 不能把尚未重置的额度抹成未知 pub(crate) fn quota_projection_ttl(snapshot: &CodexAccountQuotaSnapshot) -> Option { let now = SystemTime::now(); snapshot @@ -260,7 +260,7 @@ pub(crate) fn quota_projection_ttl(snapshot: &CodexAccountQuotaSnapshot) -> Opti .iter() .filter_map(|window| quota_window_ttl(window, snapshot.observed_at(), now)) .min() - // 新观测即使没有有效窗口,也要用“未知”替换此前缓存的排序信号。 + // 新观测即使没有有效窗口,也要用“未知”替换此前缓存的排序信号 .or_else(|| { QUOTA_SCHEDULING_TTL.checked_sub( now.duration_since(snapshot.observed_at()) @@ -278,14 +278,14 @@ fn quota_window_ttl( ) -> Option { let ttl = match window.reset_at() { Some(reset_at) => SystemTime::from(reset_at).duration_since(now).ok()?, - // 没有明确窗口边界时,不能无限延长旧观测的有效期。 + // 没有明确窗口边界时,不能无限延长旧观测的有效期 None => QUOTA_SCHEDULING_TTL .checked_sub(now.duration_since(observed_at).unwrap_or(Duration::ZERO))?, }; (!ttl.is_zero()).then_some(ttl) } -/// 从快照窗口派生容量排序事实;额度访问资格由 `QuotaState` 统一判断。 +/// 从快照窗口派生容量排序事实;额度访问资格由 `QuotaState` 统一判断 pub(crate) fn scheduling_signals_from_snapshot( snapshot: &CodexAccountQuotaSnapshot, ) -> Option { @@ -374,7 +374,7 @@ fn quota_source_order(source: &str) -> u8 { } } -/// 默认 `codex` 桶是唯一可改变账号额度访问结论的 quota 响应字段。 +/// 默认 `codex` 桶是唯一可改变账号额度访问结论的 quota 响应字段 fn authoritative_quota_state( usage: &Map, windows: &[CodexQuotaWindow], @@ -439,7 +439,7 @@ fn authoritative_quota_state( Ok(QuotaState::observed_unknown(observed_at)) } -/// 一个规范化后的限流桶,source 是上游的稳定 limit_id。 +/// 一个规范化后的限流桶,source 是上游的稳定 limit_id struct CanonicalRateLimit<'a> { source: String, account_wide: bool, @@ -534,8 +534,9 @@ fn parse_rate_limit_windows( .transpose()?; let limit_reached = optional_bool(window, "limit_reached")?.unwrap_or(false); let kind = quota_window_kind(window_seconds); - // primary/secondary 是协议槽位,不代表固定周期。窗口身份使用上游 - // limit_id 和实际时长,5h/周/月分类只参与展示。 + // primary/secondary 是协议槽位,不代表固定周期 + // 窗口身份使用上游 + // limit_id 和实际时长,5h/周/月分类只参与展示 let key = match window_seconds { Some(seconds) => format!("{source}:{seconds}s"), None => format!("{source}:{name}"), @@ -553,7 +554,7 @@ fn parse_rate_limit_windows( limit_reached, }); } - // 同桶同周期无法仅凭时长区分,保留官方槽位,避免合并两个独立限制。 + // 同桶同周期无法仅凭时长区分,保留官方槽位,避免合并两个独立限制 if let [primary, secondary] = &mut output[start..] && primary.key == secondary.key { diff --git a/backend/crates/providers/openai/src/credential/recovery_log.rs b/backend/crates/providers/openai/src/credential/recovery_log.rs index 301db9386..52518e129 100644 --- a/backend/crates/providers/openai/src/credential/recovery_log.rs +++ b/backend/crates/providers/openai/src/credential/recovery_log.rs @@ -1,23 +1,24 @@ -//! 成功取得 OAuth token 后写入独立恢复日志的记录。 +//! 成功取得 OAuth token 后写入独立恢复日志的记录 //! //! 此日志会刻意保存原始 AT/RT,避免后续数据库写入或校验失败时,已成功 -//! 交换的账号无法恢复。Host 在 oauth_recovery 配置开启时独立落盘,沿用文件日志的滚动和清理策略。 +//! 交换的账号无法恢复 +//! Host 在 oauth_recovery 配置开启时独立落盘,沿用文件日志的滚动和清理策略 const OAUTH_RECOVERY_LOG_TARGET: &str = "oauth_recovery"; const OAUTH_RECOVERY_PROVIDER: &str = "openai"; -/// 产生可恢复 token 组的 OAuth 操作。 +/// 产生可恢复 token 组的 OAuth 操作 #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum CodexOAuthRecoveryOperation { - /// 直接提供 AT/RT 的导入。 + /// 直接提供 AT/RT 的导入 ImportDirect, - /// 仅提供 RT 的导入,先交换得到 AT。 + /// 仅提供 RT 的导入,先交换得到 AT ImportRefreshToken, - /// 管理员手动触发的 RT 交换。 + /// 管理员手动触发的 RT 交换 ManualRefresh, - /// 调度器触发的 RT 交换。 + /// 调度器触发的 RT 交换 ScheduledRefresh, - /// 授权码加 PKCE 的 token 交换。 + /// 授权码加 PKCE 的 token 交换 AuthorizationCode, } @@ -33,11 +34,12 @@ impl CodexOAuthRecoveryOperation { } } -/// 将已取得的 AT/RT 写入独立恢复结构化日志。 +/// 将已取得的 AT/RT 写入独立恢复结构化日志 /// -/// 调用方必须在任何可能拒绝响应的本地校验、画像补全或数据库写入之前调用。 -/// 此处只发出结构化事件,不返回业务错误。Host 的 oauth_recovery 配置控制落盘, -/// 并负责文件分割、清理和写入队列背压。 +/// 调用方必须在任何可能拒绝响应的本地校验、画像补全或数据库写入之前调用 +/// 此处只发出结构化事件,不返回业务错误 +/// Host 的 oauth_recovery 配置控制落盘, +/// 并负责文件分割、清理和写入队列背压 pub(crate) fn record_oauth_recovery( operation: CodexOAuthRecoveryOperation, account_id: Option<&str>, diff --git a/backend/crates/providers/openai/src/credential/refresh.rs b/backend/crates/providers/openai/src/credential/refresh.rs index 2168b3d2a..663e88196 100644 --- a/backend/crates/providers/openai/src/credential/refresh.rs +++ b/backend/crates/providers/openai/src/credential/refresh.rs @@ -1,4 +1,4 @@ -//! Codex AT/RT 刷新状态机;Redis lease + ProviderAccountStore CAS,无 SQL。 +//! Codex AT/RT 刷新状态机;Redis lease + ProviderAccountStore CAS,无 SQL use std::collections::BTreeSet; use std::num::NonZeroU32; @@ -27,13 +27,13 @@ use super::types::{CodexOAuthSecret, parse_access_token_expiration}; const PROVIDER_NAME: &str = "openai"; const MAX_REFRESH_BATCH: u32 = 1_000; -/// 连续失败计数窗口;每次瞬态失败刷新该 TTL,静默满窗后计数过期归零。 +/// 连续失败计数窗口;每次瞬态失败刷新该 TTL,静默满窗后计数过期归零 const REFRESH_BACKOFF_WINDOW: Duration = Duration::from_secs(30 * 60); -/// 窗口内先执行五次指数退避,再进入固定恢复周期。 +/// 窗口内先执行五次指数退避,再进入固定恢复周期 const REFRESH_BACKOFF_MAX_ATTEMPTS: u32 = 5; -/// 耗尽指数退避后的 OAuth 恢复周期。 +/// 耗尽指数退避后的 OAuth 恢复周期 const REFRESH_RECOVERY_DELAY: Duration = Duration::from_secs(10 * 60); -/// 过期 AT 仍允许 RT 恢复的最长窗口。 +/// 过期 AT 仍允许 RT 恢复的最长窗口 const REFRESH_RECOVERY_WINDOW: Duration = Duration::from_secs(2 * 60 * 60); pub(crate) fn refresh_recovery_deadline( @@ -366,7 +366,7 @@ impl CodexCredentialRefreshService { } Err(RefreshFailure::Transport { message, upstream }) => { // 上游瞬态(401/429/5xx/超时/畸形响应等)保留现有凭据、 - // 记录最近一次失败并推进有界退避。 + // 记录最近一次失败并推进有界退避 if self .defer_refresh( &due.account, @@ -475,7 +475,7 @@ impl CodexCredentialRefreshService { .map(ExposeSecret::expose_secret), ); // 成功轮换清掉仅用于失败退避的 retry-not-before;正常预刷新窗口由 - // worker 结合当前 runtime policy 动态判断,不写入账号时间字段。 + // worker 结合当前 runtime policy 动态判断,不写入账号时间字段 let next_refresh_at = None; let result = self .repository @@ -488,7 +488,7 @@ impl CodexCredentialRefreshService { .await; match result { Ok(revision) => { - // 凭据完整成功轮换后清零连续失败计数,退避窗口重新从 base 起步。 + // 凭据完整成功轮换后清零连续失败计数,退避窗口重新从 base 起步 let _ = self .credential_state .clear_refresh_backoff(due.account.id()) diff --git a/backend/crates/providers/openai/src/credential/repository.rs b/backend/crates/providers/openai/src/credential/repository.rs index 6aec49ea3..c608f561b 100644 --- a/backend/crates/providers/openai/src/credential/repository.rs +++ b/backend/crates/providers/openai/src/credential/repository.rs @@ -1,4 +1,4 @@ -//! `ProviderAccountStore` 的 Codex 行转换;本文件不含 SQL。 +//! `ProviderAccountStore` 的 Codex 行转换;本文件不含 SQL use std::sync::Arc; use std::time::SystemTime; @@ -33,10 +33,10 @@ impl CodexCredentialRepository { &self.store } - /// 持久化成功 RT exchange 的 token,同时保留既有账号身份投影。 + /// 持久化成功 RT exchange 的 token,同时保留既有账号身份投影 /// /// Refresh endpoint 已经是这次 AT/RT 轮换的授权边界;这里仅以 revision - /// CAS 保护并发写入,不重新验证新 access token 的身份声明。 + /// CAS 保护并发写入,不重新验证新 access token 的身份声明 pub async fn rotate_refreshed_oauth_secret( &self, account: &ProviderAccount, @@ -49,7 +49,7 @@ impl CodexCredentialRepository { .load_credential(account.id(), account.revision()) .await?; // load_credential 已校验凭据版本;套餐与额度可在 RT exchange 期间更新, - // 这些运行时事实变化不能使已成功轮换的 token 丢失。 + // 这些运行时事实变化不能使已成功轮换的 token 丢失 let mut data = CodexCredentialCodec::decode_complete(¤t.credential)?; let oauth = data .oauth_mut() @@ -84,7 +84,7 @@ impl CodexCredentialRepository { cas_revision(self.store.compare_and_swap_credential(update).await?) } - /// 以相同 credential 原子推进刷新退避及本次上游错误事实。 + /// 以相同 credential 原子推进刷新退避及本次上游错误事实 pub async fn defer_refresh( &self, account: &ProviderAccount, @@ -226,7 +226,7 @@ impl CodexCredentialRepository { .await } - /// 写入凭据事实及其稳定错误原因;额度事实不经过此入口。 + /// 写入凭据事实及其稳定错误原因;额度事实不经过此入口 pub async fn apply_state_with_reason( &self, account: &ProviderAccount, @@ -302,7 +302,7 @@ impl From for CredentialRepositoryError { } } -/// OAuth 未取得用户身份时保留未验证状态;通用账号层不解释认证类型。 +/// OAuth 未取得用户身份时保留未验证状态;通用账号层不解释认证类型 fn oauth_account_state(account: &ProviderAccount, observed: CredentialState) -> CredentialState { if account.authentication_kind() == super::CODEX_AUTHENTICATION_KIND_OAUTH && account.upstream_user_id().is_none() diff --git a/backend/crates/providers/openai/src/credential/security.rs b/backend/crates/providers/openai/src/credential/security.rs index 156444dda..bd440045e 100644 --- a/backend/crates/providers/openai/src/credential/security.rs +++ b/backend/crates/providers/openai/src/credential/security.rs @@ -1,4 +1,4 @@ -//! Codex 明文 credential JSON 的 schema 校验与日志脱敏边界。 +//! Codex 明文 credential JSON 的 schema 校验与日志脱敏边界 use super::api_key::ApiKeyAuthentication; use gateway_core::account::PlaintextCredential; @@ -15,7 +15,7 @@ const CODEX_CREDENTIAL_SCHEMA_VERSION: u32 = 1; const MAX_CREDENTIAL_BYTES: usize = 256 * 1024; const MAX_COOKIES: usize = 128; -/// 已解析且只在 Provider 内可见的认证材料。 +/// 已解析且只在 Provider 内可见的认证材料 pub struct CodexRuntimeCredential { pub transport: super::ResponsesTransport, pub authentication: CodexRuntimeAuthentication, @@ -85,7 +85,7 @@ pub enum CodexCredentialDataError { TooLarge, } -/// 不加密、不解密;只验证 Codex-owned JSON 并做运行时 secret 包装。 +/// 不加密、不解密;只验证 Codex-owned JSON 并做运行时 secret 包装 #[derive(Debug, Default, Clone, Copy)] pub struct CodexCredentialCodec; @@ -106,7 +106,7 @@ impl CodexCredentialCodec { ) } - /// 为尚未解析资料的 OAuth 账号编码凭据。 + /// 为尚未解析资料的 OAuth 账号编码凭据 pub(crate) fn encode_unresolved( secret: &CodexOAuthSecret, installation_id: String, diff --git a/backend/crates/providers/openai/src/credential/selector.rs b/backend/crates/providers/openai/src/credential/selector.rs index 82793dcd4..0ea4eefe3 100644 --- a/backend/crates/providers/openai/src/credential/selector.rs +++ b/backend/crates/providers/openai/src/credential/selector.rs @@ -1,4 +1,4 @@ -//! AttemptContext 驱动的 Codex 账号选择与 Redis lease port。 +//! AttemptContext 驱动的 Codex 账号选择与 Redis lease port use std::collections::HashMap; use std::fmt; @@ -47,30 +47,30 @@ const MAX_ACCOUNT_SNAPSHOT_RETRIES: u32 = 3; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CodexAccountFailure { - /// Access token 已被上游明确判定为过期或失效。 + /// Access token 已被上游明确判定为过期或失效 CredentialExpired, - /// 账号需要完成身份验证后才能继续使用。 + /// 账号需要完成身份验证后才能继续使用 IdentityVerificationRequired, - /// 账号、workspace 或 organization 已被封禁或停用。 + /// 账号、workspace 或 organization 已被封禁或停用 Banned, - /// 账号信用额度已耗尽。 + /// 账号信用额度已耗尽 QuotaExhausted, - /// 当前用量窗口已耗尽;到重置时间后可自动恢复。 + /// 当前用量窗口已耗尽;到重置时间后可自动恢复 UsageLimitExhausted { - /// 上游返回的窗口绝对重置时刻。 + /// 上游返回的窗口绝对重置时刻 reset_at: Option, }, - /// 账号触发临时用量限制。 + /// 账号触发临时用量限制 RateLimited { - /// 上游明确返回的最短冷却时长。 + /// 上游明确返回的最短冷却时长 retry_after: Option, }, - /// Cloudflare challenge 要求账号进入递增冷却。 + /// Cloudflare challenge 要求账号进入递增冷却 CloudflareChallenge { - /// 上游明确返回的最短冷却时长。 + /// 上游明确返回的最短冷却时长 retry_after: Option, }, - /// Cloudflare 对当前上游路径返回空 404。 + /// Cloudflare 对当前上游路径返回空 404 CloudflarePathBlocked, } @@ -106,7 +106,7 @@ struct CredentialSelectionInput<'a> { attempt: &'a AttemptContext, session_affinity_key: Option<&'a ProviderSessionAffinityKey>, session_affinity_observation: Option<&'a CodexSessionAffinity>, - /// Codex Guardian 自动审批请求;仅在配置了预留名额时获得预留与队列优先。 + /// Codex Guardian 自动审批请求;仅在配置了预留名额时获得预留与队列优先 guardian: bool, } @@ -265,7 +265,7 @@ struct AffinityTelemetry { impl CodexCredentialSelector { #[must_use] - // 选择器显式持有各能力边界,避免把 Provider 私有服务重新包装成通用容器。 + // 选择器显式持有各能力边界,避免把 Provider 私有服务重新包装成通用容器 #[expect(clippy::too_many_arguments)] pub fn new( provider_kind: ProviderKind, @@ -331,10 +331,10 @@ impl CodexCredentialSelector { .await } - /// 中间件完成请求改写后,用真实 OpenAI 会话事实复验已持有的租约。 + /// 中间件完成请求改写后,用真实 OpenAI 会话事实复验已持有的租约 /// /// 此处只复用既有亲和与 cyber-policy 端口,不再次选号;冲突必须在发送前失败, - /// 避免同一 attempt 持有旧租约时重入账号选择。 + /// 避免同一 attempt 持有旧租约时重入账号选择 pub(crate) async fn validate_translated_selection( &self, lease: &mut CodexCredentialLease, @@ -367,10 +367,10 @@ impl CodexCredentialSelector { Ok(()) } - /// 为不属于 Responses 文本模型目录的 Provider 原生端点选择账号。 + /// 为不属于 Responses 文本模型目录的 Provider 原生端点选择账号 /// /// 账号范围、健康度、配额、并发租约、cookie 与认证准备仍走同一套选择链路; - /// 原生端点没有 Responses 模型,不套用管理员配置的文本模型权限。 + /// 原生端点没有 Responses 模型,不套用管理员配置的文本模型权限 pub(crate) async fn select_for_provider_endpoint( &self, request: &SelectCodexProviderEndpointCredential<'_>, @@ -393,7 +393,7 @@ impl CodexCredentialSelector { upstream_model: Option<&str>, ) -> Result { let queue_policy = request.attempt.account_selection_policy().queue_policy(); - // 预留名额只对普通请求生效;Guardian 可用满全部名额,并在账号队列中排在普通请求之前。 + // 预留名额只对普通请求生效;Guardian 可用满全部名额,并在账号队列中排在普通请求之前 let reserve = request .attempt .account_selection_policy() @@ -438,7 +438,7 @@ impl CodexCredentialSelector { let diagnostic = request.attempt.is_diagnostic_required_account(); let mut accounts = self.repository.list_for_provider().await?; // store 侧常规调度列表不包含停用账号;管理端诊断要对固定账号执行真实上游 - // 验证,先补回 required 账号,再统一检查认证类型和传输能力。 + // 验证,先补回 required 账号,再统一检查认证类型和传输能力 if diagnostic && let Some(required) = request.attempt.required_account() && !accounts.iter().any(|account| account.id() == required) @@ -489,7 +489,7 @@ impl CodexCredentialSelector { )?; continue 'capacity; } - // 非固定账号的损坏凭据不能阻断其余账号的传输资格检查。 + // 非固定账号的损坏凭据不能阻断其余账号的传输资格检查 Err(CredentialRepositoryError::InvalidCredentialData) if pinned_account.is_none() => { @@ -595,7 +595,7 @@ impl CodexCredentialSelector { { affinity.escape(AffinityEscapeReason::PinnedAccount); } - // 根绑定只提供默认偏好,不能作为子线程 CAS 的旧值,也不能跳过子线程首绑。 + // 根绑定只提供默认偏好,不能作为子线程 CAS 的旧值,也不能跳过子线程首绑 let mut observed_affinity_account = if affinity.inherited { None } else { @@ -637,7 +637,7 @@ impl CodexCredentialSelector { && wait_candidates.contains(account_id) }); if let Some(preferred) = preferred_wait { - // 原绑定仍合格时只等待它;并发、请求间隔与已有队列均不应造成临时换号。 + // 原绑定仍合格时只等待它;并发、请求间隔与已有队列均不应造成临时换号 wait_candidates.retain(|account_id| account_id == preferred); } let capacity = AccountSelector.capacity_snapshot(&candidates, &context); @@ -662,7 +662,7 @@ impl CodexCredentialSelector { return Err(CredentialSelectionError::PolicyUnavailable); } }; - // 可等待集合使用原始排除集;这里的 Excluded 只可能来自本轮租约争用或队列让位。 + // 可等待集合使用原始排除集;这里的 Excluded 只可能来自本轮租约争用或队列让位 let selection = selection.filter(|selection| { preferred_wait.is_none() || selection.is_policy_choice() @@ -702,8 +702,8 @@ impl CodexCredentialSelector { })?; continue 'capacity; } - // 只判断本次账号范围;空池、认证失效和租约失败不能伪装成额度耗尽。 - // 使用最初的排除集合,避免本轮临时跳过的繁忙账号丢失其可恢复语义。 + // 只判断本次账号范围;空池、认证失效和租约失败不能伪装成额度耗尽 + // 使用最初的排除集合,避免本轮临时跳过的繁忙账号丢失其可恢复语义 let mut statuses = candidates .iter() .filter(|candidate| !base_excluded.contains(candidate.account.id())) @@ -733,8 +733,8 @@ impl CodexCredentialSelector { .find(|candidate| candidate.account.id() == selected.account.id()) .map(|candidate| candidate.account.clone()) .ok_or(CredentialSelectionError::InvalidCredential)?; - // 额度观测等并发更新会使整个账号快照失效,必须重新选号并校验资格。 - // 在占用租约和请求间隔前完成校验,避免重读被自己的异步释放挡住。 + // 额度观测等并发更新会使整个账号快照失效,必须重新选号并校验资格 + // 在占用租约和请求间隔前完成校验,避免重读被自己的异步释放挡住 let runtime = match self.repository.load_runtime_credential(&account).await { Ok(runtime) => runtime, Err(CredentialRepositoryError::RevisionConflict) => { @@ -743,7 +743,7 @@ impl CodexCredentialSelector { } Err(error) => return Err(error.into()), }; - // 凭据重读和插件选号都可能挂起,取得租约前再次让位于新队首。 + // 凭据重读和插件选号都可能挂起,取得租约前再次让位于新队首 if !waiting.can_try(account.id()) { excluded.insert(account.id().clone()); continue; @@ -781,7 +781,7 @@ impl CodexCredentialSelector { None }; // 原生 continuation/required account 比亲和绑定更严格;它可以 - // 使用 owner 账号,但不能把已经迁移的会话拉回旧号。 + // 使用 owner 账号,但不能把已经迁移的会话拉回旧号 if pinned_account.is_none() && let Some(effective_account) = initial_affinity_claim && &effective_account != account.id() @@ -852,8 +852,8 @@ impl CodexCredentialSelector { && observed_affinity_account.as_ref() == Some(account.id()) && let Some(key) = request.session_affinity_key { - // 命中即续期,避免长请求或客户端取消导致活跃会话提前过期。 - // CAS 防止并行请求把已经迁移的绑定改回旧账号。 + // 命中即续期,避免长请求或客户端取消导致活跃会话提前过期 + // CAS 防止并行请求把已经迁移的绑定改回旧账号 self.update_session_affinity(key, account.id(), account.id()) .await; } @@ -1146,7 +1146,7 @@ impl CodexCredentialSelector { ) .await .map_err(|_| CredentialSelectionError::Store), - // 429:临时限流只写运行时冷却,不改变凭据或额度事实。 + // 429:临时限流只写运行时冷却,不改变凭据或额度事实 CodexAccountFailure::RateLimited { retry_after } => { self.quota .apply_rate_limit_429(account, retry_after, now) @@ -1154,7 +1154,7 @@ impl CodexCredentialSelector { .map_err(|_| CredentialSelectionError::Store)?; Ok(()) } - // Cloudflare 挑战:内存退避表(记录风险计数),不写账号事实。 + // Cloudflare 挑战:内存退避表(记录风险计数),不写账号事实 CodexAccountFailure::CloudflareChallenge { retry_after } => { let delay = self.cloudflare_challenge_delay(account.id(), now, retry_after); let recovery = now @@ -1163,7 +1163,7 @@ impl CodexCredentialSelector { self.apply_cookie_recovery(account, recovery).await?; Ok(()) } - // Cloudflare 路径被封:连续超阈值才置 Invalid;否则内存退避。 + // Cloudflare 路径被封:连续超阈值才置 Invalid;否则内存退避 CodexAccountFailure::CloudflarePathBlocked => { let blocked = self.record_cloudflare_path_block(account.id(), now); if blocked >= CLOUDFLARE_PATH_BLOCK_THRESHOLD { @@ -1558,7 +1558,7 @@ impl CodexCredentialLease { self.cyber_policy_scope.as_ref() } - /// 禁用账号的管理端诊断必须只返回真实上游结果,不能回写账号侧状态。 + /// 禁用账号的管理端诊断必须只返回真实上游结果,不能回写账号侧状态 #[must_use] pub(crate) const fn allows_account_state_mutation(&self) -> bool { self.allows_account_state_mutation @@ -1587,7 +1587,7 @@ impl CodexCredentialLease { self.capacity } - /// 成功反馈只能从选择时观察到的绑定迁移,避免迟到请求覆盖较新 winner。 + /// 成功反馈只能从选择时观察到的绑定迁移,避免迟到请求覆盖较新 winner #[must_use] pub(crate) const fn affinity_expected_account_id(&self) -> &ProviderAccountId { &self.affinity_expected_account_id diff --git a/backend/crates/providers/openai/src/credential/token_client.rs b/backend/crates/providers/openai/src/credential/token_client.rs index d475fd6ec..ad322ef88 100644 --- a/backend/crates/providers/openai/src/credential/token_client.rs +++ b/backend/crates/providers/openai/src/credential/token_client.rs @@ -1,4 +1,4 @@ -//! OpenAI OAuth token exchange 与 Codex PAT 验证的 Reqwest 适配器。 +//! OpenAI OAuth token exchange 与 Codex PAT 验证的 Reqwest 适配器 use super::types::CodexOAuthMetadata; @@ -18,15 +18,15 @@ const MAX_OAUTH_RESPONSE_BYTES: usize = 64 * 1024; const TOKEN_REQUEST_TIMEOUT: Duration = Duration::from_secs(30); const TOKEN_CONNECT_TIMEOUT: Duration = Duration::from_secs(10); -/// Codex Desktop 使用的官方 OAuth public client。 +/// Codex Desktop 使用的官方 OAuth public client pub const OFFICIAL_CODEX_OAUTH_CLIENT_ID: &str = "app_EMoamEEZ73f0CkXaXp7hrann"; -/// Codex Desktop 使用的官方 token endpoint。 +/// Codex Desktop 使用的官方 token endpoint pub const OFFICIAL_CODEX_TOKEN_ENDPOINT: &str = "https://auth.openai.com/oauth/token"; -/// Codex Desktop loopback callback;管理员复制完整回调 URL 交回固定 complete API。 +/// Codex Desktop loopback callback;管理员复制完整回调 URL 交回固定 complete API pub const OFFICIAL_CODEX_REDIRECT_URI: &str = "http://localhost:1455/auth/callback"; const PERSONAL_ACCESS_TOKEN_WHOAMI_PATH: &str = "/api/accounts/v1/user-auth-credential/whoami"; -/// PAT 验证失败;不保留令牌、响应体或可能包含秘密的底层 HTTP 错误。 +/// PAT 验证失败;不保留令牌、响应体或可能包含秘密的底层 HTTP 错误 #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] pub enum PersonalAccessTokenError { #[error("Codex PAT must be a non-empty at- token without whitespace or control characters")] @@ -41,7 +41,7 @@ pub enum PersonalAccessTokenError { InvalidResponse, } -// 与 Codex personal_access_token.rs 一致:email 可缺失,其余身份字段必填。 +// 与 Codex personal_access_token.rs 一致:email 可缺失,其余身份字段必填 #[derive(Deserialize)] struct PersonalAccessTokenResponse { email: Option, @@ -52,14 +52,14 @@ struct PersonalAccessTokenResponse { _chatgpt_account_is_fedramp: bool, } -/// Token 刷新成功后得到的认证材料。 +/// Token 刷新成功后得到的认证材料 #[derive(Clone)] pub struct TokenPair { - /// 官方刷新响应省略时由持久化调用方保留当前 access token。 + /// 官方刷新响应省略时由持久化调用方保留当前 access token pub access_token: Option, - /// 官方刷新响应省略时由持久化调用方保留当前 refresh token。 + /// 官方刷新响应省略时由持久化调用方保留当前 refresh token pub refresh_token: Option, - /// 官方刷新响应省略时由持久化调用方保留当前 ID token。 + /// 官方刷新响应省略时由持久化调用方保留当前 ID token pub id_token: Option, } @@ -80,7 +80,7 @@ impl fmt::Debug for TokenPair { } } -/// Codex token 刷新的稳定失败分类。 +/// Codex token 刷新的稳定失败分类 #[derive(Clone, PartialEq, Eq, thiserror::Error)] pub enum RefreshFailure { #[error("refresh token is invalid or expired")] @@ -145,9 +145,10 @@ impl RefreshFailure { } } -/// 当前 OAuth 刷新请求收到的完整非成功响应。 +/// 当前 OAuth 刷新请求收到的完整非成功响应 /// -/// 该值不持久化;`Debug` 不输出正文。调用方仅在受控刷新失败日志中显式记录正文。 +/// 该值不持久化;`Debug` 不输出正文 +/// 调用方仅在受控刷新失败日志中显式记录正文 #[derive(Clone, PartialEq, Eq)] pub struct RefreshUpstreamFailure { status: u16, @@ -203,7 +204,7 @@ impl fmt::Debug for RefreshUpstreamFailure { } } -/// Codex token 刷新端口。 +/// Codex token 刷新端口 #[async_trait] pub trait TokenRefresher: Send + Sync + 'static { async fn refresh(&self, refresh_token: &str) -> Result; @@ -225,7 +226,7 @@ fn proxy_refresh_failure() -> RefreshFailure { } } -/// Authorization Code + PKCE 的一次性 grant。 +/// Authorization Code + PKCE 的一次性 grant pub struct AuthorizationCodeGrant { pub code: SecretString, pub code_verifier: SecretString, @@ -241,10 +242,11 @@ impl fmt::Debug for AuthorizationCodeGrant { } } -/// 官方 token endpoint 返回的 token set。 +/// 官方 token endpoint 返回的 token set /// /// 与官方首次 authorization-code exchange 一致:`id_token`、`access_token` 与 -/// `refresh_token` 都是响应的必填字段。这里不检查 token 内容、签名或 claims。 +/// `refresh_token` 都是响应的必填字段 +/// 这里不检查 token 内容、签名或 claims pub struct AuthorizationTokenSet { pub secret: crate::credential::CodexOAuthSecret, pub id_token: SecretString, @@ -260,7 +262,7 @@ impl fmt::Debug for AuthorizationTokenSet { } } -/// Authorization Code exchange 的低基数失败。 +/// Authorization Code exchange 的低基数失败 #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] pub enum AuthorizationCodeExchangeError { #[error("authorization code was rejected")] @@ -289,16 +291,16 @@ pub trait AuthorizationCodeExchanger: Send + Sync + 'static { } } -/// OpenAI token 续期客户端配置。 +/// OpenAI token 续期客户端配置 #[derive(Debug, Clone, PartialEq, Eq)] pub struct TokenClientConfig { - /// OpenAI 客户端 ID。 + /// OpenAI 客户端 ID pub client_id: String, - /// Token 交换入口。 + /// Token 交换入口 pub token_endpoint: String, } -/// OpenAI token 续期客户端。 +/// OpenAI token 续期客户端 #[derive(Clone)] pub struct OpenAiTokenClient { client: Client, @@ -306,7 +308,7 @@ pub struct OpenAiTokenClient { profile: CodexWireProfileState, } -/// 官方 Codex token client 无法安全构建。 +/// 官方 Codex token client 无法安全构建 #[derive(Debug, thiserror::Error)] #[error("official Codex token client could not be built")] pub struct TokenClientBuildError; @@ -329,7 +331,7 @@ impl OpenAiTokenClient { build_reqwest_client_with_custom_ca(builder).map_err(|_| TokenClientBuildError)?; Ok(Self::new(client, self.config.clone(), self.profile.clone())) } - /// 共享运行时画像;刷新时取快照,授权码交换仍使用 raw auth 请求。 + /// 共享运行时画像;刷新时取快照,授权码交换仍使用 raw auth 请求 pub fn new(client: Client, config: TokenClientConfig, profile: CodexWireProfileState) -> Self { Self { client, @@ -352,7 +354,7 @@ impl OpenAiTokenClient { { return Err(PersonalAccessTokenError::InvalidToken); } - // 复用固定 auth origin 及其 TLS/超时/禁止重定向策略;导入文档不能指定验证地址。 + // 复用固定 auth origin 及其 TLS/超时/禁止重定向策略;导入文档不能指定验证地址 let mut endpoint = reqwest::Url::parse(&self.config.token_endpoint) .map_err(|_| PersonalAccessTokenError::Unavailable)?; endpoint.set_path(PERSONAL_ACCESS_TOKEN_WHOAMI_PATH); @@ -406,11 +408,11 @@ impl OpenAiTokenClient { } } -/// 构建禁止 redirect 且无自动重试的 Codex token client。 +/// 构建禁止 redirect 且无自动重试的 Codex token client /// /// # Errors /// -/// 本地 TLS/HTTP client 初始化失败时返回脱敏错误。 +/// 本地 TLS/HTTP client 初始化失败时返回脱敏错误 pub fn openai_token_client( config: TokenClientConfig, profile: CodexWireProfileState, @@ -633,16 +635,16 @@ fn parse_token_pair(body: &[u8]) -> Result { } Ok(TokenPair { access_token: tokens.access_token, - // OAuth 刷新响应可能省略未变更的 RT;缺失时由调用方保留当前值。 + // OAuth 刷新响应可能省略未变更的 RT;缺失时由调用方保留当前值 refresh_token: tokens.refresh_token, - // 官方刷新响应允许省略 ID token;缺失时由调用方保留当前值。 + // 官方刷新响应允许省略 ID token;缺失时由调用方保留当前值 id_token: tokens.id_token, }) } fn classify_refresh_failure(status: StatusCode, body: &[u8]) -> RefreshFailure { // 官方刷新错误的消息与错误码分别位于 `error.message`、`error.code`; - // `error` 字符串与顶层 `code` 仅用于兼容官方客户端自身的错误码提取契约。 + // `error` 字符串与顶层 `code` 仅用于兼容官方客户端自身的错误码提取契约 let error = serde_json::from_slice::(body).ok(); let message = error.as_ref().and_then(RefreshErrorResponse::message); let upstream = || { @@ -657,14 +659,14 @@ fn classify_refresh_failure(status: StatusCode, body: &[u8]) -> RefreshFailure { .and_then(RefreshErrorResponse::code) .map(str::to_ascii_lowercase); // 生产策略故意与官方 Codex 的“任意 401 立即终态”不同: - // 显式 401 先进入有界恢复退避,避免瞬时授权故障直接失效账号。 + // 显式 401 先进入有界恢复退避,避免瞬时授权故障直接失效账号 if status == StatusCode::UNAUTHORIZED { return RefreshFailure::Transport { message, upstream: upstream(), }; } - // 非 401 响应仍与官方一致:三个明确的 RT 原因是永久失败。 + // 非 401 响应仍与官方一致:三个明确的 RT 原因是永久失败 if matches!( normalized_code.as_deref(), Some("refresh_token_expired" | "refresh_token_reused" | "refresh_token_invalidated") diff --git a/backend/crates/providers/openai/src/credential/types.rs b/backend/crates/providers/openai/src/credential/types.rs index 2edce2d94..84ad38c8e 100644 --- a/backend/crates/providers/openai/src/credential/types.rs +++ b/backend/crates/providers/openai/src/credential/types.rs @@ -1,4 +1,4 @@ -//! Codex credential 的 Provider-owned 明文结构与安全运行时值对象。 +//! Codex credential 的 Provider-owned 明文结构与安全运行时值对象 use std::fmt; @@ -8,7 +8,7 @@ use secrecy::SecretString; use serde::{Deserialize, Serialize, de::DeserializeOwned}; use url::Url; -/// OAuth AT/RT/ID Token;`Debug` 永不输出明文。 +/// OAuth AT/RT/ID Token;`Debug` 永不输出明文 #[derive(Clone)] pub struct CodexOAuthSecret { pub access_token: SecretString, @@ -30,7 +30,7 @@ impl fmt::Debug for CodexOAuthSecret { } } -/// OAuth credential 持久化时使用的账号投影。 +/// OAuth credential 持久化时使用的账号投影 #[derive(Clone)] pub struct CodexAccountProfile { pub email: Option, @@ -42,9 +42,9 @@ pub struct CodexAccountProfile { pub access_token_expires_at: Option>, } -/// 从 ChatGPT OAuth JWT payload 尽力提取的账号资料。 +/// 从 ChatGPT OAuth JWT payload 尽力提取的账号资料 /// -/// 这只是与官方客户端一致的 base64 JSON 读取,不校验 JWT 签名或 claims。 +/// 这只是与官方客户端一致的 base64 JSON 读取,不校验 JWT 签名或 claims #[derive(Clone, Default)] pub(crate) struct CodexOAuthMetadata { pub(crate) email: Option, @@ -71,11 +71,12 @@ impl fmt::Debug for CodexOAuthMetadata { } } -/// 按官方 `token_data.rs::parse_chatgpt_jwt_claims` 的字段优先级读取 JWT payload。 +/// 按官方 `token_data.rs::parse_chatgpt_jwt_claims` 的字段优先级读取 JWT payload /// -/// 官方对 ID token 和外部 ChatGPT access token 复用同一解析逻辑。JWT 外形、 -/// payload base64 或 JSON 无法解析时返回错误;这不是签名或 claims 验证。 -/// 各账号字段本身仍全部可缺失。 +/// 官方对 ID token 和外部 ChatGPT access token 复用同一解析逻辑 +/// JWT 外形、 +/// payload base64 或 JSON 无法解析时返回错误;这不是签名或 claims 验证 +/// 各账号字段本身仍全部可缺失 pub(crate) fn parse_chatgpt_jwt_claims(jwt: &str) -> Result { let claims = decode_jwt_payload::(jwt)?; let email = claims @@ -95,10 +96,11 @@ pub(crate) fn parse_chatgpt_jwt_claims(jwt: &str) -> Result Option> { let claims = decode_jwt_payload::(jwt).ok()?; claims @@ -142,7 +144,7 @@ struct AuthClaims { _chatgpt_account_is_fedramp: bool, } -/// 与官方 `codex_protocol::auth::PlanType` 同构的 claims 反序列化类型。 +/// 与官方 `codex_protocol::auth::PlanType` 同构的 claims 反序列化类型 #[derive(Deserialize)] #[serde(untagged)] enum ChatgptPlanType { @@ -159,7 +161,7 @@ impl ChatgptPlanType { } } -/// 与官方 `codex_protocol::auth::KnownPlan` 的 wire 名称和 aliases 保持一致。 +/// 与官方 `codex_protocol::auth::KnownPlan` 的 wire 名称和 aliases 保持一致 #[derive(Clone, Copy, Deserialize)] #[serde(rename_all = "lowercase")] enum KnownChatgptPlan { @@ -234,7 +236,7 @@ impl fmt::Debug for CodexAccountProfile { } } -/// 持久化在 Provider credential JSON 中的签名认证主体。 +/// 持久化在 Provider credential JSON 中的签名认证主体 #[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct CodexCredentialPrincipal { @@ -252,7 +254,7 @@ impl fmt::Debug for CodexCredentialPrincipal { } } -/// 存在 `provider_credentials_json` 内的 Cookie。 +/// 存在 `provider_credentials_json` 内的 Cookie #[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct CodexCookie { pub name: String, @@ -299,11 +301,11 @@ impl ResponsesTransport { pub const CODEX_AUTHENTICATION_KIND_OAUTH: &str = "oauth"; -/// Codex OAuth 对 `provider_credentials_json` 的完整明文 schema。 +/// Codex OAuth 对 `provider_credentials_json` 的完整明文 schema #[derive(Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct CodexOAuthCredentialData { - // 默认值不扩展旧凭据 JSON,恢复 WS 优先后仍可由旧版本读取。 + // 默认值不扩展旧凭据 JSON,恢复 WS 优先后仍可由旧版本读取 #[serde( default = "ResponsesTransport::oauth_default", skip_serializing_if = "ResponsesTransport::is_oauth_default" @@ -346,7 +348,7 @@ impl fmt::Debug for CodexOAuthCredentialData { } } -/// OpenAI Provider 的规范化凭据形态。 +/// OpenAI Provider 的规范化凭据形态 #[derive(Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum CodexCredentialData { @@ -417,7 +419,7 @@ impl fmt::Debug for CodexCredentialData { } } -/// 运行时使用的 Cookie;值受 `secrecy` 保护。 +/// 运行时使用的 Cookie;值受 `secrecy` 保护 pub struct RuntimeCodexCookie { pub name: String, pub value: SecretString, @@ -443,14 +445,14 @@ impl fmt::Debug for RuntimeCodexCookie { } } -/// 一批 `Set-Cookie` CAS 写回的结果。 +/// 一批 `Set-Cookie` CAS 写回的结果 #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct CodexCookieCaptureOutcome { pub credential_revision: Option, pub rejected: usize, } -/// 单个已验证 Cookie 的 Provider JSON CAS 输入。 +/// 单个已验证 Cookie 的 Provider JSON CAS 输入 pub struct UpsertCodexCookie { pub account_id: String, pub expected_credential_revision: u64, diff --git a/backend/crates/providers/openai/src/lib.rs b/backend/crates/providers/openai/src/lib.rs index 9659126f1..c07972524 100644 --- a/backend/crates/providers/openai/src/lib.rs +++ b/backend/crates/providers/openai/src/lib.rs @@ -1,4 +1,4 @@ -//! OpenAI Provider 专属能力。 +//! OpenAI Provider 专属能力 mod admin; pub mod config; @@ -42,14 +42,14 @@ pub use transport::{ encode_generate_request, openai_billing_breakdown, }; -/// OpenAI 初始化后交给组装根的最小能力集。 +/// OpenAI 初始化后交给组装根的最小能力集 pub struct ProviderBundle { core_provider: Arc, admin_provider: Arc, worker_contributions: Vec, } -/// 构造 OpenAI 数据面、Provider-owned 后台任务与 Redis OAuth pending owner。 +/// 构造 OpenAI 数据面、Provider-owned 后台任务与 Redis OAuth pending owner pub async fn initialize( config: OpenAiConfig, ports: ProviderStorePorts, @@ -266,13 +266,13 @@ impl ProviderBundle { Arc::clone(&self.admin_provider) } - /// 一次性移交 Host 任务计划,防止同一 owner 被重复注册。 + /// 一次性移交 Host 任务计划,防止同一 owner 被重复注册 pub fn take_worker_contributions(&mut self) -> Vec { std::mem::take(&mut self.worker_contributions) } } -/// OpenAI 初始化失败的脱敏分类。 +/// OpenAI 初始化失败的脱敏分类 #[derive(Debug, thiserror::Error)] pub enum OpenAiInitializeError { #[error("OpenAI runtime policy is unavailable")] diff --git a/backend/crates/providers/openai/src/provider/execution.rs b/backend/crates/providers/openai/src/provider/execution.rs index 6d40d43e8..9ec43a182 100644 --- a/backend/crates/providers/openai/src/provider/execution.rs +++ b/backend/crates/providers/openai/src/provider/execution.rs @@ -1,4 +1,4 @@ -//! OpenAI attempt 的选择、发送与响应流执行。 +//! OpenAI attempt 的选择、发送与响应流执行 use gateway_core::metering::{CalculatedCost, Usage}; @@ -207,7 +207,7 @@ impl CodexProvider { lease.capacity_snapshot(), )); // Standalone Provider 端点没有可证明的账号 owner;Search metadata 必须按 - // 跨账号输入收敛到当前 lease,不能沿用下游声明的账号或 installation identity。 + // 跨账号输入收敛到当前 lease,不能沿用下游声明的账号或 installation identity let turn_metadata = request.turn_metadata.as_deref().and_then(|metadata| { crate::transport::request::scope_turn_metadata(metadata, lease.installation_id(), true) }); @@ -544,7 +544,7 @@ pub(super) fn cold_json_response_stream(request: ColdJsonResponse) -> EventStrea }; if allows_account_state_mutation && let Some(key) = request.session_affinity_key.as_ref() { - // JSON 已完整接收;在首个 yield 前提交亲和迁移,避免下游取消漏掉更新。 + // JSON 已完整接收;在首个 yield 前提交亲和迁移,避免下游取消漏掉更新 request.selector.update_session_affinity( key, request.lease.affinity_expected_account_id(), @@ -616,7 +616,7 @@ fn image_response_metering( body: &[u8], prices: &gateway_core::metering::PricingOverrides, ) -> Option<(Usage, Option)> { - // 只保留 usage,跳过通常很大的 base64 图片;原始响应仍按字节透传。 + // 只保留 usage,跳过通常很大的 base64 图片;原始响应仍按字节透传 #[derive(Deserialize)] struct ImageUsageEnvelope { usage: Option, @@ -637,7 +637,7 @@ fn image_response_metering( usage.image_output_tokens = raw .pointer("/output_tokens_details/image_tokens") .and_then(Value::as_u64); - // 总量是上游独立报告的事实;图片明细是总输入/输出的子集,不能再次相加。 + // 总量是上游独立报告的事实;图片明细是总输入/输出的子集,不能再次相加 usage.total_tokens = raw.get("total_tokens").and_then(Value::as_u64); let cost = crate::transport::usage::image_calculated_cost(request_body, &raw, prices); (usage != Usage::default()).then_some((usage, cost)) @@ -818,7 +818,7 @@ pub(super) fn cold_response_stream(response: ColdResponse) -> EventStream { let mut body = response.body; let mut failure_diagnostics = response.diagnostics.clone(); if response_transport == CodexBackendTransport::WebSocket { - // opening ID 标识连接,不可作为缺失请求级错误头时的当前请求 ID。 + // opening ID 标识连接,不可作为缺失请求级错误头时的当前请求 ID failure_diagnostics.request_id = None; } let failure_set_cookie_headers = response.set_cookie_headers.clone(); @@ -828,9 +828,9 @@ pub(super) fn cold_response_stream(response: ColdResponse) -> EventStream { let rate_limit_updates = response.rate_limit_updates; let response_metadata_updates = response.response_metadata_updates; // OpenAI 线路为透明代理:HTTP SSE 与 WebSocket 两条上游均启用 raw 透传, - // 下游按字节转发上游原文,避免 serde 往返改写数值/精度(大整数→f64、logprobs 等)。 + // 下游按字节转发上游原文,避免 serde 往返改写数值/精度(大整数→f64、logprobs 等) // WS 帧由 reducer 以 encode_sse_event(&event, raw) 逐字节内嵌上游原始 JSON - // (transport/protocol/websocket.rs),push_frames 抽出的 data 即上游原文。 + // (transport/protocol/websocket.rs),push_frames 抽出的 data 即上游原文 let mut decoder = CodexCanonicalDecoder::new(upstream_model.as_str()) .with_pricing(context.pricing().get("openai").and_then(|models| models.get(upstream_model.as_str())).cloned()) .with_reported_model(response.response_metadata.effective_model.as_deref()) @@ -1038,7 +1038,7 @@ pub(super) fn cold_response_stream(response: ColdResponse) -> EventStream { attach_openai_session_update(&mut events, &mut session_capture); if allows_account_state_mutation && completed && terminal_failure.is_none() { // 完成事件一旦交给下游,Core 可以立刻停止轮询 Provider stream; - // 在此之前持久化亲和关系,保证成功请求不会因流被提前 drop 而丢失绑定。 + // 在此之前持久化亲和关系,保证成功请求不会因流被提前 drop 而丢失绑定 selector .record_success( &active_account, @@ -1169,7 +1169,7 @@ pub(super) fn cold_response_stream(response: ColdResponse) -> EventStream { session_transport_recovery.websocket_succeeded(key); } if allows_account_state_mutation && completed && terminal_failure.is_none() { - // 同上:尾部 finish() 也可能产出 completed,亲和记录必须先于任何下游 yield。 + // 同上:尾部 finish() 也可能产出 completed,亲和记录必须先于任何下游 yield selector .record_success( &active_account, diff --git a/backend/crates/providers/openai/src/provider/failure.rs b/backend/crates/providers/openai/src/provider/failure.rs index d4cac601d..0a430f8ab 100644 --- a/backend/crates/providers/openai/src/provider/failure.rs +++ b/backend/crates/providers/openai/src/provider/failure.rs @@ -1,13 +1,13 @@ -//! OpenAI 上游失败分类、恢复决策与稳定错误投影。 +//! OpenAI 上游失败分类、恢复决策与稳定错误投影 use super::*; use gateway_core::diagnostics::TraceContext; -/// OpenAI 失败对 Smart 账号分数的结构化 reason 闭集。 +/// OpenAI 失败对 Smart 账号分数的结构化 reason 闭集 /// -/// 已归一的上游 code 优先;code 缺失时才读取结构化客户端错误的 code/type。 -/// 新增错误、裸 HTTP 状态和内部错误 kind 默认都不会进入该闭集。 -/// 容量拒绝影响短期调度健康度,但不证明账号凭据或额度失效。 +/// 已归一的上游 code 优先;code 缺失时才读取结构化客户端错误的 code/type +/// 新增错误、裸 HTTP 状态和内部错误 kind 默认都不会进入该闭集 +/// 容量拒绝影响短期调度健康度,但不证明账号凭据或额度失效 const OPENAI_ACCOUNT_SCORE_FAILURE_REASONS: &[&str] = &[ "server_is_overloaded", "slow_down", @@ -32,7 +32,7 @@ fn openai_account_score_failure_reason(error: &ProviderError) -> Option<&str> { upstream.code().or_else(|| upstream.error_type()) } -/// 返回该 OpenAI 失败是否属于 Smart 账号计分闭集。 +/// 返回该 OpenAI 失败是否属于 Smart 账号计分闭集 #[doc(hidden)] pub fn openai_failure_affects_account_score(error: &ProviderError) -> bool { openai_account_score_failure_reason(error).is_some_and(is_openai_account_score_failure_reason) @@ -42,7 +42,7 @@ pub(super) struct MappedProviderFailure { pub(super) error: ProviderError, pub(super) websocket_transport_retryable: bool, pub(super) account_failure: Option, - /// 原始上游错误描述,仅在凭据错误状态下持久化。 + /// 原始上游错误描述,仅在凭据错误状态下持久化 pub(super) error_message: Option, pub(super) cyber_policy_failure: bool, pub(super) upstream_capacity_failure: bool, @@ -82,11 +82,13 @@ pub(super) async fn wait_for_replay_grace(deadline: Option) { } } -/// 提交边界前的上游事件预取。 +/// 提交边界前的上游事件预取 /// -/// 原始 chunk 计数而不是重编码后的 event 大小。时间与字节阈值共同限定无感换号 +/// 原始 chunk 计数而不是重编码后的 event 大小 +/// 时间与字节阈值共同限定无感换号 /// 窗口;任一边界到达都会提交已缓存 wire,不能因网关私有资源规则伪造上游协议 -/// 失败。一旦提交,后续事件不再具备无痕重放资格。 +/// 失败 +/// 一旦提交,后续事件不再具备无痕重放资格 pub(super) struct PreCommitClientEvents { pending: Vec, prefetched_bytes: usize, @@ -167,7 +169,7 @@ impl PreCommitClientEvents { return incoming; } self.pending.extend(incoming); - // 此入口只在同批次已出现语义输出、随后失败时释放原始事件。 + // 此入口只在同批次已出现语义输出、随后失败时释放原始事件 self.commit_pending(PreCommitReleaseReason::SemanticOutput) } @@ -188,7 +190,7 @@ impl PreCommitClientEvents { } pub(super) fn commit_pending(&mut self, reason: PreCommitReleaseReason) -> Vec { - // Provider 只记录释放缓存的原因;实际下游提交仍由 Core 记录和裁决。 + // Provider 只记录释放缓存的原因;实际下游提交仍由 Core 记录和裁决 if self.trace.is_enabled() { self.trace.record("provider.precommit.released", json!({ "reason": reason, @@ -430,7 +432,7 @@ pub(super) async fn apply_failure( "Failed to persist OpenAI response cookies" ); } - // 探测只验证恢复,不能把探测自身的单请求并发当作业务负载证据。 + // 探测只验证恢复,不能把探测自身的单请求并发当作业务负载证据 if context.allows_capacity_feedback && failure.upstream_capacity_failure { context .quota @@ -443,9 +445,9 @@ pub(super) fn schedule_authoritative_quota_refresh_after_failure( quota: &Arc, account: &ProviderAccount, ) { - // 限额错误可以确认账号状态,但 Responses 事件中的 used_percent 可能仍停在上一结算点。 - // usage 快照在后台补齐展示基线,不得撤销同一轮真实失败,也不能阻塞原始响应。 - // 先等待 2 秒让上游结算,再查询;5 秒仅限制实际查询自身。 + // 限额错误可以确认账号状态,但 Responses 事件中的 used_percent 可能仍停在上一结算点 + // usage 快照在后台补齐展示基线,不得撤销同一轮真实失败,也不能阻塞原始响应 + // 先等待 2 秒让上游结算,再查询;5 秒仅限制实际查询自身 let quota = Arc::clone(quota); let account_id = account.id().clone(); drop(tokio::spawn(async move { @@ -543,7 +545,7 @@ pub(super) fn apply_websocket_recovery_policy( failure: &mut MappedProviderFailure, context: WebSocketRecoveryContext<'_>, ) { - // 明确账号拒绝走已有换号路径,容量拒绝走请求内退避;两者都不消耗 WS 传输预算。 + // 明确账号拒绝走已有换号路径,容量拒绝走请求内退避;两者都不消耗 WS 传输预算 if failure.error.replay_is_safe() && (failure.account_failure.is_some() || matches!( @@ -567,7 +569,7 @@ pub(super) fn apply_websocket_recovery_policy( ); return; } - // close 1009 只拒绝当前请求,不代表会话的 WS 传输不可用。 + // close 1009 只拒绝当前请求,不代表会话的 WS 传输不可用 let session_budget_exhausted = failure.error.kind() != ProviderErrorKind::MessageTooBig && context.session_affinity_key.is_some_and(|key| { context @@ -609,7 +611,7 @@ pub(super) fn apply_websocket_recovery_policy( } // 传输恢复必须保持原账号可调度;最终 HTTP attempt 若仍失败,再按真实 HTTP - // 结果更新账号健康度,避免中间 WS 错误把同账号钉选提前冷却掉。 + // 结果更新账号健康度,避免中间 WS 错误把同账号钉选提前冷却掉 failure.account_failure = None; let fallback_now = context.policy == WebSocketFailurePolicy::ImmediateFallback || context.retry_count >= context.max_retries @@ -708,8 +710,8 @@ pub(super) fn continuation_replay_required_error(reason: &'static str) -> Provid .with_client_visible_upstream_error(continuation_replay_error_detail()) } -/// 额度拒绝使当前账号无法继续原生历史,由客户端携带完整输入创建新链。 -/// 只投影客户端响应;真实上游分类、发送状态和错误正文继续用于隔离与记账。 +/// 额度拒绝使当前账号无法继续原生历史,由客户端携带完整输入创建新链 +/// 只投影客户端响应;真实上游分类、发送状态和错误正文继续用于隔离与记账 pub(super) fn quota_continuation_replay_error( mut error: ProviderError, request: &CodexResponsesRequest, @@ -730,13 +732,13 @@ pub(super) fn quota_continuation_replay_error( response .headers() .iter() - // 额度窗口的等待时间不适用于客户端重建历史。 + // 额度窗口的等待时间不适用于客户端重建历史 .filter(|header| !header.name().eq_ignore_ascii_case("retry-after")) .cloned() .collect() }) .unwrap_or_default(); - // 流内拒绝没有原始 HTTP 失败响应,仍需把该次失败的关联 ID 交给 HTTP 客户端。 + // 流内拒绝没有原始 HTTP 失败响应,仍需把该次失败的关联 ID 交给 HTTP 客户端 if !headers.iter().any(|header| { header.name().eq_ignore_ascii_case("x-request-id") || header.name().eq_ignore_ascii_case("x-oai-request-id") @@ -754,7 +756,8 @@ pub(super) fn quota_continuation_replay_error( "type": detail.error_type(), }}); // 未交付的 response.created 与额度错误帧必须一起丢弃,否则原错误帧会 - // 覆盖恢复投影。该拒绝发生在语义输出和 commit 之前,不能带走已交付事件。 + // 覆盖恢复投影 + // 该拒绝发生在语义输出和 commit 之前,不能带走已交付事件 drop(error.take_atomic_client_events()); error .with_continuation_failure(ContinuationFailure::HistoryUnavailable) @@ -919,7 +922,7 @@ pub(super) fn map_client_error( diagnostics, .. } => { - // 已知响应头只补充观测事实;不将残缺响应重新分类成可换号重放的上游拒绝。 + // 已知响应头只补充观测事实;不将残缺响应重新分类成可换号重放的上游拒绝 let mut error = provider_error( if source.is_timeout() { ProviderErrorKind::Timeout @@ -1036,7 +1039,7 @@ pub(super) fn map_client_error( failure } -/// 在消费 transport 错误前提取可持久化事实,不能使用可能携带 URL/凭据的 Display。 +/// 在消费 transport 错误前提取可持久化事实,不能使用可能携带 URL/凭据的 Display fn client_diagnostic(error: &CodexClientError) -> Option { let (stage, code, message) = match error { CodexClientError::ConnectionBudgetExhausted => ( @@ -1310,7 +1313,7 @@ pub(super) fn websocket_client_visible_error( }, str::to_owned, ); - // RFC 6455 close 1009 是请求过大,客户端需收到可行动的错误码。 + // RFC 6455 close 1009 是请求过大,客户端需收到可行动的错误码 let (code, error_type) = if message_too_big { (Some("message_too_big".to_owned()), "invalid_request_error") } else { @@ -1433,7 +1436,7 @@ pub(super) fn map_upstream_failure( ), error_message: failure.client_message, cyber_policy_failure, - // 普通 5xx 与未知流内错误不足以证明容量拒绝,不能用于冻结账号。 + // 普通 5xx 与未知流内错误不足以证明容量拒绝,不能用于冻结账号 upstream_capacity_failure: capacity_unavailable, set_cookie_headers: failure.set_cookie_headers, rate_limit_headers: failure.rate_limit_headers, @@ -1580,7 +1583,7 @@ pub(super) fn websocket_error_kind(error: &CodexWebSocketExchangeError) -> Provi | CodexWebSocketExchangeError::ReusedConnectionDiedBeforeFirstEvent { .. } => { ProviderErrorKind::Transport } - // live 流可能包在 PostSendAmbiguous 中;RFC 6455 close 1009 是请求过大。 + // live 流可能包在 PostSendAmbiguous 中;RFC 6455 close 1009 是请求过大 CodexWebSocketExchangeError::ClosedBeforeTerminal(_) | CodexWebSocketExchangeError::PostSendAmbiguous { .. } => { if error @@ -1605,7 +1608,7 @@ pub(super) fn provider_error( ProviderError::new(kind, send_state) } -/// 只恢复明确未发送的 HTTP 建连失败;已知 TLS/配置错误不反复尝试同一路径。 +/// 只恢复明确未发送的 HTTP 建连失败;已知 TLS/配置错误不反复尝试同一路径 fn transient_http_connect(error: &reqwest::Error) -> bool { if !error.is_connect() || error.is_builder() { return false; diff --git a/backend/crates/providers/openai/src/provider/mod.rs b/backend/crates/providers/openai/src/provider/mod.rs index dfb37ef97..0119f697d 100644 --- a/backend/crates/providers/openai/src/provider/mod.rs +++ b/backend/crates/providers/openai/src/provider/mod.rs @@ -1,4 +1,4 @@ -//! Codex 的 `gateway-core` Provider adapter。 +//! Codex 的 `gateway-core` Provider adapter use std::collections::BTreeSet; use std::fmt; @@ -118,17 +118,17 @@ const PROVIDER_NAME: &str = "openai"; const HTTP_SSE_TRANSPORT: &str = "http_sse"; const HTTP_JSON_TRANSPORT: &str = "http_json"; const WEBSOCKET_TRANSPORT: &str = "websocket"; -// 在已观测到的 Codex OAuth 上游 16 MiB 附近消息边界前留出传输 metadata 余量。 +// 在已观测到的 Codex OAuth 上游 16 MiB 附近消息边界前留出传输 metadata 余量 const WEBSOCKET_HTTP_FALLBACK_THRESHOLD_BYTES: usize = 15 * 1024 * 1024; const MAX_COOKIE_HEADER_BYTES: usize = 16 * 1024; /// 提交边界前预取 128 KiB 原始上游 chunk;容纳携带配置回显的前导事件, /// 超过阈值后结束无感换号窗口(最后一个 chunk 可越过阈值), -/// 但不会把上游数据改写成协议失败。 +/// 但不会把上游数据改写成协议失败 const MAX_STREAM_PREFETCH_BYTES: usize = 128 * 1024; /// 短暂保留 response.created 等结构事件,让随后到达的明确拒绝可以无感换号; -/// 到期即放行,避免模型长时间思考时让客户端一直收不到首事件。 +/// 到期即放行,避免模型长时间思考时让客户端一直收不到首事件 const STREAM_REPLAY_GRACE: Duration = Duration::from_millis(2_500); -// 额度拒绝后先给上游额度结算留出时间,再以受限时长同步 usage 快照。 +// 额度拒绝后先给上游额度结算留出时间,再以受限时长同步 usage 快照 const QUOTA_FAILURE_REFRESH_DELAY: Duration = Duration::from_secs(2); const QUOTA_FAILURE_REFRESH_TIMEOUT: Duration = Duration::from_secs(5); pub const OFFICIAL_CODEX_BASE_PATH: &str = "/backend-api"; @@ -236,7 +236,7 @@ impl CodexProvider { Ok(self.client.clone().with_request_profile(profile)) } - // Provider 构造集中装配独立领域服务和透明传输依赖,拆分参数会模糊所有权。 + // Provider 构造集中装配独立领域服务和透明传输依赖,拆分参数会模糊所有权 #[expect(clippy::too_many_arguments)] pub fn new( selector: Arc, @@ -472,7 +472,7 @@ impl Provider for CodexProvider { UpstreamSendState::NotSent, )); }; - // 请求设置先形成原生正文基线;attempt 的显式改写进入终端后不再次被覆盖。 + // 请求设置先形成原生正文基线;attempt 的显式改写进入终端后不再次被覆盖 let mut operation = Operation::Generate(generate.clone()); if context.disable_fast() && let Operation::Generate(generate) = &operation @@ -514,13 +514,13 @@ impl Provider for CodexProvider { return Err(continuation_replay_required_error("scope_unavailable")); } // 其他协议必须先取得真实账号,再按固定 attempt 阶段调用转换器;选号前不能 - // 把未知正文当成 OpenAI wire 解释会话、亲和或传输字段。 + // 把未知正文当成 OpenAI wire 解释会话、亲和或传输字段 let upstream = (generate.protocol_payload().protocol() == PROVIDER_NAME) .then(|| encode_generate_request(generate, upstream_model.as_str(), None)) .transpose() .map_err(map_request_error)?; - // 固定调度约束:Guardian 分类先于选号,不能随原生/适配器发送路径改变。 - // 复用编码器的权威 metadata 解析,但只为原生路径准备会话与传输状态。 + // 固定调度约束:Guardian 分类先于选号,不能随原生/适配器发送路径改变 + // 复用编码器的权威 metadata 解析,但只为原生路径准备会话与传输状态 let guardian = upstream .as_ref() .is_some_and(CodexResponsesRequest::is_guardian); @@ -563,7 +563,7 @@ impl Provider for CodexProvider { .await .map_err(map_selection_error) }; - // 恢复期间排队也消耗启动窗口;只包住选账号,不限制业务响应时长。 + // 恢复期间排队也消耗启动窗口;只包住选账号,不限制业务响应时长 let lease = if let Some(remaining) = context.connection_budget().startup_remaining() { tokio::time::timeout(remaining, selection) .await @@ -781,7 +781,7 @@ impl CodexProvider { }) { // HTTP store=false 没有原生续链,完整历史仍由客户端持有;不能把 delta - // 当作独立新请求发送,也不能用 previous_response_id 猜测上游存储状态。 + // 当作独立新请求发送,也不能用 previous_response_id 猜测上游存储状态 return Err(continuation_replay_required_error("scope_unavailable")); } scope_request_to_account( @@ -961,7 +961,7 @@ impl CodexProvider { } fn native_request_requirements(request: &GenerateRequest) -> CapabilityRequirements { - // 此处只解释已知 OpenAI wire;不在 Core 的通用转换路径推断任意目标协议。 + // 此处只解释已知 OpenAI wire;不在 Core 的通用转换路径推断任意目标协议 Operation::Generate(GenerateRequest::from_protocol_payload( request.protocol_payload().clone(), )) diff --git a/backend/crates/providers/openai/src/provider/observation.rs b/backend/crates/providers/openai/src/provider/observation.rs index 8f7235611..77ef0b31e 100644 --- a/backend/crates/providers/openai/src/provider/observation.rs +++ b/backend/crates/providers/openai/src/provider/observation.rs @@ -1,4 +1,4 @@ -//! OpenAI 请求、响应与 transport 观测事实归一化。 +//! OpenAI 请求、响应与 transport 观测事实归一化 use super::*; @@ -8,7 +8,7 @@ pub(super) fn endpoint_requested_model( if payload.protocol() != PROVIDER_NAME { return None; } - // 只读取 model,避免把编辑请求中的整张图片复制到观测数据。 + // 只读取 model,避免把编辑请求中的整张图片复制到观测数据 #[derive(Deserialize)] struct RequestModel { model: Option, @@ -19,10 +19,10 @@ pub(super) fn endpoint_requested_model( gateway_core::routing::PublicModelId::new(model.as_str()?.to_owned()).ok() } -/// OpenAI Responses 的观测状态完全归 Provider 所有。 +/// OpenAI Responses 的观测状态完全归 Provider 所有 /// /// 每次原始 SSE/WS 事件推进状态后重新生成不可变 observation,Core 只负责携带、 -/// 持久化和展示这份安全快照,不解释任何 OpenAI 协议字段。 +/// 持久化和展示这份安全快照,不解释任何 OpenAI 协议字段 pub(super) struct OpenAiResponseObservationState { transport: CodexBackendTransport, diagnostics: CodexUpstreamDiagnostics, @@ -94,7 +94,7 @@ impl OpenAiResponseObservationState { &self, failure: Option<&ProviderError>, ) -> Option { - // 失败快照的请求 ID 来自当前错误;连接诊断仍保持 opening 原貌,不维护第二份可变 ID。 + // 失败快照的请求 ID 来自当前错误;连接诊断仍保持 opening 原貌,不维护第二份可变 ID let failure_diagnostics = failure .filter(|_| self.transport == CodexBackendTransport::WebSocket) .map(|error| { @@ -119,7 +119,7 @@ impl OpenAiResponseObservationState { if let Some(metadata) = self.provider_metadata() { observation = observation.with_provider_metadata(metadata); } - // 统计与计费采用最终发送档位;响应回显只作为独立诊断事实保留。 + // 统计与计费采用最终发送档位;响应回显只作为独立诊断事实保留 if let Some(service_tier) = &self.requested_service_tier { observation = observation.with_service_tier_if_valid(service_tier.clone()); } @@ -154,7 +154,7 @@ impl OpenAiResponseObservationState { ) -> bool { let mut changed = false; // 首个非前导输出事件(结构帧也算)开启首字计时; - // 真实语义首字由 first_reasoning_ms / first_text_ms 单独观测。 + // 真实语义首字由 first_reasoning_ms / first_text_ms 单独观测 if signals.output_start { changed |= insert_first_timing(&mut self.timings.first_token_ms, started_at); } @@ -330,8 +330,8 @@ pub(super) fn codex_response_observation( WebSocketPoolKind::New }); } - // WebSocket opening 的 101 是成功升级事实,不是业务请求的失败 HTTP 状态。 - // opening 明确拒绝仍由 `codex_error_observation` 保存真实上游状态。 + // WebSocket opening 的 101 是成功升级事实,不是业务请求的失败 HTTP 状态 + // opening 明确拒绝仍由 `codex_error_observation` 保存真实上游状态 if transport != CodexBackendTransport::WebSocket && let Some(status_code) = diagnostics.status_code { @@ -576,8 +576,8 @@ pub(super) fn nonnegative_millis(value: Option) -> Option { } pub(super) fn compile_model_capabilities(model: &CodexCatalogModel) -> ProviderModelCapabilities { - // Catalog membership is enough to publish Generate. `supported_in_api` is advisory; - // the upstream response is authoritative for normal and diagnostic requests. + // 模型进入目录即可发布 Generate 能力,`supported_in_api` 仅作提示 + // 普通请求和诊断请求均以上游响应为准 let capabilities = ModelCapabilities::new(BTreeSet::from([OperationKind::Generate]), None) .with_upstream_feature_validation(); ProviderModelCapabilities::new(model.request_model().clone(), capabilities) @@ -587,7 +587,7 @@ pub(super) fn compile_model_capabilities(model: &CodexCatalogModel) -> ProviderM pub(super) fn codex_model_presentation(model: &CodexCatalogModel) -> ModelPresentation { let capabilities = model.capabilities(); let reasoning_efforts = capabilities.reasoning_efforts().to_vec(); - // Codex 目录不声明默认 effort;有 medium 时对齐官方 picker 缺省,否则取首项。 + // Codex 目录不声明默认 effort;有 medium 时对齐官方 picker 缺省,否则取首项 let default_reasoning = reasoning_efforts .iter() .find(|effort| effort.as_str() == "medium") @@ -616,7 +616,7 @@ pub(super) fn codex_model_presentation(model: &CodexCatalogModel) -> ModelPresen .map(std::num::NonZeroU64::get), ) .with_image_input(capabilities.image_input() == CodexCatalogCapabilityEvidence::DeclaredNative) - // Codex Responses 工具协议随 API 支持一并可用;只有明确不支持才关闭。 + // Codex Responses 工具协议随 API 支持一并可用;只有明确不支持才关闭 .with_agent_tools( capabilities.responses_api() != CodexCatalogCapabilityEvidence::DeclaredUnsupported, capabilities.parallel_tool_calls() == CodexCatalogCapabilityEvidence::DeclaredNative, diff --git a/backend/crates/providers/openai/src/provider/upstream_adapter.rs b/backend/crates/providers/openai/src/provider/upstream_adapter.rs index 6f434fc72..d506f1918 100644 --- a/backend/crates/providers/openai/src/provider/upstream_adapter.rs +++ b/backend/crates/providers/openai/src/provider/upstream_adapter.rs @@ -1,4 +1,4 @@ -//! 插件只替换业务 wire;账号租约、凭据解释与费用来源仍属于 OpenAI。 +//! 插件只替换业务 wire;账号租约、凭据解释与费用来源仍属于 OpenAI use super::*; use crate::transport::usage::{OpenAiBillingUsage, openai_billing_breakdown_with_override}; @@ -19,7 +19,7 @@ struct SelectedConnection { } impl CodexProvider { - // 与原生 terminal 共用一次选号结果,不能为适配器重新获取账号或租约。 + // 与原生 terminal 共用一次选号结果,不能为适配器重新获取账号或租约 #[expect(clippy::too_many_arguments)] pub(super) fn execute_upstream_adapter( self: Arc, @@ -122,12 +122,12 @@ impl UpstreamAccountConnection for SelectedConnection { bytes::Bytes::copy_from_slice(account_id.as_bytes()), )); } - // 业务目标可能与原生 Responses 不同,不能把原域名的 Cookie 带到新目标。 + // 业务目标可能与原生 Responses 不同,不能把原域名的 Cookie 带到新目标 Ok(headers) } fn calculate_cost(&self, _: Option<&str>, usage: &Usage) -> Option { - // 与原生路径一样按请求策略计价;响应回显只作观测,不能切换模型或档位。 + // 与原生路径一样按请求策略计价;响应回显只作观测,不能切换模型或档位 let usage = gateway_protocol::openai::events::TokenUsage { input_tokens: usage.input_tokens?, output_tokens: usage.output_tokens?, diff --git a/backend/crates/providers/openai/src/provider/workers.rs b/backend/crates/providers/openai/src/provider/workers.rs index adfd79d48..b251a6166 100644 --- a/backend/crates/providers/openai/src/provider/workers.rs +++ b/backend/crates/providers/openai/src/provider/workers.rs @@ -1,4 +1,4 @@ -//! OpenAI Provider 向 Host 贡献的后台 worker。 +//! OpenAI Provider 向 Host 贡献的后台 worker use super::*; use crate::transport::profile::cli_release::CliReleaseService; @@ -228,7 +228,7 @@ impl ScheduledTask for OpenAiDesktopReleaseTask { }; if let Err(error) = result { // 上游检查失败已经作为 Provider 观察事实保存;本周期本身正常完成, - // 避免 Host 的短退避持续请求固定官方 appcast。 + // 避免 Host 的短退避持续请求固定官方 appcast tracing::warn!(error = %error, "OpenAI Desktop release check failed"); } Ok(()) @@ -376,7 +376,7 @@ impl ScheduledTask for OpenAiWarmupTask { use chrono::Timelike as _; let now = chrono::Utc::now(); let local_now = self.timezone.local(now); - // 回拨产生的第二个相同时刻不能再次执行。 + // 回拨产生的第二个相同时刻不能再次执行 if self .timezone .resolve_local(local_now.naive_local()) diff --git a/backend/crates/providers/openai/src/session_transport.rs b/backend/crates/providers/openai/src/session_transport.rs index 8a9d208f1..9bdf23ed1 100644 --- a/backend/crates/providers/openai/src/session_transport.rs +++ b/backend/crates/providers/openai/src/session_transport.rs @@ -1,4 +1,4 @@ -//! 根会话的连续 WS 失败预算;耗尽后保持 HTTP,空闲过期后释放状态。 +//! 根会话的连续 WS 失败预算;耗尽后保持 HTTP,空闲过期后释放状态 use std::collections::HashMap; use std::sync::{Arc, Mutex, MutexGuard}; @@ -40,7 +40,7 @@ impl CodexSessionTransportRecovery { matches!(session.state, TransportState::Http) } - /// 返回连续失败是否已耗尽首发加重试预算。 + /// 返回连续失败是否已耗尽首发加重试预算 pub(crate) fn record_websocket_failure( &self, key: &ProviderSessionAffinityKey, diff --git a/backend/crates/providers/openai/src/transport/canonical.rs b/backend/crates/providers/openai/src/transport/canonical.rs index bc15a83f3..6a24d7db9 100644 --- a/backend/crates/providers/openai/src/transport/canonical.rs +++ b/backend/crates/providers/openai/src/transport/canonical.rs @@ -1,4 +1,4 @@ -//! Codex Responses SSE 到核心 canonical event 的单一解码边界。 +//! Codex Responses SSE 到核心 canonical event 的单一解码边界 use std::{ collections::{BTreeMap, BTreeSet}, @@ -30,10 +30,10 @@ use super::usage::{ const CONTENTS_PER_OUTPUT: u32 = 1_024; -/// 单 attempt 的增量 Responses decoder。 +/// 单 attempt 的增量 Responses decoder /// -/// 上游 wire 是客户端可见的事实来源;canonical facts 只用于观测、亲和和计费。 -/// 因而未知或形状变化的 JSON event 只能放弃 canonical 投影,不能截断 wire 流。 +/// 上游 wire 是客户端可见的事实来源;canonical facts 只用于观测、亲和和计费 +/// 因而未知或形状变化的 JSON event 只能放弃 canonical 投影,不能截断 wire 流 pub struct CodexCanonicalDecoder { pricing: Option, decoder: SseEventDecoder, @@ -56,13 +56,13 @@ pub struct CodexCanonicalDecoder { raw_sse_passthrough: bool, } -/// 上游 Responses 事件的两类失败:协议损坏,或上游明确报告业务失败。 +/// 上游 Responses 事件的两类失败:协议损坏,或上游明确报告业务失败 #[derive(Error)] pub enum CodexCanonicalError { - /// SSE/JSON 违反了已知协议不变量。 + /// SSE/JSON 违反了已知协议不变量 #[error("invalid Codex Responses event")] Protocol(#[source] ProviderError), - /// 上游在成功建立流后发送了明确的失败事件。 + /// 上游在成功建立流后发送了明确的失败事件 #[error("Codex upstream reported a failed response")] Upstream(Box), } @@ -82,18 +82,18 @@ impl From for CodexCanonicalError { } } -/// 单次增量解码的有序结果。 +/// 单次增量解码的有序结果 /// /// 上游失败不是解析异常:它与失败前已经产生的事件一起返回,Provider 因而可以 -/// 先保留真实输出,再把类型化失败交给 Core 收敛。 +/// 先保留真实输出,再把类型化失败交给 Core 收敛 pub enum CodexCanonicalOutcome { - /// 本批次只包含正常事件。 + /// 本批次只包含正常事件 Events(Vec), - /// 本批次在若干正常事件后到达失败边界。 + /// 本批次在若干正常事件后到达失败边界 Failed(CodexCanonicalFailure), } -/// `response.failed` 或协议错误发生时的单一 typed outcome。 +/// `response.failed` 或协议错误发生时的单一 typed outcome pub struct CodexCanonicalFailure { events: Vec, error: CodexCanonicalError, @@ -112,17 +112,17 @@ impl fmt::Debug for CodexCanonicalFailure { } impl CodexCanonicalFailure { - /// 失败前按上游顺序产生的事件。 + /// 失败前按上游顺序产生的事件 pub fn events(&self) -> &[ProviderEvent] { &self.events } - /// 类型化失败;其 Debug/Display 不包含上游正文。 + /// 类型化失败;其 Debug/Display 不包含上游正文 pub const fn error(&self) -> &CodexCanonicalError { &self.error } - /// 失败前是否已经出现客户端可消费的真实输出。 + /// 失败前是否已经出现客户端可消费的真实输出 pub const fn semantic_output_seen(&self) -> bool { self.semantic_output_seen } @@ -142,7 +142,7 @@ impl CodexCanonicalDecoder { self } - /// 使用路由后最终发往上游的请求模型计价,并在响应缺少模型时用于 canonical 兜底。 + /// 使用路由后最终发往上游的请求模型计价,并在响应缺少模型时用于 canonical 兜底 pub fn new(upstream_model: impl Into) -> Self { Self { decoder: SseEventDecoder::default(), @@ -167,23 +167,23 @@ impl CodexCanonicalDecoder { } } - /// 让 HTTP SSE 上游帧以原始字节随 wire event 向下游传递。 + /// 让 HTTP SSE 上游帧以原始字节随 wire event 向下游传递 /// - /// JSON 投影仍只供 canonical facts、计费与亲和旁路读取;不会用于重建 SSE。 + /// JSON 投影仍只供 canonical facts、计费与亲和旁路读取;不会用于重建 SSE #[must_use] pub fn with_raw_sse_passthrough(mut self) -> Self { self.raw_sse_passthrough = true; self } - /// 使用最终发送给上游的请求档位估算费用,不由响应回显覆盖。 + /// 使用最终发送给上游的请求档位估算费用,不由响应回显覆盖 #[must_use] pub fn with_requested_service_tier(mut self, service_tier: Option<&str>) -> Self { self.requested_service_tier = normalize_service_tier(service_tier); self } - /// 记录请求工具类型,用于区分标准与预览 Web Search 的按次价格。 + /// 记录请求工具类型,用于区分标准与预览 Web Search 的按次价格 #[must_use] pub fn with_request_tool_pricing(mut self, model: &str, tools: Option<&[Value]>) -> Self { self.web_search_pricing = web_search_pricing(model, tools); @@ -226,36 +226,36 @@ impl CodexCanonicalDecoder { self.decode(events) } - /// 取走最近一次解码中由原始 Responses 事件观察到的计时语义。 + /// 取走最近一次解码中由原始 Responses 事件观察到的计时语义 /// /// 这份事实独立于 canonical 投影:未知的未来事件仍可保留 wire 透明转发, - /// 同时让 Provider 正确记录首个可消费输出的时延。 + /// 同时让 Provider 正确记录首个可消费输出的时延 #[must_use] pub fn take_timing_signals(&mut self) -> ResponseEventSignals { std::mem::take(&mut self.timing_signals) } - /// 返回本 attempt 的上游响应回显档位,仅供诊断,不作为本地计费依据。 + /// 返回本 attempt 的上游响应回显档位,仅供诊断,不作为本地计费依据 #[must_use] pub fn response_service_tier(&self) -> Option<&str> { self.response_service_tier.as_deref() } - /// 真实 HTTP 响应头提供初始报告;流内请求级报告可覆盖它。 + /// 真实 HTTP 响应头提供初始报告;流内请求级报告可覆盖它 #[must_use] pub fn with_reported_model(mut self, model: Option<&str>) -> Self { self.reported_model = model.and_then(observed_model_name).map(str::to_owned); self } - /// 接收 transport 已解析的内部 metadata 报告,内部帧无需交付客户端。 + /// 接收 transport 已解析的内部 metadata 报告,内部帧无需交付客户端 pub(crate) fn observe_reported_model(&mut self, model: &str) { if let Some(model) = observed_model_name(model) { self.reported_model = Some(model.to_owned()); } } - /// 官方服务端报告优先;缺少报告时仅使用正文明确声明,不使用请求兜底值。 + /// 官方服务端报告优先;缺少报告时仅使用正文明确声明,不使用请求兜底值 #[must_use] pub fn response_model(&self) -> Option<&str> { self.reported_model @@ -323,8 +323,8 @@ impl CodexCanonicalDecoder { .as_deref() .or_else(|| value.get("type").and_then(Value::as_str)); if event_type == Some("codex.rate_limits") { - // HTTP transport has already projected this control frame into local quota facts. - // It must not become client output or start first-output timing. + // HTTP 传输已将此控制帧投影为本地额度事实 + // 此帧不能成为客户端输出,也不能启动首个输出计时 return Ok(()); } self.observe_response_service_tier(&value); @@ -342,8 +342,9 @@ impl CodexCanonicalDecoder { ); let mut canonical = Vec::new(); if !self.started { - // 失败首帧可能是唯一携带 response_id 的上游事实。身份投影只做 - // 最佳努力:缺少 response/id 时仍必须保留原 typed failure。 + // 失败首帧可能是唯一携带 response_id 的上游事实 + // 身份投影只做 + // 最佳努力:缺少 response/id 时仍必须保留原 typed failure let _ = self.start(&value, &mut canonical); } if let Some(wire) = Self::wire_for_event(event, value, raw_sse_frame) { @@ -392,10 +393,10 @@ impl CodexCanonicalDecoder { self.response_service_tier = Some(service_tier); } - /// 构造下发的 wire event。 + /// 构造下发的 wire event /// /// raw 帧存在时原始字节原样透传;解析出的 event/id 只作旁路元数据,不能 - /// 反过来决定客户端事件是否可交付。 + /// 反过来决定客户端事件是否可交付 fn wire_for_event( event: SseEvent, value: Value, @@ -487,7 +488,7 @@ impl CodexCanonicalDecoder { output: &mut Vec, ) -> Result<(), ProviderError> { if self.started { - // `response.in_progress` 是 created 后的结构事件,不重复发 Started。 + // `response.in_progress` 是 created 后的结构事件,不重复发 Started return Ok(()); } let response = response_object(value).ok_or_else(protocol_error_marker)?; @@ -905,7 +906,7 @@ impl CodexCanonicalDecoder { .filter(|model| !model.is_empty()) .unwrap_or(&self.upstream_model) .to_owned(); - // 按最终发送的模型与档位估算,响应回显仅作观测,不改变本地计价口径。 + // 按最终发送的模型与档位估算,响应回显仅作观测,不改变本地计价口径 let service_tier = self.requested_service_tier.as_deref(); let tool_calls = billable_tool_calls(response); if let Some(breakdown) = usage @@ -1063,7 +1064,7 @@ fn billable_tool_calls(response: &Value) -> Option<(u64, u64)> { { file = file.checked_add(1)?; } - // 这些输出没有独立的 OpenAI 工具调用费。 + // 这些输出没有独立的 OpenAI 工具调用费 "message" | "reasoning" | "function_call" @@ -1076,7 +1077,7 @@ fn billable_tool_calls(response: &Value) -> Option<(u64, u64)> { | "mcp_approval_request" | "compaction" => {} // 容器会话、生图及未知工具需要额外计费事实,不能把 token 小计 - // 当作完整费用。 + // 当作完整费用 _ => return None, } } diff --git a/backend/crates/providers/openai/src/transport/catalog.rs b/backend/crates/providers/openai/src/transport/catalog.rs index 734a17708..bb1e50b64 100644 --- a/backend/crates/providers/openai/src/transport/catalog.rs +++ b/backend/crates/providers/openai/src/transport/catalog.rs @@ -1,4 +1,4 @@ -//! Codex 官方模型目录 wire 与安全快照。 +//! Codex 官方模型目录 wire 与安全快照 use std::collections::BTreeSet; use std::num::NonZeroU64; @@ -11,7 +11,7 @@ use serde::Deserialize; use super::client::OpenAiUpstreamProtocol; -/// 单次 Codex 模型目录响应允许的最大字节数。 +/// 单次 Codex 模型目录响应允许的最大字节数 pub const MAX_CODEX_MODEL_CATALOG_BYTES: usize = 1024 * 1024; const MAX_CATALOG_MODELS: usize = 2_048; @@ -20,14 +20,14 @@ const MAX_DESCRIPTION_BYTES: usize = 4 * 1024; const MAX_SERVICE_TIER_ID_BYTES: usize = 64; const MAX_ETAG_BYTES: usize = 256; -/// 上游目录对一项能力给出的明确证据。 +/// 上游目录对一项能力给出的明确证据 #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum CodexCatalogCapabilityEvidence { - /// 上游明确声明原生支持。 + /// 上游明确声明原生支持 DeclaredNative, - /// 上游明确声明不支持。 + /// 上游明确声明不支持 DeclaredUnsupported, - /// 上游没有提供可依赖的声明。 + /// 上游没有提供可依赖的声明 #[default] Unknown, } @@ -42,7 +42,7 @@ impl CodexCatalogCapabilityEvidence { } } -/// Codex 目录中允许进入控制面的能力证据。 +/// Codex 目录中允许进入控制面的能力证据 #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct CodexCatalogCapabilities { responses_api: CodexCatalogCapabilityEvidence, @@ -57,62 +57,62 @@ pub struct CodexCatalogCapabilities { } impl CodexCatalogCapabilities { - /// 返回 Responses API 支持证据。 + /// 返回 Responses API 支持证据 #[must_use] pub const fn responses_api(&self) -> CodexCatalogCapabilityEvidence { self.responses_api } - /// 返回 reasoning 支持证据。 + /// 返回 reasoning 支持证据 #[must_use] pub const fn reasoning(&self) -> CodexCatalogCapabilityEvidence { self.reasoning } - /// 返回并行工具调用支持证据。 + /// 返回并行工具调用支持证据 #[must_use] pub const fn parallel_tool_calls(&self) -> CodexCatalogCapabilityEvidence { self.parallel_tool_calls } - /// 返回文本输入支持证据。 + /// 返回文本输入支持证据 #[must_use] pub const fn text_input(&self) -> CodexCatalogCapabilityEvidence { self.text_input } - /// 返回图片输入支持证据。 + /// 返回图片输入支持证据 #[must_use] pub const fn image_input(&self) -> CodexCatalogCapabilityEvidence { self.image_input } - /// 返回原图 detail 支持证据。 + /// 返回原图 detail 支持证据 #[must_use] pub const fn image_detail_original(&self) -> CodexCatalogCapabilityEvidence { self.image_detail_original } - /// 返回 Web search 支持证据。 + /// 返回 Web search 支持证据 #[must_use] pub const fn web_search(&self) -> CodexCatalogCapabilityEvidence { self.web_search } - /// 返回 verbosity 支持证据。 + /// 返回 verbosity 支持证据 #[must_use] pub const fn verbosity(&self) -> CodexCatalogCapabilityEvidence { self.verbosity } - /// 返回上游明确列出的 reasoning effort。 + /// 返回上游明确列出的 reasoning effort #[must_use] pub fn reasoning_efforts(&self) -> &[String] { &self.reasoning_efforts } } -/// Codex 目录中明确声明的模型限制。 +/// Codex 目录中明确声明的模型限制 #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct CodexCatalogLimits { context_window_tokens: Option, @@ -120,20 +120,20 @@ pub struct CodexCatalogLimits { } impl CodexCatalogLimits { - /// 返回正常上下文窗口;缺失表示未知。 + /// 返回正常上下文窗口;缺失表示未知 #[must_use] pub const fn context_window_tokens(&self) -> Option { self.context_window_tokens } - /// 返回上游允许覆盖到的最大上下文窗口;缺失表示未知。 + /// 返回上游允许覆盖到的最大上下文窗口;缺失表示未知 #[must_use] pub const fn max_context_window_tokens(&self) -> Option { self.max_context_window_tokens } } -/// Codex 目录中允许持久化的原始元数据白名单。 +/// Codex 目录中允许持久化的原始元数据白名单 #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct CodexCatalogMetadata { description: Option, @@ -143,47 +143,47 @@ pub struct CodexCatalogMetadata { } impl CodexCatalogMetadata { - /// 返回安全的模型说明。 + /// 返回安全的模型说明 #[must_use] pub fn description(&self) -> Option<&str> { self.description.as_deref() } - /// 返回上游排序优先级。 + /// 返回上游排序优先级 #[must_use] pub const fn priority(&self) -> Option { self.priority } - /// 返回上游 picker 可见性。 + /// 返回上游 picker 可见性 #[must_use] pub const fn visibility(&self) -> Option { self.visibility } - /// 返回上游明确声明的服务档位;缺失时为空,不根据模型名补齐。 + /// 返回上游明确声明的服务档位;缺失时为空,不根据模型名补齐 #[must_use] pub fn service_tiers(&self) -> &[ModelServiceTier] { &self.service_tiers } } -/// Codex 官方 picker 可见性。 +/// Codex 官方 picker 可见性 #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] #[serde(rename_all = "lowercase")] pub enum CodexCatalogVisibility { - /// 正常列出。 + /// 正常列出 List, - /// 对普通 picker 隐藏。 + /// 对普通 picker 隐藏 Hide, - /// 不进入 picker。 + /// 不进入 picker None, - /// 上游新增的可见性值;保留模型条目但不作本地可见性判断。 + /// 上游新增的可见性值;保留模型条目但不作本地可见性判断 #[serde(other)] Unknown, } -/// 一个已完整校验的 Codex 真实模型。 +/// 一个已完整校验的 Codex 真实模型 #[derive(Debug, Clone, PartialEq, Eq)] pub struct CodexCatalogModel { document: RawJsonPayload, @@ -195,44 +195,44 @@ pub struct CodexCatalogModel { } impl CodexCatalogModel { - /// 完整原生对象只用于客户端目录,不写入控制面画像或日志。 + /// 完整原生对象只用于客户端目录,不写入控制面画像或日志 #[must_use] pub const fn document(&self) -> &RawJsonPayload { &self.document } - /// 返回实际写入上游请求的模型 slug。 + /// 返回实际写入上游请求的模型 slug #[must_use] pub const fn request_model(&self) -> &UpstreamModelId { &self.request_model } - /// 返回上游展示名。 + /// 返回上游展示名 #[must_use] pub fn display_name(&self) -> &str { &self.display_name } - /// 返回能力证据。 + /// 返回能力证据 #[must_use] pub const fn capabilities(&self) -> &CodexCatalogCapabilities { &self.capabilities } - /// 返回明确限制。 + /// 返回明确限制 #[must_use] pub const fn limits(&self) -> &CodexCatalogLimits { &self.limits } - /// 返回白名单元数据。 + /// 返回白名单元数据 #[must_use] pub const fn metadata(&self) -> &CodexCatalogMetadata { &self.metadata } } -/// 一次完整成功的 Codex 远端模型快照。 +/// 一次完整成功的 Codex 远端模型快照 #[derive(Debug, Clone, PartialEq, Eq)] pub struct CodexModelCatalogSnapshot { models: Vec, @@ -240,50 +240,50 @@ pub struct CodexModelCatalogSnapshot { } impl CodexModelCatalogSnapshot { - /// 返回本轮全部模型。 + /// 返回本轮全部模型 #[must_use] pub fn models(&self) -> &[CodexCatalogModel] { &self.models } - /// 返回经过语法和长度白名单校验的 HTTP ETag。 + /// 返回经过语法和长度白名单校验的 HTTP ETag #[must_use] pub fn etag(&self) -> Option<&str> { self.etag.as_deref() } } -/// Codex 模型目录不满足完整快照约束。 +/// Codex 模型目录不满足完整快照约束 #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] pub enum CodexModelCatalogError { - /// 响应超过硬上限。 + /// 响应超过硬上限 #[error("Codex model catalog response exceeds the byte limit")] ResponseTooLarge, - /// 顶层不是唯一受支持的 `{models:[...]}` wire。 + /// 顶层不是唯一受支持的 `{models:[...]}` wire #[error("Codex model catalog response violates the official wire contract")] InvalidWire, - /// 目录没有任何模型。 + /// 目录没有任何模型 #[error("Codex model catalog snapshot is empty")] EmptySnapshot, - /// 目录模型数量超过安全上限。 + /// 目录模型数量超过安全上限 #[error("Codex model catalog contains too many models")] TooManyModels, - /// 实际请求模型 slug 不合法。 + /// 实际请求模型 slug 不合法 #[error("Codex model catalog contains an invalid request model slug")] InvalidModelSlug, - /// 同一实际请求模型重复出现。 + /// 同一实际请求模型重复出现 #[error("Codex model catalog contains a duplicate request model slug")] DuplicateModelSlug, - /// 展示或白名单元数据不合法。 + /// 展示或白名单元数据不合法 #[error("Codex model catalog contains invalid public metadata")] InvalidMetadata, - /// 模型限制不是明确的正整数。 + /// 模型限制不是明确的正整数 #[error("Codex model catalog contains invalid model limits")] InvalidLimits, - /// 能力声明不合法或自相矛盾。 + /// 能力声明不合法或自相矛盾 #[error("Codex model catalog contains invalid capability evidence")] InvalidCapabilities, - /// ETag 不属于允许持久化的安全格式。 + /// ETag 不属于允许持久化的安全格式 #[error("Codex model catalog ETag is invalid")] InvalidEtag, } @@ -334,11 +334,11 @@ enum CodexInputModalityWire { Unknown, } -/// 解析唯一受支持的 Codex 官方完整快照。 +/// 解析唯一受支持的 Codex 官方完整快照 /// /// # Errors /// -/// 任一条目、重复 slug 或 ETag 不合法时整轮失败;未知字段保留在原生正文中。 +/// 任一条目、重复 slug 或 ETag 不合法时整轮失败;未知字段保留在原生正文中 pub fn parse_codex_model_catalog( body: &[u8], etag: Option<&str>, @@ -402,7 +402,7 @@ fn normalize_model( document: RawJsonPayload, protocol: OpenAiUpstreamProtocol, ) -> Result { - // 协商为 Codex 对象不改变 Responses API 的模型 ID 合同,命名空间和微调模型仍有效。 + // 协商为 Codex 对象不改变 Responses API 的模型 ID 合同,命名空间和微调模型仍有效 if wire.slug.trim() != wire.slug || (protocol == OpenAiUpstreamProtocol::Codex && !valid_model_slug(&wire.slug)) { @@ -414,7 +414,7 @@ fn normalize_model( if let Some(description) = wire.description.as_deref() { validate_public_text(description, MAX_DESCRIPTION_BYTES, true)?; } - // name 供客户端生成命令,id 才是请求选档值;不能从命令名推导或改写 id。 + // name 供客户端生成命令,id 才是请求选档值;不能从命令名推导或改写 id let service_tiers = wire .service_tiers .into_iter() @@ -601,7 +601,7 @@ fn validate_etag(value: &str) -> Result { Ok(value.to_owned()) } -/// 标准 API 模型 ID 遵循核心 ID 合同,不套用 Codex slug 语法或推断原生画像。 +/// 标准 API 模型 ID 遵循核心 ID 合同,不套用 Codex slug 语法或推断原生画像 pub(crate) fn parse_api_model_catalog( body: &[u8], etag: Option<&str>, @@ -620,7 +620,7 @@ pub(crate) fn parse_api_model_catalog( } let value: serde_json::Value = serde_json::from_slice(body).map_err(|_| CodexModelCatalogError::InvalidWire)?; - // 部分 Responses 上游按客户端版本返回完整 Codex 目录;不能降格为只有 ID 的画像。 + // 部分 Responses 上游按客户端版本返回完整 Codex 目录;不能降格为只有 ID 的画像 if value.get("models").is_some() { return parse_native_model_catalog(body, etag, OpenAiUpstreamProtocol::ResponsesApi); } diff --git a/backend/crates/providers/openai/src/transport/client.rs b/backend/crates/providers/openai/src/transport/client.rs index e2e81fc49..7d3243318 100644 --- a/backend/crates/providers/openai/src/transport/client.rs +++ b/backend/crates/providers/openai/src/transport/client.rs @@ -1,4 +1,4 @@ -//! Codex HTTP/SSE 上游客户端、请求头构造、TLS 与自定义 CA。 +//! Codex HTTP/SSE 上游客户端、请求头构造、TLS 与自定义 CA use std::{ collections::HashMap, @@ -49,7 +49,7 @@ const X_CODEX_WS_STREAM_REQUEST_START_MS_CLIENT_METADATA_KEY: &str = type ReqwestClientCacheKey = (Option, String, Duration); type ReqwestClientCache = Mutex>; -/// 构建复用连接池的 Codex HTTP 客户端。 +/// 构建复用连接池的 Codex HTTP 客户端 pub fn build_reqwest_client() -> Result { build_account_http_client("", None) } @@ -82,7 +82,7 @@ pub(super) fn build_account_http_client_with_timeout( return Ok(client.clone()); } - // 连接池与 TCP、HTTP/2 保活沿用官方 Core 的 reqwest 默认值。 + // 连接池与 TCP、HTTP/2 保活沿用官方 Core 的 reqwest 默认值 let mut builder = Client::builder() .no_proxy() .redirect(reqwest::redirect::Policy::none()) @@ -121,7 +121,7 @@ fn egress_key(account_id: &str, proxy: Option<&gateway_core::account::OutboundPr // 错误类型 // --------------------------------------------------------------------------- -/// 当前 Responses 请求最终失败时可原样返回给客户端的上游 HTTP 响应。 +/// 当前 Responses 请求最终失败时可原样返回给客户端的上游 HTTP 响应 #[derive(Clone, PartialEq, Eq)] pub struct CodexClientVisibleUpstreamResponse { status: u16, @@ -193,18 +193,18 @@ impl fmt::Debug for CodexClientVisibleUpstreamResponse { } } -/// Codex 上游 HTTP 客户端错误。 +/// Codex 上游 HTTP 客户端错误 #[derive(Error)] pub enum CodexClientError { #[error("connection recovery budget exhausted")] ConnectionBudgetExhausted, - /// Reqwest 传输失败。 + /// Reqwest 传输失败 #[error("http transport error: {0}")] Http(#[from] reqwest::Error), - /// 非流式 JSON 请求的 Reqwest 传输失败。 + /// 非流式 JSON 请求的 Reqwest 传输失败 #[error("HTTP JSON transport error: {0}")] HttpJson(#[source] reqwest::Error), - /// 已收到错误响应头,但未能读取完整正文;不能作为可透传的 HTTP 原响应。 + /// 已收到错误响应头,但未能读取完整正文;不能作为可透传的 HTTP 原响应 #[error("upstream error response body read failed for status {status}")] ErrorBodyRead { #[source] @@ -214,62 +214,62 @@ pub enum CodexClientError { transport: CodexBackendTransport, transport_metrics: Box, }, - /// 自定义 CA 构建失败。 + /// 自定义 CA 构建失败 #[error("custom CA transport error: {0}")] CustomCa(#[from] CustomCaError), - /// 请求头名字无效。 + /// 请求头名字无效 #[error("invalid request header name: {0}")] InvalidHeaderName(#[from] reqwest::header::InvalidHeaderName), - /// 请求头值无效。 + /// 请求头值无效 #[error("invalid request header value: {0}")] InvalidHeaderValue(#[from] reqwest::header::InvalidHeaderValue), - /// 中间件业务头试图覆盖 Provider 已构造的受管头。 - /// SSE 响应解析失败。 + /// 中间件业务头试图覆盖 Provider 已构造的受管头 + /// SSE 响应解析失败 #[error("invalid upstream SSE response: {0}")] InvalidSse(#[from] SseError), - /// 模型目录不是一个完整、安全的官方快照。 + /// 模型目录不是一个完整、安全的官方快照 #[error("invalid Codex model catalog: {0}")] ModelCatalog(#[from] super::catalog::CodexModelCatalogError), - /// HTTP/SSE 上游在空闲窗口内没有发送任何数据。 + /// HTTP/SSE 上游在空闲窗口内没有发送任何数据 #[error("upstream HTTP/SSE stream idle for {timeout:?}")] StreamIdleTimeout { - /// 相邻数据块允许的最大空闲时间。 + /// 相邻数据块允许的最大空闲时间 timeout: Duration, }, - /// WebSocket 请求编码失败。 + /// WebSocket 请求编码失败 #[error("failed to encode websocket request: {0}")] WebSocketEncode(#[source] serde_json::Error), - /// 上游请求体 JSON 序列化失败。 + /// 上游请求体 JSON 序列化失败 #[error("failed to encode upstream request body: {0}")] RequestBodyEncode(#[source] serde_json::Error), - /// 上游请求体 zstd 压缩失败。 + /// 上游请求体 zstd 压缩失败 #[error("failed to compress upstream request body: {0}")] RequestCompression(#[source] std::io::Error), - /// WebSocket 请求失败。 + /// WebSocket 请求失败 #[error("websocket request failed: {0}")] WebSocket(#[from] CodexWebSocketExchangeError), - /// 上游返回非成功响应。 + /// 上游返回非成功响应 #[error("upstream returned status {status}")] Upstream { - /// 上游状态码。 + /// 上游状态码 status: StatusCode, - /// 上游错误体。 + /// 上游错误体 body: String, - /// 仅供当前 Responses 请求最终失败时原样返回的响应。 + /// 仅供当前 Responses 请求最终失败时原样返回的响应 client_response: Option>, - /// 推导出的重试秒数。 + /// 推导出的重试秒数 retry_after_seconds: Option, - /// 上游诊断元数据。 + /// 上游诊断元数据 diagnostics: Box, - /// 上游透传的 `set-cookie` 列表。 + /// 上游透传的 `set-cookie` 列表 set_cookie_headers: Vec, - /// 上游错误响应携带的限流头。 + /// 上游错误响应携带的限流头 rate_limit_headers: Vec<(String, String)>, - /// 实际收到该上游响应的 transport。 + /// 实际收到该上游响应的 transport transport: CodexBackendTransport, - /// 错误响应前已经确认的 transport 与 HTTP 阶段事实。 + /// 错误响应前已经确认的 transport 与 HTTP 阶段事实 transport_metrics: Box, - /// 上游拒绝发生时业务 payload 的发送阶段。 + /// 上游拒绝发生时业务 payload 的发送阶段 send_phase: CodexUpstreamSendPhase, }, } @@ -334,7 +334,7 @@ impl fmt::Debug for CodexClientError { } impl CodexClientError { - /// 返回错误实际发生的 transport;请求编码等本地错误没有 transport。 + /// 返回错误实际发生的 transport;请求编码等本地错误没有 transport pub fn transport(&self) -> Option { match self { Self::Http(_) @@ -400,10 +400,10 @@ impl CodexClientError { } } -/// Codex 客户端结果类型。 +/// Codex 客户端结果类型 pub type CodexClientResult = Result; -/// Codex SSE 字节流。 +/// Codex SSE 字节流 pub type CodexBackendSseStream = Pin> + Send + 'static>>; @@ -411,7 +411,7 @@ pub type CodexBackendSseStream = // 请求上下文与响应类型 // --------------------------------------------------------------------------- -/// 账号亲和决策传给 transport 的不可变遥测快照。 +/// 账号亲和决策传给 transport 的不可变遥测快照 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct CodexAccountSelectionTelemetry<'a> { affinity_hit: bool, @@ -455,44 +455,44 @@ impl<'a> CodexAccountSelectionTelemetry<'a> { } } -/// 单次 Codex 上游请求的上下文。 +/// 单次 Codex 上游请求的上下文 #[derive(Clone, Copy)] pub struct CodexRequestContext<'a> { - /// 显式传递的请求诊断上下文,跨连接任务共享。 + /// 显式传递的请求诊断上下文,跨连接任务共享 pub trace: Option<&'a gateway_core::diagnostics::TraceContext>, - /// Provider 已构造并脱敏持有的完整 Authorization 值。 + /// Provider 已构造并脱敏持有的完整 Authorization 值 pub authorization: &'a str, - /// ChatGPT 账号 ID。 + /// ChatGPT 账号 ID pub account_id: Option<&'a str>, - /// 代理请求 ID。 + /// 代理请求 ID pub request_id: &'a str, - /// 当前账号同一 turn 内的 opaque sticky-routing 状态。 + /// 当前账号同一 turn 内的 opaque sticky-routing 状态 pub turn_state: Option<&'a str>, - /// 客户端 turn metadata;其中 installation ID 已按当前账号处理。 + /// 客户端 turn metadata;其中 installation ID 已按当前账号处理 pub turn_metadata: Option<&'a str>, - /// x-codex-beta-features 头值。 + /// x-codex-beta-features 头值 pub beta_features: Option<&'a str>, - /// x-responsesapi-include-timing-metrics 头值。 + /// x-responsesapi-include-timing-metrics 头值 pub include_timing_metrics: Option<&'a str>, - /// 下游 `version` 扩展头;存在时上游值由 Desktop 版本画像统一生成。 + /// 下游 `version` 扩展头;存在时上游值由 Desktop 版本画像统一生成 pub version: Option<&'a str>, - /// 客户端 window ID。 + /// 客户端 window ID pub codex_window_id: Option<&'a str>, - /// 客户端 parent thread ID。 + /// 客户端 parent thread ID pub parent_thread_id: Option<&'a str>, - /// cookie 头。 + /// cookie 头 pub cookie_header: Option<&'a str>, - /// 当前账号稳定派生的 installation ID。 + /// 当前账号稳定派生的 installation ID pub installation_id: Option<&'a str>, - /// 客户端 session ID。 + /// 客户端 session ID pub session_id: Option<&'a str>, - /// 客户端 thread ID。 + /// 客户端 thread ID pub thread_id: Option<&'a str>, - /// 客户端逻辑 request ID;缺失时使用代理请求 ID。 + /// 客户端逻辑 request ID;缺失时使用代理请求 ID pub client_request_id: Option<&'a str>, - /// 客户端 turn ID。 + /// 客户端 turn ID pub turn_id: Option<&'a str>, - /// 账号亲和选择结果;仅用于结构化遥测,不参与 transport 决策。 + /// 账号亲和选择结果;仅用于结构化遥测,不参与 transport 决策 pub account_selection: CodexAccountSelectionTelemetry<'a>, } @@ -552,18 +552,18 @@ impl fmt::Debug for CodexRequestContext<'_> { } } -/// Codex Responses 实际使用的上游传输。 +/// Codex Responses 实际使用的上游传输 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CodexBackendTransport { - /// HTTP SSE 传输。 + /// HTTP SSE 传输 HttpSse, - /// 非流式 HTTP JSON 传输。 + /// 非流式 HTTP JSON 传输 HttpJson, - /// WebSocket 传输。 + /// WebSocket 传输 WebSocket, } -/// transport owner 最终做出的稳定决策。 +/// transport owner 最终做出的稳定决策 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CodexTransportDecision { HttpRequired, @@ -593,7 +593,7 @@ impl CodexTransportDecision { } } -/// transport 决策与握手阶段观测值。 +/// transport 决策与握手阶段观测值 #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct CodexTransportMetrics { pub decision: Option, @@ -604,55 +604,55 @@ pub struct CodexTransportMetrics { pub http_version: Option, } -/// 响应头之后在 live 流中采集的结构化限流更新。 +/// 响应头之后在 live 流中采集的结构化限流更新 pub type CodexRateLimitUpdates = CodexWebSocketRateLimitUpdates; -/// 响应头之后在 live 流中采集的请求级 metadata 更新。 +/// 响应头之后在 live 流中采集的请求级 metadata 更新 pub type CodexResponseMetadataUpdates = CodexWebSocketResponseMetadataUpdates; -/// Codex Responses 上游 live SSE 响应。 +/// Codex Responses 上游 live SSE 响应 pub struct CodexBackendStreamingResponse { - /// 上游 SSE 字节流。 + /// 上游 SSE 字节流 pub body: CodexBackendSseStream, - /// 实际使用的上游传输。 + /// 实际使用的上游传输 pub transport: CodexBackendTransport, - /// WebSocket 响应所绑定的连接;HTTP transport 为 `None`。 + /// WebSocket 响应所绑定的连接;HTTP transport 为 `None` pub websocket_connection_id: Option, - /// 响应头或 metadata 事件里最先确认的 turn state。 + /// 响应头或 metadata 事件里最先确认的 turn state pub turn_state: Option, - /// 上游透传的 `set-cookie` 列表。 + /// 上游透传的 `set-cookie` 列表 pub set_cookie_headers: Vec, - /// 上游透传的限流头。 + /// 上游透传的限流头 pub rate_limit_headers: Vec<(String, String)>, - /// live stream 期间捕获的结构化限流更新。 + /// live stream 期间捕获的结构化限流更新 pub rate_limit_updates: Option, - /// live stream 期间捕获的请求级 metadata 更新。 + /// live stream 期间捕获的请求级 metadata 更新 pub response_metadata_updates: Option, - /// WebSocket 连接池决策。 + /// WebSocket 连接池决策 pub websocket_pool_decision: Option, - /// 上游诊断元数据。 + /// 上游诊断元数据 pub diagnostics: CodexUpstreamDiagnostics, - /// 安全响应元数据。 + /// 安全响应元数据 pub response_metadata: CodexResponseMetadata, - /// 传输选择与低延迟阶段耗时。 + /// 传输选择与低延迟阶段耗时 pub transport_metrics: CodexTransportMetrics, - /// terminal completed 后是否由池中 WebSocket 保留 connection-local continuation。 + /// terminal completed 后是否由池中 WebSocket 保留 connection-local continuation pub connection_local_continuation: bool, } -/// Codex 上游非流式 JSON 响应。 +/// Codex 上游非流式 JSON 响应 pub struct CodexBackendJsonResponse { - /// 未经解析或重编码的完整响应正文。 + /// 未经解析或重编码的完整响应正文 pub body: Bytes, - /// 上游透传的 `set-cookie` 列表。 + /// 上游透传的 `set-cookie` 列表 pub set_cookie_headers: Vec, - /// 上游透传的限流头。 + /// 上游透传的限流头 pub rate_limit_headers: Vec<(String, String)>, - /// 上游诊断元数据。 + /// 上游诊断元数据 pub diagnostics: CodexUpstreamDiagnostics, - /// 已筛选、可交给客户端的响应头。 + /// 已筛选、可交给客户端的响应头 pub response_metadata: CodexResponseMetadata, - /// HTTP 阶段耗时与版本。 + /// HTTP 阶段耗时与版本 pub transport_metrics: CodexTransportMetrics, } @@ -675,7 +675,7 @@ impl OpenAiUpstreamProtocol { } } -/// Codex HTTP/SSE 上游客户端。 +/// Codex HTTP/SSE 上游客户端 #[derive(Clone)] pub struct CodexBackendClient { pub(super) timezone: gateway_core::time::DeploymentTimeZone, @@ -736,7 +736,7 @@ impl CodexBackendClient { if remaining >= UPSTREAM_CONNECT_TIMEOUT { return Ok(self.client.clone()); } - // 按整秒向下取整限制缓存种类;不足一秒不缓存新的 client 配置。 + // 按整秒向下取整限制缓存种类;不足一秒不缓存新的 client 配置 let timeout = Duration::from_secs(remaining.as_secs()); if timeout.is_zero() { return Err(CodexClientError::ConnectionBudgetExhausted); @@ -748,14 +748,14 @@ impl CodexBackendClient { )?) } - /// 覆盖官方账号接口基址,用于隔离上游联调与协议测试。 + /// 覆盖官方账号接口基址,用于隔离上游联调与协议测试 #[must_use] pub fn with_official_base_url(mut self, official_base_url: impl Into) -> Self { self.official_base_url = official_base_url.into().trim_end_matches('/').to_string(); self } - /// 自定义账号路由明确不存在时才回退一次;调用方继续解释最后一次响应。 + /// 自定义账号路由明确不存在时才回退一次;调用方继续解释最后一次响应 pub(super) async fn send_account_request( &self, request: reqwest::RequestBuilder, @@ -772,7 +772,7 @@ impl CodexBackendClient { "custom account endpoint returned 404; falling back to official endpoint" ); drop(response); - // 消费可能已在回退端完成,传输失败不能被先前的 404 覆盖。 + // 消费可能已在回退端完成,传输失败不能被先前的 404 覆盖 fallback.send().await.map_err(CodexClientError::HttpJson) } @@ -818,7 +818,7 @@ impl CodexBackendClient { } } -/// 已完成账号级 opening 准备、但尚未发送 payload 的 transport。 +/// 已完成账号级 opening 准备、但尚未发送 payload 的 transport pub(crate) struct PreparedResponseTransport { pub(super) requirement: TransportRequirement, pub(super) route: PreparedResponseRoute, @@ -886,7 +886,7 @@ pub(super) fn retry_after_seconds(headers: &HeaderMap, body: Option<&str>) -> Op .or_else(|| body.and_then(retry_after_seconds_from_body)) } -/// 解析 RFC 7231 `Retry-After` 值:delay-seconds 或 HTTP-date,统一换算为剩余秒数。 +/// 解析 RFC 7231 `Retry-After` 值:delay-seconds 或 HTTP-date,统一换算为剩余秒数 pub(super) fn parse_retry_after(value: &str) -> Option { let value = value.trim(); if let Ok(seconds) = value.parse::() { @@ -898,7 +898,7 @@ pub(super) fn parse_retry_after(value: &str) -> Option { .filter(|seconds| *seconds > 0) } -/// IMF-fixdate 优先;RFC 7231 要求接收方兼容过时的 RFC 850 与 asctime 形式。 +/// IMF-fixdate 优先;RFC 7231 要求接收方兼容过时的 RFC 850 与 asctime 形式 fn parse_http_date(value: &str) -> Option> { if let Ok(date) = DateTime::parse_from_rfc2822(value) { return Some(date.with_timezone(&Utc)); @@ -978,7 +978,7 @@ pub(super) async fn read_error_response_body( pub(super) fn websocket_upstream_request(request: &CodexResponsesRequest) -> CodexResponsesRequest { let mut request = request.clone(); - // 上游通过流式事件执行,下游仍按客户端原来的偏好返回响应。 + // 上游通过流式事件执行,下游仍按客户端原来的偏好返回响应 if !request.stream() { request .body_mut() diff --git a/backend/crates/providers/openai/src/transport/client_json.rs b/backend/crates/providers/openai/src/transport/client_json.rs index 909f49944..433c0feda 100644 --- a/backend/crates/providers/openai/src/transport/client_json.rs +++ b/backend/crates/providers/openai/src/transport/client_json.rs @@ -1,4 +1,4 @@ -//! Codex 非流式 JSON 上游透明传输。 +//! Codex 非流式 JSON 上游透明传输 use std::time::Instant; @@ -19,7 +19,7 @@ use super::{ }; impl CodexBackendClient { - /// 向固定 Provider 端点发送一次原始 JSON;请求与成功响应正文均不经过 serde。 + /// 向固定 Provider 端点发送一次原始 JSON;请求与成功响应正文均不经过 serde pub(crate) async fn post_raw_json( &self, endpoint_path: &'static str, @@ -27,7 +27,7 @@ impl CodexBackendClient { image_turn_id: Option<&str>, context: CodexRequestContext<'_>, ) -> CodexClientResult { - // Provider 端点以 Codex 路径标识;API Key 在自己的 API 前缀下使用对应相对路径。 + // Provider 端点以 Codex 路径标识;API Key 在自己的 API 前缀下使用对应相对路径 let endpoint_path = if self.protocol == super::client::OpenAiUpstreamProtocol::ResponsesApi { endpoint_path diff --git a/backend/crates/providers/openai/src/transport/client_sse.rs b/backend/crates/providers/openai/src/transport/client_sse.rs index e920b288c..8c44aef4c 100644 --- a/backend/crates/providers/openai/src/transport/client_sse.rs +++ b/backend/crates/providers/openai/src/transport/client_sse.rs @@ -1,3 +1,5 @@ +//! Codex Responses 流式传输编排、HTTP SSE 解码与 WebSocket 连接复用 + use gateway_core::diagnostics::{StreamCapture, StreamFormat, TraceContext, diagnostic_json}; use std::{sync::Arc, time::Instant}; @@ -50,13 +52,13 @@ impl CodexBackendClient { &self.profile } - /// 请求只持有自己的画像副本;连接池和 HTTP client 继续共享既有资源。 + /// 请求只持有自己的画像副本;连接池和 HTTP client 继续共享既有资源 pub fn with_request_profile(mut self, profile: super::profile::CodexWireProfile) -> Self { self.profile = CodexWireProfileState::new(profile); self } - /// 构造客户端。 + /// 构造客户端 pub fn new( client: Client, base_url: impl Into, @@ -82,13 +84,13 @@ impl CodexBackendClient { } } - /// 为 Responses WebSocket 请求启用连接池。 + /// 为 Responses WebSocket 请求启用连接池 pub fn with_websocket_pool(mut self, pool: Arc) -> Self { self.websocket_pool = Some(pool); self } - /// 附加当前 attempt 经 Core 复核的业务请求头。 + /// 附加当前 attempt 经 Core 复核的业务请求头 #[must_use] pub(crate) fn with_middleware_headers( mut self, @@ -98,14 +100,14 @@ impl CodexBackendClient { self } - /// 驱逐指定账号的 Responses WebSocket 池连接。 + /// 驱逐指定账号的 Responses WebSocket 池连接 pub async fn evict_websocket_account(&self, account_id: &str) { if let Some(pool) = &self.websocket_pool { pool.evict_account(account_id).await; } } - /// 发送 Responses SSE 请求并返回 live SSE 流(HTTP SSE fallback)。 + /// 发送 Responses SSE 请求并返回 live SSE 流(HTTP SSE fallback) pub(crate) async fn create_response_stream_http_sse( &self, upstream_request: &CodexResponsesRequest, @@ -113,10 +115,11 @@ impl CodexBackendClient { ) -> CodexClientResult { let headers = self.request_headers_for_http_response(upstream_request, context)?; let headers_started_at = Instant::now(); - // OAuth 请求遵循 Codex 压缩合同;API Key 上游使用普通 JSON。 + // OAuth 请求遵循 Codex 压缩合同;API Key 上游使用普通 JSON // Codex 上游只交付 SSE;即使下游请求 `stream: false`,也要上游流式执行, - // 再由 API 层收集 canonical events 并返回完整 JSON。不能把下游的传输偏好 - // 直接透传给 Codex,否则上游会以 400 拒绝非流式请求。 + // 再由 API 层收集 canonical events 并返回完整 JSON + // 不能把下游的传输偏好 + // 直接透传给 Codex,否则上游会以 400 拒绝非流式请求 let mut upstream_body = upstream_request.body().clone(); upstream_body.insert("stream".to_owned(), serde_json::Value::Bool(true)); let body = @@ -249,7 +252,7 @@ impl CodexBackendClient { .await } - /// 在发送 payload 前完成 transport 选择和可取消的 WebSocket opening。 + /// 在发送 payload 前完成 transport 选择和可取消的 WebSocket opening #[doc(hidden)] pub(crate) async fn prepare_response_transport_with_pool_account( &self, @@ -297,7 +300,7 @@ impl CodexBackendClient { .map(|(name, value)| (name.as_str(), value.as_bytes())), ); // 审计未启用时跳过 artifact 构造:payload 快照会深拷贝整个请求 body, - // 且位于首字节前的关键路径上。 + // 且位于首字节前的关键路径上 if websocket_audit_dir().is_some() { let artifact = websocket_audit_artifact_from_attempt( &websocket_request, @@ -526,7 +529,7 @@ impl CodexBackendClient { Some(key) } - /// 客户端目录按调用方版本协商;后台目录仍使用经过核验的服务端画像版本。 + /// 客户端目录按调用方版本协商;后台目录仍使用经过核验的服务端画像版本 pub async fn fetch_models_with_context( &self, context: CodexRequestContext<'_>, @@ -585,11 +588,11 @@ impl CodexBackendClient { } } -/// 首个可投递帧前的交付边界结果。 +/// 首个可投递帧前的交付边界结果 enum DeliveryBoundary { - /// 已越过边界,可开始向下游投递。 + /// 已越过边界,可开始向下游投递 Ready, - /// 首个可投递帧是上游连接寿命限制错误。 + /// 首个可投递帧是上游连接寿命限制错误 ConnectionLimitReached(Box), } diff --git a/backend/crates/providers/openai/src/transport/connection.rs b/backend/crates/providers/openai/src/transport/connection.rs index 846b06aea..61e2c32d0 100644 --- a/backend/crates/providers/openai/src/transport/connection.rs +++ b/backend/crates/providers/openai/src/transport/connection.rs @@ -1,4 +1,4 @@ -//! HTTP 与 WS 共用的建连资源上限,不保留出口健康或冷却状态。 +//! HTTP 与 WS 共用的建连资源上限,不保留出口健康或冷却状态 use std::future::Future; use std::pin::Pin; @@ -8,7 +8,7 @@ use tokio::sync::{Semaphore, SemaphorePermit}; use tower_layer::Layer; use tower_service::Service; -// 为单副本的文件描述符和 TLS 握手保留硬上限;不限制已经建立的长流。 +// 为单副本的文件描述符和 TLS 握手保留硬上限;不限制已经建立的长流 const ACTIVE_CONNECTIONS: usize = 128; const WAITING_CONNECTIONS: usize = 1024; static ACTIVE: OnceLock = OnceLock::new(); @@ -18,7 +18,7 @@ static WAITING: OnceLock = OnceLock::new(); #[error("local connection capacity unavailable")] struct ConnectionAdmissionRejected; -/// 只根据自有类型识别本地容量拒绝,不将任意 IO 文案当作上游故障。 +/// 只根据自有类型识别本地容量拒绝,不将任意 IO 文案当作上游故障 pub(crate) fn is_admission_failure(error: &(dyn std::error::Error + 'static)) -> bool { let mut source = Some(error); while let Some(cause) = source { @@ -39,7 +39,7 @@ fn admission_rejected() -> std::io::Error { std::io::Error::new(std::io::ErrorKind::WouldBlock, ConnectionAdmissionRejected) } -/// 可回退 HTTP 的 WS 快路径不排队,避免本地等待被当作上游 opening 变慢。 +/// 可回退 HTTP 的 WS 快路径不排队,避免本地等待被当作上游 opening 变慢 pub(super) fn try_acquire() -> Result, std::io::Error> { ACTIVE .get_or_init(|| Semaphore::new(ACTIVE_CONNECTIONS)) diff --git a/backend/crates/providers/openai/src/transport/diagnostics.rs b/backend/crates/providers/openai/src/transport/diagnostics.rs index 69209f82d..2460670b7 100644 --- a/backend/crates/providers/openai/src/transport/diagnostics.rs +++ b/backend/crates/providers/openai/src/transport/diagnostics.rs @@ -1,4 +1,4 @@ -//! 在 transport 边界采集的上游响应诊断信息。 +//! 在 transport 边界采集的上游响应诊断信息 use super::client::CodexClientVisibleUpstreamResponse; use super::protocol::responses::ResponsesSseFailure; @@ -75,25 +75,25 @@ const PERSISTABLE_UPSTREAM_CODES: &[&str] = &[ "workspace_owner_usage_limit_reached", ]; -/// 上游拒绝相对业务 payload 的发送阶段。 +/// 上游拒绝相对业务 payload 的发送阶段 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CodexUpstreamSendPhase { - /// DNS/TCP/TLS/WS opening 阶段,业务 payload 尚未发送。 + /// DNS/TCP/TLS/WS opening 阶段,业务 payload 尚未发送 BeforePayload, - /// 上游已收到业务 payload,并返回了明确拒绝。 + /// 上游已收到业务 payload,并返回了明确拒绝 AfterPayload, - /// 无法证明 payload 是否到达上游。 + /// 无法证明 payload 是否到达上游 Ambiguous, } -/// Transport 边界对上游失败的账号级分类。 +/// Transport 边界对上游失败的账号级分类 #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum CodexFailureCategory { ModelUnsupported, CredentialExpired, IdentityVerificationRequired, Banned, - /// 当前用量窗口已经耗尽,但存在自动恢复的重置时间。 + /// 当前用量窗口已经耗尽,但存在自动恢复的重置时间 UsageLimitExhausted, RateLimited, QuotaExhausted, @@ -102,24 +102,24 @@ pub enum CodexFailureCategory { InvalidRequest, PermissionDenied, Timeout, - /// 模型容量暂时不足,只影响当前请求,不证明账号健康异常。 + /// 模型容量暂时不足,只影响当前请求,不证明账号健康异常 CapacityUnavailable, Unavailable, Transport, } -/// 一次已分类的上游拒绝及其可持久化事实。 +/// 一次已分类的上游拒绝及其可持久化事实 pub struct CodexUpstreamFailure { pub(crate) status: Option, pub(crate) code: Option, pub(crate) client_message: Option, pub(crate) client_code: Option, pub(crate) client_error_type: Option, - /// HTTP 错误正文或 SSE error/response.failed 的原始 JSON data。 + /// HTTP 错误正文或 SSE error/response.failed 的原始 JSON data pub(crate) raw_body: String, pub(crate) client_response: Option>, pub(crate) identity_error_code: Option, - /// 官方 `usage_limit_reached` 错误体携带的窗口重置时间(Unix 秒)。 + /// 官方 `usage_limit_reached` 错误体携带的窗口重置时间(Unix 秒) pub(crate) usage_limit_resets_at: Option, pub(crate) retry_after_seconds: Option, pub(crate) request_id: Option, @@ -130,7 +130,7 @@ pub struct CodexUpstreamFailure { } impl CodexUpstreamFailure { - /// 用完整 HTTP 响应事实构造并分类一次上游拒绝。 + /// 用完整 HTTP 响应事实构造并分类一次上游拒绝 #[expect( clippy::too_many_arguments, reason = "the classifier consumes one flat set of transport response facts" @@ -222,13 +222,13 @@ impl CodexUpstreamFailure { } } - /// 返回账号级失败分类。 + /// 返回账号级失败分类 #[must_use] pub const fn category(&self) -> CodexFailureCategory { self.category } - /// 返回该拒绝是否允许换号重放。 + /// 返回该拒绝是否允许换号重放 #[must_use] pub const fn replay_is_safe(&self) -> bool { match self.send_phase { @@ -342,7 +342,7 @@ impl CodexUpstreamDiagnostics { if !UPSTREAM_REQUEST_ID_HEADERS.contains(&name.as_str()) { continue; } - // 与官方 wrapped error 一致,只接受合法 HTTP 标量头;任意对象和数组不作 ID。 + // 与官方 wrapped error 一致,只接受合法 HTTP 标量头;任意对象和数组不作 ID let value = match value { Value::String(value) => value.clone(), Value::Number(_) | Value::Bool(_) => value.to_string(), @@ -431,7 +431,7 @@ fn classify_upstream_failure( let message = fields.message.to_ascii_lowercase(); let body = body.to_ascii_lowercase(); - // 容量拒绝可能带 400/429/503;仅用结构化错误字段识别,不能扫描任意正文。 + // 容量拒绝可能带 400/429/503;仅用结构化错误字段识别,不能扫描任意正文 if is_capacity_error( fields.code.as_deref(), fields.error_type.as_deref(), @@ -441,8 +441,8 @@ fn classify_upstream_failure( } // 与官方 Codex HTTP/WS 路径一致:429 只有结构化 - // `error.type=usage_limit_reached` 才能确认额度窗口耗尽;其余 429 都是临时限流。 - // SSE `response.failed` 的结构化字段形态不同,保留其 code/type 语义单独分类。 + // `error.type=usage_limit_reached` 才能确认额度窗口耗尽;其余 429 都是临时限流 + // SSE `response.failed` 的结构化字段形态不同,保留其 code/type 语义单独分类 if matches!(source, UpstreamFailureSource::HttpResponse) && status == Some(StatusCode::TOO_MANY_REQUESTS) { @@ -453,7 +453,7 @@ fn classify_upstream_failure( }; } - // 官方 Codex 将流内 invalid_prompt 归为请求错误,不依赖 HTTP 状态码。 + // 官方 Codex 将流内 invalid_prompt 归为请求错误,不依赖 HTTP 状态码 if matches!(source, UpstreamFailureSource::SseFailure) && code == "invalid_prompt" { return CodexFailureCategory::InvalidRequest; } @@ -482,7 +482,7 @@ fn classify_upstream_failure( return CodexFailureCategory::Banned; } // message/body 是自由文本,正文里偶然出现 "unauthorized" 不能证明凭证过期; - // 只有结构化 code/type、身份错误头,或配合 401/403 状态的文本才算数。 + // 只有结构化 code/type、身份错误头,或配合 401/403 状态的文本才算数 let structured_auth_signals = [ identity_code.as_str(), identity_authorization.as_str(), @@ -683,7 +683,7 @@ pub(crate) fn is_capacity_error( ) -> bool { let code = normalized(code); let error_type = normalized(error_type); - // 明确额度窗口/账号错误保留原分类,不能被描述中的容量词覆盖。 + // 明确额度窗口/账号错误保留原分类,不能被描述中的容量词覆盖 if !matches!( error_type.as_str(), "" | "server_error" diff --git a/backend/crates/providers/openai/src/transport/downstream/body.rs b/backend/crates/providers/openai/src/transport/downstream/body.rs index 1d90b05c2..e88e20b7f 100644 --- a/backend/crates/providers/openai/src/transport/downstream/body.rs +++ b/backend/crates/providers/openai/src/transport/downstream/body.rs @@ -1,9 +1,9 @@ -//! 下游客户端模型请求正文的 Codex 协议兼容。 +//! 下游客户端模型请求正文的 Codex 协议兼容 use serde_json::{Map, Value, json}; -/// 非官方客户端及跨客户端历史回填的兼容入口;调用方必须已选定 Codex/OAuth 上游。 -/// 只处理已验证被拒绝的字段,不根据客户端品牌推断整份历史是否合法。 +/// 非官方客户端及跨客户端历史回填的兼容入口;调用方必须已选定 Codex/OAuth 上游 +/// 只处理已验证被拒绝的字段,不根据客户端品牌推断整份历史是否合法 pub(crate) fn normalize_non_codex_request_body(body: &mut Map) { let Some(input) = body.get_mut("input").and_then(Value::as_array_mut) else { return; @@ -15,10 +15,10 @@ pub(crate) fn normalize_non_codex_request_body(body: &mut Map) { if item.get("type").and_then(Value::as_str) != Some("reasoning") { continue; } - // SDK 输出项的 status 不属于 Codex reasoning 输入合同;其他项的 status 可能合法。 + // SDK 输出项的 status 不属于 Codex reasoning 输入合同;其他项的 status 可能合法 item.shift_remove("status"); // Codex 只接受空 content 数组;仅在加密历史仍可回填时去掉冗余明文, - // 没有加密内容的历史不自动丢弃,保留给上游明确拒绝。 + // 没有加密内容的历史不自动丢弃,保留给上游明确拒绝 if item .get("encrypted_content") .and_then(Value::as_str) @@ -33,17 +33,17 @@ pub(crate) fn normalize_non_codex_request_body(body: &mut Map) { } } -/// 补齐 Codex 请求缺省字段并适配已确认不兼容的请求形状,不递归清洗业务正文。 +/// 补齐 Codex 请求缺省字段并适配已确认不兼容的请求形状,不递归清洗业务正文 /// -/// 兼容基准是 Codex Core/Desktop 的模型请求,不是公开 OpenAI Responses API。 -/// 未知字段继续透传,不能因官方请求结构中没有某个字段就将其列入过滤规则。 +/// 兼容基准是 Codex Core/Desktop 的模型请求,不是公开 OpenAI Responses API +/// 未知字段继续透传,不能因官方请求结构中没有某个字段就将其列入过滤规则 pub(in crate::transport) fn normalize_codex_request_body(body: &mut Map) { - // 官方 Core/Desktop 显式发送 store=false;仅为缺字段的下游请求补齐,保留显式值。 + // 官方 Core/Desktop 显式发送 store=false;仅为缺字段的下游请求补齐,保留显式值 body.entry("store").or_insert(Value::Bool(false)); // 公开 Responses API 允许 `input` 为字符串(等价于一条 user 文本消息), - // 而 Codex 后端只接受条目数组,否则返回 400 "Input must be a list"。 - // 这里按官方 ResponseItem::Message 的形状展开;其他非数组类型不猜测语义,交给上游判定。 + // 而 Codex 后端只接受条目数组,否则返回 400 "Input must be a list" + // 这里按官方 ResponseItem::Message 的形状展开;其他非数组类型不猜测语义,交给上游判定 if let Some(Value::String(text)) = body.get_mut("input") { let text = std::mem::take(text); body.insert( @@ -57,7 +57,7 @@ pub(in crate::transport) fn normalize_codex_request_body(body: &mut Map) -> bool { .filter(|entry| entry.len() == 2) .filter_map(|entry| entry.first().and_then(Value::as_str)) .filter_map(|name| HeaderName::from_bytes(name.as_bytes()).ok()) - // 未知的扩展头只做出站过滤,不据此修改请求正文。 + // 未知的扩展头只做出站过滤,不据此修改请求正文 .any(|name| is_request_marker(name.as_str())) }) } -// 只删除已观察到的指令开场白,保留其余文字、空白与内容块边界。 +// 只删除已观察到的指令开场白,保留其余文字、空白与内容块边界 fn normalize_instruction_identity(content: &mut Value) { let text = match content { Value::String(text) => Some(text), diff --git a/backend/crates/providers/openai/src/transport/downstream/headers.rs b/backend/crates/providers/openai/src/transport/downstream/headers.rs index f41d3534b..49b1b85be 100644 --- a/backend/crates/providers/openai/src/transport/downstream/headers.rs +++ b/backend/crates/providers/openai/src/transport/downstream/headers.rs @@ -1,38 +1,39 @@ -//! 已识别的非 Codex 环境信息过滤;官方身份和会话规则由 transport 自身维护。 +//! 已识别的非 Codex 环境信息过滤;官方身份和会话规则由 transport 自身维护 -/// 调用方须传入 HeaderName 规范化后的小写名称。 +/// 调用方须传入 HeaderName 规范化后的小写名称 /// /// 此处的“官方”仅指 Codex Core/Desktop 发往 Codex 上游的请求协议, -/// 不包括整个 OpenAI SDK 生态。名单中的字段可能是合法 HTTP 字段, -/// 过滤表示网关不继承下游环境,不代表官方服务端必然拒绝该字段。 -/// 按已知来源命名空间覆盖扩展;其他未知业务头不因源码中未出现而被过滤。 +/// 不包括整个 OpenAI SDK 生态 +/// 名单中的字段可能是合法 HTTP 字段, +/// 过滤表示网关不继承下游环境,不代表官方服务端必然拒绝该字段 +/// 按已知来源命名空间覆盖扩展;其他未知业务头不因源码中未出现而被过滤 pub(super) fn is_non_codex_request_header(name: &str) -> bool { - // Cloudflare 链路信息及 Access 认证不跨到上游;cf-* 不只包含诊断字段。 + // Cloudflare 链路信息及 Access 认证不跨到上游;cf-* 不只包含诊断字段 name.starts_with("cf-") - // 反代记录的是客户端到网关这一段地址、协议和路由。 + // 反代记录的是客户端到网关这一段地址、协议和路由 || name.starts_with("x-forwarded-") // OpenAI 官方 Python/Node SDK 也发送 X-Stainless-*;Pi 的普通 Responses - // 适配通过该 SDK 发送这些环境、版本和重试信息,不作为 Codex 上游画像继承。 + // 适配通过该 SDK 发送这些环境、版本和重试信息,不作为 Codex 上游画像继承 || name.starts_with("x-stainless-") // UA Client Hints 与 Fetch Metadata 是浏览器标准,描述下游浏览器 - // 和页面请求上下文,不能当作网关连接上游时的环境。 + // 和页面请求上下文,不能当作网关连接上游时的环境 || name.starts_with("sec-ch-ua") || name.starts_with("sec-fetch-") || matches!( name, // Forwarded、Via、CDN-Loop 有 RFC 定义;剥离是本应用重建 - // Provider 请求的策略,不是通用 HTTP 代理的协议要求。 + // Provider 请求的策略,不是通用 HTTP 代理的协议要求 "forwarded" | "via" | "cdn-loop" - // Nginx/代理与 Cloudflare 使用的原始访客地址。 + // Nginx/代理与 Cloudflare 使用的原始访客地址 | "x-real-ip" | "true-client-ip" - // 页面来源与引用地址属于下游请求,不继承到上游。 + // 页面来源与引用地址属于下游请求,不继承到上游 | "origin" | "referer" // Pi 普通 Responses 适配使用的会话头别名;协议层已提取其 - // 会话语义,上游按 session-id 输出,这里仅过滤原始 HTTP 头。 + // 会话语义,上游按 session-id 输出,这里仅过滤原始 HTTP 头 | "session_id" ) } diff --git a/backend/crates/providers/openai/src/transport/downstream/mod.rs b/backend/crates/providers/openai/src/transport/downstream/mod.rs index 6d9355db3..1df4df5e6 100644 --- a/backend/crates/providers/openai/src/transport/downstream/mod.rs +++ b/backend/crates/providers/openai/src/transport/downstream/mod.rs @@ -1,5 +1,5 @@ -//! 下游客户端对 Codex Core/Desktop 请求协议的兼容处理。 -//! 账号身份保护、会话规范化和 HTTP 传输规则由对应职责模块维护。 +//! 下游客户端对 Codex Core/Desktop 请求协议的兼容处理 +//! 账号身份保护、会话规范化和 HTTP 传输规则由对应职责模块维护 mod body; mod grok; diff --git a/backend/crates/providers/openai/src/transport/endpoints.rs b/backend/crates/providers/openai/src/transport/endpoints.rs index ab5d0253e..a59d3c102 100644 --- a/backend/crates/providers/openai/src/transport/endpoints.rs +++ b/backend/crates/providers/openai/src/transport/endpoints.rs @@ -1,3 +1,5 @@ +//! Codex 上游端点常量、基础地址校验与请求 URL 构造 + /// `/codex/responses` pub const CODEX_RESPONSES_PATH: &str = "/codex/responses"; /// `/codex/images/generations` @@ -10,15 +12,15 @@ pub const CODEX_ALPHA_SEARCH_PATH: &str = "/codex/alpha/search"; pub const CODEX_USAGE_API_PATH: &str = "/api/codex/usage"; /// `/wham/usage` pub const WHAM_USAGE_PATH: &str = "/wham/usage"; -/// Codex Desktop 当前账号个人资料与累计统计。 +/// Codex Desktop 当前账号个人资料与累计统计 pub const WHAM_PROFILE_STATISTICS_PATH: &str = "/wham/profiles/me"; -/// Codex Desktop 主动额度重置卡列表。 +/// Codex Desktop 主动额度重置卡列表 pub const WHAM_RATE_LIMIT_RESET_CREDITS_PATH: &str = "/wham/rate-limit-reset-credits"; -/// Codex Desktop 主动消费额度重置卡。 +/// Codex Desktop 主动消费额度重置卡 pub const WHAM_RATE_LIMIT_RESET_CREDITS_CONSUME_PATH: &str = "/wham/rate-limit-reset-credits/consume"; -/// 拼接完整 endpoint URL。 +/// 拼接完整 endpoint URL pub fn endpoint_url(base_url: &str, endpoint_path: &str) -> String { format!( "{}/{}", @@ -27,7 +29,7 @@ pub fn endpoint_url(base_url: &str, endpoint_path: &str) -> String { ) } -/// 上游启动配置与账号地址共用同一校验,HTTP 仅允许本机联调。 +/// 上游启动配置与账号地址共用同一校验,HTTP 仅允许本机联调 pub(crate) fn valid_upstream_base_url(value: &str) -> bool { let Ok(url) = url::Url::parse(value) else { return false; @@ -45,12 +47,12 @@ pub(crate) fn valid_upstream_base_url(value: &str) -> bool { && url.fragment().is_none() } -/// 返回与 base path 对应的唯一 usage endpoint。 +/// 返回与 base path 对应的唯一 usage endpoint pub fn usage_endpoint_url(base_url: &str) -> String { account_endpoint_url(base_url, "usage") } -/// Core backend-client 的两个官方路径风格,供所有账号接口共用。 +/// Core backend-client 的两个官方路径风格,供所有账号接口共用 pub fn account_endpoint_url(base_url: &str, resource: &str) -> String { let prefix = if has_backend_api_base_path(base_url) { "wham" diff --git a/backend/crates/providers/openai/src/transport/headers.rs b/backend/crates/providers/openai/src/transport/headers.rs index bb8cfee10..866778e5d 100644 --- a/backend/crates/providers/openai/src/transport/headers.rs +++ b/backend/crates/providers/openai/src/transport/headers.rs @@ -1,4 +1,4 @@ -//! 各官方调用链的请求头契约;版本与平台信息统一来自运行时画像快照。 +//! 各官方调用链的请求头契约;版本与平台信息统一来自运行时画像快照 use gateway_protocol::openai::{ X_OPENAI_INTERNAL_CODEX_RESPONSES_LITE_HEADER, X_OPENAI_MEMGEN_REQUEST_HEADER, @@ -16,8 +16,8 @@ use super::protocol::responses::CodexResponsesRequest; const CODEX_RESIDENCY_HEADER: &str = "x-openai-internal-codex-residency"; -/// 不透传下游携带的认证、账号及相关身份字段,避免影响网关选定的上游身份。 -/// 上游需要的官方身份头由网关构造;这里也包含通用认证和 Cookie 字段。 +/// 不透传下游携带的认证、账号及相关身份字段,避免影响网关选定的上游身份 +/// 上游需要的官方身份头由网关构造;这里也包含通用认证和 Cookie 字段 pub(super) fn is_managed_identity_header(name: &str) -> bool { matches!( name, @@ -30,15 +30,15 @@ pub(super) fn is_managed_identity_header(name: &str) -> bool { | "chatgpt-project-id" | "openai-organization" | "openai-project" - // 工作区路由和合规属性属于认证账号,不能继承下游账号的值。 + // 工作区路由和合规属性属于认证账号,不能继承下游账号的值 | "x-openai-account-routing-override" | "x-openai-fedramp" - // 安装身份由当前账号写入 client_metadata,不继承下游安装头。 + // 安装身份由当前账号写入 client_metadata,不继承下游安装头 | "x-codex-installation-id" ) } -/// 构造 Codex Core 为模型请求设置的稳定身份请求头。 +/// 构造 Codex Core 为模型请求设置的稳定身份请求头 pub fn build_codex_model_headers( profile: &CodexWireProfile, authorization: &str, @@ -54,7 +54,7 @@ pub fn build_codex_model_headers( Ok(headers) } -/// Core 默认客户端身份,用于模型接口及 OAuth refresh;raw token exchange 不使用。 +/// Core 默认客户端身份,用于模型接口及 OAuth refresh;raw token exchange 不使用 pub fn build_codex_profile_headers(profile: &CodexWireProfile) -> CodexClientResult { let mut headers = HeaderMap::new(); headers.insert( @@ -71,7 +71,7 @@ pub fn build_codex_profile_headers(profile: &CodexWireProfile) -> CodexClientRes Ok(headers) } -/// Core backend-client 的账号请求:不携带模型版本或 originator。 +/// Core backend-client 的账号请求:不携带模型版本或 originator pub fn build_codex_account_headers( profile: &CodexWireProfile, authorization: &str, @@ -84,7 +84,7 @@ pub fn build_codex_account_headers( Ok(headers) } -/// 桌面下载的账号身份;完整性凭据须由认证主体持有,不能借用下游客户端的值。 +/// 桌面下载的账号身份;完整性凭据须由认证主体持有,不能借用下游客户端的值 pub fn build_codex_download_headers( profile: &CodexWireProfile, authorization: &str, @@ -110,7 +110,7 @@ impl CodexBackendClient { profile: &CodexWireProfile, context: CodexRequestContext<'_>, ) -> CodexClientResult { - // 客户端画像与认证方式无关;API Key 不携带 OAuth 账号身份和 Cookie。 + // 客户端画像与认证方式无关;API Key 不携带 OAuth 账号身份和 Cookie let (account_id, cookie_header) = match self.protocol { OpenAiUpstreamProtocol::Codex => (context.account_id, context.cookie_header), OpenAiUpstreamProtocol::ResponsesApi => (None, None), @@ -162,7 +162,7 @@ impl CodexBackendClient { Ok(headers) } - /// HTTP 和 WS 共用的 Core 会话字段;传输专属字段由各自入口生成。 + /// HTTP 和 WS 共用的 Core 会话字段;传输专属字段由各自入口生成 fn response_headers( &self, request: &CodexResponsesRequest, @@ -220,7 +220,7 @@ impl CodexBackendClient { let mut replaced = std::collections::HashSet::new(); for header in &self.middleware_headers { let name = HeaderName::from_bytes(header.name().as_bytes())?; - // 宿主画像和账号鉴权先形成基线;插件首次写入同名头时替换,随后保留多值。 + // 宿主画像和账号鉴权先形成基线;插件首次写入同名头时替换,随后保留多值 if replaced.insert(name.clone()) { headers.remove(&name); } @@ -232,7 +232,7 @@ impl CodexBackendClient { fn append_passthrough_headers(headers: &mut HeaderMap, request: &CodexResponsesRequest) { for name in request.passthrough_headers.keys() { - // 身份与传输字段只由画像/正文生成;其余协议头保留原始多值字节。 + // 身份与传输字段只由画像/正文生成;其余协议头保留原始多值字节 if matches!( name.as_str(), "originator" @@ -274,7 +274,7 @@ pub(super) fn insert_optional_header( Ok(()) } -/// 尽力投影客户端协议值;无法表示为 HTTP header 时保留正文并跳过投影。 +/// 尽力投影客户端协议值;无法表示为 HTTP header 时保留正文并跳过投影 pub(super) fn insert_optional_protocol_header( headers: &mut HeaderMap, name: &'static str, @@ -290,7 +290,7 @@ fn header_pairs(headers: &HeaderMap) -> Vec<(String, String)> { headers .iter() // tungstenite 的 opening serializer 只接受可 `to_str()` 的值;逐条跳过 - // 无法构造的头,不能让一个扩展头中断业务 payload。 + // 无法构造的头,不能让一个扩展头中断业务 payload .filter_map(|(name, value)| { value .to_str() @@ -304,7 +304,8 @@ pub(super) fn websocket_header_pairs(headers: &HeaderMap) -> Vec<(String, String let mut pairs = header_pairs(headers); // WebSocket opening 的 HeaderMap 先由业务头构造,再被 tungstenite 插入协议头; // 这里复现官方 HeaderMap 交给 tungstenite 时的迭代顺序,最终序列化后的 - // 线级顺序由 fingerprint 测试锁定。未知扩展头仍保持相对顺序。 + // 线级顺序由 fingerprint 测试锁定 + // 未知扩展头仍保持相对顺序 pairs.sort_by_key(|(name, _)| websocket_header_order(name)); pairs } diff --git a/backend/crates/providers/openai/src/transport/mod.rs b/backend/crates/providers/openai/src/transport/mod.rs index 16363e250..5aaaac8f4 100644 --- a/backend/crates/providers/openai/src/transport/mod.rs +++ b/backend/crates/providers/openai/src/transport/mod.rs @@ -1,4 +1,4 @@ -//! Codex HTTP/SSE/WebSocket 上游 transport。 +//! Codex HTTP/SSE/WebSocket 上游 transport pub mod canonical; pub mod catalog; diff --git a/backend/crates/providers/openai/src/transport/profile/cli_release.rs b/backend/crates/providers/openai/src/transport/profile/cli_release.rs index d49124461..561794509 100644 --- a/backend/crates/providers/openai/src/transport/profile/cli_release.rs +++ b/backend/crates/providers/openai/src/transport/profile/cli_release.rs @@ -1,4 +1,4 @@ -//! CLI 稳定发布来自官方 npm 包及其平台依赖,不与 Desktop 的内嵌版本混用。 +//! CLI 稳定发布来自官方 npm 包及其平台依赖,不与 Desktop 的内嵌版本混用 use std::collections::BTreeMap; use std::sync::Arc; @@ -33,7 +33,7 @@ struct NpmRelease { optional_dependencies: BTreeMap, } -/// 验证稳定标签和各平台包确实来自同一次官方发布。 +/// 验证稳定标签和各平台包确实来自同一次官方发布 pub fn parse_cli_release(bytes: &[u8]) -> Result { if bytes.len() > MAX_BYTES { return Err(CliReleaseError::Invalid); @@ -218,7 +218,7 @@ impl CliReleaseService { } pub(super) fn seed_releases(state: &CodexWireProfileState) { - // 2026-09-18 核对官方 npm latest 及六个平台依赖;这里只提供离线启动资料。 + // 2026-09-18 核对官方 npm latest 及六个平台依赖;这里只提供离线启动资料 for (platform, arch, _) in TARGETS { state.seed_client_release( ClientKind::Cli, diff --git a/backend/crates/providers/openai/src/transport/profile/desktop_artifact.rs b/backend/crates/providers/openai/src/transport/profile/desktop_artifact.rs index bc448d273..d6c607066 100644 --- a/backend/crates/providers/openai/src/transport/profile/desktop_artifact.rs +++ b/backend/crates/providers/openai/src/transport/profile/desktop_artifact.rs @@ -1,4 +1,4 @@ -//! 有界读取 Codex Desktop ZIP 内嵌的 arm64 Core 版本。 +//! 有界读取 Codex Desktop ZIP 内嵌的 arm64 Core 版本 use std::cmp; @@ -12,7 +12,7 @@ const OFFICIAL_ARTIFACT_HOST: &str = "persistent.oaistatic.com"; const EOCD_SIGNATURE: &[u8; 4] = b"PK\x05\x06"; const CENTRAL_SIGNATURE: &[u8; 4] = b"PK\x01\x02"; const LOCAL_SIGNATURE: &[u8; 4] = b"PK\x03\x04"; -// Core 位于独立的 CodexCLI.app 中,bin/codex 仅为启动脚本。 +// Core 位于独立的 CodexCLI.app 中,bin/codex 仅为启动脚本 const CORE_PATH_SUFFIX: &[u8] = b"/Contents/Resources/codex-cli/CodexCLI.app/Contents/MacOS/codex"; const CORE_VERSION_ANCHOR: &[u8] = b"codex-mcp-client/"; const MAX_EOCD_SEARCH_BYTES: u64 = 65_535 + 22; @@ -59,18 +59,18 @@ struct CentralDirectory { entries: u16, } -/// ZIP central directory 中已经校验的 Codex Core 条目。 +/// ZIP central directory 中已经校验的 Codex Core 条目 #[derive(Debug)] pub struct CoreEntry { - /// ZIP 内完整条目名。 + /// ZIP 内完整条目名 pub name: Vec, flags: u16, compression: u16, - /// 压缩后的字节数。 + /// 压缩后的字节数 pub compressed_size: u64, - /// 解压后的字节数。 + /// 解压后的字节数 pub uncompressed_size: u64, - /// local file header 在制品内的偏移。 + /// local file header 在制品内的偏移 pub local_header_offset: u64, } @@ -190,7 +190,7 @@ async fn read_range( Ok(body) } -/// 解析严格的 `bytes start-end/size` Content-Range。 +/// 解析严格的 `bytes start-end/size` Content-Range #[must_use] pub fn parse_content_range(value: &str) -> Option<(u64, u64, u64)> { let value = value.strip_prefix("bytes ")?; @@ -248,11 +248,11 @@ fn parse_eocd( }) } -/// 在 central directory 中定位唯一的 bundled Codex Core。 +/// 在 central directory 中定位唯一的 bundled Codex Core /// /// # Errors /// -/// ZIP 元数据无效、条目不受支持,或 Core 不唯一时返回制品错误。 +/// ZIP 元数据无效、条目不受支持,或 Core 不唯一时返回制品错误 pub fn find_core_entry( central: &[u8], expected_entries: u16, @@ -479,18 +479,18 @@ fn validate_macho_header(header: &[u8]) -> Result<(), CodexDesktopArtifactError> Ok(()) } -/// 跨分块查找 bundled Core 版本标记的有界扫描器。 +/// 跨分块查找 bundled Core 版本标记的有界扫描器 #[derive(Default)] pub struct CoreVersionScanner { carry: Vec, } impl CoreVersionScanner { - /// 推入一个解压分块,并在发现完整合法版本时返回它。 + /// 推入一个解压分块,并在发现完整合法版本时返回它 /// /// # Errors /// - /// 标记后的版本超过限制或不满足受支持格式时返回制品错误。 + /// 标记后的版本超过限制或不满足受支持格式时返回制品错误 pub fn push(&mut self, bytes: &[u8]) -> Result, CodexDesktopArtifactError> { let mut window = Vec::with_capacity(self.carry.len() + bytes.len()); window.extend_from_slice(&self.carry); diff --git a/backend/crates/providers/openai/src/transport/profile/identity.rs b/backend/crates/providers/openai/src/transport/profile/identity.rs index ccf7489e4..9a020252b 100644 --- a/backend/crates/providers/openai/src/transport/profile/identity.rs +++ b/backend/crates/providers/openai/src/transport/profile/identity.rs @@ -1,4 +1,4 @@ -//! 预设与完整自定义 UA 的统一解析边界,身份配套头由 Provider 所有。 +//! 预设与完整自定义 UA 的统一解析边界,身份配套头由 Provider 所有 use chrono::DateTime; use gateway_core::account::OpaqueProviderData; @@ -10,7 +10,7 @@ use super::selection::{ }; use super::{CodexWireProfile, CodexWireProfileState}; -/// 已解析的配置;没有 mode 的预设配置复用官方版本与平台合同。 +/// 已解析的配置;没有 mode 的预设配置复用官方版本与平台合同 #[derive(Debug, Clone)] pub struct RequestProfileSelection(ParsedSelection); diff --git a/backend/crates/providers/openai/src/transport/profile/mod.rs b/backend/crates/providers/openai/src/transport/profile/mod.rs index a6dbbe211..3a20e1ab2 100644 --- a/backend/crates/providers/openai/src/transport/profile/mod.rs +++ b/backend/crates/providers/openai/src/transport/profile/mod.rs @@ -1,4 +1,4 @@ -//! Codex Desktop 上游请求画像。 +//! Codex Desktop 上游请求画像 use std::collections::BTreeMap; use std::sync::{Arc, RwLock}; @@ -28,10 +28,10 @@ pub mod selection; use selection::{ClientKind, ClientPlatform, ClientRelease}; -/// Codex Desktop 官方 appcast 地址。 +/// Codex Desktop 官方 appcast 地址 pub const CODEX_DESKTOP_APPCAST_URL: &str = "https://persistent.oaistatic.com/codex-app-prod/appcast.xml"; -/// 官方发布元数据检查周期。 +/// 官方发布元数据检查周期 pub const APPCAST_POLL_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60); const APPCAST_TIMEOUT: Duration = Duration::from_secs(30); @@ -39,45 +39,45 @@ const MAX_APPCAST_BYTES: usize = 1024 * 1024; const ARTIFACT_PROFILE_CACHE_TTL: Duration = Duration::from_secs(30 * 24 * 60 * 60); const ARTIFACT_PROFILE_SCHEMA_VERSION: u64 = 1; -/// 与官方 managed residency requirement 相同的可选请求约束。 +/// 与官方 managed residency requirement 相同的可选请求约束 #[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum CodexResidency { Us, } -/// 保留已有配置类型入口;位置规则由出口领域统一维护。 +/// 保留已有配置类型入口;位置规则由出口领域统一维护 pub use gateway_core::account::RequestLocation as CodexRequestLocation; -/// Codex 上游请求身份快照。 +/// Codex 上游请求身份快照 /// -/// 预设按官方配套版本生成 UA;自定义身份保留完整 UA 及其配套请求头。 -/// 官方制品核验只更新独立的基线与预设资料,不改写已冻结的请求身份。 +/// 预设按官方配套版本生成 UA;自定义身份保留完整 UA 及其配套请求头 +/// 官方制品核验只更新独立的基线与预设资料,不改写已冻结的请求身份 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct CodexWireProfile { pub client_kind: ClientKind, - /// `originator` 请求头及 User-Agent 产品名。 + /// `originator` 请求头及 User-Agent 产品名 pub originator: String, - /// Core 版本,用于模型请求的 version 和 client_version。 + /// Core 版本,用于模型请求的 version 和 client_version pub codex_version: String, - /// Desktop 应用版本,用于 app-server `clientInfo.version` 对应的 UA 后缀。 + /// Desktop 应用版本,用于 app-server `clientInfo.version` 对应的 UA 后缀 pub desktop_version: String, - /// Desktop 制品构建号,仅用于发布对齐诊断。 + /// Desktop 制品构建号,仅用于发布对齐诊断 pub desktop_build: String, - /// Codex Core UA 中的目标操作系统类型。 + /// Codex Core UA 中的目标操作系统类型 pub os_type: String, - /// Codex Core UA 中的目标操作系统版本。 + /// Codex Core UA 中的目标操作系统版本 pub os_version: String, - /// Codex Core UA 中的目标架构。 + /// Codex Core UA 中的目标架构 pub arch: String, - /// Codex Core UA 中的终端标记。 + /// Codex Core UA 中的终端标记 pub terminal: String, - /// 入口专属或自定义的完整值;存在时不得用默认预设重新拼接。 + /// 入口专属或自定义的完整值;存在时不得用默认预设重新拼接 #[serde(default, skip_serializing_if = "Option::is_none")] pub exact_user_agent: Option, - /// 未配置时不发送 residency 头;不随制品版本更新而改变。 + /// 未配置时不发送 residency 头;不随制品版本更新而改变 pub residency: Option, - /// 版本元组最后一次经制品核验的时间;不表示 TLS 传输已重新核验。 + /// 版本元组最后一次经制品核验的时间;不表示 TLS 传输已重新核验 pub verified_at: DateTime, } @@ -95,14 +95,14 @@ impl Default for CodexWireProfile { terminal: "unknown".to_owned(), exact_user_agent: None, residency: None, - // 制品核验于 2026-09-06T03:26:12.084Z;进程启动不构成重新核验。 + // 制品核验于 2026-09-06T03:26:12.084Z;进程启动不构成重新核验 verified_at: DateTime::UNIX_EPOCH + chrono::Duration::milliseconds(1_788_665_172_084), } } } impl CodexWireProfile { - /// 原样返回已解析的完整 UA;默认预设按 bundled Core app-server 格式生成。 + /// 原样返回已解析的完整 UA;默认预设按 bundled Core app-server 格式生成 pub fn user_agent(&self) -> String { if let Some(user_agent) = &self.exact_user_agent { return user_agent.clone(); @@ -131,11 +131,12 @@ impl CodexWireProfile { ) } - /// 按 Codex Desktop 暴露的稳定桌面 surface 格式构造 User-Agent。 + /// 按 Codex Desktop 暴露的稳定桌面 surface 格式构造 User-Agent /// /// Electron 网络栈会为 renderer 请求附加运行时 User-Agent;非 Electron transport - /// 使用同一官方包中 `getDesktopUserAgent` 的稳定格式。它与 bundled Core 请求使用的 - /// 复合 User-Agent 是两个独立 surface。 + /// 使用同一官方包中 `getDesktopUserAgent` 的稳定格式 + /// 它与 bundled Core 请求使用的 + /// 复合 User-Agent 是两个独立 surface #[must_use] pub fn desktop_user_agent(&self) -> String { format!( @@ -145,7 +146,7 @@ impl CodexWireProfile { } } -/// 跨 Codex 上游请求共享的运行时请求画像。 +/// 跨 Codex 上游请求共享的运行时请求画像 #[derive(Debug, Clone)] pub struct CodexWireProfileState { profile: Arc>, @@ -162,7 +163,7 @@ struct ClientReleaseObservation { } impl CodexWireProfileState { - /// 从启动画像创建运行时状态。 + /// 从启动画像创建运行时状态 pub fn new(profile: CodexWireProfile) -> Self { let state = Self { profile: Arc::new(RwLock::new(profile)), @@ -173,7 +174,7 @@ impl CodexWireProfileState { state } - /// 返回当前画像的独立快照,避免持锁执行网络请求。 + /// 返回当前画像的独立快照,避免持锁执行网络请求 pub fn snapshot(&self) -> CodexWireProfile { self.profile .read() @@ -181,7 +182,7 @@ impl CodexWireProfileState { .clone() } - /// 原子发布同一 Desktop ZIP 中核验出的完整版本元组。 + /// 原子发布同一 Desktop ZIP 中核验出的完整版本元组 pub fn update_bundled_release(&self, release: &CodexBundledReleaseProfile) { let mut profile = self .profile @@ -270,7 +271,7 @@ impl CodexWireProfileState { } } -/// 同一个官方 Desktop ZIP 中核验出的原子版本元组。 +/// 同一个官方 Desktop ZIP 中核验出的原子版本元组 #[derive(Debug, Clone, PartialEq, Eq)] pub struct CodexBundledReleaseProfile { pub codex_version: String, @@ -402,7 +403,7 @@ fn decode_artifact_profile( Ok(decoded) } -/// 官方 appcast 中按顺序出现的首个完整 Desktop 制品。 +/// 官方 appcast 中按顺序出现的首个完整 Desktop 制品 #[derive(Debug, Clone, PartialEq, Eq)] pub struct CodexDesktopRelease { pub version: String, @@ -415,7 +416,7 @@ pub struct CodexDesktopRelease { pub signature_present: bool, } -/// 最近一次官方 Desktop 制品检查结果。 +/// 最近一次官方 Desktop 制品检查结果 #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct CodexDesktopReleaseSnapshot { pub checked_at: Option>, @@ -423,7 +424,7 @@ pub struct CodexDesktopReleaseSnapshot { pub last_error: Option, } -/// Provider 内共享的 appcast 观察状态。 +/// Provider 内共享的 appcast 观察状态 #[derive(Debug, Clone, Default)] pub struct CodexDesktopReleaseStatus { snapshot: Arc>, @@ -459,7 +460,8 @@ impl CodexDesktopReleaseStatus { } } -/// Desktop appcast 获取边界。生产实现固定访问官方 HTTPS,测试实现只替换此边界。 +/// Desktop appcast 获取边界 +/// 生产实现固定访问官方 HTTPS,测试实现只替换此边界 pub trait CodexDesktopReleaseTransport: Send + Sync { fn fetch(&self) -> BoxFuture<'_, Result>; @@ -469,7 +471,7 @@ pub trait CodexDesktopReleaseTransport: Send + Sync { ) -> BoxFuture<'a, Result>; } -/// 固定访问官方 Desktop appcast 的生产 transport。 +/// 固定访问官方 Desktop appcast 的生产 transport #[derive(Clone)] pub struct OfficialCodexDesktopReleaseTransport { client: Client, @@ -477,7 +479,7 @@ pub struct OfficialCodexDesktopReleaseTransport { } impl OfficialCodexDesktopReleaseTransport { - /// 构造禁用环境代理和 redirect 的官方 HTTPS transport。 + /// 构造禁用环境代理和 redirect 的官方 HTTPS transport pub fn new() -> Result { let endpoint = Url::parse(CODEX_DESKTOP_APPCAST_URL) .map_err(|_| CodexDesktopReleaseError::InvalidEndpoint)?; @@ -546,7 +548,7 @@ impl CodexDesktopReleaseTransport for OfficialCodexDesktopReleaseTransport { } } -/// 拉取 appcast 并原子发布请求画像的 Provider 服务。 +/// 拉取 appcast 并原子发布请求画像的 Provider 服务 #[derive(Clone)] pub struct CodexDesktopReleaseService { transport: Arc, @@ -557,7 +559,7 @@ pub struct CodexDesktopReleaseService { } impl CodexDesktopReleaseService { - /// 从正式 transport seam 构造服务;生产组装只注入官方 transport。 + /// 从正式 transport seam 构造服务;生产组装只注入官方 transport #[must_use] pub fn new( profile: CodexWireProfileState, @@ -582,7 +584,7 @@ impl CodexDesktopReleaseService { self.status.clone() } - /// 执行一次有界检查;失败只更新观察状态,不修改上一份成功画像。 + /// 执行一次有界检查;失败只更新观察状态,不修改上一份成功画像 pub async fn refresh(&self) -> Result { let checked_at = Utc::now(); let result = self.refresh_inner(checked_at).await; @@ -701,7 +703,7 @@ fn parse_build_sequence(value: &str) -> Result { .ok_or(CodexDesktopReleaseError::InvalidBuild) } -/// 解析 appcast 中按顺序出现的首个完整发布项。 +/// 解析 appcast 中按顺序出现的首个完整发布项 pub fn parse_desktop_release(xml: &str) -> Result { let document = Document::parse(xml).map_err(|_| CodexDesktopReleaseError::InvalidDocument)?; for item in document @@ -792,7 +794,7 @@ fn numeric_dotted_version(value: &str) -> bool { valid >= 2 && valid == value.split('.').count() } -/// Desktop appcast 的稳定、无响应正文错误分类。 +/// Desktop appcast 的稳定、无响应正文错误分类 #[derive(Debug, thiserror::Error)] pub enum CodexDesktopReleaseError { #[error("Codex Desktop appcast client initialization failed")] diff --git a/backend/crates/providers/openai/src/transport/profile/platform_release/artifact.rs b/backend/crates/providers/openai/src/transport/profile/platform_release/artifact.rs index 4a0e79523..0baac707c 100644 --- a/backend/crates/providers/openai/src/transport/profile/platform_release/artifact.rs +++ b/backend/crates/providers/openai/src/transport/profile/platform_release/artifact.rs @@ -1,4 +1,4 @@ -//! 从 Windows MSIX 和 Linux DEB 的同一制品读取应用元数据与 bundled Core。 +//! 从 Windows MSIX 和 Linux DEB 的同一制品读取应用元数据与 bundled Core use super::super::desktop_artifact::CoreVersionScanner; use super::super::selection::{ClientPlatform, ClientRelease}; diff --git a/backend/crates/providers/openai/src/transport/profile/platform_release/mod.rs b/backend/crates/providers/openai/src/transport/profile/platform_release/mod.rs index e2d170a14..fa0883c71 100644 --- a/backend/crates/providers/openai/src/transport/profile/platform_release/mod.rs +++ b/backend/crates/providers/openai/src/transport/profile/platform_release/mod.rs @@ -1,4 +1,4 @@ -//! Windows / Linux Desktop 按平台和架构独立核验、缓存配套版本。 +//! Windows / Linux Desktop 按平台和架构独立核验、缓存配套版本 use super::selection::{ ClientKind, ClientPlatform, ClientProfileSelection, ClientRelease, VersionMode, object, @@ -214,7 +214,7 @@ impl PlatformDesktopReleaseService { return Err("官方 Desktop 发布低于当前版本或同构建版本不一致".to_owned()); } let verified_at = Utc::now(); - // 核验时间属于缓存信封,避免同构建重复检查被判定为不同制品。 + // 核验时间属于缓存信封,避免同构建重复检查被判定为不同制品 artifact.release.verified_at = None; let payload = object(&serde_json::to_value(&artifact).map_err(|_| "Desktop 发布序列化失败")?) @@ -280,8 +280,8 @@ fn sequence(target: DesktopTarget, release: &ClientRelease) -> io::Result { } fn baseline_artifacts() -> BTreeMap { - // 2026-09-18 分别读取四个官方安装包,版本元组与 ETag 均来自同一个包。 - // 离线可直接选择自动模式;ETag 变化后必须重新核验,不能延用另一平台的版本。 + // 2026-09-18 分别读取四个官方安装包,版本元组与 ETag 均来自同一个包 + // 离线可直接选择自动模式;ETag 变化后必须重新核验,不能延用另一平台的版本 let identities = [ (774_919_598, "\"0x8DF141FFDF86290\""), (771_727_321, "\"0x8DF14202C49201F\""), diff --git a/backend/crates/providers/openai/src/transport/profile/platform_release/range.rs b/backend/crates/providers/openai/src/transport/profile/platform_release/range.rs index 256f78377..fc9fb5991 100644 --- a/backend/crates/providers/openai/src/transport/profile/platform_release/range.rs +++ b/backend/crates/providers/openai/src/transport/profile/platform_release/range.rs @@ -1,4 +1,4 @@ -//! 将同一 ETag 的有界 HTTP Range 暴露为归档库需要的 Read + Seek。 +//! 将同一 ETag 的有界 HTTP Range 暴露为归档库需要的 Read + Seek use std::io::{self, Read, Seek, SeekFrom}; use std::time::{Duration, Instant}; @@ -61,7 +61,7 @@ impl Read for RemoteFile { fn read(&mut self, output: &mut [u8]) -> io::Result { if self.cancel.is_cancelled() || Instant::now() >= self.deadline { // 取消和预算耗尽是终态;Interrupted 会被 read_exact/read_to_end - // 自动重试,导致 blocking 线程空转并阻止 Tokio runtime 退出。 + // 自动重试,导致 blocking 线程空转并阻止 Tokio runtime 退出 return Err(io::Error::other("Desktop 制品读取已取消或超时")); } if output.is_empty() || self.position >= self.identity.size { diff --git a/backend/crates/providers/openai/src/transport/profile/platform_release/xz.rs b/backend/crates/providers/openai/src/transport/profile/platform_release/xz.rs index 45769cc3b..0edb2ea8b 100644 --- a/backend/crates/providers/openai/src/transport/profile/platform_release/xz.rs +++ b/backend/crates/providers/openai/src/transport/profile/platform_release/xz.rs @@ -1,4 +1,4 @@ -//! 利用 XZ 索引跳过无关 tar 文件;每次只保留一个已校验的压缩块。 +//! 利用 XZ 索引跳过无关 tar 文件;每次只保留一个已校验的压缩块 use super::range::{invalid, seek_position}; use std::io::{self, Read, Seek, SeekFrom}; @@ -122,7 +122,7 @@ impl Read for IndexedXz { bytes.resize(12 + padded as usize, 0); self.source.seek(SeekFrom::Start(block.offset))?; self.source.read_exact(&mut bytes[12..])?; - // 重建单块 XZ 外壳,让 liblzma 同时校验原始块的校验和与解压长度。 + // 重建单块 XZ 外壳,让 liblzma 同时校验原始块的校验和与解压长度 let mut index = vec![0, 1]; put_vli(block.compressed, &mut index); put_vli(block.unpacked, &mut index); diff --git a/backend/crates/providers/openai/src/transport/profile/selection.rs b/backend/crates/providers/openai/src/transport/profile/selection.rs index 37effe41a..6d08a564a 100644 --- a/backend/crates/providers/openai/src/transport/profile/selection.rs +++ b/backend/crates/providers/openai/src/transport/profile/selection.rs @@ -1,4 +1,4 @@ -//! 管理端选择的唯一解析入口;持久化配置与官方发布资料分别管理。 +//! 管理端选择的唯一解析入口;持久化配置与官方发布资料分别管理 use chrono::{DateTime, Utc}; use gateway_core::account::OpaqueProviderData; @@ -14,7 +14,7 @@ pub enum ClientKind { Cli, } -/// CLI 共用官方发布版本,但不同入口提供各自的客户端标识和 UA 后缀。 +/// CLI 共用官方发布版本,但不同入口提供各自的客户端标识和 UA 后缀 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum CliEntry { @@ -71,7 +71,7 @@ pub enum VersionMode { Fixed, } -/// 空的可选字段表示使用对应预设参数;Key 覆盖始终是一份完整选择。 +/// 空的可选字段表示使用对应预设参数;Key 覆盖始终是一份完整选择 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct ClientProfileSelection { @@ -244,8 +244,8 @@ impl ClientProfileSelection { verified_at: release.verified_at.unwrap_or(DateTime::UNIX_EPOCH), }; if let Some(entry) = self.cli_entry { - // 入口后缀使用同一次解析得到的 Core 版本,避免每日更新后头部与后缀混用。 - // originator 可单独覆盖;入口名与官方 clientInfo.name 的语义保持一致。 + // 入口后缀使用同一次解析得到的 Core 版本,避免每日更新后头部与后缀混用 + // originator 可单独覆盖;入口名与官方 clientInfo.name 的语义保持一致 profile.exact_user_agent = Some(format!( "{} ({}; {})", profile.user_agent(), @@ -257,7 +257,7 @@ impl ClientProfileSelection { } } -/// 一个具体客户端制品的配套版本;自定义值不携带核验时间。 +/// 一个具体客户端制品的配套版本;自定义值不携带核验时间 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct ClientRelease { diff --git a/backend/crates/providers/openai/src/transport/profile_avatar.rs b/backend/crates/providers/openai/src/transport/profile_avatar.rs index f5ff22cbf..ca7bf3c54 100644 --- a/backend/crates/providers/openai/src/transport/profile_avatar.rs +++ b/backend/crates/providers/openai/src/transport/profile_avatar.rs @@ -1,4 +1,4 @@ -//! Codex 官方个人资料头像的固定来源流式 transport。 +//! Codex 官方个人资料头像的固定来源流式 transport use std::{pin::Pin, time::Duration}; @@ -24,16 +24,16 @@ const AUTH0_AVATAR_PATH_PREFIX: &str = "/avatars/"; const PROFILE_AVATAR_HEADERS_TIMEOUT: Duration = Duration::from_secs(15); const PROFILE_AVATAR_STREAM_IDLE_TIMEOUT: Duration = Duration::from_secs(30); -/// 头像正文流;不累积完整正文,也不设置总字节数上限。 +/// 头像正文流;不累积完整正文,也不设置总字节数上限 pub type CodexProfileAvatarStream = Pin> + Send + 'static>>; -/// 响应开始后的头像流失败;不得携带带签名的上游 URL。 +/// 响应开始后的头像流失败;不得携带带签名的上游 URL #[derive(Debug, thiserror::Error)] #[error("Codex profile avatar stream failed")] pub struct CodexProfileAvatarStreamError; -/// 已打开的官方头像响应;MIME 只透传,不做格式白名单判断。 +/// 已打开的官方头像响应;MIME 只透传,不做格式白名单判断 pub struct CodexProfileAvatar { pub content_type: Option, pub content_length: Option, @@ -63,11 +63,11 @@ pub enum CodexProfileAvatarFetchError { TransportUnavailable, } -/// 校验来源并构造头像请求;账号凭据仅用于 ChatGPT Estuary。 +/// 校验来源并构造头像请求;账号凭据仅用于 ChatGPT Estuary /// /// # Errors /// -/// 来源不受支持或请求头无法编码时返回脱敏错误。 +/// 来源不受支持或请求头无法编码时返回脱敏错误 pub fn build_profile_avatar_request( client: &Client, base_url: &str, @@ -97,7 +97,7 @@ pub fn build_profile_avatar_request( } AUTH0_AVATAR_ORIGIN => { avatar_path(&source, AUTH0_AVATAR_PATH_PREFIX)?; - // Auth0 的默认头像是公开 CDN 资源,不能携带 ChatGPT 账号凭据。 + // Auth0 的默认头像是公开 CDN 资源,不能携带 ChatGPT 账号凭据 client .get(source) .header(USER_AGENT, profile.desktop_user_agent()) @@ -109,11 +109,11 @@ pub fn build_profile_avatar_request( .map_err(|_| CodexProfileAvatarFetchError::TransportUnavailable) } -/// 打开已校验来源的头像字节流。 +/// 打开已校验来源的头像字节流 /// /// # Errors /// -/// 来源不受支持、请求失败或上游返回非成功状态时返回错误。 +/// 来源不受支持、请求失败或上游返回非成功状态时返回错误 pub async fn fetch_profile_avatar( client: &Client, base_url: &str, diff --git a/backend/crates/providers/openai/src/transport/profile_statistics.rs b/backend/crates/providers/openai/src/transport/profile_statistics.rs index afc0bf60f..620406b29 100644 --- a/backend/crates/providers/openai/src/transport/profile_statistics.rs +++ b/backend/crates/providers/openai/src/transport/profile_statistics.rs @@ -1,4 +1,4 @@ -//! Codex Desktop 当前账号个人资料与累计统计 HTTP contract。 +//! Codex Desktop 当前账号个人资料与累计统计 HTTP contract use chrono::NaiveDate; use gateway_protocol::openai::events::retry_after_seconds_from_body; @@ -16,7 +16,7 @@ use super::{ response_meta, }; -/// 单次 profile 响应允许保留和解析的最大字节数。 +/// 单次 profile 响应允许保留和解析的最大字节数 pub const MAX_CODEX_PROFILE_STATISTICS_BODY_BYTES: usize = 1024 * 1024; #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/backend/crates/providers/openai/src/transport/protocol/mod.rs b/backend/crates/providers/openai/src/transport/protocol/mod.rs index 4fbe258ca..f116ddba0 100644 --- a/backend/crates/providers/openai/src/transport/protocol/mod.rs +++ b/backend/crates/providers/openai/src/transport/protocol/mod.rs @@ -1,6 +1,6 @@ -//! Codex 协议类型。 +//! Codex 协议类型 -/// Codex Responses / Compact 请求体。 +/// Codex Responses / Compact 请求体 pub mod responses; -/// WebSocket 帧类型与纯编解码。 +/// WebSocket 帧类型与纯编解码 pub mod websocket; diff --git a/backend/crates/providers/openai/src/transport/protocol/responses.rs b/backend/crates/providers/openai/src/transport/protocol/responses.rs index 87e96e0da..889dddac8 100644 --- a/backend/crates/providers/openai/src/transport/protocol/responses.rs +++ b/backend/crates/providers/openai/src/transport/protocol/responses.rs @@ -1,3 +1,5 @@ +//! Codex Responses 请求、传输要求、事件信号与流式错误的协议解析 + use std::fmt; use gateway_protocol::openai::{ @@ -8,76 +10,78 @@ use reqwest::header::HeaderMap; use serde::Serialize; use serde_json::{Map, Value}; -/// 官方 Codex 客户端据此触发完整历史重放的稳定错误码。 +/// 官方 Codex 客户端据此触发完整历史重放的稳定错误码 pub(crate) const PREVIOUS_RESPONSE_NOT_FOUND_CODE: &str = "previous_response_not_found"; -/// Responses WebSocket 用于回传同一 turn 不透明状态的官方 client metadata 键。 +/// Responses WebSocket 用于回传同一 turn 不透明状态的官方 client metadata 键 pub(crate) const X_CODEX_TURN_STATE_CLIENT_METADATA_KEY: &str = "x-codex-turn-state"; -/// 本地生成 history unavailable 错误时使用的官方提示文本。 +/// 本地生成 history unavailable 错误时使用的官方提示文本 pub(crate) const PREVIOUS_RESPONSE_NOT_FOUND_MESSAGE: &str = "Previous response was not found. Retrying the full request."; -/// Codex 自动审批(Guardian)请求声明的子代理类型。 +/// Codex 自动审批(Guardian)请求声明的子代理类型 const GUARDIAN_SUBAGENT_KIND: &str = "guardian"; -/// Codex Responses 上游请求体。 +/// Codex Responses 上游请求体 /// -/// 发往上游的 Responses 请求。`body` 持有客户端原始 JSON object,逐字段(含顺序、 +/// 发往上游的 Responses 请求 +/// `body` 持有客户端原始 JSON object,逐字段(含顺序、 /// 含未知字段)透传上游,是上游请求体的唯一来源;`use_websocket`/`force_http_sse` -/// 仅用于本地传输选择,不写入 body。常用字段通过访问器方法读写。 -/// 普通客户端请求不修改 body;模型路由只写入明确受控字段。 +/// 仅用于本地传输选择,不写入 body +/// 常用字段通过访问器方法读写 +/// 普通客户端请求不修改 body;模型路由只写入明确受控字段 /// -/// 其余字段是代理控制状态,不进上游 body(原 `#[serde(skip)]` 字段)。 +/// 其余字段是代理控制状态,不进上游 body(原 `#[serde(skip)]` 字段) #[derive(Clone)] pub struct CodexResponsesRequest { - /// 上游请求体(唯一真相源)。 + /// 上游请求体(唯一真相源) body: Map, - /// API 边界保存、Provider 逐条恢复的普通客户端请求头。 + /// API 边界保存、Provider 逐条恢复的普通客户端请求头 pub(crate) passthrough_headers: HeaderMap, - /// 是否由客户端显式提供了 prompt cache key。 + /// 是否由客户端显式提供了 prompt cache key pub explicit_prompt_cache_key: bool, - /// 客户端会话 ID。 + /// 客户端会话 ID pub client_conversation_id: Option, - /// 客户端 session ID,仅保留在受控本地上下文。 + /// 客户端 session ID,仅保留在受控本地上下文 pub client_session_id: Option, - /// 客户端 thread ID,仅保留在受控本地上下文。 + /// 客户端 thread ID,仅保留在受控本地上下文 pub client_thread_id: Option, - /// 客户端 request ID,仅保留在受控本地上下文。 + /// 客户端 request ID,仅保留在受控本地上下文 pub client_request_id: Option, - /// 客户端 turn ID,仅保留在受控本地上下文。 + /// 客户端 turn ID,仅保留在受控本地上下文 pub client_turn_id: Option, - /// 连接池和 affinity 使用的本地会话身份,不发送上游。 + /// 连接池和 affinity 使用的本地会话身份,不发送上游 pub local_conversation_id: Option, - /// 变体身份键。 + /// 变体身份键 pub variant_identity: Option, - /// 代理侧识别的客户端 IP,仅用于管理端使用记录展示。 + /// 代理侧识别的客户端 IP,仅用于管理端使用记录展示 pub client_ip: Option, - /// 客户端 User-Agent,仅用于管理端使用记录展示。 + /// 客户端 User-Agent,仅用于管理端使用记录展示 pub client_user_agent: Option, - /// 已鉴权客户端 API key 的稳定 ID,仅用于事实归因。 + /// 已鉴权客户端 API key 的稳定 ID,仅用于事实归因 pub client_api_key_id: Option, - /// 是否偏好 WebSocket 传输。 + /// 是否偏好 WebSocket 传输 pub use_websocket: bool, - /// 是否强制 HTTP SSE。 + /// 是否强制 HTTP SSE pub force_http_sse: bool, - /// turn state 透传头。 + /// turn state 透传头 pub turn_state: Option, - /// turn metadata 透传头。 + /// turn metadata 透传头 pub turn_metadata: Option, - /// beta features 透传头。 + /// beta features 透传头 pub beta_features: Option, - /// 下游 `version` 扩展头;存在时上游值由 Desktop 版本画像统一生成。 + /// 下游 `version` 扩展头;存在时上游值由 Desktop 版本画像统一生成 pub version: Option, - /// timing metrics 透传头。 + /// timing metrics 透传头 pub include_timing_metrics: Option, - /// Responses Lite 请求语义;HTTP 使用 header,WebSocket 使用 client metadata 投影。 + /// Responses Lite 请求语义;HTTP 使用 header,WebSocket 使用 client metadata 投影 pub responses_lite: Option, - /// Memory consolidation 请求语义;HTTP 与 WebSocket opening 均使用 header。 + /// Memory consolidation 请求语义;HTTP 与 WebSocket opening 均使用 header pub memgen_request: Option, - /// Codex window ID。 + /// Codex window ID pub codex_window_id: Option, - /// 代理分配的下游 WebSocket 连接 ID,仅用于隔离本地连接池通道。 + /// 代理分配的下游 WebSocket 连接 ID,仅用于隔离本地连接池通道 pub downstream_websocket_connection_id: Option, - /// 父线程 ID。 + /// 父线程 ID pub parent_thread_id: Option, - /// 已知 previous response 的持久化范围,仅用于本地 transport 校验。 + /// 已知 previous response 的持久化范围,仅用于本地 transport 校验 pub previous_response_scope: Option, } @@ -97,7 +101,7 @@ impl fmt::Debug for CodexResponsesRequest { } } -/// previous response 在上游的可续接范围。 +/// previous response 在上游的可续接范围 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, serde::Deserialize)] #[serde(rename_all = "snake_case")] pub enum PreviousResponseScope { @@ -107,7 +111,7 @@ pub enum PreviousResponseScope { } impl Serialize for CodexResponsesRequest { - /// 上游 body 序列化即原始 `body` map(HTTP SSE 直发;WebSocket 在外层前置 `type`)。 + /// 上游 body 序列化即原始 `body` map(HTTP SSE 直发;WebSocket 在外层前置 `type`) fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, @@ -116,28 +120,28 @@ impl Serialize for CodexResponsesRequest { } } -/// Codex Responses 请求对上游传输的显式要求。 +/// Codex Responses 请求对上游传输的显式要求 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TransportRequirement { - /// 客户端显式要求 HTTP。 + /// 客户端显式要求 HTTP HttpRequired, - /// `generate=false + store=false` 预热必须保留在同一条 WebSocket。 + /// `generate=false + store=false` 预热必须保留在同一条 WebSocket ExplicitWebSocketWarmup, - /// 客户端 WebSocket 的非持久化新链,默认在池化连接上建立后续续接状态。 - /// Provider 可在发送前对 OAuth 大新链选择 HTTP,后续由客户端完整重放。 + /// 客户端 WebSocket 的非持久化新链,默认在池化连接上建立后续续接状态 + /// Provider 可在发送前对 OAuth 大新链选择 HTTP,后续由客户端完整重放 WebSocketNewChain, - /// 只能使用持有指定 connection-local response 的精确 WebSocket。 + /// 只能使用持有指定 connection-local response 的精确 WebSocket ExactWebSocketContinuation, - /// previous response 已持久化,允许 WebSocket 或 HTTP/2。 + /// previous response 已持久化,允许 WebSocket 或 HTTP/2 PersistedContinuation, - /// previous response 的所有权未知,只允许当前选定账号原样尝试。 + /// previous response 的所有权未知,只允许当前选定账号原样尝试 ExternalUnknown, - /// 没有 previous response 的普通新链。 + /// 没有 previous response 的普通新链 NewChain, } impl TransportRequirement { - /// 默认是否要求 WebSocket;Provider 的 OAuth 大新链预检可在发送前选择 HTTP。 + /// 默认是否要求 WebSocket;Provider 的 OAuth 大新链预检可在发送前选择 HTTP pub fn requires_websocket(self) -> bool { matches!( self, @@ -147,7 +151,7 @@ impl TransportRequirement { ) } - /// WebSocket 尚未发送 payload 时失败,是否允许切到同账号 HTTP/2。 + /// WebSocket 尚未发送 payload 时失败,是否允许切到同账号 HTTP/2 pub fn allows_pre_send_http_fallback(self) -> bool { matches!( self, @@ -155,12 +159,12 @@ impl TransportRequirement { ) } - /// 无续接依赖的新请求可以在明确的连接级拒绝后重新建连。 + /// 无续接依赖的新请求可以在明确的连接级拒绝后重新建连 pub fn allows_connection_restart(self) -> bool { matches!(self, Self::NewChain | Self::WebSocketNewChain) } - /// 用于审计与遥测的稳定名称。 + /// 用于审计与遥测的稳定名称 pub fn as_str(self) -> &'static str { match self { Self::HttpRequired => "http_required", @@ -174,7 +178,7 @@ impl TransportRequirement { } } -/// 将已完成 history preparation 的请求规范化为唯一 transport requirement。 +/// 将已完成 history preparation 的请求规范化为唯一 transport requirement pub fn transport_requirement(request: &CodexResponsesRequest) -> TransportRequirement { if !request.generate() && !request.store() { return TransportRequirement::ExplicitWebSocketWarmup; @@ -193,7 +197,7 @@ pub fn transport_requirement(request: &CodexResponsesRequest) -> TransportRequir } }, // HTTP store=false 的成功响应无法在池化 WebSocket 上续接,默认不走 HTTP 快路径; - // Provider 的 OAuth 大新链预检例外通过客户端完整重放恢复后续请求。 + // Provider 的 OAuth 大新链预检例外通过客户端完整重放恢复后续请求 None if request.downstream_websocket_connection_id.is_some() && !request.store() => { TransportRequirement::WebSocketNewChain } @@ -201,11 +205,11 @@ pub fn transport_requirement(request: &CodexResponsesRequest) -> TransportRequir } } -/// 单个 Responses 事件对计时系统提供的稳定语义信号。 +/// 单个 Responses 事件对计时系统提供的稳定语义信号 /// /// `protocol_progress` 只说明上游仍在工作,不能替代首字;`output_start` 标记首个会 /// 开启客户端输出的非前导事件(结构帧也算);其余字段分别标记客户端可消费的 -/// 输出、reasoning 输出与正文输出。 +/// 输出、reasoning 输出与正文输出 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct ResponseEventSignals { pub protocol_progress: bool, @@ -215,13 +219,14 @@ pub struct ResponseEventSignals { pub text_output: bool, } -/// 从已解析的 Responses 事件提取计时语义。 +/// 从已解析的 Responses 事件提取计时语义 /// /// `output_start` 在任意非前导、非失败事件上置位(含结构帧 /// `response.output_item.added`/`content_part.added`),用于开启首字计时;前导帧 -/// (`response.created`/`response.in_progress`)与失败帧不置位。语义输出仍要求 +/// (`response.created`/`response.in_progress`)与失败帧不置位 +/// 语义输出仍要求 /// 实际携带文本、工具参数、推理或图片结果,`response.output_item.done` 与终态帧 -/// 只有携带语义内容才算。 +/// 只有携带语义内容才算 pub fn response_event_signals(event_type: Option<&str>, value: &Value) -> ResponseEventSignals { let mut signals = ResponseEventSignals { protocol_progress: !matches!(event_type, Some("response.failed" | "error")), @@ -350,7 +355,7 @@ fn output_item_signals(item: &Value) -> ResponseEventSignals { signals.semantic_output = signals.text_output; } Some(item_type) if item_type.ends_with("_call") => { - // done 的工具调用本身已进入不可安全重试的语义边界;不依赖每种工具的字段表。 + // done 的工具调用本身已进入不可安全重试的语义边界;不依赖每种工具的字段表 signals.semantic_output = true; } _ => { @@ -392,24 +397,24 @@ fn is_tool_execution_event(event_type: &str) -> bool { ) } -/// Codex Responses SSE 失败事件。 +/// Codex Responses SSE 失败事件 #[derive(Clone, PartialEq, Eq)] pub struct ResponsesSseFailure { - /// SSE event 名称。 + /// SSE event 名称 pub event: String, - /// 上游错误消息。 + /// 上游错误消息 pub message: String, - /// 上游错误码。 + /// 上游错误码 pub upstream_code: Option, - /// 上游显式错误类型;不从业务码推导。 + /// 上游显式错误类型;不从业务码推导 pub upstream_type: Option, - /// 上游显式状态码;不从业务码或错误类型推导。 + /// 上游显式状态码;不从业务码或错误类型推导 pub explicit_status_code: Option, - /// 上游显式重试间隔,或从官方限流消息中解析出的重试间隔。 + /// 上游显式重试间隔,或从官方限流消息中解析出的重试间隔 pub retry_after_seconds: Option, - /// 当前错误事件自身携带的请求 ID;不是连接 opening ID。 + /// 当前错误事件自身携带的请求 ID;不是连接 opening ID pub(crate) request_id: Option, - /// 上游错误事件的原始 JSON data;只应在明确的失败审计边界读取。 + /// 上游错误事件的原始 JSON data;只应在明确的失败审计边界读取 raw_body: String, } @@ -448,7 +453,7 @@ impl ResponsesSseFailure { } } - /// 返回上游错误事件未经重编码的 JSON data。 + /// 返回上游错误事件未经重编码的 JSON data #[must_use] pub fn raw_body(&self) -> &str { &self.raw_body @@ -495,10 +500,10 @@ fn failure_error(value: &Value) -> Option<&Value> { } impl CodexResponsesRequest { - /// 从客户端原始 Responses JSON object 构造上游请求。 + /// 从客户端原始 Responses JSON object 构造上游请求 /// - /// 客户端提供的字段(含未知字段)原样保留在 `body` 中透传上游。 - /// 协议默认值仅由类型化访问器在本地解释,不写回上游正文。 + /// 客户端提供的字段(含未知字段)原样保留在 `body` 中透传上游 + /// 协议默认值仅由类型化访问器在本地解释,不写回上游正文 pub fn from_body(body: Map) -> Self { Self { body, @@ -530,19 +535,19 @@ impl CodexResponsesRequest { } } - /// 上游 body 的只读视图。 + /// 上游 body 的只读视图 pub fn body(&self) -> &Map { &self.body } - /// Provider adapter 编码阶段写入已经白名单校验的上游字段。 + /// Provider adapter 编码阶段写入已经白名单校验的上游字段 pub(crate) fn body_mut(&mut self) -> &mut Map { &mut self.body } // --- body 字段类型化访问器(上游语义字段,透传不重写)--- - /// 模型名。 + /// 模型名 pub fn model(&self) -> &str { self.body .get("model") @@ -550,7 +555,7 @@ impl CodexResponsesRequest { .unwrap_or_default() } - /// 指令文本(缺省空串)。 + /// 指令文本(缺省空串) pub fn instructions(&self) -> &str { self.body .get("instructions") @@ -558,7 +563,7 @@ impl CodexResponsesRequest { .unwrap_or_default() } - /// 输入条目切片(非数组时为空)。 + /// 输入条目切片(非数组时为空) pub fn input(&self) -> &[Value] { self.body .get("input") @@ -566,7 +571,7 @@ impl CodexResponsesRequest { .map_or(&[], Vec::as_slice) } - /// 是否流式返回。 + /// 是否流式返回 pub fn stream(&self) -> bool { self.body .get("stream") @@ -574,7 +579,7 @@ impl CodexResponsesRequest { .unwrap_or(true) } - /// 是否要求上游存储响应。 + /// 是否要求上游存储响应 pub fn store(&self) -> bool { self.body .get("store") @@ -582,7 +587,7 @@ impl CodexResponsesRequest { .unwrap_or(false) } - /// 是否实际生成模型响应;官方预热请求会显式传入 `false`。 + /// 是否实际生成模型响应;官方预热请求会显式传入 `false` pub fn generate(&self) -> bool { self.body .get("generate") @@ -590,12 +595,12 @@ impl CodexResponsesRequest { .unwrap_or(true) } - /// reasoning 配置(透传,不规整)。 + /// reasoning 配置(透传,不规整) pub fn reasoning(&self) -> Option<&Value> { self.body.get("reasoning") } - /// 工具定义数组(非数组或空时 None)。 + /// 工具定义数组(非数组或空时 None) pub fn tools(&self) -> Option<&[Value]> { self.body .get("tools") @@ -604,17 +609,17 @@ impl CodexResponsesRequest { .map(Vec::as_slice) } - /// include 列表(透传原值)。 + /// include 列表(透传原值) pub fn include(&self) -> Option<&Value> { self.body.get("include") } - /// service tier(透传原值)。 + /// service tier(透传原值) pub fn service_tier(&self) -> Option<&str> { self.body.get("service_tier").and_then(Value::as_str) } - /// 只覆盖顶层 Fast 请求,显式使用官方标准档退出值。 + /// 只覆盖顶层 Fast 请求,显式使用官方标准档退出值 pub(crate) fn apply_fast_policy(&mut self, disable_fast: bool) { if disable_fast && self.service_tier().is_some_and(|tier| { @@ -629,14 +634,14 @@ impl CodexResponsesRequest { } } - /// 前一个 response ID。 + /// 前一个 response ID pub fn previous_response_id(&self) -> Option<&str> { self.body .get("previous_response_id") .and_then(Value::as_str) } - /// 设置 / 清除前一个 response ID。 + /// 设置 / 清除前一个 response ID pub fn set_previous_response_id(&mut self, previous_response_id: Option) { match previous_response_id { Some(value) => { @@ -650,24 +655,24 @@ impl CodexResponsesRequest { } } - /// 提示缓存键。 + /// 提示缓存键 pub fn prompt_cache_key(&self) -> Option<&str> { self.body.get("prompt_cache_key").and_then(Value::as_str) } - /// client metadata(透传原值)。 + /// client metadata(透传原值) pub fn client_metadata(&self) -> Option<&Value> { self.body.get("client_metadata") } - /// 提取 Codex 请求类型、子代理类型、推理预设与压缩语义。 + /// 提取 Codex 请求类型、子代理类型、推理预设与压缩语义 pub fn semantics(&self) -> CodexRequestSemantics { let mut semantics = codex_responses_request_semantics_with_turn_metadata( self.body(), self.turn_metadata.as_deref(), ); - // 官方 generate=false 只准备连接与上下文,不是模型推理。 - // 预热分类参与用量筛选,不能仅凭客户端的 request_kind 提示排除真实推理。 + // 官方 generate=false 只准备连接与上下文,不是模型推理 + // 预热分类参与用量筛选,不能仅凭客户端的 request_kind 提示排除真实推理 if !self.generate() { semantics.request_kind = Some("prewarm".to_owned()); } else if semantics.request_kind.as_deref() == Some("prewarm") { @@ -676,11 +681,12 @@ impl CodexResponsesRequest { semantics } - /// 返回请求语义与 child transport 隔离所用的子代理区分值。 + /// 返回请求语义与 child transport 隔离所用的子代理区分值 /// /// Codex 原生请求在 turn metadata 中声明 `subagent_kind`;兼容客户端也可通过 - /// `client_metadata.x-openai-subagent` 声明同一语义。它不拆分根会话的账号首选项; - /// `thread_spawn` 仍用它派生独立 WebSocket/continuation transport identity。 + /// `client_metadata.x-openai-subagent` 声明同一语义 + /// 它不拆分根会话的账号首选项; + /// `thread_spawn` 仍用它派生独立 WebSocket/continuation transport identity pub fn subagent_kind(&self) -> Option { self.semantics() .subagent_kind @@ -696,12 +702,12 @@ impl CodexResponsesRequest { }) } - /// Codex 执行命令前的 Guardian 自动审批请求;客户端以 `guardian` 子代理类型声明。 + /// Codex 执行命令前的 Guardian 自动审批请求;客户端以 `guardian` 子代理类型声明 pub fn is_guardian(&self) -> bool { self.subagent_kind().as_deref() == Some(GUARDIAN_SUBAGENT_KIND) } - /// 设置 / 合并 client metadata。 + /// 设置 / 合并 client metadata pub fn set_client_metadata(&mut self, client_metadata: Option) { match client_metadata { Some(value) => { @@ -713,7 +719,7 @@ impl CodexResponsesRequest { } } - /// 替换客户端原本提供的账号身份字段;无法安全重建时删除该字段。 + /// 替换客户端原本提供的账号身份字段;无法安全重建时删除该字段 pub fn replace_existing_identity_field(&mut self, key: &str, value: Option<&str>) { if !self.body.contains_key(key) { return; diff --git a/backend/crates/providers/openai/src/transport/protocol/websocket.rs b/backend/crates/providers/openai/src/transport/protocol/websocket.rs index 86f1194fd..451afb005 100644 --- a/backend/crates/providers/openai/src/transport/protocol/websocket.rs +++ b/backend/crates/providers/openai/src/transport/protocol/websocket.rs @@ -1,3 +1,5 @@ +//! Codex WebSocket 请求帧编码、事件解析与脱敏审计投影 + use gateway_protocol::openai::sse::encode_sse_event; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; @@ -8,55 +10,55 @@ use crate::transport::protocol::responses::{ const REDACTED_PAYLOAD_VALUE: &str = ""; -/// WebSocket 握手审计快照。 +/// WebSocket 握手审计快照 #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] pub struct OpeningAuditSnapshot { - /// 请求行。 + /// 请求行 pub request_line: String, - /// 请求头顺序。 + /// 请求头顺序 pub header_order: Vec, - /// 脱敏后的请求头。 + /// 脱敏后的请求头 pub headers: Vec, } -/// WebSocket 握手审计请求头。 +/// WebSocket 握手审计请求头 #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] pub struct OpeningAuditHeader { - /// 请求头名。 + /// 请求头名 pub name: String, - /// 请求头值。 + /// 请求头值 pub value: String, } -/// WebSocket payload 审计快照。 +/// WebSocket payload 审计快照 #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] pub struct PayloadAuditSnapshot { - /// 按构造顺序记录的顶层字段。 + /// 按构造顺序记录的顶层字段 pub top_level_keys: Vec, - /// 脱敏后的 JSON payload。 + /// 脱敏后的 JSON payload pub body: Value, } -/// WebSocket 审计产物。 +/// WebSocket 审计产物 #[derive(Debug, Clone, Serialize, PartialEq, Eq)] pub struct WebSocketAuditArtifact { - /// 实际选择的传输模式。 + /// 实际选择的传输模式 pub transport_mode: String, - /// 当前请求是否允许 HTTP/SSE fallback。 + /// 当前请求是否允许 HTTP/SSE fallback pub fallback_allowed: bool, - /// 打开握手快照。 + /// 打开握手快照 pub opening: Option, - /// 首个 `response.create` payload 快照。 + /// 首个 `response.create` payload 快照 pub payload: Option, } -/// 将一条公开 WebSocket JSON 事件编码为 SSE 帧。 +/// 将一条公开 WebSocket JSON 事件编码为 SSE 帧 pub fn websocket_event_to_sse_frame(raw: &str) -> Option { let value = serde_json::from_str::(raw).ok()?; websocket_event_frame(&value, raw) } -/// 复用已解析的 JSON,剥离内部帧并原样转发公开事件。 +/// 复用已解析的 JSON,剥离内部帧并原样转发公开事件 pub(crate) fn websocket_event_frame(value: &Value, raw: &str) -> Option { let event = websocket_event_type(value)?; if is_internal_websocket_event(event) || event == "response.metadata" { @@ -73,7 +75,7 @@ fn is_internal_websocket_event(event: &str) -> bool { event == "codex.rate_limits" } -/// 提取 Responses WebSocket metadata 帧中的字符串响应头。 +/// 提取 Responses WebSocket metadata 帧中的字符串响应头 pub fn websocket_metadata_headers(value: &Value) -> Vec<(String, String)> { if !is_websocket_metadata_event(websocket_event_type(value)) { return Vec::new(); @@ -87,7 +89,7 @@ pub fn websocket_metadata_headers(value: &Value) -> Vec<(String, String)> { .collect() } -/// 从 Responses WebSocket metadata 帧中提取 `x-codex-turn-state`。 +/// 从 Responses WebSocket metadata 帧中提取 `x-codex-turn-state` pub fn websocket_metadata_turn_state(value: &Value) -> Option { websocket_metadata_headers(value) .into_iter() @@ -101,11 +103,11 @@ fn is_websocket_metadata_event(event: Option<&str>) -> bool { matches!(event, Some("response.metadata" | "codex.response.metadata")) } -/// 上游 WebSocket 连接寿命限制错误码。 +/// 上游 WebSocket 连接寿命限制错误码 pub(crate) const WEBSOCKET_CONNECTION_LIMIT_REACHED_CODE: &str = "websocket_connection_limit_reached"; -/// 若首个可投递 SSE 帧是连接寿命限制错误,保留原始失败事实而不改变恢复分类。 +/// 若首个可投递 SSE 帧是连接寿命限制错误,保留原始失败事实而不改变恢复分类 pub(crate) fn websocket_connection_limit_failure(frame: &[u8]) -> Option { let text = std::str::from_utf8(frame).ok()?; let event = gateway_protocol::openai::sse::parse_sse_events(text) @@ -138,9 +140,9 @@ fn json_value_as_string(value: &Value) -> Option { } } -/// 从正常完成或官方中断终态旁路提取 response ID,供连接池记录续接能力。 +/// 从正常完成或官方中断终态旁路提取 response ID,供连接池记录续接能力 /// -/// 该值不参与客户端 wire 的可交付性判断;无法读取时只是不记录连接内续接状态。 +/// 该值不参与客户端 wire 的可交付性判断;无法读取时只是不记录连接内续接状态 pub fn websocket_response_completed_id(value: &Value) -> Option { if value.get("type").and_then(Value::as_str) != Some("response.completed") && !websocket_response_is_interrupted(value) @@ -161,7 +163,7 @@ pub(crate) fn websocket_response_is_interrupted(value: &Value) -> bool { == Some("interrupted") } -/// 生成 Responses WebSocket payload 审计快照。 +/// 生成 Responses WebSocket payload 审计快照 pub fn websocket_payload_audit_snapshot(request: &CodexResponsesRequest) -> PayloadAuditSnapshot { let body = websocket_response_create_payload(request); PayloadAuditSnapshot { @@ -170,7 +172,7 @@ pub fn websocket_payload_audit_snapshot(request: &CodexResponsesRequest) -> Payl } } -/// 为单次 opening 尝试构建 WebSocket 审计 artifact。 +/// 为单次 opening 尝试构建 WebSocket 审计 artifact pub fn websocket_audit_artifact_from_attempt( request: &CodexResponsesRequest, opening: OpeningAuditSnapshot, @@ -184,10 +186,10 @@ pub fn websocket_audit_artifact_from_attempt( } } -/// 生成 Responses WebSocket `response.create` payload。 +/// 生成 Responses WebSocket `response.create` payload /// /// payload = `{"type": "response.create"}` 加上原始上游 body 的全部字段 -/// (保持插入顺序,含未知字段),逐字段原样透传。 +/// (保持插入顺序,含未知字段),逐字段原样透传 pub fn websocket_response_create_payload(request: &CodexResponsesRequest) -> Value { let mut payload = Map::new(); payload.insert( @@ -200,10 +202,10 @@ pub fn websocket_response_create_payload(request: &CodexResponsesRequest) -> Val Value::Object(payload) } -/// 借用原始 body 序列化 `response.create` 帧,不复制字段。 +/// 借用原始 body 序列化 `response.create` 帧,不复制字段 /// /// 输出与序列化 [`websocket_response_create_payload`] 的合并 Map 逐字节一致: -/// body 自带 `type` 键时以 body 值置于首位,其余字段按插入顺序原样透传。 +/// body 自带 `type` 键时以 body 值置于首位,其余字段按插入顺序原样透传 struct ResponseCreateFrame<'a> { body: &'a Map, } @@ -226,7 +228,7 @@ impl serde::Serialize for ResponseCreateFrame<'_> { } } -/// 生成 Responses WebSocket `response.create` 文本帧内容。 +/// 生成 Responses WebSocket `response.create` 文本帧内容 pub fn websocket_response_create_payload_text( request: &CodexResponsesRequest, ) -> Result { diff --git a/backend/crates/providers/openai/src/transport/request.rs b/backend/crates/providers/openai/src/transport/request.rs index 82f757cf3..ed5e7fac0 100644 --- a/backend/crates/providers/openai/src/transport/request.rs +++ b/backend/crates/providers/openai/src/transport/request.rs @@ -1,4 +1,4 @@ -//! 核心 Generate operation 到 Codex Responses wire request 的严格编码。 +//! 核心 Generate operation 到 Codex Responses wire request 的严格编码 use std::io; @@ -88,7 +88,7 @@ impl RequestAccountScope { } } -/// Provider 专属编码错误;不保存 prompt、schema 或 option 值。 +/// Provider 专属编码错误;不保存 prompt、schema 或 option 值 #[derive(Debug, thiserror::Error, PartialEq, Eq)] pub enum CodexRequestEncodeError { #[error("Codex request is missing its OpenAI protocol payload")] @@ -131,7 +131,7 @@ pub(crate) fn align_structured_location_fields( now: DateTime, location: &CodexRequestLocation, ) { - // 只改写带环境标记的日期和时区;epoch 时间戳保持绝对时间原值。 + // 只改写带环境标记的日期和时区;epoch 时间戳保持绝对时间原值 let current_date = now .with_timezone(&location.timezone) .format("%Y-%m-%d") @@ -285,8 +285,8 @@ impl ExtractedRequestContext { let client_metadata = body.get("client_metadata").and_then(Value::as_object); Self { // 官方 downstream WebSocket 无法逐帧更新 HTTP header,因此把 - // response.metadata 返回的 turn state 放回下一帧 client_metadata。 - // Provider 仍会在账号/turn 归属确定后决定是否允许复用该状态。 + // response.metadata 返回的 turn state 放回下一帧 client_metadata + // Provider 仍会在账号/turn 归属确定后决定是否允许复用该状态 turn_state: body_string(body, "turnState").or_else(|| { client_metadata.and_then(|metadata| { string_value(metadata.get(X_CODEX_TURN_STATE_CLIENT_METADATA_KEY)) @@ -305,12 +305,12 @@ impl ExtractedRequestContext { .and_then(|metadata| string_value(metadata.get(TURN_ID_CLIENT_METADATA_KEY))) }), // Responses Lite 在 WebSocket body 中的这个键是官方 header 投影, - // 不是普通上下文字段的 metadata 回退。 + // 不是普通上下文字段的 metadata 回退 responses_lite: client_metadata.and_then(|metadata| { string_value(metadata.get(WS_REQUEST_HEADER_RESPONSES_LITE_CLIENT_METADATA_KEY)) }), // Memory consolidation 只由官方请求头提供;body 中同名字段不参与 - // transport 事实提取。 + // transport 事实提取 memgen_request: None, } } @@ -353,8 +353,8 @@ pub(crate) fn derive_conversation_anchor( }) } -/// `thread_spawn` 与父任务共享根会话,但它本身是独立的子任务执行。 -/// 因此子线程必须拥有独立的 continuation/WebSocket 传输身份;账号亲和另行派生。 +/// `thread_spawn` 与父任务共享根会话,但它本身是独立的子任务执行 +/// 因此子线程必须拥有独立的 continuation/WebSocket 传输身份;账号亲和另行派生 fn thread_spawn_conversation_anchor( request: &CodexResponsesRequest, ) -> Option<(&'static str, String)> { @@ -450,12 +450,13 @@ fn normalize_conversation_anchor_text(text: &str) -> String { rest.to_owned() } -/// 把客户端正文收敛到当前 lease 的账号身份边界。 +/// 把客户端正文收敛到当前 lease 的账号身份边界 /// /// 真实 account ID 由随后构造的 `CodexRequestContext` 注入请求头;installation ID -/// 统一写入 Core 的 `client_metadata["x-codex-installation-id"]`,并替换原有兼容字段。 -/// 绝不接受客户端提供的 token、cookie 或账号身份。`input` 是 Responses 的可回放会话正文, -/// item ID、encrypted content 和 compaction 都必须原样保留。 +/// 统一写入 Core 的 `client_metadata["x-codex-installation-id"]`,并替换原有兼容字段 +/// 绝不接受客户端提供的 token、cookie 或账号身份 +/// `input` 是 Responses 的可回放会话正文, +/// item ID、encrypted content 和 compaction 都必须原样保留 pub(crate) fn scope_request_to_account( request: &mut CodexResponsesRequest, installation_id: &str, @@ -490,7 +491,7 @@ pub(crate) fn scope_request_to_account( request.body_mut().remove(*key); } } else { - // 同账号的透传 turn metadata 也会覆盖重建的请求头,安装身份须同步改写。 + // 同账号的透传 turn metadata 也会覆盖重建的请求头,安装身份须同步改写 for (name, value) in &mut request.passthrough_headers { if name == "x-codex-turn-metadata" && let Ok(raw) = value.to_str() @@ -537,11 +538,11 @@ pub(crate) fn scope_request_to_account( metadata.remove(*key); } // Guardian 顶层父引用指向原账号的响应;turn metadata 内的同名 - // 扩展只是客户端关联信息,不能加入各层共用的清理名单。 + // 扩展只是客户端关联信息,不能加入各层共用的清理名单 metadata.remove("parent_response_id"); } // 官方 Core 在 client_metadata 使用带 x-codex 前缀的键, - // turn metadata 内仍使用 installation_id;两处均取当前账号的安装身份。 + // turn metadata 内仍使用 installation_id;两处均取当前账号的安装身份 metadata.insert( "x-codex-installation-id".to_owned(), Value::String(installation_id.to_owned()), @@ -616,9 +617,9 @@ pub(crate) fn scope_turn_metadata( metadata.insert((*key).to_owned(), Value::String(installation_id.to_owned())); } } - // Codex 的 turn metadata 同时承载于 HTTP header 与 WS client_metadata。 + // Codex 的 turn metadata 同时承载于 HTTP header 与 WS client_metadata // 改写安装 ID 后仍须保持官方 to_ascii_json_string 的编码合同;普通 - // to_string 会把中文工作区路径还原成 UTF-8,触发上游 WS metadata 后 Close 1000。 + // to_string 会把中文工作区路径还原成 UTF-8,触发上游 WS metadata 后 Close 1000 let mut bytes = Vec::new(); metadata .serialize(&mut serde_json::Serializer::with_formatter( @@ -688,7 +689,7 @@ fn replace_metadata_field(metadata: &mut Map, key: &str, value: O // 迁移契约是 header-authoritative:连接边界解析出的协议上下文(官方请求头) // 优先;body 顶层别名只在 header 缺失时兜底(例如 WebSocket 帧无法逐轮携带 -// 请求头的场景)。 +// 请求头的场景) fn apply_protocol_context(request: &mut CodexResponsesRequest, context: &Map) { request.passthrough_headers = decode_passthrough_headers(context); request.turn_state = diff --git a/backend/crates/providers/openai/src/transport/reset_credits.rs b/backend/crates/providers/openai/src/transport/reset_credits.rs index fe537aec7..ec53a6e25 100644 --- a/backend/crates/providers/openai/src/transport/reset_credits.rs +++ b/backend/crates/providers/openai/src/transport/reset_credits.rs @@ -1,4 +1,4 @@ -//! Codex Desktop 主动额度重置卡 HTTP contract。 +//! Codex Desktop 主动额度重置卡 HTTP contract use chrono::{DateTime, Utc}; use reqwest::StatusCode; @@ -16,13 +16,13 @@ use super::{ response_meta, }; -/// 单次 reset-credit 响应允许保留和解析的最大字节数。 +/// 单次 reset-credit 响应允许保留和解析的最大字节数 pub const MAX_CODEX_RESET_CREDITS_BODY_BYTES: usize = 1024 * 1024; const MAX_RESET_CREDITS: usize = 1024; const MAX_IDENTIFIER_BYTES: usize = 512; const MAX_TEXT_BYTES: usize = 2048; -/// 上游返回的一张安全重置卡投影。 +/// 上游返回的一张安全重置卡投影 #[derive(Debug, Clone, PartialEq, Eq)] pub struct CodexRateLimitResetCredit { pub id: String, @@ -32,14 +32,15 @@ pub struct CodexRateLimitResetCredit { pub reset_type: Option, } -/// 上游重置卡列表。 +/// 上游重置卡列表 #[derive(Debug, Clone, PartialEq, Eq)] pub struct CodexRateLimitResetCredits { pub available_count: u64, pub credits: Vec, } -/// 上游消费结果。`code` 的业务含义由调用方按官方状态机解释。 +/// 上游消费结果 +/// `code` 的业务含义由调用方按官方状态机解释 #[derive(Debug, Clone, PartialEq, Eq)] pub struct CodexRateLimitResetCreditsConsumeResult { pub code: String, @@ -80,7 +81,7 @@ struct ConsumeRequest<'a> { } impl CodexBackendClient { - /// 查询当前账号的 Codex Desktop 主动额度重置卡。 + /// 查询当前账号的 Codex Desktop 主动额度重置卡 pub async fn list_rate_limit_reset_credits( &self, context: CodexRequestContext<'_>, @@ -112,9 +113,9 @@ impl CodexBackendClient { }) } - /// 消费一张 Codex Desktop 主动额度重置卡。 + /// 消费一张 Codex Desktop 主动额度重置卡 /// - /// 仅自定义路由明确 404 时按原幂等键回退;传输失败由上层按不确定结果处理。 + /// 仅自定义路由明确 404 时按原幂等键回退;传输失败由上层按不确定结果处理 pub async fn consume_rate_limit_reset_credit( &self, context: CodexRequestContext<'_>, diff --git a/backend/crates/providers/openai/src/transport/response_meta.rs b/backend/crates/providers/openai/src/transport/response_meta.rs index e1f773933..4acde1c88 100644 --- a/backend/crates/providers/openai/src/transport/response_meta.rs +++ b/backend/crates/providers/openai/src/transport/response_meta.rs @@ -1,4 +1,4 @@ -//! 上游响应元数据提取辅助。 +//! 上游响应元数据提取辅助 use std::fmt; @@ -8,16 +8,16 @@ use reqwest::header::{HeaderMap, SET_COOKIE}; use super::diagnostics::CodexUpstreamDiagnostics; -/// Codex Responses 上游响应元数据。 +/// Codex Responses 上游响应元数据 #[derive(Clone, Default, PartialEq, Eq)] pub struct CodexResponseMetadata { - /// 上游实际选用的模型。 + /// 上游实际选用的模型 pub effective_model: Option, - /// 模型目录版本。 + /// 模型目录版本 pub models_etag: Option, - /// 上游是否声明响应包含 reasoning。 + /// 上游是否声明响应包含 reasoning pub reasoning_included: bool, - /// 允许交给 Core 的普通响应头;名称和值保持 transport 观察到的顺序与字节。 + /// 允许交给 Core 的普通响应头;名称和值保持 transport 观察到的顺序与字节 pub client_headers: Vec<(String, Bytes)>, } @@ -72,10 +72,10 @@ pub(super) fn response_metadata(headers: &HeaderMap) -> CodexResponseMetadata { response_metadata_from_client_headers(client_headers(headers)) } -/// 提取响应中可交给客户端 adapter 的普通头。 +/// 提取响应中可交给客户端 adapter 的普通头 /// /// 账号身份、凭据、cookie、逐跳头和已经由 adapter 重建的 framing 在进入 Core 前剔除; -/// 其余名称和值保持 HeaderMap 的多值顺序和原始字节。 +/// 其余名称和值保持 HeaderMap 的多值顺序和原始字节 pub(super) fn client_headers(headers: &HeaderMap) -> Vec<(String, Bytes)> { filter_client_headers(headers.iter().map(|(name, value)| { ( @@ -129,7 +129,7 @@ fn observe_typed_response_header(metadata: &mut CodexResponseMetadata, name: &st } } -/// 与官方 Codex 一致,优先读取 response.headers,再读取 WS metadata 的顶层 headers。 +/// 与官方 Codex 一致,优先读取 response.headers,再读取 WS metadata 的顶层 headers pub(super) fn reported_model_from_event(value: &serde_json::Value) -> Option<&str> { [value.pointer("/response/headers"), value.get("headers")] .into_iter() @@ -185,7 +185,7 @@ fn client_response_header_is_forwardable(name: &str, connection_options: &[Strin !matches!( name.as_str(), - // 逐跳和 framing 由下游 adapter 针对实际 JSON/SSE/WebSocket 载体重建。 + // 逐跳和 framing 由下游 adapter 针对实际 JSON/SSE/WebSocket 载体重建 "connection" | "keep-alive" | "proxy-connection" @@ -196,7 +196,7 @@ fn client_response_header_is_forwardable(name: &str, connection_options: &[Strin | "content-length" | "content-type" | "content-encoding" - // 上游账号、凭据和 cookie 不能跨越换号边界。 + // 上游账号、凭据和 cookie 不能跨越换号边界 | "authorization" | "x-api-key" | "www-authenticate" diff --git a/backend/crates/providers/openai/src/transport/session.rs b/backend/crates/providers/openai/src/transport/session.rs index 5958998ed..ed1c24129 100644 --- a/backend/crates/providers/openai/src/transport/session.rs +++ b/backend/crates/providers/openai/src/transport/session.rs @@ -1,8 +1,9 @@ -//! Codex 本地会话锚点的稳定派生。 +//! Codex 本地会话锚点的稳定派生 //! //! 旧版 Redis 会话亲和与 WebSocket 复用以运行数据目录下的 `identity_hmac_secret` -//! 生成的 `lc_` 值为键。该密钥是持久运行数据,不是账号凭据;保留它可避免 -//! 架构迁移后同一客户端会话全部换键。 +//! 生成的 `lc_` 值为键 +//! 该密钥是持久运行数据,不是账号凭据;保留它可避免 +//! 架构迁移后同一客户端会话全部换键 use std::fs; use std::io::Write as _; @@ -14,7 +15,7 @@ use sha2::{Digest, Sha256}; use super::protocol::responses::CodexResponsesRequest; use super::request::derive_conversation_anchor; -/// OpenAI Provider 的持久会话锚点密钥。 +/// OpenAI Provider 的持久会话锚点密钥 #[derive(Clone)] pub(crate) struct CodexSessionIdentity { secret: [u8; 32], @@ -58,7 +59,7 @@ impl CodexSessionIdentity { } } - /// 在账号选择前固定一次旧版 `lc_` 会话键。 + /// 在账号选择前固定一次旧版 `lc_` 会话键 pub(crate) fn prepare_local_conversation(&self, request: &mut CodexResponsesRequest) { if request.local_conversation_id.is_some() { return; diff --git a/backend/crates/providers/openai/src/transport/subscription.rs b/backend/crates/providers/openai/src/transport/subscription.rs index b3820743a..b69edccc2 100644 --- a/backend/crates/providers/openai/src/transport/subscription.rs +++ b/backend/crates/providers/openai/src/transport/subscription.rs @@ -1,4 +1,4 @@ -//! 当前绑定账号的订阅周期安全投影,与令牌有效期和额度状态无关。 +//! 当前绑定账号的订阅周期安全投影,与令牌有效期和额度状态无关 use std::time::Duration; @@ -19,7 +19,7 @@ pub struct CodexSubscription { } impl CodexBackendClient { - /// 可选展示查询失败只返回未知;自定义路由的 404 回退也计入同一查询预算。 + /// 可选展示查询失败只返回未知;自定义路由的 404 回退也计入同一查询预算 pub async fn fetch_subscription( &self, context: CodexRequestContext<'_>, diff --git a/backend/crates/providers/openai/src/transport/tls.rs b/backend/crates/providers/openai/src/transport/tls.rs index cdd2d3742..9dfd43b99 100644 --- a/backend/crates/providers/openai/src/transport/tls.rs +++ b/backend/crates/providers/openai/src/transport/tls.rs @@ -1,3 +1,5 @@ +//! OpenAI 出站 TLS 配置、系统根证书与自定义 CA 加载 + use std::{ env, fs, io, path::PathBuf, @@ -16,8 +18,9 @@ const CA_CERT_HINT: &str = "If you set CODEX_CA_CERTIFICATE or SSL_CERT_FILE, en type PemSection = (SectionKind, Vec); /// 与官方 Codex 的 rustls 路径一致,安装支持 P-521 和混合后量子密钥交换的 -/// aws-lc provider。依赖图也包含 ring,因此不能让 rustls 自动选择 provider。 -/// 与官方嵌入场景一致,保留宿主在初始化之前已经安装的 provider。 +/// aws-lc provider +/// 依赖图也包含 ring,因此不能让 rustls 自动选择 provider +/// 与官方嵌入场景一致,保留宿主在初始化之前已经安装的 provider pub fn ensure_rustls_provider() { static INSTALL: OnceLock<()> = OnceLock::new(); INSTALL.get_or_init(|| { @@ -25,17 +28,17 @@ pub fn ensure_rustls_provider() { }); } -/// 自定义 CA 证书环境变量名。 +/// 自定义 CA 证书环境变量名 pub const CODEX_CA_CERT_ENV: &str = "CODEX_CA_CERTIFICATE"; -/// 系统 CA 文件环境变量名。 +/// 系统 CA 文件环境变量名 pub const SSL_CERT_FILE_ENV: &str = "SSL_CERT_FILE"; -/// 自定义 CA 错误。 +/// 自定义 CA 错误 #[derive(Debug, Error)] pub enum CustomCaError { #[error("invalid account proxy configuration")] ProxyConfiguration, - /// 读取 CA 证书文件失败。 + /// 读取 CA 证书文件失败 #[error( "Failed to read CA certificate file {} selected by {}: {source}. {hint}", path.display(), @@ -47,7 +50,7 @@ pub enum CustomCaError { path: PathBuf, source: io::Error, }, - /// CA 证书文件格式无效。 + /// CA 证书文件格式无效 #[error( "Failed to load CA certificates from {} selected by {}: {detail}. {hint}", path.display(), @@ -59,7 +62,7 @@ pub enum CustomCaError { path: PathBuf, detail: String, }, - /// 证书无法注册为 reqwest 根证书。 + /// 证书无法注册为 reqwest 根证书 #[error( "Failed to parse certificate #{certificate_index} from {} selected by {}: {source}. {hint}", path.display(), @@ -72,7 +75,7 @@ pub enum CustomCaError { certificate_index: usize, source: reqwest::Error, }, - /// 证书无法注册到 rustls root store。 + /// 证书无法注册到 rustls root store #[error( "Failed to register certificate #{certificate_index} from {} selected by {} in rustls root store: {source}. {hint}", path.display(), @@ -85,39 +88,39 @@ pub enum CustomCaError { certificate_index: usize, source: rustls::Error, }, - /// 使用自定义 CA 构建 reqwest client 失败。 + /// 使用自定义 CA 构建 reqwest client 失败 #[error("Failed to build HTTP client while using CA bundle from {} ({}): {source}", source_env, path.display())] BuildClientWithCustomCa { source_env: &'static str, path: PathBuf, source: reqwest::Error, }, - /// 使用系统根证书构建 reqwest client 失败。 + /// 使用系统根证书构建 reqwest client 失败 #[error("Failed to build HTTP client while using system root certificates: {0}")] BuildClientWithSystemRoots(reqwest::Error), - /// 读取系统根证书失败。 + /// 读取系统根证书失败 #[error("Failed to load native root certificates for custom CA transport: {0}")] LoadNativeRoots(io::Error), } -/// 自定义 CA 结果类型。 +/// 自定义 CA 结果类型 pub type CustomCaResult = Result; -/// 在 reqwest builder 上应用自定义 CA。 +/// 在 reqwest builder 上应用自定义 CA pub fn build_reqwest_client_with_custom_ca( builder: reqwest::ClientBuilder, ) -> CustomCaResult { build_reqwest_client_with_env(&ProcessEnv, builder) } -/// 返回当前自定义 CA 的缓存键。 +/// 返回当前自定义 CA 的缓存键 pub fn custom_ca_env_cache_key() -> Option { ProcessEnv .configured_ca_bundle() .map(|bundle| format!("{}={}", bundle.source_env, bundle.path.display())) } -/// 构建 rustls client config,若未配置自定义 CA 则返回 `None`。 +/// 构建 rustls client config,若未配置自定义 CA 则返回 `None` pub fn maybe_build_rustls_client_config_with_custom_ca() -> CustomCaResult>> { maybe_build_rustls_client_config_with_env(&ProcessEnv) diff --git a/backend/crates/providers/openai/src/transport/usage.rs b/backend/crates/providers/openai/src/transport/usage.rs index 890594203..37cb86445 100644 --- a/backend/crates/providers/openai/src/transport/usage.rs +++ b/backend/crates/providers/openai/src/transport/usage.rs @@ -1,3 +1,5 @@ +//! OpenAI 用量解析、模型价格匹配与费用明细计算 + use gateway_core::metering::{ CalculatedCost, CalculatedCostAmounts, CalculatedCostBreakdown, CalculatedCostRates, CurrencyCode, Decimal, Money, @@ -18,7 +20,7 @@ const WEB_SEARCH_CALL_TICKS: u128 = 100_000_000; const WEB_SEARCH_PREVIEW_NON_REASONING_CALL_TICKS: u128 = 250_000_000; const FILE_SEARCH_CALL_TICKS: u128 = 25_000_000; -/// OpenAI 公开 Token 价格计算所需的单次用量事实。 +/// OpenAI 公开 Token 价格计算所需的单次用量事实 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct OpenAiBillingUsage { input_tokens: u64, @@ -33,7 +35,7 @@ pub struct OpenAiBillingUsage { } impl OpenAiBillingUsage { - /// 构造不含托管工具费用的 Token 用量。 + /// 构造不含托管工具费用的 Token 用量 #[must_use] pub const fn new( input_tokens: u64, @@ -113,7 +115,7 @@ struct TokenRates { impl TokenRates { const ZERO: Self = Self::new(0, 0, 0); - /// 参数单位为 USD / 1M Token 的万分之一,数值也恰好等于单 Token 的 USD ticks。 + /// 参数单位为 USD / 1M Token 的万分之一,数值也恰好等于单 Token 的 USD ticks const fn new(input_ticks: u128, output_ticks: u128, cache_read_ticks: u128) -> Self { Self { input_ticks, @@ -197,9 +199,8 @@ impl ModelPricing { if long_context && self.unpriced_long_context { return None; } - // Only models with a published long-context column switch at the threshold. - // A dash in that column is not a second, unavailable price tier for models - // whose entire supported context is covered by the short-context price. + // 只有公布长上下文价格的模型才在阈值处切换费率 + // 若短上下文价格覆盖全部支持范围,长上下文栏的横线不代表额外的不可用档位 let uses_long_rates = long_context && self.long_standard.is_configured(); let rates = match (tier, uses_long_rates) { (PricingTier::Standard, false) => self.standard, @@ -226,14 +227,14 @@ struct PricingRule { pricing: ModelPricing, } -// 价格来源:https://developers.openai.com/api/docs/pricing。 -// 只登记已核验的常规价格及服务档位;临时优惠不写入内置价目。 -// 原有规则于 2026-09-09 核验,GPT-6.1 Sol 于 2026-09-29 核验,其余新增型号见对应条目。 +// 价格来源:https://developers.openai.com/api/docs/pricing +// 只登记已核验的常规价格及服务档位;临时优惠不写入内置价目 +// 原有规则于 2026-09-09 核验,GPT-6.1 Sol 于 2026-09-29 核验,其余新增型号见对应条目 // 已按 https://developers.openai.com/api/docs/deprecations 核验至 2026-09-13, -// 移除已关闭的型号;仅宣布弃用但尚未到关闭日期的型号继续保留。 +// 移除已关闭的型号;仅宣布弃用但尚未到关闭日期的型号继续保留 const PRICING_RULES: &[PricingRule] = &[ // Astra:https://developers.openai.com/api/docs/models/gpt-6-astra - // 已于 2026-09-09 对照官方价目表核验。 + // 已于 2026-09-09 对照官方价目表核验 PricingRule { model: "gpt-6-astra", pricing: ModelPricing::new(100_000, 500_000, 10_000) @@ -254,7 +255,7 @@ const PRICING_RULES: &[PricingRule] = &[ .with_long_flex(20_000, 75_000, 1_000) .with_long_fast(80_000, 300_000, 4_000), }, - // https://developers.openai.com/api/docs/models/gpt-6-sol,核验日期 2026-09-24。 + // https://developers.openai.com/api/docs/models/gpt-6-sol,核验日期 2026-09-24 PricingRule { model: "gpt-6-sol", pricing: ModelPricing::new(20_000, 100_000, 2_000) @@ -265,7 +266,7 @@ const PRICING_RULES: &[PricingRule] = &[ .with_long_flex(20_000, 75_000, 2_000) .with_long_fast(80_000, 300_000, 8_000), }, - // https://developers.openai.com/api/docs/models/gpt-6-luna,核验日期 2026-09-24。 + // https://developers.openai.com/api/docs/models/gpt-6-luna,核验日期 2026-09-24 PricingRule { model: "gpt-6-luna", pricing: ModelPricing::new(1_000, 5_000, 100) @@ -467,8 +468,8 @@ const PRICING_RULES: &[PricingRule] = &[ model: "gpt-3.5-turbo", pricing: ModelPricing::new(5_000, 15_000, 0), }, - // 各变体独立采用已公开的价格与档位,不能继承父型号的所有档位。 - // Cyber 长上下文的官方来源存在差异,该区间暂不估价。 + // 各变体独立采用已公开的价格与档位,不能继承父型号的所有档位 + // Cyber 长上下文的官方来源存在差异,该区间暂不估价 PricingRule { model: "gpt-5.6-cyber", pricing: ModelPricing::new(125_000, 750_000, 12_500) @@ -494,7 +495,7 @@ struct TokenAmounts { total_ticks: u128, } -/// 按 OpenAI Provider 当前受控价格规则计算费用明细。 +/// 按 OpenAI Provider 当前受控价格规则计算费用明细 #[must_use] pub fn openai_billing_breakdown( model: &str, @@ -510,7 +511,7 @@ pub fn openai_billing_breakdown( ) } -/// 使用请求开始时冻结的价格覆盖,不读取管理存储。 +/// 使用请求开始时冻结的价格覆盖,不读取管理存储 #[must_use] pub fn openai_billing_breakdown_with_override( model: &str, @@ -670,8 +671,8 @@ pub(crate) fn pricing_catalog() } else { rates.cache_read_ticks })?, - // 内置表没有独立缓存写入费时,这部分 Token 原本按输入计费。 - // 管理页复制来源档位不能把它改成显式免费。 + // 内置表没有独立缓存写入费时,这部分 Token 原本按输入计费 + // 管理页复制来源档位不能把它改成显式免费 cache_write: price(if pricing.cache_write_percent == 0 { rates.input_ticks } else { @@ -727,7 +728,7 @@ const IMAGE_MODELS: &[&str] = &[ const IMAGE_TEXT_RATES: TokenRates = TokenRates::new(50_000, 0, 12_500); const IMAGE_TOKEN_RATES: TokenRates = TokenRates::new(80_000, 300_000, 20_000); -/// 独立 Images 端点按公开标准 API 单价估算;不是 ChatGPT 账号实际扣费。 +/// 独立 Images 端点按公开标准 API 单价估算;不是 ChatGPT 账号实际扣费 pub(crate) fn image_calculated_cost( request_body: &[u8], usage: &Value, @@ -737,7 +738,7 @@ pub(crate) fn image_calculated_cost( struct ImageModel { model: String, } - // 只读取模型,跳过编辑请求中可能很大的 base64 图片。 + // 只读取模型,跳过编辑请求中可能很大的 base64 图片 let request = serde_json::from_slice::(request_body).ok()?; let custom = prices .get("openai") @@ -770,7 +771,7 @@ pub(crate) fn image_calculated_cost( { return None; } - // 此模型仅输出图片;上游如报告其他输出模态,不能套用图片单价。 + // 此模型仅输出图片;上游如报告其他输出模态,不能套用图片单价 if let Some(details) = usage.get("output_tokens_details") && (details.get("image_tokens")?.as_u64()? != output || details.get("text_tokens")?.as_u64()? != 0) @@ -784,7 +785,7 @@ pub(crate) fn image_calculated_cost( if cached > input { return None; } - // 缓存只给总量时,混合输入无法判定应套用哪种缓存单价。 + // 缓存只给总量时,混合输入无法判定应套用哪种缓存单价 let (cached_text, cached_image) = match (text_input, image_input, cached) { (_, _, 0) => (0, 0), (_, 0, cached) => (cached, 0), @@ -792,9 +793,9 @@ pub(crate) fn image_calculated_cost( (_, _, cached) if cached == input => (text_input, image_input), _ => return None, }; - // 价格来源:https://developers.openai.com/api/docs/pricing,核验日期 2026-09-09。 - // GPT Image 2 和两个 2.5 型号按相同的 token 单价计费,不使用按张估价。 - // 每百万 token 的美元单价:文本 5 / 缓存 1.25;图片 8 / 缓存 2 / 输出 30。 + // 价格来源:https://developers.openai.com/api/docs/pricing,核验日期 2026-09-09 + // GPT Image 2 和两个 2.5 型号按相同的 token 单价计费,不使用按张估价 + // 每百万 token 的美元单价:文本 5 / 缓存 1.25;图片 8 / 缓存 2 / 输出 30 let text = token_amounts(text_rates, 0, text_input, 0, cached_text, 0)?; let image = token_amounts(image_rates, 0, image_input, output, cached_image, 0)?; let total = usd_money(text.total_ticks.checked_add(image.total_ticks)?)?; @@ -852,9 +853,8 @@ pub(crate) fn web_search_pricing(model: &str, tools: Option<&[Value]>) -> Option } } match (standard, preview) { - // These two models bill non-preview search content as a fixed 8K input block. - // The response does not expose enough detail to replace that block without - // double-counting ordinary input, so leave the request unpriced. + // 这两个模型将非预览搜索内容按固定 8K 输入块计费 + // 响应缺少足够明细,无法排除与普通输入重复计费,因此保留为未定价 (true, false) if fixed_block_web_search_model(model) => None, (true, false) => Some(WebSearchPricing::Standard), (false, true) if reasoning_model(model) => Some(WebSearchPricing::Standard), @@ -973,7 +973,7 @@ fn normalize_model_name(model: &str) -> String { } // 仅已核验的别名和快照可以共用价格;未知后缀、未来日期和微调模型 -// 不得直接继承父型号的价格。 +// 不得直接继承父型号的价格 fn pricing_model_name(model: &str) -> &str { match model { "gpt-3.5-turbo-0125" => "gpt-3.5-turbo", @@ -1014,7 +1014,7 @@ fn model_matches_rule(model: &str, rule: &str) -> bool { pricing_model_name(model) == rule } -/// 规范化请求或响应携带的服务档位,供观测与计费共用。 +/// 规范化请求或响应携带的服务档位,供观测与计费共用 pub(crate) fn normalize_service_tier(service_tier: Option<&str>) -> Option { service_tier .map(str::trim) @@ -1042,11 +1042,11 @@ fn usd_price_per_million(per_token_ticks: u128) -> Option { usd_money(per_token_ticks.checked_mul(1_000_000)?) } -/// 单次 Codex usage 响应允许保留和解析的最大字节数。 +/// 单次 Codex usage 响应允许保留和解析的最大字节数 pub const MAX_CODEX_USAGE_BODY_BYTES: usize = 1024 * 1024; impl CodexBackendClient { - /// 获取 Codex usage JSON。 + /// 获取 Codex usage JSON pub async fn fetch_usage(&self, context: CodexRequestContext<'_>) -> CodexClientResult { let headers = self.account_request_headers(context)?; let request = |base_url| { diff --git a/backend/crates/providers/openai/src/transport/websocket/audit.rs b/backend/crates/providers/openai/src/transport/websocket/audit.rs index d2b5cdf05..14944024c 100644 --- a/backend/crates/providers/openai/src/transport/websocket/audit.rs +++ b/backend/crates/providers/openai/src/transport/websocket/audit.rs @@ -1,4 +1,4 @@ -//! WebSocket opening 与 payload 审计输出。 +//! WebSocket opening 与 payload 审计输出 use std::{ io, @@ -17,12 +17,12 @@ use gateway_core::time::DeploymentTimeZone; use super::model::CodexWebSocketConnection; const REDACTED_HEADER_VALUE: &str = ""; -/// WebSocket audit artifact 输出目录环境变量。 +/// WebSocket audit artifact 输出目录环境变量 pub const WS_AUDIT_DIR_ENV: &str = "CODEX_PROXY_WS_AUDIT_DIR"; static WS_AUDIT_DIR: OnceLock> = OnceLock::new(); -/// 返回进程级审计输出目录;未配置时调用方应整体跳过 artifact 构造。 +/// 返回进程级审计输出目录;未配置时调用方应整体跳过 artifact 构造 pub fn websocket_audit_dir() -> Option<&'static Path> { WS_AUDIT_DIR .get_or_init(|| { @@ -33,7 +33,7 @@ pub fn websocket_audit_dir() -> Option<&'static Path> { .as_deref() } -/// 显式写入 WebSocket audit artifact。 +/// 显式写入 WebSocket audit artifact pub async fn write_websocket_audit_artifact_for_dir( dir: Option<&Path>, artifact: &WebSocketAuditArtifact, @@ -50,7 +50,7 @@ pub async fn write_websocket_audit_artifact_for_dir( Ok(Some(path)) } -/// 按环境变量配置写入 WebSocket audit artifact。 +/// 按环境变量配置写入 WebSocket audit artifact pub async fn write_websocket_audit_artifact_from_env( artifact: &WebSocketAuditArtifact, timezone: DeploymentTimeZone, @@ -59,7 +59,7 @@ pub async fn write_websocket_audit_artifact_from_env( } impl CodexWebSocketConnection { - /// 生成打开握手审计快照。 + /// 生成打开握手审计快照 pub fn opening_audit_snapshot(&self) -> OpeningAuditSnapshot { OpeningAuditSnapshot { request_line: request_line_for_endpoint(&self.endpoint), diff --git a/backend/crates/providers/openai/src/transport/websocket/breaker.rs b/backend/crates/providers/openai/src/transport/websocket/breaker.rs index 5ca180c6d..4a59c949f 100644 --- a/backend/crates/providers/openai/src/transport/websocket/breaker.rs +++ b/backend/crates/providers/openai/src/transport/websocket/breaker.rs @@ -1,4 +1,4 @@ -//! Responses WebSocket 冷建连的 origin 级熔断器。 +//! Responses WebSocket 冷建连的 origin 级熔断器 use std::{ collections::{HashMap, VecDeque}, @@ -16,7 +16,7 @@ const DEFAULT_FAILURE_THRESHOLD: usize = 3; const DEFAULT_FAILURE_WINDOW: Duration = Duration::from_secs(30); const DEFAULT_OPEN_DURATION: Duration = Duration::from_secs(30); -/// origin 熔断策略。 +/// origin 熔断策略 #[derive(Debug, Clone, Copy)] pub struct WebSocketOriginBreakerConfig { pub failure_threshold: usize, @@ -34,14 +34,14 @@ impl Default for WebSocketOriginBreakerConfig { } } -/// 冷建连是否获准进入 origin。 +/// 冷建连是否获准进入 origin pub enum WebSocketOriginBreakerDecision { Allowed(WebSocketOriginBreakerPermit), Open, HalfOpenBusy, } -/// origin 级 WebSocket 快路径熔断器。 +/// origin 级 WebSocket 快路径熔断器 #[derive(Clone)] pub struct WebSocketOriginBreaker { inner: Arc>>, @@ -65,7 +65,7 @@ impl WebSocketOriginBreaker { } } - /// 已有热 socket 不经过这里;本方法只裁决新的 WebSocket opening。 + /// 已有热 socket 不经过这里;本方法只裁决新的 WebSocket opening pub fn try_acquire(&self, origin_key: &str) -> WebSocketOriginBreakerDecision { let now = Instant::now(); let mut circuits = self @@ -162,7 +162,7 @@ impl WebSocketOriginBreaker { } } -/// 一次新的 WebSocket opening 许可;消费式完成保证 half-open 探针不会泄漏。 +/// 一次新的 WebSocket opening 许可;消费式完成保证 half-open 探针不会泄漏 pub struct WebSocketOriginBreakerPermit { breaker: WebSocketOriginBreaker, origin_key: String, @@ -182,7 +182,7 @@ impl WebSocketOriginBreakerPermit { } } - /// 创建可跨前台预算与后台 opening 生命周期共享的快路径观察器。 + /// 创建可跨前台预算与后台 opening 生命周期共享的快路径观察器 pub(crate) fn fast_path_reporter(&self) -> WebSocketOriginFastPathReporter { WebSocketOriginFastPathReporter { breaker: self.breaker.clone(), @@ -209,7 +209,7 @@ impl WebSocketOriginBreakerPermit { self.armed = false; } - /// opening 因账号驱逐或服务关闭而取消,不污染 origin 可用性。 + /// opening 因账号驱逐或服务关闭而取消,不污染 origin 可用性 pub(crate) fn cancel(mut self) { self.breaker.cancel_probe(&self.origin_key, self.probe_id); self.armed = false; @@ -230,7 +230,7 @@ impl Drop for WebSocketOriginBreakerPermit { } } -/// 前台等待结束后仍可报告同一次后台 opening 错过快路径预算。 +/// 前台等待结束后仍可报告同一次后台 opening 错过快路径预算 pub(crate) struct WebSocketOriginFastPathReporter { breaker: WebSocketOriginBreaker, origin_key: String, diff --git a/backend/crates/providers/openai/src/transport/websocket/coordinator.rs b/backend/crates/providers/openai/src/transport/websocket/coordinator.rs index 0ba67c05e..10932731b 100644 --- a/backend/crates/providers/openai/src/transport/websocket/coordinator.rs +++ b/backend/crates/providers/openai/src/transport/websocket/coordinator.rs @@ -1,4 +1,4 @@ -//! Responses WebSocket pre-send/post-send 与 pool/breaker 编排。 +//! Responses WebSocket pre-send/post-send 与 pool/breaker 编排 use std::{ future::Future, @@ -35,15 +35,16 @@ use super::{ pub(crate) const WEBSOCKET_FAST_PATH_BUDGET: Duration = Duration::from_millis(800); -/// WebSocket 快路径的控制流结果;预算未命中不代表连接失败。 +/// WebSocket 快路径的控制流结果;预算未命中不代表连接失败 pub(crate) enum WebSocketFastPath { - /// 在前台预算内获得结果。 + /// 在前台预算内获得结果 Ready(T), - /// 前台停止等待;池化 opening 可以继续在后台完成。 + /// 前台停止等待;池化 opening 可以继续在后台完成 Missed, } -/// 尚未发送 `response.create` 的 WebSocket。只有该类型可以安全切换到 HTTP。 +/// 尚未发送 `response.create` 的 WebSocket +/// 只有该类型可以安全切换到 HTTP pub(crate) struct PreparedWebSocket { connection: PooledWebSocketConnection, binding: PoolBinding, @@ -109,7 +110,7 @@ impl PreparedWebSocket { } } -/// 只建立或租用 WebSocket,不发送 payload。 +/// 只建立或租用 WebSocket,不发送 payload pub(crate) async fn prepare_response_create_request_with_pool( request: &CodexWebSocketRequest, pool: Option<(&CodexWebSocketPool, CodexWebSocketPoolKey)>, @@ -400,7 +401,7 @@ fn start_pooled_websocket_connect( Err(error) => { let connection_exit_reason = opening_failure_reason(&error); let error_message = error.to_string(); - // 先交付 opening 原始错误,避免连接池清理侵占前台 fast-path 预算。 + // 先交付 opening 原始错误,避免连接池清理侵占前台 fast-path 预算 let foreground_waiting = sender.send(Err(error)).is_ok(); connect_lease.failed().await; tracing::warn!( @@ -443,7 +444,7 @@ fn duration_millis_u64(duration: Duration) -> u64 { .max(1) } -/// 非池化建连的 pump 日志上下文:从业务头提取账号归属(会话 hash 只有池 key 才有)。 +/// 非池化建连的 pump 日志上下文:从业务头提取账号归属(会话 hash 只有池 key 才有) fn pump_log_context_from_connection(connection: &CodexWebSocketConnection) -> PumpLogContext { let account_id = connection .headers() @@ -525,7 +526,7 @@ fn finish_breaker_attempt( Err(error) } Err(CodexWebSocketExchangeError::Upstream(upstream)) if upstream.status_code < 500 => { - // 账号或请求级 opening 响应证明 origin 可达,不得污染 transport 熔断器。 + // 账号或请求级 opening 响应证明 origin 可达,不得污染 transport 熔断器 permit.succeed(); Err(CodexWebSocketExchangeError::Upstream(upstream)) } @@ -543,7 +544,7 @@ async fn wait_for_shared_connect( wait_for_fast_path(waiter.started_at(), fast_path_budget, waiter.wait()).await } -// 同一次 opening 的所有等待者共享截止时间;后来的请求不会重置 800ms 预算。 +// 同一次 opening 的所有等待者共享截止时间;后来的请求不会重置 800ms 预算 async fn wait_for_fast_path( started_at: TokioInstant, budget: Option, diff --git a/backend/crates/providers/openai/src/transport/websocket/error.rs b/backend/crates/providers/openai/src/transport/websocket/error.rs index f08a982c9..861591edc 100644 --- a/backend/crates/providers/openai/src/transport/websocket/error.rs +++ b/backend/crates/providers/openai/src/transport/websocket/error.rs @@ -1,4 +1,4 @@ -//! Responses WebSocket 分阶段错误。 +//! Responses WebSocket 分阶段错误 use std::{fmt, time::Duration}; @@ -6,7 +6,7 @@ use gateway_protocol::openai::sse::SseError; use thiserror::Error; use uuid::Uuid; -/// RFC 6455 close code 1009:上游拒收超出大小限制的消息。 +/// RFC 6455 close code 1009:上游拒收超出大小限制的消息 pub(crate) const WEBSOCKET_CLOSE_MESSAGE_TOO_BIG: u16 = 1009; use crate::transport::client::CodexClientVisibleUpstreamResponse; @@ -17,91 +17,91 @@ use crate::transport::protocol::responses::ResponsesSseFailure; use super::PreviousResponseUnavailableReason; use super::pump::WebSocketConnectionObservation; -/// Responses WebSocket 交互错误。 +/// Responses WebSocket 交互错误 #[derive(Debug, Error)] pub enum CodexWebSocketExchangeError { - /// opening request 无法构造。 + /// opening request 无法构造 #[error("invalid websocket request: {0}")] InvalidRequest(#[from] tungstenite::http::Error), - /// WebSocket 传输失败。 + /// WebSocket 传输失败 #[error("websocket transport error: {0}")] Transport(#[from] tungstenite::Error), - /// DNS、TCP、TLS 或 opening handshake 在发送 payload 前失败。 + /// DNS、TCP、TLS 或 opening handshake 在发送 payload 前失败 #[error("websocket connect failed before payload send: {0}")] Connect(#[source] tungstenite::Error), - /// DNS、TCP、TLS 或 WebSocket upgrade 未在限定时间内完成。 + /// DNS、TCP、TLS 或 WebSocket upgrade 未在限定时间内完成 #[error("websocket connect timed out after {timeout:?}")] ConnectTimeout { - /// 建连超时时长。 + /// 建连超时时长 timeout: Duration, }, - /// origin WebSocket 冷建连熔断中。 + /// origin WebSocket 冷建连熔断中 #[error("websocket origin circuit is open")] OriginCircuitOpen, - /// origin WebSocket 熔断器正在执行唯一 half-open 探针。 + /// origin WebSocket 熔断器正在执行唯一 half-open 探针 #[error("websocket origin circuit half-open probe is already running")] OriginHalfOpenBusy, - /// 同一精确会话的单飞建连失败。 + /// 同一精确会话的单飞建连失败 #[error("shared websocket connection attempt failed before payload send")] SharedConnectFailed, - /// payload 已可能送达上游,禁止自动重放到其他 transport 或账号。 + /// payload 已可能送达上游,禁止自动重放到其他 transport 或账号 #[error("websocket failed after payload send; replay outcome is ambiguous: {message}")] PostSendAmbiguous { - /// 原始失败说明。 + /// 原始失败说明 message: String, - /// 原始 typed transport/protocol failure。 + /// 原始 typed transport/protocol failure #[source] source: Option>, }, - /// 请求帧未在限定时间内写入上游连接。 + /// 请求帧未在限定时间内写入上游连接 #[error("websocket request send timed out after {timeout:?}")] SendTimeout { - /// 发送超时时长。 + /// 发送超时时长 timeout: Duration, }, - /// SSE 聚合结果无法解析。 + /// SSE 聚合结果无法解析 #[error("invalid websocket SSE response: {0}")] InvalidSse(#[from] SseError), - /// 上游 WebSocket 错误帧。 + /// 上游 WebSocket 错误帧 #[error("{0}")] Upstream(Box), - /// 上游要求结束当前 WebSocket 连接并在新连接上重试。 + /// 上游要求结束当前 WebSocket 连接并在新连接上重试 #[error("websocket connection limit reached")] ConnectionLimitReached(Box), - /// 请求依赖的连接本地 previous response 无法在当前连接满足。 + /// 请求依赖的连接本地 previous response 无法在当前连接满足 #[error("websocket continuation unavailable: {reason}")] ContinuationUnavailable { reason: PreviousResponseUnavailableReason, }, - /// 上游在 terminal 事件前关闭。 + /// 上游在 terminal 事件前关闭 #[error("{0}")] ClosedBeforeTerminal(CodexWebSocketCloseError), - /// 未收到 Close 帧即结束,保留 pump 的安全原因码与本地保活时限。 + /// 未收到 Close 帧即结束,保留 pump 的安全原因码与本地保活时限 #[error("websocket stream ended before terminal event ({reason})")] StreamEndedBeforeTerminal { reason: &'static str, timeout: Option, last_event_type: Option, }, - /// 上游在指定时间内没有发送任何事件。 + /// 上游在指定时间内没有发送任何事件 #[error("websocket receive idle timeout after {timeout:?}")] ReceiveIdleTimeout { - /// 超时时长。 + /// 超时时长 timeout: Duration, }, - /// 上游返回非文本事件帧。 + /// 上游返回非文本事件帧 #[error("unexpected binary websocket event")] UnexpectedBinaryEvent, - /// 复用的池连接在收到首个上游事件前失效。 + /// 复用的池连接在收到首个上游事件前失效 #[error("reused websocket connection died before first upstream event: {message}")] ReusedConnectionDiedBeforeFirstEvent { - /// 底层失效原因。 + /// 底层失效原因 message: String, - /// 原始 typed transport failure。 + /// 原始 typed transport failure #[source] source: Option>, }, - /// 将一个已分类交互错误与物理连接生命周期快照绑定。 + /// 将一个已分类交互错误与物理连接生命周期快照绑定 #[error("{source}")] ConnectionObserved { observation: WebSocketConnectionObservation, @@ -110,9 +110,9 @@ pub enum CodexWebSocketExchangeError { }, } -/// 上游在 terminal 事件前发送的 WebSocket close 信息。 +/// 上游在 terminal 事件前发送的 WebSocket close 信息 /// -/// close reason 只供原请求的客户端错误响应使用;`Debug` 与 `Display` 均不会输出它。 +/// close reason 只供原请求的客户端错误响应使用;`Debug` 与 `Display` 均不会输出它 #[derive(Clone, PartialEq, Eq)] pub struct CodexWebSocketCloseError { connection_id: Option, @@ -141,25 +141,25 @@ impl CodexWebSocketCloseError { self } - /// 返回承载该 close frame 的 WebSocket 连接标识。 + /// 返回承载该 close frame 的 WebSocket 连接标识 #[must_use] pub const fn connection_id(&self) -> Option { self.connection_id } - /// 返回上游 close code;没有 close frame 时为 `None`。 + /// 返回上游 close code;没有 close frame 时为 `None` #[must_use] pub const fn code(&self) -> Option { self.code } - /// 返回上游 close reason;仅可用于当前请求的客户端协议响应。 + /// 返回上游 close reason;仅可用于当前请求的客户端协议响应 #[must_use] pub fn reason(&self) -> Option<&str> { self.reason.as_deref() } - /// 返回关闭前最后一条经过白名单校验的事件类型;不包含事件 payload。 + /// 返回关闭前最后一条经过白名单校验的事件类型;不包含事件 payload #[must_use] pub fn last_event_type(&self) -> Option<&str> { self.last_event_type.as_deref() @@ -190,22 +190,22 @@ impl fmt::Display for CodexWebSocketCloseError { } } -/// WebSocket 上游错误帧载荷。 +/// WebSocket 上游错误帧载荷 #[derive(Clone, PartialEq, Eq)] pub struct CodexWebSocketUpstreamError { - /// 上游返回的 HTTP 风格状态码。 + /// 上游返回的 HTTP 风格状态码 pub status_code: u16, - /// 推导出的重试秒数。 + /// 推导出的重试秒数 pub retry_after_seconds: Option, - /// 原始错误帧。 + /// 原始错误帧 pub body: String, - /// opening 失败时可返回给当前客户端的原始 HTTP 响应。 + /// opening 失败时可返回给当前客户端的原始 HTTP 响应 pub client_response: Option>, - /// 上游透传的 `set-cookie` 列表。 + /// 上游透传的 `set-cookie` 列表 pub set_cookie_headers: Vec, - /// 上游诊断元数据。 + /// 上游诊断元数据 pub diagnostics: CodexUpstreamDiagnostics, - /// 上游拒绝相对业务 payload 的发送阶段。 + /// 上游拒绝相对业务 payload 的发送阶段 pub send_phase: CodexUpstreamSendPhase, } @@ -234,7 +234,7 @@ impl fmt::Display for CodexWebSocketUpstreamError { } impl CodexWebSocketExchangeError { - /// 仅供诊断展示穿透发送状态与连接快照包装;重试分类仍使用 classified。 + /// 仅供诊断展示穿透发送状态与连接快照包装;重试分类仍使用 classified pub(crate) fn diagnostic_cause(&self) -> &Self { match self { Self::PostSendAmbiguous { @@ -250,8 +250,8 @@ impl CodexWebSocketExchangeError { } } - /// 返回可持久化的传输错误分类,不包含底层错误中的地址、报文或凭据。 - /// 与已有指标粗分类分开:缺少关闭握手本身不能证明收到 TCP RST。 + /// 返回可持久化的传输错误分类,不包含底层错误中的地址、报文或凭据 + /// 与已有指标粗分类分开:缺少关闭握手本身不能证明收到 TCP RST pub(crate) fn transport_failure_reason(&self) -> Option<&'static str> { match self.diagnostic_cause() { Self::Transport(error) | Self::Connect(error) => Some(match error { @@ -291,7 +291,7 @@ impl CodexWebSocketExchangeError { ) } - /// 返回错误链中的上游终态前 Close 帧(若存在)。 + /// 返回错误链中的上游终态前 Close 帧(若存在) #[must_use] pub fn close_before_terminal(&self) -> Option<&CodexWebSocketCloseError> { match self { @@ -322,7 +322,7 @@ impl CodexWebSocketExchangeError { } } - /// 返回错误关联的物理 WebSocket 连接生命周期快照。 + /// 返回错误关联的物理 WebSocket 连接生命周期快照 #[must_use] pub fn connection_observation(&self) -> Option<&WebSocketConnectionObservation> { match self { @@ -339,7 +339,7 @@ impl CodexWebSocketExchangeError { } } - /// 返回去掉观测 wrapper 后的原始分类错误。 + /// 返回去掉观测 wrapper 后的原始分类错误 pub(crate) fn classified(&self) -> &Self { match self { Self::ConnectionObserved { source, .. } => source.classified(), diff --git a/backend/crates/providers/openai/src/transport/websocket/exchange/io.rs b/backend/crates/providers/openai/src/transport/websocket/exchange/io.rs index 9ff34b642..29efe9678 100644 --- a/backend/crates/providers/openai/src/transport/websocket/exchange/io.rs +++ b/backend/crates/providers/openai/src/transport/websocket/exchange/io.rs @@ -1,4 +1,4 @@ -//! WebSocket exchange 共用接收边界。 +//! WebSocket exchange 共用接收边界 use std::time::Duration; diff --git a/backend/crates/providers/openai/src/transport/websocket/exchange/mod.rs b/backend/crates/providers/openai/src/transport/websocket/exchange/mod.rs index 2984ceb69..b4fbb0228 100644 --- a/backend/crates/providers/openai/src/transport/websocket/exchange/mod.rs +++ b/backend/crates/providers/openai/src/transport/websocket/exchange/mod.rs @@ -1,4 +1,4 @@ -//! WebSocket 帧交换、流转发与结果类型。 +//! WebSocket 帧交换、流转发与结果类型 mod io; mod reducer; @@ -23,43 +23,43 @@ const WEBSOCKET_STREAM_BUFFER: usize = 16; pub(super) use self::stream::{WebSocketStreamPoolReturn, stream_websocket_response}; -/// Responses WebSocket live SSE 交互结果。 +/// Responses WebSocket live SSE 交互结果 pub struct CodexWebSocketStreamingExchange { - /// 关联请求日志与底层 pump 生命周期日志的连接标识。 + /// 关联请求日志与底层 pump 生命周期日志的连接标识 pub(crate) websocket_connection_id: Uuid, - /// 由 WebSocket 事件转换出的 live SSE 字节流。 + /// 由 WebSocket 事件转换出的 live SSE 字节流 pub body: CodexWebSocketSseStream, - /// 上游为本次响应返回的首个 turn state。 + /// 上游为本次响应返回的首个 turn state pub turn_state: Option, - /// 上游握手响应里的 `set-cookie` 列表。 + /// 上游握手响应里的 `set-cookie` 列表 pub set_cookie_headers: Vec, - /// 上游握手响应里的限流头。 + /// 上游握手响应里的限流头 pub rate_limit_headers: Vec<(String, String)>, - /// 上游内部 `codex.rate_limits` 事件里的结构化动态更新。 + /// 上游内部 `codex.rate_limits` 事件里的结构化动态更新 pub rate_limit_updates: CodexWebSocketRateLimitUpdates, - /// 上游内部 metadata 事件里的请求级动态更新。 + /// 上游内部 metadata 事件里的请求级动态更新 pub response_metadata_updates: CodexWebSocketResponseMetadataUpdates, - /// WebSocket 连接池决策。 + /// WebSocket 连接池决策 pub pool_decision: Option, - /// terminal completed 后该 socket 是否会保留 connection-local continuation。 + /// terminal completed 后该 socket 是否会保留 connection-local continuation pub connection_local_continuation: bool, - /// 上游诊断元数据。 + /// 上游诊断元数据 pub diagnostics: CodexUpstreamDiagnostics, - /// 安全响应元数据。 + /// 安全响应元数据 pub response_metadata: CodexResponseMetadata, } -/// Responses WebSocket live SSE 字节流。 +/// Responses WebSocket live SSE 字节流 pub type CodexWebSocketSseStream = Pin> + Send + 'static>>; -/// live 流中的结构化限流动态更新。 +/// live 流中的结构化限流动态更新 pub type CodexWebSocketRateLimitUpdates = Arc>>; -/// 单次响应的动态 metadata,与连接池保存的握手快照隔离。 +/// 单次响应的动态 metadata,与连接池保存的握手快照隔离 #[derive(Debug, Default)] pub struct CodexWebSocketResponseMetadataUpdate { - /// 当前响应首次声明的会话续接状态。 + /// 当前响应首次声明的会话续接状态 pub turn_state: Option, - /// 当前响应最新的服务端模型报告。 + /// 当前响应最新的服务端模型报告 pub reported_model: Option, } @@ -70,7 +70,7 @@ pub(super) fn reusable_websocket_metadata( ) -> CodexWebSocketConnectionMetadata { metadata.rate_limit_headers.clear(); metadata.turn_state = None; - // 模型报告属于上一轮请求,池中连接不能把它带到下一轮缺失报告的响应。 + // 模型报告属于上一轮请求,池中连接不能把它带到下一轮缺失报告的响应 metadata.response_metadata.effective_model = None; metadata .response_metadata diff --git a/backend/crates/providers/openai/src/transport/websocket/exchange/reducer.rs b/backend/crates/providers/openai/src/transport/websocket/exchange/reducer.rs index e8284e19d..1ae816ee7 100644 --- a/backend/crates/providers/openai/src/transport/websocket/exchange/reducer.rs +++ b/backend/crates/providers/openai/src/transport/websocket/exchange/reducer.rs @@ -1,4 +1,4 @@ -//! WebSocket aggregate/stream 共用事件归约器。 +//! WebSocket aggregate/stream 共用事件归约器 use gateway_protocol::openai::events; use serde_json::Value; @@ -43,7 +43,7 @@ pub(super) fn reduce_websocket_event( continuation: &mut WebSocketContinuationState, ) -> Result { // 每帧只解析一次 JSON,后续提取全部复用同一 Value; - // 不可解析的帧不承载可路由的事件类型,忽略。 + // 不可解析的帧不承载可路由的事件类型,忽略 let Ok(value) = serde_json::from_str::(raw) else { return Ok(ReducedWebSocketEvent { created_response_id: None, diff --git a/backend/crates/providers/openai/src/transport/websocket/exchange/stream.rs b/backend/crates/providers/openai/src/transport/websocket/exchange/stream.rs index 3177cf87f..eb129746d 100644 --- a/backend/crates/providers/openai/src/transport/websocket/exchange/stream.rs +++ b/backend/crates/providers/openai/src/transport/websocket/exchange/stream.rs @@ -1,4 +1,4 @@ -//! 流式 WebSocket exchange 与连接回收。 +//! 流式 WebSocket exchange 与连接回收 use std::{sync::Arc, time::Duration}; @@ -164,14 +164,14 @@ async fn forward_websocket_response_stream(state: WebSocketStreamForwardState) { let mut interrupt_requested: futures::future::BoxFuture<'static, ()> = Box::pin(std::future::pending()); let mut interrupt_sent = false; - // 官方 run_websocket_response_stream 按响应消费 metadata 事件;它们不属于握手头。 - // 复用连接时恢复握手快照,避免上一轮模型信息及普通响应头随每次请求累积。 + // 官方 run_websocket_response_stream 按响应消费 metadata 事件;它们不属于握手头 + // 复用连接时恢复握手快照,避免上一轮模型信息及普通响应头随每次请求累积 let opening_response_metadata = metadata.response_metadata.clone(); loop { let message = tokio::select! { biased; // 客户端断开下游 SSE 流:立即丢弃连接并释放池 slot, - // 不再傻等上游 idle 超时(否则同会话后续请求会一直 bypass/busy)。 + // 不再傻等上游 idle 超时(否则同会话后续请求会一直 bypass/busy) () = tx.closed() => { drop(active_interrupt.take()); trace.record("upstream.cancelled", json!({"reason": "receiver_dropped"})); @@ -199,7 +199,7 @@ async fn forward_websocket_response_stream(state: WebSocketStreamForwardState) { "response_id": active.response_id(), "mode": "discard_partial_items", }).to_string(); - // 复用正在消费此响应的原 socket;信号不经过选号或新请求恢复。 + // 复用正在消费此响应的原 socket;信号不经过选号或新请求恢复 if let Err(error) = super::super::handshake::send_websocket_request(&websocket, &payload).await { drop(active_interrupt.take()); discard_stream_websocket(websocket, pool_return, StreamWebSocketDiscardReason::UpstreamReceiveFailed).await; @@ -218,7 +218,7 @@ async fn forward_websocket_response_stream(state: WebSocketStreamForwardState) { let message = match message { Ok(message) => message, Err(error) => { - // 先撤销 owner 再发布失败;下一次 attempt 不等待旧 socket 异步清理。 + // 先撤销 owner 再发布失败;下一次 attempt 不等待旧 socket 异步清理 drop(active_interrupt.take()); let observation = connection_observation(&websocket, &error); trace.record( @@ -337,7 +337,7 @@ async fn forward_websocket_response_stream(state: WebSocketStreamForwardState) { { active_interrupt = control.activate(response_id); if let Some(active) = &active_interrupt { - // 跨消息复用同一个等待 future,避免每帧留下新的取消等待者。 + // 跨消息复用同一个等待 future,避免每帧留下新的取消等待者 interrupt_requested = Box::pin(active.requested()); } if active_interrupt.is_none() { @@ -374,7 +374,7 @@ async fn forward_websocket_response_stream(state: WebSocketStreamForwardState) { ExchangeAction::Ignore => continue, }; if terminal.is_some() { - // 先撤销控制权,再交付终态和归还连接,避免迟到的中断影响下一轮。 + // 先撤销控制权,再交付终态和归还连接,避免迟到的中断影响下一轮 drop(active_interrupt.take()); } if tx.send(Ok(Bytes::from(frame))).await.is_err() { diff --git a/backend/crates/providers/openai/src/transport/websocket/handshake.rs b/backend/crates/providers/openai/src/transport/websocket/handshake.rs index 043250464..3d609c147 100644 --- a/backend/crates/providers/openai/src/transport/websocket/handshake.rs +++ b/backend/crates/providers/openai/src/transport/websocket/handshake.rs @@ -1,4 +1,4 @@ -//! Responses WebSocket endpoint、opening handshake 与首帧发送。 +//! Responses WebSocket endpoint、opening handshake 与首帧发送 use std::time::Duration; @@ -40,7 +40,7 @@ const WEBSOCKET_CONNECT_TIMEOUT: Duration = Duration::from_secs(15); const WEBSOCKET_SEND_TIMEOUT: Duration = Duration::from_secs(5 * 60); impl CodexWebSocketConnection { - /// 构造 Responses WebSocket 连接描述。 + /// 构造 Responses WebSocket 连接描述 pub fn responses( base_url: &str, websocket_key: &str, @@ -70,7 +70,7 @@ impl CodexWebSocketConnection { } } - /// 构造 Responses WebSocket opening 与首个 `response.create` 文本帧。 + /// 构造 Responses WebSocket opening 与首个 `response.create` 文本帧 pub fn responses_create_request( base_url: &str, websocket_key: &str, @@ -106,7 +106,7 @@ impl CodexWebSocketConnection { } } -/// 将 Codex backend base URL 转换为 Responses WebSocket endpoint。 +/// 将 Codex backend base URL 转换为 Responses WebSocket endpoint pub fn responses_websocket_endpoint(base_url: &str) -> String { let endpoint = format!("{}{}", base_url.trim_end_matches('/'), CODEX_RESPONSES_PATH); if let Some(rest) = endpoint.strip_prefix("https://") { @@ -191,7 +191,7 @@ async fn connect_websocket( crate::transport::connection::acquire().await } .map_err(tungstenite::Error::Io)?; - // Preserve the native direct handshake; explicit egress never inherits a global proxy. + // 保留原生直连握手,显式出口不继承全局代理 if connection.outbound_proxy.is_none() && matches!( tungstenite::proxy::ProxyConfig::from_env(request.uri()), @@ -277,7 +277,7 @@ async fn dial_account( } else { MaybeTlsStream::Plain(tcp) }; - // The pinned tungstenite fork sends domains remotely for both SOCKS schemes. + // 固定的 tungstenite 分支对两种 SOCKS 协议均把域名交给代理解析 let target = if proxy_url.scheme() == "socks5" { lookup_host((dns_host.as_str(), port)) .await? @@ -290,9 +290,9 @@ async fn dial_account( } else { host.to_owned() }; - // 依赖分两次写入 SOCKS 方法协商,再显式 flush;部分代理会提前拒绝半包。 - // 仅在代理握手期间合并写入,不缓冲读取,也不改变后续 TLS/WS 的发送方式。 - // 这只是兼容措施,不能保证网络层不再拆分 TCP 数据。 + // 依赖分两次写入 SOCKS 方法协商,再显式 flush;部分代理会提前拒绝半包 + // 仅在代理握手期间合并写入,不缓冲读取,也不改变后续 TLS/WS 的发送方式 + // 这只是兼容措施,不能保证网络层不再拆分 TCP 数据 tokio_tungstenite::proxy::connect_via_proxy(BufWriter::new(stream), &config, &target, port) .await .map(BufWriter::into_inner) @@ -302,7 +302,7 @@ async fn connect_tcp(host: &str, port: u16) -> Result().map_or_else( |_| format!("{host}:{port}"), |ip| std::net::SocketAddr::new(ip, port).to_string(), @@ -341,7 +341,7 @@ fn websocket_config() -> WebSocketConfig { let mut config = WebSocketConfig::default(); // 上游 Responses 事件属于 Codex 协议数据,不能沿用 tungstenite 的私有 - // 64 MiB message / 16 MiB frame 默认限制提前中断本可继续的响应。 + // 64 MiB message / 16 MiB frame 默认限制提前中断本可继续的响应 config.max_message_size = None; config.max_frame_size = None; config.extensions = extensions; diff --git a/backend/crates/providers/openai/src/transport/websocket/mod.rs b/backend/crates/providers/openai/src/transport/websocket/mod.rs index b2464aecd..3471cd654 100644 --- a/backend/crates/providers/openai/src/transport/websocket/mod.rs +++ b/backend/crates/providers/openai/src/transport/websocket/mod.rs @@ -1,4 +1,4 @@ -//! Codex Responses WebSocket 传输。 +//! Codex Responses WebSocket 传输 mod audit; mod breaker; diff --git a/backend/crates/providers/openai/src/transport/websocket/model.rs b/backend/crates/providers/openai/src/transport/websocket/model.rs index 23750c382..00299a011 100644 --- a/backend/crates/providers/openai/src/transport/websocket/model.rs +++ b/backend/crates/providers/openai/src/transport/websocket/model.rs @@ -1,4 +1,4 @@ -//! Responses WebSocket 请求与续接模型。 +//! Responses WebSocket 请求与续接模型 use std::fmt; @@ -6,7 +6,7 @@ use crate::transport::protocol::responses::{ CodexResponsesRequest, TransportRequirement, transport_requirement, }; -/// WebSocket opening 描述。 +/// WebSocket opening 描述 #[derive(Debug, Clone)] pub struct CodexWebSocketConnection { pub(crate) connection_budget: Option, @@ -15,7 +15,7 @@ pub struct CodexWebSocketConnection { pub(super) headers: Vec<(String, String)>, } -// 请求预算不是连接身份;连接池复用不能由请求局部状态决定。 +// 请求预算不是连接身份;连接池复用不能由请求局部状态决定 impl PartialEq for CodexWebSocketConnection { fn eq(&self, other: &Self) -> bool { self.endpoint == other.endpoint @@ -25,7 +25,7 @@ impl PartialEq for CodexWebSocketConnection { } impl Eq for CodexWebSocketConnection {} -/// 已构造完成的 Responses WebSocket 请求描述。 +/// 已构造完成的 Responses WebSocket 请求描述 #[derive(Debug, Clone, PartialEq, Eq)] pub struct CodexWebSocketRequest { pub(crate) connection: CodexWebSocketConnection, @@ -33,16 +33,16 @@ pub struct CodexWebSocketRequest { pub(super) continuation: WebSocketContinuationRequirement, } -/// 当前 WebSocket 请求对 previous response 状态的要求。 +/// 当前 WebSocket 请求对 previous response 状态的要求 #[derive(Debug, Clone, PartialEq, Eq)] pub enum WebSocketContinuationRequirement { - /// 不依赖任何已有响应状态。 + /// 不依赖任何已有响应状态 NewChain, - /// 上游已持久化 response,可在新连接 hydration。 + /// 上游已持久化 response,可在新连接 hydration Persisted { response_id: String }, - /// Provider 没有跨目标路由所有权,只允许 Engine 选定的单个凭据原样尝试。 + /// Provider 没有跨目标路由所有权,只允许 Engine 选定的单个凭据原样尝试 ExternalUnknown { response_id: String }, - /// `store=false` response,只能在拥有该 ID 的原连接续接。 + /// `store=false` response,只能在拥有该 ID 的原连接续接 ConnectionLocal { response_id: String }, } @@ -71,7 +71,7 @@ impl WebSocketContinuationRequirement { } } -/// 连接本地 previous response 无法满足的原因。 +/// 连接本地 previous response 无法满足的原因 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PreviousResponseUnavailableReason { PoolUnavailable, @@ -103,24 +103,24 @@ impl fmt::Display for PreviousResponseUnavailableReason { } impl CodexWebSocketRequest { - /// 返回连接描述。 + /// 返回连接描述 pub fn connection(&self) -> &CodexWebSocketConnection { &self.connection } - /// 返回将要发送的首个文本帧。 + /// 返回将要发送的首个文本帧 pub fn payload_text(&self) -> &str { &self.payload_text } - /// 返回连接续接要求。 + /// 返回连接续接要求 pub fn continuation(&self) -> &WebSocketContinuationRequirement { &self.continuation } } impl CodexWebSocketConnection { - /// 构造待打开的 WebSocket 连接描述。 + /// 构造待打开的 WebSocket 连接描述 pub fn new(endpoint: impl Into, headers: Vec<(String, String)>) -> Self { Self { endpoint: endpoint.into(), @@ -130,12 +130,12 @@ impl CodexWebSocketConnection { } } - /// 返回 WebSocket endpoint。 + /// 返回 WebSocket endpoint pub fn endpoint(&self) -> &str { &self.endpoint } - /// 返回按发送顺序保存的请求头。 + /// 返回按发送顺序保存的请求头 pub fn headers(&self) -> &[(String, String)] { &self.headers } diff --git a/backend/crates/providers/openai/src/transport/websocket/pool/lease.rs b/backend/crates/providers/openai/src/transport/websocket/pool/lease.rs index cd26d8c04..b0f7a9de4 100644 --- a/backend/crates/providers/openai/src/transport/websocket/pool/lease.rs +++ b/backend/crates/providers/openai/src/transport/websocket/pool/lease.rs @@ -1,4 +1,4 @@ -//! WebSocket 连接池 reservation、handoff 与 lease 生命周期。 +//! WebSocket 连接池 reservation、handoff 与 lease 生命周期 use tokio::{ sync::{OwnedSemaphorePermit, watch}, @@ -62,7 +62,7 @@ pub(crate) struct WebSocketPoolConnectLease { pub(super) started_at: Instant, pub(super) outcome: watch::Sender, pub(super) cancellation: CancellationToken, - // slot 分配时即注册,封闭 acquire 与后台 task spawn 之间的 shutdown 竞态。 + // slot 分配时即注册,封闭 acquire 与后台 task spawn 之间的 shutdown 竞态 _task_registration: TaskTrackerToken, _connect_permit: OwnedSemaphorePermit, armed: bool, @@ -123,7 +123,7 @@ impl WebSocketPoolConnectLease { pub(crate) async fn failed(mut self) { self.cancellation.cancel(); - // 先唤醒共享等待者,pool mutex 清理不得延迟前台 transport 决策。 + // 先唤醒共享等待者,pool mutex 清理不得延迟前台 transport 决策 self.outcome .send_replace(WebSocketPoolConnectOutcome::Failed); self.pool.fail_connect(&self.key, self.id).await; diff --git a/backend/crates/providers/openai/src/transport/websocket/pool/mod.rs b/backend/crates/providers/openai/src/transport/websocket/pool/mod.rs index 9171b8450..10cd0f3bc 100644 --- a/backend/crates/providers/openai/src/transport/websocket/pool/mod.rs +++ b/backend/crates/providers/openai/src/transport/websocket/pool/mod.rs @@ -1,4 +1,4 @@ -//! Codex WebSocket 连接池。 +//! Codex WebSocket 连接池 mod lease; mod state; @@ -36,11 +36,11 @@ const DEFAULT_MAX_CONNECTING: usize = 16; const DEFAULT_MAX_AGE: Duration = Duration::from_mins(55); const DEFAULT_MAINTENANCE_INTERVAL: Duration = Duration::from_secs(25); const DEFAULT_PING_INTERVAL: Duration = Duration::from_secs(25); -// 心跳也覆盖正在生成的连接,给短时链路停顿留出恢复余量。 +// 心跳也覆盖正在生成的连接,给短时链路停顿留出恢复余量 const DEFAULT_PING_TIMEOUT: Duration = Duration::from_secs(30); pub(crate) const DEFAULT_STREAM_IDLE_TIMEOUT: Duration = Duration::from_secs(300); -/// WebSocket 连接池。 +/// WebSocket 连接池 #[derive(Clone)] pub struct CodexWebSocketPool { inner: Arc>, @@ -57,24 +57,24 @@ impl Default for CodexWebSocketPool { } } -/// WebSocket 连接池配置。 +/// WebSocket 连接池配置 #[derive(Debug, Clone, Copy)] pub struct CodexWebSocketPoolConfig { - /// 是否启用连接池。 + /// 是否启用连接池 pub enabled: bool, - /// 单个 socket 的最大生命周期。 + /// 单个 socket 的最大生命周期 pub max_age: Duration, - /// 所有账号合计允许并发执行的 opening 数。 + /// 所有账号合计允许并发执行的 opening 数 pub max_connecting: usize, - /// 后台维护间隔;`None` 表示不启动后台任务。 + /// 后台维护间隔;`None` 表示不启动后台任务 pub maintenance_interval: Option, - /// 池化连接的探活 ping 间隔(包括正在生成的连接);`None` 表示不主动 ping。 + /// 池化连接的探活 ping 间隔(包括正在生成的连接);`None` 表示不主动 ping pub ping_interval: Option, - /// 发送 ping 后等待任意入站帧的超时时间;零值表示不校验 Pong deadline。 + /// 发送 ping 后等待任意入站帧的超时时间;零值表示不校验 Pong deadline pub ping_timeout: Duration, - /// idle socket 无活动多久后视为失活。 + /// idle socket 无活动多久后视为失活 pub liveness_timeout: Option, - /// 等待下一条上游消息的空闲超时;`None` 或零值使用默认 300 秒。 + /// 等待下一条上游消息的空闲超时;`None` 或零值使用默认 300 秒 pub stream_idle_timeout: Option, } @@ -88,7 +88,7 @@ impl Default for CodexWebSocketPoolConfig { ping_interval: Some(DEFAULT_PING_INTERVAL), ping_timeout: DEFAULT_PING_TIMEOUT, // idle 连接不设失活截断:靠 ping/pong 保活,只在 max_age(55 分钟) - // 或 ping 失败时关闭,维持跨轮可复用连接。 + // 或 ping 失败时关闭,维持跨轮可复用连接 liveness_timeout: None, stream_idle_timeout: Some(DEFAULT_STREAM_IDLE_TIMEOUT), } @@ -96,7 +96,7 @@ impl Default for CodexWebSocketPoolConfig { } impl CodexWebSocketPoolConfig { - /// pump 后台任务的保活策略:从连接池配置派生出 ping/pong 与 liveness 策略。 + /// pump 后台任务的保活策略:从连接池配置派生出 ping/pong 与 liveness 策略 pub(crate) fn keepalive(&self) -> PumpKeepalive { PumpKeepalive { ping_interval: self.ping_interval, @@ -107,7 +107,7 @@ impl CodexWebSocketPoolConfig { } impl CodexWebSocketPool { - /// 构造不限制累计 slot 数的连接池策略和状态。 + /// 构造不限制累计 slot 数的连接池策略和状态 pub fn new(max_age: Duration) -> Self { Self::with_config(CodexWebSocketPoolConfig { max_age, @@ -118,7 +118,7 @@ impl CodexWebSocketPool { }) } - /// 使用完整配置构造连接池。 + /// 使用完整配置构造连接池 pub fn with_config(config: CodexWebSocketPoolConfig) -> Self { let pool = Self { inner: Arc::new(Mutex::new(WebSocketPoolState::default())), @@ -132,17 +132,17 @@ impl CodexWebSocketPool { pool } - /// pump 后台任务的保活策略(供建连时传入)。 + /// pump 后台任务的保活策略(供建连时传入) pub(crate) fn keepalive(&self) -> PumpKeepalive { self.config.keepalive() } - /// 等待下一条上游消息的空闲超时;`None` 或零值使用默认 300 秒。 + /// 等待下一条上游消息的空闲超时;`None` 或零值使用默认 300 秒 pub(crate) fn stream_idle_timeout(&self) -> Option { self.config.stream_idle_timeout } - /// 注册由连接池生命周期托管的 opening 任务。 + /// 注册由连接池生命周期托管的 opening 任务 pub(crate) fn spawn_connect_task(&self, future: impl Future + Send + 'static) { drop(self.tasks.spawn(future)); } @@ -195,7 +195,7 @@ impl CodexWebSocketPool { return WebSocketPoolAcquire::Bypass(WebSocketPoolBypassReason::Busy); }; // 零成本探活:后台 pump 已实时感知连接死亡(RST/Close/EOF/失活), - // 复用前只需读取 is_closed 标志,避免复用到静默死连接后卡到超时。 + // 复用前只需读取 is_closed 标志,避免复用到静默死连接后卡到超时 let expired = connection.created_at.elapsed() >= self.config.max_age; let closed = connection.websocket.is_closed(); if !expired && !closed { diff --git a/backend/crates/providers/openai/src/transport/websocket/pool/state.rs b/backend/crates/providers/openai/src/transport/websocket/pool/state.rs index a4bb5c96f..5930970ec 100644 --- a/backend/crates/providers/openai/src/transport/websocket/pool/state.rs +++ b/backend/crates/providers/openai/src/transport/websocket/pool/state.rs @@ -1,4 +1,4 @@ -//! WebSocket 连接池状态和值对象。 +//! WebSocket 连接池状态和值对象 use std::{ collections::{HashMap, VecDeque}, @@ -16,7 +16,7 @@ use super::super::super::{ use super::super::pump::{PumpedWebSocket, WebSocketConnectionObservation}; use super::lease::WebSocketPoolConnectOutcome; -/// WebSocket 连接池 key。 +/// WebSocket 连接池 key #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct CodexWebSocketPoolKey { base_url: String, @@ -28,7 +28,7 @@ pub struct CodexWebSocketPoolKey { } impl CodexWebSocketPoolKey { - /// 构造连接池 key。 + /// 构造连接池 key pub fn new( base_url: impl Into, account_id: impl Into, @@ -44,13 +44,13 @@ impl CodexWebSocketPoolKey { } } - /// 区分实际 WebSocket opening 画像,防止复用旧 UA 或不同握手语义的连接。 + /// 区分实际 WebSocket opening 画像,防止复用旧 UA 或不同握手语义的连接 pub(crate) fn with_connection_profile(mut self, connection_profile: impl Into) -> Self { self.connection_profile = connection_profile.into(); self } - /// 隔离同一逻辑会话中由不同下游 WebSocket 驱动的并发响应链。 + /// 隔离同一逻辑会话中由不同下游 WebSocket 驱动的并发响应链 pub(crate) fn with_downstream_connection_id( mut self, connection_id: impl Into, @@ -173,7 +173,7 @@ pub(crate) struct PooledWebSocketConnection { pub(crate) created_at: Instant, } -/// 只随具体 WebSocket 生命周期存在的续接状态。 +/// 只随具体 WebSocket 生命周期存在的续接状态 #[derive(Debug, Clone, Default, PartialEq, Eq)] pub(crate) struct WebSocketContinuationState { latest_response_id: Option, @@ -231,7 +231,7 @@ pub(super) async fn close_pooled_connections(connections: Vec bool { self.lock_state().shutting_down } diff --git a/backend/crates/providers/openai/src/transport/websocket/pump.rs b/backend/crates/providers/openai/src/transport/websocket/pump.rs index a16e40dce..1332e7a21 100644 --- a/backend/crates/providers/openai/src/transport/websocket/pump.rs +++ b/backend/crates/providers/openai/src/transport/websocket/pump.rs @@ -1,17 +1,17 @@ -//! 常驻后台的 WebSocket 泵(对齐官方 Codex CLI 的连接管理)。 +//! 常驻后台的 WebSocket 泵(对齐官方 Codex CLI 的连接管理) //! //! 每条上游 WebSocket 连接都由一个后台 pump 任务独占: -//! - 持续读取 socket:一旦观察到 `Close` / EOF / 传输错误,立即把连接标记为 `closed`。 -//! - 自动回应上游 `Ping`,并按 `ping_interval` 主动 `Ping`;收到任意入站帧即解除本次心跳 deadline。 -//! - 可选 `liveness_timeout`:长时间无任何入站活动时判定连接失活并退出。 +//! - 持续读取 socket:一旦观察到 `Close` / EOF / 传输错误,立即把连接标记为 `closed` +//! - 自动回应上游 `Ping`,并按 `ping_interval` 主动 `Ping`;收到任意入站帧即解除本次心跳 deadline +//! - 可选 `liveness_timeout`:长时间无任何入站活动时判定连接失活并退出 //! //! 因此空闲连接的“死没死”在后台被实时感知;复用方只需零成本读取 [`PumpedWebSocket::is_closed`], -//! 无需在请求路径上做同步探活 ping(消除“复用到静默死连接才卡住超时”的长尾)。 +//! 无需在请求路径上做同步探活 ping(消除“复用到静默死连接才卡住超时”的长尾) //! //! 收发都通过 channel 与 pump 任务交互: -//! - 发送:`send` 走 command channel,等待 pump 回执。 -//! - 接收:`next` 从 message channel 取出 pump 转发的入站帧(`Ping`/`Pong` 已被 pump 吞掉)。 -//! - 入站缓冲满时暂停读取 socket 和主动探活,仅继续处理发送/关闭命令;消费恢复后按原顺序继续转发。 +//! - 发送:`send` 走 command channel,等待 pump 回执 +//! - 接收:`next` 从 message channel 取出 pump 转发的入站帧(`Ping`/`Pong` 已被 pump 吞掉) +//! - 入站缓冲满时暂停读取 socket 和主动探活,仅继续处理发送/关闭命令;消费恢复后按原顺序继续转发 use std::sync::{ Arc, Mutex, @@ -30,7 +30,7 @@ use uuid::Uuid; use super::CodexWebSocketCloseError; -/// 底层 tungstenite WebSocket 流。 +/// 底层 tungstenite WebSocket 流 pub(crate) trait WebSocketIo: Stream> + Sink @@ -51,7 +51,7 @@ pub(crate) type RawWsStream = Box; const PUMP_COMMAND_BUFFER: usize = 32; -/// pump 日志的账号级关联上下文;空闲连接被上游重置等场景用于回溯归属。 +/// pump 日志的账号级关联上下文;空闲连接被上游重置等场景用于回溯归属 #[derive(Debug, Clone, Default)] pub(crate) struct PumpLogContext { pub(crate) account_id: Option, @@ -67,19 +67,19 @@ impl PumpLogContext { } } -/// pump 保活策略。 +/// pump 保活策略 #[derive(Debug, Clone, Copy)] pub(crate) struct PumpKeepalive { - /// 主动 `Ping` 间隔;`None` 表示 pump 不主动 ping(仅被动读取 + 回应上游 ping)。 + /// 主动 `Ping` 间隔;`None` 表示 pump 不主动 ping(仅被动读取 + 回应上游 ping) pub(crate) ping_interval: Option, - /// 主动 `Ping` 发出后等待任意入站帧的 deadline;`None` 表示不校验响应。 + /// 主动 `Ping` 发出后等待任意入站帧的 deadline;`None` 表示不校验响应 pub(crate) ping_timeout: Option, - /// 无入站活动多久后判定失活;`None` 表示只靠显式 close/传输错误发现死亡。 + /// 无入站活动多久后判定失活;`None` 表示只靠显式 close/传输错误发现死亡 pub(crate) liveness_timeout: Option, } impl PumpKeepalive { - /// 不做主动保活(用于即用即弃的非池化连接)。 + /// 不做主动保活(用于即用即弃的非池化连接) pub(crate) fn disabled() -> Self { Self { ping_interval: None, @@ -280,7 +280,7 @@ impl PumpLifecycleState { } } -/// 由后台 pump 任务托管的 WebSocket 连接句柄。 +/// 由后台 pump 任务托管的 WebSocket 连接句柄 pub(crate) struct PumpedWebSocket { connection_id: Uuid, tx_command: mpsc::Sender, @@ -300,7 +300,7 @@ impl std::fmt::Debug for PumpedWebSocket { } impl PumpedWebSocket { - /// 用底层流启动一个 pump 任务并返回句柄。 + /// 用底层流启动一个 pump 任务并返回句柄 pub(crate) fn new( inner: RawWsStream, keepalive: PumpKeepalive, @@ -342,7 +342,7 @@ impl PumpedWebSocket { } } - /// 通过 pump 发送一帧,返回底层 `send` 的结果。 + /// 通过 pump 发送一帧,返回底层 `send` 的结果 pub(crate) async fn send(&self, message: Message) -> Result<(), tungstenite::Error> { let (ack, rx_ack) = oneshot::channel(); if self @@ -358,14 +358,15 @@ impl PumpedWebSocket { .unwrap_or(Err(tungstenite::Error::ConnectionClosed)) } - /// 取出下一帧入站消息(`Ping`/`Pong` 已被 pump 处理,不会到达这里)。 + /// 取出下一帧入站消息(`Ping`/`Pong` 已被 pump 处理,不会到达这里) /// - /// 返回 `None` 表示连接已结束(pump 已退出且缓冲已排空)。 + /// 返回 `None` 表示连接已结束(pump 已退出且缓冲已排空) pub(crate) async fn next(&mut self) -> Option> { self.rx_message.recv().await } - /// 连接是否已被后台 pump 判定关闭/失活。零成本,用于复用前探活。 + /// 连接是否已被后台 pump 判定关闭/失活 + /// 零成本,用于复用前探活 pub(crate) fn is_closed(&self) -> bool { self.closed.load(Ordering::Acquire) || self.tx_command.is_closed() } @@ -389,7 +390,7 @@ impl PumpedWebSocket { .observation(self.connection_id, Instant::now()) } - /// 主动关闭连接(best-effort 发送 Close 帧)。 + /// 主动关闭连接(best-effort 发送 Close 帧) pub(crate) async fn close(&self) { if !self.is_closed() { let _ = self.send(Message::Close(None)).await; @@ -428,7 +429,7 @@ async fn pump_loop( let liveness_timeout = keepalive.liveness_timeout.filter(|d| !d.is_zero()); let mut ping_ticker = ping_interval.map(|d| { // 首个 tick 推迟一整个间隔:tokio::time::interval 默认会让首个 tick 立即就绪, - // 否则连接一建立就会立刻发一帧 Ping,与首个请求 Text 抢跑、打乱帧序。 + // 否则连接一建立就会立刻发一帧 Ping,与首个请求 Text 抢跑、打乱帧序 let mut ticker = tokio::time::interval_at(tokio::time::Instant::now() + d, d); ticker.set_missed_tick_behavior(MissedTickBehavior::Skip); ticker @@ -453,7 +454,7 @@ async fn pump_loop( if let Some(reason) = pending.exit_reason { break 'pump reason; } - // 背压期间无法读取 socket,不能把这段本地下游阻塞误算成上游静默。 + // 背压期间无法读取 socket,不能把这段本地下游阻塞误算成上游静默 last_activity = Instant::now(); record_activity(&lifecycle, last_activity); } @@ -649,7 +650,7 @@ fn observe_inbound(connection_id: Uuid, pending_ping: &mut Option, "Responses WebSocket pump received a non-matching Pong" ); } - // 匹配 Pong 是探针的直接回执;其他入站帧同样证明上游链路仍可达。 + // 匹配 Pong 是探针的直接回执;其他入站帧同样证明上游链路仍可达 *pending_ping = None; } @@ -670,7 +671,7 @@ pub(crate) fn transport_metric_reason(error: &tungstenite::Error) -> &'static st std::io::ErrorKind::TimedOut => "transport_timeout", _ => "transport_error", }, - // 未收到关闭握手只能证明连接异常结束,不能据此认定收到 TCP RST。 + // 未收到关闭握手只能证明连接异常结束,不能据此认定收到 TCP RST tungstenite::Error::Protocol( tungstenite::error::ProtocolError::ResetWithoutClosingHandshake, ) => "reset_without_closing_handshake", @@ -761,7 +762,7 @@ fn log_pump_exit( } } -/// 等待 ping ticker;`None` 时永远挂起,让 `select!` 分支实际禁用。 +/// 等待 ping ticker;`None` 时永远挂起,让 `select!` 分支实际禁用 async fn tick(ticker: &mut Option) { match ticker { Some(ticker) => { @@ -771,7 +772,7 @@ async fn tick(ticker: &mut Option) { } } -/// 等待可选 deadline;`None` 时永远挂起,让 `select!` 分支实际禁用。 +/// 等待可选 deadline;`None` 时永远挂起,让 `select!` 分支实际禁用 async fn wait_until(deadline: Option) { match deadline { Some(deadline) => tokio::time::sleep_until(deadline).await, diff --git a/backend/crates/providers/openai/tests/admin.rs b/backend/crates/providers/openai/tests/admin.rs index 929f6af19..3d8e451d7 100644 --- a/backend/crates/providers/openai/tests/admin.rs +++ b/backend/crates/providers/openai/tests/admin.rs @@ -1,3 +1,5 @@ +//! 验证 OpenAI 管理能力、请求画像、Bundle 组装与额度投影 + use std::collections::{BTreeMap, BTreeSet}; use std::num::NonZeroU32; use std::sync::{Arc, Mutex}; @@ -559,7 +561,7 @@ async fn openai_core_provider_projects_codex_request_observation_without_routing assert_eq!(observation.request_kind.as_deref(), Some("compaction")); assert_eq!(observation.subagent_kind.as_deref(), Some("review")); - // Codex 当前只在特定多代理预设组合下给出 reasoning_preset;普通 high 保持空值。 + // Codex 当前只在特定多代理预设组合下给出 reasoning_preset;普通 high 保持空值 assert_eq!(observation.reasoning_preset, None); assert!(observation.compact); } @@ -1359,7 +1361,7 @@ async fn openai_admin_preserves_expired_window_usage_and_exhaustion_attribution( .await .expect("project quota"); assert_eq!(projected.limit_reached, exhausted); - // 账号接口还会归一化耗尽展示;过期周窗口不能把触顶错误转移到短期窗口。 + // 账号接口还会归一化耗尽展示;过期周窗口不能把触顶错误转移到短期窗口 projected.apply_limit_reached_display(); let primary = projected .windows @@ -2386,7 +2388,7 @@ mod errors { .prepare_refresh(command()) .await .unwrap_err(); - // 既有 transport 策略未认定此错误为安全重试,本次不能因展示更详细而放宽重试边界。 + // 既有 transport 策略未认定此错误为安全重试,本次不能因展示更详细而放宽重试边界 assert_eq!(error.kind(), Kind::Ambiguous); assert_eq!( error.public_message(), diff --git a/backend/crates/providers/openai/tests/config.rs b/backend/crates/providers/openai/tests/config.rs index 9b59a1bda..259827a8c 100644 --- a/backend/crates/providers/openai/tests/config.rs +++ b/backend/crates/providers/openai/tests/config.rs @@ -1,3 +1,5 @@ +//! 验证 OpenAI 配置默认值、路径派生与上游地址约束 + use std::path::Path; use provider_openai::config::{DEFAULT_STREAM_MAX_RETRIES, MAX_STREAM_MAX_RETRIES, OpenAiConfig}; diff --git a/backend/crates/providers/openai/tests/credential/admin.rs b/backend/crates/providers/openai/tests/credential/admin.rs index e1cc07f36..78eb91a13 100644 --- a/backend/crates/providers/openai/tests/credential/admin.rs +++ b/backend/crates/providers/openai/tests/credential/admin.rs @@ -1,3 +1,5 @@ +//! 验证 OpenAI 账号导入的代理绑定、凭据解析与提交前校验 + use std::{ num::NonZeroU32, sync::Arc, diff --git a/backend/crates/providers/openai/tests/credential/catalog.rs b/backend/crates/providers/openai/tests/credential/catalog.rs index 3582c98d0..b35be886f 100644 --- a/backend/crates/providers/openai/tests/credential/catalog.rs +++ b/backend/crates/providers/openai/tests/credential/catalog.rs @@ -1,3 +1,5 @@ +//! 验证 OpenAI 模型目录的账号和版本隔离、缓存合并与失效 + use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -39,7 +41,7 @@ impl Respond for ReplacingCatalogResponder { } } -/// 按调用顺序为不同套餐 scope 返回不同的目录正文。 +/// 按调用顺序为不同套餐 scope 返回不同的目录正文 struct SequencedCatalogResponder { calls: Arc, bodies: [&'static [u8]; 2], @@ -882,7 +884,7 @@ async fn api_key_client_catalog_negotiates_and_preserves_versioned_native_object .seed_api_key("acct_api", upstream.uri(), ResponsesTransport::Http) .await; let account = store.account("acct_api").expect("API account"); - // 先填满普通 ID 目录,确保客户端不会把已有套餐缓存误当成原生元数据。 + // 先填满普通 ID 目录,确保客户端不会把已有套餐缓存误当成原生元数据 Mock::given(method("GET")) .and(path("/models")) .and(|request: &Request| request.url.query().is_none()) @@ -1379,8 +1381,8 @@ async fn slow_api_key_catalogs_share_a_deadline_and_preserve_healthy_catalogs() }) .await .expect("healthy API catalog completed and released a concurrency slot"); - // 四个并发读取中,健康 API 目录完成后才会发出第四个慢请求,另有一次独立刷新。 - // 只快进慢目录的共享期限,随后恢复实时时钟让 OAuth 冷请求完整通过真实 HTTP。 + // 四个并发读取中,健康 API 目录完成后才会发出第四个慢请求,另有一次独立刷新 + // 只快进慢目录的共享期限,随后恢复实时时钟让 OAuth 冷请求完整通过真实 HTTP tokio::time::pause(); tokio::time::advance(Duration::from_secs(15)).await; tokio::time::resume(); @@ -1434,14 +1436,14 @@ async fn account_catalog_documents_keep_native_objects_of_the_account_plan() { assert_eq!(models.len(), 2); assert_eq!(models[0].request_model().as_str(), "gpt-pro"); assert_eq!(models[1].request_model().as_str(), "gpt-shared"); - // 导出的目录文件靠原生对象携带上下文窗口等元数据,正文必须原样保留。 + // 导出的目录文件靠原生对象携带上下文窗口等元数据,正文必须原样保留 assert_eq!(models[0].document().protocol(), "codex"); let document: serde_json::Value = serde_json::from_slice(models[1].document().body()).expect("native document"); assert_eq!(document["context_window"], 272_000); assert_eq!(document["display_name"], "Pro"); - // 另一套餐的账号不能拿到别的套餐条目,否则客户端会列出自己用不了的模型。 + // 另一套餐的账号不能拿到别的套餐条目,否则客户端会列出自己用不了的模型 let (plus_models, _) = service .account_catalog_documents(&plus) .await diff --git a/backend/crates/providers/openai/tests/credential/contract/capacity.rs b/backend/crates/providers/openai/tests/credential/contract/capacity.rs index 07cb11c18..930c24798 100644 --- a/backend/crates/providers/openai/tests/credential/contract/capacity.rs +++ b/backend/crates/providers/openai/tests/credential/contract/capacity.rs @@ -1,3 +1,5 @@ +//! 验证 OpenAI 选号容量、亲和排队与硬性不可用后的切换 + use futures::{FutureExt, future::BoxFuture}; use gateway_core::account::AccountRuntimeSignals; use gateway_core::concurrency::{ConcurrencyQueuePolicy, QueueRejection}; @@ -185,7 +187,7 @@ async fn affinity_queue_full_and_timeout_preserve_binding_despite_free_fallback( }; let mut head = Box::pin(selector.select(&request)); assert!(head.as_mut().now_or_never().is_none()); - // 槽位已空,但新请求不能越过旧等待者,也不能转而抢占另一账号。 + // 槽位已空,但新请求不能越过旧等待者,也不能转而抢占另一账号 leases.busy_accounts.lock().unwrap().clear(); let rejected = selector .select(&SelectCodexCredential { diff --git a/backend/crates/providers/openai/tests/credential/contract/mod.rs b/backend/crates/providers/openai/tests/credential/contract/mod.rs index 8bb47dda3..9ad9c05a9 100644 --- a/backend/crates/providers/openai/tests/credential/contract/mod.rs +++ b/backend/crates/providers/openai/tests/credential/contract/mod.rs @@ -1,3 +1,5 @@ +//! OpenAI 凭据合同测试入口,以及凭据编码、持久化与选号约束测试 + mod capacity; use std::collections::{BTreeMap, BTreeSet}; @@ -1326,7 +1328,7 @@ fn rate_limited_failure_does_not_consult_stale_quota_snapshot() { .credential_state(), CredentialState::Ready ); - // 429 临时限流写入 Redis 冷却,不读写 quota JSON。 + // 429 临时限流写入 Redis 冷却,不读写 quota JSON assert_eq!(store.quota_reads(), 0); let cooldown = block_on(cooldowns.read(account.id())).expect("read cooldown"); assert!( diff --git a/backend/crates/providers/openai/tests/credential/cookie.rs b/backend/crates/providers/openai/tests/credential/cookie.rs index 2bebd4286..5717fa7b0 100644 --- a/backend/crates/providers/openai/tests/credential/cookie.rs +++ b/backend/crates/providers/openai/tests/credential/cookie.rs @@ -1,3 +1,5 @@ +//! 验证 Codex Cookie 接收范围与回放时的主机和安全属性限制 + use url::Url; use provider_openai::credential::{CodexCookiePolicy, CookiePolicyError}; diff --git a/backend/crates/providers/openai/tests/credential/mod.rs b/backend/crates/providers/openai/tests/credential/mod.rs index b57af9a90..d77ca0837 100644 --- a/backend/crates/providers/openai/tests/credential/mod.rs +++ b/backend/crates/providers/openai/tests/credential/mod.rs @@ -1,3 +1,5 @@ +//! OpenAI 凭据管理、授权、目录与额度的测试入口 + mod admin; mod catalog; mod contract; diff --git a/backend/crates/providers/openai/tests/credential/oauth.rs b/backend/crates/providers/openai/tests/credential/oauth.rs index 5fe555ccc..d4ddf6f24 100644 --- a/backend/crates/providers/openai/tests/credential/oauth.rs +++ b/backend/crates/providers/openai/tests/credential/oauth.rs @@ -1,3 +1,5 @@ +//! 验证 Codex 授权参数、安装身份与令牌交换合同 + use std::collections::BTreeMap; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime}; @@ -146,7 +148,7 @@ fn reauthorization_mutation( fn id_token(payload: serde_json::Value) -> String { let payload = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&payload).expect("payload JSON")); - // 官方逻辑只读取 payload;header/signature 不参与本地 metadata 解析。 + // 官方逻辑只读取 payload;header/signature 不参与本地 metadata 解析 format!("unverified-header.{payload}.unverified-signature") } diff --git a/backend/crates/providers/openai/tests/credential/profile_statistics.rs b/backend/crates/providers/openai/tests/credential/profile_statistics.rs index 6db4ee460..fe51d0826 100644 --- a/backend/crates/providers/openai/tests/credential/profile_statistics.rs +++ b/backend/crates/providers/openai/tests/credential/profile_statistics.rs @@ -1,3 +1,5 @@ +//! 验证账号资料、头像与订阅查询的绑定、缓存及失败处理 + use std::sync::Arc; use chrono::{TimeZone as _, Utc}; diff --git a/backend/crates/providers/openai/tests/credential/quota/capacity_freeze.rs b/backend/crates/providers/openai/tests/credential/quota/capacity_freeze.rs index 546f7f537..742d0b752 100644 --- a/backend/crates/providers/openai/tests/credential/quota/capacity_freeze.rs +++ b/backend/crates/providers/openai/tests/credential/quota/capacity_freeze.rs @@ -1,4 +1,4 @@ -//! 容量熔断触发:高频容量类失败按滑动窗口计数冻结账号,成功调用清空证据。 +//! 容量熔断触发:高频容量类失败按滑动窗口计数冻结账号,成功调用清空证据 use std::sync::Arc; use std::time::{Duration, SystemTime}; @@ -150,7 +150,7 @@ async fn successful_inference_clears_capacity_evidence() { assert!(cooldowns.capacity_evidence(account.id()).is_none()); } -/// 阈值与窗口边界使用与迁移 check 约束一致的构造校验。 +/// 阈值与窗口边界使用与迁移 check 约束一致的构造校验 #[test] fn freeze_policy_rejects_out_of_range_configuration() { for (threshold, window, duration) in [(1, 600, 7_200), (12, 59, 7_200), (12, 600, 299)] { diff --git a/backend/crates/providers/openai/tests/credential/quota/mod.rs b/backend/crates/providers/openai/tests/credential/quota/mod.rs index e7a4f5088..ab9e8a24a 100644 --- a/backend/crates/providers/openai/tests/credential/quota/mod.rs +++ b/backend/crates/providers/openai/tests/credential/quota/mod.rs @@ -1,4 +1,4 @@ -//! OpenAI 额度事实边界与展示快照回归。 +//! OpenAI 额度事实边界与展示快照回归 mod capacity_freeze; mod recovery; @@ -208,7 +208,7 @@ async fn passive_quota_updates_keep_core_and_model_specific_buckets_independent( let spark_headers = vec![ // 生产响应会把当前具名桶同时复制到旧版 `x-codex-*` 默认头;这个 - // 兼容别名不能覆盖已有的账号 core 桶。 + // 兼容别名不能覆盖已有的账号 core 桶 ("x-codex-active-limit".to_owned(), "codex".to_owned()), ("x-codex-primary-used-percent".to_owned(), "1".to_owned()), ( @@ -524,7 +524,7 @@ async fn passive_plan_observations_update_account_without_replaying_stale_plan() ); let state = exhausted_quota(None); persist_quota_state(&store, &original, state).await; - // 仅有套餐的新观察不能清除既有额度耗尽结论。 + // 仅有套餐的新观察不能清除既有额度耗尽结论 service .synchronize_passive_headers( &original, @@ -535,7 +535,7 @@ async fn passive_plan_observations_update_account_without_replaying_stale_plan() let changed = store.account("acct_passive_plan").unwrap(); assert_eq!(changed.plan_type(), Some("plus")); assert_eq!(changed.quota(), state); - // 后台 RT 刷新拿着旧账号快照返回时,仍要提交新 token 并保留刚观测的套餐。 + // 后台 RT 刷新拿着旧账号快照返回时,仍要提交新 token 并保留刚观测的套餐 store .repository() .rotate_refreshed_oauth_secret( @@ -710,7 +710,7 @@ async fn quota_endpoint_rejection_preserves_details_without_changing_account_fac } other => panic!("expected upstream rejection, got {other:?}"), } - // 额度查询诊断不能进入凭据快照,否则会干扰在途 OAuth 刷新的终态提交。 + // 额度查询诊断不能进入凭据快照,否则会干扰在途 OAuth 刷新的终态提交 let current = store.account(account_id).expect("account after refresh"); assert_eq!(current, before); store diff --git a/backend/crates/providers/openai/tests/credential/quota/recovery.rs b/backend/crates/providers/openai/tests/credential/quota/recovery.rs index f3490c6b9..7930a5d48 100644 --- a/backend/crates/providers/openai/tests/credential/quota/recovery.rs +++ b/backend/crates/providers/openai/tests/credential/quota/recovery.rs @@ -1,4 +1,4 @@ -//! 额度刷新与凭证事实相互独立的回归测试。 +//! 额度刷新与凭证事实相互独立的回归测试 use super::*; diff --git a/backend/crates/providers/openai/tests/credential/quota/refresh_timing.rs b/backend/crates/providers/openai/tests/credential/quota/refresh_timing.rs index 437af4114..c2f1049cc 100644 --- a/backend/crates/providers/openai/tests/credential/quota/refresh_timing.rs +++ b/backend/crates/providers/openai/tests/credential/quota/refresh_timing.rs @@ -1,4 +1,4 @@ -//! 通过 quota 服务验证周期复核与 reset 宽限期,不依赖内部调度状态。 +//! 通过 quota 服务验证周期复核与 reset 宽限期,不依赖内部调度状态 use std::time::Duration; @@ -25,7 +25,7 @@ async fn reset_grace_bypasses_periodic_throttle_once_then_allows_the_next_window let account = store.account("acct_quota_timing").expect("account"); let server = MockServer::start().await; let service = quota_service_with_base_url(&store, reqwest::Client::new(), server.uri()); - // 显式推进调度时刻,真实 HTTP 与落库路径仍完整执行。 + // 显式推进调度时刻,真实 HTTP 与落库路径仍完整执行 let short_reset = now .duration_since(SystemTime::UNIX_EPOCH) .unwrap() @@ -88,7 +88,7 @@ async fn reset_grace_bypasses_periodic_throttle_once_then_allows_the_next_window .expect("next scan before weekly grace deadline"); assert_eq!(server.received_requests().await.expect("requests").len(), 1); - // 周窗口的到期复核仍未恢复时,也不能每轮扫描重复请求。 + // 周窗口的到期复核仍未恢复时,也不能每轮扫描重复请求 now = reset_grace(week_reset); assert_eq!( service @@ -192,7 +192,7 @@ async fn allowed_account_with_expired_window_synchronizes_at_reset_grace() { "正常账号首次复核也必须等待 reset 宽限期" ); - // 当到达 reset + 120s 宽限期后,账号虽然处于 allowed 状态,但包含已到期的非零用量窗口,被调度主动同步。 + // 当到达 reset + 120s 宽限期后,账号虽然处于 allowed 状态,但包含已到期的非零用量窗口,被调度主动同步 mount_usage(&server, usage(0, short_reset + 18_000)).await; now = reset_grace(short_reset); diff --git a/backend/crates/providers/openai/tests/credential/quota/scheduling.rs b/backend/crates/providers/openai/tests/credential/quota/scheduling.rs index 9ff120d67..f85730496 100644 --- a/backend/crates/providers/openai/tests/credential/quota/scheduling.rs +++ b/backend/crates/providers/openai/tests/credential/quota/scheduling.rs @@ -1,4 +1,4 @@ -//! 持久化额度的有效期与短暂并发占用共同参与选号。 +//! 持久化额度的有效期与短暂并发占用共同参与选号 use std::collections::BTreeSet; use std::num::NonZeroU32; diff --git a/backend/crates/providers/openai/tests/credential/quota/slots.rs b/backend/crates/providers/openai/tests/credential/quota/slots.rs index f3b2a54a0..9a6fa0e33 100644 --- a/backend/crates/providers/openai/tests/credential/quota/slots.rs +++ b/backend/crates/providers/openai/tests/credential/quota/slots.rs @@ -1,4 +1,4 @@ -//! 各额度槽独立恢复,跨刷新保留未解除的限制。 +//! 各额度槽独立恢复,跨刷新保留未解除的限制 use super::*; @@ -255,7 +255,7 @@ async fn worker_detects_early_resets_without_unlocking_other_exhausted_windows() "{name}" ); - // 未恢复的周窗口仍在未来,也必须保留复核节流;恢复后则退出定时复核。 + // 未恢复的周窗口仍在未来,也必须保留复核节流;恢复后则退出定时复核 service.synchronize().await.expect("next worker cycle"); assert_eq!( server.received_requests().await.expect("requests").len(), @@ -565,8 +565,9 @@ async fn an_unknown_slot_establishes_a_baseline_before_it_can_recover() { #[tokio::test] async fn weekly_usage_falling_back_within_the_same_window_recovers_after_two_observations() { // 滑动周窗口的用量可以在同一窗口内回落:reset 不滚动、用量从 100% 降到 - // 正常水平。此时"reset 前进 + 低用量"永远不会成立,解除依赖连续两次 - // 观测都未触顶;单次观测无法排除耗尽后立刻拉到的旧快照,保持锁定。 + // 正常水平 + // 此时"reset 前进 + 低用量"永远不会成立,解除依赖连续两次 + // 观测都未触顶;单次观测无法排除耗尽后立刻拉到的旧快照,保持锁定 let store = Arc::new(MemoryAccountStore::default()); create_account(&store, "acct_weekly_fallout").await; let account = store.account("acct_weekly_fallout").expect("account"); @@ -581,7 +582,7 @@ async fn weekly_usage_falling_back_within_the_same_window_recovers_after_two_obs .await .expect("seed exhausted quota"); - // 5h 窗口滚动前进,周窗口 reset 不变但用量回落到正常水平:只标记候选。 + // 5h 窗口滚动前进,周窗口 reset 不变但用量回落到正常水平:只标记候选 let mut first_observation = usage((0, SHORT_RESET + 18_000), (18, WEEK_RESET)); first_observation["rate_limit"]["allowed"] = json!(true); mount_usage(&server, first_observation).await; @@ -591,7 +592,7 @@ async fn weekly_usage_falling_back_within_the_same_window_recovers_after_two_obs .expect("first fallout observation"); assert!(first.quota().is_exhausted()); - // 第二次连续观测仍未触顶:解除周窗口。 + // 第二次连续观测仍未触顶:解除周窗口 let mut second_observation = usage((9, SHORT_RESET + 36_000), (18, WEEK_RESET)); second_observation["rate_limit"]["allowed"] = json!(true); mount_usage(&server, second_observation).await; @@ -604,7 +605,7 @@ async fn weekly_usage_falling_back_within_the_same_window_recovers_after_two_obs #[tokio::test] async fn a_single_same_window_fallout_observation_cannot_break_the_reset_baseline() { - // 连续性中断(观测到重新触顶)后必须重新积累两次未触顶证据。 + // 连续性中断(观测到重新触顶)后必须重新积累两次未触顶证据 let store = Arc::new(MemoryAccountStore::default()); create_account(&store, "acct_weekly_fallout_gap").await; let account = store.account("acct_weekly_fallout_gap").expect("account"); @@ -627,7 +628,7 @@ async fn a_single_same_window_fallout_observation_cannot_break_the_reset_baselin .quota() .is_exhausted() ); - // 观测到重新触顶,候选证据清零。 + // 观测到重新触顶,候选证据清零 mount_usage(&server, usage((0, SHORT_RESET + 36_000), (100, WEEK_RESET))).await; assert!( service diff --git a/backend/crates/providers/openai/tests/credential/quota/snapshot.rs b/backend/crates/providers/openai/tests/credential/quota/snapshot.rs index cdee4e083..070a864d9 100644 --- a/backend/crates/providers/openai/tests/credential/quota/snapshot.rs +++ b/backend/crates/providers/openai/tests/credential/quota/snapshot.rs @@ -1,6 +1,6 @@ -//! 快照聚合、窗口滚动和调度信号回归。 +//! 快照聚合、窗口滚动和调度信号回归 //! -//! 覆盖 raw JSON 解析、`limit_reached` 快照级聚合与窗口投影。 +//! 覆盖 raw JSON 解析、`limit_reached` 快照级聚合与窗口投影 use std::sync::Arc; use std::time::{Duration, SystemTime}; @@ -62,7 +62,7 @@ fn parser_keeps_full_percent_as_display_without_provider_exhaustion_signal() { })) .expect("valid full-percent quota"); - // 百分比只负责展示;额度访问结论由规范化 QuotaState 承载。 + // 百分比只负责展示;额度访问结论由规范化 QuotaState 承载 assert_eq!(fact.remaining_percent(), Some(0)); } @@ -462,7 +462,7 @@ async fn code_review_limit_projects_as_one_snapshot_per_limit_id() { .expect("quota snapshot"); // spend_control 不生成窗口(只作 exhaustion 信号);官方 map 协议中同一个 - // limit_id 只能保留一个快照,顶层 code_review 事实优先于重复 additional。 + // limit_id 只能保留一个快照,顶层 code_review 事实优先于重复 additional let review = snapshot .windows() .iter() diff --git a/backend/crates/providers/openai/tests/credential/quota/subscription.rs b/backend/crates/providers/openai/tests/credential/quota/subscription.rs index a97c0c927..839873994 100644 --- a/backend/crates/providers/openai/tests/credential/quota/subscription.rs +++ b/backend/crates/providers/openai/tests/credential/quota/subscription.rs @@ -1,4 +1,4 @@ -//! 订阅展示不得随手工或后台额度检查预取。 +//! 订阅展示不得随手工或后台额度检查预取 use super::*; diff --git a/backend/crates/providers/openai/tests/credential/quota/warmup.rs b/backend/crates/providers/openai/tests/credential/quota/warmup.rs index 48ef15d9c..8aad5877b 100644 --- a/backend/crates/providers/openai/tests/credential/quota/warmup.rs +++ b/backend/crates/providers/openai/tests/credential/quota/warmup.rs @@ -1,3 +1,5 @@ +//! 验证额度预热按 SSE 终态判定结果并跳过过期凭据 + use super::*; #[tokio::test] diff --git a/backend/crates/providers/openai/tests/credential/refresh.rs b/backend/crates/providers/openai/tests/credential/refresh.rs index 728024731..993f9bcc4 100644 --- a/backend/crates/providers/openai/tests/credential/refresh.rs +++ b/backend/crates/providers/openai/tests/credential/refresh.rs @@ -1,3 +1,5 @@ +//! 验证 OpenAI 令牌刷新、凭据状态与失败反馈的持久化 + use std::num::NonZeroU32; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; @@ -829,7 +831,7 @@ struct QuotaRejectionDuringRefresh { #[async_trait] impl TokenRefresher for QuotaRejectionDuringRefresh { async fn refresh(&self, _: &str) -> Result { - // 模拟 RT 请求在途时,手动或周期额度查询先收到 401。 + // 模拟 RT 请求在途时,手动或周期额度查询先收到 401 if self.background_quota { assert_eq!( self.quota diff --git a/backend/crates/providers/openai/tests/credential/token_client.rs b/backend/crates/providers/openai/tests/credential/token_client.rs index 86119bca2..198fa800a 100644 --- a/backend/crates/providers/openai/tests/credential/token_client.rs +++ b/backend/crates/providers/openai/tests/credential/token_client.rs @@ -1,3 +1,5 @@ +//! 验证 OAuth 令牌请求的代理、客户端画像与响应解析边界 + use provider_openai::credential::token_client::{ AuthorizationCodeExchangeError, AuthorizationCodeExchanger, AuthorizationCodeGrant, OpenAiTokenClient, RefreshFailure, TokenClientConfig, TokenRefresher, diff --git a/backend/crates/providers/openai/tests/credential/types.rs b/backend/crates/providers/openai/tests/credential/types.rs index 05a7fd2e3..f6635cfd9 100644 --- a/backend/crates/providers/openai/tests/credential/types.rs +++ b/backend/crates/providers/openai/tests/credential/types.rs @@ -1,3 +1,5 @@ +//! 验证 OpenAI 凭据类型的脱敏、Cookie 往返与字段约束 + use chrono::Utc; use provider_openai::credential::{ CodexAccountProfile, CodexCookie, CodexCredentialData, CodexCredentialPrincipal, diff --git a/backend/crates/providers/openai/tests/main.rs b/backend/crates/providers/openai/tests/main.rs index c4aa9a5f2..5395a6382 100644 --- a/backend/crates/providers/openai/tests/main.rs +++ b/backend/crates/providers/openai/tests/main.rs @@ -1,3 +1,5 @@ +//! OpenAI Provider、凭据与上游传输的测试入口 + mod admin; mod config; mod credential; diff --git a/backend/crates/providers/openai/tests/provider/contract/account_isolation.rs b/backend/crates/providers/openai/tests/provider/contract/account_isolation.rs index eca74f771..0e4dbb803 100644 --- a/backend/crates/providers/openai/tests/provider/contract/account_isolation.rs +++ b/backend/crates/providers/openai/tests/provider/contract/account_isolation.rs @@ -1,3 +1,5 @@ +//! 验证 HTTP 与 WebSocket 的账号头部隔离及中间件覆盖 + use super::*; const SELECTED_ACCOUNT: &str = "acct_scope_same"; diff --git a/backend/crates/providers/openai/tests/provider/contract/capacity.rs b/backend/crates/providers/openai/tests/provider/contract/capacity.rs index 63a248506..9ac5d9a9b 100644 --- a/backend/crates/providers/openai/tests/provider/contract/capacity.rs +++ b/backend/crates/providers/openai/tests/provider/contract/capacity.rs @@ -1,3 +1,5 @@ +//! 验证 OpenAI 请求等待账号容量与 Guardian 预留槽位 + use futures::FutureExt; use gateway_core::account::AccountRuntimeSignals; use gateway_core::concurrency::ConcurrencyQueuePolicy; @@ -88,7 +90,7 @@ async fn queued_session_sends_only_after_capacity_recovers_while_a_new_child_can )); assert!(waiting.as_mut().now_or_never().is_none()); - // 子线程还没有自己的绑定,父账号只是默认偏好,不能被强制挤进父账号队列。 + // 子线程还没有自己的绑定,父账号只是默认偏好,不能被强制挤进父账号队列 let mut child = provider .execute( planned_request("openai", operation("capacity-child")), @@ -182,7 +184,7 @@ async fn guardian_requests_can_use_the_reserved_slot_that_normal_requests_cannot .expect(3) .mount(&server) .await; - // 默认账号上限为 2、预留 1:普通请求只能使用 1 个名额,Guardian 可以用满 2 个。 + // 默认账号上限为 2、预留 1:普通请求只能使用 1 个名额,Guardian 可以用满 2 个 let provider = provider_with_affinity_and_base_url_and_leases( &store, Arc::new(MemorySessionAffinity::default()), @@ -252,7 +254,7 @@ async fn guardian_requests_can_use_the_reserved_slot_that_normal_requests_cannot assert_eq!(limits(&leases), [1, 2]); drop(guardian); - // 同一个 Provider 的新请求读取关闭后的策略,不需要重新初始化选择器。 + // 同一个 Provider 的新请求读取关闭后的策略,不需要重新初始化选择器 let mut unreserved = provider .execute( planned_request("openai", subagent_operation(None)), diff --git a/backend/crates/providers/openai/tests/provider/contract/mod.rs b/backend/crates/providers/openai/tests/provider/contract/mod.rs index 3903a4219..424af1eac 100644 --- a/backend/crates/providers/openai/tests/provider/contract/mod.rs +++ b/backend/crates/providers/openai/tests/provider/contract/mod.rs @@ -1,3 +1,5 @@ +//! OpenAI 执行合同测试入口,以及协议转换与发送前校验测试 + mod account_isolation; mod capacity; mod precommit; @@ -682,7 +684,7 @@ async fn selected_proxy_location_overrides_global_and_reloads_without_mutating_c .mount(proxy) .await; } - // 目标不可解析;收到请求证明使用的是账号代理,而非测试机默认出口。 + // 目标不可解析;收到请求证明使用的是账号代理,而非测试机默认出口 let provider = provider_with_base_url(&store, "http://upstream.invalid".to_owned()); let original = json!({"model":"gpt-5.4", "input":[ {"role":"user", "content":[{"type":"input_text", "text":"UTC"}], "internal_chat_message_metadata_passthrough":{"content_item_kinds":["environments.environment_context"], "create_time":1789293131.822}}, @@ -740,7 +742,7 @@ async fn replay_compatibility_should_remove_only_reasoning_status_on_both_transp } for websocket in [false, true] { let actual = capture_replay_compatibility_request(input.clone(), false, websocket).await; - // 比较序列化结果,同时保护未修改字段的顺序。 + // 比较序列化结果,同时保护未修改字段的顺序 assert_eq!(actual["input"].to_string(), expected.to_string()); } } @@ -2348,7 +2350,7 @@ async fn api_websocket_precheck_and_selection_share_the_snapshot_retry_budget() count <= 6, "both credential checks must share one retry budget" ); - // 第 1、4 次在传输预检冲突,第 3、6 次在最终候选校验冲突。 + // 第 1、4 次在传输预检冲突,第 3、6 次在最终候选校验冲突 if matches!(count, 1 | 3 | 4 | 6) { let account = store.account(id.as_str()).expect("account"); let observed_at = account @@ -2431,7 +2433,7 @@ async fn exhausted_account_pool_returns_usage_limit_before_http_or_websocket_net create_account(&store, id).await; exhaust_account_quota(&store, id).await; } - // 范围外的可用账号不能掩盖当前 Client Key 的额度耗尽。 + // 范围外的可用账号不能掩盖当前 Client Key 的额度耗尽 create_account(&store, "acct_outside_scope").await; let server = MockServer::start().await; let provider = provider_with_base_url(&store, server.uri()); @@ -2775,7 +2777,7 @@ async fn image_usage_should_preserve_unknown_fields_and_explicit_zero_counts() { #[tokio::test] async fn image_prices_should_use_modality_rates_and_precede_delivery() { - // First case is the real gpt-image-2 response verified on 2026-09-08. + // 首个用例使用 2026-09-08 核验的真实 gpt-image-2 响应 let cases = [ (18, 0, 229, None, 69_600_000_u128), (17, 1457, 1372, None, 529_010_000), @@ -3130,7 +3132,7 @@ async fn assert_cross_endpoint_affinity(thread_id: Option<&str>) { AccountWeight::new(100).expect("weight"), ); - // 官方 Search 正文的 id 与 Responses 的 session-id 是同一个根身份。 + // 官方 Search 正文的 id 与 Responses 的 session-id 是同一个根身份 let search = Operation::Search(StandaloneSearchRequest::from_raw_json( RawJsonPayload::new( "openai", @@ -3172,7 +3174,7 @@ async fn assert_cross_endpoint_affinity(thread_id: Option<&str>) { } drop(same); - // 仍由旧租约流程报告繁忙并换号,亲和不绕过并发限制。 + // 仍由旧租约流程报告繁忙并换号,亲和不绕过并发限制 leases .busy_accounts .lock() @@ -3194,7 +3196,7 @@ async fn assert_cross_endpoint_affinity(thread_id: Option<&str>) { .await .expect("first event") .expect("successful JSON response"); - drop(fallback); // 只消费一个事件也必须完成绑定迁移。 + drop(fallback); // 只消费一个事件也必须完成绑定迁移 leases.busy_accounts.lock().expect("busy accounts").clear(); store.set_scheduling( first_account.as_str(), @@ -3646,7 +3648,7 @@ async fn repeated_message_too_big_closes_keep_session_on_websocket() { assert!(!format!("{error:?}").contains("message too big")); assert!(!error.to_string().contains("message too big")); // 上游 close 1009 是 RFC 6455 "message too big":必须归因为请求自身问题, - // 而不是 provider 传输故障(否则会被熔断器和换号逻辑误伤其他请求)。 + // 而不是 provider 传输故障(否则会被熔断器和换号逻辑误伤其他请求) assert_eq!(error.kind(), ProviderErrorKind::MessageTooBig); assert_eq!(error.send_state(), UpstreamSendState::Ambiguous); assert!( @@ -3930,7 +3932,7 @@ async fn abrupt_websocket_disconnect_preserves_diagnosis_and_ambiguous_send_stat let (stream, _) = listener.accept().await.unwrap(); let mut websocket = accept_codex_test_websocket(stream).await; websocket.next().await.unwrap().unwrap(); - // The peer disappears after receiving the payload, without sending a Close frame. + // 对端收到载荷后直接断开,不发送 Close 帧 }); let operation = Operation::Generate(generate_with_persisted_session_context( ACCOUNT_ID, @@ -3983,7 +3985,7 @@ async fn websocket_midstream_error_frame_surfaces_upstream_message_after_deliver .await .expect("WebSocket request") .expect("valid WebSocket request"); - // 真实生产观察:OpenAI 在流已开始后发送带原话的 `error` 帧再断连。 + // 真实生产观察:OpenAI 在流已开始后发送带原话的 `error` 帧再断连 websocket .send(Message::Text( json!({ @@ -4024,7 +4026,7 @@ async fn websocket_midstream_error_frame_surfaces_upstream_message_after_deliver )) .await .expect("send error frame"); - // 不发送任何终止事件,直接断开,模拟上游发完错误帧后的真实行为。 + // 不发送任何终止事件,直接断开,模拟上游发完错误帧后的真实行为 }); let provider = provider_with_base_url(&store, base_url); @@ -4049,13 +4051,13 @@ async fn websocket_midstream_error_frame_surfaces_upstream_message_after_deliver ProviderErrorKind::UpstreamCapacityUnavailable ); assert_eq!(failure.send_state(), UpstreamSendState::Sent); - // 交给 Core 的失败必须保留上游原话:客户端只能靠它知道失败原因。 + // 交给 Core 的失败必须保留上游原话:客户端只能靠它知道失败原因 let visible = failure .client_visible_upstream_error() .expect("overload frame must carry a client-visible upstream error"); assert_eq!(visible.code(), Some("server_is_overloaded")); assert_eq!(visible.message(), OVERLOAD_MESSAGE); - // 原始错误帧必须随失败一起交给交付边界(SSE 侧据此翻译成 response.failed)。 + // 原始错误帧必须随失败一起交给交付边界(SSE 侧据此翻译成 response.failed) let atomic = failure.take_atomic_client_events(); let error_frame = atomic .iter() @@ -4192,7 +4194,7 @@ async fn websocket_pong_timeout_diagnosis_survives_ambiguous_send_wrapping() { } std::assert_matches!(websocket.next().await.unwrap().unwrap(), Message::Ping(_)); ping_seen_tx.send(()).unwrap(); - // 停止 poll,避免 tungstenite 自动回 Pong,复现本地保活超时。 + // 停止 poll,避免 tungstenite 自动回 Pong,复现本地保活超时 futures::future::pending::<()>().await; }); let operation = Operation::Generate(generate_with_persisted_session_context( @@ -5554,7 +5556,7 @@ async fn websocket_opening_account_rejection_keeps_replay_safe_without_transport let (mut opening, _) = listener.accept().await.unwrap(); let request = capture_http_request(&mut opening).await; assert!(String::from_utf8_lossy(&request).starts_with("GET /codex/responses")); - // 额度耗尽拒绝通常携带小时级的 retry-after;同账号重试注定再次命中。 + // 额度耗尽拒绝通常携带小时级的 retry-after;同账号重试注定再次命中 let body = r#"{"error":{"message":"You have reached your usage limit.","type":"rate_limit_error"}}"#; opening .write_all( @@ -5593,7 +5595,7 @@ async fn websocket_opening_account_rejection_keeps_replay_safe_without_transport "before-payload rejection is replay safe" ); // 回放安全的账号级拒绝必须把换号决策留给 Core,不得钉死同账号传输重试 - // (旧行为会携带小时级 retry-after 的同账号重试标记,请求必然超时)。 + // (旧行为会携带小时级 retry-after 的同账号重试标记,请求必然超时) assert_eq!(error.pre_delivery_retry(), None); } @@ -6188,7 +6190,7 @@ async fn websocket_account_scoping_preserves_ascii_turn_metadata_and_unicode_inp }).to_string().into())).await.expect("complete response"); body }); - // 官方 Codex 在工作区包含 Unicode 时也保持内嵌 turn metadata 为 ASCII。 + // 官方 Codex 在工作区包含 Unicode 时也保持内嵌 turn metadata 为 ASCII let raw = r#"{"installation_id":"client-installation","workspaces":{"C:\\Users\\\u9879\u76ee\\\ud83d\ude80":{"label":"caf\u00e9","literal":"\\u4e2d","quoted":"\"line\n"}}}"#; let input = json!([{"role": "user", "content": "中文正文 🚀"}]); let payload = ProtocolPayload::json_object( @@ -6663,7 +6665,7 @@ async fn account_selection_log_should_include_affinity_observation_fields() { .with_writer(captured.clone()) .finish(); // 该 integration test binary 没有其他 subscriber;全局安装可避免并行测试切换 - // thread-local dispatcher 时重建 tracing callsite interest 所产生的竞争。 + // thread-local dispatcher 时重建 tracing callsite interest 所产生的竞争 tracing::subscriber::set_global_default(subscriber) .expect("install affinity observation log subscriber"); @@ -7216,7 +7218,7 @@ async fn child_busy_failover_should_leave_the_running_parent_and_siblings_on_the ); drop(child); - // 租约层报告 A 已满;既有子线程和首次出现的子线程都能独立选择 B。 + // 租约层报告 A 已满;既有子线程和首次出现的子线程都能独立选择 B leases .busy_accounts .lock() @@ -7821,10 +7823,10 @@ async fn official_usage_limit_failure_persists_fact_without_fabricating_usage() .and(path("/api/codex/usage")) .and(header("authorization", format!("Bearer at-{account_id}"))) // 失败后的补查只更新观察时间,即使响应满足恢复条件, - // 也不能覆盖本次推理刚确认的耗尽;恢复由独立的主动刷新判断。 + // 也不能覆盖本次推理刚确认的耗尽;恢复由独立的主动刷新判断 .respond_with( ResponseTemplate::new(200) - // 验证后台 usage 同步不能把原始的额度错误响应拖到查询完成之后。 + // 验证后台 usage 同步不能把原始的额度错误响应拖到查询完成之后 .set_delay(Duration::from_millis(750)) .set_body_json(json!({ "rate_limit": { @@ -8137,7 +8139,7 @@ async fn capacity_feedback_only_counts_overload_rejections_and_excludes_diagnost .await; let (provider, _) = provider_with_capacity_tracking(&store, server.uri(), Arc::clone(&cooldowns)); - // 同时验证窗口证据已过期与尚未过期:探测不能重建峰值,也不能改写原计数。 + // 同时验证窗口证据已过期与尚未过期:探测不能重建峰值,也不能改写原计数 for existing_evidence in [None, Some((4, 20))] { if let Some((count, peak)) = existing_evidence { for _ in 0..count { @@ -8229,7 +8231,7 @@ async fn local_websocket_connection_cancellation_does_not_supply_capacity_eviden .expect("opening deadline") .expect("opening"); read_http_request(&mut opening).await; - // 复现账号更新驱逐正在建连的连接,未收到任何上游容量拒绝。 + // 复现账号更新驱逐正在建连的连接,未收到任何上游容量拒绝 pool.evict_account(account_id).await; let error = timeout(Duration::from_secs(5), attempt) .await @@ -8327,7 +8329,7 @@ async fn capacity_feedback_in_stream_only_counts_explicit_overload() { create_account(&store, "acct_provider_contract").await; let account = store.account("acct_provider_contract").expect("account"); let cooldowns = Arc::new(MemoryCooldownPort::new()); - // 距离冻结阈值只差一次,验证非容量错误不会把可调度账号推入冷却。 + // 距离冻结阈值只差一次,验证非容量错误不会把可调度账号推入冷却 for _ in 0..11 { cooldowns .record_capacity_failure(account.id(), Duration::from_secs(600), 20) @@ -10551,7 +10553,7 @@ async fn api_key_default_http_uses_own_prefix_plain_json_and_only_own_authentica provider_openai::credential::ResponsesTransport::Http, ) .await; - // 后台发现不协商客户端版本;客户端目录独立请求并按实际版本缓存。 + // 后台发现不协商客户端版本;客户端目录独立请求并按实际版本缓存 for query in [None, Some("client_version=1.0.0")] { Mock::given(method("GET")) .and(path(format!("{prefix}/models"))) @@ -11287,7 +11289,7 @@ async fn quota_continuation_full_client_replay_selects_another_account() { QuotaAccessState::Exhausted ); create_account(&store, "acct_affinity_switch_b").await; - // 客户端重建完整历史,保留同一会话标识;选号必须跳过刚刚耗尽的原账号。 + // 客户端重建完整历史,保留同一会话标识;选号必须跳过刚刚耗尽的原账号 let replay = Operation::Generate(GenerateRequest::from_protocol_payload( ProtocolPayload::json_object("openai", json!({"model":"gpt-5.4","session_id":"quota-replay","thread_id":"turn","input":full_input}).as_object().unwrap().clone()).unwrap(), )); @@ -11328,7 +11330,7 @@ async fn fast_policy_changes_preserve_the_websocket_continuation_and_meter_each_ let (socket, _) = listener.accept().await.unwrap(); let mut websocket = crate::transport::accept_codex_test_websocket_with(socket, |request, response| { - // 提示头只在首次握手发送;后续档位由各自 response.create 正文指定。 + // 提示头只在首次握手发送;后续档位由各自 response.create 正文指定 assert_eq!( request.headers()["x-codex-routing-hint"], "model=gpt-5.4;tier=priority" @@ -11393,7 +11395,7 @@ async fn fast_policy_changes_preserve_the_websocket_continuation_and_meter_each_ GenerateRequest::from_protocol_payload( ProtocolPayload::json_object("openai", body.as_object().unwrap().clone()) .unwrap() - // 与客户端 WebSocket 一致,首轮也禁止按快路径预算降级到 HTTP。 + // 与客户端 WebSocket 一致,首轮也禁止按快路径预算降级到 HTTP .with_context(Map::from_iter([ ("use_websocket".to_owned(), json!(true)), ( @@ -11838,7 +11840,7 @@ async fn stalled_proxy_connections_share_thirty_seconds_instead_of_resetting_tim .await .expect("proxy accepted the real socket") .unwrap(); - // 先确认真实连接已建立,再推进共享预算与 transport 的同一单调时钟。 + // 先确认真实连接已建立,再推进共享预算与 transport 的同一单调时钟 tokio::time::pause(); tokio::time::advance(Duration::from_secs(29)).await; execution.await.unwrap(); @@ -11851,7 +11853,7 @@ async fn stalled_proxy_connections_share_thirty_seconds_instead_of_resetting_tim pub(crate) async fn assert_local_connection_capacity_is_not_an_upstream_failure() { let store = Arc::new(MemoryAccountStore::default()); create_account(&store, "acct_provider_contract").await; - // 显式出口使 Provider 使用生产建连层,而不是测试注入的裸 reqwest client。 + // 显式出口使 Provider 使用生产建连层,而不是测试注入的裸 reqwest client store.set_egress( "acct_provider_contract", Some(gateway_core::account::OutboundProxy::parse("http://127.0.0.1:9").unwrap()), @@ -12062,8 +12064,8 @@ async fn capture_grok_request( .as_object() .expect("request object") .clone(); - // 每次测试创建不同账号,账号绑定的安装标识由独立身份合同覆盖。 - // 这里只比较下游兼容对业务正文的影响。 + // 每次测试创建不同账号,账号绑定的安装标识由独立身份合同覆盖 + // 这里只比较下游兼容对业务正文的影响 sent.remove("client_metadata"); sent } @@ -12195,7 +12197,7 @@ async fn public_catalog_filters_each_api_account_before_union_without_gating_inf let model = PublicModelId::new(id).unwrap(); assert!(!snapshot.contains_public_model_for_scope(&model, &scope)); } - // 目录来源只决定展示,发现型目录仍允许把请求交给政策合规的上游判断。 + // 目录来源只决定展示,发现型目录仍允许把请求交给政策合规的上游判断 for id in ["kimi-k2.5", "kimi-public", "not-yet-discovered"] { let model = PublicModelId::new(id).unwrap(); assert_eq!( diff --git a/backend/crates/providers/openai/tests/provider/contract/precommit.rs b/backend/crates/providers/openai/tests/provider/contract/precommit.rs index 7392b9137..b7c102a1d 100644 --- a/backend/crates/providers/openai/tests/provider/contract/precommit.rs +++ b/backend/crates/providers/openai/tests/provider/contract/precommit.rs @@ -1,3 +1,5 @@ +//! 验证结构事件缓冲、交付宽限与取消对重试边界的影响 + use gateway_core::diagnostics::TraceContext; use gateway_core::engine::provider::ProviderStream; use gateway_core::error::ProviderError; @@ -82,7 +84,7 @@ async fn large_structural_events_preserve_overload_replay_past_the_old_grace_on_ for websocket in [false, true] { let store = Arc::new(MemoryAccountStore::default()); create_account(&store, "acct_provider_contract").await; - // #259 仅保留了事件长度;用合成配置回显复现尺寸,不依赖现场私有正文。 + // #259 仅保留了事件长度;用合成配置回显复现尺寸,不依赖现场私有正文 let created = structural_event("response.created", 38_781); let progress = structural_event("response.in_progress", 38_785); let failure = overload(); @@ -238,7 +240,7 @@ async fn immediate_release_preserves_wire_and_records_the_boundary_once() { create_account(&store, "acct_provider_contract").await; let (base_url, release, _, server) = paused_chunked_sse_server(body.clone(), String::new()).await; - // EOF 是另一种边界;其余场景必须在上游结束前立即释放。 + // EOF 是另一种边界;其余场景必须在上游结束前立即释放 let mut release = Some(release); if reason == "eof" { release.take().unwrap().send(()).unwrap(); diff --git a/backend/crates/providers/openai/tests/provider/contract/response_interrupt.rs b/backend/crates/providers/openai/tests/provider/contract/response_interrupt.rs index 306edd422..f3923ede6 100644 --- a/backend/crates/providers/openai/tests/provider/contract/response_interrupt.rs +++ b/backend/crates/providers/openai/tests/provider/contract/response_interrupt.rs @@ -1,3 +1,5 @@ +//! 验证响应中断使用当前连接并在复用前释放控制权 + use super::*; use gateway_core::engine::response_control::{ResponseControl, ResponseInterruptError}; diff --git a/backend/crates/providers/openai/tests/provider/contract/upstream_adapter.rs b/backend/crates/providers/openai/tests/provider/contract/upstream_adapter.rs index 895dac33a..c88c2d1a8 100644 --- a/backend/crates/providers/openai/tests/provider/contract/upstream_adapter.rs +++ b/backend/crates/providers/openai/tests/provider/contract/upstream_adapter.rs @@ -1,3 +1,5 @@ +//! 验证插件上游适配复用原生账号、保留槽位与凭据更新 + use super::*; use gateway_core::account::AccountRuntimeSignals; use gateway_core::engine::upstream_adapter::{ @@ -260,7 +262,7 @@ async fn selected_adapter_connection( #[tokio::test] async fn guardian_reservation_survives_upstream_adapters_and_metadata_precedence() { - // 上限 2、已有 1 个在途请求:预留启用时只有 Guardian 能继续取得租约。 + // 上限 2、已有 1 个在途请求:预留启用时只有 Guardian 能继续取得租约 for (subagent, turn_kind, reserved, allowed) in [ (Some("guardian"), None, 1, true), (None, Some("guardian"), 1, true), @@ -377,7 +379,7 @@ async fn upstream_adapter_reloads_rotated_credentials_and_proxy_without_acceptin let before = store.account(account_id).unwrap(); assert_eq!(old.outbound_proxy(), Some(&old_proxy)); - // 使用原生刷新成功后的 CAS 入口;适配器不能复制令牌或缓存第二份账号代理。 + // 使用原生刷新成功后的 CAS 入口;适配器不能复制令牌或缓存第二份账号代理 let revision = store .repository() .rotate_refreshed_oauth_secret( @@ -418,7 +420,7 @@ async fn upstream_adapter_reloads_rotated_credentials_and_proxy_without_acceptin ); } - // 旧请求的 401 不能使已轮换凭据失效,未发送失败也不能污染账号状态。 + // 旧请求的 401 不能使已轮换凭据失效,未发送失败也不能污染账号状态 for (connection, send_state) in [ (&old, UpstreamSendState::Sent), (¤t, UpstreamSendState::NotSent), diff --git a/backend/crates/providers/openai/tests/provider/failure.rs b/backend/crates/providers/openai/tests/provider/failure.rs index 75b0691f5..3e6b522d0 100644 --- a/backend/crates/providers/openai/tests/provider/failure.rs +++ b/backend/crates/providers/openai/tests/provider/failure.rs @@ -1,3 +1,5 @@ +//! 验证 OpenAI 上游失败分类与账号评分反馈规则 + use std::num::NonZeroU32; use std::sync::Arc; use std::time::Duration; diff --git a/backend/crates/providers/openai/tests/provider/mod.rs b/backend/crates/providers/openai/tests/provider/mod.rs index 321fdeb5d..67bf9158f 100644 --- a/backend/crates/providers/openai/tests/provider/mod.rs +++ b/backend/crates/providers/openai/tests/provider/mod.rs @@ -1,3 +1,5 @@ +//! OpenAI Provider 执行合同与失败处理的测试入口 + mod contract; mod failure; diff --git a/backend/crates/providers/openai/tests/support.rs b/backend/crates/providers/openai/tests/support.rs index 4601576fa..0eaa99a4a 100644 --- a/backend/crates/providers/openai/tests/support.rs +++ b/backend/crates/providers/openai/tests/support.rs @@ -1,4 +1,4 @@ -//! Codex Provider 测试用内存 ports;不依赖 SQL、Redis 或 secret 加密。 +//! Codex Provider 测试用内存 ports;不依赖 SQL、Redis 或 secret 加密 use std::collections::{BTreeMap, BTreeSet}; use std::num::NonZeroU32; @@ -261,7 +261,7 @@ impl ProviderAccountStore for MemoryAccountStore { if self.fail_provider_listing.load(Ordering::SeqCst) { return Err(store_error(StoreErrorKind::Unavailable)); } - // 与 Postgres 实现的调度列表语义一致:停用账号不进入常规候选。 + // 与 Postgres 实现的调度列表语义一致:停用账号不进入常规候选 Ok(self .accounts .lock() @@ -308,7 +308,7 @@ impl ProviderAccountStore for MemoryAccountStore { .lock() .expect("credential hook lock") .clone(); - // 在列表快照与凭据读取之间确定性提交并发变更,不依赖线程调度或延时。 + // 在列表快照与凭据读取之间确定性提交并发变更,不依赖线程调度或延时 if let Some(hook) = hook { hook(self, account, count).await?; } @@ -1022,11 +1022,11 @@ pub(crate) fn runtime_policy() -> Arc { Arc::new(StaticRuntimePolicy) } -/// 冻结策略可配置的运行时策略 fake;容量熔断触发测试用。 +/// 冻结策略可配置的运行时策略 fake;容量熔断触发测试用 pub(crate) struct StaticFreezePolicy(ProviderFreezePolicy); impl StaticFreezePolicy { - /// 构造带固定冻结策略的端口句柄;命名沿用测试构造器语义。 + /// 构造带固定冻结策略的端口句柄;命名沿用测试构造器语义 #[must_use] pub(crate) fn policy_port(policy: ProviderFreezePolicy) -> Arc { Arc::new(Self(policy)) @@ -1070,7 +1070,7 @@ pub(crate) fn account_policy() -> gateway_core::account::AccountSelectionPolicy /// 内存 `ProviderCooldownPort`:实现 `read`/`put_if_later` 与容量失败计数 /// 证据(openai selector 429 冷却与容量熔断路径用),scope 变体测试不涉及, -/// 返回占位。 +/// 返回占位 #[derive(Clone, Default)] pub(crate) struct MemoryCooldownPort { pub(crate) cooldowns: Arc>>, @@ -1083,7 +1083,7 @@ impl MemoryCooldownPort { Self::default() } - /// 测试断言用:窗口内累计失败次数与在途峰值。 + /// 测试断言用:窗口内累计失败次数与在途峰值 #[must_use] pub(crate) fn capacity_evidence(&self, account_id: &ProviderAccountId) -> Option<(u32, u32)> { self.capacity_failures diff --git a/backend/crates/providers/openai/tests/transport/account_proxy.rs b/backend/crates/providers/openai/tests/transport/account_proxy.rs index 3de0e9612..6dba37684 100644 --- a/backend/crates/providers/openai/tests/transport/account_proxy.rs +++ b/backend/crates/providers/openai/tests/transport/account_proxy.rs @@ -1,3 +1,5 @@ +//! 验证账号代理的协议支持、连接隔离与失败后禁止直连回退 + use super::*; use gateway_core::{ account::{CredentialRevision, OutboundProxy, ProviderAccount, ProviderAccountId}, @@ -223,7 +225,7 @@ async fn socks_account_egress_preserves_dns_mode_auth_and_ipv6_for_sse_and_webso ("socks5h", "upstream.invalid"), ("socks5h", "[::1]"), ]; - // reqwest's local resolver does not accept IPv6 literals; use socks5h for those. + // reqwest 本地解析器不接受 IPv6 字面量,此场景使用 socks5h if websocket { targets.push(("socks5", "[::1]")); } @@ -341,7 +343,7 @@ async fn https_account_proxy_starts_tls_and_rejection_never_falls_back_to_direct } } -// 以底层写入调用为边界模拟问题代理,避免将 TCP 单次读取误当成报文边界。 +// 以底层写入调用为边界模拟问题代理,避免将 TCP 单次读取误当成报文边界 struct GreetingSensitiveProxy { greeting: Vec, replies: Cursor>, @@ -376,7 +378,7 @@ impl AsyncRead for GreetingSensitiveProxy { "proxy rejected partial method negotiation", ))); } - // 回复逐字节交付,验证客户端不依赖响应的读取边界。 + // 回复逐字节交付,验证客户端不依赖响应的读取边界 if buf.remaining() > 0 { let mut byte = [0]; let count = Read::read(&mut self.replies, &mut byte)?; @@ -410,7 +412,7 @@ async fn socks_proxy_flush_contract_coalesces_greeting_without_losing_auth_or_co use tokio_tungstenite::proxy::connect_via_proxy; use tungstenite::proxy::ProxyConfig; - // 直接验证锁定依赖与握手写缓冲的合同;生产接线由已有账号代理集成测试覆盖。 + // 直接验证锁定依赖与握手写缓冲的合同;生产接线由已有账号代理集成测试覆盖 for scheme in ["socks5", "socks5h"] { for authenticated in [false, true] { let authentication = if authenticated { "user:pass@" } else { "" }; diff --git a/backend/crates/providers/openai/tests/transport/canonical.rs b/backend/crates/providers/openai/tests/transport/canonical.rs index 9014a2f4f..8594375b1 100644 --- a/backend/crates/providers/openai/tests/transport/canonical.rs +++ b/backend/crates/providers/openai/tests/transport/canonical.rs @@ -1,3 +1,5 @@ +//! 验证 Codex SSE 转换保留协议事件并提取模型和用量事实 + use gateway_core::event::{ContentKind, FinishReason, GatewayEvent, ProviderEvent}; use provider_openai::transport::canonical::{ @@ -154,8 +156,8 @@ fn raw_sse_passthrough_should_keep_original_bytes_alongside_canonical_facts() { #[test] fn websocket_raw_passthrough_should_preserve_upstream_number_bytes() { // OpenAI 线路透明代理:WebSocket 上游帧经 reducer→SSE→decoder(raw 透传) 后, - // data 段必须与上游原文逐字节一致,不得经 serde 往返改写数值/精度。 - // `1e3` 若被重序列化会变成 `1000.0`——用它作为字节改写的探针。 + // data 段必须与上游原文逐字节一致,不得经 serde 往返改写数值/精度 + // `1e3` 若被重序列化会变成 `1000.0`——用它作为字节改写的探针 let upstream = r#"{"type":"response.created","response":{"id":"resp_ws_raw","model":"gpt-test","x_precision":1e3}}"#; let frame = websocket_event_to_sse_frame(upstream) .expect("client-visible WS event yields an SSE frame"); diff --git a/backend/crates/providers/openai/tests/transport/catalog.rs b/backend/crates/providers/openai/tests/transport/catalog.rs index a3e1804a8..e15d630e9 100644 --- a/backend/crates/providers/openai/tests/transport/catalog.rs +++ b/backend/crates/providers/openai/tests/transport/catalog.rs @@ -1,3 +1,5 @@ +//! 验证原生模型目录保留未知字段并生成安全能力投影 + use chrono::{TimeZone, Utc}; use gateway_core::routing::ModelServiceTier; use provider_openai::transport::profile::{CodexWireProfile, CodexWireProfileState}; diff --git a/backend/crates/providers/openai/tests/transport/client.rs b/backend/crates/providers/openai/tests/transport/client.rs index efe3038f9..2d89298fb 100644 --- a/backend/crates/providers/openai/tests/transport/client.rs +++ b/backend/crates/providers/openai/tests/transport/client.rs @@ -1,3 +1,5 @@ +//! 验证 OpenAI 客户端请求上下文与上游错误的诊断脱敏 + use provider_openai::transport::CodexUpstreamSendPhase; use provider_openai::transport::{CodexBackendTransport, CodexClientError, CodexRequestContext}; diff --git a/backend/crates/providers/openai/tests/transport/connection.rs b/backend/crates/providers/openai/tests/transport/connection.rs index 5445ad0b9..5fd8f7edb 100644 --- a/backend/crates/providers/openai/tests/transport/connection.rs +++ b/backend/crates/providers/openai/tests/transport/connection.rs @@ -1,4 +1,4 @@ -//! 在独立测试进程中验证共享建连准入,避免其他网络测试占用全局名额。 +//! 在独立测试进程中验证共享建连准入,避免其他网络测试占用全局名额 use super::*; use gateway_core::account::OutboundProxy; @@ -40,7 +40,7 @@ async fn cold_connection_admission_bounds_queue_and_releases_cancelled_work() { .0, ); } - // 128 条 CONNECT 握手保持挂起;1024 个等待者之外的一次请求应立即拒绝。 + // 128 条 CONNECT 握手保持挂起;1024 个等待者之外的一次请求应立即拒绝 for _ in 0..1025 { tasks.push(spawn(client.clone(), completed.clone())); } @@ -121,7 +121,7 @@ async fn cold_connection_admission_bounds_queue_and_releases_cancelled_work() { for task in tasks { let _ = task.await; } - // 同时取消活动与排队请求后,新请求仍能取得名额。 + // 同时取消活动与排队请求后,新请求仍能取得名额 let fresh = spawn(client, completed); let _socket = timeout(Duration::from_secs(3), listener.accept()) .await diff --git a/backend/crates/providers/openai/tests/transport/diagnostics.rs b/backend/crates/providers/openai/tests/transport/diagnostics.rs index c7c780a78..503a76f4f 100644 --- a/backend/crates/providers/openai/tests/transport/diagnostics.rs +++ b/backend/crates/providers/openai/tests/transport/diagnostics.rs @@ -1,3 +1,5 @@ +//! 验证上游诊断的容量错误分类、请求标识与响应事实提取 + use base64::{Engine as _, engine::general_purpose::STANDARD}; use provider_openai::transport::CodexUpstreamDiagnostics; use provider_openai::transport::diagnostics::{ diff --git a/backend/crates/providers/openai/tests/transport/downstream/body.rs b/backend/crates/providers/openai/tests/transport/downstream/body.rs index 022fcc09c..0865d240b 100644 --- a/backend/crates/providers/openai/tests/transport/downstream/body.rs +++ b/backend/crates/providers/openai/tests/transport/downstream/body.rs @@ -1,3 +1,5 @@ +//! 验证下游请求兼容转换保留显式值与未知字段 + use gateway_core::operation::{GenerateRequest, ProtocolPayload}; use provider_openai::encode_generate_request; @@ -6,7 +8,7 @@ use super::super::*; #[test] fn encoder_should_adapt_pi_responses_parameters_without_losing_codex_fields() { // 对照本机 Pi 0.79.0 普通 Responses 适配在 onPayload 阶段生成的正文; - // maxTokens、temperature 和长缓存选项最终会产生下面三个顶层字段。 + // maxTokens、temperature 和长缓存选项最终会产生下面三个顶层字段 let body = json!({ "model": "client-model", "input": [{"role": "user", "content": [{"type": "input_text", "text": "hello"}]}], @@ -46,7 +48,7 @@ fn encoder_should_adapt_pi_responses_parameters_without_losing_codex_fields() { #[test] fn encoder_should_expand_string_input_into_user_message_item() { // 公开 Responses API 的字符串 input 等价于一条 user 文本消息; - // 目标形状对照 codex-rs/protocol/src/models.rs 的 ResponseItem::Message + ContentItem::InputText。 + // 目标形状对照 codex-rs/protocol/src/models.rs 的 ResponseItem::Message + ContentItem::InputText let encoded = encode_downstream_request(json!({"model": "gpt-test", "input": "hello"})); assert_eq!( @@ -66,8 +68,8 @@ fn encoder_should_expand_string_input_into_user_message_item() { #[test] fn encoder_should_keep_non_string_input_shapes_untouched() { - // 数组按原样透传;空串仍展开为空文本消息,语义由上游判定。 - // null、对象等既不是官方形状也没有公开 API 的等价定义,不猜测语义。 + // 数组按原样透传;空串仍展开为空文本消息,语义由上游判定 + // null、对象等既不是官方形状也没有公开 API 的等价定义,不猜测语义 let items = json!([{"type": "message", "role": "user", "content": [ {"type": "input_text", "text": "hi"}, {"type": "input_image", "image_url": "data:image/png;base64,AA=="} diff --git a/backend/crates/providers/openai/tests/transport/downstream/headers.rs b/backend/crates/providers/openai/tests/transport/downstream/headers.rs index 8c1087ae5..e77ed4dbb 100644 --- a/backend/crates/providers/openai/tests/transport/downstream/headers.rs +++ b/backend/crates/providers/openai/tests/transport/downstream/headers.rs @@ -1,3 +1,5 @@ +//! 验证 HTTP 与 WebSocket 保留业务头部并过滤下游传输头 + use base64::{Engine as _, engine::general_purpose::STANDARD}; use gateway_core::operation::{GenerateRequest, ProtocolPayload}; use provider_openai::encode_generate_request; @@ -194,7 +196,7 @@ async fn backend_http_should_preserve_business_headers_without_downstream_transp write_completed_sse_response(&mut stream).await; request }); - // 不经 API 解码,直接构造协议上下文,验证 Provider 自身的过滤边界。 + // 不经 API 解码,直接构造协议上下文,验证 Provider 自身的过滤边界 let request = request_with_opaque_headers(false); let profile = test_wire_profile(); let profile_snapshot = profile.snapshot(); @@ -231,7 +233,7 @@ async fn backend_http_should_preserve_business_headers_without_downstream_transp assert!(raw_header_values(&raw, name).is_empty(), "leaked {name}"); } for &(name, _) in DOWNSTREAM_TRANSPORT_HEADERS { - // HTTP 正文由 transport 重编码为 zstd,不能沿用下游 Content-Encoding。 + // HTTP 正文由 transport 重编码为 zstd,不能沿用下游 Content-Encoding if name == "content-encoding" { assert_eq!(raw_header_values(&raw, name), vec![b"zstd".to_vec()]); } else { @@ -264,7 +266,7 @@ async fn backend_http_should_preserve_business_headers_without_downstream_transp ] { assert_eq!(raw_header_values(&raw, name), vec![value.to_vec()]); } - // 指纹头由运行时画像生成,不透传客户端值。 + // 指纹头由运行时画像生成,不透传客户端值 assert_eq!( raw_header_values(&raw, "user-agent"), vec![expected_user_agent.as_bytes().to_vec()] diff --git a/backend/crates/providers/openai/tests/transport/downstream/mod.rs b/backend/crates/providers/openai/tests/transport/downstream/mod.rs index 8f7c24fba..197c220fd 100644 --- a/backend/crates/providers/openai/tests/transport/downstream/mod.rs +++ b/backend/crates/providers/openai/tests/transport/downstream/mod.rs @@ -1,2 +1,4 @@ +//! 下游客户端正文与头部兼容处理的测试入口 + mod body; mod headers; diff --git a/backend/crates/providers/openai/tests/transport/endpoints.rs b/backend/crates/providers/openai/tests/transport/endpoints.rs index 5e9541985..8c3000c0a 100644 --- a/backend/crates/providers/openai/tests/transport/endpoints.rs +++ b/backend/crates/providers/openai/tests/transport/endpoints.rs @@ -1,3 +1,5 @@ +//! 验证 Codex 上游地址拼接与额度端点路径 + use provider_openai::transport::{endpoint_url, usage_endpoint_url}; #[test] diff --git a/backend/crates/providers/openai/tests/transport/headers.rs b/backend/crates/providers/openai/tests/transport/headers.rs index 2e1f0f98b..4f1e2eb5a 100644 --- a/backend/crates/providers/openai/tests/transport/headers.rs +++ b/backend/crates/providers/openai/tests/transport/headers.rs @@ -1,3 +1,5 @@ +//! 验证 Codex HTTP 与 WebSocket 的请求身份、头部顺序与脱敏 + use provider_openai::transport::websocket::CodexWebSocketConnection; use serde_json::Value; diff --git a/backend/crates/providers/openai/tests/transport/http_client.rs b/backend/crates/providers/openai/tests/transport/http_client.rs index d7f0b14b2..f1b64917d 100644 --- a/backend/crates/providers/openai/tests/transport/http_client.rs +++ b/backend/crates/providers/openai/tests/transport/http_client.rs @@ -1,3 +1,5 @@ +//! 验证 OpenAI HTTP 客户端的 CA、错误响应、SSE 与超时处理 + use super::*; #[test] @@ -76,7 +78,7 @@ fn custom_ca_should_report_environment_cache_key_consistently() { String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); - // libtest 匹配零个测试也返回成功;完成标记证明该环境分支执行了生产断言。 + // libtest 匹配零个测试也返回成功;完成标记证明该环境分支执行了生产断言 let stdout = String::from_utf8_lossy(&output.stdout); let completed = format!("{CASE_COMPLETED}{case}"); assert_eq!( @@ -547,8 +549,8 @@ async fn build_reqwest_client_should_reuse_cached_connection_pool() { }); let url = format!("http://{addr}/reuse"); - // 前后分别取得生产缓存中的客户端,避免只验证单个 reqwest client 自身能复用连接。 - // 本地服务使用 HTTP/1.1;该断言不代表已验证 HTTP/2 的 idle ping 或保活间隔。 + // 前后分别取得生产缓存中的客户端,避免只验证单个 reqwest client 自身能复用连接 + // 本地服务使用 HTTP/1.1;该断言不代表已验证 HTTP/2 的 idle ping 或保活间隔 let client = provider_openai::transport::build_reqwest_client().unwrap(); client.get(&url).send().await.unwrap().text().await.unwrap(); let client = provider_openai::transport::build_reqwest_client().unwrap(); diff --git a/backend/crates/providers/openai/tests/transport/latency.rs b/backend/crates/providers/openai/tests/transport/latency.rs index 86f87a41f..5e9b163b4 100644 --- a/backend/crates/providers/openai/tests/transport/latency.rs +++ b/backend/crates/providers/openai/tests/transport/latency.rs @@ -1,3 +1,5 @@ +//! 验证 WebSocket 冷建连时限、后台完成与续接请求的传输选择 + use provider_openai::transport::{ protocol::responses::{CodexResponsesRequest, PreviousResponseScope}, websocket::{ diff --git a/backend/crates/providers/openai/tests/transport/mod.rs b/backend/crates/providers/openai/tests/transport/mod.rs index 08e6e5cac..c77f18dcb 100644 --- a/backend/crates/providers/openai/tests/transport/mod.rs +++ b/backend/crates/providers/openai/tests/transport/mod.rs @@ -1,3 +1,5 @@ +//! OpenAI 上游 HTTP、SSE、WebSocket 与客户端画像的测试入口 + use std::{ process::Command, sync::{ diff --git a/backend/crates/providers/openai/tests/transport/profile/cli_release.rs b/backend/crates/providers/openai/tests/transport/profile/cli_release.rs index 3203e0d2b..9cf7fbcb8 100644 --- a/backend/crates/providers/openai/tests/transport/profile/cli_release.rs +++ b/backend/crates/providers/openai/tests/transport/profile/cli_release.rs @@ -1,3 +1,5 @@ +//! 验证 CLI 发行信息的平台依赖一致性与请求画像同步更新 + use provider_openai::transport::profile::cli_release::parse_cli_release; use serde_json::json; @@ -75,7 +77,7 @@ async fn official_cli_cache_updates_entry_headers_together_and_preserves_frozen_ )) .unwrap(); - // 复用官方版本服务的恢复入口,模拟每日检查已核验并写入的下一份发布资料。 + // 复用官方版本服务的恢复入口,模拟每日检查已核验并写入的下一份发布资料 cache .replace_if_newer( ProviderArtifactProfile::new( diff --git a/backend/crates/providers/openai/tests/transport/profile/desktop_artifact.rs b/backend/crates/providers/openai/tests/transport/profile/desktop_artifact.rs index 71dbaf9d3..dc3de473a 100644 --- a/backend/crates/providers/openai/tests/transport/profile/desktop_artifact.rs +++ b/backend/crates/providers/openai/tests/transport/profile/desktop_artifact.rs @@ -1,3 +1,5 @@ +//! 验证 Desktop 制品内嵌 Core 版本扫描与 ZIP 目录约束 + use provider_openai::transport::profile::desktop_artifact::{ CodexDesktopArtifactError, CoreVersionScanner, find_core_entry, parse_content_range, }; diff --git a/backend/crates/providers/openai/tests/transport/profile/identity.rs b/backend/crates/providers/openai/tests/transport/profile/identity.rs index 4d734dd25..50891b5af 100644 --- a/backend/crates/providers/openai/tests/transport/profile/identity.rs +++ b/backend/crates/providers/openai/tests/transport/profile/identity.rs @@ -1,3 +1,5 @@ +//! 验证自定义客户端身份、配套头部与注入防护 + use gateway_core::account::OpaqueProviderData; use provider_openai::transport::headers::build_codex_model_headers; use provider_openai::transport::profile::identity::RequestProfileSelection; diff --git a/backend/crates/providers/openai/tests/transport/profile/mod.rs b/backend/crates/providers/openai/tests/transport/profile/mod.rs index 25da1c85b..58cf5408f 100644 --- a/backend/crates/providers/openai/tests/transport/profile/mod.rs +++ b/backend/crates/providers/openai/tests/transport/profile/mod.rs @@ -1,3 +1,5 @@ +//! 客户端画像测试入口,以及 Desktop 发行解析和原子更新测试 + use std::collections::VecDeque; use std::sync::{Arc, Mutex}; use std::time::Duration; diff --git a/backend/crates/providers/openai/tests/transport/profile/platform_release/artifact.rs b/backend/crates/providers/openai/tests/transport/profile/platform_release/artifact.rs index 5f76feca0..1e4f16be8 100644 --- a/backend/crates/providers/openai/tests/transport/profile/platform_release/artifact.rs +++ b/backend/crates/providers/openai/tests/transport/profile/platform_release/artifact.rs @@ -1,3 +1,5 @@ +//! 验证 Windows 与 Linux 制品的版本、架构和归档完整性 + use super::xz::compress as xz; use provider_openai::transport::profile::platform_release::{ artifact::{read_linux, read_windows}, diff --git a/backend/crates/providers/openai/tests/transport/profile/platform_release/mod.rs b/backend/crates/providers/openai/tests/transport/profile/platform_release/mod.rs index 6d81326aa..866f22f18 100644 --- a/backend/crates/providers/openai/tests/transport/profile/platform_release/mod.rs +++ b/backend/crates/providers/openai/tests/transport/profile/platform_release/mod.rs @@ -1,3 +1,5 @@ +//! 验证各平台发行缓存的独立更新、失败保留与回退拒绝 + use futures::future::BoxFuture; use gateway_core::provider_ports::{ ProviderArtifactProfile, ProviderArtifactProfileCachePort, ProviderStoreError, diff --git a/backend/crates/providers/openai/tests/transport/profile/platform_release/range.rs b/backend/crates/providers/openai/tests/transport/profile/platform_release/range.rs index d92ae3987..b2ca56d55 100644 --- a/backend/crates/providers/openai/tests/transport/profile/platform_release/range.rs +++ b/backend/crates/providers/openai/tests/transport/profile/platform_release/range.rs @@ -1,3 +1,5 @@ +//! 验证 HTTP Range 的 ETag、精确范围与取消传播 + use provider_openai::transport::profile::platform_release::range::RemoteFile; use std::io::{self, Read}; use std::sync::Arc; @@ -60,7 +62,7 @@ async fn cancelled_reads_should_terminate_standard_read_helpers() { cancellation.cancel(); tokio::task::spawn_blocking(move || { - // 先验证单次 read,回归时直接失败,避免 read_exact 的自动重试挂住测试进程。 + // 先验证单次 read,回归时直接失败,避免 read_exact 的自动重试挂住测试进程 assert_eq!( reader.read(&mut [0]).unwrap_err().kind(), io::ErrorKind::Other, diff --git a/backend/crates/providers/openai/tests/transport/profile/platform_release/xz.rs b/backend/crates/providers/openai/tests/transport/profile/platform_release/xz.rs index 078389df0..7280ece44 100644 --- a/backend/crates/providers/openai/tests/transport/profile/platform_release/xz.rs +++ b/backend/crates/providers/openai/tests/transport/profile/platform_release/xz.rs @@ -1,3 +1,5 @@ +//! 验证 XZ 索引读取的定位、跨块访问与无关块跳过 + use provider_openai::transport::profile::platform_release::xz::IndexedXz; use std::io::{Cursor, Read, Seek, SeekFrom, Write}; @@ -48,7 +50,7 @@ fn indexed_xz_reads_across_blocks_and_can_skip_a_corrupt_unused_block() { let mut actual = [0; 40]; reader.read_exact(&mut actual).unwrap(); assert_eq!(actual, bytes[49_990..50_030]); - // 未访问的块不会下载、解压;访问它时仍须拒绝校验和错误。 + // 未访问的块不会下载、解压;访问它时仍须拒绝校验和错误 compressed[first_block_end - 1] ^= 1; let mut reader = IndexedXz::new(Cursor::new(&compressed), 0, compressed.len() as u64).unwrap(); reader.seek(SeekFrom::Start(80_000)).unwrap(); diff --git a/backend/crates/providers/openai/tests/transport/profile/selection.rs b/backend/crates/providers/openai/tests/transport/profile/selection.rs index a22f6b841..5feb6dc3c 100644 --- a/backend/crates/providers/openai/tests/transport/profile/selection.rs +++ b/backend/crates/providers/openai/tests/transport/profile/selection.rs @@ -1,3 +1,5 @@ +//! 验证客户端画像预设、版本模式与请求冻结行为 + use gateway_core::account::OpaqueProviderData; use provider_openai::transport::profile::selection::{ CliEntry, ClientKind, ClientPlatform, ClientProfileSelection, VersionMode, diff --git a/backend/crates/providers/openai/tests/transport/profile_avatar.rs b/backend/crates/providers/openai/tests/transport/profile_avatar.rs index fecb263d4..c96e75b66 100644 --- a/backend/crates/providers/openai/tests/transport/profile_avatar.rs +++ b/backend/crates/providers/openai/tests/transport/profile_avatar.rs @@ -1,3 +1,5 @@ +//! 验证头像请求的官方来源限制、认证边界与流式交付 + use futures::TryStreamExt as _; use provider_openai::transport::{ CodexProfileAvatarFetchError, CodexRequestContext, fetch_profile_avatar, diff --git a/backend/crates/providers/openai/tests/transport/profile_contract.rs b/backend/crates/providers/openai/tests/transport/profile_contract.rs index 1db4e61e9..86491355a 100644 --- a/backend/crates/providers/openai/tests/transport/profile_contract.rs +++ b/backend/crates/providers/openai/tests/transport/profile_contract.rs @@ -1,11 +1,13 @@ +//! 验证各端点共用当前客户端画像并保留显式驻留配置 + use super::*; use provider_openai::transport::build_reqwest_client; use provider_openai::transport::profile::CodexResidency; use uuid::Uuid; use wiremock::{Mock, MockServer, ResponseTemplate, matchers::any}; -/// 官方本机样本:模型请求带 Core version,backend-client 账号请求不带。 -/// 通过真实 HTTP 请求验证画像更新后每条路径取到同一制品中的正确字段。 +/// 官方本机样本:模型请求带 Core version,backend-client 账号请求不带 +/// 通过真实 HTTP 请求验证画像更新后每条路径取到同一制品中的正确字段 #[tokio::test] async fn each_core_endpoint_uses_the_current_bundled_release() { let server = MockServer::start().await; diff --git a/backend/crates/providers/openai/tests/transport/protocol/mod.rs b/backend/crates/providers/openai/tests/transport/protocol/mod.rs index 3efe1b519..b86ddb390 100644 --- a/backend/crates/providers/openai/tests/transport/protocol/mod.rs +++ b/backend/crates/providers/openai/tests/transport/protocol/mod.rs @@ -1,2 +1,4 @@ +//! Codex Responses 与 WebSocket 线协议的测试入口 + mod responses; mod websocket; diff --git a/backend/crates/providers/openai/tests/transport/protocol/responses.rs b/backend/crates/providers/openai/tests/transport/protocol/responses.rs index 0db365f1b..3fe7f1bb6 100644 --- a/backend/crates/providers/openai/tests/transport/protocol/responses.rs +++ b/backend/crates/providers/openai/tests/transport/protocol/responses.rs @@ -1,3 +1,5 @@ +//! 验证 Responses 语义输出信号、传输要求与流式错误解析 + use gateway_protocol::openai::sse::parse_sse_events; use provider_openai::transport::protocol::responses::{ CodexResponsesRequest, PreviousResponseScope, ResponsesSseFailure, TransportRequirement, @@ -110,7 +112,7 @@ fn output_start_should_ignore_preamble_and_failure_frames() { #[test] fn output_start_should_count_structural_and_semantic_frames() { for body in [ - // 结构帧无内容也算。 + // 结构帧无内容也算 b"event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"type\":\"message\"}}\n\n".as_slice(), b"event: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"part\":{\"type\":\"output_text\"}}\n\n", b"event: response.output_text.delta\ndata: {\"delta\":\"hello\"}\n\n", diff --git a/backend/crates/providers/openai/tests/transport/protocol/websocket.rs b/backend/crates/providers/openai/tests/transport/protocol/websocket.rs index f40a95003..289b348d6 100644 --- a/backend/crates/providers/openai/tests/transport/protocol/websocket.rs +++ b/backend/crates/providers/openai/tests/transport/protocol/websocket.rs @@ -1,3 +1,5 @@ +//! 验证 WebSocket 请求编码、事件处理与载荷审计脱敏 + use provider_openai::transport::protocol::responses::CodexResponsesRequest; use provider_openai::transport::protocol::websocket::{ OpeningAuditSnapshot, websocket_audit_artifact_from_attempt, websocket_event_to_sse_frame, @@ -92,7 +94,7 @@ fn websocket_response_create_payload_should_keep_explicit_empty_instructions() { #[test] fn websocket_response_create_payload_text_should_match_merged_map_serialization() { - // body 自带 type 键与未知字段:文本帧必须与「合并 Map 再序列化」逐字节一致。 + // body 自带 type 键与未知字段:文本帧必须与「合并 Map 再序列化」逐字节一致 let request = CodexResponsesRequest::from_body( json!({ "model": "gpt-test", diff --git a/backend/crates/providers/openai/tests/transport/request.rs b/backend/crates/providers/openai/tests/transport/request.rs index 694fcbf42..6acb7e8b7 100644 --- a/backend/crates/providers/openai/tests/transport/request.rs +++ b/backend/crates/providers/openai/tests/transport/request.rs @@ -1,3 +1,5 @@ +//! 验证生成请求编码的模型、位置覆盖与不透明字段保留 + use chrono::{Datelike as _, Utc}; use chrono_tz::America::New_York; use gateway_core::operation::{GenerateRequest, ProtocolPayload}; diff --git a/backend/crates/providers/openai/tests/transport/reset_credits.rs b/backend/crates/providers/openai/tests/transport/reset_credits.rs index d57600929..609540406 100644 --- a/backend/crates/providers/openai/tests/transport/reset_credits.rs +++ b/backend/crates/providers/openai/tests/transport/reset_credits.rs @@ -1,3 +1,5 @@ +//! 验证额度重置卡查询和消费的请求合同与禁止重放边界 + use chrono::{TimeZone as _, Utc}; use provider_openai::credential::CodexResetCreditsError; use provider_openai::transport::profile::{CodexWireProfile, CodexWireProfileState}; diff --git a/backend/crates/providers/openai/tests/transport/subscription.rs b/backend/crates/providers/openai/tests/transport/subscription.rs index ae7d0abb9..b9afd1af5 100644 --- a/backend/crates/providers/openai/tests/transport/subscription.rs +++ b/backend/crates/providers/openai/tests/transport/subscription.rs @@ -1,3 +1,5 @@ +//! 验证订阅查询的账号绑定、安全投影与未知状态保留 + use super::{CodexBackendClient, CodexRequestContext, test_wire_profile}; use serde_json::json; use std::time::Duration; diff --git a/backend/crates/providers/openai/tests/transport/tls.rs b/backend/crates/providers/openai/tests/transport/tls.rs index 18d7c41e8..226284a0b 100644 --- a/backend/crates/providers/openai/tests/transport/tls.rs +++ b/backend/crates/providers/openai/tests/transport/tls.rs @@ -1,11 +1,13 @@ +//! 验证 HTTP 与 WebSocket 的 TLS 握手特征 + use std::collections::BTreeMap; use bytes::{Buf as _, Bytes}; use super::*; -// 2026-09-19 从 Desktop 26.915.31945 / Core 0.155.0-alpha.9.2 各重复抓取三次。 -// HTTP/native-tls 的扩展顺序固定;WebSocket/rustls 会随机化扩展顺序和临时密钥。 +// 2026-09-19 从 Desktop 26.915.31945 / Core 0.155.0-alpha.9.2 各重复抓取三次 +// HTTP/native-tls 的扩展顺序固定;WebSocket/rustls 会随机化扩展顺序和临时密钥 #[derive(Debug, PartialEq, Eq)] struct ClientHello { cipher_suites: Vec, @@ -50,7 +52,7 @@ fn http_client_hello_should_match_official_native_tls_transport() { } // Cargo 运行测试时可能注入 SSL_CERT_FILE,生产代码会把它视作自定义 CA 并切到 rustls; - // 因此在清理相关环境变量的子进程里验证默认路径。 + // 因此在清理相关环境变量的子进程里验证默认路径 let current_exe = std::env::current_exe().expect("current test binary path"); let output = Command::new(current_exe) .arg("--exact") @@ -149,7 +151,7 @@ async fn read_client_hello(listener: TcpListener) -> ClientHello { let mut hello = Bytes::from(record); assert_eq!(hello.get_u8(), 1, "ClientHello message"); - hello.advance(3 + 2 + 32); // Message length, legacy version, random. + hello.advance(3 + 2 + 32); // 消息长度、旧版协议版本与随机数 let session_id_len = usize::from(hello.get_u8()); hello.advance(session_id_len); let cipher_suites = u16_values(take_vector(&mut hello)); diff --git a/backend/crates/providers/openai/tests/transport/usage.rs b/backend/crates/providers/openai/tests/transport/usage.rs index 28dafe3f5..d862235ac 100644 --- a/backend/crates/providers/openai/tests/transport/usage.rs +++ b/backend/crates/providers/openai/tests/transport/usage.rs @@ -1,3 +1,5 @@ +//! 验证 OpenAI 各模型、服务档位与上下文区间的费用明细计算 + use std::collections::BTreeSet; use std::io::{Read, Write}; use std::net::TcpListener; @@ -32,7 +34,7 @@ fn billing_usage( #[test] fn astra_billing_should_preserve_components_across_tiers_and_context_boundary() { - // USD per million tokens, in order: input, cache read, cache write, output. + // 每百万 token 的美元价格,依次为输入、缓存读取、缓存写入与输出 for (tier, input, expected, multiplier) in [ (None, 272_000, ["10", "1", "12.5", "50"], 100), (None, 272_001, ["20", "2", "25", "75"], 100), @@ -78,7 +80,7 @@ fn astra_billing_should_preserve_components_across_tiers_and_context_boundary() #[test] fn new_gpt_6_models_should_use_published_prices_across_tiers_and_context_boundary() { - // 每档依次核对每百万 Token 的输入、缓存读取、缓存写入和输出价格。 + // 每档依次核对每百万 Token 的输入、缓存读取、缓存写入和输出价格 for (model, cases) in [ ( "gpt-6.1-sol", @@ -308,7 +310,7 @@ async fn exact_limit_success_body_should_be_accepted() { #[test] fn billing_should_use_explicit_variant_prices_and_verified_snapshots() { - // 每个用例包含一个普通输入 token 和一个输出 token,金额单位为 USD ticks。 + // 每个用例包含一个普通输入 token 和一个输出 token,金额单位为 USD ticks for (model, expected) in [ ("gpt-5.6-cyber", 875_000), ("gpt-5.5-cyber", 875_000), @@ -333,7 +335,7 @@ fn billing_should_use_explicit_variant_prices_and_verified_snapshots() { #[test] fn billing_should_not_price_shutdown_models_or_restore_them_through_aliases() { - // 官方关闭日期已过;同族仍在服务的基础型号不能为这些旧型号兜底。 + // 官方关闭日期已过;同族仍在服务的基础型号不能为这些旧型号兜底 for model in [ "gpt-4-0314", " OpenAI/GPT-4-0314 ", @@ -358,7 +360,7 @@ fn billing_should_not_price_shutdown_models_or_restore_them_through_aliases() { #[test] fn billing_should_keep_deprecated_models_before_their_shutdown_dates() { - // 截至 2026-09-13,这些型号尚未到官方关闭日期,仍按已公布单价计费。 + // 截至 2026-09-13,这些型号尚未到官方关闭日期,仍按已公布单价计费 for model in [ "gpt-3.5-turbo-instruct", "gpt-3.5-turbo-1106", diff --git a/backend/crates/providers/openai/tests/transport/websocket/diagnostics.rs b/backend/crates/providers/openai/tests/transport/websocket/diagnostics.rs index add2877b9..f09c1e91a 100644 --- a/backend/crates/providers/openai/tests/transport/websocket/diagnostics.rs +++ b/backend/crates/providers/openai/tests/transport/websocket/diagnostics.rs @@ -1,3 +1,5 @@ +//! 验证 WebSocket 异常断开诊断保留事实并脱敏正文 + use super::*; use gateway_core::diagnostics::TraceContext; @@ -29,7 +31,7 @@ async fn diagnostics_preserve_abrupt_disconnect_facts_without_response_content() .await .unwrap(); } - // Drop the transport without the WebSocket close handshake. + // 直接丢弃传输连接,不执行 WebSocket 关闭握手 }); let trace = TraceContext::new("req_abrupt_disconnect"); let attempt = trace.attempt(1); diff --git a/backend/crates/providers/openai/tests/transport/websocket/fingerprint.rs b/backend/crates/providers/openai/tests/transport/websocket/fingerprint.rs index 01f38a17f..160af1252 100644 --- a/backend/crates/providers/openai/tests/transport/websocket/fingerprint.rs +++ b/backend/crates/providers/openai/tests/transport/websocket/fingerprint.rs @@ -1,3 +1,5 @@ +//! 验证 Codex WebSocket 线协议指纹 + use base64::{Engine as _, engine::general_purpose::STANDARD}; use flate2::{Decompress, FlushDecompress}; use tokio_tungstenite::tungstenite::handshake::derive_accept_key; @@ -108,7 +110,7 @@ async fn official_websocket_wire_fingerprint_should_remain_aligned() { opening.lines().next(), Some("GET /backend-api/codex/responses HTTP/1.1") ); - // 官方 Codex Core 0.154.0(Desktop 26.908.40834)的原始 opening 抓包顺序。 + // 官方 Codex Core 0.154.0(Desktop 26.908.40834)的原始 opening 抓包顺序 assert_eq!( read_header_names(&opening), vec![ diff --git a/backend/crates/providers/openai/tests/transport/websocket/metadata.rs b/backend/crates/providers/openai/tests/transport/websocket/metadata.rs index ea34dfd2c..0e0b5341c 100644 --- a/backend/crates/providers/openai/tests/transport/websocket/metadata.rs +++ b/backend/crates/providers/openai/tests/transport/websocket/metadata.rs @@ -1,3 +1,5 @@ +//! 验证复用 WebSocket 时响应元数据按每次交换隔离 + use super::*; #[tokio::test] diff --git a/backend/crates/providers/openai/tests/transport/websocket/mod.rs b/backend/crates/providers/openai/tests/transport/websocket/mod.rs index 3d1a0cfce..e45e617b6 100644 --- a/backend/crates/providers/openai/tests/transport/websocket/mod.rs +++ b/backend/crates/providers/openai/tests/transport/websocket/mod.rs @@ -1,3 +1,5 @@ +//! WebSocket 传输测试入口,以及帧压缩、请求准备与审计测试 + use super::*; use provider_openai::transport::{ protocol::websocket::{OpeningAuditSnapshot, WebSocketAuditArtifact}, @@ -370,7 +372,7 @@ async fn websocket_exchange_should_accept_upstream_frame_above_removed_private_l let addr = listener.local_addr().unwrap(); let server = tokio::spawn(async move { let (stream, _) = listener.accept().await.unwrap(); - // Do not negotiate compression: the wire frame itself must exceed the former limit. + // 禁用压缩协商,确保实际传输帧足够大以覆盖大小限制场景 let mut websocket = accept_codex_test_websocket_with(stream, |_request, _response| {}).await; let _message = websocket.next().await.unwrap().unwrap(); @@ -855,7 +857,7 @@ async fn websocket_execute_response_create_request_should_forward_typed_events_w let (stream, _) = listener.accept().await.unwrap(); let mut websocket = accept_codex_test_websocket(stream).await; let _message = websocket.next().await.unwrap().unwrap(); - // 透明代理:缺官方必需字段的 delta 事件不再被丢弃,原样转发。 + // 透明代理:缺官方必需字段的 delta 事件不再被丢弃,原样转发 websocket .send(Message::Text( json!({ @@ -1845,8 +1847,9 @@ async fn codex_backend_client_stream_should_preserve_burst_during_downstream_bac let mut websocket = accept_codex_test_websocket(stream).await; let _message = websocket.next().await.unwrap().unwrap(); let (mut sink, mut source) = websocket.split(); - // 服务端持续读取控制帧,让 tungstenite 正常回 Pong。若客户端在本地入站背压 - // 期间仍执行 Pong deadline,它仍会因为暂时读不到已返回的 Pong 而误杀连接。 + // 服务端持续读取控制帧,让 tungstenite 正常回 Pong + // 若客户端在本地入站背压 + // 期间仍执行 Pong deadline,它仍会因为暂时读不到已返回的 Pong 而误杀连接 let control_frames = tokio::spawn(async move { while let Some(message) = source.next().await { match message.unwrap() { @@ -2321,7 +2324,7 @@ async fn diagnostics_capture_metadata_and_unknown_events_before_normal_close() { metadata["data"]["metadata"]["headers"]["x-request-id"], "upstream-metadata-request" ); - // 未知事件名可能来自用户内容;仍记录事件,但按默认诊断合同仅保留摘要。 + // 未知事件名可能来自用户内容;仍记录事件,但按默认诊断合同仅保留摘要 let unknown_event = gateway_core::diagnostics::body_fingerprint(b"future.metadata"); assert!( events diff --git a/backend/crates/providers/openai/tests/transport/websocket_pool.rs b/backend/crates/providers/openai/tests/transport/websocket_pool.rs index 8d13cdd65..1591b1aa8 100644 --- a/backend/crates/providers/openai/tests/transport/websocket_pool.rs +++ b/backend/crates/providers/openai/tests/transport/websocket_pool.rs @@ -1,3 +1,5 @@ +//! 验证 WebSocket 连接池的会话隔离、续接复用与失效回收 + use super::*; use provider_openai::transport::websocket::PreviousResponseUnavailableReason; @@ -268,9 +270,9 @@ async fn exact_continuation_reuses_owning_socket_after_connection_profile_update assert_eq!(accepted.load(Ordering::SeqCst), 1); } -/// idle 连接被上游静默关闭后,后台 pump 会实时把它标记为 closed。 +/// idle 连接被上游静默关闭后,后台 pump 会实时把它标记为 closed /// 复用前的零成本 `is_closed` 检查应直接丢弃它并新建连接,不经过 -/// “发请求 → 等首帧超时 → stale-reuse 重试” 的长尾(无需任何 maintenance sweep)。 +/// “发请求 → 等首帧超时 → stale-reuse 重试” 的长尾(无需任何 maintenance sweep) #[tokio::test] async fn codex_backend_client_should_open_fresh_socket_when_idle_pooled_websocket_died_silently() { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); @@ -279,7 +281,7 @@ async fn codex_backend_client_should_open_fresh_socket_when_idle_pooled_websocke let accepted_connections_for_server = Arc::clone(&accepted_connections); let (first_closed_tx, first_closed_rx) = tokio::sync::oneshot::channel(); let server = tokio::spawn(async move { - // 第一条连接:完成一次响应后由服务端主动关闭(模拟 idle 期间被上游/中间盒断开)。 + // 第一条连接:完成一次响应后由服务端主动关闭(模拟 idle 期间被上游/中间盒断开) let (first_stream, _) = listener.accept().await.unwrap(); accepted_connections_for_server.fetch_add(1, Ordering::SeqCst); let mut first_websocket = accept_codex_test_websocket(first_stream).await; @@ -294,7 +296,7 @@ async fn codex_backend_client_should_open_fresh_socket_when_idle_pooled_websocke let _ = first_websocket.next().await; first_closed_tx.send(()).unwrap(); - // 第二条连接:证明复用被跳过、直接新建。 + // 第二条连接:证明复用被跳过、直接新建 let (second_stream, _) = listener.accept().await.unwrap(); accepted_connections_for_server.fetch_add(1, Ordering::SeqCst); let mut second_websocket = accept_codex_test_websocket(second_stream).await; @@ -307,7 +309,7 @@ async fn codex_backend_client_should_open_fresh_socket_when_idle_pooled_websocke .unwrap(); second_websocket.close(None).await.unwrap(); }); - // 无 maintenance、无主动 ping:完全依赖 pump 后台读取感知连接死亡。 + // 无 maintenance、无主动 ping:完全依赖 pump 后台读取感知连接死亡 let pool = Arc::new(CodexWebSocketPool::with_config( websocket_pool_config_for_tests(None, None, None), )); @@ -348,7 +350,7 @@ async fn codex_backend_client_should_open_fresh_socket_when_idle_pooled_websocke assert!(first.body.contains("resp_silent_first")); assert!(second.body.contains("resp_silent_second")); - // 死连接在 acquire 处被零成本识别 → 直接新建,而非 stale-reuse 重试。 + // 死连接在 acquire 处被零成本识别 → 直接新建,而非 stale-reuse 重试 assert_eq!(first.websocket_pool_decision.unwrap().kind(), "new"); assert_eq!(second.websocket_pool_decision.unwrap().kind(), "new"); assert_eq!(accepted_connections.load(Ordering::SeqCst), 2); @@ -830,8 +832,8 @@ async fn websocket_pool_should_release_slot_when_client_drops_stream() { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let server = tokio::spawn(async move { - // 第一个连接:发一帧后保持沉默(模拟上游不再发帧、也不发 terminal)。 - // slot 只能靠客户端断开来释放,隔离验证 tx.closed() 机制。 + // 第一个连接:发一帧后保持沉默(模拟上游不再发帧、也不发 terminal) + // slot 只能靠客户端断开来释放,隔离验证 tx.closed() 机制 let (first_stream, _) = listener.accept().await.unwrap(); let mut first_websocket = accept_codex_test_websocket(first_stream).await; let _first_message = first_websocket.next().await.unwrap().unwrap(); @@ -847,7 +849,7 @@ async fn websocket_pool_should_release_slot_when_client_drops_stream() { .await .unwrap(); - // 第二个连接:客户端断开释放 slot 后,同 key 请求应新建连接。 + // 第二个连接:客户端断开释放 slot 后,同 key 请求应新建连接 let (second_stream, _) = listener.accept().await.unwrap(); let mut second_websocket = accept_codex_test_websocket(second_stream).await; let _second_message = second_websocket.next().await.unwrap().unwrap(); @@ -869,7 +871,7 @@ async fn websocket_pool_should_release_slot_when_client_drops_stream() { .with_websocket_pool(pool); let request = pooled_websocket_request("conversation-drop"); - // 起流式请求:slot 变 Busy。 + // 起流式请求:slot 变 Busy let mut stream = backend .create_response_stream( &request, @@ -890,11 +892,11 @@ async fn websocket_pool_should_release_slot_when_client_drops_stream() { ); // 客户端断开:drop stream → rx 被 drop → tx.closed() 完成 → - // 代理丢弃上游连接并释放 slot(不再等 idle 超时)。 + // 代理丢弃上游连接并释放 slot(不再等 idle 超时) drop(stream); tokio::time::sleep(Duration::from_millis(200)).await; - // 同 key 的后续请求:slot 已释放 → 新建连接(new),而非 bypass(busy)。 + // 同 key 的后续请求:slot 已释放 → 新建连接(new),而非 bypass(busy) let second = backend .create_response( &request, @@ -1152,7 +1154,7 @@ async fn websocket_pool_should_replace_idle_connection_after_pong_deadline() { const PONG_TIMEOUT: Duration = Duration::from_secs(30); // 服务端读取 Ping 后暂不继续 poll,避免 tungstenite 自动回 Pong;pump 必须在独立 - // deadline 到期时主动关闭连接,acquire 随后只读 closed 状态并直接新建连接。 + // deadline 到期时主动关闭连接,acquire 随后只读 closed 状态并直接新建连接 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let accepted_connections = Arc::new(AtomicUsize::new(0)); @@ -1459,7 +1461,7 @@ async fn codex_backend_client_should_keep_idle_pooled_websocket_alive_across_rep .unwrap(); // pump 会在 idle 期间反复发送 keepalive ping;服务端计数并回 pong, - // 直到下一个业务请求(response.create)到达为止。 + // 直到下一个业务请求(response.create)到达为止 let mut previous_ping = None; let mut pings_observed_tx = Some(pings_observed_tx); loop { diff --git a/backend/crates/providers/xai/src/admin.rs b/backend/crates/providers/xai/src/admin.rs index 61421c6f7..39415b468 100644 --- a/backend/crates/providers/xai/src/admin.rs +++ b/backend/crates/providers/xai/src/admin.rs @@ -1,4 +1,4 @@ -//! xAI 管理边界:Provider preparation 与 Redis OAuth pending 适配。 +//! xAI 管理边界:Provider preparation 与 Redis OAuth pending 适配 use std::collections::BTreeSet; use std::sync::Arc; @@ -396,7 +396,7 @@ impl ProviderAdmin for XaiAdminProvider { } async fn account_unavailable(&self, account_id: &ProviderAccountId) { - // 删除/禁用账号:清除该账号全部 account/model scoped cooldown key。 + // 删除/禁用账号:清除该账号全部 account/model scoped cooldown key let _ = self.cooldowns.clear_all(account_id).await; } @@ -468,7 +468,7 @@ impl ProviderAdmin for XaiAdminProvider { return Ok(None); } Ok(Some(CalculatedBillingBreakdown { - // 历史总额只能核对费用拆分,不能证明当时保存过长上下文标记。 + // 历史总额只能核对费用拆分,不能证明当时保存过长上下文标记 long_context_billing_applied: false, image: None, custom_multiplier_bps: breakdown.custom_multiplier_bps(), @@ -505,11 +505,12 @@ impl ProviderAdmin for XaiAdminProvider { let mut subjects = BTreeSet::new(); let mut credentials = Vec::new(); // 逐条目独立成败:verify 会真实轮换 RT,后续条目的任何失败都不得 - // 丢弃已轮换成功的凭据(旧 RT 已被上游作废,丢弃即不可逆报废)。 - // 失败条目跳过;仅当没有任何条目成功时返回首个失败。响应只携带 + // 丢弃已轮换成功的凭据(旧 RT 已被上游作废,丢弃即不可逆报废) + // 失败条目跳过;仅当没有任何条目成功时返回首个失败 + // 响应只携带 // 成功计数,每个被跳过的条目在此逐条留日志——尤其 verify 成功后 // 才失败/被去重的条目,其源文件里的 RT 已被轮换作废,运维只能从 - // 日志得知需要重新授权。 + // 日志得知需要重新授权 let mut first_failure: Option = None; for entry in document.into_entries() { let model_access = entry.model_access().cloned(); @@ -538,7 +539,7 @@ impl ProviderAdmin for XaiAdminProvider { } }; // 同一账号的另一份 grant:账号已由更早条目入册,本条目跳过; - // 本条目若经历了 refresh,其源文件 RT 已作废。 + // 本条目若经历了 refresh,其源文件 RT 已作废 if !subjects.insert(tokens.evidence().subject().to_owned()) { tracing::warn!( target: "xai_admin", diff --git a/backend/crates/providers/xai/src/credential/authorization_code.rs b/backend/crates/providers/xai/src/credential/authorization_code.rs index 90fc1adc4..1627388f2 100644 --- a/backend/crates/providers/xai/src/credential/authorization_code.rs +++ b/backend/crates/providers/xai/src/credential/authorization_code.rs @@ -1,3 +1,5 @@ +//! xAI 授权码流程的状态生成、回调校验与待授权状态恢复 + use std::fmt; use base64::Engine as _; @@ -14,7 +16,7 @@ use crate::{ const MAX_CALLBACK_VALUE_BYTES: usize = 64 * 1024; -/// 已解析的回调;code 与 state 在 debug 输出中保持脱敏。 +/// 已解析的回调;code 与 state 在 debug 输出中保持脱敏 pub struct AuthorizationCallback { code: Option, state: Option, @@ -22,13 +24,13 @@ pub struct AuthorizationCallback { } impl AuthorizationCallback { - /// 解析 OAuth 回调 query,并拒绝重复的安全敏感参数。 - /// 服务端原始描述文本一律丢弃。 + /// 解析 OAuth 回调 query,并拒绝重复的安全敏感参数 + /// 服务端原始描述文本一律丢弃 /// /// # Errors /// /// `code`、`state` 或 `error` 键重复出现时返回 - /// [`CallbackRejection::DuplicateParameter`]。 + /// [`CallbackRejection::DuplicateParameter`] pub fn parse(query: &str) -> Result { let mut code = None; let mut state = None; @@ -72,7 +74,7 @@ impl fmt::Debug for AuthorizationCallback { } } -/// 等待一次回调的 Authorization Code + PKCE 状态。 +/// 等待一次回调的 Authorization Code + PKCE 状态 pub struct PendingAuthorization { authorization_url: Url, redirect_uri: AllowedRedirectUri, @@ -122,17 +124,17 @@ impl PendingAuthorization { }) } - /// 返回管理员需在官方 issuer 打开的 URL。 + /// 返回管理员需在官方 issuer 打开的 URL #[must_use] pub fn authorization_url(&self) -> &Url { &self.authorization_url } - /// 将 server-only PKCE 状态编码为待加密载荷;调用方必须在离开内存前加密。 + /// 将 server-only PKCE 状态编码为待加密载荷;调用方必须在离开内存前加密 /// /// # Errors /// - /// 内部状态无法序列化时 fail closed。 + /// 内部状态无法序列化时 fail closed pub fn into_server_state(self) -> Result { let wire = PendingAuthorizationWire { schema_version: 1, @@ -147,11 +149,11 @@ impl PendingAuthorization { .map_err(|_| pending_state_error()) } - /// 从已经通过服务端 envelope 认证的载荷恢复一次性 PKCE 状态。 + /// 从已经通过服务端 envelope 认证的载荷恢复一次性 PKCE 状态 /// /// # Errors /// - /// 版本、URL 或 secret 形态不满足固定官方协议时拒绝恢复。 + /// 版本、URL 或 secret 形态不满足固定官方协议时拒绝恢复 pub fn from_server_state( config: &GrokOAuthConfig, state: &SecretValue, @@ -180,11 +182,11 @@ impl PendingAuthorization { }) } - /// 消费一次性流程并校验必需的回调 state。 + /// 消费一次性流程并校验必需的回调 state /// /// # Errors /// - /// state 缺失或不匹配、provider 拒绝、code 缺失时返回回调拒绝错误。 + /// state 缺失或不匹配、provider 拒绝、code 缺失时返回回调拒绝错误 pub fn accept_callback( self, callback: AuthorizationCallback, @@ -200,14 +202,14 @@ impl PendingAuthorization { self.accept_code(code) } - /// 消费一次性流程,并接受管理员粘贴的完整回调 URL、回调 query 或裸授权码。 + /// 消费一次性流程,并接受管理员粘贴的完整回调 URL、回调 query 或裸授权码 /// - /// 完整 URL 仍须精确匹配本流程的 redirect URI;query 输入仍须回传正确 state。 - /// 裸授权码依赖已经绑定 owner 的一次性 flow 与 PKCE verifier,不降低 token 交换约束。 + /// 完整 URL 仍须精确匹配本流程的 redirect URI;query 输入仍须回传正确 state + /// 裸授权码依赖已经绑定 owner 的一次性 flow 与 PKCE verifier,不降低 token 交换约束 /// /// # Errors /// - /// URL、state 或授权码不满足当前一次性流程约束时返回回调拒绝错误。 + /// URL、state 或授权码不满足当前一次性流程约束时返回回调拒绝错误 pub fn accept_authorization_input( self, input: &str, @@ -300,7 +302,7 @@ impl fmt::Debug for PendingAuthorization { } } -/// 已通过 state 校验、可用于一次 token 交换的授权 grant。 +/// 已通过 state 校验、可用于一次 token 交换的授权 grant pub struct AuthorizationCodeGrant { code: SecretValue, redirect_uri: AllowedRedirectUri, diff --git a/backend/crates/providers/xai/src/credential/catalog.rs b/backend/crates/providers/xai/src/credential/catalog.rs index 1e3131f91..92bd13cc8 100644 --- a/backend/crates/providers/xai/src/credential/catalog.rs +++ b/backend/crates/providers/xai/src/credential/catalog.rs @@ -1,4 +1,4 @@ -//! xAI OAuth account 的实时模型目录与可重建 TTL cache 边界。 +//! xAI OAuth account 的实时模型目录与可重建 TTL cache 边界 use std::collections::BTreeMap; use std::fmt; @@ -31,10 +31,10 @@ const MAX_CATALOG_FETCH_ATTEMPTS: usize = 3; const QUOTA_SCHEDULING_TTL: Duration = Duration::from_secs(10 * 60); const QUOTA_HYDRATION_FAILURE_TTL: Duration = Duration::from_secs(5); -/// Grok Build Free 额度的滚动观察窗口。 +/// Grok Build Free 额度的滚动观察窗口 pub const GROK_FREE_ROLLING_WINDOW_SECONDS: u64 = 86_400; -/// xAI Provider 从动态 billing JSON 解析出的旧账号页安全投影。 +/// xAI Provider 从动态 billing JSON 解析出的旧账号页安全投影 #[derive(Debug, Clone, PartialEq)] pub struct GrokBillingPresentation { plan_type: Option, @@ -49,7 +49,7 @@ pub struct GrokBillingPresentation { prepaid_balance_cents: Option, } -/// xAI credits 当前周期的官方语义。 +/// xAI credits 当前周期的官方语义 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum GrokQuotaPeriodKind { Weekly, @@ -68,7 +68,7 @@ impl GrokBillingPresentation { self.used_percent } - /// 返回 billing 是否包含可直接用于账号额度展示的付费事实。 + /// 返回 billing 是否包含可直接用于账号额度展示的付费事实 #[must_use] pub fn has_authoritative_quota(&self) -> bool { self.used_percent.is_some() @@ -147,7 +147,7 @@ impl GrokBillingPresentation { } } -/// 一个账号最近一次 xAI billing 观察结果。 +/// 一个账号最近一次 xAI billing 观察结果 #[derive(Debug, Clone, PartialEq)] pub struct GrokQuotaSnapshot { account_id: ProviderAccountId, @@ -192,7 +192,7 @@ pub enum GrokQuotaError { Store, } -/// 官方 Grok Build billing 同步与 Provider-owned quota JSON 解析服务。 +/// 官方 Grok Build billing 同步与 Provider-owned quota JSON 解析服务 #[derive(Clone)] pub struct GrokCredentialQuotaService { repository: GrokCredentialRepository, @@ -388,7 +388,7 @@ impl GrokCredentialQuotaService { } } - /// 批量预热请求级额度投影;持久层或 Provider JSON 异常只退化为未知额度。 + /// 批量预热请求级额度投影;持久层或 Provider JSON 异常只退化为未知额度 pub async fn prepare_scheduling(&self, accounts: &[ProviderAccount]) { if self.scheduling.hydration_targets(accounts).is_empty() { return; @@ -436,7 +436,7 @@ impl GrokCredentialQuotaService { self.scheduling.invalidate(account_ids); } - /// 立即刷新一个账号的动态 billing document,并以 credential revision CAS 写回。 + /// 立即刷新一个账号的动态 billing document,并以 credential revision CAS 写回 pub async fn refresh_account( &self, account_id: &ProviderAccountId, @@ -461,7 +461,7 @@ impl GrokCredentialQuotaService { .await .map_err(|_| GrokQuotaError::Upstream)?; let mut document = billing.into_document(); - // 订阅查询失败不影响已取得的额度,也不能据此写入免费套餐。 + // 订阅查询失败不影响已取得的额度,也不能据此写入免费套餐 match self.client.fetch_subscription(&session).await { Ok(Some(plan_type)) => { document.insert( @@ -498,7 +498,7 @@ impl GrokCredentialQuotaService { Ok(snapshot) } - /// 读取并重新验证 Store 中的 Provider-owned quota JSON。 + /// 读取并重新验证 Store 中的 Provider-owned quota JSON pub async fn read_account( &self, account_id: &ProviderAccountId, @@ -534,7 +534,7 @@ impl fmt::Debug for GrokCredentialQuotaService { } } -/// 官方 `/v1/models` 形成的一个 account 完整模型集合。 +/// 官方 `/v1/models` 形成的一个 account 完整模型集合 #[derive(Clone, PartialEq, Eq)] pub struct GrokCredentialCatalogSeed { etag: Option, @@ -605,12 +605,12 @@ impl fmt::Debug for GrokCredentialCatalogSeed { } } -/// xAI 以套餐划分的模型目录作用域。 +/// xAI 以套餐划分的模型目录作用域 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct GrokCatalogScope(ProviderCatalogScope); impl GrokCatalogScope { - /// 从账号已验证的套餐事实构造目录作用域。 + /// 从账号已验证的套餐事实构造目录作用域 pub fn for_account(account: &ProviderAccount) -> Result { let plan = account .plan_type() @@ -623,14 +623,14 @@ impl GrokCatalogScope { .map_err(|_| GrokCatalogCacheError::InvalidData) } - /// 返回稳定的 Provider-owned 作用域。 + /// 返回稳定的 Provider-owned 作用域 #[must_use] pub fn as_str(&self) -> &str { self.0.as_str() } } -/// Redis/内存 TTL cache 中的一条可重建套餐 catalog。 +/// Redis/内存 TTL cache 中的一条可重建套餐 catalog #[derive(Clone, Debug, PartialEq, Eq)] pub struct GrokPlanCatalog { scope: GrokCatalogScope, @@ -668,7 +668,7 @@ impl GrokPlanCatalog { } } -/// Provider-owned catalog cache;实现只能保存可重建 TTL 数据。 +/// Provider-owned catalog cache;实现只能保存可重建 TTL 数据 #[async_trait] pub trait GrokCredentialCatalogCache: Send + Sync { async fn replace(&self, catalog: GrokPlanCatalog) -> Result<(), GrokCatalogCacheError>; @@ -685,7 +685,7 @@ pub trait GrokCredentialCatalogCache: Send + Sync { ) -> Result, GrokCatalogCacheError>; } -/// xAI 负责解释 catalog 文档,Store 只保存 opaque JSON。 +/// xAI 负责解释 catalog 文档,Store 只保存 opaque JSON pub struct GrokCatalogCache { port: Arc, provider_kind: ProviderKind, @@ -809,7 +809,7 @@ impl GrokCredentialCatalogCache for GrokCatalogCache { else { return Ok(None); }; - // cache 只保存可重建 TTL 数据:损坏条目按 miss 丢弃,由调用方实时重建。 + // cache 只保存可重建 TTL 数据:损坏条目按 miss 丢弃,由调用方实时重建 match Self::decode(scope, document) { Ok(catalog) => Ok(Some(catalog)), Err(_) => { @@ -901,7 +901,7 @@ impl GrokCredentialCatalogService { ProviderCatalogGeneration::new(published.generation) } - /// 优先读取套餐目录 cache;缺失时才用当前账号所属套餐的有限候选集实时填充。 + /// 优先读取套餐目录 cache;缺失时才用当前账号所属套餐的有限候选集实时填充 pub async fn cached_or_refresh_account_catalog( &self, account: &ProviderAccount, @@ -919,7 +919,7 @@ impl GrokCredentialCatalogService { self.refresh_account_catalog(account.id()).await } - /// 实时刷新指定账号所属套餐的模型集合,并覆盖可重建 cache。 + /// 实时刷新指定账号所属套餐的模型集合,并覆盖可重建 cache pub async fn refresh_account_catalog( &self, account_id: &ProviderAccountId, @@ -931,7 +931,7 @@ impl GrokCredentialCatalogService { .map_err(|_| GrokCredentialCatalogError::Store)?; let mut groups = match catalog_candidates_by_scope(candidates) { Ok(groups) => groups, - // 全部候选都被调度列表过滤时按空组处理,让下方目标账号补回继续生效。 + // 全部候选都被调度列表过滤时按空组处理,让下方目标账号补回继续生效 Err(GrokCredentialCatalogError::NoEligibleCredential) => BTreeMap::new(), Err(error) => return Err(error), }; @@ -946,8 +946,9 @@ impl GrokCredentialCatalogService { Some(scope) => scope, None => { // 常规调度列表不含停用账号;管理端按账号查询模型要对停用账号返回 - // 真实上游结果。按同一 revision 加载账号与凭据,避免并发更新套餐时 - // 把新凭据的目录写入旧套餐 cache。 + // 真实上游结果 + // 按同一 revision 加载账号与凭据,避免并发更新套餐时 + // 把新凭据的目录写入旧套餐 cache let account = self .repository .account_by_id(account_id) @@ -990,7 +991,7 @@ impl GrokCredentialCatalogService { self.refresh_scope_catalog(scope, candidates).await } - /// 读取当前账号所属套餐的目录 cache,不触发上游请求。 + /// 读取当前账号所属套餐的目录 cache,不触发上游请求 pub async fn read_account_catalog( &self, account: &ProviderAccount, @@ -1003,7 +1004,7 @@ impl GrokCredentialCatalogService { .map_err(|_| GrokCredentialCatalogError::Cache) } - /// Provider Registry 构建 RuntimeSnapshot 时使用的实时能力目录。 + /// Provider Registry 构建 RuntimeSnapshot 时使用的实时能力目录 pub async fn query_models(&self) -> Result, GrokCredentialCatalogError> { self.fetch_and_cache().await } @@ -1018,7 +1019,7 @@ impl GrokCredentialCatalogService { .filter(eligible_catalog_candidate) .collect::>(); let groups = catalog_candidates_by_scope(candidates)?; - // 单个套餐失败只跳过该套餐,不阻断其他套餐的目录同步。 + // 单个套餐失败只跳过该套餐,不阻断其他套餐的目录同步 let mut fetched = Vec::with_capacity(groups.len()); let mut last_error = None; for (scope, candidates) in groups { @@ -1052,7 +1053,7 @@ impl GrokCredentialCatalogService { .await .is_err() { - // cache 是可重建 TTL 数据,单套餐写入失败不阻断目录发布。 + // cache 是可重建 TTL 数据,单套餐写入失败不阻断目录发布 tracing::warn!( scope = scope.as_str(), "failed to cache xAI plan catalog scope" @@ -1319,8 +1320,9 @@ fn quota_scheduling_signals(billing: &GrokBillingPresentation) -> Option &GrokOAuthConfig { &self.config @@ -50,12 +52,12 @@ impl GrokOAuthClient { client } - /// 拉取并校验官方同源 OIDC 发现文档。 + /// 拉取并校验官方同源 OIDC 发现文档 /// /// # Errors /// /// transport 失败、非成功状态、JSON 非法、issuer 不匹配、端点跨 origin、 - /// 缺少 JWKS 或算法不安全时返回错误。 + /// 缺少 JWKS 或算法不安全时返回错误 pub async fn discover(&self) -> Result { let response = self .execute( @@ -72,11 +74,11 @@ impl GrokOAuthClient { DiscoveryDocument::parse(&self.config, response.body()) } - /// 启动 Authorization Code + PKCE,不产生网络 I/O。 + /// 启动 Authorization Code + PKCE,不产生网络 I/O /// /// # Errors /// - /// 安全随机 state、nonce 或 verifier 生成失败时返回熵不可用错误。 + /// 安全随机 state、nonce 或 verifier 生成失败时返回熵不可用错误 pub fn start_authorization_code( &self, discovery: &DiscoveryDocument, @@ -87,12 +89,13 @@ impl GrokOAuthClient { } /// 对已校验 state 的授权 grant 只做一次交换,返回凭据前强制通过 - /// nonce 绑定的 ID token 验证。 + /// nonce 绑定的 ID token 验证 /// /// # Errors /// /// transport/协议错误、缺少 ID token、验证失败或任何非成功 OAuth 响应时 - /// 返回错误。不做自动重试。 + /// 返回错误 + /// 不做自动重试 pub async fn exchange_authorization_code( &self, discovery: &DiscoveryDocument, @@ -156,12 +159,13 @@ impl GrokOAuthClient { )) } - /// 验证已归一化的已有 OAuth credential。有效 AT 由官方 user-info 确认; - /// 过期 AT 先用 RT 换取新 AT,再由同一官方端点确认。 + /// 验证已归一化的已有 OAuth credential + /// 有效 AT 由官方 user-info 确认; + /// 过期 AT 先用 RT 换取新 AT,再由同一官方端点确认 /// /// # Errors /// - /// 导入 metadata、token wire、刷新、OIDC claim 或 user-info 任一失败时拒绝。 + /// 导入 metadata、token wire、刷新、OIDC claim 或 user-info 任一失败时拒绝 pub async fn verify_imported_credential( &self, discovery: &DiscoveryDocument, @@ -233,13 +237,15 @@ impl GrokOAuthClient { Ok(VerifiedTokenSet::new(tokens, evidence, scope)) } - /// 执行一次 refresh token 交换。调用方须串行化刷新,并通过 credential - /// revision CAS 持久化轮换后的 token。 + /// 执行一次 refresh token 交换 + /// 调用方须串行化刷新,并通过 credential + /// revision CAS 持久化轮换后的 token /// /// # Errors /// - /// 返回已分类的 OAuth 错误。refresh token 可能已轮换,Ambiguous 类 - /// transport 失败不在本次 exchange 内重试,后续交由 refresh scheduler 退避协调。 + /// 返回已分类的 OAuth 错误 + /// refresh token 可能已轮换,Ambiguous 类 + /// transport 失败不在本次 exchange 内重试,后续交由 refresh scheduler 退避协调 pub async fn refresh( &self, discovery: &DiscoveryDocument, diff --git a/backend/crates/providers/xai/src/credential/config.rs b/backend/crates/providers/xai/src/credential/config.rs index 7c1b20389..a0a388c59 100644 --- a/backend/crates/providers/xai/src/credential/config.rs +++ b/backend/crates/providers/xai/src/credential/config.rs @@ -1,19 +1,21 @@ +//! xAI OAuth 固定来源、重定向白名单与发现端点配置校验 + use std::fmt; use url::{Host, Url}; use crate::ConfigError; -/// Grok Build 使用的官方第一方 OIDC issuer。 +/// Grok Build 使用的官方第一方 OIDC issuer pub const OFFICIAL_ISSUER: &str = "https://auth.x.ai"; -/// Grok Build 内置的官方 public OAuth client 标识。 +/// Grok Build 内置的官方 public OAuth client 标识 pub const OFFICIAL_CLIENT_ID: &str = "b1a00492-073a-47ea-816f-4c329264a828"; -/// 官方桌面客户端使用的固定 loopback OAuth 回调地址。 +/// 官方桌面客户端使用的固定 loopback OAuth 回调地址 pub const OFFICIAL_REDIRECT_URI: &str = "http://127.0.0.1:56121/callback"; -/// 当前 Grok Build 客户端使用的官方第一方 OAuth scope 集合。 +/// 当前 Grok Build 客户端使用的官方第一方 OAuth scope 集合 pub const OFFICIAL_SCOPES: &[&str] = &[ "openid", "profile", @@ -23,12 +25,12 @@ pub const OFFICIAL_SCOPES: &[&str] = &[ "api:access", ]; -/// 已通过语法与本地 allowlist 校验的精确回调 URI。 +/// 已通过语法与本地 allowlist 校验的精确回调 URI #[derive(Debug, Clone, PartialEq, Eq)] pub struct AllowedRedirectUri(Url); impl AllowedRedirectUri { - /// 返回授权与 token 交换时发送的精确回调 URI。 + /// 返回授权与 token 交换时发送的精确回调 URI #[must_use] pub fn as_url(&self) -> &Url { &self.0 @@ -39,19 +41,20 @@ impl AllowedRedirectUri { } } -/// 显式回调 allowlist。公网 HTTPS 回调与动态 loopback 回调必须先登记才能启动流程。 +/// 显式回调 allowlist +/// 公网 HTTPS 回调与动态 loopback 回调必须先登记才能启动流程 #[derive(Debug, Clone)] pub struct RedirectUriAllowlist { entries: Vec, } impl RedirectUriAllowlist { - /// 校验并构造精确回调 allowlist。 + /// 校验并构造精确回调 allowlist /// /// # Errors /// /// 公网 URI 非 HTTPS、含 user-info、query、fragment 或格式非法时返回 - /// [`ConfigError::InvalidRedirectUri`]。 + /// [`ConfigError::InvalidRedirectUri`] pub fn new(uris: I) -> Result where I: IntoIterator, @@ -65,11 +68,11 @@ impl RedirectUriAllowlist { Ok(Self { entries }) } - /// 校验候选 URI,并要求与本地 allowlist 精确匹配。 + /// 校验候选 URI,并要求与本地 allowlist 精确匹配 /// /// # Errors /// - /// URI 非法或未登记时返回配置错误。 + /// URI 非法或未登记时返回配置错误 pub fn authorize(&self, candidate: &str) -> Result { let candidate = validate_redirect_uri(candidate)?; if self.entries.iter().any(|entry| entry == &candidate) { @@ -80,36 +83,36 @@ impl RedirectUriAllowlist { } } -/// 不可变的官方 Grok Build OAuth 配置。 +/// 不可变的官方 Grok Build OAuth 配置 #[derive(Clone)] pub struct GrokOAuthConfig { issuer: Url, } impl GrokOAuthConfig { - /// 构造固定的官方 issuer、client 与 scope 配置。 + /// 构造固定的官方 issuer、client 与 scope 配置 /// /// # Errors /// - /// 固定官方 issuer 无法构造时返回错误。 + /// 固定官方 issuer 无法构造时返回错误 pub fn official() -> Result { let issuer = Url::parse(OFFICIAL_ISSUER).map_err(|_| ConfigError::UntrustedIssuer)?; Ok(Self { issuer }) } - /// 返回固定的 issuer URL。 + /// 返回固定的 issuer URL #[must_use] pub fn issuer(&self) -> &Url { &self.issuer } - /// 返回官方 public client 标识。 + /// 返回官方 public client 标识 #[must_use] pub const fn client_id(&self) -> &'static str { OFFICIAL_CLIENT_ID } - /// 返回官方 scope 集合。 + /// 返回官方 scope 集合 #[must_use] pub const fn scopes(&self) -> &'static [&'static str] { OFFICIAL_SCOPES diff --git a/backend/crates/providers/xai/src/credential/discovery.rs b/backend/crates/providers/xai/src/credential/discovery.rs index 3a31988eb..c2a4cc865 100644 --- a/backend/crates/providers/xai/src/credential/discovery.rs +++ b/backend/crates/providers/xai/src/credential/discovery.rs @@ -1,3 +1,5 @@ +//! 解析并校验 xAI OIDC 发现文档及其端点与签名算法 + use serde::Deserialize; use url::Url; @@ -5,7 +7,7 @@ use crate::{GrokOAuthConfig, OAuthError, OAuthOperation, ProtocolViolation}; pub(crate) const MAX_OAUTH_RESPONSE_BYTES: usize = 64 * 1024; -/// issuer 与端点均已通过官方 Grok Build origin 策略校验的发现文档。 +/// issuer 与端点均已通过官方 Grok Build origin 策略校验的发现文档 #[derive(Debug, Clone, PartialEq, Eq)] pub struct DiscoveryDocument { issuer: Url, @@ -69,37 +71,37 @@ impl DiscoveryDocument { }) } - /// 返回发现文档中已校验的 issuer。 + /// 返回发现文档中已校验的 issuer #[must_use] pub fn issuer(&self) -> &Url { &self.issuer } - /// 返回已校验的 authorization 端点。 + /// 返回已校验的 authorization 端点 #[must_use] pub fn authorization_endpoint(&self) -> &Url { &self.authorization_endpoint } - /// 返回已校验的 token 端点。 + /// 返回已校验的 token 端点 #[must_use] pub fn token_endpoint(&self) -> &Url { &self.token_endpoint } - /// 返回 ID token 校验器所需的已校验 JWKS 端点。 + /// 返回 ID token 校验器所需的已校验 JWKS 端点 #[must_use] pub fn jwks_uri(&self) -> &Url { &self.jwks_uri } - /// 返回已校验的权威 user-info 端点。 + /// 返回已校验的权威 user-info 端点 #[must_use] pub fn userinfo_endpoint(&self) -> &Url { &self.userinfo_endpoint } - /// 返回发现文档声明的算法;校验器仍须强制自身的密码学 allowlist。 + /// 返回发现文档声明的算法;校验器仍须强制自身的密码学 allowlist #[must_use] pub fn signing_algorithms(&self) -> &[String] { &self.signing_algorithms diff --git a/backend/crates/providers/xai/src/credential/error.rs b/backend/crates/providers/xai/src/credential/error.rs index 14b779db5..bb25497f3 100644 --- a/backend/crates/providers/xai/src/credential/error.rs +++ b/backend/crates/providers/xai/src/credential/error.rs @@ -1,189 +1,191 @@ +//! xAI OAuth 各阶段的配置、协议、回调与验证错误分类 + use thiserror::Error; use crate::TransportFailureKind; -/// OAuth 操作标签,用于脱敏遥测与错误分类。 +/// OAuth 操作标签,用于脱敏遥测与错误分类 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum OAuthOperation { - /// OpenID Provider 发现。 + /// OpenID Provider 发现 Discovery, - /// Authorization Code 换取 token。 + /// Authorization Code 换取 token AuthorizationCodeToken, - /// Refresh token 换取 token。 + /// Refresh token 换取 token RefreshToken, - /// 已有 OAuth token 的受控导入验证。 + /// 已有 OAuth token 的受控导入验证 CredentialImport, } -/// 可安全暴露给控制面的稳定 OAuth 错误码。 +/// 可安全暴露给控制面的稳定 OAuth 错误码 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum OAuthErrorCode { - /// 用户拒绝授权。 + /// 用户拒绝授权 AccessDenied, - /// code 或 refresh token 无效或已被消费。 + /// code 或 refresh token 无效或已被消费 InvalidGrant, - /// 配置的 public client 被拒绝。 + /// 配置的 public client 被拒绝 InvalidClient, - /// 请求的官方 scope 集合被拒绝。 + /// 请求的官方 scope 集合被拒绝 InvalidScope, - /// 授权服务器暂时不可用。 + /// 授权服务器暂时不可用 TemporarilyUnavailable, - /// 无法识别的错误码;不保留服务端原文。 + /// 无法识别的错误码;不保留服务端原文 Other, } -/// 失败后协调方可采取的处置类别。 +/// 失败后协调方可采取的处置类别 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FailureClass { - /// 瞬时失败,允许后续独立协调的重试。 + /// 瞬时失败,允许后续独立协调的重试 Transient, - /// 发送状态不确定,不应在同一 exchange 内重放一次性凭据。 + /// 发送状态不确定,不应在同一 exchange 内重放一次性凭据 Ambiguous, - /// 当前 revision 的凭据被永久拒绝。 + /// 当前 revision 的凭据被永久拒绝 CredentialPermanent, - /// Provider 或 client 配置被永久拒绝。 + /// Provider 或 client 配置被永久拒绝 ConfigurationPermanent, - /// 用户已拒绝,或需要重新发起交互流程。 + /// 用户已拒绝,或需要重新发起交互流程 UserActionRequired, - /// 协议或信任边界校验失败并 fail closed。 + /// 协议或信任边界校验失败并 fail closed Security, - /// 官方部署不提供该流程。 + /// 官方部署不提供该流程 Unsupported, } -/// 发起网络请求前检出的配置错误。 +/// 发起网络请求前检出的配置错误 #[derive(Debug, Error, Clone, PartialEq, Eq)] pub enum ConfigError { - /// 回调 URI 格式非法、不安全或包含禁止成分。 + /// 回调 URI 格式非法、不安全或包含禁止成分 #[error("invalid OAuth redirect URI")] InvalidRedirectUri, - /// 回调 URI 未登记在本地 allowlist。 + /// 回调 URI 未登记在本地 allowlist #[error("OAuth redirect URI is not allowlisted")] RedirectUriNotAllowlisted, - /// issuer 不是官方 Grok Build issuer。 + /// issuer 不是官方 Grok Build issuer #[error("OIDC issuer is not the official Grok Build issuer")] UntrustedIssuer, - /// 发现文档中的端点离开了官方 issuer origin。 + /// 发现文档中的端点离开了官方 issuer origin #[error("OIDC discovery returned an untrusted endpoint")] UntrustedEndpoint, - /// 可选的 team principal 元数据为空或含不安全文本。 + /// 可选的 team principal 元数据为空或含不安全文本 #[error("invalid OAuth principal metadata")] InvalidPrincipal, } -/// 按字段定位的脱敏协议校验错误。 +/// 按字段定位的脱敏协议校验错误 #[derive(Debug, Error, Clone, Copy, PartialEq, Eq)] pub enum ProtocolViolation { - /// JSON 无法解析为预期 wire 结构。 + /// JSON 无法解析为预期 wire 结构 #[error("OAuth response is not valid JSON")] InvalidJson, - /// 缺少必需的 wire 字段。 + /// 缺少必需的 wire 字段 #[error("OAuth response is missing field `{0}`")] MissingField(&'static str), - /// wire 字段违反格式或长度约束。 + /// wire 字段违反格式或长度约束 #[error("OAuth response contains invalid field `{0}`")] InvalidField(&'static str), - /// 响应超出解析器的体积上限。 + /// 响应超出解析器的体积上限 #[error("OAuth response exceeds the maximum body size")] ResponseTooLarge, - /// 发现文档允许不安全的 `none` 签名算法。 + /// 发现文档允许不安全的 `none` 签名算法 #[error("OIDC discovery advertises an insecure signing algorithm")] InsecureSigningAlgorithm, } -/// 回调失败原因;不保留 code 与 state 值。 +/// 回调失败原因;不保留 code 与 state 值 #[derive(Debug, Error, Clone, Copy, PartialEq, Eq)] pub enum CallbackRejection { - /// query 中出现重复的安全敏感参数。 + /// query 中出现重复的安全敏感参数 #[error("OAuth callback contains a duplicate parameter")] DuplicateParameter, - /// 回调缺少一次性 state。 + /// 回调缺少一次性 state #[error("OAuth callback is missing state")] MissingState, - /// 回调缺少 authorization code。 + /// 回调缺少 authorization code #[error("OAuth callback is missing code")] MissingCode, - /// 一次性 state 与 pending flow 不匹配。 + /// 一次性 state 与 pending flow 不匹配 #[error("OAuth callback state mismatch")] StateMismatch, - /// 授权服务器返回 `access_denied`。 + /// 授权服务器返回 `access_denied` #[error("OAuth authorization was denied")] AccessDenied, - /// 授权服务器返回其他回调错误。 + /// 授权服务器返回其他回调错误 #[error("OAuth authorization callback was rejected")] ProviderRejected, } -/// 未验证 token 集合无法通过凭据边界的原因。 +/// 未验证 token 集合无法通过凭据边界的原因 #[derive(Debug, Error, Clone, Copy, PartialEq, Eq)] pub enum VerificationFailure { - /// 未注入 JWT/JWKS 或权威 user-info 校验器。 + /// 未注入 JWT/JWKS 或权威 user-info 校验器 #[error("token verification is unavailable; refusing unverified credentials")] Unavailable, - /// Authorization Code 响应缺少必需的 ID token。 + /// Authorization Code 响应缺少必需的 ID token #[error("authorization-code response is missing an ID token")] MissingIdToken, - /// 校验器拒绝了签名、issuer、audience、nonce、过期时间或身份。 + /// 校验器拒绝了签名、issuer、audience、nonce、过期时间或身份 #[error("token verification failed")] Rejected, - /// Authorization Code 流程未经 ID token 验证。 + /// Authorization Code 流程未经 ID token 验证 #[error("authorization-code flow requires verified ID-token evidence")] WrongEvidence, } -/// 不含密钥的 OAuth 协议错误。 +/// 不含密钥的 OAuth 协议错误 #[derive(Debug, Error)] pub enum OAuthError { - /// 本地信任或回调配置非法。 + /// 本地信任或回调配置非法 #[error(transparent)] Configuration(#[from] ConfigError), - /// 可替换 transport 失败;不保留原始错误信息。 + /// 可替换 transport 失败;不保留原始错误信息 #[error("OAuth transport failed during {operation:?}: {kind:?}")] Transport { - /// 执行中的操作。 + /// 执行中的操作 operation: OAuthOperation, - /// 脱敏的发送状态分类。 + /// 脱敏的发送状态分类 kind: TransportFailureKind, }, - /// 非成功 HTTP 响应,且没有可识别的 OAuth 错误码。 + /// 非成功 HTTP 响应,且没有可识别的 OAuth 错误码 #[error("OAuth endpoint returned HTTP {status} during {operation:?}")] HttpStatus { - /// 执行中的操作。 + /// 执行中的操作 operation: OAuthOperation, - /// HTTP 状态码。 + /// HTTP 状态码 status: u16, }, - /// 可识别的 OAuth 错误响应。 + /// 可识别的 OAuth 错误响应 #[error("OAuth endpoint returned {code:?} during {operation:?}")] Server { - /// 执行中的操作。 + /// 执行中的操作 operation: OAuthOperation, - /// HTTP 状态码。 + /// HTTP 状态码 status: u16, - /// 稳定 OAuth 错误码。 + /// 稳定 OAuth 错误码 code: OAuthErrorCode, }, - /// token 交换前回调即被拒绝。 + /// token 交换前回调即被拒绝 #[error(transparent)] Callback(#[from] CallbackRejection), - /// wire 响应违反严格解析契约。 + /// wire 响应违反严格解析契约 #[error("{operation:?} protocol violation: {violation}")] Protocol { - /// 解析中的操作。 + /// 解析中的操作 operation: OAuthOperation, - /// 按字段定位的脱敏原因。 + /// 按字段定位的脱敏原因 violation: ProtocolViolation, }, - /// token 集合未通过强制验证边界。 + /// token 集合未通过强制验证边界 #[error(transparent)] Verification(#[from] VerificationFailure), - /// 流程启动前无法获得安全随机数。 + /// 流程启动前无法获得安全随机数 #[error("secure OAuth entropy is unavailable")] EntropyUnavailable, } impl OAuthError { - /// 返回供编排决策使用的低基数失败类别。 + /// 返回供编排决策使用的低基数失败类别 #[must_use] pub fn class(&self) -> FailureClass { match self { diff --git a/backend/crates/providers/xai/src/credential/http.rs b/backend/crates/providers/xai/src/credential/http.rs index 582922746..ce6f4a916 100644 --- a/backend/crates/providers/xai/src/credential/http.rs +++ b/backend/crates/providers/xai/src/credential/http.rs @@ -1,3 +1,5 @@ +//! xAI OAuth HTTP 传输端口,以及区分公开值和敏感值的请求响应类型 + use std::fmt; use std::future::Future; use std::pin::Pin; @@ -7,16 +9,16 @@ use zeroize::Zeroizing; use crate::SecretValue; -/// OAuth 协议所需的 HTTP 方法。 +/// OAuth 协议所需的 HTTP 方法 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HttpMethod { - /// 只读的发现请求。 + /// 只读的发现请求 Get, - /// form 编码的 OAuth 请求。 + /// form 编码的 OAuth 请求 Post, } -/// 值为公开协议元数据的请求头。 +/// 值为公开协议元数据的请求头 #[derive(Debug, Clone, PartialEq, Eq)] pub struct HttpHeader { name: &'static str, @@ -24,7 +26,7 @@ pub struct HttpHeader { } impl HttpHeader { - /// 创建公开请求头。 + /// 创建公开请求头 #[must_use] pub fn new(name: &'static str, value: impl Into) -> Self { Self { @@ -33,30 +35,30 @@ impl HttpHeader { } } - /// 返回请求头名称。 + /// 返回请求头名称 #[must_use] pub fn name(&self) -> &'static str { self.name } - /// 返回公开的请求头值。 + /// 返回公开的请求头值 #[must_use] pub fn value(&self) -> &str { &self.value } } -/// 公开或敏感的 form 字段值。 +/// 公开或敏感的 form 字段值 #[derive(Clone)] pub enum FormValue { - /// 非敏感的协议元数据。 + /// 非敏感的协议元数据 Public(String), - /// authorization code、verifier 或 token。 + /// authorization code、verifier 或 token Secret(SecretValue), } impl FormValue { - /// 在 HTTP 编码边界暴露值。 + /// 在 HTTP 编码边界暴露值 #[must_use] pub fn expose(&self) -> &str { match self { @@ -75,7 +77,7 @@ impl fmt::Debug for FormValue { } } -/// 单个 `application/x-www-form-urlencoded` 字段。 +/// 单个 `application/x-www-form-urlencoded` 字段 #[derive(Debug, Clone)] pub struct FormField { name: &'static str, @@ -83,7 +85,7 @@ pub struct FormField { } impl FormField { - /// 创建公开字段。 + /// 创建公开字段 #[must_use] pub fn public(name: &'static str, value: impl Into) -> Self { Self { @@ -92,7 +94,7 @@ impl FormField { } } - /// 创建敏感字段。 + /// 创建敏感字段 #[must_use] pub fn secret(name: &'static str, value: SecretValue) -> Self { Self { @@ -101,20 +103,20 @@ impl FormField { } } - /// 返回 form 字段名。 + /// 返回 form 字段名 #[must_use] pub fn name(&self) -> &'static str { self.name } - /// 返回 form 字段值。 + /// 返回 form 字段值 #[must_use] pub fn value(&self) -> &FormValue { &self.value } } -/// 与 transport 无关的 OAuth HTTP 请求。 +/// 与 transport 无关的 OAuth HTTP 请求 #[derive(Debug, Clone)] pub struct OAuthHttpRequest { method: HttpMethod, @@ -145,39 +147,40 @@ impl OAuthHttpRequest { } } - /// 返回请求方法。 + /// 返回请求方法 #[must_use] pub const fn method(&self) -> HttpMethod { self.method } - /// 返回已校验的目标 URL。 + /// 返回已校验的目标 URL #[must_use] pub fn url(&self) -> &Url { &self.url } - /// 返回公开请求头。 + /// 返回公开请求头 #[must_use] pub fn headers(&self) -> &[HttpHeader] { &self.headers } - /// 返回 form 字段。transport 实现不得记录这些值。 + /// 返回 form 字段 + /// transport 实现不得记录这些值 #[must_use] pub fn form(&self) -> &[FormField] { &self.form } } -/// 响应体会被清零,且不出现在 debug 输出中。 +/// 响应体会被清零,且不出现在 debug 输出中 pub struct OAuthHttpResponse { status: u16, body: Zeroizing>, } impl OAuthHttpResponse { - /// 由状态码与原始响应体构造响应。 + /// 由状态码与原始响应体构造响应 #[must_use] pub fn new(status: u16, body: impl Into>) -> Self { Self { @@ -186,7 +189,7 @@ impl OAuthHttpResponse { } } - /// 返回 HTTP 状态码。 + /// 返回 HTTP 状态码 #[must_use] pub const fn status(&self) -> u16 { self.status @@ -208,48 +211,48 @@ impl fmt::Debug for OAuthHttpResponse { } } -/// 区分 HTTP 失败确定发生在发送前,还是可能已消费一次性授权凭据。 +/// 区分 HTTP 失败确定发生在发送前,还是可能已消费一次性授权凭据 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TransportFailureKind { - /// 未发送任何请求字节,协调方可安全重试。 + /// 未发送任何请求字节,协调方可安全重试 NotSent, - /// 服务端可能已消费 code 或轮换的 refresh token。 + /// 服务端可能已消费 code 或轮换的 refresh token Ambiguous, - /// 发生超时,发送状态未知。 + /// 发生超时,发送状态未知 Timeout, - /// 在收到有效响应前 TLS 建立或校验失败。 + /// 在收到有效响应前 TLS 建立或校验失败 Tls, } -/// 有意保持低基数且不含密钥的 transport 错误。 +/// 有意保持低基数且不含密钥的 transport 错误 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct TransportFailure { kind: TransportFailureKind, } impl TransportFailure { - /// 创建 transport 失败,不保留可能敏感的第三方错误信息。 + /// 创建 transport 失败,不保留可能敏感的第三方错误信息 #[must_use] pub const fn new(kind: TransportFailureKind) -> Self { Self { kind } } - /// 返回发送状态分类。 + /// 返回发送状态分类 #[must_use] pub const fn kind(self) -> TransportFailureKind { self.kind } } -/// OAuth HTTP transport 返回的 Future。 +/// OAuth HTTP transport 返回的 Future pub type TransportFuture<'a> = Pin> + Send + 'a>>; -/// OAuth 协议请求的可替换 HTTP 边界。 +/// OAuth 协议请求的可替换 HTTP 边界 /// /// 实现方需在本 crate 之外保证 TLS、有界响应体与 proxy/IP 亲和性, -/// 且不得记录 form 值或响应体。 +/// 且不得记录 form 值或响应体 pub trait OAuthHttpTransport: Send + Sync { - /// 执行一次请求,不做隐式重试。 + /// 执行一次请求,不做隐式重试 fn execute(&self, request: OAuthHttpRequest) -> TransportFuture<'_>; } diff --git a/backend/crates/providers/xai/src/credential/import.rs b/backend/crates/providers/xai/src/credential/import.rs index 68715c498..acad1e6db 100644 --- a/backend/crates/providers/xai/src/credential/import.rs +++ b/backend/crates/providers/xai/src/credential/import.rs @@ -1,4 +1,4 @@ -//! 已有 Grok Build OAuth token 的安全归一化边界。 +//! 已有 Grok Build OAuth token 的安全归一化边界 use std::collections::HashSet; use std::fmt; @@ -28,7 +28,7 @@ const MAX_IMPORT_ACCOUNTS: usize = 200; const MAX_ACCOUNT_NAME_BYTES: usize = 512; const MAX_EMAIL_BYTES: usize = 2_048; -/// 管理导入 adapter 归一化后的 OAuth 候选;所有秘密字段均从 `Debug` 隐去。 +/// 管理导入 adapter 归一化后的 OAuth 候选;所有秘密字段均从 `Debug` 隐去 pub struct GrokOAuthImportCandidate { access_token: SecretValue, refresh_token: SecretValue, @@ -41,7 +41,7 @@ pub struct GrokOAuthImportCandidate { access_token_expires_at: DateTime, } -/// 导入候选的三个 OAuth secret;该类型不实现 `Debug` 或序列化。 +/// 导入候选的三个 OAuth secret;该类型不实现 `Debug` 或序列化 pub struct GrokOAuthImportTokens { access_token: SecretValue, refresh_token: SecretValue, @@ -62,7 +62,7 @@ impl GrokOAuthImportTokens { } } - /// 构造不含 ID token 的导入;仅允许过期 AT 经 RT 刷新后走 user-info 验证。 + /// 构造不含 ID token 的导入;仅允许过期 AT 经 RT 刷新后走 user-info 验证 #[must_use] pub fn without_id_token(access_token: SecretValue, refresh_token: SecretValue) -> Self { Self { @@ -73,7 +73,7 @@ impl GrokOAuthImportTokens { } } -/// 导入候选的非身份 metadata;client 与 scope 仍按敏感材料处理,不实现 `Debug`。 +/// 导入候选的非身份 metadata;client 与 scope 仍按敏感材料处理,不实现 `Debug` pub struct GrokOAuthImportMetadata { token_type: String, client_id: String, @@ -110,7 +110,7 @@ impl GrokOAuthImportMetadata { } } -/// OAuth 账号文档中的一个 xAI account。 +/// OAuth 账号文档中的一个 xAI account pub struct GrokOAuthImportEntry { model_access: Option, name: String, @@ -157,16 +157,16 @@ impl fmt::Debug for GrokOAuthImportEntry { } } -/// Provider-owned xAI OAuth 账号导入。 +/// Provider-owned xAI OAuth 账号导入 pub struct GrokOAuthImportDocument { entries: Vec, } impl GrokOAuthImportDocument { - /// 从外部 JSON 提取 xAI OAuth 认证字段。 + /// 从外部 JSON 提取 xAI OAuth 认证字段 /// - /// 独立代理 URL 随账号导入;其他展示 metadata 不参与认证。 - /// 实际 token 仍须通过官方 refresh/user-info 验证;API Key 不能混入 OAuth 条目。 + /// 独立代理 URL 随账号导入;其他展示 metadata 不参与认证 + /// 实际 token 仍须通过官方 refresh/user-info 验证;API Key 不能混入 OAuth 条目 pub fn parse_json(document: &[u8]) -> Result { Self::parse_json_with_proxy(document, None) } @@ -198,7 +198,7 @@ impl GrokOAuthImportDocument { } } // 同一 refresh token 的重复条目只保留首个:RT exchange 会作废旧 - // grant,重复条目逐条交换会让先完成的轮换被后续条目作废。 + // grant,重复条目逐条交换会让先完成的轮换被后续条目作废 let mut seen_refresh_tokens = HashSet::new(); entries.retain(|entry| { seen_refresh_tokens.insert(entry.candidate.refresh_token.expose().to_owned()) @@ -370,7 +370,7 @@ fn parse_source_expiry(value: &str) -> Option> { } impl GrokOAuthImportCandidate { - /// 构造尚未获得信任的导入候选。 + /// 构造尚未获得信任的导入候选 #[must_use] pub fn new(tokens: GrokOAuthImportTokens, metadata: GrokOAuthImportMetadata) -> Self { Self { @@ -459,7 +459,7 @@ impl fmt::Debug for GrokOAuthImportCandidate { } } -/// OAuth 导入失败;错误中只保留固定字段名与低基数原因。 +/// OAuth 导入失败;错误中只保留固定字段名与低基数原因 #[derive(Debug, thiserror::Error)] pub enum GrokOAuthImportError { #[error("invalid imported OAuth field `{0}`")] diff --git a/backend/crates/providers/xai/src/credential/mod.rs b/backend/crates/providers/xai/src/credential/mod.rs index 5a6e7d0ff..f30be95c2 100644 --- a/backend/crates/providers/xai/src/credential/mod.rs +++ b/backend/crates/providers/xai/src/credential/mod.rs @@ -1,4 +1,4 @@ -//! Grok Build OAuth credential 与运行时 selector。 +//! Grok Build OAuth credential 与运行时 selector mod authorization_code; mod catalog; diff --git a/backend/crates/providers/xai/src/credential/oidc_verifier.rs b/backend/crates/providers/xai/src/credential/oidc_verifier.rs index e91d67c0f..792939263 100644 --- a/backend/crates/providers/xai/src/credential/oidc_verifier.rs +++ b/backend/crates/providers/xai/src/credential/oidc_verifier.rs @@ -1,4 +1,4 @@ -//! xAI 官方 OIDC token 的生产校验边界。 +//! xAI 官方 OIDC token 的生产校验边界 use std::collections::{HashMap, HashSet}; use std::fmt; @@ -34,7 +34,7 @@ const MAX_SUBJECT_BYTES: usize = 512; const MAX_KEY_ID_BYTES: usize = 128; const MAX_AUDIENCES: usize = 16; -/// 使用官方 JWKS 与 user-info endpoint 的严格生产 verifier。 +/// 使用官方 JWKS 与 user-info endpoint 的严格生产 verifier pub struct ReqwestOidcTokenVerifier { egress: crate::transport::egress::EgressCache< Option, @@ -51,11 +51,11 @@ struct OidcEgress { impl ReqwestOidcTokenVerifier { /// 构建 verifier;使用显式 endpoint policy 与 JWKS cache TTL,只允许访问 - /// `auth.x.ai` 官方验证端点。 + /// `auth.x.ai` 官方验证端点 /// /// # Errors /// - /// HTTP client 初始化失败时返回 transport build error。 + /// HTTP client 初始化失败时返回 transport build error pub fn new( endpoint_policy: Arc, cache_ttl: Duration, @@ -258,7 +258,7 @@ impl ReqwestOidcTokenVerifier { return Err(VerificationFailure::Rejected); } - // 每个出口持锁获取,冷启动和轮换共用单飞;不同出口互不阻塞。 + // 每个出口持锁获取,冷启动和轮换共用单飞;不同出口互不阻塞 let mut cache = egress.jwks_cache.lock().await; let cache_is_fresh = cache.uri.as_ref() == Some(jwks_uri) && cache @@ -269,7 +269,7 @@ impl ReqwestOidcTokenVerifier { return key.decoding_key(); } - // fresh cache 未命中 kid 时只强制刷新一次,以支持官方密钥轮换。 + // fresh cache 未命中 kid 时只强制刷新一次,以支持官方密钥轮换 let keys = self.fetch_jwks(jwks_uri, &egress.client).await?; cache.replace(jwks_uri.clone(), keys); return cache @@ -279,7 +279,7 @@ impl ReqwestOidcTokenVerifier { .decoding_key(); } - // 过期缓存不会在网络或协议失败时作为 stale fallback 使用。 + // 过期缓存不会在网络或协议失败时作为 stale fallback 使用 let keys = self.fetch_jwks(jwks_uri, &egress.client).await?; cache.replace(jwks_uri.clone(), keys); cache @@ -384,7 +384,7 @@ impl CachedJwk { } } -// RFC 7517 要求忽略 JWKS 与 JWK 的未知成员;字段本身的强校验在 parse_jwks。 +// RFC 7517 要求忽略 JWKS 与 JWK 的未知成员;字段本身的强校验在 parse_jwks #[derive(Deserialize)] struct JwksWire { keys: Vec, diff --git a/backend/crates/providers/xai/src/credential/pkce.rs b/backend/crates/providers/xai/src/credential/pkce.rs index 609120f4e..ccfc911db 100644 --- a/backend/crates/providers/xai/src/credential/pkce.rs +++ b/backend/crates/providers/xai/src/credential/pkce.rs @@ -1,3 +1,5 @@ +//! xAI OAuth PKCE 验证码生成、校验与 S256 挑战值计算 + use base64::Engine as _; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use sha2::{Digest as _, Sha256}; diff --git a/backend/crates/providers/xai/src/credential/refresh.rs b/backend/crates/providers/xai/src/credential/refresh.rs index ee7ec128f..9f84ef1fd 100644 --- a/backend/crates/providers/xai/src/credential/refresh.rs +++ b/backend/crates/providers/xai/src/credential/refresh.rs @@ -1,4 +1,4 @@ -//! xAI OAuth refresh state machine;并发由 lease、写回由 credential revision CAS 保证。 +//! xAI OAuth refresh state machine;并发由 lease、写回由 credential revision CAS 保证 use std::collections::BTreeSet; use std::fmt; @@ -39,13 +39,13 @@ const MAX_REFRESH_EXCLUSIONS: usize = 400; const PROVIDER_NAME: &str = "xai"; const MAX_SECRET_BYTES: usize = 64 * 1_024; const DISCOVERY_NEGATIVE_CACHE_TTL: Duration = Duration::from_secs(5); -/// 连续失败计数窗口;静默满窗后计数过期归零。 +/// 连续失败计数窗口;静默满窗后计数过期归零 const REFRESH_BACKOFF_WINDOW: Duration = Duration::from_secs(30 * 60); -/// 窗口内先执行五次指数退避,再进入固定恢复周期。 +/// 窗口内先执行五次指数退避,再进入固定恢复周期 const REFRESH_BACKOFF_MAX_ATTEMPTS: u32 = 5; -/// 耗尽指数退避后的 OAuth 恢复周期。 +/// 耗尽指数退避后的 OAuth 恢复周期 const REFRESH_RECOVERY_DELAY: Duration = Duration::from_secs(10 * 60); -/// 过期 AT 仍允许 RT 恢复的最长窗口。 +/// 过期 AT 仍允许 RT 恢复的最长窗口 const REFRESH_RECOVERY_WINDOW: Duration = Duration::from_secs(24 * 60 * 60); fn refresh_recovery_deadline(access_token_expires_at: Option) -> Option { @@ -79,7 +79,7 @@ fn bounded_refresh_retry_at( bounded } -/// 一个到期且已按 revision 读取明文 RT 的 xAI account。 +/// 一个到期且已按 revision 读取明文 RT 的 xAI account pub struct DueGrokCredential { account_id: ProviderAccountId, credential_revision: CredentialRevision, @@ -307,14 +307,14 @@ pub enum GrokCredentialRefreshOutcome { }, } -/// 一次 401 后的同账号 OAuth 恢复结论。 +/// 一次 401 后的同账号 OAuth 恢复结论 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum GrokCredentialRecoveryOutcome { - /// AT/RT 已由 CAS 刷新,或并发刷新已先完成。 + /// AT/RT 已由 CAS 刷新,或并发刷新已先完成 Recovered, - /// RT 或账号已被权威判定为不可恢复。 + /// RT 或账号已被权威判定为不可恢复 Rejected, - /// 临时错误或 lease 竞争,本次不改变账号认证状态。 + /// 临时错误或 lease 竞争,本次不改变账号认证状态 Unavailable, } @@ -420,7 +420,7 @@ impl GrokCredentialRefreshService { self.refresh_one_with_policy(credential, policy).await } - /// 手工刷新一次原子读取的当前 credential;只返回 Provider 验证后的 CAS command,不写 Store。 + /// 手工刷新一次原子读取的当前 credential;只返回 Provider 验证后的 CAS command,不写 Store pub async fn prepare_manual_refresh( &self, current: LoadedCredential, @@ -544,7 +544,7 @@ impl GrokCredentialRefreshService { { return Err(GrokCredentialRefreshError::Preparation); } - // A failed direct discovery must not prevent accounts on other exits from refreshing. + // 直连发现失败不能阻止其他出口上的账号刷新 credentials.retain(|credential| { if credential.outbound_proxy.is_some() { return true; @@ -752,7 +752,7 @@ impl GrokCredentialRefreshService { }; // 凭据提交后重新读取账号事实;刷新期间切换出口不会改变 credential CAS, - // 但后续目录请求必须使用已提交的当前出口,不能复用旧的 token-only 请求。 + // 但后续目录请求必须使用已提交的当前出口,不能复用旧的 token-only 请求 let _ = self.catalog.refresh_account_catalog(&account_id).await; let _ = self .credential_state diff --git a/backend/crates/providers/xai/src/credential/repository.rs b/backend/crates/providers/xai/src/credential/repository.rs index 9a946ab51..9a82aa20f 100644 --- a/backend/crates/providers/xai/src/credential/repository.rs +++ b/backend/crates/providers/xai/src/credential/repository.rs @@ -1,4 +1,4 @@ -//! xAI 对中立 [`ProviderAccountStore`] 的明文 OAuth adapter。 +//! xAI 对中立 [`ProviderAccountStore`] 的明文 OAuth adapter use std::collections::BTreeSet; use std::sync::Arc; @@ -33,7 +33,7 @@ const MAX_REASON_BYTES: usize = 2_048; const MAX_SECRET_BYTES: usize = 64 * 1_024; const MAX_IMPORT_BATCH: usize = 200; -/// 已跨过官方 OAuth/OIDC 验证边界、等待转为 Core 创建命令的账号。 +/// 已跨过官方 OAuth/OIDC 验证边界、等待转为 Core 创建命令的账号 pub struct VerifiedGrokAccount { pub account_id: ProviderAccountId, pub name: String, @@ -62,7 +62,7 @@ impl std::fmt::Debug for VerifiedGrokAccount { } } -/// Provider-owned OAuth 账号明文导出;Debug 永不展开凭据。 +/// Provider-owned OAuth 账号明文导出;Debug 永不展开凭据 pub struct GrokAccountExport(Value); impl GrokAccountExport { @@ -89,7 +89,7 @@ impl std::fmt::Debug for GrokAccountExport { } } -/// 已从明文 Provider JSON 完整验证的 xAI account。 +/// 已从明文 Provider JSON 完整验证的 xAI account pub(crate) struct LoadedGrokCredential { pub(crate) account: ProviderAccount, pub(crate) access_token: SecretValue, @@ -99,7 +99,7 @@ pub(crate) struct LoadedGrokCredential { pub(crate) refresh_token_expires_at: Option>, } -/// 管理端可读取的 xAI credential 生命周期事实;不包含任何 OAuth secret。 +/// 管理端可读取的 xAI credential 生命周期事实;不包含任何 OAuth secret #[derive(Clone, Debug, PartialEq, Eq)] pub struct GrokCredentialLifecycle { account_id: ProviderAccountId, @@ -138,12 +138,12 @@ impl std::fmt::Debug for LoadedGrokCredential { } } -/// 无状态的 xAI Admin command preparer;不读写持久层。 +/// 无状态的 xAI Admin command preparer;不读写持久层 #[derive(Debug, Default, Clone, Copy)] pub struct GrokCredentialAdmin; impl GrokCredentialAdmin { - /// 验证官方 OAuth account 并生成一次性 Core 创建命令。 + /// 验证官方 OAuth account 并生成一次性 Core 创建命令 pub fn prepare_import( &self, input: &CreateGrokCredential, @@ -182,7 +182,7 @@ impl GrokCredentialAdmin { }) } - /// 把已验证 token 的 Provider 生命周期事实一次性投影为 Core 创建命令。 + /// 把已验证 token 的 Provider 生命周期事实一次性投影为 Core 创建命令 pub fn prepare_verified_account( &self, input: &VerifiedGrokAccount, @@ -223,7 +223,7 @@ impl GrokCredentialAdmin { }) } - /// 将 Store 读取的 xAI 明文账号导出为规范 OAuth account bundle v1。 + /// 将 Store 读取的 xAI 明文账号导出为规范 OAuth account bundle v1 pub fn export_oauth_bundle( &self, accounts: &[LoadedCredential], @@ -302,7 +302,7 @@ impl GrokCredentialAdmin { }))) } - /// 验证身份不可重绑并生成完整 profile + credential CAS 命令。 + /// 验证身份不可重绑并生成完整 profile + credential CAS 命令 pub fn prepare_rotation( &self, input: &RotateManagedGrokCredential, @@ -319,7 +319,7 @@ impl GrokCredentialAdmin { } } -/// Provider 层只验证 xAI wire,并通过 Core port 持久化;这里没有 SQL 或加密。 +/// Provider 层只验证 xAI wire,并通过 Core port 持久化;这里没有 SQL 或加密 #[derive(Clone)] pub struct GrokCredentialRepository { store: Arc, @@ -340,7 +340,7 @@ impl GrokCredentialRepository { Self { store } } - /// 按 id 读取账号事实,不做 enabled 过滤;诊断选择用它回补停用账号候选。 + /// 按 id 读取账号事实,不做 enabled 过滤;诊断选择用它回补停用账号候选 pub(crate) async fn account_by_id( &self, account_id: &ProviderAccountId, @@ -361,7 +361,7 @@ impl GrokCredentialRepository { .map_err(map_store_error) } - /// 读取不含 secret 的生命周期投影;Provider 仍是明文 credential JSON 的唯一解析者。 + /// 读取不含 secret 的生命周期投影;Provider 仍是明文 credential JSON 的唯一解析者 pub async fn read_lifecycle( &self, account_id: &ProviderAccountId, @@ -374,7 +374,7 @@ impl GrokCredentialRepository { }) } - /// Runtime refresh 用 credential revision CAS 原子轮换 OAuth token pair。 + /// Runtime refresh 用 credential revision CAS 原子轮换 OAuth token pair pub(crate) async fn rotate_oauth_credential( &self, input: &RotateGrokCredential, @@ -407,7 +407,7 @@ impl GrokCredentialRepository { }) } - /// 临时失败时只推进持久刷新时刻,不改变 OAuth material 或账号可用性。 + /// 临时失败时只推进持久刷新时刻,不改变 OAuth material 或账号可用性 pub(crate) async fn defer_refresh( &self, account_id: &ProviderAccountId, @@ -449,7 +449,7 @@ impl GrokCredentialRepository { } } - /// 用同一 credential revision fence 更新可用性。 + /// 用同一 credential revision fence 更新可用性 pub async fn update_state( &self, input: &UpdateGrokCredentialState, @@ -483,7 +483,7 @@ impl GrokCredentialRepository { .map_err(map_store_error) } - /// 读取并完整校验一个 revision 对应的明文 OAuth credential。 + /// 读取并完整校验一个 revision 对应的明文 OAuth credential pub(crate) async fn load( &self, account_id: &ProviderAccountId, @@ -497,7 +497,7 @@ impl GrokCredentialRepository { loaded_from_core(loaded) } - /// 读取 xAI Provider 的全部账号;credential 逐行按 revision fence 加载。 + /// 读取 xAI Provider 的全部账号;credential 逐行按 revision fence 加载 pub(crate) async fn list_loaded_for_provider( &self, ) -> Result, GrokCredentialRepositoryError> { @@ -885,7 +885,7 @@ fn to_system_time(value: DateTime) -> SystemTime { value.into() } -/// xAI Provider account adapter 的脱敏错误。 +/// xAI Provider account adapter 的脱敏错误 #[derive(Debug, Error, Clone, PartialEq, Eq)] pub enum GrokCredentialRepositoryError { #[error("invalid xAI credential input: {0}")] diff --git a/backend/crates/providers/xai/src/credential/secret.rs b/backend/crates/providers/xai/src/credential/secret.rs index d61028ea4..a92e2b9b2 100644 --- a/backend/crates/providers/xai/src/credential/secret.rs +++ b/backend/crates/providers/xai/src/credential/secret.rs @@ -1,19 +1,21 @@ +//! xAI 敏感字符串的脱敏展示、内存清理与恒定时间比较 + use std::fmt; use zeroize::Zeroizing; -/// 内存中的 secret;debug 输出恒为脱敏,缓冲区在 drop 时清零。 +/// 内存中的 secret;debug 输出恒为脱敏,缓冲区在 drop 时清零 #[derive(Clone)] pub struct SecretValue(Zeroizing); impl SecretValue { - /// 包装持有所有权的 secret 值。 + /// 包装持有所有权的 secret 值 #[must_use] pub fn new(value: impl Into) -> Self { Self(Zeroizing::new(value.into())) } - /// 仅在明确的协议或 transport 边界暴露 secret。 + /// 仅在明确的协议或 transport 边界暴露 secret #[must_use] pub fn expose(&self) -> &str { self.0.as_str() diff --git a/backend/crates/providers/xai/src/credential/selector.rs b/backend/crates/providers/xai/src/credential/selector.rs index c09485c78..375eaf7db 100644 --- a/backend/crates/providers/xai/src/credential/selector.rs +++ b/backend/crates/providers/xai/src/credential/selector.rs @@ -1,4 +1,4 @@ -//! xAI OAuth account 选择、Redis lease 与失败反馈。 +//! xAI OAuth account 选择、Redis lease 与失败反馈 use std::fmt; use std::sync::Arc; @@ -53,7 +53,7 @@ struct RuntimeCooldown { scope: RuntimeCooldownScope, } -/// 仅经 Core account port、TTL catalog cache 和 Redis lease 选择一个 OAuth session。 +/// 仅经 Core account port、TTL catalog cache 和 Redis lease 选择一个 OAuth session pub struct GrokAccountSessionSelector { waiting: ConcurrencyWaitQueue, provider_kind: ProviderKind, @@ -102,7 +102,7 @@ impl GrokAccountSessionSelector { 'refresh: loop { let diagnostic = request.eligibility() == AccountEligibilityPolicy::BypassForDiagnostic; // store 侧常规调度列表不包含停用账号;管理端诊断要对固定账号执行真实上游 - // 验证,这里把不在列表里的 required 账号显式补回候选。 + // 验证,这里把不在列表里的 required 账号显式补回候选 let mut accounts = self .repository .list_accounts_for_provider() @@ -135,7 +135,7 @@ impl GrokAccountSessionSelector { let catalog_eligible = if diagnostic { accounts } else { - // 目录支持度按套餐 scope 去重后并发批量读取,避免逐账号串行往返。 + // 目录支持度按套餐 scope 去重后并发批量读取,避免逐账号串行往返 let account_scopes = accounts .iter() .map(|account| GrokCatalogScope::for_account(account).ok()) @@ -373,7 +373,7 @@ impl GrokAccountSessionSelector { if capacity_denied { return Err(GrokSessionSelectorError::CapacityUnavailable { retry_after }); } - // 钉死账号时只看该账号,否则取最早退出 cooldown 的候选。 + // 钉死账号时只看该账号,否则取最早退出 cooldown 的候选 let cooled = match request.required_account() { Some(required) => runtime_cooldowns.get(required).copied(), None => runtime_cooldowns @@ -407,7 +407,7 @@ impl GrokAccountSessionSelector { ) -> std::collections::BTreeMap { let now = SystemTime::now(); let model_scope = ProviderCooldownScope::upstream_model(upstream_model.clone()); - // 账号级和当前模型级冷却都并发批量读取,避免逐账号串行往返。 + // 账号级和当前模型级冷却都并发批量读取,避免逐账号串行往返 let account_reads = futures::future::join_all( accounts .iter() @@ -426,7 +426,7 @@ impl GrokAccountSessionSelector { if let Ok(Some(cooldown)) = account_read && cooldown.until() > now { - // 账号级限流冷却跨凭据轮换保留:上游限流不因本地凭据更换解除。 + // 账号级限流冷却跨凭据轮换保留:上游限流不因本地凭据更换解除 insert_runtime_cooldown( &mut cooled, account.id(), @@ -579,7 +579,7 @@ impl GrokSessionSelector for GrokAccountSessionSelector { } // bare 402 没有结构化 quota code,不能证明账号额度耗尽: // 只写短期账号级 runtime cooldown(TransientBackoff), - // 不持久化 QuotaExhausted(结构化 QuotaExhausted/FreeQuotaExhausted 走上面)。 + // 不持久化 QuotaExhausted(结构化 QuotaExhausted/FreeQuotaExhausted 走上面) GrokCredentialFailure::PaymentRequired { retry_after } => { let retry_after = retry_after .unwrap_or(PAYMENT_REQUIRED_COOLDOWN) @@ -588,7 +588,7 @@ impl GrokSessionSelector for GrokAccountSessionSelector { } // xAI 免费模型额度按该模型滚动窗口恢复:限流写 model-scoped // runtime cooldown(Redis 跨重启保留),不进入持久化账号状态, - // 不阻止该账号服务其他模型。 + // 不阻止该账号服务其他模型 GrokCredentialFailure::ModelQuotaExhausted { upstream_model, retry_after, @@ -685,7 +685,7 @@ impl GrokSessionSelector for GrokAccountSessionSelector { ); } // 已经发出的并发请求可能晚于耗尽事实成功返回;真实成功可以收敛 - // 该事实,但 selector 不会为恢复探测而放行耗尽账号。 + // 该事实,但 selector 不会为恢复探测而放行耗尽账号 if account.quota().access() == QuotaAccessState::Exhausted && let Err(error) = self .repository diff --git a/backend/crates/providers/xai/src/credential/token.rs b/backend/crates/providers/xai/src/credential/token.rs index 04c61b33b..1691be7e2 100644 --- a/backend/crates/providers/xai/src/credential/token.rs +++ b/backend/crates/providers/xai/src/credential/token.rs @@ -1,3 +1,5 @@ +//! xAI OAuth 令牌响应解析、主体身份与已验证令牌值对象 + use std::fmt; use std::time::Duration; @@ -12,7 +14,7 @@ use crate::{ const MAX_TOKEN_BYTES: usize = 64 * 1024; const MAX_TOKEN_LIFETIME_SECONDS: u64 = 366 * 24 * 60 * 60; -/// refresh 请求可选携带的 team principal 字段。 +/// refresh 请求可选携带的 team principal 字段 #[derive(Clone, PartialEq, Eq)] pub struct OAuthPrincipal { principal_type: String, @@ -30,12 +32,12 @@ impl fmt::Debug for OAuthPrincipal { } impl OAuthPrincipal { - /// 创建校验过的 principal 元数据。 + /// 创建校验过的 principal 元数据 /// /// # Errors /// /// 值为空、超长或含控制字符时返回 - /// [`ConfigError::InvalidPrincipal`]。 + /// [`ConfigError::InvalidPrincipal`] pub fn new( principal_type: impl Into, principal_id: impl Into, @@ -52,27 +54,27 @@ impl OAuthPrincipal { }) } - /// 返回官方 principal 类型值。 + /// 返回官方 principal 类型值 #[must_use] pub fn principal_type(&self) -> &str { &self.principal_type } - /// 返回官方 principal 标识。 + /// 返回官方 principal 标识 #[must_use] pub fn principal_id(&self) -> &str { &self.principal_id } } -/// 单次 refresh token 交换的输入。 +/// 单次 refresh token 交换的输入 #[derive(Clone)] pub struct RefreshTokenGrant { refresh_token: SecretValue, } impl RefreshTokenGrant { - /// 为已验证的现有凭据创建 refresh grant。 + /// 为已验证的现有凭据创建 refresh grant #[must_use] pub fn new(refresh_token: SecretValue) -> Self { Self { refresh_token } @@ -92,7 +94,7 @@ impl fmt::Debug for RefreshTokenGrant { } } -/// 已通过强制验证边界的初始 token set。 +/// 已通过强制验证边界的初始 token set #[derive(Clone)] pub struct VerifiedTokenSet { access_token: SecretValue, @@ -119,37 +121,37 @@ impl VerifiedTokenSet { } } - /// 返回已验证的 access token。 + /// 返回已验证的 access token #[must_use] pub fn access_token(&self) -> &SecretValue { &self.access_token } - /// 返回可选的 refresh token。 + /// 返回可选的 refresh token #[must_use] pub fn refresh_token(&self) -> Option<&SecretValue> { self.refresh_token.as_ref() } - /// 返回验证边界保留的签名 ID token;某些官方流程可能不返回。 + /// 返回验证边界保留的签名 ID token;某些官方流程可能不返回 #[must_use] pub fn id_token(&self) -> Option<&SecretValue> { self.id_token.as_ref() } - /// 返回该 token set 实际绑定的 OAuth scope 字符串。 + /// 返回该 token set 实际绑定的 OAuth scope 字符串 #[must_use] pub fn scope(&self) -> &str { &self.scope } - /// 返回服务端下发的有效期。 + /// 返回服务端下发的有效期 #[must_use] pub const fn expires_in(&self) -> Option { self.expires_in } - /// 返回可信身份验证证据。 + /// 返回可信身份验证证据 #[must_use] pub const fn evidence(&self) -> &VerificationEvidence { &self.evidence @@ -173,8 +175,9 @@ impl fmt::Debug for VerifiedTokenSet { } } -/// 刷新已验证凭据的结果。未返回 refresh token 时,调用方须在 revision CAS 下 -/// 保留原有 refresh token。 +/// 刷新已验证凭据的结果 +/// 未返回 refresh token 时,调用方须在 revision CAS 下 +/// 保留原有 refresh token #[derive(Clone)] pub struct RefreshedTokenSet { access_token: SecretValue, @@ -183,19 +186,19 @@ pub struct RefreshedTokenSet { } impl RefreshedTokenSet { - /// 返回替换后的 access token。 + /// 返回替换后的 access token #[must_use] pub fn access_token(&self) -> &SecretValue { &self.access_token } - /// 服务端下发轮换 refresh token 时返回它。 + /// 服务端下发轮换 refresh token 时返回它 #[must_use] pub fn rotated_refresh_token(&self) -> Option<&SecretValue> { self.rotated_refresh_token.as_ref() } - /// 返回替换后 access token 的有效期。 + /// 返回替换后 access token 的有效期 #[must_use] pub const fn expires_in(&self) -> Option { self.expires_in diff --git a/backend/crates/providers/xai/src/credential/types.rs b/backend/crates/providers/xai/src/credential/types.rs index 74f157601..5d3438ec6 100644 --- a/backend/crates/providers/xai/src/credential/types.rs +++ b/backend/crates/providers/xai/src/credential/types.rs @@ -1,4 +1,4 @@ -//! xAI OAuth 账号输入、状态与明文 credential wire。 +//! xAI OAuth 账号输入、状态与明文 credential wire use std::fmt; @@ -15,7 +15,7 @@ use crate::SecretValue; pub const XAI_AUTHENTICATION_KIND_OAUTH: &str = "oauth"; -/// 官方 xAI OAuth token pair;不接受 API Key。 +/// 官方 xAI OAuth token pair;不接受 API Key pub struct GrokOAuthSecret { pub access_token: SecretValue, pub refresh_token: SecretValue, @@ -35,14 +35,14 @@ impl fmt::Debug for GrokOAuthSecret { } } -/// 已由 OIDC 验证边界确认的 xAI 身份与 token 生命周期。 +/// 已由 OIDC 验证边界确认的 xAI 身份与 token 生命周期 pub struct GrokAccountProfile { pub subject: String, pub email: Option, pub upstream_account_id: Option, pub plan_type: Option, pub access_token_expires_at: DateTime, - /// Provider 明确返回时才保存;普通账号导出通常不携带 RT 过期时间。 + /// Provider 明确返回时才保存;普通账号导出通常不携带 RT 过期时间 pub refresh_token_expires_at: Option>, } @@ -63,7 +63,7 @@ impl fmt::Debug for GrokAccountProfile { } } -/// 创建一个明文 OAuth Provider account。 +/// 创建一个明文 OAuth Provider account pub struct CreateGrokCredential { pub account_id: ProviderAccountId, pub name: String, @@ -89,7 +89,7 @@ impl fmt::Debug for CreateGrokCredential { } } -/// 用 credential revision CAS 轮换完整 OAuth token pair。 +/// 用 credential revision CAS 轮换完整 OAuth token pair pub struct RotateGrokCredential { pub account_id: ProviderAccountId, pub expected_revision: CredentialRevision, @@ -97,7 +97,7 @@ pub struct RotateGrokCredential { pub verified_account: GrokAccountProfile, } -/// App 已读取的当前账号与 xAI 已验证的新 OAuth 身份材料。 +/// App 已读取的当前账号与 xAI 已验证的新 OAuth 身份材料 pub struct RotateManagedGrokCredential { pub current: LoadedCredential, pub secret: GrokOAuthSecret, @@ -115,7 +115,7 @@ impl fmt::Debug for RotateManagedGrokCredential { } } -/// xAI 验证后的管理员 rotation;App 只做 Core command 到原子事务的机械映射。 +/// xAI 验证后的管理员 rotation;App 只做 Core command 到原子事务的机械映射 pub struct PreparedGrokCredentialRotation { pub profile: ProviderAccountUpdate, pub credential: CredentialCasUpdate, @@ -162,7 +162,7 @@ impl PreparedGrokCredentialRotation { self } - /// 将 command 与 lease 一起交给 App;返回的 guard 必须活到 CAS 提交结束。 + /// 将 command 与 lease 一起交给 App;返回的 guard 必须活到 CAS 提交结束 #[must_use] pub fn into_parts( self, @@ -179,7 +179,7 @@ impl PreparedGrokCredentialRotation { } } -/// 手工刷新从 token exchange 到数据库 CAS 完成期间持有的 Redis lease。 +/// 手工刷新从 token exchange 到数据库 CAS 完成期间持有的 Redis lease pub struct PreparedGrokCredentialRotationGuard(Option); impl fmt::Debug for PreparedGrokCredentialRotationGuard { @@ -203,7 +203,7 @@ impl fmt::Debug for RotateGrokCredential { } } -/// 用 credential revision fence 更新账号状态。 +/// 用 credential revision fence 更新账号状态 #[derive(Clone, Debug)] pub struct UpdateGrokCredentialState { pub account_id: ProviderAccountId, @@ -214,7 +214,7 @@ pub struct UpdateGrokCredentialState { pub observed_at: DateTime, } -/// 不包含 secret 的写入结果。 +/// 不包含 secret 的写入结果 #[derive(Clone, Debug, PartialEq, Eq)] pub struct GrokCredentialRecord { pub account_id: ProviderAccountId, diff --git a/backend/crates/providers/xai/src/credential/verification.rs b/backend/crates/providers/xai/src/credential/verification.rs index 0da5d7d8b..0ca10bcdc 100644 --- a/backend/crates/providers/xai/src/credential/verification.rs +++ b/backend/crates/providers/xai/src/credential/verification.rs @@ -1,3 +1,5 @@ +//! xAI 令牌验证上下文、验证证据与可替换验证器端口 + use std::fmt; use std::future::Future; use std::pin::Pin; @@ -7,28 +9,29 @@ use url::Url; use crate::{SecretValue, VerificationFailure}; -/// 正在验证首个 access token 的 OAuth 流程。 +/// 正在验证首个 access token 的 OAuth 流程 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum VerificationFlow { - /// Authorization Code + PKCE;必须携带与 nonce 绑定的 ID token。 + /// Authorization Code + PKCE;必须携带与 nonce 绑定的 ID token AuthorizationCode, - /// 已有 token 的导入;必须通过官方 user-info 验证当前 access token。 + /// 已有 token 的导入;必须通过官方 user-info 验证当前 access token CredentialImport, - /// 导入的 AT 已过期并经官方 RT exchange 更新;必须验证官方 user-info。 + /// 导入的 AT 已过期并经官方 RT exchange 更新;必须验证官方 user-info CredentialImportRefreshed, } -/// 注入的校验器报告的可信验证机制。 +/// 注入的校验器报告的可信验证机制 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum VerificationMethod { - /// 完整 JWT/JWKS 校验,含 issuer、audience、过期时间与 nonce。 + /// 完整 JWT/JWKS 校验,含 issuer、audience、过期时间与 nonce IdToken, - /// 用 access token 查询官方权威 user-info。 + /// 用 access token 查询官方权威 user-info UserInfo, } -/// 可信校验器产出的验证证据。身份在 debug 输出中脱敏,且本 crate 不会通过 -/// 未经验证的 JWT 解码推导身份。 +/// 可信校验器产出的验证证据 +/// 身份在 debug 输出中脱敏,且本 crate 不会通过 +/// 未经验证的 JWT 解码推导身份 #[derive(Clone)] pub struct VerificationEvidence { method: VerificationMethod, @@ -36,7 +39,7 @@ pub struct VerificationEvidence { } impl VerificationEvidence { - /// 完整 ID token 校验通过后创建证据。 + /// 完整 ID token 校验通过后创建证据 #[must_use] pub fn id_token(subject: String) -> Self { Self { @@ -45,7 +48,7 @@ impl VerificationEvidence { } } - /// 官方权威 user-info 查询通过后创建证据。 + /// 官方权威 user-info 查询通过后创建证据 #[must_use] pub fn user_info(subject: String) -> Self { Self { @@ -54,13 +57,13 @@ impl VerificationEvidence { } } - /// 返回验证机制。 + /// 返回验证机制 #[must_use] pub const fn method(&self) -> VerificationMethod { self.method } - /// 在凭据构造边界暴露已验证的 subject。 + /// 在凭据构造边界暴露已验证的 subject #[must_use] pub fn subject(&self) -> &str { self.subject.expose() @@ -77,7 +80,7 @@ impl fmt::Debug for VerificationEvidence { } } -/// 传给 token 校验器的不可变信任上下文。 +/// 传给 token 校验器的不可变信任上下文 #[derive(Debug, Clone, Copy)] pub struct TokenVerificationContext<'a> { flow: VerificationFlow, @@ -112,8 +115,8 @@ impl<'a> TokenVerificationContext<'a> { } } - /// 绑定 OAuth owner 选择的账号出口,供公开的 [`TokenVerifier`] 实现遵守同一代理合同。 - /// 出口只决定路由,不改变 issuer、JWKS、签名和 nonce 的信任校验。 + /// 绑定 OAuth owner 选择的账号出口,供公开的 [`TokenVerifier`] 实现遵守同一代理合同 + /// 出口只决定路由,不改变 issuer、JWKS、签名和 nonce 的信任校验 #[must_use] pub fn with_outbound_proxy( mut self, @@ -123,57 +126,58 @@ impl<'a> TokenVerificationContext<'a> { self } - /// 返回本次验证的显式出口;`None` 表示直连,不得继承环境代理。 + /// 返回本次验证的显式出口;`None` 表示直连,不得继承环境代理 #[must_use] pub fn outbound_proxy(&self) -> Option<&gateway_core::account::OutboundProxy> { self.outbound_proxy } - /// 返回正在验证的流程。 + /// 返回正在验证的流程 #[must_use] pub const fn flow(&self) -> VerificationFlow { self.flow } - /// 返回精确匹配的预期 issuer。 + /// 返回精确匹配的预期 issuer #[must_use] pub fn issuer(&self) -> &Url { self.issuer } - /// 返回精确匹配的预期 audience/client 标识。 + /// 返回精确匹配的预期 audience/client 标识 #[must_use] pub const fn client_id(&self) -> &str { self.client_id } - /// 返回已校验发现文档中的同源 JWKS URL。 + /// 返回已校验发现文档中的同源 JWKS URL #[must_use] pub fn jwks_uri(&self) -> &Url { self.jwks_uri } - /// 返回发现文档中的同源权威 user-info URL。 + /// 返回发现文档中的同源权威 user-info URL #[must_use] pub fn userinfo_endpoint(&self) -> &Url { self.userinfo_endpoint } - /// 返回发现文档声明的算法。实现必须与本地密码学 allowlist 取交集,并拒绝 - /// `none`。 + /// 返回发现文档声明的算法 + /// 实现必须与本地密码学 allowlist 取交集,并拒绝 + /// `none` #[must_use] pub const fn signing_algorithms(&self) -> &[String] { self.signing_algorithms } - /// 返回 Authorization Code 流程的预期 nonce。 + /// 返回 Authorization Code 流程的预期 nonce #[must_use] pub const fn expected_nonce(&self) -> Option<&SecretValue> { self.expected_nonce } } -/// 仅向可信校验端口开放的借用 token 材料。 +/// 仅向可信校验端口开放的借用 token 材料 pub struct TokenCandidate<'a> { access_token: &'a SecretValue, id_token: Option<&'a SecretValue>, @@ -193,19 +197,19 @@ impl<'a> TokenCandidate<'a> { } } - /// 返回用于权威 user-info 验证的 access token。 + /// 返回用于权威 user-info 验证的 access token #[must_use] pub const fn access_token(&self) -> &SecretValue { self.access_token } - /// 返回用于完整 JWT/JWKS 校验的 ID token(若存在)。 + /// 返回用于完整 JWT/JWKS 校验的 ID token(若存在) #[must_use] pub const fn id_token(&self) -> Option<&SecretValue> { self.id_token } - /// 返回服务端给出的有效期。 + /// 返回服务端给出的有效期 #[must_use] pub const fn expires_in(&self) -> Option { self.expires_in @@ -223,14 +227,15 @@ impl fmt::Debug for TokenCandidate<'_> { } } -/// token 校验端口返回的 future。 +/// token 校验端口返回的 future pub type VerificationFuture<'a> = Pin> + Send + 'a>>; -/// 完整 JWT/JWKS 或权威 user-info 校验的信任边界。 +/// 完整 JWT/JWKS 或权威 user-info 校验的信任边界 pub trait TokenVerifier: Send + Sync { - /// 验证初始 token 集合。实现不得在未做签名与 claim 校验的情况下 base64 解码 - /// JWT claims。 + /// 验证初始 token 集合 + /// 实现不得在未做签名与 claim 校验的情况下 base64 解码 + /// JWT claims fn verify<'a>( &'a self, context: TokenVerificationContext<'a>, @@ -238,7 +243,7 @@ pub trait TokenVerifier: Send + Sync { ) -> VerificationFuture<'a>; } -/// 默认校验器,未完成接线时 fail closed。 +/// 默认校验器,未完成接线时 fail closed #[derive(Debug, Default)] pub struct FailClosedTokenVerifier; diff --git a/backend/crates/providers/xai/src/lib.rs b/backend/crates/providers/xai/src/lib.rs index dce876cf4..2ced85de2 100644 --- a/backend/crates/providers/xai/src/lib.rs +++ b/backend/crates/providers/xai/src/lib.rs @@ -1,4 +1,4 @@ -//! 官方 Grok Build OAuth Provider 边界。 +//! 官方 Grok Build OAuth Provider 边界 mod admin; pub mod credential; @@ -80,14 +80,14 @@ pub use transport::{ parse_grok_model_catalog, }; -/// xAI 初始化后交给组装根的最小能力集。 +/// xAI 初始化后交给组装根的最小能力集 pub struct ProviderBundle { core_provider: Arc, admin_provider: Arc, worker_contributions: Vec, } -/// 构造 xAI 数据面、管理面准备器与 Provider-owned 后台任务。 +/// 构造 xAI 数据面、管理面准备器与 Provider-owned 后台任务 pub async fn initialize(ports: ProviderStorePorts) -> Result { let profile = XaiWireProfileState::new(XaiWireProfile::default()); let oauth_config = GrokOAuthConfig::official().map_err(XaiInitializeError::Config)?; @@ -231,13 +231,13 @@ impl ProviderBundle { Arc::clone(&self.admin_provider) } - /// 一次性移交 Host 任务计划,防止同一 owner 被重复注册。 + /// 一次性移交 Host 任务计划,防止同一 owner 被重复注册 pub fn take_worker_contributions(&mut self) -> Vec { std::mem::take(&mut self.worker_contributions) } } -/// xAI 初始化失败的脱敏分类。 +/// xAI 初始化失败的脱敏分类 #[derive(Debug, thiserror::Error)] pub enum XaiInitializeError { #[error(transparent)] diff --git a/backend/crates/providers/xai/src/provider/continuation.rs b/backend/crates/providers/xai/src/provider/continuation.rs index a0f43e6c6..c9bb83b27 100644 --- a/backend/crates/providers/xai/src/provider/continuation.rs +++ b/backend/crates/providers/xai/src/provider/continuation.rs @@ -1,4 +1,4 @@ -//! xAI continuation 与 reasoning replay 状态处理。 +//! xAI continuation 与 reasoning replay 状态处理 use super::*; @@ -121,7 +121,7 @@ pub(super) fn apply_continuation( let previous = previous_session.ok_or_else(invalid_continuation)?; return apply_replay(request, previous, account, current_input); } - // Grok 原生续接继承首轮指令,不允许同时提交 instructions。 + // Grok 原生续接继承首轮指令,不允许同时提交 instructions request.clear_instructions(); request.set_previous_response_id(Some(pin.upstream_response_id().as_str().to_owned())); Ok(()) diff --git a/backend/crates/providers/xai/src/provider/failure.rs b/backend/crates/providers/xai/src/provider/failure.rs index d8393213e..18be7638c 100644 --- a/backend/crates/providers/xai/src/provider/failure.rs +++ b/backend/crates/providers/xai/src/provider/failure.rs @@ -1,4 +1,4 @@ -//! xAI 上游失败分类、账号反馈与恢复决策。 +//! xAI 上游失败分类、账号反馈与恢复决策 use super::*; use gateway_core::error::ProviderDiagnostic; @@ -384,7 +384,7 @@ pub(super) fn map_request_error(error: GrokRequestEncodeError) -> ProviderError )) } -/// 将选择阶段失败映射为带结构化 code 与 retry_after 的 Provider 错误。 +/// 将选择阶段失败映射为带结构化 code 与 retry_after 的 Provider 错误 pub(super) fn map_selection_error(error: GrokSessionSelectorError) -> ProviderError { let (retry_after, message, code) = match error { GrokSessionSelectorError::QueueRejected(error) => { diff --git a/backend/crates/providers/xai/src/provider/mod.rs b/backend/crates/providers/xai/src/provider/mod.rs index 9c6512cee..89a579db4 100644 --- a/backend/crates/providers/xai/src/provider/mod.rs +++ b/backend/crates/providers/xai/src/provider/mod.rs @@ -1,4 +1,4 @@ -//! 官方 Grok Build 会话的 `gateway-core` Provider adapter。 +//! 官方 Grok Build 会话的 `gateway-core` Provider adapter use std::collections::{BTreeMap, BTreeSet}; use std::sync::{Arc, Mutex}; @@ -87,11 +87,13 @@ const XAI_SESSION_OUTPUT_LIMIT: usize = 4_096; const REASONING_DECODE_FAILED_CODE: &str = "reasoning_decode_failed"; const RESPONSE_NOT_FOUND_CODE: &str = "not_found"; -/// 官方 Grok Build Provider;会话选择与 HTTP SSE transport 均由外部注入。 +/// 官方 Grok Build Provider;会话选择与 HTTP SSE transport 均由外部注入 /// -/// 每次调用只选择一个 OAuth 会话。仅当 xAI 明确拒绝历史 reasoning 密文时, -/// 允许在同账号、同凭据、同会话绑定上剥离密文后有界重试一次。凭据轮换、 -/// endpoint fallback 以及公开 xAI API key 推理都不在该 adapter 内。 +/// 每次调用只选择一个 OAuth 会话 +/// 仅当 xAI 明确拒绝历史 reasoning 密文时, +/// 允许在同账号、同凭据、同会话绑定上剥离密文后有界重试一次 +/// 凭据轮换、 +/// endpoint fallback 以及公开 xAI API key 推理都不在该 adapter 内 #[derive(Clone)] pub struct GrokBuildProvider { selector: Arc, @@ -124,7 +126,7 @@ struct SelectedGrokAttempt { } impl GrokBuildProvider { - /// 在显式的会话与 transport 边界上创建 Provider。 + /// 在显式的会话与 transport 边界上创建 Provider pub fn new( selector: Arc, transport: Arc, @@ -363,7 +365,7 @@ impl GrokBuildProvider { ) } else { // 源协议由转换插件拥有;这里只使用冻结的候选模型与 Core 账号 owner, - // 不把未知 JSON 中的同名字段解释为 xAI 会话或亲和事实。 + // 不把未知 JSON 中的同名字段解释为 xAI 会话或亲和事实 let operation_account = continuation_account(&context, previous_session.as_ref())?; ( upstream_model.as_str().to_owned(), @@ -475,7 +477,7 @@ impl GrokBuildProvider { if let Some(previous) = previous_session.as_ref() { upstream_request.inherit_session(previous.session_id.as_deref()); } - // 首字计时的起点:账号选择完成之后、上游建立之前。 + // 首字计时的起点:账号选择完成之后、上游建立之前 let output_started_at = Instant::now(); apply_continuation( &mut upstream_request, @@ -645,7 +647,7 @@ impl GrokBuildProvider { } fn native_request_requirements(request: &GenerateRequest) -> CapabilityRequirements { - // 这里只读取本 Provider 已知的 Responses 字段;工具别名等原生保护字段另行校验。 + // 这里只读取本 Provider 已知的 Responses 字段;工具别名等原生保护字段另行校验 Operation::Generate(GenerateRequest::from_protocol_payload( request.protocol_payload().clone(), )) @@ -810,7 +812,7 @@ fn tool_support(evidence: GrokCatalogCapabilityEvidence) -> SupportLevel { GrokCatalogCapabilityEvidence::DeclaredNative => SupportLevel::Native, GrokCatalogCapabilityEvidence::DeclaredUnsupported => SupportLevel::Unsupported, // catalog 省略该可选字段时,Grok Build 的 Responses 工具协议仍可用; - // 请求 adapter 会在发送前规范化仅客户端侧的工具结构。 + // 请求 adapter 会在发送前规范化仅客户端侧的工具结构 GrokCatalogCapabilityEvidence::Unknown => SupportLevel::Emulated, } } @@ -917,7 +919,7 @@ fn grok_model_presentation(model: &GrokCatalogModel) -> ModelPresentation { ) .with_reasoning(default_reasoning, reasoning_efforts) .with_context_window_tokens(context_window_tokens) - // Grok 目录只声明一个窗口,继续将它作为客户端可覆盖上限。 + // Grok 目录只声明一个窗口,继续将它作为客户端可覆盖上限 .with_max_context_window_tokens(context_window_tokens) .with_image_input(known_grok_4_5) .with_agent_tools( diff --git a/backend/crates/providers/xai/src/provider/stream.rs b/backend/crates/providers/xai/src/provider/stream.rs index 0b8262699..382e97852 100644 --- a/backend/crates/providers/xai/src/provider/stream.rs +++ b/backend/crates/providers/xai/src/provider/stream.rs @@ -1,4 +1,4 @@ -//! xAI inference/compaction 流执行与终态校验。 +//! xAI inference/compaction 流执行与终态校验 use super::*; @@ -44,7 +44,7 @@ fn append_middleware_grok_headers( } let value = std::str::from_utf8(header.value()) .map_err(|_| provider_error(ProviderErrorKind::Protocol, UpstreamSendState::NotSent))?; - // 插件可写任意字段,值不进入 Debug;transport 仍读取完整原值。 + // 插件可写任意字段,值不进入 Debug;transport 仍读取完整原值 target.push(crate::transport::GrokHeader::sensitive( header.name().to_owned(), crate::SecretValue::new(value), @@ -560,7 +560,7 @@ pub(super) fn cold_http_sse_stream( } }; // 首个非前导输出事件(结构帧也算)即上报携带 first_token_ms 的观测, - // 供 Core 覆盖会话级兜底值。 + // 供 Core 覆盖会话级兜底值 if decoder.take_output_start() && first_token_ms.is_none() { first_token_ms = Some( u64::try_from(output_started_at.elapsed().as_millis()).unwrap_or(u64::MAX), @@ -636,7 +636,7 @@ pub(super) fn cold_http_sse_stream( .iter() .flat_map(ProviderEvent::canonical_facts) .any(|event| matches!(event, GatewayEvent::Completed(_))); - // 尾部 finish 补全缓冲中的首个输出帧时,同样上报首字。 + // 尾部 finish 补全缓冲中的首个输出帧时,同样上报首字 if decoder.take_output_start() && first_token_ms.is_none() { first_token_ms = Some( u64::try_from(output_started_at.elapsed().as_millis()).unwrap_or(u64::MAX), diff --git a/backend/crates/providers/xai/src/provider/upstream_adapter.rs b/backend/crates/providers/xai/src/provider/upstream_adapter.rs index 47af203ba..0e56fb195 100644 --- a/backend/crates/providers/xai/src/provider/upstream_adapter.rs +++ b/backend/crates/providers/xai/src/provider/upstream_adapter.rs @@ -1,4 +1,4 @@ -//! xAI 已选 OAuth 会话的受管连接;插件不读取令牌或重新选号。 +//! xAI 已选 OAuth 会话的受管连接;插件不读取令牌或重新选号 use super::*; use gateway_core::{ @@ -57,7 +57,7 @@ impl GrokBuildProvider { let mutation = session.allows_account_state_mutation(); let stream = ProviderStream::new(metadata, events, session); Ok(if mutation { - // 插件协议或 RPC 故障不能降低原生账号评分;只接纳已解析的上游拒绝事实。 + // 插件协议或 RPC 故障不能降低原生账号评分;只接纳已解析的上游拒绝事实 stream.with_filtered_account_feedback(Arc::clone(&self.account_feedback), |error| { error.client_visible_upstream_error().is_some() }) diff --git a/backend/crates/providers/xai/src/provider/workers.rs b/backend/crates/providers/xai/src/provider/workers.rs index 66696be4a..48233005e 100644 --- a/backend/crates/providers/xai/src/provider/workers.rs +++ b/backend/crates/providers/xai/src/provider/workers.rs @@ -1,4 +1,4 @@ -//! xAI Provider 向 Host 贡献的后台 worker。 +//! xAI Provider 向 Host 贡献的后台 worker use super::*; @@ -9,7 +9,7 @@ pub(super) const WORKER_LEASE_RENEWAL: Duration = Duration::from_secs(5 * 60); pub(super) const OAUTH_REFRESH_INTERVAL: Duration = Duration::from_secs(30); pub(super) const QUOTA_CATALOG_INTERVAL: Duration = Duration::from_secs(5 * 60); // rolling 24h 描述的是上游用量窗口,不代表从本次观测起封禁 24 小时; -// 缺少可信 reset 时间时按短周期探测策略恢复检查。 +// 缺少可信 reset 时间时按短周期探测策略恢复检查 pub(super) const EXHAUSTED_QUOTA_FALLBACK_RECHECK_INTERVAL: Duration = Duration::from_secs(10 * 60); pub(super) const EXHAUSTED_QUOTA_REFRESH_RETRY_INTERVAL: Duration = QUOTA_CATALOG_INTERVAL; pub(super) const CLI_RELEASE_WORKER_OWNER: &str = "xai-cli-release"; diff --git a/backend/crates/providers/xai/src/reasoning_replay.rs b/backend/crates/providers/xai/src/reasoning_replay.rs index 8f27dfceb..289bd4a7c 100644 --- a/backend/crates/providers/xai/src/reasoning_replay.rs +++ b/backend/crates/providers/xai/src/reasoning_replay.rs @@ -1,4 +1,4 @@ -//! Grok Build 的账号隔离 reasoning replay。 +//! Grok Build 的账号隔离 reasoning replay use std::collections::{BTreeMap, BTreeSet}; use std::sync::{Arc, Mutex, MutexGuard}; diff --git a/backend/crates/providers/xai/src/transport/canonical.rs b/backend/crates/providers/xai/src/transport/canonical.rs index 8d10c74a7..3a6f8f378 100644 --- a/backend/crates/providers/xai/src/transport/canonical.rs +++ b/backend/crates/providers/xai/src/transport/canonical.rs @@ -1,4 +1,4 @@ -//! 官方 Grok Responses SSE 到网关 canonical 事件的转换。 +//! 官方 Grok Responses SSE 到网关 canonical 事件的转换 use std::collections::{BTreeMap, BTreeSet}; @@ -40,7 +40,7 @@ struct ProjectedWireEvent { wire: ProtocolWireEvent, } -/// 单次 xAI stream 独占的原生响应交付转换状态。 +/// 单次 xAI stream 独占的原生响应交付转换状态 pub(crate) struct GrokNativeResponseTranslator { response_transform: GrokResponseTransform, } @@ -58,7 +58,7 @@ struct ModelPricing { long: TokenRates, } -// 价格来源:https://docs.x.ai/developers/pricing,核验日期 2026-09-09。 +// 价格来源:https://docs.x.ai/developers/pricing,核验日期 2026-09-09 const GROK_46_PRICING: ModelPricing = ModelPricing { short: TokenRates { input_ticks: 20_000, @@ -111,7 +111,7 @@ const GROK_43_PRICING: ModelPricing = ModelPricing { }, }; -/// 按 xAI Provider 当前受控价格规则计算费用明细。 +/// 按 xAI Provider 当前受控价格规则计算费用明细 #[must_use] pub fn grok_billing_breakdown( model: &str, @@ -122,7 +122,7 @@ pub fn grok_billing_breakdown( grok_billing_breakdown_with_tier(model, input_tokens, output_tokens, cached_tokens, None) } -/// 按响应确认的实际档位计算 xAI Token 费用;未知档位不估算。 +/// 按响应确认的实际档位计算 xAI Token 费用;未知档位不估算 #[must_use] pub fn grok_billing_breakdown_with_tier( model: &str, @@ -174,7 +174,7 @@ pub fn grok_billing_breakdown_with_override( .and_then(|p| p.bands.get(fast_band)) .map(convert) .or_else(|| { - // 未覆盖档位继承内置价,不把人工标准价再次解释成 Priority 价。 + // 未覆盖档位继承内置价,不把人工标准价再次解释成 Priority 价 let builtin = model_pricing(model).map(|p| if long { p.long } else { p.short })?; Some(TokenRates { input_ticks: builtin.input_ticks.checked_mul(2)?, @@ -276,9 +276,9 @@ fn usd_price_per_million(per_token_ticks: u128) -> Option { usd_money(per_token_ticks.checked_mul(1_000_000)?) } -/// 单次官方 Grok Responses 尝试的增量解码器。 +/// 单次官方 Grok Responses 尝试的增量解码器 /// -/// 每个上游 event 同时保留 OpenAI wire,并在可识别时附加 canonical facts。 +/// 每个上游 event 同时保留 OpenAI wire,并在可识别时附加 canonical facts pub struct GrokCanonicalDecoder { pricing: Option, decoder: SseEventDecoder, @@ -355,7 +355,7 @@ impl NativeResponseTranslator for GrokNativeResponseTranslator { } impl GrokCanonicalDecoder { - /// 使用路由后最终发往上游的请求模型计价,并在响应缺少模型时用于 canonical 兜底。 + /// 使用路由后最终发往上游的请求模型计价,并在响应缺少模型时用于 canonical 兜底 pub fn new(upstream_model: impl Into) -> Self { Self { decoder: SseEventDecoder::default(), @@ -385,7 +385,7 @@ impl GrokCanonicalDecoder { } /// 创建在 canonical 与 wire 投影处理每个上游 event 前先还原请求级 tool 别名的 - /// 解码器。 + /// 解码器 #[must_use] pub fn for_request(upstream_model: impl Into, request: &GrokResponsesRequest) -> Self { Self { @@ -408,13 +408,13 @@ impl GrokCanonicalDecoder { } } - /// 上游报告的实际服务档位,供费用与持久观测共用。 + /// 上游报告的实际服务档位,供费用与持久观测共用 #[must_use] pub fn response_service_tier(&self) -> Option<&str> { self.response_service_tier.as_deref() } - /// 返回原始上游响应声明的模型,缺失时不使用请求模型补齐。 + /// 返回原始上游响应声明的模型,缺失时不使用请求模型补齐 #[must_use] pub fn response_model(&self) -> Option<&str> { self.response_model.model() @@ -425,7 +425,7 @@ impl GrokCanonicalDecoder { self.decode(events).map(|batch| batch.projected_events) } - /// 解码同一批上游事件,同时返回转换前 xAI wire 与独立生成的 OpenAI 投影。 + /// 解码同一批上游事件,同时返回转换前 xAI wire 与独立生成的 OpenAI 投影 pub(crate) fn push_before_translation( &mut self, chunk: &[u8], @@ -459,7 +459,7 @@ impl GrokCanonicalDecoder { self.decode(events).map(|batch| batch.projected_events) } - /// 取走本批解码帧里是否已出现首个非前导输出事件(结构帧也算),用于首字计时。 + /// 取走本批解码帧里是否已出现首个非前导输出事件(结构帧也算),用于首字计时 pub fn take_output_start(&mut self) -> bool { std::mem::take(&mut self.output_start_seen) } @@ -528,7 +528,7 @@ impl GrokCanonicalDecoder { if !event_type.is_empty() { self.response_model.observe(Some(&event_type), &value); } - // 工具转换可能隐藏注入的调用,计费事实必须从转换前的上游事件读取。 + // 工具转换可能隐藏注入的调用,计费事实必须从转换前的上游事件读取 if let Some(response) = value.get("response") { if let Some(tier) = response.get("service_tier").and_then(Value::as_str) { self.response_service_tier = Some(tier.trim().to_owned()); @@ -538,7 +538,7 @@ impl GrokCanonicalDecoder { if let Some(item) = value.get("item") { self.requires_provider_cost |= !token_only_output(item); } - // 转换失败必须终止,不能丢弃工具参数后仍向客户端报告成功。 + // 转换失败必须终止,不能丢弃工具参数后仍向客户端报告成功 let projected = project_response_event( &mut self.response_transform, &event_type, @@ -550,17 +550,17 @@ impl GrokCanonicalDecoder { let mut source_canonical = Vec::new(); for projected in projected { let transformed_type = projected.event_type; - // 首个非前导、非失败事件(结构帧也算)开启首字计时。 + // 首个非前导、非失败事件(结构帧也算)开启首字计时 self.output_start_seen |= !matches!( transformed_type.as_str(), "response.created" | "response.in_progress" | "response.failed" | "error" ); let value = projected.wire.data(); let mut canonical = Vec::new(); - // 终态事件(completed/incomplete)fail-closed:用量/计费校验失败即断流。 + // 终态事件(completed/incomplete)fail-closed:用量/计费校验失败即断流 // 其余内容事件容忍字段校验失败——正常上游变体(空 delta、重复 index、 - // 缺字段等)不打断已开始的客户端流:跳过 canonical 提取、wire 原样转发。 - // 真·上游错误(response.failed/error)为非 Protocol 类别,按终态传播。 + // 缺字段等)不打断已开始的客户端流:跳过 canonical 提取、wire 原样转发 + // 真·上游错误(response.failed/error)为非 Protocol 类别,按终态传播 let terminal_event = matches!( transformed_type.as_str(), "response.completed" | "response.incomplete" @@ -866,7 +866,7 @@ impl GrokCanonicalDecoder { output.push(GatewayEvent::ProviderCost(cost)); } else if !self.requires_provider_cost && let Some(cost) = usage.and_then(|usage| { - // 已报告金额仍优先;本地估价只使用实际发送模型,响应模型仅作观测。 + // 已报告金额仍优先;本地估价只使用实际发送模型,响应模型仅作观测 calculated_cost( response, &self.upstream_model, @@ -1053,14 +1053,14 @@ fn provider_reported_cost(response: &Value) -> Result bool { matches!( item.get("type").and_then(Value::as_str), @@ -1264,7 +1264,7 @@ fn upstream_event_error(value: &Value) -> ProviderError { error = error.with_upstream_code(OpaqueUpstreamValue::new(code.to_owned())); } // 结构化 message/code/type 供原客户端展示与重试分类;message 先脱去 - // 账号指纹(上游限流文案内嵌 team UUID),非结构化正文不透出。 + // 账号指纹(上游限流文案内嵌 team UUID),非结构化正文不透出 if let Some(message) = message { error = error.with_client_visible_upstream_error(ClientVisibleUpstreamError::new( scrub_account_fingerprints(message), diff --git a/backend/crates/providers/xai/src/transport/catalog.rs b/backend/crates/providers/xai/src/transport/catalog.rs index e621469be..e457de139 100644 --- a/backend/crates/providers/xai/src/transport/catalog.rs +++ b/backend/crates/providers/xai/src/transport/catalog.rs @@ -1,4 +1,4 @@ -//! 官方 Grok CLI proxy 模型目录 wire、transport port 与安全快照。 +//! 官方 Grok CLI proxy 模型目录 wire、transport port 与安全快照 use std::collections::BTreeSet; use std::fmt; @@ -15,16 +15,16 @@ use zeroize::Zeroizing; use crate::{GrokHeader, SecretValue, XaiWireProfileState}; -/// 官方 Grok CLI proxy 模型目录 URL。 +/// 官方 Grok CLI proxy 模型目录 URL pub const GROK_MODEL_CATALOG_URL: &str = "https://cli-chat-proxy.grok.com/v1/models"; -/// 官方 Grok Build credits/billing URL。 +/// 官方 Grok Build credits/billing URL pub const GROK_BILLING_URL: &str = "https://cli-chat-proxy.grok.com/v1/billing?format=credits"; -/// 官方实时订阅查询 URL;额度响应本身不提供套餐事实。 +/// 官方实时订阅查询 URL;额度响应本身不提供套餐事实 pub const GROK_SUBSCRIPTION_URL: &str = "https://cli-chat-proxy.grok.com/v1/user?include=subscription"; -/// 单次 Grok 模型目录响应允许的最大字节数。 +/// 单次 Grok 模型目录响应允许的最大字节数 pub const MAX_GROK_MODEL_CATALOG_BYTES: usize = 1024 * 1024; -/// 单次 Grok billing 响应允许的最大字节数。 +/// 单次 Grok billing 响应允许的最大字节数 pub const MAX_GROK_BILLING_BYTES: usize = 512 * 1024; pub(crate) const MAX_CATALOG_MODELS: usize = 2_048; @@ -33,15 +33,15 @@ const MAX_DESCRIPTION_BYTES: usize = 4 * 1024; const MAX_ETAG_BYTES: usize = 256; const SUBSCRIPTION_REQUEST_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); -/// 构造模型目录 OAuth session 失败。 +/// 构造模型目录 OAuth session 失败 #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] pub enum GrokModelCatalogSessionError { - /// OAuth、身份或兼容版本无法安全写入 HTTP header。 + /// OAuth、身份或兼容版本无法安全写入 HTTP header #[error("Grok model catalog OAuth session contains invalid header data")] InvalidHeaderData, } -/// 一次模型目录请求使用的 OAuth session,不支持 API Key。 +/// 一次模型目录请求使用的 OAuth session,不支持 API Key #[derive(Clone)] pub struct GrokModelCatalogSession { access_token: SecretValue, @@ -52,11 +52,11 @@ pub struct GrokModelCatalogSession { } impl GrokModelCatalogSession { - /// 创建仅用于官方 CLI proxy 的 OAuth session。 + /// 创建仅用于官方 CLI proxy 的 OAuth session /// /// # Errors /// - /// 任一 header 值为空、过长或含非可见 ASCII 时返回错误。 + /// 任一 header 值为空、过长或含非可见 ASCII 时返回错误 pub fn new( access_token: SecretValue, user_id: SecretValue, @@ -102,7 +102,7 @@ impl fmt::Debug for GrokModelCatalogSession { } } -/// 交给单次 GET transport 的完整模型目录请求。 +/// 交给单次 GET transport 的完整模型目录请求 #[derive(Debug)] pub struct GrokModelCatalogRequest { endpoint: Url, @@ -138,27 +138,27 @@ impl GrokModelCatalogRequest { }) } - /// 返回固定官方 `/v1/models` URL。 + /// 返回固定官方 `/v1/models` URL #[must_use] pub const fn endpoint(&self) -> &Url { &self.endpoint } - /// 返回完整官方 OAuth/session header;敏感值保留类型标记。 + /// 返回完整官方 OAuth/session header;敏感值保留类型标记 #[must_use] pub fn headers(&self) -> &[GrokHeader] { &self.headers } } -/// 单次成功 GET 的有界原始响应。 +/// 单次成功 GET 的有界原始响应 pub struct GrokModelCatalogTransportResponse { body: Zeroizing>, etag: Option, } impl GrokModelCatalogTransportResponse { - /// 创建 transport 响应;上层仍会重复执行字节和 ETag 校验。 + /// 创建 transport 响应;上层仍会重复执行字节和 ETag 校验 #[must_use] pub fn new(body: impl Into>, etag: Option) -> Self { Self { @@ -187,26 +187,26 @@ impl fmt::Debug for GrokModelCatalogTransportResponse { } } -/// 模型目录 GET transport 失败类型。 +/// 模型目录 GET transport 失败类型 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum GrokModelCatalogTransportErrorKind { - /// URL、header 或响应协议不合法。 + /// URL、header 或响应协议不合法 Protocol, - /// OAuth access token 被拒绝。 + /// OAuth access token 被拒绝 Unauthorized, - /// OAuth session 没有目录权限。 + /// OAuth session 没有目录权限 PermissionDenied, - /// 官方 proxy 限流。 + /// 官方 proxy 限流 RateLimited, - /// 请求超时。 + /// 请求超时 Timeout, - /// 网络或 TLS 失败。 + /// 网络或 TLS 失败 Transport, - /// 官方 proxy 暂不可用。 + /// 官方 proxy 暂不可用 Unavailable, } -/// 不携带上游正文的模型目录 transport 错误。 +/// 不携带上游正文的模型目录 transport 错误 #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] #[error("Grok model catalog transport failed: {kind:?}")] pub struct GrokModelCatalogTransportError { @@ -215,13 +215,13 @@ pub struct GrokModelCatalogTransportError { } impl GrokModelCatalogTransportError { - /// 创建未附带 HTTP 状态的失败。 + /// 创建未附带 HTTP 状态的失败 #[must_use] pub const fn new(kind: GrokModelCatalogTransportErrorKind) -> Self { Self { kind, status: None } } - /// 附加合法 HTTP 状态码。 + /// 附加合法 HTTP 状态码 #[must_use] pub fn with_status(mut self, status: u16) -> Self { if (100..=599).contains(&status) { @@ -230,20 +230,20 @@ impl GrokModelCatalogTransportError { self } - /// 返回稳定失败分类。 + /// 返回稳定失败分类 #[must_use] pub const fn kind(&self) -> GrokModelCatalogTransportErrorKind { self.kind } - /// 返回可公开 HTTP 状态。 + /// 返回可公开 HTTP 状态 #[must_use] pub const fn status(&self) -> Option { self.status } } -/// 模型目录 transport future。 +/// 模型目录 transport future pub type GrokModelCatalogTransportFuture<'a> = Pin< Box< dyn Future< @@ -253,13 +253,13 @@ pub type GrokModelCatalogTransportFuture<'a> = Pin< >, >; -/// 只执行一次固定官方 GET 的模型目录 transport port。 +/// 只执行一次固定官方 GET 的模型目录 transport port pub trait GrokModelCatalogTransport: Send + Sync { - /// 发送一次请求;实现不得重试、改用 API Key 或跟随 redirect。 + /// 发送一次请求;实现不得重试、改用 API Key 或跟随 redirect fn execute(&self, request: GrokModelCatalogRequest) -> GrokModelCatalogTransportFuture<'_>; } -/// 交给单次官方计费或订阅 GET transport 的完整请求。 +/// 交给单次官方计费或订阅 GET transport 的完整请求 #[derive(Debug)] pub struct GrokBillingRequest { endpoint: Url, @@ -301,20 +301,20 @@ impl GrokBillingRequest { }) } - /// 返回固定官方计费或订阅 URL。 + /// 返回固定官方计费或订阅 URL #[must_use] pub const fn endpoint(&self) -> &Url { &self.endpoint } - /// 返回官方 OAuth/session headers;敏感值保留类型标记。 + /// 返回官方 OAuth/session headers;敏感值保留类型标记 #[must_use] pub fn headers(&self) -> &[GrokHeader] { &self.headers } } -/// 单次成功 billing GET 的有界原始响应。 +/// 单次成功 billing GET 的有界原始响应 pub struct GrokBillingTransportResponse { body: Zeroizing>, } @@ -342,7 +342,7 @@ impl fmt::Debug for GrokBillingTransportResponse { } } -/// Billing transport 的稳定失败分类。 +/// Billing transport 的稳定失败分类 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum GrokBillingTransportErrorKind { Protocol, @@ -354,7 +354,7 @@ pub enum GrokBillingTransportErrorKind { Unavailable, } -/// 不携带上游正文的 billing transport 错误。 +/// 不携带上游正文的 billing transport 错误 #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] #[error("Grok billing transport failed: {kind:?}")] pub struct GrokBillingTransportError { @@ -387,7 +387,7 @@ impl GrokBillingTransportError { } } -/// 计费 transport 的 future。 +/// 计费 transport 的 future pub type GrokBillingTransportFuture<'a> = Pin< Box< dyn Future> @@ -396,13 +396,13 @@ pub type GrokBillingTransportFuture<'a> = Pin< >, >; -/// 只执行一次固定官方 billing GET 的 transport port。 +/// 只执行一次固定官方 billing GET 的 transport port pub trait GrokBillingTransport: Send + Sync { - /// 实现不得重试、改用 API Key 或跟随 redirect。 + /// 实现不得重试、改用 API Key 或跟随 redirect fn execute(&self, request: GrokBillingRequest) -> GrokBillingTransportFuture<'_>; } -/// 已验证但仍由 xAI Provider 独占解释的动态 billing JSON。 +/// 已验证但仍由 xAI Provider 独占解释的动态 billing JSON #[derive(Clone, PartialEq)] pub struct GrokBillingSnapshot { document: Map, @@ -442,7 +442,7 @@ pub enum GrokBillingError { InvalidWire, } -/// 有界官方 billing client。 +/// 有界官方 billing client #[derive(Clone)] pub struct GrokBillingClient { transport: Arc, @@ -467,7 +467,7 @@ impl GrokBillingClient { parse_grok_billing(response.body()) } - /// 查询实时订阅,只返回套餐事实,不保存用户资料。 + /// 查询实时订阅,只返回套餐事实,不保存用户资料 pub async fn fetch_subscription( &self, session: &GrokModelCatalogSession, @@ -503,7 +503,7 @@ fn parse_subscription_tier(body: &[u8]) -> Result, GrokBillingErr let tier = tier.trim(); Ok((!tier.is_empty()).then(|| tier.to_owned())) } - // 官方仅在存在有效付费订阅时返回套餐名;缺字段或团队身份不能推断为 Free。 + // 官方仅在存在有效付费订阅时返回套餐名;缺字段或团队身份不能推断为 Free Some(Value::Null) if document.get("principalType").and_then(Value::as_str) == Some("User") && document.get("teamId") == Some(&Value::Null) @@ -525,7 +525,7 @@ impl fmt::Debug for GrokBillingClient { } } -/// 验证已限长的动态 billing document,同时保留 Provider 后续新增字段。 +/// 验证已限长的动态 billing document,同时保留 Provider 后续新增字段 pub fn parse_grok_billing(body: &[u8]) -> Result { if body.len() > MAX_GROK_BILLING_BYTES { return Err(GrokBillingError::ResponseTooLarge); @@ -632,14 +632,14 @@ fn validate_dynamic_text(value: &Value, max_bytes: usize) -> Result<(), GrokBill Ok(()) } -/// 上游目录对一项能力给出的明确证据。 +/// 上游目录对一项能力给出的明确证据 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum GrokCatalogCapabilityEvidence { - /// 上游明确声明原生支持。 + /// 上游明确声明原生支持 DeclaredNative, - /// 上游明确声明不支持。 + /// 上游明确声明不支持 DeclaredUnsupported, - /// 上游没有提供可依赖的声明。 + /// 上游没有提供可依赖的声明 Unknown, } @@ -653,22 +653,22 @@ impl GrokCatalogCapabilityEvidence { } } -/// Grok 官方目录声明的 API backend。 +/// Grok 官方目录声明的 API backend #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] #[serde(rename_all = "snake_case")] pub enum GrokCatalogApiBackend { - /// OpenAI Responses wire。 + /// OpenAI Responses wire Responses, - /// OpenAI Chat Completions wire。 + /// OpenAI Chat Completions wire ChatCompletions, - /// Anthropic Messages wire。 + /// Anthropic Messages wire Messages, - /// 上游新增、当前 adapter 尚未识别的 backend。 + /// 上游新增、当前 adapter 尚未识别的 backend #[serde(other)] Unknown, } -/// Grok 官方目录允许声明的 reasoning effort。 +/// Grok 官方目录允许声明的 reasoning effort #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] #[serde(rename_all = "lowercase")] pub enum GrokCatalogReasoningEffort { @@ -679,13 +679,13 @@ pub enum GrokCatalogReasoningEffort { High, Xhigh, Max, - /// 上游新增、当前 adapter 尚未识别的 effort;不进入能力投影。 + /// 上游新增、当前 adapter 尚未识别的 effort;不进入能力投影 #[serde(other)] Unknown, } impl GrokCatalogReasoningEffort { - /// 返回 Responses API 使用的规范值。 + /// 返回 Responses API 使用的规范值 #[must_use] pub const fn as_str(self) -> &'static str { match self { @@ -701,7 +701,7 @@ impl GrokCatalogReasoningEffort { } } -/// Grok 目录中允许进入控制面的能力证据。 +/// Grok 目录中允许进入控制面的能力证据 #[derive(Debug, Clone, PartialEq, Eq)] pub struct GrokCatalogCapabilities { responses_api: GrokCatalogCapabilityEvidence, @@ -714,50 +714,50 @@ pub struct GrokCatalogCapabilities { } impl GrokCatalogCapabilities { - /// 返回 Responses API 支持证据。 + /// 返回 Responses API 支持证据 #[must_use] pub const fn responses_api(&self) -> GrokCatalogCapabilityEvidence { self.responses_api } - /// 返回 reasoning effort 支持证据。 + /// 返回 reasoning effort 支持证据 #[must_use] pub const fn reasoning_effort(&self) -> GrokCatalogCapabilityEvidence { self.reasoning_effort } - /// 返回上游声明的可选 reasoning effort,顺序与目录一致。 + /// 返回上游声明的可选 reasoning effort,顺序与目录一致 #[must_use] pub fn reasoning_efforts(&self) -> &[GrokCatalogReasoningEffort] { &self.reasoning_efforts } - /// 返回上游声明或菜单推导出的默认 reasoning effort。 + /// 返回上游声明或菜单推导出的默认 reasoning effort #[must_use] pub const fn default_reasoning_effort(&self) -> Option { self.default_reasoning_effort } - /// 返回后端 search 支持证据。 + /// 返回后端 search 支持证据 #[must_use] pub const fn backend_search(&self) -> GrokCatalogCapabilityEvidence { self.backend_search } - /// 返回流式工具调用支持证据。 + /// 返回流式工具调用支持证据 #[must_use] pub const fn streaming_tool_calls(&self) -> GrokCatalogCapabilityEvidence { self.streaming_tool_calls } - /// 返回上游明确声明的 API backend。 + /// 返回上游明确声明的 API backend #[must_use] pub const fn api_backend(&self) -> Option { self.api_backend } } -/// Grok 目录中明确声明的模型限制。 +/// Grok 目录中明确声明的模型限制 #[derive(Debug, Clone, PartialEq, Eq)] pub struct GrokCatalogLimits { context_window_tokens: Option, @@ -765,20 +765,20 @@ pub struct GrokCatalogLimits { } impl GrokCatalogLimits { - /// 返回明确上下文窗口;缺失表示未知。 + /// 返回明确上下文窗口;缺失表示未知 #[must_use] pub const fn context_window_tokens(&self) -> Option { self.context_window_tokens } - /// 返回明确最大输出 token;缺失表示未知。 + /// 返回明确最大输出 token;缺失表示未知 #[must_use] pub const fn max_output_tokens(&self) -> Option { self.max_output_tokens } } -/// Grok 目录中允许持久化的原始元数据白名单。 +/// Grok 目录中允许持久化的原始元数据白名单 #[derive(Debug, Clone, PartialEq, Eq)] pub struct GrokCatalogMetadata { catalog_entry_id: Option, @@ -787,26 +787,26 @@ pub struct GrokCatalogMetadata { } impl GrokCatalogMetadata { - /// 返回目录自身的稳定 entry ID;它不替代实际请求模型。 + /// 返回目录自身的稳定 entry ID;它不替代实际请求模型 #[must_use] pub fn catalog_entry_id(&self) -> Option<&str> { self.catalog_entry_id.as_deref() } - /// 返回安全说明文本。 + /// 返回安全说明文本 #[must_use] pub fn description(&self) -> Option<&str> { self.description.as_deref() } - /// 返回上游明确隐藏标记。 + /// 返回上游明确隐藏标记 #[must_use] pub const fn hidden(&self) -> Option { self.hidden } } -/// 一个已完整校验的 Grok 真实模型。 +/// 一个已完整校验的 Grok 真实模型 #[derive(Debug, Clone, PartialEq, Eq)] pub struct GrokCatalogModel { request_model: UpstreamModelId, @@ -817,38 +817,38 @@ pub struct GrokCatalogModel { } impl GrokCatalogModel { - /// 返回实际写入上游请求的模型 slug。 + /// 返回实际写入上游请求的模型 slug #[must_use] pub const fn request_model(&self) -> &UpstreamModelId { &self.request_model } - /// 返回上游明确提供的展示名;不从 slug 猜测。 + /// 返回上游明确提供的展示名;不从 slug 猜测 #[must_use] pub fn display_name(&self) -> Option<&str> { self.display_name.as_deref() } - /// 返回能力证据。 + /// 返回能力证据 #[must_use] pub const fn capabilities(&self) -> &GrokCatalogCapabilities { &self.capabilities } - /// 返回明确限制。 + /// 返回明确限制 #[must_use] pub const fn limits(&self) -> &GrokCatalogLimits { &self.limits } - /// 返回白名单元数据。 + /// 返回白名单元数据 #[must_use] pub const fn metadata(&self) -> &GrokCatalogMetadata { &self.metadata } } -/// 一次完整成功的 Grok 远端模型快照。 +/// 一次完整成功的 Grok 远端模型快照 #[derive(Debug, Clone, PartialEq, Eq)] pub struct GrokModelCatalogSnapshot { models: Vec, @@ -856,74 +856,74 @@ pub struct GrokModelCatalogSnapshot { } impl GrokModelCatalogSnapshot { - /// 返回本轮全部模型。 + /// 返回本轮全部模型 #[must_use] pub fn models(&self) -> &[GrokCatalogModel] { &self.models } - /// 返回经过语法和长度白名单校验的 HTTP ETag。 + /// 返回经过语法和长度白名单校验的 HTTP ETag #[must_use] pub fn etag(&self) -> Option<&str> { self.etag.as_deref() } } -/// 模型目录请求或完整快照失败。 +/// 模型目录请求或完整快照失败 #[derive(Debug, thiserror::Error)] pub enum GrokModelCatalogError { - /// 固定官方请求无法构造。 + /// 固定官方请求无法构造 #[error("Grok model catalog request is invalid")] InvalidRequest, - /// 单次 transport 失败。 + /// 单次 transport 失败 #[error(transparent)] Transport(#[from] GrokModelCatalogTransportError), - /// 响应超过硬上限。 + /// 响应超过硬上限 #[error("Grok model catalog response exceeds the byte limit")] ResponseTooLarge, - /// 顶层不是唯一受支持的 `{data:[...]}` wire。 + /// 顶层不是唯一受支持的 `{data:[...]}` wire #[error("Grok model catalog response violates the official wire contract")] InvalidWire, - /// 目录没有任何模型。 + /// 目录没有任何模型 #[error("Grok model catalog snapshot is empty")] EmptySnapshot, - /// 目录模型数量超过安全上限。 + /// 目录模型数量超过安全上限 #[error("Grok model catalog contains too many models")] TooManyModels, - /// 实际请求模型 slug 不合法。 + /// 实际请求模型 slug 不合法 #[error("Grok model catalog contains an invalid request model slug")] InvalidModelSlug, - /// 同一实际请求模型重复出现。 + /// 同一实际请求模型重复出现 #[error("Grok model catalog contains a duplicate request model slug")] DuplicateModelSlug, - /// 展示或白名单元数据不合法。 + /// 展示或白名单元数据不合法 #[error("Grok model catalog contains invalid public metadata")] InvalidMetadata, - /// 模型限制不是明确的正整数。 + /// 模型限制不是明确的正整数 #[error("Grok model catalog contains invalid model limits")] InvalidLimits, - /// ETag 不属于允许持久化的安全格式。 + /// ETag 不属于允许持久化的安全格式 #[error("Grok model catalog ETag is invalid")] InvalidEtag, } -/// 仅负责编排一次严格 transport 与快照解析的目录 client。 +/// 仅负责编排一次严格 transport 与快照解析的目录 client pub struct GrokModelCatalogClient { transport: Arc, } impl GrokModelCatalogClient { - /// 注入一个严格的单次 GET transport。 + /// 注入一个严格的单次 GET transport #[must_use] pub fn new(transport: Arc) -> Self { Self { transport } } - /// 获取并验证一次完整快照。 + /// 获取并验证一次完整快照 /// /// # Errors /// - /// transport、字节上限、wire、任一模型或 ETag 不合法时整轮失败。 + /// transport、字节上限、wire、任一模型或 ETag 不合法时整轮失败 pub async fn fetch( &self, session: &GrokModelCatalogSession, @@ -1023,11 +1023,11 @@ impl GrokReasoningEffortOptionWire { } } -/// 解析唯一受支持的 Grok 官方完整快照。 +/// 解析唯一受支持的 Grok 官方完整快照 /// /// # Errors /// -/// 任一条目、重复 slug 或 ETag 不合法时整轮失败;上游新增的未知字段会忽略。 +/// 任一条目、重复 slug 或 ETag 不合法时整轮失败;上游新增的未知字段会忽略 pub fn parse_grok_model_catalog( body: &[u8], etag: Option<&str>, @@ -1093,7 +1093,7 @@ fn normalize_model(wire: GrokModelWire) -> Result` 用户上下文,因此不保留上游模板中的占位符。 +// `/compact ` 用户上下文,因此不保留上游模板中的占位符 const GROK_COMPACTION_PROMPT: &str = r#"Your task is to produce a faithful, concise summary of the conversation so far so that a successor assistant can continue the work seamlessly after the earlier turns are discarded. The successor will see the user's original query plus this summary. Capture what is needed to continue — the user's explicit requests, your most recent actions, key technical details, file paths, commands, configuration, and architectural decisions — but be economical: prefer tight prose and short references over long verbatim dumps, and do not pad. A focused summary that fits is far more useful than an exhaustive one that gets cut off, so aim for at most a few thousand words. CRITICAL: If earlier turns include a prior compaction summary (marked with tags or a "This session is being continued" preamble), treat it as authoritative for the early history and carry its still-relevant information forward into your new summary so nothing important is lost across successive compactions. @@ -34,10 +36,10 @@ IMPORTANT: Do NOT call or use any tools. Respond with ONLY the

..., affinity: Option, @@ -45,11 +47,11 @@ pub struct GrokCompactionRequest { } impl GrokCompactionRequest { - /// 编码一次无工具、无 native continuation 的 Grok 摘要调用。 + /// 编码一次无工具、无 native continuation 的 Grok 摘要调用 /// /// # Errors /// - /// 完整历史无法按 Grok Responses contract 规范化时返回错误。 + /// 完整历史无法按 Grok Responses contract 规范化时返回错误 pub fn encode( request: &GenerateRequest, upstream_model: &str, @@ -68,7 +70,7 @@ impl GrokCompactionRequest { body.insert("input".to_owned(), Value::Array(input)); // 压缩请求已经携带规范化后的完整历史,不能让账号派生的缓存身份 - // 把它重新变成依赖原账号状态的原生续接请求。 + // 把它重新变成依赖原账号状态的原生续接请求 body.remove("prompt_cache_key"); body.insert("include".to_owned(), json!(["reasoning.encrypted_content"])); if body @@ -90,49 +92,50 @@ impl GrokCompactionRequest { }) } - /// 返回将发送到 Grok `/v1/responses` 的 JSON object。 + /// 返回将发送到 Grok `/v1/responses` 的 JSON object #[must_use] pub const fn body(&self) -> &Map { &self.body } - /// 返回用于选择同一 Grok 账号的软亲和键。 + /// 返回用于选择同一 Grok 账号的软亲和键 #[must_use] pub const fn affinity(&self) -> Option<&GrokSessionAffinityKey> { self.affinity.as_ref() } - /// 返回用于在成功 compact 后清除 reasoning replay 的显式会话身份。 + /// 返回用于在成功 compact 后清除 reasoning replay 的显式会话身份 #[must_use] pub(crate) fn reasoning_replay_session_id(&self) -> Option<&str> { self.reasoning_replay_session_id.as_deref() } - /// 返回归一化后的 xAI wire 模型。 + /// 返回归一化后的 xAI wire 模型 pub(crate) fn upstream_model(&self) -> Option<&str> { self.body.get("model").and_then(Value::as_str) } - /// 序列化上游请求正文。 + /// 序列化上游请求正文 /// /// # Errors /// - /// JSON 序列化失败时返回错误。 + /// JSON 序列化失败时返回错误 pub fn to_json_bytes(&self) -> Result, GrokRequestEncodeError> { serde_json::to_vec(&self.body).map_err(|_| GrokRequestEncodeError::Serialization) } - /// 为同账号的一次 `invalid_encrypted_content` 恢复请求剥离被拒绝密文。 + /// 为同账号的一次 `invalid_encrypted_content` 恢复请求剥离被拒绝密文 pub(crate) fn strip_invalid_encrypted_reasoning(&mut self) -> bool { strip_invalid_encrypted_reasoning_from_body(&mut self.body) } } -/// 返回 xAI 专用全历史压缩是否由客户端请求。 +/// 返回 xAI 专用全历史压缩是否由客户端请求 /// -/// `compaction_trigger` 是 Codex Responses wire 的控制项。它只在 xAI adapter +/// `compaction_trigger` 是 Codex Responses wire 的控制项 +/// 它只在 xAI adapter /// 内被识别,并且只接受处在 `input` 最末尾的触发器;OpenAI 透明路径不会调用 -/// 本函数,因此会保留该项原样上游。 +/// 本函数,因此会保留该项原样上游 #[must_use] pub(crate) fn has_terminal_compaction_trigger(request: &GenerateRequest) -> bool { request @@ -158,9 +161,9 @@ impl fmt::Debug for GrokCompactionRequest { } } -/// 从一次专用 Grok 摘要响应中提取 xAI adapter 私有的摘要文本。 +/// 从一次专用 Grok 摘要响应中提取 xAI adapter 私有的摘要文本 /// -/// xAI 随后自行投影为 OpenAI Responses wire,Core 不再承载 compaction 语义。 +/// xAI 随后自行投影为 OpenAI Responses wire,Core 不再承载 compaction 语义 #[derive(Default)] pub struct GrokCompactionSummaryDecoder { text: String, @@ -169,7 +172,7 @@ pub struct GrokCompactionSummaryDecoder { } impl GrokCompactionSummaryDecoder { - /// 创建空 decoder。 + /// 创建空 decoder #[must_use] pub const fn new() -> Self { Self { @@ -179,11 +182,11 @@ impl GrokCompactionSummaryDecoder { } } - /// 消费 Grok canonical decoder 已校验的 facts。 + /// 消费 Grok canonical decoder 已校验的 facts /// /// # Errors /// - /// 摘要文本无法处理时返回错误。 + /// 摘要文本无法处理时返回错误 pub fn observe(&mut self, event: &ProviderEvent) -> Result<(), GrokCompactionDecodeError> { for fact in event.canonical_facts() { if let GatewayEvent::TextDelta(delta) = fact { @@ -197,16 +200,16 @@ impl GrokCompactionSummaryDecoder { Ok(()) } - /// 完成摘要解码并返回上游摘要文本。 + /// 完成摘要解码并返回上游摘要文本 /// /// # Errors /// - /// 保留此 `Result` 签名以稳定 decoder contract。 + /// 保留此 `Result` 签名以稳定 decoder contract pub fn finish(self) -> Result { Ok(self.summary_text().unwrap_or_default()) } - /// 完成摘要解码,并返回可见摘要与 xAI 真实 reasoning 密文。 + /// 完成摘要解码,并返回可见摘要与 xAI 真实 reasoning 密文 pub(crate) fn finish_with_encrypted_content( self, ) -> Result<(Option, String), GrokCompactionDecodeError> { @@ -246,7 +249,7 @@ impl fmt::Debug for GrokCompactionSummaryDecoder { } } -/// Grok 摘要响应不满足压缩 contract。 +/// Grok 摘要响应不满足压缩 contract #[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)] pub enum GrokCompactionDecodeError { #[error("Grok compaction response omitted reasoning.encrypted_content")] @@ -266,9 +269,9 @@ impl GrokCompactionWireEvents { } } -/// 把 xAI 的专用摘要结果投影为 Codex 需要的 OpenAI Responses 事件。 +/// 把 xAI 的专用摘要结果投影为 Codex 需要的 OpenAI Responses 事件 /// -/// 这是 xAI adapter 的局部兼容职责,不经过 API 层的通用 canonical 重建器。 +/// 这是 xAI adapter 的局部兼容职责,不经过 API 层的通用 canonical 重建器 pub(crate) fn compaction_wire_events( started: &ResponseMeta, completed: &ResponseMeta, @@ -286,7 +289,7 @@ pub(crate) fn compaction_wire_events( Value::Null, ); let item = compaction_item(summary, encrypted_content); - // compact 成功以真实密文为依据,不向客户端延续摘要生成阶段的 incomplete 状态。 + // compact 成功以真实密文为依据,不向客户端延续摘要生成阶段的 incomplete 状态 let terminal_response = compaction_response( completed, terminal_source.or(created_source), diff --git a/backend/crates/providers/xai/src/transport/config.rs b/backend/crates/providers/xai/src/transport/config.rs index 1ddba30cf..7819ddece 100644 --- a/backend/crates/providers/xai/src/transport/config.rs +++ b/backend/crates/providers/xai/src/transport/config.rs @@ -1,4 +1,4 @@ -//! 固定的官方 Grok Build inference 配置。 +//! 固定的官方 Grok Build inference 配置 pub const XAI_PROVIDER_NAME: &str = "xai"; pub const GROK_CLI_BASE_URL: &str = "https://cli-chat-proxy.grok.com/v1"; diff --git a/backend/crates/providers/xai/src/transport/egress.rs b/backend/crates/providers/xai/src/transport/egress.rs index 1229ffecc..904725c2b 100644 --- a/backend/crates/providers/xai/src/transport/egress.rs +++ b/backend/crates/providers/xai/src/transport/egress.rs @@ -1,4 +1,4 @@ -//! 出口状态的有界生命周期;client 初始化与 JWKS 单飞共享同一淘汰边界。 +//! 出口状态的有界生命周期;client 初始化与 JWKS 单飞共享同一淘汰边界 use std::collections::VecDeque; use std::sync::{Arc, Mutex}; @@ -29,8 +29,8 @@ impl EgressCache { } } - /// 初始化或 JWKS 验证期间持有 cell,避免淘汰后产生第二个单飞 owner。 - /// 推理初始化完成后仅保留 client clone;淘汰不会中断旧出口的在途请求。 + /// 初始化或 JWKS 验证期间持有 cell,避免淘汰后产生第二个单飞 owner + /// 推理初始化完成后仅保留 client clone;淘汰不会中断旧出口的在途请求 pub(crate) fn entry(&self, key: K) -> Option>> { let mut entries = self.entries.lock().ok()?; if let Some(index) = entries.iter().position(|(candidate, _)| candidate == &key) { @@ -40,7 +40,7 @@ impl EgressCache { return Some(cell); } if entries.len() == MAX_CACHED_EGRESS_STATES { - // 满载且全部在用时拒绝新出口;不扩容,也不破坏既有出口的单飞。 + // 满载且全部在用时拒绝新出口;不扩容,也不破坏既有出口的单飞 let idle = entries .iter() .position(|(_, value)| Arc::strong_count(value) == 1)?; diff --git a/backend/crates/providers/xai/src/transport/headers.rs b/backend/crates/providers/xai/src/transport/headers.rs index 360a0aae3..e9c726f24 100644 --- a/backend/crates/providers/xai/src/transport/headers.rs +++ b/backend/crates/providers/xai/src/transport/headers.rs @@ -1,3 +1,5 @@ +//! 根据 Grok 会话与客户端画像生成区分敏感值的上游请求头 + use std::fmt; use gateway_core::engine::ModelRequestId; @@ -8,7 +10,7 @@ use crate::{SecretValue, XaiWireProfileState}; use super::SelectedGrokSession; -/// 进程级 Grok Build 客户端身份;请求级身份在构造 headers 时单独生成。 +/// 进程级 Grok Build 客户端身份;请求级身份在构造 headers 时单独生成 #[derive(Clone)] pub struct GrokClientIdentity(SecretValue); @@ -35,17 +37,17 @@ impl fmt::Debug for GrokClientIdentity { } } -/// 官方 Grok 请求头的公开或敏感取值。 +/// 官方 Grok 请求头的公开或敏感取值 #[derive(Clone)] pub enum GrokHeaderValue { - /// 非敏感的协议元数据。 + /// 非敏感的协议元数据 Public(String), - /// 必须脱敏的 OAuth、身份或会话值。 + /// 必须脱敏的 OAuth、身份或会话值 Sensitive(SecretValue), } impl GrokHeaderValue { - /// 仅在注入的 HTTP transport 边界处暴露取值。 + /// 仅在注入的 HTTP transport 边界处暴露取值 #[must_use] pub fn expose(&self) -> &str { match self { @@ -64,7 +66,7 @@ impl fmt::Debug for GrokHeaderValue { } } -/// 一个官方 Grok CLI 代理请求头。 +/// 一个官方 Grok CLI 代理请求头 #[derive(Debug, Clone)] pub struct GrokHeader { name: String, @@ -72,13 +74,13 @@ pub struct GrokHeader { } impl GrokHeader { - /// 返回 header 名。 + /// 返回 header 名 #[must_use] pub fn name(&self) -> &str { &self.name } - /// 返回类型化的 header 值。 + /// 返回类型化的 header 值 #[must_use] pub const fn value(&self) -> &GrokHeaderValue { &self.value diff --git a/backend/crates/providers/xai/src/transport/inference.rs b/backend/crates/providers/xai/src/transport/inference.rs index bc55fe80e..bce28254d 100644 --- a/backend/crates/providers/xai/src/transport/inference.rs +++ b/backend/crates/providers/xai/src/transport/inference.rs @@ -1,3 +1,5 @@ +//! Grok 推理传输的请求、响应流、网络观测与错误端口合同 + use std::fmt; use std::future::Future; use std::pin::Pin; @@ -12,7 +14,7 @@ use zeroize::Zeroizing; use super::{GrokHeader, GrokSessionBinding}; -/// 交给注入的 HTTP SSE transport 的自持有请求。 +/// 交给注入的 HTTP SSE transport 的自持有请求 pub struct GrokInferenceRequest { trace: gateway_core::diagnostics::TraceContext, endpoint: Url, @@ -47,25 +49,25 @@ impl GrokInferenceRequest { &self.trace } - /// 返回严格限定的官方 Responses 端点。 + /// 返回严格限定的官方 Responses 端点 #[must_use] pub const fn endpoint(&self) -> &Url { &self.endpoint } - /// 返回类型化 header;适配层不得记录敏感值。 + /// 返回类型化 header;适配层不得记录敏感值 #[must_use] pub fn headers(&self) -> &[GrokHeader] { &self.headers } - /// 返回序列化后的类型化 Responses body。 + /// 返回序列化后的类型化 Responses body #[must_use] pub fn body(&self) -> &[u8] { &self.body } - /// 返回假名化的 proxy/egress 查找绑定。 + /// 返回假名化的 proxy/egress 查找绑定 #[must_use] pub const fn binding(&self) -> &GrokSessionBinding { &self.binding @@ -87,16 +89,16 @@ impl fmt::Debug for GrokInferenceRequest { } } -/// POST 被接受后返回的原始 SSE 字节块流。 +/// POST 被接受后返回的原始 SSE 字节块流 pub type GrokInferenceChunkStream = Pin> + Send + 'static>>; -/// 账号隔离的推理 client 是否已在缓存中。 +/// 账号隔离的推理 client 是否已在缓存中 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum GrokInferenceClientCacheStatus { - /// 绑定复用了已有的账号隔离 client。 + /// 绑定复用了已有的账号隔离 client Hit, - /// 请求首次查缓存时该绑定不存在。 + /// 请求首次查缓存时该绑定不存在 Miss, } @@ -110,12 +112,12 @@ impl GrokInferenceClientCacheStatus { } } -/// 提供本次请求所用地址的解析器。 +/// 提供本次请求所用地址的解析器 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum GrokInferenceDnsSource { - /// 系统解析器返回了完全公网的地址集合。 + /// 系统解析器返回了完全公网的地址集合 System, - /// 系统结果不可用,由受信 DoH 回退提供地址。 + /// 系统结果不可用,由受信 DoH 回退提供地址 TrustedDoh, } @@ -129,7 +131,7 @@ impl GrokInferenceDnsSource { } } -/// 建立上游连接期间观测到的 DNS 工作量。 +/// 建立上游连接期间观测到的 DNS 工作量 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct GrokInferenceDnsObservation { source: GrokInferenceDnsSource, @@ -156,7 +158,7 @@ impl GrokInferenceDnsObservation { } } -/// 单次推理请求的低基数 transport 耗时与连接池事实。 +/// 单次推理请求的低基数 transport 耗时与连接池事实 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct GrokInferenceTransportMetrics { headers_ms: Option, @@ -202,8 +204,9 @@ impl GrokInferenceTransportMetrics { } } -/// 已被接受的推理响应。非成功 HTTP 响应必须改为返回 -/// [`GrokInferenceTransportError`]。 +/// 已被接受的推理响应 +/// 非成功 HTTP 响应必须改为返回 +/// [`GrokInferenceTransportError`] pub struct GrokInferenceResponse { body: GrokInferenceChunkStream, http_version: UpstreamHttpVersion, @@ -213,7 +216,7 @@ pub struct GrokInferenceResponse { } impl GrokInferenceResponse { - /// 包装一个已被接受的 SSE 响应体。 + /// 包装一个已被接受的 SSE 响应体 #[must_use] pub fn new( body: GrokInferenceChunkStream, @@ -277,44 +280,44 @@ impl fmt::Debug for GrokInferenceResponse { } } -/// 不含密钥的 transport 失败分类。 +/// 不含密钥的 transport 失败分类 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum GrokInferenceTransportErrorKind { - /// 官方代理拒绝了 canonical 请求语义。 + /// 官方代理拒绝了 canonical 请求语义 InvalidRequest, - /// 官方代理不支持请求的某项能力。 + /// 官方代理不支持请求的某项能力 Unsupported, - /// access token 被拒绝。 + /// access token 被拒绝 Unauthorized, - /// 会话缺少模型或特性授权。 + /// 会话缺少模型或特性授权 PermissionDenied, - /// 会话级或 Provider 级限流。 + /// 会话级或 Provider 级限流 RateLimited, - /// 账号付费额度或 spending limit 耗尽。 + /// 账号付费额度或 spending limit 耗尽 QuotaExhausted, - /// 账号订阅级免费额度耗尽。 + /// 账号订阅级免费额度耗尽 FreeQuotaExhausted, - /// 上游声明指定模型的免费用量耗尽;selector 会收敛为账号级可恢复额度状态。 + /// 上游声明指定模型的免费用量耗尽;selector 会收敛为账号级可恢复额度状态 ModelQuotaExhausted, - /// 当前账号只缺少指定模型的访问权限。 + /// 当前账号只缺少指定模型的访问权限 ModelAccessDenied, - /// 上游要求付费,但没有给出可证明账号额度耗尽的稳定信号。 + /// 上游要求付费,但没有给出可证明账号额度耗尽的稳定信号 PaymentRequired, - /// 当前请求被内容安全策略拒绝,不代表账号或模型不可用。 + /// 当前请求被内容安全策略拒绝,不代表账号或模型不可用 SafetyRejected, - /// 截止时间或 transport 超时。 + /// 截止时间或 transport 超时 Timeout, - /// 网络/TLS/连接失败。 + /// 网络/TLS/连接失败 Transport, - /// HTTP/SSE 响应违反预期契约。 + /// HTTP/SSE 响应违反预期契约 Protocol, - /// 官方 CLI 代理不可用。 + /// 官方 CLI 代理不可用 Unavailable, - /// transport 观测到调用方取消。 + /// transport 观测到调用方取消 Cancelled, } -/// 已分类的 transport 错误,绝不包含上游响应体。 +/// 已分类的 transport 错误,绝不包含上游响应体 #[derive(Clone, PartialEq, Eq)] pub struct GrokInferenceTransportError { kind: GrokInferenceTransportErrorKind, @@ -332,7 +335,7 @@ pub struct GrokInferenceTransportError { } impl GrokInferenceTransportError { - /// 以 transport 保守判定的发送状态创建已分类错误。 + /// 以 transport 保守判定的发送状态创建已分类错误 #[must_use] pub const fn new(kind: GrokInferenceTransportErrorKind, send_state: UpstreamSendState) -> Self { Self { @@ -355,7 +358,7 @@ impl GrokInferenceTransportError { } } - /// 保存 transport 提取的安全诊断,避免 Provider 映射时丢失原因。 + /// 保存 transport 提取的安全诊断,避免 Provider 映射时丢失原因 #[must_use] pub fn with_diagnostic(mut self, diagnostic: ProviderDiagnostic) -> Self { self.diagnostic = Some(Box::new(diagnostic)); @@ -367,7 +370,7 @@ impl GrokInferenceTransportError { self.diagnostic.as_deref() } - /// 附着合法的 HTTP 状态码。 + /// 附着合法的 HTTP 状态码 #[must_use] pub fn with_status(mut self, status: u16) -> Self { if (100..=599).contains(&status) { @@ -376,7 +379,7 @@ impl GrokInferenceTransportError { self } - /// 附着 transport 解析出的有界重试延迟。 + /// 附着 transport 解析出的有界重试延迟 #[must_use] pub const fn with_retry_after(mut self, retry_after: Duration) -> Self { self.retry_after = Some(retry_after); @@ -394,14 +397,14 @@ impl GrokInferenceTransportError { self } - /// 附着从错误 JSON 中提取并清洗后的稳定机器码。 + /// 附着从错误 JSON 中提取并清洗后的稳定机器码 #[must_use] pub fn with_upstream_code(mut self, code: OpaqueUpstreamValue) -> Self { self.upstream_code = Some(Box::new(code)); self } - /// 附着仅供原客户端协议展示的结构化上游错误。 + /// 附着仅供原客户端协议展示的结构化上游错误 #[must_use] pub fn with_client_visible_upstream_error(mut self, error: ClientVisibleUpstreamError) -> Self { self.client_visible_upstream_error = Some(Box::new(error)); @@ -423,32 +426,32 @@ impl GrokInferenceTransportError { self } - /// 丢弃可能敏感的上游响应体,仅保留「已丢弃」这一事实。 + /// 丢弃可能敏感的上游响应体,仅保留「已丢弃」这一事实 #[must_use] pub fn redact_sensitive_context(mut self, _value: impl AsRef) -> Self { self.sensitive_context_redacted = true; self } - /// 返回稳定的 transport 分类。 + /// 返回稳定的 transport 分类 #[must_use] pub const fn kind(&self) -> GrokInferenceTransportErrorKind { self.kind } - /// 返回保守判定的 payload 发送状态。 + /// 返回保守判定的 payload 发送状态 #[must_use] pub const fn send_state(&self) -> UpstreamSendState { self.send_state } - /// 返回清洗后的 HTTP 状态码。 + /// 返回清洗后的 HTTP 状态码 #[must_use] pub const fn status(&self) -> Option { self.status } - /// 返回可选的重试延迟。 + /// 返回可选的重试延迟 #[must_use] pub const fn retry_after(&self) -> Option { self.retry_after @@ -484,7 +487,7 @@ impl GrokInferenceTransportError { self.credential_recovery_required } - /// 报告是否丢弃过敏感响应体。 + /// 报告是否丢弃过敏感响应体 #[must_use] pub const fn sensitive_context_was_redacted(&self) -> bool { self.sensitive_context_redacted @@ -535,16 +538,17 @@ impl fmt::Display for GrokInferenceTransportError { impl std::error::Error for GrokInferenceTransportError {} -/// 推理 transport 返回的 future。 +/// 推理 transport 返回的 future pub type GrokInferenceTransportFuture<'a> = Pin< Box< dyn Future> + Send + 'a, >, >; -/// 运行时 HTTP SSE 端口。实现必须只发出一次 POST,用传入的 session binding -/// 维持 egress 亲和,且不得重试、切换凭据或回退到其他端点。 +/// 运行时 HTTP SSE 端口 +/// 实现必须只发出一次 POST,用传入的 session binding +/// 维持 egress 亲和,且不得重试、切换凭据或回退到其他端点 pub trait GrokInferenceTransport: Send + Sync { - /// 发起一次官方 CLI 代理请求。 + /// 发起一次官方 CLI 代理请求 fn execute(&self, request: GrokInferenceRequest) -> GrokInferenceTransportFuture<'_>; } diff --git a/backend/crates/providers/xai/src/transport/mod.rs b/backend/crates/providers/xai/src/transport/mod.rs index 75413b664..d7fe899a2 100644 --- a/backend/crates/providers/xai/src/transport/mod.rs +++ b/backend/crates/providers/xai/src/transport/mod.rs @@ -1,4 +1,4 @@ -//! Grok Build CLI Responses provider 边界。 +//! Grok Build CLI Responses provider 边界 pub(crate) mod canonical; pub(crate) mod catalog; @@ -111,9 +111,8 @@ pub(crate) fn classify_grok_quota_failure( let code = code.map(str::to_ascii_lowercase); let error_type = error_type.map(str::to_ascii_lowercase); let message = message.map(str::to_ascii_lowercase); - // `code` and `type` are stable upstream fields. Do not let a conflicting - // human-readable message override them; use the message only as a - // fallback or to refine a confirmed free-usage signal's rolling window. + // `code` 与 `type` 是稳定的上游字段,冲突的可读消息不能覆盖它们 + // 消息仅用于回退判断,或细化已确认免费额度信号的滚动窗口 let structured_signal = code .as_deref() .and_then(classify_quota_signal) @@ -156,7 +155,7 @@ fn contains_any(value: &str, needles: &[&str]) -> bool { } /// 把 message 中的 UUID 替换为占位符,控制字符归一为空格, -/// 使文本满足客户端可见错误的约束且不携带可定位账号的标识。 +/// 使文本满足客户端可见错误的约束且不携带可定位账号的标识 fn scrub_account_fingerprints(message: &str) -> String { let bytes = message.as_bytes(); let mut scrubbed = String::with_capacity(message.len()); diff --git a/backend/crates/providers/xai/src/transport/network.rs b/backend/crates/providers/xai/src/transport/network.rs index 6436dbc11..ac9aba3e2 100644 --- a/backend/crates/providers/xai/src/transport/network.rs +++ b/backend/crates/providers/xai/src/transport/network.rs @@ -1,4 +1,4 @@ -//! Explicit account egress with redirects and business retries disabled. +//! xAI 显式账号出口传输,禁用自动重定向与业务重试 use gateway_core::diagnostics::{StreamCapture, StreamFormat, TraceContext}; use std::fmt; @@ -67,31 +67,31 @@ tokio::task_local! { static REQUEST_DNS_OBSERVER: Arc; } -/// 构建严格 reqwest transport 失败。 +/// 构建严格 reqwest transport 失败 #[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)] pub enum GrokReqwestTransportBuildError { - /// Reqwest TLS/client 初始化失败。 + /// Reqwest TLS/client 初始化失败 #[error("Grok reqwest transport initialization failed")] ClientInitialization, } -/// 固定官方 host 的 DNS 解析路径;只有系统结果全部为公网地址时才直接使用。 +/// 固定官方 host 的 DNS 解析路径;只有系统结果全部为公网地址时才直接使用 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum GrokDnsResolutionPlan { - /// 使用系统 resolver 返回的全部公网地址。 + /// 使用系统 resolver 返回的全部公网地址 System, - /// 系统解析失败、为空或包含非公网地址,改用固定 bootstrap 的可信 DoH。 + /// 系统解析失败、为空或包含非公网地址,改用固定 bootstrap 的可信 DoH TrustedDoh, } -/// xAI 官方 host 的 DNS rebinding 防护策略。 +/// xAI 官方 host 的 DNS rebinding 防护策略 #[derive(Debug, Clone, Copy)] pub struct GrokDnsResolutionPolicy { allowed_host: &'static str, } impl GrokDnsResolutionPolicy { - /// OAuth、JWKS 与 user-info 官方 host 策略。 + /// OAuth、JWKS 与 user-info 官方 host 策略 #[must_use] pub const fn official_oauth() -> Self { Self { @@ -99,7 +99,7 @@ impl GrokDnsResolutionPolicy { } } - /// 推理与模型目录官方 host 策略。 + /// 推理与模型目录官方 host 策略 #[must_use] pub const fn official_inference() -> Self { Self { @@ -107,11 +107,11 @@ impl GrokDnsResolutionPolicy { } } - /// 决定系统解析结果可直接使用还是必须走可信 DoH。 + /// 决定系统解析结果可直接使用还是必须走可信 DoH /// /// # Errors /// - /// 请求 host 不等于本策略固定的官方 host 时拒绝,且不会触发 fallback。 + /// 请求 host 不等于本策略固定的官方 host 时拒绝,且不会触发 fallback pub fn plan_system_resolution( self, requested_host: &str, @@ -127,11 +127,11 @@ impl GrokDnsResolutionPolicy { ) } - /// 验证可信 DoH 返回的整个地址集合;任一非公网地址会拒绝全部结果。 + /// 验证可信 DoH 返回的整个地址集合;任一非公网地址会拒绝全部结果 /// /// # Errors /// - /// Host 不匹配、结果为空或任一地址非公网时拒绝。 + /// Host 不匹配、结果为空或任一地址非公网时拒绝 pub fn validate_trusted_doh_resolution( self, requested_host: &str, @@ -158,12 +158,12 @@ impl GrokDnsResolutionPolicy { } } -/// DNS policy 低基数错误;不保留请求 host、地址或 resolver 正文。 +/// DNS policy 低基数错误;不保留请求 host、地址或 resolver 正文 #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] #[error("Grok official DNS resolution was rejected")] pub struct GrokDnsResolutionError; -/// HTTP client 构造与端点校验合为一个可注入的安全边界。 +/// HTTP client 构造与端点校验合为一个可注入的安全边界 pub trait GrokEndpointPolicy: fmt::Debug + Send + Sync { fn build_oauth_client( &self, @@ -236,18 +236,19 @@ impl GrokEndpointPolicy for OfficialGrokEndpointPolicy { } } -/// 官方 OAuth HTTP transport。只允许 `auth.x.ai:443`。 +/// 官方 OAuth HTTP transport +/// 只允许 `auth.x.ai:443` pub struct ReqwestOAuthTransport { clients: EgressCache, Client>, endpoint_policy: Arc, } impl ReqwestOAuthTransport { - /// 使用系统原生根证书构建生产 transport。 + /// 使用系统原生根证书构建生产 transport /// /// # Errors /// - /// TLS client 初始化失败时返回错误。 + /// TLS client 初始化失败时返回错误 pub fn new( endpoint_policy: Arc, ) -> Result { @@ -318,7 +319,7 @@ impl OAuthHttpTransport for ReqwestOAuthTransport { } } -/// 官方 Grok Responses HTTP SSE transport。 +/// 官方 Grok Responses HTTP SSE transport pub struct ReqwestGrokInferenceTransport { clients: EgressCache, unbound_client: Mutex>, @@ -326,14 +327,14 @@ pub struct ReqwestGrokInferenceTransport { } impl ReqwestGrokInferenceTransport { - /// 单个进程缓存的账号隔离推理连接池上限。 + /// 单个进程缓存的账号隔离推理连接池上限 pub const MAX_CACHED_ACCOUNT_CLIENTS: usize = MAX_CACHED_EGRESS_STATES; - /// 构建只允许官方 CLI proxy 的生产 transport。 + /// 构建只允许官方 CLI proxy 的生产 transport /// /// # Errors /// - /// TLS client 初始化失败时返回错误。 + /// TLS client 初始化失败时返回错误 pub fn new( endpoint_policy: Arc, ) -> Result { @@ -453,7 +454,7 @@ impl GrokInferenceTransport for ReqwestGrokInferenceTransport { .is_some_and(|total| total <= MAX_INFERENCE_BODY_BYTES) => { *observed += chunk.len(); - // Bytes 直传,长流不再逐 chunk 复制到 Vec。 + // Bytes 直传,长流不再逐 chunk 复制到 Vec Ok(chunk) } Ok(_) => Err(GrokInferenceTransportError::new( @@ -518,18 +519,18 @@ fn inference_transport_metrics( metrics } -/// 官方 Grok CLI proxy 模型目录 GET transport。 +/// 官方 Grok CLI proxy 模型目录 GET transport pub struct ReqwestGrokModelCatalogTransport { clients: EgressCache, Client>, endpoint_policy: Arc, } impl ReqwestGrokModelCatalogTransport { - /// 构建只允许官方 CLI proxy `/v1/models` 的生产 transport。 + /// 构建只允许官方 CLI proxy `/v1/models` 的生产 transport /// /// # Errors /// - /// TLS client 初始化失败时返回错误。 + /// TLS client 初始化失败时返回错误 pub fn new( endpoint_policy: Arc, ) -> Result { @@ -684,7 +685,7 @@ fn build_official_client( proxy: Option<&OutboundProxy>, ) -> Result { let mut builder = Client::builder() - // 工作区可能同时启用 native-tls;与官方 Grok CLI 一样显式固定 rustls,避免握手画像漂移。 + // 工作区可能同时启用 native-tls;与官方 Grok CLI 一样显式固定 rustls,避免握手画像漂移 .use_rustls_tls() .redirect(Policy::none()) .no_proxy() @@ -699,7 +700,7 @@ fn build_official_client( .https_only(true); if let Some(proxy) = proxy { // 保留 reqwest 的代理解析语义(socks5 本地解析、socks5h 远端解析); - // 只允许官方 host 的直连 resolver 不能用于解析代理端点。 + // 只允许官方 host 的直连 resolver 不能用于解析代理端点 builder = builder.proxy( reqwest::Proxy::all(proxy.expose_url()) .map_err(|_| GrokReqwestTransportBuildError::ClientInitialization)?, @@ -1142,7 +1143,7 @@ struct InferenceErrorMetadata { code: Option, error_type: Option, message: Option, - // 仅结构化 JSON 的 message 可进入客户端协议;纯文本正文只参与内部分类。 + // 仅结构化 JSON 的 message 可进入客户端协议;纯文本正文只参与内部分类 client_message: Option, } diff --git a/backend/crates/providers/xai/src/transport/profile.rs b/backend/crates/providers/xai/src/transport/profile.rs index aeab85cb3..cccfa783e 100644 --- a/backend/crates/providers/xai/src/transport/profile.rs +++ b/backend/crates/providers/xai/src/transport/profile.rs @@ -1,4 +1,4 @@ -//! Grok CLI 运行时 wire profile。 +//! Grok CLI 运行时 wire profile use std::{ sync::{Arc, RwLock}, @@ -11,7 +11,7 @@ use reqwest::{Client, redirect::Policy}; use serde::{Deserialize, Serialize}; use url::Url; -/// Grok CLI 运行时身份;默认值作为官方发布检查的内置基线。 +/// Grok CLI 运行时身份;默认值作为官方发布检查的内置基线 #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] pub struct XaiWireProfile { pub client_identifier: String, @@ -38,7 +38,7 @@ impl Default for XaiWireProfile { impl XaiWireProfile { #[must_use] pub fn user_agent(&self) -> String { - // Grok CLI 的产品名独立于 x-grok-client-identifier;自定义请求头不替换 UA 产品名。 + // Grok CLI 的产品名独立于 x-grok-client-identifier;自定义请求头不替换 UA 产品名 let arch = match self.target_arch.as_str() { "arm64" => "aarch64", arch => arch, @@ -211,7 +211,7 @@ impl GrokCliReleaseService { } } -/// 最近一次官方 Grok CLI 发布检查结果。 +/// 最近一次官方 Grok CLI 发布检查结果 #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct GrokCliReleaseSnapshot { pub checked_at: Option>, @@ -219,7 +219,7 @@ pub struct GrokCliReleaseSnapshot { pub last_error: Option, } -/// Provider 内共享的 Grok CLI 发布检查观察状态。 +/// Provider 内共享的 Grok CLI 发布检查观察状态 #[derive(Debug, Clone, Default)] pub struct GrokCliReleaseStatus { snapshot: Arc>, diff --git a/backend/crates/providers/xai/src/transport/request/history.rs b/backend/crates/providers/xai/src/transport/request/history.rs index c835f1b0d..1c6f5a5e4 100644 --- a/backend/crates/providers/xai/src/transport/request/history.rs +++ b/backend/crates/providers/xai/src/transport/request/history.rs @@ -1,4 +1,4 @@ -//! OpenAI history 到 Grok Build history 的重建与清理。 +//! OpenAI history 到 Grok Build history 的重建与清理 use super::*; @@ -20,7 +20,7 @@ pub(super) fn require_content_source( pub(super) fn sanitize_reasoning_input(item: &Map) -> Map { let mut converted = copy_non_null_history_fields(item, &["id", "summary", "content", "encrypted_content"]); - // Grok CLI 会在 summary/content 条目上注入 phase 等内部键。 + // Grok CLI 会在 summary/content 条目上注入 phase 等内部键 strip_grok_internal_entry_keys(&mut converted, &["summary", "content"]); converted.insert("type".to_owned(), Value::String("reasoning".to_owned())); if has_portable_reasoning_content(&converted) { @@ -40,7 +40,7 @@ pub(super) fn normalize_compaction_input( .and_then(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()); - // 密文只作为 reasoning 回放;可见摘要只读取 summary。 + // 密文只作为 reasoning 回放;可见摘要只读取 summary let mut converted = Vec::with_capacity(2); if let Some(encrypted) = encrypted { converted.push(json!({ @@ -157,7 +157,7 @@ pub(super) fn sanitize_native_history_input( }; let mut converted = copy_non_null_history_fields(item, fields); // Grok CLI 会在 shell_call 的 action object 里注入内部键与 null 占位 - // 字段(如 `timeout_ms: null`);只在这一层剥离,深层内容原样保留。 + // 字段(如 `timeout_ms: null`);只在这一层剥离,深层内容原样保留 if item_type == "shell_call" && let Some(Value::Object(action)) = converted.get_mut("action") { diff --git a/backend/crates/providers/xai/src/transport/request/identity.rs b/backend/crates/providers/xai/src/transport/request/identity.rs index 8be18003b..ff8e7cf6e 100644 --- a/backend/crates/providers/xai/src/transport/request/identity.rs +++ b/backend/crates/providers/xai/src/transport/request/identity.rs @@ -1,4 +1,4 @@ -//! 会话身份、租户隔离与缓存路由。 +//! 会话身份、租户隔离与缓存路由 use super::*; @@ -62,8 +62,9 @@ pub(super) fn sanitize_account_identity(body: &mut Map) { pub(super) fn sanitize_client_metadata(body: &mut Map) { // Codex 的 client_metadata 是本地 transport envelope,可能包含工作目录、仓库地址、 - // installation/session 标识。会话亲和信息已在调用本函数前提取,整个 envelope 都不能 - // 越过 Grok Build 边界。 + // installation/session 标识 + // 会话亲和信息已在调用本函数前提取,整个 envelope 都不能 + // 越过 Grok Build 边界 body.remove("client_metadata"); body.remove("metadata"); } @@ -88,7 +89,7 @@ pub(super) fn enable_grok_prompt_cache_route( tools.push(json_object([("type", Value::String((*tool).to_owned()))])); response.mark_injected_cache_tool(tool); } - // 无客户端工具时以 none 选中缓存路由,同时不授予搜索能力。 + // 无客户端工具时以 none 选中缓存路由,同时不授予搜索能力 body.insert("tool_choice".to_owned(), Value::String("none".to_owned())); } else if !has_tool_type(tools, "x_search") { tools.push(json_object([( diff --git a/backend/crates/providers/xai/src/transport/request/mod.rs b/backend/crates/providers/xai/src/transport/request/mod.rs index e9d813450..bcfa08aa2 100644 --- a/backend/crates/providers/xai/src/transport/request/mod.rs +++ b/backend/crates/providers/xai/src/transport/request/mod.rs @@ -1,4 +1,4 @@ -//! OpenAI Responses 请求到官方 Grok Build wire 的转换边界。 +//! OpenAI Responses 请求到官方 Grok Build wire 的转换边界 use std::collections::{BTreeMap, BTreeSet}; use std::fmt; @@ -27,11 +27,11 @@ use schema::*; use tools::*; /// Grok CLI 在历史条目上注入、Grok Build 无法反序列化的内部键;只在已知 -/// 注入位置剥离,避免误伤工具 schema 或输出中恰好同名的键。 +/// 注入位置剥离,避免误伤工具 schema 或输出中恰好同名的键 const GROK_INTERNAL_HISTORY_KEYS: &[&str] = &["phase", "internal_chat_message_metadata_passthrough"]; -/// 保留客户端 OpenAI Responses object 的 xAI 上游请求。 +/// 保留客户端 OpenAI Responses object 的 xAI 上游请求 pub struct GrokResponsesRequest { body: Map, session_id: Option, @@ -41,24 +41,24 @@ pub struct GrokResponsesRequest { } impl GrokResponsesRequest { - /// 返回发送到 `/v1/responses` 的 JSON object。 + /// 返回发送到 `/v1/responses` 的 JSON object #[must_use] pub const fn body(&self) -> &Map { &self.body } - /// 返回按下游租户隔离后的稳定 Grok 会话 UUID。 + /// 返回按下游租户隔离后的稳定 Grok 会话 UUID #[must_use] pub fn session_id(&self) -> Option<&str> { self.session_id.as_deref() } - /// 返回只有显式会话身份才启用的 reasoning replay scope。 + /// 返回只有显式会话身份才启用的 reasoning replay scope pub(crate) fn reasoning_replay_session_id(&self) -> Option<&str> { self.reasoning_replay_session_id.as_deref() } - /// 返回归一化后的 xAI wire 模型。 + /// 返回归一化后的 xAI wire 模型 pub(crate) fn upstream_model(&self) -> Option<&str> { self.body.get("model").and_then(Value::as_str) } @@ -75,7 +75,7 @@ impl GrokResponsesRequest { self.body.get("input").and_then(Value::as_array).cloned() } - /// 返回与会话一致、额外绑定模型的账号亲和键。 + /// 返回与会话一致、额外绑定模型的账号亲和键 #[must_use] pub const fn affinity(&self) -> Option<&GrokSessionAffinityKey> { self.affinity.as_ref() @@ -119,10 +119,10 @@ impl GrokResponsesRequest { Ok(()) } - /// 为同账号的一次 xAI `invalid_encrypted_content` 恢复请求移除被拒绝的密文。 + /// 为同账号的一次 xAI `invalid_encrypted_content` 恢复请求移除被拒绝的密文 /// /// 只在至少一个 reasoning item 含非空密文时改写;可读 summary/content、ID 与 - /// 其他历史保持不变,剥离后只剩 `type` 的空壳一并删除。 + /// 其他历史保持不变,剥离后只剩 `type` 的空壳一并删除 pub(crate) fn strip_invalid_encrypted_reasoning(&mut self) -> bool { strip_invalid_encrypted_reasoning_from_body(&mut self.body) } @@ -206,8 +206,8 @@ impl GrokResponsesRequest { consume_terminal_compaction_trigger(&mut body)?; } let upstream_model = resolve_grok_text_responses_model_id(upstream_model); - // 这些字段属于 Codex/OpenAI 侧请求控制,不是 xAI 上游协议字段。 - // OpenAI 透明路径会保留未知字段;这里只在 xAI adapter 内做最小剥离。 + // 这些字段属于 Codex/OpenAI 侧请求控制,不是 xAI 上游协议字段 + // OpenAI 透明路径会保留未知字段;这里只在 xAI adapter 内做最小剥离 body.remove("provider_options"); body.remove("service_tier"); let session_seed = explicit_session_seed(request, &body); @@ -316,19 +316,19 @@ impl fmt::Debug for GrokResponsesRequest { } } -/// Generate 到 Responses 的编码错误,不保留 option 与 prompt 值。 +/// Generate 到 Responses 的编码错误,不保留 option 与 prompt 值 #[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)] pub enum GrokRequestEncodeError { - /// 数据面只接受 OpenAI adapter 保留的原始 Responses object。 + /// 数据面只接受 OpenAI adapter 保留的原始 Responses object #[error("Grok Build request is missing its OpenAI protocol payload")] InvalidProtocolPayload, - /// JSON 序列化意外失败。 + /// JSON 序列化意外失败 #[error("Grok Build request serialization failed")] Serialization, - /// Responses 兼容字段无法安全归一化。 + /// Responses 兼容字段无法安全归一化 #[error("Grok Build request normalization failed")] InvalidRequestNormalization, - /// 请求中的具体字段无法安全转换为 Grok Build 接受的形态。 + /// 请求中的具体字段无法安全转换为 Grok Build 接受的形态 #[error("Grok Build request field `{field}` could not be normalized safely")] InvalidRequestField { field: &'static str }, } diff --git a/backend/crates/providers/xai/src/transport/request/model.rs b/backend/crates/providers/xai/src/transport/request/model.rs index 0f6019c24..28f805f02 100644 --- a/backend/crates/providers/xai/src/transport/request/model.rs +++ b/backend/crates/providers/xai/src/transport/request/model.rs @@ -1,4 +1,4 @@ -//! 当前 Responses 参数到 Grok 模型能力的转换。 +//! 当前 Responses 参数到 Grok 模型能力的转换 use super::*; diff --git a/backend/crates/providers/xai/src/transport/request/response.rs b/backend/crates/providers/xai/src/transport/request/response.rs index 8d879f40b..1926381bc 100644 --- a/backend/crates/providers/xai/src/transport/request/response.rs +++ b/backend/crates/providers/xai/src/transport/request/response.rs @@ -1,4 +1,4 @@ -//! Grok Build 输出恢复为 OpenAI Responses wire 所需的状态。 +//! Grok Build 输出恢复为 OpenAI Responses wire 所需的状态 use super::*; @@ -454,7 +454,7 @@ impl GrokResponseTransform { } fn rewrite_response_value(&self, value: &mut Value) -> Result<(), GrokRequestEncodeError> { - // 只转换协议中的输出条目,避免把 metadata、schema 或工具业务结果当成调用。 + // 只转换协议中的输出条目,避免把 metadata、schema 或工具业务结果当成调用 if let Some(item) = value.get_mut("item").and_then(Value::as_object_mut) && string_field(item, "type") == "function_call" { @@ -762,7 +762,7 @@ pub(super) fn custom_tool_stream_payload( Value::Object(result) } -/// item ID 随工具类型转换,call_id 始终保留原始配对语义。 +/// item ID 随工具类型转换,call_id 始终保留原始配对语义 pub(super) fn retype_tool_item_id(item: &mut Map, field: &str, kind: &str) { let prefix = match kind { "function_call" => "fc_", diff --git a/backend/crates/providers/xai/src/transport/request/schema.rs b/backend/crates/providers/xai/src/transport/request/schema.rs index fb61bfd01..1bc5ee77e 100644 --- a/backend/crates/providers/xai/src/transport/request/schema.rs +++ b/backend/crates/providers/xai/src/transport/request/schema.rs @@ -1,8 +1,8 @@ -//! 工具 JSON Schema 校验与参数数值归一化。 +//! 工具 JSON Schema 校验与参数数值归一化 use super::*; -/// 仅移除参数根节点的 null 分支,保持嵌套 schema 的业务语义。 +/// 仅移除参数根节点的 null 分支,保持嵌套 schema 的业务语义 pub(super) fn normalize_function_parameters_root( value: &Value, ) -> Result, GrokRequestEncodeError> { diff --git a/backend/crates/providers/xai/src/transport/request/tools.rs b/backend/crates/providers/xai/src/transport/request/tools.rs index 7473cef33..75343651f 100644 --- a/backend/crates/providers/xai/src/transport/request/tools.rs +++ b/backend/crates/providers/xai/src/transport/request/tools.rs @@ -1,4 +1,4 @@ -//! Codex/OpenAI 工具声明与历史调用到 Grok 工具合同的归一化。 +//! Codex/OpenAI 工具声明与历史调用到 Grok 工具合同的归一化 use super::response::{ToolIdentity, ToolKind, retype_tool_item_id}; use super::*; @@ -184,7 +184,7 @@ impl ToolNormalizer { "x_search" | "collections_search" | "file_search" | "code_execution" | "code_interpreter" => Ok(vec![Value::Object(without_defer_loading(tool))]), // 对未获 xAI 接受的工具做静默过滤,并在过滤后同步收敛 tool_choice; - // 不要把客户端可选扩展升级成整单 400。 + // 不要把客户端可选扩展升级成整单 400 "" | "computer_use_preview" | "image_generation" => Ok(Vec::new()), _ => Ok(Vec::new()), } @@ -308,7 +308,7 @@ impl ToolNormalizer { } let identity = ToolIdentity::new(ToolKind::Custom, namespace, name); let argument_field = "input"; - // function 不支持 custom 的 grammar 字段,必须把约束连同原说明交给模型。 + // function 不支持 custom 的 grammar 字段,必须把约束连同原说明交给模型 let mut description = format!( "This function wraps a custom tool. The original instructions below describe the \ decoded {argument_field} string.\n{}", @@ -981,7 +981,7 @@ impl ToolNormalizer { }; let content = self.normalize_message_content(item.get("content"), role)?; // 透明代理合法形态:只覆盖归一化字段并剥离 Grok 注入键,未知官方 - // 字段原样保留。 + // 字段原样保留 let mut converted = item.clone(); strip_grok_internal_keys(&mut converted); converted.insert("type".to_owned(), Value::String("message".to_owned())); diff --git a/backend/crates/providers/xai/src/transport/session.rs b/backend/crates/providers/xai/src/transport/session.rs index 03706d35d..4859a9cf9 100644 --- a/backend/crates/providers/xai/src/transport/session.rs +++ b/backend/crates/providers/xai/src/transport/session.rs @@ -1,3 +1,5 @@ +//! Grok 会话选择、亲和键、账号租约与凭据失败反馈的传输合同 + use std::collections::BTreeSet; use std::fmt; use std::future::Future; @@ -16,16 +18,16 @@ use sha2::{Digest as _, Sha256}; use crate::SecretValue; -/// 注入的推理 transport 可识别的不透明假名化 egress/session 键。 +/// 注入的推理 transport 可识别的不透明假名化 egress/session 键 #[derive(Clone, PartialEq, Eq, Hash)] pub struct GrokSessionBinding(String, Option); impl GrokSessionBinding { - /// 创建有界的非敏感绑定引用。 + /// 创建有界的非敏感绑定引用 /// /// # Errors /// - /// 值为空、超长、含控制字符或使用保留前缀时返回错误。 + /// 值为空、超长、含控制字符或使用保留前缀时返回错误 pub fn new(value: impl Into) -> Result { let value = value.into(); if value.is_empty() @@ -38,7 +40,7 @@ impl GrokSessionBinding { Ok(Self(value, None)) } - /// 返回假名化的 transport 查找键。 + /// 返回假名化的 transport 查找键 #[must_use] pub fn as_str(&self) -> &str { &self.0 @@ -64,7 +66,7 @@ impl fmt::Debug for GrokSessionBinding { } } -/// 下游租户与显式会话共同派生的账号亲和键。 +/// 下游租户与显式会话共同派生的账号亲和键 #[derive(Clone, PartialEq, Eq)] pub struct GrokSessionAffinityKey([u8; 32]); @@ -73,7 +75,7 @@ impl GrokSessionAffinityKey { Self(digest) } - /// 为候选账号计算稳定 rendezvous 分值,原始会话值不会离开 Provider。 + /// 为候选账号计算稳定 rendezvous 分值,原始会话值不会离开 Provider pub(crate) fn score(&self, account_id: &ProviderAccountId) -> [u8; 32] { let mut hasher = Sha256::new(); hasher.update(b"xai-account-affinity-v1\0"); @@ -90,12 +92,12 @@ impl fmt::Debug for GrokSessionAffinityKey { } } -/// 由 selector 持有的凭据、容量与 egress 亲和守卫。 +/// 由 selector 持有的凭据、容量与 egress 亲和守卫 pub trait GrokSessionLeaseGuard: Send + Sync + 'static {} impl GrokSessionLeaseGuard for T where T: Send + Sync + 'static {} -/// 一个选中的 OAuth 会话及其持有的活跃 lease。 +/// 一个选中的 OAuth 会话及其持有的活跃 lease pub struct SelectedGrokSession { account_id: ProviderAccountId, credential_revision: CredentialRevision, @@ -109,11 +111,11 @@ pub struct SelectedGrokSession { } impl SelectedGrokSession { - /// 从 Provider 持有的明文 OAuth 材料构造选中会话。 + /// 从 Provider 持有的明文 OAuth 材料构造选中会话 /// /// # Errors /// - /// 快照缺少凭据,或 auth/header 值格式非法时返回错误。 + /// 快照缺少凭据,或 auth/header 值格式非法时返回错误 pub fn new( account_id: ProviderAccountId, credential_revision: CredentialRevision, @@ -144,7 +146,7 @@ impl SelectedGrokSession { }) } - /// 标记禁用账号的管理端诊断只读取真实上游结果,不回写账号状态。 + /// 标记禁用账号的管理端诊断只读取真实上游结果,不回写账号状态 #[must_use] pub(crate) const fn without_account_state_mutation(mut self) -> Self { self.allows_account_state_mutation = false; @@ -160,37 +162,37 @@ impl SelectedGrokSession { self } - /// 返回选中的账号 ID。 + /// 返回选中的账号 ID #[must_use] pub const fn account_id(&self) -> &ProviderAccountId { &self.account_id } - /// 返回 selector 冻结的凭据 revision。 + /// 返回 selector 冻结的凭据 revision #[must_use] pub const fn credential_revision(&self) -> CredentialRevision { self.credential_revision } - /// 返回用于显式构造 header 的 OAuth access token。 + /// 返回用于显式构造 header 的 OAuth access token #[must_use] pub const fn access_token(&self) -> &SecretValue { &self.access_token } - /// 返回用于官方代理 header 的已验证 user ID。 + /// 返回用于官方代理 header 的已验证 user ID #[must_use] pub const fn user_id(&self) -> &SecretValue { &self.user_id } - /// 返回用于官方代理 header 的可选已验证 email。 + /// 返回用于官方代理 header 的可选已验证 email #[must_use] pub const fn email(&self) -> Option<&SecretValue> { self.email.as_ref() } - /// 返回假名化的 egress/session transport 绑定。 + /// 返回假名化的 egress/session transport 绑定 #[must_use] pub const fn binding(&self) -> &GrokSessionBinding { &self.binding @@ -226,7 +228,7 @@ impl fmt::Debug for SelectedGrokSession { } } -/// 单次 selector 调用的入参,持有冻结且不含密钥的尝试视图。 +/// 单次 selector 调用的入参,持有冻结且不含密钥的尝试视图 #[derive(Debug, Clone)] pub struct GrokSessionSelection { upstream_model: UpstreamModelId, @@ -257,7 +259,7 @@ impl GrokSessionSelection { &self.cancellation } - /// 创建不可变的选择请求。 + /// 创建不可变的选择请求 #[must_use] pub fn new( upstream_model: UpstreamModelId, @@ -285,7 +287,7 @@ impl GrokSessionSelection { } } - /// 附着普通数据面请求冻结的插件策略;管理诊断保持空值。 + /// 附着普通数据面请求冻结的插件策略;管理诊断保持空值 #[must_use] pub(crate) fn with_request_policy( mut self, @@ -297,14 +299,14 @@ impl GrokSessionSelection { self } - /// 附着仅由显式客户端会话派生的账号亲和键。 + /// 附着仅由显式客户端会话派生的账号亲和键 #[must_use] pub fn with_affinity(mut self, affinity: Option) -> Self { self.affinity = affinity; self } - /// 延续 Core 的请求级排队预算。 + /// 延续 Core 的请求级排队预算 #[must_use] pub(crate) fn with_concurrency_wait_budget( mut self, @@ -320,7 +322,7 @@ impl GrokSessionSelection { &self.concurrency_wait_budget } - /// 为固定账号的管理端诊断指定本地可用性判定策略。 + /// 为固定账号的管理端诊断指定本地可用性判定策略 #[must_use] pub(crate) const fn with_eligibility_policy( mut self, @@ -330,25 +332,25 @@ impl GrokSessionSelection { self } - /// 返回冻结的上游模型。 + /// 返回冻结的上游模型 #[must_use] pub const fn upstream_model(&self) -> &UpstreamModelId { &self.upstream_model } - /// 返回协调方已尝试过的账号。 + /// 返回协调方已尝试过的账号 #[must_use] pub const fn excluded_accounts(&self) -> &BTreeSet { &self.excluded_accounts } - /// 返回 Core 约束下本次调用唯一允许的账号。 + /// 返回 Core 约束下本次调用唯一允许的账号 #[must_use] pub const fn required_account(&self) -> Option<&ProviderAccountId> { self.required_account.as_ref() } - /// 返回冻结的全局账号调度策略。 + /// 返回冻结的全局账号调度策略 #[must_use] pub const fn account_selection_policy(&self) -> AccountSelectionPolicy { self.account_selection_policy @@ -359,13 +361,13 @@ impl GrokSessionSelection { self.eligibility } - /// 返回不含原始客户端会话值的账号亲和键。 + /// 返回不含原始客户端会话值的账号亲和键 #[must_use] pub const fn affinity(&self) -> Option<&GrokSessionAffinityKey> { self.affinity.as_ref() } - /// 返回限定调度租约的绝对截止时间。 + /// 返回限定调度租约的绝对截止时间 #[must_use] pub const fn deadline(&self) -> gateway_core::lifecycle::Deadline { self.deadline @@ -392,71 +394,71 @@ impl GrokSessionSelection { } } -/// Grok session selector 返回的 future。 +/// Grok session selector 返回的 future pub type GrokSessionSelectorFuture<'a> = Pin< Box> + Send + 'a>, >; -/// 已脱敏的上游失败反馈;selector 决定账号持久状态或细粒度运行时 cooldown。 +/// 已脱敏的上游失败反馈;selector 决定账号持久状态或细粒度运行时 cooldown #[derive(Debug, Clone, PartialEq, Eq)] pub enum GrokCredentialFailure { - /// 选中的 OAuth token 被拒绝。 + /// 选中的 OAuth token 被拒绝 Unauthorized, - /// 选中的 OAuth 账号被限流。 + /// 选中的 OAuth 账号被限流 RateLimited { - /// 已解析并脱敏的延迟;不保留上游原始 header。 + /// 已解析并脱敏的延迟;不保留上游原始 header retry_after: Option, }, - /// 选中的 OAuth 账号付费额度耗尽。 + /// 选中的 OAuth 账号付费额度耗尽 QuotaExhausted, - /// 选中的 OAuth 账号订阅级免费额度耗尽。 + /// 选中的 OAuth 账号订阅级免费额度耗尽 FreeQuotaExhausted, - /// HTTP 402 没有给出可证明账号额度耗尽的稳定信号。 + /// HTTP 402 没有给出可证明账号额度耗尽的稳定信号 PaymentRequired { - /// 已解析并脱敏的延迟;缺失时由 selector 使用短暂默认值。 + /// 已解析并脱敏的延迟;缺失时由 selector 使用短暂默认值 retry_after: Option, }, - /// 上游声明指定模型的免费用量耗尽;selector 会收敛为账号级可恢复额度状态。 + /// 上游声明指定模型的免费用量耗尽;selector 会收敛为账号级可恢复额度状态 ModelQuotaExhausted { upstream_model: UpstreamModelId, retry_after: Option, }, - /// 选中账号只缺少指定模型的访问权限。 + /// 选中账号只缺少指定模型的访问权限 ModelAccessDenied { upstream_model: UpstreamModelId, retry_after: Option, }, - /// 非内容策略的 403:当前账号的访问资格暂不可用。 + /// 非内容策略的 403:当前账号的访问资格暂不可用 AccessDenied, - /// 上游明确报告模型容量不足;重模型按模型隔离,其余账号短暂冷却。 + /// 上游明确报告模型容量不足;重模型按模型隔离,其余账号短暂冷却 ModelCapacity { upstream_model: UpstreamModelId }, - /// 上游返回空模型输出。 + /// 上游返回空模型输出 EmptyUpstream, - /// 上游 5xx/529 暂时不可用。 + /// 上游 5xx/529 暂时不可用 UpstreamUnavailable, - /// 成功的 SSE 响应在终止 Responses 事件前结束。 + /// 成功的 SSE 响应在终止 Responses 事件前结束 StreamInterrupted, } -/// 一次 best-effort 凭据反馈写入返回的 future。 +/// 一次 best-effort 凭据反馈写入返回的 future pub type GrokCredentialFeedbackFuture<'a> = Pin + Send + 'a>>; /// 运行时端口:选出恰好一个可用 OAuth 会话,并获取流生命周期所需的全部 -/// 凭据、容量与出口租约。 +/// 凭据、容量与出口租约 pub trait GrokSessionSelector: Send + Sync { - /// 执行一次选择,不做 Provider 内部回退。 + /// 执行一次选择,不做 Provider 内部回退 fn select(&self, request: GrokSessionSelection) -> GrokSessionSelectorFuture<'_>; - /// 记录一条已分类的失败反馈,不重试也不替换原错误。 + /// 记录一条已分类的失败反馈,不重试也不替换原错误 fn record_failure<'a>( &'a self, session: &'a SelectedGrokSession, failure: GrokCredentialFailure, ) -> GrokCredentialFeedbackFuture<'a>; - /// 成功完成真实上游请求后收敛账号的可恢复状态。 + /// 成功完成真实上游请求后收敛账号的可恢复状态 /// - /// 默认实现让只关心选择或失败反馈的测试替身无需模拟持久状态写入。 + /// 默认实现让只关心选择或失败反馈的测试替身无需模拟持久状态写入 fn record_success<'a>( &'a self, _: &'a SelectedGrokSession, @@ -465,53 +467,53 @@ pub trait GrokSessionSelector: Send + Sync { } } -/// 不含密钥的选择器失败。 +/// 不含密钥的选择器失败 #[derive(Debug, thiserror::Error, PartialEq, Eq)] pub enum GrokSessionSelectorError { #[error(transparent)] QueueRejected(#[from] gateway_core::concurrency::QueueRejection), - /// 没有会话同时满足模型、状态与排除约束。 + /// 没有会话同时满足模型、状态与排除约束 #[error("no eligible Grok Build session is available")] NoEligibleSession, - /// 所有可用会话当前均被租出或处于限速间隔。 + /// 所有可用会话当前均被租出或处于限速间隔 #[error("Grok Build session capacity is unavailable")] CapacityUnavailable { - /// 选择器推导出的可选延迟。 + /// 选择器推导出的可选延迟 retry_after: Option, }, - /// 每个可用会话都处于运行时冷却窗口内。 + /// 每个可用会话都处于运行时冷却窗口内 #[error("Grok Build account is cooling down")] AccountCoolingDown { - /// 最早恢复的会话剩余冷却时长。 + /// 最早恢复的会话剩余冷却时长 retry_after: Option, }, - /// 每个可用会话都只针对当前模型处于运行时冷却窗口内。 + /// 每个可用会话都只针对当前模型处于运行时冷却窗口内 #[error("Grok Build model is cooling down for the available accounts")] ModelCoolingDown { - /// 最早恢复的账号+模型组合剩余冷却时长。 + /// 最早恢复的账号+模型组合剩余冷却时长 retry_after: Option, }, - /// 会话元数据或 Provider 持有的明文密钥非法。 + /// 会话元数据或 Provider 持有的明文密钥非法 #[error("Grok Build session data is invalid")] InvalidSession, - /// 选择器依赖的后端服务不可用。 + /// 选择器依赖的后端服务不可用 #[error("Grok Build session selector is unavailable")] Unavailable, - /// 插件调度策略明确拒绝本次请求。 + /// 插件调度策略明确拒绝本次请求 #[error("account scheduling policy rejected the request")] PolicyRejected, - /// 插件调度策略调用失败且配置为拒绝。 + /// 插件调度策略调用失败且配置为拒绝 #[error("account scheduling policy is unavailable")] PolicyUnavailable, } -/// 构造选中会话时的失败。 +/// 构造选中会话时的失败 #[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)] pub enum GrokSessionDataError { - /// access token 或已验证身份头格式非法。 + /// access token 或已验证身份头格式非法 #[error("selected session contains an invalid secret value")] InvalidSecretValue, - /// 出口/会话绑定非法。 + /// 出口/会话绑定非法 #[error("selected session binding is invalid")] InvalidBinding, } diff --git a/backend/crates/providers/xai/tests/admin.rs b/backend/crates/providers/xai/tests/admin.rs index 79ff83693..de667cb1b 100644 --- a/backend/crates/providers/xai/tests/admin.rs +++ b/backend/crates/providers/xai/tests/admin.rs @@ -1,3 +1,5 @@ +//! 验证 xAI 管理能力、账号清理与后台目录任务的组装和错误处理 + use std::collections::BTreeMap; use std::num::NonZeroU32; use std::sync::{Arc, Mutex}; @@ -117,7 +119,7 @@ async fn account_unavailable_clears_real_selector_cooldowns_only_for_deleted_acc (), ) .unwrap(); - // cooldown 必须来自生产 selector 对真实反馈类型的处理,不能由 mock 手工伪造。 + // cooldown 必须来自生产 selector 对真实反馈类型的处理,不能由 mock 手工伪造 selector .record_failure( &session, @@ -1104,7 +1106,7 @@ mod errors { } seed_input(&store, &input).await.unwrap(); if expired { - // 新导入不接受已过期凭据;等待合法存入的短期凭据自然过期,验证历史账号刷新。 + // 新导入不接受已过期凭据;等待合法存入的短期凭据自然过期,验证历史账号刷新 let remaining = input.account.refresh_token_expires_at.unwrap() - Utc::now(); tokio::time::sleep( remaining.to_std().unwrap_or_default() + Duration::from_millis(10), @@ -1152,7 +1154,7 @@ mod errors { let store = MemoryProviderAccountStore::shared(); let input = create_input("refresh_busy", "synthetic-subject"); seed_input(&store, &input).await.unwrap(); - // 账号使用出站代理时不提前准备直连 discovery;占用失败不得触发任何代理请求。 + // 账号使用出站代理时不提前准备直连 discovery;占用失败不得触发任何代理请求 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); listener.set_nonblocking(true).unwrap(); let proxy = diff --git a/backend/crates/providers/xai/tests/credential/authorization_code.rs b/backend/crates/providers/xai/tests/credential/authorization_code.rs index cf1ddc837..d1da6c861 100644 --- a/backend/crates/providers/xai/tests/credential/authorization_code.rs +++ b/backend/crates/providers/xai/tests/credential/authorization_code.rs @@ -1,3 +1,5 @@ +//! 验证 xAI 授权回调输入、状态恢复与敏感字段脱敏 + use provider_xai::{ AuthorizationCallback, CallbackRejection, DiscoveryDocument, GrokOAuthConfig, PendingAuthorization, RedirectUriAllowlist, diff --git a/backend/crates/providers/xai/tests/credential/catalog.rs b/backend/crates/providers/xai/tests/credential/catalog.rs index 20e0a20c9..4835c4ce1 100644 --- a/backend/crates/providers/xai/tests/credential/catalog.rs +++ b/backend/crates/providers/xai/tests/credential/catalog.rs @@ -1,3 +1,5 @@ +//! 验证 xAI 目录缓存、并发合并与指定账号刷新 + use std::collections::VecDeque; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; @@ -137,7 +139,7 @@ impl GrokBillingTransport for QueueBillingTransport { } } -/// 每次读取都返回同一份无法解码的 catalog 文档的存储端口。 +/// 每次读取都返回同一份无法解码的 catalog 文档的存储端口 struct CorruptCatalogCachePort; impl ProviderCatalogCachePort for CorruptCatalogCachePort { @@ -568,7 +570,7 @@ async fn failed_plan_scope_is_skipped_and_surviving_plans_still_cache() { let store = MemoryProviderAccountStore::shared(); let account_store: Arc = store.clone(); let repository = GrokCredentialRepository::new(account_store); - // "plan:pro" 按 scope 排序在 "plan:standard" 之前,先消费失败响应。 + // "plan:pro" 按 scope 排序在 "plan:standard" 之前,先消费失败响应 let mut pro = create_input("plan-pro", "subject-pro"); pro.account.plan_type = Some("pro".to_owned()); seed_input(&store, &pro).await.expect("create pro account"); diff --git a/backend/crates/providers/xai/tests/credential/client.rs b/backend/crates/providers/xai/tests/credential/client.rs index 9f5ee3fd6..0d09ba6d8 100644 --- a/backend/crates/providers/xai/tests/credential/client.rs +++ b/backend/crates/providers/xai/tests/credential/client.rs @@ -1,4 +1,4 @@ -//! transport 与验证端口的外置 fixture 驱动契约。 +//! transport 与验证端口的外置 fixture 驱动契约 use std::collections::VecDeque; use std::future::ready; diff --git a/backend/crates/providers/xai/tests/credential/config.rs b/backend/crates/providers/xai/tests/credential/config.rs index c205115d1..243366311 100644 --- a/backend/crates/providers/xai/tests/credential/config.rs +++ b/backend/crates/providers/xai/tests/credential/config.rs @@ -1,3 +1,5 @@ +//! 验证 xAI OAuth 重定向白名单与发现端点同源约束 + use provider_xai::{ConfigError, GrokOAuthConfig, RedirectUriAllowlist}; #[test] diff --git a/backend/crates/providers/xai/tests/credential/contract.rs b/backend/crates/providers/xai/tests/credential/contract.rs index bedf10c23..179629c61 100644 --- a/backend/crates/providers/xai/tests/credential/contract.rs +++ b/backend/crates/providers/xai/tests/credential/contract.rs @@ -1,3 +1,5 @@ +//! 验证 xAI 凭据持久化、主体身份与版本比较更新 + use std::sync::Arc; use chrono::Utc; diff --git a/backend/crates/providers/xai/tests/credential/discovery.rs b/backend/crates/providers/xai/tests/credential/discovery.rs index 78cadaca5..9e0e31c40 100644 --- a/backend/crates/providers/xai/tests/credential/discovery.rs +++ b/backend/crates/providers/xai/tests/credential/discovery.rs @@ -1,3 +1,5 @@ +//! 验证 xAI OIDC 发现文档的来源、必要字段与签名算法限制 + use provider_xai::{DiscoveryDocument, FailureClass, GrokOAuthConfig, OAuthError}; const VALID_DISCOVERY: &str = include_str!("fixtures/discovery.json"); diff --git a/backend/crates/providers/xai/tests/credential/http.rs b/backend/crates/providers/xai/tests/credential/http.rs index 666d2dcba..6bc0bccc9 100644 --- a/backend/crates/providers/xai/tests/credential/http.rs +++ b/backend/crates/providers/xai/tests/credential/http.rs @@ -1,3 +1,5 @@ +//! 验证 OAuth HTTP 请求的诊断输出隐藏敏感表单值 + use url::Url; use provider_xai::{FormField, HttpHeader, OAuthHttpRequest, SecretValue}; diff --git a/backend/crates/providers/xai/tests/credential/import.rs b/backend/crates/providers/xai/tests/credential/import.rs index f6d0c70fd..81c0d9ef6 100644 --- a/backend/crates/providers/xai/tests/credential/import.rs +++ b/backend/crates/providers/xai/tests/credential/import.rs @@ -1,3 +1,5 @@ +//! 验证 xAI 凭据导入的刷新、身份验证、来源与 scope 约束 + use std::future::ready; use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -317,14 +319,14 @@ fn duplicate_refresh_token_entries_should_collapse_to_the_first() { .push(duplicate); // 同一 refresh token 重复出现时解析层去重,避免逐条 RT exchange - // 让先完成的轮换被后续重复条目作废。 + // 让先完成的轮换被后续重复条目作废 let parsed = GrokOAuthImportDocument::parse_json( &serde_json::to_vec(&document).expect("serialize fixture"), ) .expect("duplicate refresh tokens collapse at parse"); assert_eq!(parsed.into_entries().len(), 1); - // 不同 refresh token 的同名条目不受影响,重复检测仍归上游身份验证。 + // 不同 refresh token 的同名条目不受影响,重复检测仍归上游身份验证 let mut document: serde_json::Value = serde_json::from_slice(&oauth_account_document()).expect("fixture JSON"); let mut distinct = document["accounts"][0].clone(); diff --git a/backend/crates/providers/xai/tests/credential/mod.rs b/backend/crates/providers/xai/tests/credential/mod.rs index 3a249d844..f48673bf5 100644 --- a/backend/crates/providers/xai/tests/credential/mod.rs +++ b/backend/crates/providers/xai/tests/credential/mod.rs @@ -1,3 +1,5 @@ +//! xAI OAuth、凭据持久化、刷新与选号的测试入口 + mod authorization_code; mod catalog; mod client; diff --git a/backend/crates/providers/xai/tests/credential/oidc_verifier.rs b/backend/crates/providers/xai/tests/credential/oidc_verifier.rs index 342fe0c80..905605d7c 100644 --- a/backend/crates/providers/xai/tests/credential/oidc_verifier.rs +++ b/backend/crates/providers/xai/tests/credential/oidc_verifier.rs @@ -1,4 +1,4 @@ -//! 外置的 OIDC verifier 契约。 +//! 外置的 OIDC verifier 契约 use std::time::Duration; @@ -54,7 +54,7 @@ async fn proxy_jwks_cache_evicts_idle_exits_at_the_fixed_bound() { .await .unwrap(); } - // 65 个出口各取一次,最近出口命中,最旧出口被淘汰后重新获取。 + // 65 个出口各取一次,最近出口命中,最旧出口被淘汰后重新获取 assert_eq!(exit.received_requests().await.unwrap().len(), capacity + 2); assert!(origin.received_requests().await.unwrap().is_empty()); } @@ -71,7 +71,7 @@ async fn proxy_jwks_cache_single_flights_and_isolates_each_exit() { let algorithms = vec!["ES256".to_owned()]; let access = SecretValue::new("access-token"); let verifier = verifier(&endpoint.origin, CACHE_TTL); - // 同一 kid 配不同密钥:复用别的出口缓存会直接使验签失败。 + // 同一 kid 配不同密钥:复用别的出口缓存会直接使验签失败 for (server, proxy, seed) in [ (&exit_a, Some(&proxy_a), 31), (&exit_b, Some(&proxy_b), 32), @@ -136,7 +136,7 @@ async fn proxy_jwks_rotation_and_expired_cache_remain_fail_closed() { .expect(1) .mount(&exit) .await; - // TTL 到期或未知 kid 均不能在获取失败后借用旧缓存。 + // TTL 到期或未知 kid 均不能在获取失败后借用旧缓存 let rejected_key = if ttl.is_zero() { &new } else { &old }; let token = SecretValue::new(rejected_key.sign(&valid_claims(&nonce), Algorithm::ES256)); assert!(matches!( diff --git a/backend/crates/providers/xai/tests/credential/pkce.rs b/backend/crates/providers/xai/tests/credential/pkce.rs index 6c3b3036b..225e2c9d2 100644 --- a/backend/crates/providers/xai/tests/credential/pkce.rs +++ b/backend/crates/providers/xai/tests/credential/pkce.rs @@ -1,3 +1,5 @@ +//! 验证 PKCE S256 挑战值符合 RFC 7636 样本 + use provider_xai::Pkce; #[test] diff --git a/backend/crates/providers/xai/tests/credential/refresh.rs b/backend/crates/providers/xai/tests/credential/refresh.rs index a7c2e704e..54afb2912 100644 --- a/backend/crates/providers/xai/tests/credential/refresh.rs +++ b/backend/crates/providers/xai/tests/credential/refresh.rs @@ -1,3 +1,5 @@ +//! 验证 xAI 令牌轮换、账号出口更新与刷新状态处理 + use std::collections::{BTreeMap, VecDeque}; use std::num::NonZeroU32; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -35,7 +37,7 @@ const OAUTH_BACKOFF_ATTEMPTS: u32 = 5; #[tokio::test] async fn refreshed_catalog_uses_committed_account_exit_including_proxy_changes() { - // 0: 保留 A;1: 刷新在途时 A→B;2: 刷新在途时 A→直连。 + // 0: 保留 A;1: 刷新在途时 A→B;2: 刷新在途时 A→直连 for selected_exit in 0..3 { let (proxy_a, capture_a) = rejecting_account_proxy().await; let (proxy_b, capture_b) = rejecting_account_proxy().await; @@ -122,7 +124,7 @@ impl GrokCredentialRefresher for SwitchingProxyRefresher { proxy: Option<&OutboundProxy>, ) -> Result { assert_eq!(proxy, Some(&self.initial_proxy)); - // 精确合成网络刷新已开始、credential CAS 尚未提交的出口修改。 + // 精确合成网络刷新已开始、credential CAS 尚未提交的出口修改 self.store .set_outbound_proxy(&self.account_id, self.current_proxy.clone()); assert_eq!( @@ -156,8 +158,8 @@ impl GrokEndpointPolicy for CatalogExitPolicy { timeout: Option, proxy: Option<&OutboundProxy>, ) -> Result { - // “直连”也终止于本地 sink,旧实现漏代理时不会触达真实上游。 - // 显式代理分支仍使用生产构造器及官方 HTTPS/CONNECT 目标。 + // “直连”也终止于本地 sink,旧实现漏代理时不会触达真实上游 + // 显式代理分支仍使用生产构造器及官方 HTTPS/CONNECT 目标 OfficialGrokEndpointPolicy .build_inference_client(timeout, Some(proxy.unwrap_or(&self.direct_sink))) } @@ -326,7 +328,7 @@ async fn fixture( fixture_many([input], responses, lease_available).await } -/// 真实累加连续失败计数的测试 double,用于断言退避的指数增长与成功清零。 +/// 真实累加连续失败计数的测试 double,用于断言退避的指数增长与成功清零 #[derive(Default)] struct CountingCredentialState { counts: Mutex>, @@ -487,7 +489,7 @@ async fn fixture_many_with_state_and_runtime_policy( (store, repository, refresher, service) } -/// 用 store 的 CAS 把 next_refresh_at 复位到过去,使已退避到未来的账号再次到期。 +/// 用 store 的 CAS 把 next_refresh_at 复位到过去,使已退避到未来的账号再次到期 async fn force_due(store: &MemoryProviderAccountStore, id: &ProviderAccountId) { let account = store.account(id).expect("account to reset"); let credential = store.credential(id).expect("credential to reset"); @@ -1008,7 +1010,7 @@ async fn refresh_backoff_grows_exponentially_across_attempts() { .expect("second delay is in the future"); assert_eq!(counting.count(&id), 2); - // base=5s、factor=3:第二次(attempt=2)应比第一次(attempt=1)显著更久。 + // base=5s、factor=3:第二次(attempt=2)应比第一次(attempt=1)显著更久 assert!( second_delay > first_delay * 2, "second backoff {second_delay:?} should grow well beyond first {first_delay:?}" diff --git a/backend/crates/providers/xai/tests/credential/repository.rs b/backend/crates/providers/xai/tests/credential/repository.rs index 1c2ec7297..c1119c108 100644 --- a/backend/crates/providers/xai/tests/credential/repository.rs +++ b/backend/crates/providers/xai/tests/credential/repository.rs @@ -1,3 +1,5 @@ +//! 验证 xAI 凭据仓储的请求身份约束与安全生命周期投影 + use std::sync::Arc; use gateway_core::account::ProviderAccountStore; diff --git a/backend/crates/providers/xai/tests/credential/secret.rs b/backend/crates/providers/xai/tests/credential/secret.rs index d483b6ebd..8b211dd64 100644 --- a/backend/crates/providers/xai/tests/credential/secret.rs +++ b/backend/crates/providers/xai/tests/credential/secret.rs @@ -1,3 +1,5 @@ +//! 验证敏感字符串的诊断脱敏与恒定时间比较结果 + use provider_xai::SecretValue; #[test] diff --git a/backend/crates/providers/xai/tests/credential/selector.rs b/backend/crates/providers/xai/tests/credential/selector.rs index 680be2c86..072bc6a22 100644 --- a/backend/crates/providers/xai/tests/credential/selector.rs +++ b/backend/crates/providers/xai/tests/credential/selector.rs @@ -1,3 +1,5 @@ +//! 验证 xAI 选号边界、账号容量与失败冷却反馈 + use std::collections::{BTreeMap, BTreeSet}; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime}; @@ -420,7 +422,7 @@ async fn account_scoped_cooldown_survives_credential_rotation() { CredentialCasOutcome::Updated(revision) if revision.get() == 2 )); - // 账号级限流冷却跨凭据轮换保留:轮换后仍被冷却排除。 + // 账号级限流冷却跨凭据轮换保留:轮换后仍被冷却排除 assert!(matches!( fixture .selector @@ -514,7 +516,7 @@ async fn payment_required_feedback_writes_short_account_cooldown_without_persist .await; // bare 402 无结构化 quota code:只写短期账号 runtime cooldown, - // 不持久化 QuotaExhausted(避免长期错误状态)。 + // 不持久化 QuotaExhausted(避免长期错误状态) assert_eq!( fixture .store @@ -562,7 +564,7 @@ async fn model_quota_feedback_writes_model_scoped_cooldown_without_blocking_othe assert_eq!(account.credential_state(), CredentialState::Ready); let minimum_until = SystemTime::now() + Duration::from_secs(9 * 60); let scope = ProviderCooldownScope::upstream_model(failed_model); - // model-scoped cooldown:目标模型被排除,账号级无 cooldown。 + // model-scoped cooldown:目标模型被排除,账号级无 cooldown assert!( fixture .cooldowns @@ -570,7 +572,7 @@ async fn model_quota_feedback_writes_model_scoped_cooldown_without_blocking_othe .is_some_and(|cooldown| cooldown.until() > minimum_until) ); assert!(fixture.cooldowns.cooldown(&selected).is_none()); - // 失败模型不可选(ModelCoolingDown 由 model-scoped cooldown 派生)。 + // 失败模型不可选(ModelCoolingDown 由 model-scoped cooldown 派生) assert!(matches!( fixture .selector @@ -580,7 +582,7 @@ async fn model_quota_feedback_writes_model_scoped_cooldown_without_blocking_othe retry_after: Some(_) }) )); - // 另一模型不受 model cooldown 影响:同一账号在 grok-4.6 下仍可选。 + // 另一模型不受 model cooldown 影响:同一账号在 grok-4.6 下仍可选 let other_model = fixture .selector .select(fixture.request_for_model("grok-4.6", Some(selected.clone()))) @@ -956,7 +958,7 @@ async fn smart_strategy_never_reuses_quota_projection_after_credential_rotation( .await; fixture .seed_quota( - // 剩余 75% 低于旧观测的 95%,但高于观测失效后的未知额度中性值。 + // 剩余 75% 低于旧观测的 95%,但高于观测失效后的未知额度中性值 &account_id("zzz-current-known"), 25.0, Duration::from_secs(600), @@ -1035,7 +1037,7 @@ async fn quota_reset_strategy_uses_provider_reported_earliest_reset() { #[tokio::test] async fn bare_402_cooldown_expires_and_account_recovers_without_persisted_exhaustion() { // bare 402 只写短期 account cooldown,不持久化 - // QuotaExhausted;退避到期后账号自动恢复可选。 + // QuotaExhausted;退避到期后账号自动恢复可选 let fixture = SelectorFixture::new(&["payment-recover", "available"]).await; let session = fixture .selector @@ -1053,7 +1055,7 @@ async fn bare_402_cooldown_expires_and_account_recovers_without_persisted_exhaus ) .await; - // 退避活跃期间该账号被排除,另一账号可选。 + // 退避活跃期间该账号被排除,另一账号可选 assert_eq!( fixture .store @@ -1070,7 +1072,7 @@ async fn bare_402_cooldown_expires_and_account_recovers_without_persisted_exhaus .expect("another account available during cooldown"); assert_ne!(next.account_id(), &selected); - // 等短 cooldown 到期。 + // 等短 cooldown 到期 tokio::time::sleep(Duration::from_millis(1_100)).await; let recovered = fixture .selector @@ -1160,7 +1162,7 @@ async fn smart_queue_weight_changes_the_selected_wait_queue_and_respects_require other => panic!("unexpected queue probe: {other:?}"), } drop((next, probe)); - // 其他账号先空闲不能解除 required account 限制。 + // 其他账号先空闲不能解除 required account 限制 fixture .coordinator .signals diff --git a/backend/crates/providers/xai/tests/credential/token.rs b/backend/crates/providers/xai/tests/credential/token.rs index 02b07966c..871ce6223 100644 --- a/backend/crates/providers/xai/tests/credential/token.rs +++ b/backend/crates/providers/xai/tests/credential/token.rs @@ -1,3 +1,5 @@ +//! 验证 xAI 令牌错误分类与令牌、主体身份脱敏 + use provider_xai::{ FailureClass, OAuthHttpResponse, OAuthOperation, OAuthPrincipal, parse_oauth_error, parse_refresh_success, diff --git a/backend/crates/providers/xai/tests/credential/verification.rs b/backend/crates/providers/xai/tests/credential/verification.rs index 0cac43313..81e2a983c 100644 --- a/backend/crates/providers/xai/tests/credential/verification.rs +++ b/backend/crates/providers/xai/tests/credential/verification.rs @@ -1,3 +1,5 @@ +//! 验证令牌验证证据的诊断输出隐藏已验证主体 + use provider_xai::{VerificationEvidence, VerificationMethod}; #[test] diff --git a/backend/crates/providers/xai/tests/main.rs b/backend/crates/providers/xai/tests/main.rs index 6bdd89bc9..9853bbe81 100644 --- a/backend/crates/providers/xai/tests/main.rs +++ b/backend/crates/providers/xai/tests/main.rs @@ -1,3 +1,5 @@ +//! xAI Provider、凭据与上游传输的测试入口 + mod admin; mod credential; mod provider; diff --git a/backend/crates/providers/xai/tests/provider/contract.rs b/backend/crates/providers/xai/tests/provider/contract.rs index 05e1b6147..aaf300d0a 100644 --- a/backend/crates/providers/xai/tests/provider/contract.rs +++ b/backend/crates/providers/xai/tests/provider/contract.rs @@ -1,3 +1,5 @@ +//! 验证 xAI 原生执行的协议转换、账号选择与交付状态隔离 + use std::collections::{BTreeMap, BTreeSet, VecDeque}; use std::num::NonZeroU32; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -1691,7 +1693,7 @@ async fn execute_successfully(provider: &Arc, operation: Oper assert!(events.iter().all(Result::is_ok)); } -/// 与 Postgres 调度列表一致:常规选择不返回停用账号,只有诊断能取回。 +/// 与 Postgres 调度列表一致:常规选择不返回停用账号,只有诊断能取回 struct DiagnosticLeasePort; impl ProviderLeasePort for DiagnosticLeasePort { diff --git a/backend/crates/providers/xai/tests/provider/mod.rs b/backend/crates/providers/xai/tests/provider/mod.rs index 6ccaa47df..306b2db71 100644 --- a/backend/crates/providers/xai/tests/provider/mod.rs +++ b/backend/crates/providers/xai/tests/provider/mod.rs @@ -1 +1,3 @@ +//! xAI Provider 执行合同的测试入口 + mod contract; diff --git a/backend/crates/providers/xai/tests/support.rs b/backend/crates/providers/xai/tests/support.rs index 417ca6743..92740f311 100644 --- a/backend/crates/providers/xai/tests/support.rs +++ b/backend/crates/providers/xai/tests/support.rs @@ -1,3 +1,5 @@ +//! xAI 测试共用的客户端画像、目录服务与本地端点策略 + use std::collections::{BTreeMap, BTreeSet}; use std::net::IpAddr; use std::num::NonZeroU32; @@ -343,7 +345,7 @@ impl ProviderAccountStore for MemoryProviderAccountStore { if self.fail_provider_listing.load(Ordering::SeqCst) { return Err(invalid()); } - // 与 Postgres 实现的调度列表语义一致:停用账号不进入常规候选。 + // 与 Postgres 实现的调度列表语义一致:停用账号不进入常规候选 Ok(lock(&self.accounts) .values() .filter(|stored| stored.account.provider() == provider && stored.account.enabled()) diff --git a/backend/crates/providers/xai/tests/transport/canonical.rs b/backend/crates/providers/xai/tests/transport/canonical.rs index f851ec0bf..c8d93b0a6 100644 --- a/backend/crates/providers/xai/tests/transport/canonical.rs +++ b/backend/crates/providers/xai/tests/transport/canonical.rs @@ -1,3 +1,5 @@ +//! 验证 Grok 事件转换的模型、用量、首个输出与完成语义 + use gateway_core::error::ProviderError; use gateway_core::event::{ContentKind, FinishReason, GatewayEvent, ProviderEvent}; use gateway_core::operation::{GenerateRequest, ProtocolPayload}; @@ -304,7 +306,7 @@ fn zero_provider_cost_should_remain_an_authoritative_zero() { #[test] fn billing_should_price_grok_46_cache_and_priority_at_the_context_boundary() { - // 短、长上下文分别检查普通输入、缓存、输出的单价,Priority 只采用已确认档位。 + // 短、长上下文分别检查普通输入、缓存、输出的单价,Priority 只采用已确认档位 for (input, tier, expected) in [ (199_999, None, ["2", "0.5", "6"]), (200_000, None, ["4", "1", "12"]), @@ -460,7 +462,7 @@ fn billing_should_require_provider_totals_for_server_tools() { } let request = tool_request(serde_json::json!({"input": "查资料", "tools": [{"type": "web_search"}]})); - // 工具结果可能被上游省略,允许调用收费工具时不能仅靠空 output 认定没有附加费。 + // 工具结果可能被上游省略,允许调用收费工具时不能仅靠空 output 认定没有附加费 let mut response = serde_json::json!({"output": [], "usage": {"input_tokens": 100, "output_tokens": 1}}); assert_eq!( @@ -564,8 +566,8 @@ fn decoder_should_fail_closed_for_non_integer_provider_cost() { #[test] fn decoder_should_tolerate_content_field_error_without_breaking_stream() { - // 内容事件的字段校验失败(此处空 delta)不应打断已开始的客户端流。 - // 旧行为在此断整条流;修复后跳过该事件的 canonical 提取、wire 原样转发,并继续解出终态。 + // 内容事件的字段校验失败(此处空 delta)不应打断已开始的客户端流 + // 旧行为在此断整条流;修复后跳过该事件的 canonical 提取、wire 原样转发,并继续解出终态 let body = concat!( "event: response.created\n", "data: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_a3\",\"model\":\"grok-code-test\"}}\n\n", @@ -577,14 +579,14 @@ fn decoder_should_tolerate_content_field_error_without_breaking_stream() { let events = GrokCanonicalDecoder::new("fallback") .push(body.as_bytes()) .expect("content field error must not break the stream"); - // 空 delta 的 wire 帧仍原样转发给客户端(透明)。 + // 空 delta 的 wire 帧仍原样转发给客户端(透明) assert!( wire_events(&events) .iter() .any(|wire| wire.data().get("delta") == Some(&serde_json::json!(""))), "malformed delta wire frame should still be forwarded to the client" ); - // 终态仍被解出。 + // 终态仍被解出 let canonical: Vec = events .into_iter() .flat_map(|event| event.into_parts().0) @@ -880,7 +882,7 @@ fn decoder_should_reject_custom_arguments_beyond_the_buffer_limit() { }).to_string(), ); decoder.push(added.as_bytes()).expect("custom item added"); - // 分帧累计也必须受限,不能只限制单个 SSE frame。 + // 分帧累计也必须受限,不能只限制单个 SSE frame for size in [1 << 20, 1] { let delta = encode_sse_event( "response.function_call_arguments.delta", diff --git a/backend/crates/providers/xai/tests/transport/catalog.rs b/backend/crates/providers/xai/tests/transport/catalog.rs index fb629e9d6..4940717c8 100644 --- a/backend/crates/providers/xai/tests/transport/catalog.rs +++ b/backend/crates/providers/xai/tests/transport/catalog.rs @@ -1,3 +1,5 @@ +//! 验证 Grok 目录与额度请求的 OAuth 头部及订阅事实解析 + use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; diff --git a/backend/crates/providers/xai/tests/transport/client_profile.rs b/backend/crates/providers/xai/tests/transport/client_profile.rs index 029e15102..508c65bac 100644 --- a/backend/crates/providers/xai/tests/transport/client_profile.rs +++ b/backend/crates/providers/xai/tests/transport/client_profile.rs @@ -1,3 +1,5 @@ +//! 验证 Grok 客户端画像的环境、版本模式与头部注入防护 + use gateway_core::account::OpaqueProviderData; use provider_xai::transport::client_profile::{GrokClientProfileSelection, VersionMode}; use provider_xai::{XaiWireProfile, XaiWireProfileState}; diff --git a/backend/crates/providers/xai/tests/transport/compaction.rs b/backend/crates/providers/xai/tests/transport/compaction.rs index 7dea55242..1c36ef904 100644 --- a/backend/crates/providers/xai/tests/transport/compaction.rs +++ b/backend/crates/providers/xai/tests/transport/compaction.rs @@ -1,3 +1,5 @@ +//! 验证 Grok 压缩请求的历史顺序、约束与终态触发处理 + use gateway_core::event::{ GatewayEvent, ProtocolWireEvent, ProviderEvent, ReasoningDelta, TextDelta, ToolCallDelta, }; diff --git a/backend/crates/providers/xai/tests/transport/config.rs b/backend/crates/providers/xai/tests/transport/config.rs index b03f6b58a..2d85c02ed 100644 --- a/backend/crates/providers/xai/tests/transport/config.rs +++ b/backend/crates/providers/xai/tests/transport/config.rs @@ -1,3 +1,5 @@ +//! 验证 Grok 推理端点限定为配置的官方 CLI 来源 + use provider_xai::GROK_CLI_BASE_URL; use provider_xai::transport::GROK_RESPONSES_URL; diff --git a/backend/crates/providers/xai/tests/transport/egress.rs b/backend/crates/providers/xai/tests/transport/egress.rs index 5777fed58..eec046c6a 100644 --- a/backend/crates/providers/xai/tests/transport/egress.rs +++ b/backend/crates/providers/xai/tests/transport/egress.rs @@ -1,4 +1,4 @@ -//! 通过生产辅助请求 transport 观察出口 cache 的真实连接生命周期。 +//! 通过生产辅助请求 transport 观察出口 cache 的真实连接生命周期 use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; diff --git a/backend/crates/providers/xai/tests/transport/headers.rs b/backend/crates/providers/xai/tests/transport/headers.rs index 50bf81c9d..8a81b6e71 100644 --- a/backend/crates/providers/xai/tests/transport/headers.rs +++ b/backend/crates/providers/xai/tests/transport/headers.rs @@ -1,3 +1,5 @@ +//! 验证 Grok 请求头绑定所选账号、会话与客户端身份并保持脱敏 + use gateway_core::account::{CredentialRevision, ProviderAccountId}; use gateway_core::engine::ModelRequestId; use gateway_core::routing::UpstreamModelId; diff --git a/backend/crates/providers/xai/tests/transport/mod.rs b/backend/crates/providers/xai/tests/transport/mod.rs index 317206b03..9ed9c4516 100644 --- a/backend/crates/providers/xai/tests/transport/mod.rs +++ b/backend/crates/providers/xai/tests/transport/mod.rs @@ -1,3 +1,5 @@ +//! Grok 请求、事件、客户端画像与会话传输的测试入口 + mod canonical; mod catalog; mod client_profile; diff --git a/backend/crates/providers/xai/tests/transport/network.rs b/backend/crates/providers/xai/tests/transport/network.rs index 24ce236a3..0284f1792 100644 --- a/backend/crates/providers/xai/tests/transport/network.rs +++ b/backend/crates/providers/xai/tests/transport/network.rs @@ -1,4 +1,4 @@ -//! 外置的 reqwest 网络策略契约。 +//! 外置的 reqwest 网络策略契约 use std::cell::Cell; use std::net::IpAddr; @@ -47,7 +47,7 @@ async fn official_inference_tls_should_advertise_grok_cli_alpn() { .write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n") .await .unwrap(); - // 只读取公开的 ClientHello 后关闭连接,不发送证书,也不接收认证和模型请求。 + // 只读取公开的 ClientHello 后关闭连接,不发送证书,也不接收认证和模型请求 let mut handshake = Vec::new(); loop { assert_eq!(socket.read_u8().await.unwrap(), 22); @@ -98,10 +98,10 @@ fn client_hello_alpn(mut hello: &[u8]) -> Vec> { .fold(0_usize, |len, byte| len * 256 + usize::from(*byte)); take(input, len) } - take(&mut hello, 34); // 跳过版本号与随机值。 - vector(&mut hello, 1); // 跳过会话 ID。 - vector(&mut hello, 2); // 跳过密码套件列表。 - vector(&mut hello, 1); // 跳过压缩方法列表。 + take(&mut hello, 34); // 跳过版本号与随机值 + vector(&mut hello, 1); // 跳过会话 ID + vector(&mut hello, 2); // 跳过密码套件列表 + vector(&mut hello, 1); // 跳过压缩方法列表 let mut extensions = vector(&mut hello, 2); while !extensions.is_empty() { let kind = u16::from_be_bytes(take(&mut extensions, 2).try_into().unwrap()); @@ -349,7 +349,7 @@ async fn inference_proxy_client_is_built_once_for_concurrent_same_binding() { for task in tasks { assert!(task.await.unwrap().iter().all(Result::is_ok)); } - // 启动时的直连 client + 一个代理 client;不再先构建并丢弃另一个直连 client。 + // 启动时的直连 client + 一个代理 client;不再先构建并丢弃另一个直连 client assert_eq!(builds.load(Ordering::SeqCst), 2); assert!(origin_server.received_requests().await.unwrap().is_empty()); } diff --git a/backend/crates/providers/xai/tests/transport/profile.rs b/backend/crates/providers/xai/tests/transport/profile.rs index 0a22a77c7..d14010762 100644 --- a/backend/crates/providers/xai/tests/transport/profile.rs +++ b/backend/crates/providers/xai/tests/transport/profile.rs @@ -1,3 +1,5 @@ +//! 验证 Grok 客户端默认身份、架构规范化与发行版本刷新 + use std::collections::VecDeque; use std::sync::{Arc, Mutex}; diff --git a/backend/crates/providers/xai/tests/transport/request/mod.rs b/backend/crates/providers/xai/tests/transport/request/mod.rs index f0e4d32bf..2860962fa 100644 --- a/backend/crates/providers/xai/tests/transport/request/mod.rs +++ b/backend/crates/providers/xai/tests/transport/request/mod.rs @@ -1,3 +1,5 @@ +//! Grok 请求转换测试入口,以及工具、历史与未知字段处理测试 + use gateway_core::operation::{GenerateRequest, ProtocolPayload}; use gateway_core::policy::ClientApiKeyId; use serde_json::{Map, Value, json}; @@ -1130,14 +1132,14 @@ fn history_sanitizer_should_only_strip_known_grok_injection_sites() { .expect("history normalization"); let body = Value::Object(encoded.body().clone()); - // 工具 schema 里恰好叫 phase 的属性与语义 null 不再被误删。 + // 工具 schema 里恰好叫 phase 的属性与语义 null 不再被误删 assert_eq!( body.pointer("/input/0/tools/0/input_schema/properties/phase"), Some(&json!({"type": "string"})) ); assert_eq!(body.pointer("/input/1/output/result"), Some(&Value::Null)); assert_eq!(body.pointer("/input/1/output/phase"), Some(&json!("keep"))); - // shell_call action 是已知注入点:内部键与 null 占位字段仍被剥离。 + // shell_call action 是已知注入点:内部键与 null 占位字段仍被剥离 assert_eq!( body.pointer("/input/2/action/commands/0"), Some(&json!("pwd")) diff --git a/backend/crates/providers/xai/tests/transport/session.rs b/backend/crates/providers/xai/tests/transport/session.rs index 5c6be3897..ff26cf8e0 100644 --- a/backend/crates/providers/xai/tests/transport/session.rs +++ b/backend/crates/providers/xai/tests/transport/session.rs @@ -1,3 +1,5 @@ +//! 验证 Grok 会话绑定拒绝保留值并隐藏诊断中的会话标识 + use provider_xai::{GrokSessionBinding, GrokSessionDataError}; #[test]