后端客户端、远程目录和启动刷新
这一段发生在配置和登录都准备好之后,像开店前补货。它先拉云端配置,决定用缓存还是联网,并在后台刷新;再整理可选模型,既问 Codex 或模型服务商,也检查本机 Ollama、LM Studio。接着同步连接器、插件目录、MCP 工具和工作区权限,让界面能显示真实可用的东西。还会顺手查任务、额度、记忆写入安全和版本更新,尽量用缓存和后台任务,别拖慢启动。
本阶段涉及的状态22
reg-effective-config系统最终采用的那份配置,把文件、云端规则、项目设置、命令行参数和临时覆盖合在一起,后面各模块都按它办事。reg-feature-flags当前打开或关闭的实验功能和能力开关,决定界面、模型、工具、插件等功能能不能用。reg-auth-account用户当前是谁、有没有登录、用的是哪种账号或套餐、令牌是否还能用的身份状态。reg-secret-store本机保存的 API Key、ChatGPT 令牌、个人访问令牌、Bedrock Key 等秘密凭据。reg-account-quota-limits账号的额度、限流、消费控制和剩余可用量信息,用来决定请求能不能继续发。reg-model-catalog系统当前知道有哪些模型、各模型有什么能力、来自哪个提供方以及能不能选用的模型清单。reg-backend-remote-config-cache从云端拉下来的配置、目录和规则的本地缓存,启动和后台刷新都会读写它。reg-plugin-registry已安装、可用、来自市场或本地的插件清单,以及每个插件的能力和加载结果。reg-mcp-registryMCP 服务器、连接、工具和资源的登记表,模型和前端都靠它知道能调用哪些外部能力。reg-skills-catalog当前能用的技能目录,包括技能来自哪里、说明文件在哪里、是否允许自动调用。reg-memory-store系统保存和检索长期记忆的地方,包含记忆文件、目录、搜索结果和后台整理状态。reg-background-job-queue后台刷新云配置、同步目录、整理记忆、清理连接、检查更新等异步任务的队列和运行状态。reg-observability-telemetry日志、指标、分析事件、请求耗时、工具结果和错误报告等观测数据的收集与发送状态。reg-network-client-proxyHTTP 客户端、证书、Cookie、重试、流式连接和网络代理放行/拒绝规则的共享联网状态。reg-cloud-task-state云端任务、任务列表、默认环境、尝试次数、状态和结果在本地界面与后端之间同步的那份状态。reg-auto-update-state自动更新检查、可用版本、下载进度、安装结果和待重启标记等更新生命周期状态。reg-workspace-trust-state当前工作区和目录的信任状态、授权根目录及同步到前端和沙箱的工作区权限信息。reg-runtime-environment-registry本地、云端或远程执行环境的可用清单、默认选择和当前线程选中的运行目标。reg-connector-registry从后端同步的外部连接器及其可用性、授权和目录刷新状态。reg-attestation-state宿主或客户端提供的证明材料、证明请求结果以及附加到上游请求的 attestation 头状态。reg-installation-identity本机安装实例的持久编号和识别信息,用于账号、后端请求、遥测和安全身份关联。reg-active-model-selection当前线程或会话实际选中的模型、提供方和推理参数状态,区别于仅列出可用模型的模型目录。
云配置加载
这些文件定义公共云配置接口、后端获取适配器、bundle 生命周期服务,以及启动时使用的集成加载器。
cloud-config/src/lib.rs源码 ↗
这个库解决的是“配置从云上来以后,怎么取、怎么缓存、什么时候刷新、怎么检查是否可用”的问题。可以把它想成一个专门接收云端配置包的前台:后台有运输、缓存、指标记录、服务逻辑、校验等不同岗位,但外面的人不需要直接找每个岗位。这个 lib.rs 就是前台目录。它声明了几个内部模块,比如 backend、cache、service、validation,让编译器把这些文件纳入这个库;同时它只公开 cloud_config_bundle_loader 和 cloud_config_bundle_loader_for_storage 这两个装载配置包的入口。注释里也特别说明:这个库管“从云端拿到配置并维护它”,但具体怎么解析、怎么和本地配置合并,是另一个 codex-config 库的事。这样分工能避免配置系统混成一团。
cloud-config/src/backend.rs源码 ↗
这份文件像一个“取件员”:上层只说“用这个登录身份去拿云端配置”,它就负责创建后端客户端、发请求、判断失败原因,然后把后端给的原始包裹拆成项目内部能看懂的配置包。这里特别重要的是错误分类:有些错误只是暂时失败,比如网络问题或服务器暂时不可用,可以稍后重试;但如果是未授权,也就是登录身份不被接受,就不能假装只是网络抖动。文件里还定义了一个抽象接口 BundleClient,意思是“任何能拿配置包的东西都可以长这样”,这样以后测试或替换后端实现会更容易。拿到后端返回后,bundle_from_response 会把里面可能为空、可能嵌套的配置列表摊平,并转换成 CloudConfigBundle,交给后面的服务层继续校验、缓存和解析。
RetryableFailureKind::status_code19–24 ↗
fn status_code(self) -> Option<u16>
作用:这个函数从“可重试失败”里取出 HTTP 状态码。HTTP 状态码就是服务器用数字说明请求结果的方式,比如 401 表示未授权,500 表示服务器出错;如果失败发生在还没发出请求之前,就没有状态码。
数据流:进去的是一个 RetryableFailureKind,代表一种可以稍后再试的失败 → 它检查失败类型:如果是后端客户端创建失败,就返回空;如果是请求失败,就拿出里面保存的状态码 → 出来的是一个 Option<u16>,有状态码就给数字,没有就给 None。
调用关系:它本身不发请求,只是帮后续重试逻辑读信息。调用图里显示 retry_after_request_failure 会用它来判断一次失败有没有服务器状态码,从而决定后面怎么安排重试。
调用图:被 1 处调用(retry_after_request_failure)。
BackendBundleClient::new52–54 ↗
fn new(base_url: String) -> Self
作用:这个函数创建一个真正会访问后端的配置包客户端。调用者只需要给它后端服务器地址,它就把地址保存起来,等之后请求配置包时使用。
数据流:进去的是 base_url,也就是后端服务的基础网址 → 它把这个网址放进 BackendBundleClient 结构里 → 出来的是一个新的 BackendBundleClient 实例,后面可以拿它去请求云端配置。
调用关系:它是这个后端客户端的入口小工厂。调用图里显示 cloud_config_bundle_loader 会调用它,说明加载云配置的流程启动时,会先用它准备好一个能联系后端的对象。
调用图:被 1 处调用(cloud_config_bundle_loader)。
BackendBundleClient::get_bundle58–87 ↗
async fn get_bundle(&self, auth: &CodexAuth) -> Result<CloudConfigBundle, BundleRequestError>
作用:这个函数用当前用户的登录身份去后端服务器领取云端配置包。它还会把失败分清楚:哪些值得重试,哪些是未授权,需要让上层另行处理。
数据流:进去的是 CodexAuth,也就是当前登录认证信息,以及这个客户端里保存的 base_url → 它先调用外部的 from_auth 创建后端客户端;如果创建失败,就记录警告并返回“可重试的客户端初始化失败” → 如果客户端创建成功,它发起 get_config_bundle 请求 → 请求失败时,它读取服务器状态码,并判断是不是未授权;未授权就返回 Unauthorized,其他请求错误就返回可重试错误 → 请求成功时,它把后端原始响应交给 bundle_from_response 转成内部的 CloudConfigBundle → 出来的是配置包,或者一个清楚分类过的错误。
调用关系:这是 BundleClient 接口在真实后端上的实现,也是本文件最核心的动作。它把活儿分成三段:先靠外部 BackendClient::from_auth 准备网络客户端,再向后端要配置,最后把响应交给 bundle_from_response 整理成内部格式。
调用图:调用 1 个内部函数(bundle_from_response);外部调用 1 个(from_auth)。
bundle_from_response90–118 ↗
fn bundle_from_response(response: ConfigBundleResponse) -> CloudConfigBundle
作用:这个函数把后端返回的原始配置包,整理成项目内部统一使用的 CloudConfigBundle。它主要处理“可能没有数据”和“数据套了好几层”的情况,让后面的代码拿到的是干净稳定的结构。
数据流:进去的是 ConfigBundleResponse,也就是后端接口直接返回的结果 → 它分别查看 config_toml 和 requirements_toml 两类内容;每一类都可能为空,所以它会逐层打开,缺失时就当作空列表 → 对列表里的每个 DeliveredTomlFragment,它分别转换成内部的配置片段或要求片段 → 出来的是 CloudConfigBundle,里面包含 enterprise_managed 下发配置和 enterprise_managed 下发要求。
调用关系:它被 BackendBundleClient::get_bundle 在请求成功后调用。get_bundle 负责把东西从服务器拿回来,而这个函数负责把“快递箱里的原包装”拆成项目内部能直接使用的格式。
调用图:被 1 处调用(get_bundle)。
config_fragment_from_delivered120–126 ↗
fn config_fragment_from_delivered(fragment: DeliveredTomlFragment) -> CloudConfigFragment
作用:这个小函数把后端下发的一个 TOML 配置片段,改装成内部使用的 CloudConfigFragment。TOML 是一种常见配置文件格式,长得像键值对文本。
数据流:进去的是 DeliveredTomlFragment,里面有 id、name、contents 三样信息 → 它不改内容,只是把这三样字段搬到 CloudConfigFragment 里 → 出来的是一个内部配置片段,后续可以按云端配置来处理。
调用关系:它由 bundle_from_response 在整理 config_toml 列表时使用。它的角色很单纯,就是把后端数据模型翻译成配置系统自己的数据模型。
requirements_fragment_from_delivered128–136 ↗
fn requirements_fragment_from_delivered(
fragment: DeliveredTomlFragment,
) -> CloudRequirementsFragment
作用:这个小函数把后端下发的一个 TOML 要求片段,改装成内部使用的 CloudRequirementsFragment。这里的“要求”可以理解成云端告诉本地必须满足或遵守的配置约束。
数据流:进去的是 DeliveredTomlFragment,包含 id、name、contents → 它把这些字段原样放进 CloudRequirementsFragment → 出来的是一个内部要求片段,供后续配置校验或约束处理使用。
调用关系:它由 bundle_from_response 在整理 requirements_toml 列表时使用。它和 config_fragment_from_delivered 很像,只是目标类型不同:一个用于普通配置,一个用于配置要求。
cloud-config/src/service.rs源码 ↗
云端配置包可以理解成公司或工作区下发的一包规则,比如哪些配置由企业统一管。这个文件做的事很像进门前查钥匙:先看用户有没有登录、账号类型是否有资格用这些规则;再优先读本地缓存,缓存不对、不是这个用户的、或内容校验不过,就去远端服务器重新拿。拿远端数据时,它会设置超时,失败会按退避重试(越失败等得越久再试),遇到登录失效还会尝试自动刷新登录。拿到后会先校验,再写入缓存。一个重要点是:启动时加载出来的配置是当次运行的“快照”,后台刷新只暖缓存,不会偷偷改掉已经运行中的配置。
auth_identity43–45 ↗
fn auth_identity(auth: &CodexAuth) -> (Option<String>, Option<String>)
作用:从登录信息里取出两个身份标记:ChatGPT 用户 ID 和账号 ID。这样缓存就能确认“这包配置是不是属于当前这个人”。
数据流:输入是一份登录信息 → 它读取用户 ID 和账号 ID → 输出两个可有可无的字符串;它不改动任何东西。
调用关系:启动加载会先用它拿身份去匹配缓存;远端拉到新配置后,也会用它把缓存保存到正确的用户身份名下。
调用图:调用 2 个内部函数(get_account_id, get_chatgpt_user_id);被 2 处调用(load_startup_bundle, validate_and_cache_remote_bundle)。
cloud_config_eligible_auth47–54 ↗
fn cloud_config_eligible_auth(auth: &CodexAuth) -> bool
作用:判断当前登录账号有没有资格使用云端配置包。不是所有账号都需要或允许拿企业管理配置。
数据流:输入是一份登录信息 → 它检查账号计划类型、是否使用 Codex 后端,以及是否像企业/商业/教育账号 → 输出 true 或 false。
调用关系:启动加载和后台刷新都会先问它一句:这个账号值不值得继续拉配置?如果答案是否,就直接跳过后面的缓存和网络请求。
调用图:调用 2 个内部函数(account_plan_type, uses_codex_backend);被 2 处调用(load_startup_bundle, refresh_cache_once);外部调用 1 个(matches!)。
optional_bundle56–62 ↗
fn optional_bundle(bundle: CloudConfigBundle) -> Option<CloudConfigBundle>
作用:把“空配置包”转换成“没有配置”。这能让调用方不用把空包当成真正有规则的包来处理。
数据流:输入一个配置包 → 它检查里面是不是空的 → 如果空就输出 None,否则输出这个配置包;不写缓存也不访问网络。
调用关系:读缓存成功或远端拉取成功后都会经过它,让上层统一看到“有配置”或“没有配置”这两种清楚结果。
调用图:调用 1 个内部函数(is_empty);被 2 处调用(load_valid_cached_bundle, validate_and_cache_remote_bundle)。
CloudConfigBundleService::clone83–91 ↗
CloudConfigBundleService::new98–112 ↗
fn new(
auth_manager: Arc<AuthManager>,
client: Arc<C>,
codex_home: PathBuf,
timeout: Duration,
) -> Self
作用:创建云端配置包服务。调用者把登录管理器、服务器客户端、Codex 主目录和超时时间交进来,它组装成一个能加载和刷新配置的对象。
数据流:输入认证管理器、远端客户端、目录路径和超时时间 → 它把目录解析成绝对路径,并用这个目录建立缓存 → 输出一个 CloudConfigBundleService。
调用关系:系统的云配置加载器会在准备使用云端配置前调用它;测试也大量用它搭出服务实例,检查缓存、鉴权和重试行为。
调用图:调用 2 个内部函数(new, resolve_path_against_base);被 19 处调用(cloud_config_bundle_loader, get_bundle_allows_eligible_workspace_plans_and_writes_cache, get_bundle_does_not_use_cache_when_auth_identity_is_incomplete, get_bundle_empty_response_is_success_and_cached, get_bundle_ignores_cache_for_different_auth_identity, get_bundle_ignores_invalid_cache_and_refetches, get_bundle_recovers_after_unauthorized_reload, get_bundle_recovers_after_unauthorized_reload_updates_cache_identity, get_bundle_rejects_invalid_remote_bundle_before_cache_write, get_bundle_retries_until_success (+9 more));外部调用 1 个(clone)。
CloudConfigBundleService::load_startup_bundle_with_timeout114–169 ↗
async fn load_startup_bundle_with_timeout(
&self,
) -> Result<Option<CloudConfigBundle>, CloudConfigBundleLoadError>
作用:启动时加载云端配置包,并给整个过程加一个时间上限。这样程序不会因为等云端配置而一直卡住。
数据流:输入是服务自身的认证、缓存、客户端和超时设置 → 它启动计时,调用真正的启动加载流程,并用超时包住它 → 输出配置包或加载错误,同时记录日志和指标。
调用关系:这是启动阶段面向外部的主要入口;它把实际取缓存/拉远端的工作交给 load_startup_bundle,并负责把成功、失败、超时这些结果变成可观测的日志和统计。
调用图:调用 2 个内部函数(emit_load_metric, load_startup_bundle);外部调用 4 个(now, start_global_timer, timeout, info!)。
CloudConfigBundleService::load_startup_bundle171–194 ↗
async fn load_startup_bundle(
&self,
) -> Result<Option<CloudConfigBundle>, CloudConfigBundleLoadError>
作用:执行启动加载的核心决策:没登录或账号不合适就不加载;能用缓存就用缓存;缓存不行才去服务器拿。
数据流:它读取当前登录信息 → 检查资格,取身份,尝试读取并校验本地缓存 → 如果命中就输出缓存结果;如果未命中就发起远端获取并更新缓存。
调用关系:它由 load_startup_bundle_with_timeout 调用;内部把身份提取交给 auth_identity,把资格判断交给 cloud_config_eligible_auth,把缓存读取交给 load_valid_cached_bundle,把远端获取交给 fetch_remote_bundle_and_update_cache_with_retries。
调用图:调用 4 个内部函数(fetch_remote_bundle_and_update_cache_with_retries, load_valid_cached_bundle, auth_identity, cloud_config_eligible_auth);被 1 处调用(load_startup_bundle_with_timeout)。
CloudConfigBundleService::load_valid_cached_bundle196–225 ↗
async fn load_valid_cached_bundle(
&self,
chatgpt_user_id: Option<&str>,
account_id: Option<&str>,
) -> CachedBundleLookup
作用:尝试读取一份“属于当前用户且内容合法”的本地缓存。这样启动时通常不用等网络。
数据流:输入当前用户的两个身份标记 → 它让缓存按身份读取文件;读到后再校验配置包内容 → 如果合法就输出命中结果,否则记录原因并输出未命中。
调用关系:启动加载会先找它要缓存;它发现缓存坏了、身份不匹配或读不到时,不自己联网,而是告诉 load_startup_bundle 继续走远端拉取。
调用图:调用 4 个内部函数(load, log_load_status, optional_bundle, validate_bundle);被 1 处调用(load_startup_bundle);外部调用 3 个(Hit, info!, warn!)。
CloudConfigBundleService::fetch_remote_bundle_and_update_cache_with_retries227–298 ↗
async fn fetch_remote_bundle_and_update_cache_with_retries(
&self,
mut auth: CodexAuth,
trigger: &'static str,
) -> Result<Option<CloudConfigBundle>, CloudConfigBundleLoadE
作用:从服务器拉云端配置包,并处理失败、重试、登录失效和最终报错。它是远端获取这条路的主控循环。
数据流:输入一份登录信息和触发来源,比如 startup 或 refresh → 它最多尝试固定次数:成功就校验并写缓存;可重试错误就等待后再试;未授权就尝试恢复登录 → 输出配置包或加载错误。
调用关系:启动缓存未命中和后台刷新都会调用它;它把成功后的处理交给 validate_and_cache_remote_bundle,把普通失败交给 retry_after_request_failure,把未授权问题交给 handle_unauthorized。
调用图:调用 5 个内部函数(emit_fetch_final_metric, handle_unauthorized, retry_after_request_failure, validate_and_cache_remote_bundle, new);被 2 处调用(load_startup_bundle, refresh_cache_once);外部调用 1 个(error!)。
CloudConfigBundleService::validate_and_cache_remote_bundle300–341 ↗
async fn validate_and_cache_remote_bundle(
&self,
auth: &CodexAuth,
trigger: &'static str,
attempt: usize,
bundle: CloudConfigBundle,
) -> Result<Option<Clo
作用:处理刚从服务器拿到的配置包:先确认它安全有效,再保存到本地缓存。这样坏配置不会进入运行流程,也不会污染缓存。
数据流:输入登录信息、触发来源、尝试次数和远端配置包 → 它记录一次成功请求,校验配置内容,提取身份并尝试写缓存 → 输出空/非空配置包,或在校验失败时输出错误。
调用关系:它只在远端请求成功后被 fetch_remote_bundle_and_update_cache_with_retries 调用;如果写缓存失败,它只报警告,不让一次可用的远端配置因为缓存写入问题而失败。
调用图:调用 6 个内部函数(save, emit_fetch_attempt_metric, emit_fetch_final_metric, auth_identity, optional_bundle, validate_bundle);被 1 处调用(fetch_remote_bundle_and_update_cache_with_retries);外部调用 2 个(clone, warn!)。
CloudConfigBundleService::retry_after_request_failure343–363 ↗
async fn retry_after_request_failure(
&self,
trigger: &'static str,
attempt: usize,
status: RetryableFailureKind,
) -> bool
作用:处理可以重试的网络或服务器失败。它决定这次失败后还要不要等一会儿再试。
数据流:输入触发来源、当前第几次尝试和失败类型 → 它记录失败指标,若还没到最大次数就按退避时间睡眠 → 输出 true 表示继续重试,false 表示别再试了。
调用关系:远端获取循环遇到临时失败时会叫它;它不直接再发请求,只告诉 fetch_remote_bundle_and_update_cache_with_retries 是否该进入下一轮。
调用图:调用 3 个内部函数(status_code, emit_fetch_attempt_metric, backoff);被 1 处调用(fetch_remote_bundle_and_update_cache_with_retries);外部调用 2 个(sleep, warn!)。
CloudConfigBundleService::handle_unauthorized365–455 ↗
async fn handle_unauthorized(
&self,
auth: &mut CodexAuth,
auth_recovery: &mut UnauthorizedRecovery,
trigger: &'static str,
attempt: usize,
status_code:
作用:处理服务器说“你没权限/登录过期”的情况。它会尽量自动刷新登录,让用户不用立刻手动退出再登录。
数据流:输入当前登录、登录恢复器、触发来源、尝试次数、状态码和错误消息 → 它记录未授权指标;如果还能恢复登录,就尝试刷新并更新传入的登录信息;恢复失败则返回认证错误 → 输出下一步动作:同一次重试、下一次重试,或直接失败。
调用关系:远端获取循环遇到未授权错误时会调用它;它会和 AuthManager、UnauthorizedRecovery 配合,成功时把新登录交回 fetch_remote_bundle_and_update_cache_with_retries 继续请求。
调用图:调用 6 个内部函数(emit_fetch_attempt_metric, emit_fetch_final_metric, new, backoff, has_next, next);被 1 处调用(fetch_remote_bundle_and_update_cache_with_retries);外部调用 3 个(sleep, error!, warn!)。
CloudConfigBundleService::refresh_cache_in_background457–471 ↗
async fn refresh_cache_in_background(&self)
作用:在后台定时刷新云端配置包缓存。它不会改变当前已经加载进运行中的配置,只是让下次启动更可能直接用新缓存。
数据流:它没有额外输入,只使用服务里的超时时间、认证、客户端和缓存 → 每隔一段时间调用一次刷新,并给刷新加超时 → 如果没有登录或账号不适合就停止循环;超时则记录错误后下次再试。
调用关系:这是后台刷新任务的主循环;它把单次刷新交给 refresh_cache_once,自己负责定时、超时保护和决定是否继续循环。
调用图:调用 2 个内部函数(emit_load_metric, refresh_cache_once);外部调用 3 个(sleep, timeout, error!)。
CloudConfigBundleService::refresh_cache_once473–496 ↗
async fn refresh_cache_once(&self) -> bool
作用:执行一次后台缓存刷新。它只关心把远端最新配置写进缓存,不负责改变当前运行中的配置。
数据流:它读取当前登录信息 → 没登录或账号不合适就输出 false,表示后台循环可以停;合适则远端拉取并更新缓存 → 无论这次刷新成功还是失败,通常输出 true 让后台以后继续试。
调用关系:它由 refresh_cache_in_background 定时调用;真正联网和重试仍交给 fetch_remote_bundle_and_update_cache_with_retries,自己负责把结果记成刷新成功或刷新失败的指标。
调用图:调用 3 个内部函数(emit_load_metric, fetch_remote_bundle_and_update_cache_with_retries, cloud_config_eligible_auth);被 1 处调用(refresh_cache_in_background);外部调用 1 个(error!)。
cloud-config/src/bundle_loader.rs源码 ↗
可以把这个文件看成云端配置的“启动按钮”。程序需要从服务器拿一包配置,里面可能影响功能开关、策略或默认行为;如果没有这一步,程序可能只能用本地旧配置,甚至不知道该按什么规则运行。这里先用 AuthManager(登录和凭据管理器)准备访问服务器所需的身份信息,再用 BackendBundleClient(和后端服务器说话的客户端)创建 CloudConfigBundleService(真正执行下载和缓存的服务)。它会同时做两件事:一个任务负责启动时加载配置包,另一个任务在后台刷新缓存。文件里还用 OnceLock 和 Mutex 保存后台刷新任务的位置:OnceLock 像“只安装一次的盒子”,Mutex 是“一把锁,防止多个地方同时改盒子”。如果新的刷新任务来了,旧的会被停止,避免后台跑出一堆重复任务。
refresher_task_slot16–19 ↗
fn refresher_task_slot() -> &'static Mutex<Option<JoinHandle<()>>>
作用:这个函数提供一个全局的小格子,用来存放当前正在跑的后台刷新任务。这样后面创建新刷新任务时,可以找到旧任务并停掉它。
数据流:进去没有普通参数;它读取一个只初始化一次的静态位置 → 如果这个位置还没建好,就创建一个带锁的空槽位 → 出来的是同一个全局槽位的引用,里面可以放一个后台任务句柄。
调用关系:它只被 cloud_config_bundle_loader 使用。cloud_config_bundle_loader 每次启动新的云配置刷新任务时,都会先来这里拿到槽位,再把新任务放进去,并在需要时取消旧任务。
调用图:被 1 处调用(cloud_config_bundle_loader);外部调用 1 个(new)。
cloud_config_bundle_loader21–53 ↗
fn cloud_config_bundle_loader(
auth_manager: Arc<AuthManager>,
chatgpt_base_url: String,
codex_home: PathBuf,
) -> CloudConfigBundleLoader
作用:这个函数用已有的登录管理器、服务器地址和本地目录,组装出一个 CloudConfigBundleLoader(云配置包加载器)。调用它以后,启动加载会立刻开始,后台刷新也会开始。
数据流:进去的是 AuthManager(身份信息来源)、chatgpt_base_url(服务器地址)和 codex_home(本地工作目录)→ 它创建后端客户端和配置服务,启动一个“加载启动配置”的异步任务,再启动一个“后台刷新缓存”的异步任务;随后把刷新任务登记到全局槽位里,若已有旧刷新任务就中止旧的 → 出来的是 CloudConfigBundleLoader,别人等待它时会拿到启动配置加载结果;如果加载任务崩了,会被包装成 CloudConfigBundleLoadError 这种清楚的错误。
调用关系:它是这个文件的核心装配函数,由 cloud_config_bundle_loader_for_storage 在准备好登录管理器后调用。它自己会调用 refresher_task_slot 来管理后台刷新任务,并把实际下载、超时和缓存工作交给 CloudConfigBundleService。
调用图:调用 4 个内部函数(new, refresher_task_slot, new, new);被 1 处调用(cloud_config_bundle_loader_for_storage);外部调用 2 个(new, spawn)。
cloud_config_bundle_loader_for_storage55–71 ↗
async fn cloud_config_bundle_loader_for_storage(
codex_home: PathBuf,
enable_codex_api_key_env: bool,
credentials_store_mode: AuthCredentialsStoreMode,
keyring_backend_kind: AuthKeyrin
作用:这个函数是更省事的一站式入口:调用者不用先准备 AuthManager,它会根据本地目录、凭据保存方式、钥匙串后端和服务器地址先建好登录管理器,再返回云配置加载器。
数据流:进去的是 codex_home、本地是否允许用环境变量里的 API key、凭据保存模式、钥匙串后端类型和服务器地址 → 它异步创建共享的 AuthManager,并复制需要继续使用的路径和地址 → 最后把这些交给 cloud_config_bundle_loader,出来的是可用于加载云端配置包的 CloudConfigBundleLoader。
调用关系:它位于更外层,适合启动阶段直接调用。它先调用 AuthManager::shared 准备身份信息,再把后续装配工作交给 cloud_config_bundle_loader。
调用图:调用 2 个内部函数(cloud_config_bundle_loader, shared);外部调用 1 个(clone)。
模型目录客户端
这些文件涵盖远程和静态模型目录来源,以及将它们转换为可用模型预设和元数据的管理器。
codex-api/src/endpoint/models.rs源码 ↗
这个文件解决的问题很直接:程序需要知道服务器支持哪些模型,比如模型名字、能力、优先级等。如果每个地方都自己拼网址、加认证、解析返回内容,就很容易出错。这里把这些事集中到 ModelsClient 里。它内部用 EndpointSession 统一处理请求的基础地址、认证信息、额外请求头和网络发送。真正列模型时,它会访问 models 路径,把客户端版本 client_version 加到网址查询参数里,好让服务器知道“这个客户端是什么版本”。服务器返回 JSON(一种常见的文本数据格式)后,它把内容转成 ModelsResponse,再取出模型列表。同时它还读取 HTTP 头里的 ETag,ETag 可以理解成“这份清单的版本号”。文件后半部分是测试用的假网络层和假认证,用来确认请求网址拼得对、返回内容能解析、ETag 没丢。
ModelsClient::new19–23 ↗
fn new(transport: T, provider: Provider, auth: SharedAuthProvider) -> Self
作用:创建一个可以访问模型列表接口的客户端。调用者把网络发送工具、服务商配置和认证方式交进来,它把这些装进一个会话里,后面发请求都靠这个会话。
数据流:进去的是 transport(真正发 HTTP 请求的工具)、provider(服务器地址、重试规则等配置)和 auth(加认证头的工具)→ 它调用 EndpointSession::new 把这些合成一个统一会话 → 出来的是 ModelsClient,里面已经带好以后请求模型接口所需的基础能力。
调用关系:这是使用模型接口前的第一步。测试里的 appends_client_version_query、parses_models_response、list_models_includes_etag 会用它搭出客户端;其他流程如 models_client_hits_models_endpoint 和 list_models 相关场景也会先通过它准备好会话。它把具体初始化工作交给 EndpointSession::new。
调用图:调用 1 个内部函数(new);被 5 处调用(appends_client_version_query, list_models_includes_etag, parses_models_response, models_client_hits_models_endpoint, list_models)。
ModelsClient::with_telemetry25–29 ↗
fn with_telemetry(self, request: Option<Arc<dyn RequestTelemetry>>) -> Self
作用:给这个客户端加上请求遥测。遥测可以理解成“记录请求过程的观察器”,比如统计耗时或记录请求事件。
数据流:进去的是一个已有的 ModelsClient 和可选的 RequestTelemetry → 它把遥测交给内部 session 的 with_request_telemetry → 出来的是一个新的 ModelsClient,功能和原来一样,但请求时会带上这套观察记录能力。
调用关系:它通常在客户端创建之后、真正请求之前调用。它不自己发网络请求,只是把记录请求信息的工具接到 EndpointSession 上,后续 list_models 发请求时会间接受到影响。
调用图:调用 1 个内部函数(with_request_telemetry)。
ModelsClient::path31–33 ↗
fn path() -> &'static str
作用:返回模型列表接口固定使用的路径:models。这样路径只写在一个地方,避免到处手写字符串写错。
数据流:不需要输入 → 直接给出字符串 models → 调用方拿这个字符串去拼完整请求地址。
调用关系:ModelsClient::list_models 在发请求前会用它确定访问哪个接口。它像一个小标签,告诉会话“这次要打到模型列表这个门口”。
ModelsClient::append_client_version_query35–38 ↗
fn append_client_version_query(req: &mut codex_client::Request, client_version: &str)
作用:把客户端版本号追加到请求网址后面。服务器可以根据这个版本号决定返回哪些模型,或者判断客户端是否太旧。
数据流:进去的是一个可修改的请求 req 和 client_version 字符串 → 它先看网址里有没有问号,如果已有查询参数就用 &,没有就用 ? → 最后把 client_version=版本号拼到 req.url 上,直接改动这个请求。
调用关系:它是在 ModelsClient::list_models 准备请求时使用的小工具。list_models 把它作为改造请求的一步,让 EndpointSession 真正发送前的网址已经带好版本信息。
调用图:外部调用 1 个(format!)。
ModelsClient::list_models40–73 ↗
async fn list_models(
&self,
client_version: &str,
extra_headers: HeaderMap,
) -> Result<(Vec<ModelInfo>, Option<String>), ApiError>
作用:向服务器要模型清单,并把服务器返回的原始 JSON 变成程序能直接用的模型对象列表。它还会返回 ETag,用来表示这份清单的版本。
数据流:进去的是 client_version 和 extra_headers(额外 HTTP 请求头)→ 它用 GET 方法访问 models 路径,发送前把 client_version 加到网址里,并带上额外请求头 → 收到响应后读取 ETag,再把响应体从 JSON 解析成 ModelsResponse → 出来的是模型列表和可选的 ETag;如果 JSON 解析失败,就返回 ApiError,错误里会带上失败原因和原始响应内容,方便排查。
调用关系:这是这个文件的核心函数,上层想知道有哪些模型时会调用它。它先问 ModelsClient::path 要接口路径,再把实际发送交给 EndpointSession::execute_with;请求发送前还会借助 append_client_version_query 修改网址。测试函数会围绕它检查网址、解析结果和 ETag。
调用图:调用 1 个内部函数(execute_with);外部调用 1 个(path)。
tests::CapturingTransport::default101–107 ↗
fn default() -> Self
作用:创建一个测试用的默认假网络工具。它不会真的访问网络,而是准备好一个空模型列表,并能记住最后一次请求。
数据流:不需要输入 → 它新建一个共享位置保存 last_request,新建一个空的 ModelsResponse,并把 etag 设为空 → 出来的是 CapturingTransport,测试可以用它观察客户端到底发了什么请求。
调用关系:这是测试辅助工具的默认构造方式。它服务于测试里的 ModelsClient::new,让客户端以为自己在用真实网络层,但实际请求会被 CapturingTransport::execute 截住。
tests::CapturingTransport::execute111–123 ↗
async fn execute(&self, req: Request) -> Result<Response, TransportError>
作用:模拟一次普通 HTTP 请求。它的重点不是联网,而是把请求保存下来,并返回测试预设的模型列表。
数据流:进去的是一个 Request → 它先把这个请求存到 last_request,方便测试稍后检查网址;然后把预设的 ModelsResponse 转成 JSON 字节;如果测试设置了 etag,就放进响应头 → 出来的是一个状态为 OK 的 Response,里面有响应头和响应体。
调用关系:当测试里的 ModelsClient::list_models 真正执行请求时,EndpointSession 最终会调用这个 execute。它让测试不用依赖真实服务器,也能验证 list_models 拼出来的请求和处理返回内容的行为。
tests::CapturingTransport::stream125–127 ↗
async fn stream(&self, _req: Request) -> Result<StreamResponse, TransportError>
作用:模拟流式请求,但这里故意让它失败。因为模型列表接口不应该走流式通道,测试中如果走到这里,就说明流程跑偏了。
数据流:进去的是一个 Request,但它不使用 → 直接返回一个 TransportError::Build,错误信息说明 stream 不应该运行 → 没有成功响应,也不会改动保存的请求记录。
调用关系:它是 HttpTransport 接口要求必须实现的一部分。ModelsClient::list_models 应该调用普通 execute,而不是 stream;所以这个函数像一个警报器,防止测试悄悄走错网络路径。
调用图:外部调用 1 个(Build)。
tests::DummyAuth::add_auth_headers134–134 ↗
fn add_auth_headers(&self, _headers: &mut HeaderMap)
作用:测试用的假认证。它什么认证头都不加,因为这些测试只关心模型接口请求和响应,不关心真实登录。
数据流:进去的是可修改的 HeaderMap → 它不做任何改动 → 出来的请求头保持原样。
调用关系:ModelsClient::new 需要一个认证提供者,所以测试用 DummyAuth 填这个位置。真正请求准备过程中如果会话要求加认证头,它调用到这里也不会影响测试结果。
tests::provider137–152 ↗
fn provider(base_url: &str) -> Provider
作用:快速造一个测试用的服务商配置。测试只要给基础地址,就能得到一个带重试规则和超时设置的 Provider。
数据流:进去的是 base_url 字符串 → 它把名字设成 test,把基础地址填进去,准备空请求头和固定重试配置、超时时间 → 出来的是 Provider,可交给 ModelsClient::new 使用。
调用关系:三个测试函数都会用它来避免重复写一大段配置。它生成的 Provider 会被 EndpointSession 用来拼出最终请求地址,比如 base_url 加上 models 路径。
调用图:外部调用 3 个(from_millis, from_secs, new)。
tests::appends_client_version_query155–189 ↗
async fn appends_client_version_query()
作用:测试 list_models 会不会把客户端版本号正确加到请求网址里。这能防止以后改代码时漏掉 client_version。
数据流:进去的是测试里写死的空模型响应和版本号 0.99.0 → 它创建 CapturingTransport、Provider、DummyAuth 和 ModelsClient,然后调用 list_models → 最后读取假网络层保存的请求网址,并断言网址等于带 client_version 的完整地址。
调用关系:这是围绕 ModelsClient::list_models 的行为测试。它通过 CapturingTransport::execute 抓住实际请求,再检查 append_client_version_query 的效果是否真的体现在最终 URL 上。
调用图:调用 1 个内部函数(new);外部调用 6 个(new, new, new, new, assert_eq!, provider)。
tests::parses_models_response192–243 ↗
async fn parses_models_response()
作用:测试客户端能不能把服务器返回的模型 JSON 正确读成 ModelInfo。它确认不仅请求能发出去,返回的数据也能被程序正常使用。
数据流:进去的是一个测试构造的 ModelsResponse,里面有一个名为 gpt-test 的模型和许多字段 → 它用假网络层返回这份响应,再调用 list_models → 出来的是解析后的模型列表;测试检查列表长度、slug、supported_in_api 和 priority 等关键字段是否正确。
调用关系:这是 ModelsClient::list_models 的解析能力测试。CapturingTransport::execute 提供预设 JSON,list_models 负责解码,测试最后用断言确认解码结果没有丢字段或读错字段。
调用图:调用 1 个内部函数(new);外部调用 6 个(new, new, new, assert_eq!, provider, vec!)。
tests::list_models_includes_etag246–268 ↗
async fn list_models_includes_etag()
作用:测试 list_models 会不会把响应头里的 ETag 带出来。ETag 对缓存很重要,丢了就很难判断模型清单是否更新过。
数据流:进去的是一个空模型响应和预设的 ETag "abc" → 它创建假网络层,让响应头带上这个 ETag,然后调用 list_models → 出来的是空模型列表和 Some("abc");测试确认两者都符合预期。
调用关系:这是 ModelsClient::list_models 对响应头处理的测试。CapturingTransport::execute 负责放入 ETag,list_models 负责读取并返回,测试用断言保证这条信息没有在中途丢失。
调用图:调用 1 个内部函数(new);外部调用 6 个(new, new, new, new, assert_eq!, provider)。
model-provider/src/models_endpoint.rs源码 ↗
可以把这个文件想成一个“模型菜单服务员”。上层只问它:现在有哪些模型能用?它先确认该用哪种认证方式,比如登录态、API Key,或服务商自己的取密钥命令;再把服务商信息整理成 API 请求需要的样子;然后用 HTTP 客户端访问 OpenAI 兼容的 /models 地址。为了不让程序一直卡住,它给这次刷新设了 5 秒超时。请求结束后,不管成功失败,它还会记一份遥测信息(也就是给开发者排查问题用的运行记录),包括有没有带认证头、状态码、耗时、请求 ID 等。文件底部的测试专门确认:即使没有缓存的登录信息,只要服务商配置了“命令取认证”,也能正确报告出来。
OpenAiModelsEndpoint::new42–50 ↗
fn new(
provider_info: ModelProviderInfo,
auth_manager: Option<Arc<AuthManager>>,
) -> Self
作用:创建一个模型列表端点对象。别人后面要查模型列表时,会先用它把“服务商信息”和“可选的登录管理器”装在一起。
数据流:进去的是服务商配置和一个可能存在的认证管理器 → 它把这两样保存到 OpenAiModelsEndpoint 结构里 → 出来的是一个可被模型管理器使用的端点对象,本身不立刻发网络请求。
调用关系:模型管理器会用它创建实际的模型查询入口;测试也用它造出不同配置的端点,检查命令认证的判断是否正确。
调用图:被 3 处调用(command_auth_provider_reports_command_auth_without_cached_auth, provider_without_command_auth_reports_no_command_auth, models_manager)。
OpenAiModelsEndpoint::auth52–57 ↗
async fn auth(&self) -> Option<CodexAuth>
作用:尝试拿到当前可用的登录或认证信息。它的作用是让后续请求知道该不该带令牌、用哪种认证方式。
数据流:进去的是这个端点里保存的认证管理器 → 如果有认证管理器,就异步询问它当前认证;如果没有,就直接认为没有认证 → 出来的是一个可能为空的 CodexAuth。
调用关系:查询模型列表和判断是否使用 Codex 后端时都会先调用它。它是认证信息进入这个文件后续流程的入口。
调用图:被 2 处调用(list_models, uses_codex_backend)。
OpenAiModelsEndpoint::auth_env96–102 ↗
fn auth_env(&self) -> AuthEnvTelemetry
作用:收集当前环境里和认证有关的线索,用来写日志和排查问题。比如哪些 API Key 环境变量存在、Codex API Key 是否启用。
数据流:进去的是服务商信息,以及认证管理器里关于 Codex API Key 环境变量是否启用的状态 → 它调用 collect_auth_env_telemetry 汇总这些线索 → 出来的是一份 AuthEnvTelemetry,供请求遥测使用。
调用关系:模型列表请求准备遥测对象时会调用它。它不决定认证是否成功,只负责把环境状态整理成可记录的信息。
调用图:被 1 处调用(list_models);外部调用 1 个(collect_auth_env_telemetry)。
OpenAiModelsEndpoint::has_command_auth106–108 ↗
fn has_command_auth(&self) -> bool
作用:告诉外部:这个服务商是不是配置了“运行一个命令来取得认证信息”。这对上层判断认证能力很重要,尤其是在还没有缓存登录态时。
数据流:进去的是端点里保存的服务商配置 → 它检查配置中的认证部分是否包含命令认证 → 出来的是 true 或 false,不修改任何数据。
调用关系:这是 ModelsEndpointClient 接口的一部分,模型管理器可以通过它了解这个端点的认证方式;测试专门验证了有命令和没命令两种情况。
调用图:调用 1 个内部函数(has_command_auth)。
OpenAiModelsEndpoint::uses_codex_backend110–112 ↗
fn uses_codex_backend(&self) -> ModelsEndpointFuture<'_, bool>
作用:判断当前认证是否指向 Codex 自家的后端。上层可能需要根据这个结果选择不同的行为或展示方式。
数据流:进去的是端点对象 → 它先调用 auth 拿当前认证,再检查这份认证是否声明使用 Codex 后端 → 出来的是一个异步结果,最终值是 true 或 false。
调用关系:这是 ModelsEndpointClient 接口里的方法;它把真正的异步判断包装成 future(一种代表“以后会完成的结果”的对象),方便模型管理器统一调用。
调用图:调用 1 个内部函数(auth);外部调用 1 个(pin)。
OpenAiModelsEndpoint::list_models114–119 ↗
fn list_models(
&'a self,
client_version: &'a str,
) -> ModelsEndpointFuture<'a, CoreResult<(Vec<ModelInfo>, Option<String>)>>
作用:真正去远端服务请求可用模型列表。它把认证、服务商地址、HTTP 客户端、超时和错误处理都串起来,让上层只拿到“模型列表或错误”。
数据流:进去的是客户端版本号,以及端点里已有的服务商信息和认证管理器 → 它获取认证,整理 API 服务商配置,解析该带的认证信息,创建 HTTP 传输层和遥测对象,然后访问 /models;如果 5 秒内没完成就转成超时错误,如果 API 返回错误就映射成项目统一错误 → 出来的是模型信息列表和一个可选字符串,同时会记录请求耗时和认证相关遥测。
调用关系:模型管理器刷新模型列表时会走到这里。它会调用 auth、auth_env、HTTP 客户端构建、服务商转换和认证解析等步骤,并把请求遥测交给 ModelsRequestTelemetry::on_request 在每次请求后记录。
调用图:调用 6 个内部函数(new, new, build_reqwest_client, to_api_provider, auth, auth_env);外部调用 7 个(new, pin, new, auth_header_telemetry, start_global_timer, resolve_provider_auth, timeout)。
ModelsRequestTelemetry::on_request131–211 ↗
fn on_request(
&self,
attempt: u64,
status: Option<http::StatusCode>,
error: Option<&TransportError>,
duration: Duration,
)
作用:每次 /models 请求结束后记录一份运行记录。它帮助开发者知道请求是否成功、花了多久、认证头有没有带上、服务端返回了什么排错线索。
数据流:进去的是请求尝试次数、HTTP 状态码、可能的网络错误和耗时 → 它判断成功失败,提取错误消息和响应里的调试信息,比如请求 ID、Cloudflare 标识、认证错误码 → 出来没有普通返回值,但会写两类 tracing 事件,并发出反馈标签供后续分析。
调用关系:它实现了 RequestTelemetry 接口,所以 HTTP 模型客户端在请求完成时会回调它。它把 list_models 事先准备好的认证模式和环境信息,变成日志和反馈系统能读懂的字段。
调用图:外部调用 2 个(emit_feedback_request_tags_with_auth_env, event!)。
tests::provider_info_with_command_auth221–236 ↗
fn provider_info_with_command_auth() -> ModelProviderInfo
作用:为测试造一个带“命令认证”的服务商配置。这样测试不用依赖真实配置文件,也能验证命令认证相关判断。
数据流:进去没有外部参数 → 它从默认 OpenAI 服务商配置开始,填入一个叫 print-token 的取令牌命令、超时时间、刷新间隔和当前工作目录 → 出来是一份带命令认证的 ModelProviderInfo。
调用关系:它只服务于测试,被 tests::command_auth_provider_reports_command_auth_without_cached_auth 调用,用来搭建“服务商配置了命令认证”的场景。
调用图:调用 1 个内部函数(create_openai_provider);外部调用 3 个(new, new, current_dir)。
tests::command_auth_provider_reports_command_auth_without_cached_auth239–246 ↗
fn command_auth_provider_reports_command_auth_without_cached_auth()
作用:验证一个关键边界情况:即使没有认证管理器和缓存认证,只要服务商配置了命令认证,端点也应该报告“有命令认证”。
数据流:进去没有参数 → 它先用 provider_info_with_command_auth 造配置,再用 OpenAiModelsEndpoint::new 创建端点,且不传认证管理器 → 最后断言 has_command_auth 返回 true。
调用关系:这是针对 OpenAiModelsEndpoint::has_command_auth 的测试。它保证上层不会因为当前没有缓存登录态,就误以为这个服务商完全没有认证办法。
调用图:调用 1 个内部函数(new);外部调用 2 个(assert!, provider_info_with_command_auth)。
tests::provider_without_command_auth_reports_no_command_auth249–256 ↗
fn provider_without_command_auth_reports_no_command_auth()
作用:验证普通 OpenAI 服务商配置在没有命令认证时,会正确报告“没有命令认证”。
数据流:进去没有参数 → 它创建一份默认 OpenAI 服务商配置,再创建端点,同样不传认证管理器 → 最后断言 has_command_auth 返回 false。
调用关系:这是另一条配套测试,和有命令认证的测试形成对照,确保 has_command_auth 不是永远返回 true。
调用图:调用 2 个内部函数(create_openai_provider, new);外部调用 1 个(assert!)。
model-provider/src/amazon_bedrock/catalog.rs源码 ↗
可以把这个文件想成一张给前台看的菜单:用户看到的是 GPT-5.5、GPT-5.4 这类模型,但真正下单到 Amazon Bedrock 时,必须用 Bedrock 自己的模型编号。这里先从项目内置的模型资料里取出对应模型,再把模型名换成 Bedrock 的编号,并统一设置 Bedrock 支持的上下文窗口大小。上下文窗口可以理解成模型一次能“看见”的文字容量。另一个关键点是服务档位:有些平台可能支持更快或更贵的档位,但这里明确说明 Bedrock 目前对这些 GPT 模型只使用默认档位,所以会把额外档位全部清空。文件末尾的测试会检查三件事:模型编号是不是 Bedrock 的、上下文窗口是不是正确、是否真的只允许默认档位。
static_model_catalog11–26 ↗
fn static_model_catalog() -> ModelsResponse
作用:生成一份固定的 Bedrock 模型清单,里面目前包含 GPT-5.5 和 GPT-5.4。别人需要知道 Bedrock 能用哪些模型时,就会从这里拿这份清单。
数据流:进去不需要外部参数 → 它创建一个模型列表,把 GPT-5.5 和 GPT-5.4 放进去,并交给后续步骤去清理服务档位 → 出来的是一个 ModelsResponse,也就是一份可返回给上层使用的模型清单。
调用关系:这是这个文件的主要出口。测试函数会调用它来确认清单内容正确;它内部会把做好的清单交给 with_default_only_service_tier,让所有模型都符合 Bedrock 只用默认服务档位的限制。
调用图:调用 1 个内部函数(with_default_only_service_tier);被 3 处调用(catalog_uses_mantle_model_ids_as_slugs, gpt_5_bedrock_models_only_allow_default_service_tier, gpt_5_bedrock_models_use_bedrock_context_window);外部调用 1 个(vec!)。
with_default_only_service_tier28–36 ↗
fn with_default_only_service_tier(mut catalog: ModelsResponse) -> ModelsResponse
作用:把一份模型清单里的所有“加速档位”或“服务档位”信息清掉。这样上层就不会误以为 Bedrock 支持 priority 之类的特殊档位。
数据流:进去的是一份可修改的模型清单 → 它逐个检查里面的模型,把 additional_speed_tiers、service_tiers 清空,并把 default_service_tier 设为 None → 出来的还是同一份清单,但里面已经只剩下隐含的默认档位。
调用关系:它由 static_model_catalog 调用,是生成 Bedrock 清单时的最后一道整理工序。它不负责挑选模型,只负责把服务档位改成 Bedrock 当前真实支持的样子。
调用图:被 1 处调用(static_model_catalog)。
gpt_5_bedrock_model38–45 ↗
fn gpt_5_bedrock_model(openai_slug: &str, bedrock_slug: &str, priority: i32) -> ModelInfo
作用:把一个通用的 GPT-5 模型资料改造成 Bedrock 版本。重点是把公开模型名换成 Bedrock 的模型编号,并设置 Bedrock 专用的上下文窗口大小和排序优先级。
数据流:进去的是 OpenAI 风格的模型名、Bedrock 风格的模型名,以及优先级数字 → 它先找出内置的 OpenAI 模型资料,再改写 slug、priority、context_window 和 max_context_window → 出来的是一条 Bedrock 可用的 ModelInfo。
调用关系:它是制作单个 Bedrock 模型条目的小零件。做模型条目时,它会把查找基础资料的工作交给 bundled_openai_model,然后自己负责把这条资料改成 Bedrock 需要的样子。
调用图:调用 1 个内部函数(bundled_openai_model)。
bundled_openai_model47–54 ↗
fn bundled_openai_model(slug: &str) -> ModelInfo
作用:从项目自带的模型资料文件里,按模型名找出一条 OpenAI 模型资料。它相当于先拿一份标准模板,后面再改成 Bedrock 版本。
数据流:进去的是一个模型名 slug → 它读取内置的 models.json 模型清单,找到 slug 相同的模型 → 找到了就返回 ModelInfo;如果内置资料解析失败或找不到模型,就直接报错停止,因为这说明程序打包的基础资料有问题。
调用关系:它由 gpt_5_bedrock_model 调用,专门负责取基础模型模板。真正读取内置模型清单的动作交给外部的 bundled_models_response 完成。
调用图:被 1 处调用(gpt_5_bedrock_model);外部调用 1 个(bundled_models_response)。
tests::catalog_uses_mantle_model_ids_as_slugs64–70 ↗
fn catalog_uses_mantle_model_ids_as_slugs()
作用:这个测试确认生成出来的 Bedrock 清单没有继续使用 OpenAI 风格的模型名,而是使用 Bedrock 真正需要的模型编号。
数据流:进去没有测试输入 → 它调用 static_model_catalog 生成清单,然后检查清单长度和两个模型的 slug → 如果 slug 不是预期的 Bedrock 模型编号,测试就失败。
调用关系:它站在使用者角度检查 static_model_catalog 的结果,防止以后有人改代码时把模型名换回了不适合 Bedrock 的名字。
调用图:调用 1 个内部函数(static_model_catalog);外部调用 1 个(assert_eq!)。
tests::gpt_5_bedrock_models_use_bedrock_context_window73–100 ↗
fn gpt_5_bedrock_models_use_bedrock_context_window()
作用:这个测试确认 Bedrock 版 GPT-5.5 和 GPT-5.4 都使用 Bedrock 规定的上下文窗口大小。上下文窗口就是模型一次最多能处理多少内容。
数据流:进去没有测试输入 → 它调用 static_model_catalog,找到 GPT-5.5 和 GPT-5.4 两条模型记录 → 检查它们的 context_window 和 max_context_window 都等于 Bedrock 的固定数值。
调用关系:它检查 static_model_catalog 最终产物里的容量设置,间接保护 gpt_5_bedrock_model 对模型窗口大小的改写行为。
调用图:调用 1 个内部函数(static_model_catalog);外部调用 1 个(assert_eq!)。
tests::gpt_5_bedrock_models_only_allow_default_service_tier103–120 ↗
fn gpt_5_bedrock_models_only_allow_default_service_tier()
作用:这个测试确认 Bedrock 模型不会暴露 priority 等额外服务档位,只保留默认行为。这样调用方就不会发出 Bedrock 不支持的档位请求。
数据流:进去没有测试输入 → 它调用 static_model_catalog,逐个检查模型的额外速度档位、服务档位和默认服务档位都被清空 → 还会模拟请求 priority 和 default,确认都不会被解析成可用档位。
调用关系:它验证 static_model_catalog 调用 with_default_only_service_tier 之后的效果,防止未来改动让 Bedrock 清单错误地带上服务档位信息。
调用图:调用 1 个内部函数(static_model_catalog);外部调用 1 个(assert_eq!)。
models-manager/src/manager.rs源码 ↗
可以把这个文件看成“模型菜单的管家”。模型列表可能来自程序自带的清单,也可能来自 OpenAI 兼容服务的 /models 接口,还可能来自本地缓存文件。这个文件先判断要不要联网刷新,再决定能不能用缓存,最后按登录方式和可见性过滤,做成用户界面能直接展示的模型预设。它还会挑默认模型、查某个模型的详细参数,并处理 ETag(一种服务器给数据版本贴的标签,用来判断缓存是否过期)。这里有两种管家:OpenAiModelsManager 会联网和读写缓存;StaticModelsManager 只使用传进来的固定清单,适合测试或不需要联网的场景。
RefreshStrategy::as_str61–67 ↗
fn as_str(self) -> &'static str
作用:把刷新策略变成固定的英文小写文字,方便写日志或显示。比如“强制联网”会变成 online。
数据流:进去的是一个刷新策略枚举值 → 它用简单匹配找到对应字符串 → 出来的是一段静态文字,不改任何状态。
调用关系:它是 RefreshStrategy::fmt 的小帮手;当刷新策略需要被格式化成文字时,会先来这里取标准写法。
调用图:被 1 处调用(fmt)。
RefreshStrategy::fmt71–73 ↗
ModelsManager::list_models83–97 ↗
fn list_models(
&self,
refresh_strategy: RefreshStrategy,
) -> ModelsManagerFuture<'_, Vec<ModelPreset>>
作用:列出用户当前真正能看到、能选择的模型列表。它不是简单返回原始数据,而是会刷新、排序、过滤,再标出默认项。
数据流:进去的是刷新策略,比如联网或只用缓存 → 它先拿当前原始模型目录,再调用 ModelsManager::build_available_models 整理 → 出来的是适合选择器展示的模型预设列表。
调用关系:ModelsManager::get_default_model 会用它来找默认模型;它自己负责串起“拿目录”和“整理成可选菜单”这两步。
调用图:调用 1 个内部函数(build_available_models);被 1 处调用(get_default_model);外部调用 2 个(pin, info_span!)。
ModelsManager::build_available_models117–129 ↗
fn build_available_models(&self, mut remote_models: Vec<ModelInfo>) -> Vec<ModelPreset>
作用:把原始模型信息加工成用户能选的模型菜单。它会按优先级排序,还会根据登录方式隐藏不该显示的模型。
数据流:进去的是一批远程或内置模型信息 → 先按优先级排序,再转换成模型预设,接着按认证方式过滤,最后标出默认显示项 → 出来的是整理好的模型预设列表。
调用关系:ModelsManager::list_models 和 ModelsManager::try_list_models 都会把拿到的模型交给它;它再调用模型预设里的过滤和标默认能力完成收尾。
调用图:调用 2 个内部函数(filter_by_auth, mark_default_by_picker_visibility);被 2 处调用(list_models, try_list_models)。
ModelsManager::try_list_models139–142 ↗
fn try_list_models(&self) -> Result<Vec<ModelPreset>, TryLockError>
作用:尝试立刻列出模型,但不愿意等待锁。适合不能卡住当前流程的地方使用。
数据流:进去没有额外参数 → 它尝试读取当前内存里的模型;如果锁拿不到就返回错误,拿到了就调用 ModelsManager::build_available_models 整理 → 出来是模型列表或“现在读不了”的错误。
调用关系:它是 ModelsManager::list_models 的快速版;不会触发刷新,只用当前已有状态,适合需要低延迟的调用点。
调用图:调用 1 个内部函数(build_available_models)。
ModelsManager::get_default_model149–167 ↗
fn get_default_model(
&'a self,
model: &'a Option<String>,
refresh_strategy: RefreshStrategy,
) -> ModelsManagerFuture<'a, String>
作用:决定最终要用哪个模型。用户指定了就尊重用户;没指定时,就从可用模型里挑默认的。
数据流:进去的是可选的模型名和刷新策略 → 如果模型名存在,直接返回它;否则调用 ModelsManager::list_models 获取可选列表,再交给 default_model_from_available 挑一个 → 出来是模型名字符串。
调用关系:它位于配置和实际会话之间:会话需要一个模型名时会用它;它把列模型的工作交给 ModelsManager::list_models,把挑选规则交给 default_model_from_available。
调用图:调用 2 个内部函数(list_models, default_model_from_available);外部调用 2 个(pin, info_span!)。
ModelsManager::get_model_info171–183 ↗
fn get_model_info(
&'a self,
model: &'a str,
config: &'a ModelsManagerConfig,
) -> ModelsManagerFuture<'a, ModelInfo>
作用:查某个模型的详细资料,比如能力、优先级、可见性等,并套用配置里的调整。
数据流:进去的是模型名和模型管理配置 → 它读取当前内存里的远程模型候选,再调用 construct_model_info_from_candidates 找最合适的资料并应用配置覆盖 → 出来是一份最终模型信息。
调用关系:当核心逻辑需要知道某个模型具体怎么用时会调用它;它把实际匹配和配置修正交给 construct_model_info_from_candidates。
调用图:调用 1 个内部函数(construct_model_info_from_candidates);外部调用 2 个(pin, info_span!)。
OpenAiModelsManager::new215–230 ↗
fn new(
codex_home: PathBuf,
endpoint_client: Arc<dyn ModelsEndpointClient>,
auth_manager: Option<Arc<AuthManager>>,
) -> Self
作用:创建一个会联网、会用缓存的模型管理器。它把缓存文件位置、远程接口客户端、登录管理器和初始模型清单装配起来。
数据流:进去的是 Codex 主目录、远程接口客户端和可选登录管理器 → 它拼出 models_cache.json 路径,创建缓存管理器,并从内置模型文件加载初始模型 → 出来是一个可用的 OpenAiModelsManager。
调用关系:上层创建模型系统时会调用它;测试里的 OpenAI 管理器构造函数也会用它。后续刷新、列模型、读缓存都依赖这里装好的零件。
调用图:调用 2 个内部函数(new, load_remote_models_from_file);被 2 处调用(models_manager, openai_manager_for_tests_with_auth);外部调用 2 个(join, new)。
StaticModelsManager::new235–240 ↗
fn new(auth_manager: Option<Arc<AuthManager>>, model_catalog: ModelsResponse) -> Self
作用:创建一个只使用固定模型清单的管理器。它不联网,也不写缓存。
数据流:进去的是可选登录管理器和一份模型目录 → 它把目录里的模型列表保存到结构体里 → 出来是一个静态模型管理器。
调用关系:一些固定场景和测试会用它,比如自动审查或本地构造模型目录时;它提供和 OpenAI 管理器一样的接口,但行为更简单。
调用图:被 5 处调用(guardian_request_model_for_auto_review, models_manager, models_manager, static_manager_for_tests, static_manager_reads_latest_auth_mode)。
OpenAiModelsManager::get_remote_models254–256 ↗
fn get_remote_models(&self) -> ModelsManagerFuture<'_, Vec<ModelInfo>>
作用:异步读取当前内存里的模型清单。这里的“远程模型”其实是当前生效的模型快照,可能来自远程、缓存或内置清单。
数据流:进去没有额外参数 → 它等待读锁,复制内存中的模型列表 → 出来是一份模型列表副本,不改原数据。
调用关系:OpenAiModelsManager::raw_model_catalog 会在刷新后调用它拿最终结果;其他 trait 默认方法也会通过管理器接口读取它。
调用图:被 1 处调用(raw_model_catalog);外部调用 1 个(pin)。
OpenAiModelsManager::try_get_remote_models258–260 ↗
fn try_get_remote_models(&self) -> Result<Vec<ModelInfo>, TryLockError>
作用:不等待地读取当前内存模型列表。如果此刻有人正在写入,它就直接失败,而不是排队等待。
数据流:进去没有额外参数 → 它尝试拿读锁;成功就复制模型列表,失败就返回锁错误 → 内存状态不变。
调用关系:ModelsManager::try_list_models 会依赖它做快速列模型;这个函数适合不能阻塞当前线程的路径。
OpenAiModelsManager::auth_manager262–264 ↗
fn auth_manager(&self) -> Option<&AuthManager>
作用:把当前登录管理器暴露给通用模型逻辑,用来判断哪些模型该显示。
数据流:进去没有额外参数 → 它把内部保存的可选登录管理器转成普通引用 → 出来是登录管理器引用,或者没有登录管理器。
调用关系:ModelsManager::build_available_models 会通过这个接口判断是否使用 Codex 后端,从而过滤模型列表。
OpenAiModelsManager::list_collaboration_modes266–268 ↗
fn list_collaboration_modes(&self) -> Vec<CollaborationModeMask>
作用:返回内置的协作模式预设。协作模式可以理解成一组“模型协同工作方式”的开关组合。
数据流:进去没有额外参数 → 它调用 builtin_collaboration_mode_presets 取内置预设 → 出来是一组协作模式掩码。
调用关系:上层需要展示或使用协作模式时会调用它;实际预设内容由 builtin_collaboration_mode_presets 提供。
调用图:调用 1 个内部函数(builtin_collaboration_mode_presets)。
OpenAiModelsManager::raw_model_catalog276–283 ↗
async fn raw_model_catalog(&self, refresh_strategy: RefreshStrategy) -> ModelsResponse
作用:拿到当前原始模型目录,并按要求先尝试刷新。即使刷新失败,它也会尽量返回已有的模型。
数据流:进去的是刷新策略 → 它调用 OpenAiModelsManager::refresh_available_models 尝试更新;失败只记日志不中断,然后调用 OpenAiModelsManager::get_remote_models 读取当前快照 → 出来是 ModelsResponse。
调用关系:trait 方法 ModelsManager::list_models 会先通过它拿原始目录;它是“刷新”和“返回当前模型”的连接点。
调用图:调用 2 个内部函数(get_remote_models, refresh_available_models);外部调用 2 个(pin, error!)。
OpenAiModelsManager::refresh_if_new_etag285–296 ↗
async fn refresh_if_new_etag(&self, etag: String)
作用:收到服务器的新 ETag 后,判断模型目录是否可能变了。没变就延长缓存寿命,变了就强制联网刷新。
数据流:进去的是新的 ETag 字符串 → 它读取当前 ETag;如果相同且已有值,就调用缓存管理器续期;否则用在线策略刷新模型 → 出来没有数据,但可能更新内存和缓存。
调用关系:当外部知道模型版本标签时会调用它;它先问 OpenAiModelsManager::get_etag,再根据结果选择 renew_cache_ttl 或 OpenAiModelsManager::refresh_available_models。
调用图:调用 3 个内部函数(renew_cache_ttl, get_etag, refresh_available_models);外部调用 2 个(pin, error!)。
OpenAiModelsManager::refresh_available_models299–330 ↗
async fn refresh_available_models(&self, refresh_strategy: RefreshStrategy) -> CoreResult<()>
作用:按照刷新策略决定模型列表从哪里来:只读缓存、先用缓存、不行再联网,或直接联网。
数据流:进去的是刷新策略 → 它先用 OpenAiModelsManager::should_refresh_models 判断当前认证条件是否允许刷新;然后按策略调用 OpenAiModelsManager::try_load_cache 或 OpenAiModelsManager::fetch_and_update_models → 出来是成功或错误,同时可能更新内存模型。
调用关系:OpenAiModelsManager::raw_model_catalog 和 OpenAiModelsManager::refresh_if_new_etag 都会调用它;它是整个刷新流程的调度中心。
调用图:调用 3 个内部函数(fetch_and_update_models, should_refresh_models, try_load_cache);被 2 处调用(raw_model_catalog, refresh_if_new_etag);外部调用 2 个(info!, matches!)。
OpenAiModelsManager::fetch_and_update_models332–341 ↗
async fn fetch_and_update_models(&self) -> CoreResult<()>
作用:真正去远程接口拉取最新模型,并把结果写进内存和缓存。
数据流:进去没有额外参数 → 它获取当前客户端版本,调用远程客户端的 list_models,拿到模型和 ETag;再调用 OpenAiModelsManager::apply_remote_models 更新内存,保存 ETag,并持久化到缓存 → 出来是成功或远程错误。
调用关系:只有 OpenAiModelsManager::refresh_available_models 决定必须联网时才会调用它;它把网络返回的数据交给应用和缓存两条后续路径。
调用图:调用 2 个内部函数(persist_cache, apply_remote_models);被 1 处调用(refresh_available_models);外部调用 2 个(list_models, client_version_to_whole)。
OpenAiModelsManager::should_refresh_models343–345 ↗
async fn should_refresh_models(&self) -> bool
作用:判断当前身份是否适合刷新远程模型。没有合适认证时,它会避免不必要或不允许的联网拉取。
数据流:进去没有额外参数 → 它询问远程端点是否使用 Codex 后端,或者是否支持命令级认证 → 出来是一个布尔值,表示要不要尝试刷新。
调用关系:OpenAiModelsManager::refresh_available_models 开始时会先问它;它把认证细节留给端点客户端处理。
调用图:被 1 处调用(refresh_available_models);外部调用 2 个(has_command_auth, uses_codex_backend)。
OpenAiModelsManager::get_etag347–349 ↗
async fn get_etag(&self) -> Option<String>
作用:读取当前保存的 ETag,也就是当前模型缓存对应的数据版本标签。
数据流:进去没有额外参数 → 它等待读锁,复制内部 ETag → 出来是一个可选字符串。
调用关系:OpenAiModelsManager::refresh_if_new_etag 用它来判断外部给的新 ETag 是否和当前一致。
调用图:被 1 处调用(refresh_if_new_etag)。
OpenAiModelsManager::apply_remote_models352–381 ↗
async fn apply_remote_models(&self, models: Vec<ModelInfo>)
作用:把新拿到的模型列表应用到内存里。它会决定是完全相信远程列表,还是把远程结果合并进内置列表。
数据流:进去的是一批模型信息 → 如果远程列表非空、有可展示模型,并且用户是 ChatGPT 账号登录,就直接替换内存清单;否则先读取内置模型,再按模型标识更新或追加远程模型 → 出来没有返回值,但会改写内存中的模型列表。
调用关系:OpenAiModelsManager::fetch_and_update_models 和 OpenAiModelsManager::try_load_cache 都会把拿到的模型交给它;它是保证最终模型清单合理的合并点。
调用图:调用 1 个内部函数(load_remote_models_from_file);被 2 处调用(fetch_and_update_models, try_load_cache)。
OpenAiModelsManager::try_load_cache384–407 ↗
async fn try_load_cache(&self) -> bool
作用:尝试从本地缓存文件恢复模型列表,前提是缓存还新鲜、客户端版本也匹配。
数据流:进去没有额外参数 → 它记录计时,取当前客户端版本,调用缓存管理器读取新鲜缓存;如果没有可用缓存就返回 false;如果有,就写入 ETag,并调用 OpenAiModelsManager::apply_remote_models 应用模型 → 出来是是否成功使用缓存。
调用关系:OpenAiModelsManager::refresh_available_models 在离线或先缓存后联网的策略下会调用它;它成功时可以避免一次网络请求。
调用图:调用 2 个内部函数(load_fresh, apply_remote_models);被 1 处调用(refresh_available_models);外部调用 3 个(start_global_timer, client_version_to_whole, info!)。
StaticModelsManager::raw_model_catalog411–420 ↗
fn raw_model_catalog(
&self,
_refresh_strategy: RefreshStrategy,
) -> ModelsManagerFuture<'_, ModelsResponse>
作用:返回静态管理器里的原始模型目录。刷新策略对它不起作用,因为它本来就不联网。
数据流:进去的是刷新策略但会被忽略 → 它调用 StaticModelsManager::get_remote_models 复制固定模型列表 → 出来是 ModelsResponse。
调用关系:它实现和 OpenAI 管理器相同的接口,方便上层不关心背后是静态清单还是远程清单。
调用图:调用 1 个内部函数(get_remote_models);外部调用 1 个(pin)。
StaticModelsManager::get_remote_models422–424 ↗
fn get_remote_models(&self) -> ModelsManagerFuture<'_, Vec<ModelInfo>>
作用:读取静态管理器保存的模型列表。
数据流:进去没有额外参数 → 它复制结构体里的模型列表 → 出来是一份模型列表副本,不改原数据。
调用关系:StaticModelsManager::raw_model_catalog 会调用它;通用 trait 方法也可以通过它拿模型快照。
调用图:被 1 处调用(raw_model_catalog);外部调用 1 个(pin)。
StaticModelsManager::try_get_remote_models426–428 ↗
fn try_get_remote_models(&self) -> Result<Vec<ModelInfo>, TryLockError>
作用:立即返回静态模型列表。因为没有异步锁,所以基本不会因为等待而失败。
数据流:进去没有额外参数 → 它直接复制固定模型列表 → 出来是成功结果里的模型列表。
调用关系:通用的 ModelsManager::try_list_models 可以用它快速构建可选模型列表。
StaticModelsManager::auth_manager430–432 ↗
fn auth_manager(&self) -> Option<&AuthManager>
作用:返回静态管理器绑定的登录管理器,用来让通用过滤逻辑判断哪些模型可见。
数据流:进去没有额外参数 → 它把内部可选登录管理器转成引用 → 出来是登录管理器引用或空。
调用关系:ModelsManager::build_available_models 会通过这个接口读取认证状态;静态和远程管理器都提供同样入口。
StaticModelsManager::list_collaboration_modes434–436 ↗
fn list_collaboration_modes(&self) -> Vec<CollaborationModeMask>
作用:返回内置协作模式预设,和 OpenAI 管理器保持同样行为。
数据流:进去没有额外参数 → 它调用 builtin_collaboration_mode_presets → 出来是一组协作模式配置。
调用关系:上层请求协作模式时会调用它;具体预设由公共的内置预设函数提供。
调用图:调用 1 个内部函数(builtin_collaboration_mode_presets)。
StaticModelsManager::refresh_if_new_etag438–440 ↗
fn refresh_if_new_etag(&self, _etag: String) -> ModelsManagerFuture<'_, ()>
作用:静态管理器收到 ETag 时什么也不做。因为它的数据是固定的,不靠服务器版本标签更新。
数据流:进去的是 ETag 字符串但被忽略 → 它返回一个立即完成的异步任务 → 出来没有结果,也不改任何状态。
调用关系:它只是为了满足 ModelsManager 接口;上层可以统一调用,不必专门判断当前是哪种管理器。
调用图:外部调用 1 个(pin)。
load_remote_models_from_file443–445 ↗
fn load_remote_models_from_file() -> Result<Vec<ModelInfo>, std::io::Error>
作用:读取程序自带的模型清单,作为没有远程数据时的基础列表。
数据流:进去没有额外参数 → 它调用 bundled_models_response 读取打包进程序的模型响应,并取出其中的模型数组 → 出来是模型列表,或者读取错误。
调用关系:OpenAiModelsManager::new 用它初始化模型;OpenAiModelsManager::apply_remote_models 在需要合并远程和内置模型时也会用它。
调用图:被 2 处调用(apply_remote_models, new);外部调用 1 个(bundled_models_response)。
default_model_from_available447–454 ↗
fn default_model_from_available(available: Vec<ModelPreset>) -> String
作用:从可用模型列表里挑一个默认模型。优先选已经标成默认的;没有就选第一个。
数据流:进去的是整理好的模型预设列表 → 它先找 is_default 为真的模型;找不到就拿列表第一个;列表为空就给空字符串 → 出来是模型名。
调用关系:ModelsManager::get_default_model 在用户没有指定模型时调用它,完成最后的默认选择。
调用图:被 1 处调用(get_default_model)。
find_model_by_longest_prefix456–472 ↗
fn find_model_by_longest_prefix(model: &str, candidates: &[ModelInfo]) -> Option<ModelInfo>
作用:按“最长前缀”找最匹配的模型资料。这样像 gpt-5-codex-extra 这类名字,也能匹配到最具体的基础模型。
数据流:进去的是一个模型名和候选模型列表 → 它逐个检查候选的 slug 是否是模型名开头,并保留 slug 最长的那个 → 出来是最匹配的模型信息,或者没有匹配。
调用关系:construct_model_info_from_candidates 直接用它做主匹配;find_model_by_namespaced_suffix 也会在去掉命名空间后再调用它。
调用图:被 2 处调用(construct_model_info_from_candidates, find_model_by_namespaced_suffix)。
find_model_by_namespaced_suffix474–491 ↗
fn find_model_by_namespaced_suffix(model: &str, candidates: &[ModelInfo]) -> Option<ModelInfo>
作用:处理带命名空间的模型名,比如 custom/gpt-5。它只在格式很简单、安全时,才去掉前面的命名空间再尝试匹配。
数据流:进去的是模型名和候选列表 → 它把名字按第一个 / 分成命名空间和后缀;如果后缀还有 / 或命名空间含奇怪字符,就放弃;否则用后缀调用 find_model_by_longest_prefix → 出来是匹配到的模型信息或空。
调用关系:construct_model_info_from_candidates 在普通匹配失败后会用它补救;它把真正的匹配工作交回 find_model_by_longest_prefix。
调用图:调用 1 个内部函数(find_model_by_longest_prefix)。
construct_model_info_from_candidates493–512 ↗
fn construct_model_info_from_candidates(
model: &str,
candidates: &[ModelInfo],
config: &ModelsManagerConfig,
) -> ModelInfo
作用:为指定模型拼出最终模型信息。它先尽量从候选里找相近资料,找不到才用模型名生成一份兜底资料。
数据流:进去的是模型名、候选模型列表和配置 → 它先调用 find_model_by_longest_prefix,失败再试命名空间后缀匹配;如果找到远程资料,就把 slug 改成用户实际请求的模型名;如果没找到,就调用 model_info_from_slug 生成默认资料;最后用 with_config_overrides 套用配置修改 → 出来是最终 ModelInfo。
调用关系:ModelsManager::get_model_info 会调用它来回答“这个模型有什么能力”;测试也会直接用它验证离线构造行为。
调用图:调用 3 个内部函数(find_model_by_longest_prefix, model_info_from_slug, with_config_overrides);被 2 处调用(get_model_info, construct_model_info_offline_for_tests)。
本地 OSS 提供方就绪
这些文件提供共享 OSS 助手,以及基于本地客户端检查构建的具体 LM Studio 和 Ollama 启动集成。
utils/oss/src/lib.rs源码 ↗
这个文件解决的是“用户选了本地模型服务以后,系统该怎么安全启动”的问题。OSS provider 可以理解成一种模型供应方,这里主要是 LM Studio 和 Ollama。系统不能直接假设它们已经装好、模型已经下载、服务已经能连上,否则后面一发请求就会失败。这个文件先用 provider_id 判断用户选的是哪一家;如果只是想知道默认模型,就返回对应服务推荐的模型名;如果要真正开始用,就调用对应服务自己的检查和准备函数。Ollama 还多一步:先确认它支持系统需要的 responses 能力。遇到不认识的 provider,这里不会报错,而是跳过准备,避免误伤别的配置。整体上它像一个前台分诊台:先看你是哪类服务,再把你交给对应的专门检查员。
get_default_model_for_oss_provider8–14 ↗
fn get_default_model_for_oss_provider(provider_id: &str) -> Option<&'static str>
作用:根据模型服务的名字,告诉调用者这个服务默认应该用哪个模型。有人在创建配置或给用户填默认值时,会用它避免让用户从零开始猜模型名。
数据流:输入是一个 provider_id,也就是模型服务的标识字符串。函数把它和已知的 LM Studio、Ollama 标识做比较;如果匹配,就拿出对应库里写好的默认模型名;如果都不匹配,就返回 None,表示“不知道默认模型”。它不改任何外部状态,只给出一个可选结果。
调用关系:它是这个文件里最简单的查询入口。三个测试函数会分别用 LM Studio、Ollama 和未知 provider 来调用它,确认它对已知服务返回正确模型,对未知服务保持沉默。
调用图:被 3 处调用(test_get_default_model_for_provider_lmstudio, test_get_default_model_for_provider_ollama, test_get_default_model_for_provider_unknown)。
ensure_oss_provider_ready17–38 ↗
async fn ensure_oss_provider_ready(
provider_id: &str,
config: &Config,
) -> Result<(), std::io::Error>
作用:在真正使用某个本地或开源模型服务之前,确认它已经能工作。它会检查服务是否可达、需要的能力是否支持、模型是否已经准备好,避免后面聊天或执行任务时才突然失败。
数据流:输入是 provider_id 和整份 Config 配置。函数先判断这是 LM Studio、Ollama,还是未知服务;LM Studio 会交给 codex_lmstudio 的准备函数;Ollama 会先检查 responses 能力,再交给 codex_ollama 的准备函数。外部准备函数如果失败,这里会把错误包装成标准的 std::io::Error 返回;如果一切正常,或者 provider 不认识但选择跳过,就返回 Ok(())。
调用关系:它站在启动和使用模型之间,像一道开机前检查。它自己不下载或连接服务的细节,而是把具体工作交给 LM Studio 或 Ollama 各自的库函数;调用者只需要调用这一个函数,就能用统一方式准备不同的 OSS provider。
调用图:外部调用 3 个(ensure_oss_ready, ensure_oss_ready, ensure_responses_supported)。
tests::test_get_default_model_for_provider_lmstudio45–48 ↗
fn test_get_default_model_for_provider_lmstudio()
作用:这个测试确认:当传入 LM Studio 的 provider_id 时,默认模型函数确实返回 LM Studio 的默认模型名。
数据流:测试输入是 LM Studio 的固定标识。它调用 get_default_model_for_oss_provider 得到结果,然后用 assert_eq! 比较结果是不是 Some(codex_lmstudio::DEFAULT_OSS_MODEL)。测试不会改系统状态,只验证返回值是否符合预期。
调用关系:它是 get_default_model_for_oss_provider 的一条保护线,专门覆盖 LM Studio 这条分支。以后如果有人改坏了 LM Studio 的默认模型映射,这个测试会提醒。
调用图:调用 1 个内部函数(get_default_model_for_oss_provider);外部调用 1 个(assert_eq!)。
tests::test_get_default_model_for_provider_ollama51–54 ↗
fn test_get_default_model_for_provider_ollama()
作用:这个测试确认:当传入 Ollama 的 provider_id 时,默认模型函数会返回 Ollama 的默认模型名。
数据流:测试把 Ollama 的固定标识传进 get_default_model_for_oss_provider,拿到一个可选模型名。然后它用 assert_eq! 检查结果是否等于 Some(codex_ollama::DEFAULT_OSS_MODEL)。它只做验证,不产生业务输出。
调用关系:它覆盖 get_default_model_for_oss_provider 里 Ollama 的那条分支。这样当 Ollama 的默认模型常量或匹配逻辑被改动时,测试能尽早发现不一致。
调用图:调用 1 个内部函数(get_default_model_for_oss_provider);外部调用 1 个(assert_eq!)。
tests::test_get_default_model_for_provider_unknown57–60 ↗
fn test_get_default_model_for_provider_unknown()
作用:这个测试确认:遇到不认识的 provider_id 时,默认模型函数不会乱猜,而是返回 None。
数据流:测试输入是字符串 unknown-provider。它调用 get_default_model_for_oss_provider,期望得到 None;随后用 assert_eq! 验证这个结果。它的重点是确认未知服务不会被错误地映射到某个默认模型。
调用关系:它覆盖 get_default_model_for_oss_provider 的兜底分支。这个测试保证函数对新服务或拼错的服务名保持安全保守,不会偷偷选择 LM Studio 或 Ollama 的模型。
调用图:调用 1 个内部函数(get_default_model_for_oss_provider);外部调用 1 个(assert_eq!)。
ollama/src/client.rs源码 ↗
Ollama 是一个在本机运行大模型的服务。这个文件里的 OllamaClient 就是专门访问它的客户端。没有它,项目就不知道本机 Ollama 是否可用,也无法列出模型、确认版本、或拉取新模型。它先从配置里找 Ollama 的地址,把地址整理成真正的服务器根地址,再用 HTTP(网页和服务之间常用的请求方式)去探测健康接口。下载模型时,Ollama 会持续吐出一行行 JSON 文本;这里用 LineBuffer 把零散网络数据拼成完整行,再交给解析器变成 PullEvent,比如“正在下载”“某块进度”“成功”或“失败”。一个容易忽略的点是:Ollama 有时即使下载失败,HTTP 状态也可能还是 200,所以代码不能只看请求是否成功,还必须读下载流里的错误事件。
OllamaClient::try_from_oss_provider35–50 ↗
async fn try_from_oss_provider(config: &Config) -> io::Result<Self>
作用:从项目配置里找到内置的开源 Ollama 提供方,并创建一个可用的 OllamaClient。它的用处是:在真正使用本机模型前,先确认配置里有这个提供方,而且 Ollama 服务真的能连上。
数据流:输入是一份 Config 配置;它从里面按内置提供方 ID 找到 Ollama 的地址等信息。如果找不到,就返回“找不到内置提供方”的错误;如果找到了,就把提供方信息交给后续创建逻辑。出来的是一个已经探测过服务器的 OllamaClient,或者一个带说明的错误。
调用关系:它是上层准备 Ollama 环境时会走的入口之一;调用图里 ensure_oss_ready 会用它来确认本机开源模型环境可用。它自己不直接拼网络请求,而是把 provider 交给 try_from_provider 继续处理。
调用图:被 1 处调用(ensure_oss_ready);外部调用 1 个(try_from_provider)。
OllamaClient::try_from_provider_with_base_url53–56 ↗
async fn try_from_provider_with_base_url(base_url: &str) -> io::Result<Self>
作用:这是测试专用的小门。它用一个临时给定的网址造出假的 Ollama 提供方,再尝试创建客户端,方便测试不用依赖真实用户配置。
数据流:输入是一个 base_url 字符串;它先用测试辅助函数做出一个 provider,再把 provider 交给正常的客户端创建流程。出来的是可用客户端,或者连接失败等错误。
调用关系:多个测试会调用它,比如测试版本查询、健康探测、服务器存在或不存在时的行为。它的作用是把测试服务器地址接到正式的 try_from_provider 流程里,保证测试覆盖的仍是主要逻辑。
调用图:被 4 处调用(test_fetch_version, test_probe_server_happy_path_openai_compat_and_native, test_try_from_oss_provider_err_when_server_missing, test_try_from_oss_provider_ok_when_server_running);外部调用 2 个(try_from_provider, create_oss_provider_with_base_url)。
OllamaClient::try_from_provider59–78 ↗
async fn try_from_provider(provider: &ModelProviderInfo) -> io::Result<Self>
作用:根据一个模型提供方定义创建 OllamaClient,并立刻试探服务器是否能访问。它相当于“拿到电话号码后先拨一下,确认真的能接通”。
数据流:输入是 ModelProviderInfo,里面包含 base_url;它判断这个地址是不是 OpenAI 兼容接口,把地址整理成主机根地址,创建带 5 秒连接超时的 HTTP 客户端,然后调用服务器探测。出来的是已保存好 HTTP 客户端、主机地址和接口类型的 OllamaClient,或者连接错误。
调用关系:调用图里 ensure_responses_supported 会用它来确认提供方能支持需要的响应接口。它会借助 base_url_to_host_root 和 is_openai_compatible_base_url 处理地址,再通过 probe_server 做最后的可用性检查。
调用图:调用 2 个内部函数(base_url_to_host_root, is_openai_compatible_base_url);被 1 处调用(ensure_responses_supported);外部调用 2 个(builder, from_secs)。
OllamaClient::probe_server81–101 ↗
async fn probe_server(&self) -> io::Result<()>
作用:检查 Ollama 服务是否真的开着、是否能正常回应。用户没启动 Ollama 时,它会返回一条很明确的提示,告诉用户运行 ollama serve。
数据流:它读取客户端里保存的 host_root 和接口类型;如果是 OpenAI 兼容模式,就请求 /v1/models,否则请求 Ollama 原生的 /api/tags。请求成功且状态码表示成功,就返回空结果表示没问题;连不上或状态不对,就记录警告并返回友好的连接错误。
调用关系:它通常在创建客户端时被 try_from_provider 调用,属于“上车前检查车能不能开”的步骤。它不负责后续业务,只负责给其他功能一个可靠前提:服务器存在并可访问。
OllamaClient::fetch_models104–127 ↗
async fn fetch_models(&self) -> io::Result<Vec<String>>
作用:向本机 Ollama 询问当前有哪些模型。界面或命令行想展示“可选模型列表”时,就需要它。
数据流:它读取客户端里的服务器根地址,访问 /api/tags。若服务器返回失败状态,它不抛错,而是返回空列表;若成功,就读取 JSON 里的 models 数组,从每一项取出 name 字段,最后输出模型名字符串列表。
调用关系:这是客户端对外提供的查询能力之一。它直接通过 HTTP GET 找 Ollama 要数据,不再调用本文件里的其他业务函数。测试 test_fetch_models_happy_path 会用假服务器验证它能正确取出模型名。
OllamaClient::fetch_version130–153 ↗
async fn fetch_version(&self) -> io::Result<Option<Version>>
作用:查询本机 Ollama 的版本号。需要判断功能兼容性,或给用户展示环境信息时,会用到它。
数据流:它访问 /api/version,成功后从 JSON 里找 version 字段。拿到字符串后会去掉开头可能存在的 v,再用语义化版本号解析器解析,比如把 0.14.1 变成可比较的 Version 对象。失败、字段缺失或格式不对时,返回 None 而不是让程序崩掉。
调用关系:它是健康检查之后的补充查询能力。测试 test_fetch_version 会先用 try_from_provider_with_base_url 建立客户端,再确认这个函数能把服务端返回的版本字符串变成 Version。
OllamaClient::pull_model_stream157–211 ↗
async fn pull_model_stream(
&self,
model: &str,
) -> io::Result<BoxStream<'static, PullEvent>>
作用:开始下载一个模型,并把下载过程变成一串事件流。它不只是等下载结束,而是边收到服务器消息边吐出“状态、进度、成功、错误”。
数据流:输入是模型名;它向 /api/pull 发 POST 请求,请求体里说明要下载哪个模型并开启 stream。成功后,它不断读取网络传来的字节块,用 LineBuffer 拼成完整文本行,再把每行 JSON 交给 pull_events_from_value 变成 PullEvent。输出是一个异步事件流;如果读到 error 字段,会吐出错误并结束;如果读到 status 为 success,会吐出成功并结束。
调用关系:pull_with_reporter 会调用它来获得下载事件。它处在网络响应和用户可见进度之间:下游只需要看 PullEvent,不需要关心 HTTP 分块、换行 JSON、半行数据这些麻烦事。
调用图:被 1 处调用(pull_with_reporter);外部调用 8 个(pin, new, stream!, post, other, format!, default, json!)。
OllamaClient::pull_with_reporter214–245 ↗
async fn pull_with_reporter(
&self,
model: &str,
reporter: &mut dyn PullProgressReporter,
) -> io::Result<()>
作用:这是更高层的下载助手:它负责拉取模型,并把每个进度事件交给一个进度报告器显示或记录。调用者不用自己读事件流。
数据流:输入是模型名和一个 PullProgressReporter;它先报告“正在拉取模型”,然后调用 pull_model_stream 获取事件流。每来一个事件就交给 reporter;遇到 Success 就返回成功,遇到 Error 就返回失败;如果流突然断了但没有成功,也返回异常结束的错误。
调用关系:它把底层的 pull_model_stream 包装成更方便的“一步下载并汇报进度”流程。调用者只要提供一个会处理 PullEvent 的报告器,就能得到完整下载体验。
调用图:调用 1 个内部函数(pull_model_stream);外部调用 4 个(other, format!, Status, on_event)。
OllamaClient::from_host_root249–259 ↗
fn from_host_root(host_root: impl Into<String>) -> Self
作用:这是测试专用的简易构造函数,用一个服务器根地址直接造 OllamaClient。它绕过配置和健康探测,方便测试单个功能。
数据流:输入是 host_root;它创建一个带 5 秒连接超时的 HTTP 客户端,把 host_root 存进去,并默认使用 Ollama 原生接口而不是 OpenAI 兼容接口。输出是一个可用于测试请求的 OllamaClient。
调用关系:测试 test_fetch_models_happy_path、test_probe_server_happy_path_openai_compat_and_native 和 test_pull_model_stream_parses_large_json_lines 会用它连接 wiremock 假服务器。它不用于正式运行,只是让测试能精确控制服务器返回什么。
调用图:被 3 处调用(test_fetch_models_happy_path, test_probe_server_happy_path_openai_compat_and_native, test_pull_model_stream_parses_large_json_lines);外部调用 3 个(into, builder, from_secs)。
tests::test_fetch_models_happy_path270–298 ↗
async fn test_fetch_models_happy_path()
作用:测试“正常情况下能取到模型列表”。它保证 fetch_models 能从 Ollama 返回的数据里提取出模型名。
数据流:它先检查测试环境是否禁用了网络;如果禁用就跳过。否则启动一个假 HTTP 服务器,让 /api/tags 返回两个模型名,然后用 from_host_root 创建客户端并调用 fetch_models。最后检查结果里确实包含 llama3.2:3b 和 mistral。
调用关系:这个测试验证 fetch_models 这条链路:假服务器模拟 Ollama,from_host_root 生成客户端,fetch_models 发请求并解析结果,断言负责确认行为正确。
调用图:调用 1 个内部函数(from_host_root);外部调用 9 个(assert!, json!, var, info!, given, start, new, method, path)。
tests::test_fetch_version301–334 ↗
async fn test_fetch_version()
作用:测试版本查询能正常工作。它确认 fetch_version 可以把服务器返回的版本文本解析成标准版本对象。
数据流:它启动假服务器,让 /api/tags 可用于创建时探测,让 /api/version 返回 {"version":"0.14.1"}。然后用 try_from_provider_with_base_url 建客户端,调用 fetch_version,最后比较结果是否等于 0.14.1。
调用关系:这个测试覆盖了客户端创建时的探测,以及随后 fetch_version 的解析。它把测试专用构造流程和真实版本查询逻辑连在一起检查。
调用图:调用 1 个内部函数(try_from_provider_with_base_url);外部调用 9 个(assert_eq!, json!, var, info!, given, start, new, method, path)。
tests::test_pull_model_stream_parses_large_json_lines337–378 ↗
async fn test_pull_model_stream_parses_large_json_lines()
作用:测试下载流遇到很长的一行 JSON 时也能正确解析。这样可以防止模型下载进度信息太大时被截断或丢失。
数据流:它启动假服务器,让 /api/pull 返回两行 JSON,其中第一行带有很大的 padding 字段。然后创建客户端,调用 pull_model_stream 收集所有事件。最后确认得到的是两个状态事件:pulling layers 和 complete。
调用关系:这个测试专门盯住 pull_model_stream 和 LineBuffer 的配合。假服务器负责制造大行数据,事件流负责把它转成 PullEvent,断言确认长行没有把解析流程弄坏。
调用图:调用 1 个内部函数(from_host_root);外部调用 9 个(assert_matches!, format!, var, info!, given, start, new, method, path)。
tests::test_probe_server_happy_path_openai_compat_and_native381–415 ↗
async fn test_probe_server_happy_path_openai_compat_and_native()
作用:测试两种 Ollama 访问方式都能通过健康探测:原生接口和 OpenAI 兼容接口。它保证地址形式不同也不会误判服务器不可用。
数据流:它启动假服务器,先让 /api/tags 返回成功,用 from_host_root 建原生客户端并调用 probe_server。然后让 /v1/models 返回成功,用带 /v1 的地址创建 OpenAI 兼容客户端,再调用 probe_server。两个探测都应该成功。
调用关系:这个测试覆盖 probe_server 的分支选择。原生路径通过 from_host_root 测,OpenAI 兼容路径通过 try_from_provider_with_base_url 测,确保地址判断和探测接口配合正确。
调用图:调用 2 个内部函数(from_host_root, try_from_provider_with_base_url);外部调用 8 个(format!, var, info!, given, start, new, method, path)。
tests::test_try_from_oss_provider_ok_when_server_running418–439 ↗
async fn test_try_from_oss_provider_ok_when_server_running()
作用:测试服务器正常运行时,客户端创建应该成功。它防止健康探测过于严格,导致明明能访问却创建失败。
数据流:它启动假服务器,让 OpenAI 兼容的 /v1/models 返回 200。然后把这个地址交给 try_from_provider_with_base_url。预期结果是成功得到客户端。
调用关系:这个测试主要覆盖 try_from_provider_with_base_url 到 try_from_provider 再到 probe_server 的成功路径。它用假服务器模拟一个已经运行的 Ollama。
调用图:调用 1 个内部函数(try_from_provider_with_base_url);外部调用 8 个(format!, var, info!, given, start, new, method, path)。
tests::test_try_from_oss_provider_err_when_server_missing442–457 ↗
async fn test_try_from_oss_provider_err_when_server_missing()
作用:测试服务器缺失或没有正确响应时,创建客户端会失败,并给出友好的 Ollama 启动提示。
数据流:它启动一个假服务器但不配置 /v1/models 的成功响应,然后用 try_from_provider_with_base_url 尝试创建客户端。结果应是错误,并且错误文本等于预设的 OLLAMA_CONNECTION_ERROR。
调用关系:这个测试覆盖客户端创建的失败路径。它确认 probe_server 的错误会一路传回调用者,而且用户看到的是可操作的提示,而不是晦涩的网络异常。
调用图:调用 1 个内部函数(try_from_provider_with_base_url);外部调用 5 个(assert_eq!, format!, var, info!, start)。
ollama/src/lib.rs源码 ↗
这个文件像一个“开机检查员”。当用户想用本地开源模型时,它先决定要用哪个模型:如果用户没指定,就用默认的 gpt-oss:20b。然后它尝试连上本机的 Ollama 服务,也就是本地跑模型的后台程序;如果连不上,后面的聊天自然也没法正常工作。连上后,它会看看本地有没有这个模型,没有就触发下载,并用命令行进度条告诉用户进展。文件里还做了一个版本检查:Codex 需要 Ollama 支持 Responses API(可以理解为一种新的对话接口),所以它会判断当前 Ollama 版本是不是足够新。特别的是,如果版本接口不存在或读不出来,它不会立刻报错,而是先放行,让更上层在真正使用时再暴露问题。
ensure_oss_ready23–50 ↗
async fn ensure_oss_ready(config: &Config) -> std::io::Result<()>
作用:在用户选择本地开源模型时,提前确认 Ollama 服务能连上,并且需要的模型已经在本机。如果模型缺失,它会自动下载,避免用户开始使用时才发现缺东西。
数据流:输入是一份配置 Config,里面可能写了用户指定的模型名。函数先从配置里取模型名,没写就使用默认模型;接着用配置创建 Ollama 客户端并连接本地服务;然后读取本机已有模型列表,如果目标模型不在列表里,就创建一个命令行进度报告器并开始拉取模型。最后成功时返回 Ok(());如果连接或下载失败,会把错误交给调用者;如果只是查询模型列表失败,它只记录警告,不立刻中断。
调用关系:它通常在启动阶段被上层流程调用,目的是在真正发起模型请求前把本地环境准备好。它把连接 Ollama 的工作交给 try_from_oss_provider,把显示下载进度的工作交给 CliProgressReporter::new 创建出的报告器;遇到查询模型列表失败时,它通过 warn! 写一条警告,让后续流程继续尝试。
调用图:调用 2 个内部函数(try_from_oss_provider, new);外部调用 1 个(warn!)。
min_responses_version52–54 ↗
fn min_responses_version() -> Version
作用:给出 Ollama 支持 Responses API 所需的最低版本号。这样最低要求只写在一个地方,后面判断版本时不会散落各处。
数据流:它不需要外部输入。函数内部直接构造版本号 0.13.4,然后把这个版本对象返回给调用者。
调用关系:它是版本判断的小尺子。supports_responses 用它来比较当前版本够不够新,ensure_responses_supported 用它在报错信息里告诉用户至少需要哪个版本。
调用图:被 2 处调用(ensure_responses_supported, supports_responses);外部调用 1 个(new)。
supports_responses56–58 ↗
fn supports_responses(version: &Version) -> bool
作用:判断某个 Ollama 版本能不能使用 Responses API。它把“哪些版本算可用”这条规则集中起来,方便测试和复用。
数据流:输入是一个版本号。函数会检查它是不是特殊的 0.0.0,这个通常代表开发版或无法正常标记的版本,文件里把它当作支持;否则就拿它和最低版本 0.13.4 比较。输出是一个布尔值:true 表示可以用,false 表示太旧。
调用关系:它被 ensure_responses_supported 调用,用来做真正的版本门禁。它自己会调用 min_responses_version 取得最低版本,并用版本库的构造函数生成特殊版本 0.0.0 来比较。
调用图:调用 1 个内部函数(min_responses_version);被 1 处调用(ensure_responses_supported);外部调用 1 个(new)。
ensure_responses_supported63–77 ↗
async fn ensure_responses_supported(provider: &ModelProviderInfo) -> std::io::Result<()>
作用:确认正在运行的 Ollama 足够新,可以支持 Codex 需要的 Responses API。如果版本太旧,它会给出清楚的错误信息,提醒用户升级。
数据流:输入是一个模型提供方信息 ModelProviderInfo,里面有连接 Ollama 所需的地址等信息。函数先创建 Ollama 客户端,再向服务询问版本;如果服务没有返回版本,就直接放行。拿到版本后,它调用 supports_responses 判断是否支持;支持就返回成功,不支持就生成一条错误,说明当前版本太旧以及最低要求是什么。
调用关系:它一般在准备使用 Ollama 的早期阶段运行,是一道兼容性检查。它把建客户端的工作交给 try_from_provider,把版本规则交给 supports_responses,并用 min_responses_version 填写面向用户的错误提示。
调用图:调用 3 个内部函数(try_from_provider, min_responses_version, supports_responses);外部调用 2 个(other, format!)。
tests::supports_responses_for_dev_zero84–86 ↗
fn supports_responses_for_dev_zero()
作用:这是一个测试,用来确认特殊版本 0.0.0 会被当作支持 Responses API。这样开发版 Ollama 不会因为版本号特殊而被误拦。
数据流:测试构造版本号 0.0.0,把它交给版本判断函数,然后断言结果必须为真。它不改动系统状态,只验证规则是否符合预期。
调用关系:它属于测试模块,运行测试时才会执行。它用断言检查 supports_responses 的一个特殊分支,防止以后有人改规则时无意破坏开发版兼容性。
调用图:外部调用 1 个(assert!)。
tests::does_not_support_responses_before_cutoff89–91 ↗
fn does_not_support_responses_before_cutoff()
作用:这是一个测试,用来确认最低版本线之前的 Ollama 会被判定为不支持。它防止太旧的版本被错误放行。
数据流:测试创建版本号 0.13.3,也就是最低要求 0.13.4 的前一个版本;然后把它交给判断逻辑,并断言结果必须为假。输出只是测试通过或失败。
调用关系:它在测试阶段执行,专门守住版本下限。它间接保护 ensure_responses_supported,确保真实启动检查不会让不兼容的 Ollama 混过去。
调用图:外部调用 1 个(assert!)。
tests::supports_responses_at_or_after_cutoff94–97 ↗
fn supports_responses_at_or_after_cutoff()
作用:这是一个测试,用来确认最低版本以及更高版本都会被判定为支持。它保证升级后的 Ollama 不会被误判为不可用。
数据流:测试分别构造 0.13.4 和 0.14.0 两个版本,交给版本判断函数,然后断言结果都为真。它只验证判断结果,不产生其他副作用。
调用关系:它在测试运行时执行,覆盖版本线上的正常情况。它和前两个测试一起,把 supports_responses 的关键边界都检查了一遍。
调用图:外部调用 1 个(assert!)。
lmstudio/src/lib.rs源码 ↗
这个文件像是“出发前检查清单”。用户想用本地的开源大模型时,程序不能直接假设一切都准备好了:LM Studio 这个本地模型服务可能没开,模型可能还没下载,模型也可能还没加载。这里公开了默认模型名 DEFAULT_OSS_MODEL,也公开了 LMStudioClient 这个客户端工具。核心函数 ensure_oss_ready 会先从配置里找用户指定的模型;如果没指定,就用默认模型。接着它创建一个能跟本地 LM Studio 说话的客户端,询问本地已有模型列表。如果目标模型不在本地,就触发下载。最后它不会卡住主流程,而是开一个后台任务去加载模型;加载失败只记一条警告,不会立刻让整个程序崩掉。这让上层流程可以继续走,真正的问题也可以在后面更合适的位置暴露出来。
ensure_oss_ready13–46 ↗
async fn ensure_oss_ready(config: &Config) -> std::io::Result<()>
作用:这个函数在启用本地开源模型模式时使用,用来把 LM Studio 的本地模型环境尽量准备好。它会选定要用的模型,确认本地服务可访问,必要时下载模型,并在后台尝试加载模型。
数据流:进去的是一份配置 Config,里面可能写了用户想用的模型名。函数先决定最终模型:有配置就用配置,没有就用默认的 openai/gpt-oss-20b。然后它根据配置创建 LM Studio 客户端,去本地服务查询已有模型;如果查到了列表但目标模型不在里面,就让客户端下载它。如果查询失败,它只写一条警告。最后它启动一个后台任务去加载模型,函数本身返回成功或遇到的输入输出错误。
调用关系:它通常在程序启动、确认要走 --oss 本地模型路线时被上层调用。它把具体和 LM Studio 通信的事情交给 LMStudioClient::try_from_provider 创建出来的客户端去做;后台加载模型则交给 tokio::spawn,这样主流程不用一直等。遇到无法查询或无法加载模型时,它通过 tracing::warn! 留下警告,方便之后排查。
调用图:调用 1 个内部函数(try_from_provider);外部调用 2 个(spawn, warn!)。
连接器与 MCP 发现
这些文件获取连接器目录,建立 MCP 客户端访问,并组装 ChatGPT 可见的连接器列表和工作区门控设置。
connectors/src/lib.rs源码 ↗
可以把这个文件想成“连接器商店的前台”。外部服务返回的是一页一页的原始应用资料,里面可能有重复项、隐藏项、空描述,也可能工作区账号还多一份专属列表。这里先按缓存钥匙找内存缓存;需要正式刷新时,再分页拉普通目录,必要时再拉工作区目录。拉回来后,它会过滤隐藏应用,把同一个 id 的应用合并,把原始 DirectoryApp 转成系统统一认识的 AppInfo,再补安装链接、修剪名字和描述,最后按名字排序。结果会写进内存缓存和磁盘缓存。这样用户看到的是干净、稳定、可复用的连接器列表;网络失败或重复请求带来的麻烦也少很多。
ConnectorDirectoryCacheKey::new39–51 ↗
fn new(
chatgpt_base_url: String,
account_id: Option<String>,
chatgpt_user_id: Option<String>,
is_workspace_account: bool,
) -> Self
作用:创建一个“缓存钥匙”,用来区分不同服务器、账号、用户和账号类型对应的连接器列表。没有这把钥匙,不同用户的缓存可能会串在一起。
数据流:进去的是 ChatGPT 地址、账号 id、用户 id、是否工作区账号 → 函数把这些字段原样装进 ConnectorDirectoryCacheKey → 出来的是一个可比较、可保存的缓存标识。
调用关系:它通常在准备连接器缓存上下文时被调用,测试里也用它造假数据;后面的缓存读取和写入都靠这个 key 判断“这份列表是不是给当前用户用的”。
调用图:被 3 处调用(connector_directory_cache_context, cache_key, cached_directory_connectors_for_tool_suggest_with_auth)。
cached_directory_connectors89–108 ↗
fn cached_directory_connectors(
cache_context: &ConnectorDirectoryCacheContext,
) -> Option<Vec<AppInfo>>
作用:只尝试读取已有缓存,不主动去服务器刷新。它适合在想快速展示旧连接器列表时使用。
数据流:进去的是缓存上下文,里面有缓存钥匙和磁盘路径 → 它先查内存缓存,没找到再读磁盘缓存;如果磁盘命中,还会顺手写回内存 → 出来是连接器列表,或者没有缓存时返回 None。
调用关系:它把工作先交给 cached_directory_connectors_in_memory;内存没命中时交给磁盘缓存模块 load_cached_directory_connectors_from_disk;磁盘读到后再交给 write_cached_directory_connectors_in_memory,让下次读取更快。
调用图:调用 3 个内部函数(cached_directory_connectors_in_memory, load_cached_directory_connectors_from_disk, write_cached_directory_connectors_in_memory);被 1 处调用(cached_directory_connectors_reads_directory_disk_cache)。
cached_directory_connectors_in_memory110–120 ↗
fn cached_directory_connectors_in_memory(
cache_key: &ConnectorDirectoryCacheKey,
) -> Option<Vec<AppInfo>>
作用:从进程内存里拿缓存的连接器列表。它不检查是否过期,只看缓存钥匙是否一致。
数据流:进去的是缓存钥匙 → 它锁住全局缓存,防止同时读写造成混乱,再比较 key → 如果 key 对得上,就复制一份连接器列表返回;否则返回 None,不改数据。
调用关系:它只服务于 cached_directory_connectors,是“先看桌面抽屉里有没有”的第一步;如果这里没找到,外层函数才会去磁盘找。
调用图:被 1 处调用(cached_directory_connectors)。
unexpired_directory_connectors_in_memory122–133 ↗
fn unexpired_directory_connectors_in_memory(
cache_key: &ConnectorDirectoryCacheKey,
) -> Option<Vec<AppInfo>>
作用:读取仍在有效期内的内存缓存。它用于避免刚拉过列表又马上重复请求服务器。
数据流:进去的是缓存钥匙 → 它锁住全局缓存,检查 key 是否一致,再用当前时间判断有没有过期 → 没过期就返回复制的列表,过期或不匹配就返回 None。
调用关系:list_all_connectors_with_options 在正式联网前会先问它;只有它说“没有新鲜缓存”时,流程才继续去拉目录页面。
调用图:被 1 处调用(list_all_connectors_with_options);外部调用 1 个(now)。
list_all_connectors_with_options135–178 ↗
async fn list_all_connectors_with_options(
cache_context: ConnectorDirectoryCacheContext,
is_workspace_account: bool,
force_refetch: bool,
mut fetch_page: F,
) -> anyhow::Result<Vec<Ap
作用:这是这个文件的主流程:拿到完整连接器目录,并把它整理成应用能直接使用的列表。它支持强制刷新,也支持工作区账号的额外目录。
数据流:进去的是缓存上下文、是否工作区账号、是否强制刷新、以及一个真正请求页面的 fetch_page 函数 → 它先尝试新鲜内存缓存;没命中就分页拉普通目录,工作区账号再拉工作区目录;之后合并重复应用、转成 AppInfo、补安装链接、清理空白字段、排序并写缓存 → 出来是一份干净的 AppInfo 列表。
调用关系:它像总调度员:先问 unexpired_directory_connectors_in_memory,要联网时交给 list_directory_connectors 和 list_workspace_connectors,整理时用 merge_directory_apps、connector_install_url、normalize_connector_name、normalize_connector_value,最后交给 write_cached_directory_connectors 保存结果。
调用图:调用 8 个内部函数(connector_install_url, list_directory_connectors, list_workspace_connectors, merge_directory_apps, normalize_connector_name, normalize_connector_value, unexpired_directory_connectors_in_memory, write_cached_directory_connectors);被 4 处调用(cached_directory_connectors_reads_directory_disk_cache, list_all_connectors_merges_and_normalizes_directory_apps, list_all_connectors_refreshes_when_only_directory_disk_cache_exists, list_all_connectors_uses_shared_directory_cache)。
write_cached_directory_connectors180–190 ↗
fn write_cached_directory_connectors(
cache_context: &ConnectorDirectoryCacheContext,
connectors: &[AppInfo],
)
作用:把刚整理好的连接器列表同时保存到内存和磁盘。这样短时间内访问更快,重启后也有机会复用旧数据。
数据流:进去的是缓存上下文和连接器列表 → 它先用固定有效期写入内存缓存,再调用磁盘缓存模块写入文件 → 结果没有直接返回值,但全局内存缓存和磁盘缓存都会被更新。
调用关系:它由 list_all_connectors_with_options 在成功刷新后调用;内部把快缓存交给 write_cached_directory_connectors_in_memory,把持久保存交给 directory_cache::write_cached_directory_connectors_to_disk。
调用图:调用 2 个内部函数(write_cached_directory_connectors_to_disk, write_cached_directory_connectors_in_memory);被 1 处调用(list_all_connectors_with_options)。
write_cached_directory_connectors_in_memory192–205 ↗
fn write_cached_directory_connectors_in_memory(
cache_key: ConnectorDirectoryCacheKey,
connectors: &[AppInfo],
ttl: Duration,
)
作用:更新进程里的那份连接器缓存,并设置到期时间。它就像把最新清单放到柜台抽屉里,方便马上再拿。
数据流:进去的是缓存钥匙、连接器切片和有效时长 ttl → 它锁住全局缓存,把列表复制成自己的 Vec,并记录当前时间加 ttl 作为过期时间 → 出来没有返回值,但内存缓存被替换成新内容。
调用关系:cached_directory_connectors 在磁盘命中后会用它回填内存;write_cached_directory_connectors 在刷新成功后也会用它写入正常有效期缓存。
调用图:被 2 处调用(cached_directory_connectors, write_cached_directory_connectors);外部调用 2 个(now, to_vec)。
list_directory_connectors207–238 ↗
async fn list_directory_connectors(fetch_page: &mut F) -> anyhow::Result<Vec<DirectoryApp>>
作用:分页拉取普通连接器目录。服务器一次可能只给一页,它负责一页页翻到最后。
数据流:进去的是 fetch_page 请求函数 → 它从第一页路径开始请求;如果响应里有 next_token,就把 token 编码进下一页 URL;每页都把隐藏应用过滤掉并追加到列表 → 出来是所有普通目录应用的原始 DirectoryApp 列表。
调用关系:list_all_connectors_with_options 需要普通目录时会调用它;测试 list_directory_connectors_omits_tier_for_all_pages 专门检查它生成的分页路径是否符合预期。
调用图:被 2 处调用(list_all_connectors_with_options, list_directory_connectors_omits_tier_for_all_pages);外部调用 3 个(new, format!, encode)。
list_workspace_connectors240–255 ↗
async fn list_workspace_connectors(fetch_page: &mut F) -> anyhow::Result<Vec<DirectoryApp>>
作用:拉取工作区专属连接器目录。它很宽容:如果这个接口失败,就当作没有工作区连接器,而不是让整个列表失败。
数据流:进去的是 fetch_page 请求函数 → 它请求 /connectors/directory/list_workspace?external_logos=true;成功就过滤隐藏应用并返回,失败就返回空列表 → 出来总是一个列表,可能为空。
调用关系:list_all_connectors_with_options 只有在当前账号是工作区账号时才会调用它;它的失败不会打断普通目录的显示。
调用图:被 1 处调用(list_all_connectors_with_options);外部调用 1 个(new)。
merge_directory_apps257–267 ↗
fn merge_directory_apps(apps: Vec<DirectoryApp>) -> Vec<DirectoryApp>
作用:把同一个 id 的重复应用合成一个。目录和工作区列表可能给出同一应用的不同字段,这里负责拼成更完整的一份。
数据流:进去的是一批 DirectoryApp → 它用 id 做字典键,第一次见到就放进去,再次见到就调用 merge_directory_app 把新信息补到旧记录上 → 出来是不重复的 DirectoryApp 列表。
调用关系:list_all_connectors_with_options 拉完多个来源后会调用它;它自己不判断字段细节,而是把单个应用的合并规则交给 merge_directory_app。
调用图:调用 1 个内部函数(merge_directory_app);被 1 处调用(list_all_connectors_with_options);外部调用 1 个(new)。
merge_directory_app269–407 ↗
fn merge_directory_app(existing: &mut DirectoryApp, incoming: DirectoryApp)
作用:把一个新来的应用记录补进已有记录里。规则是尽量保留已有值,但用更有内容的新字段补空缺或更新描述。
数据流:进去的是可修改的 existing 和一个 incoming → 它检查名字、描述、logo、分发渠道、品牌信息、元数据、标签等字段;空的地方用 incoming 补上,描述如果新值非空就替换,某些布尔标记会合并成 true → 出来没有返回值,但 existing 被改得更完整。
调用关系:它只被 merge_directory_apps 调用,是去重合并时真正干细活的零件;外层负责找到同 id 的两条记录,它负责决定每个字段该不该补。
调用图:被 1 处调用(merge_directory_apps)。
directory_app_to_app_info413–429 ↗
fn directory_app_to_app_info(app: DirectoryApp) -> AppInfo
作用:把服务器返回的原始目录应用转换成系统内部统一使用的 AppInfo。这样后面的代码不用认识 DirectoryApp 这种接口格式。
数据流:进去的是 DirectoryApp → 它把 id、名字、描述、logo、品牌、元数据、标签等字段搬到 AppInfo,并给安装链接、可访问状态、插件显示名等字段填默认值 → 出来是一条 AppInfo。
调用关系:它属于目录数据进入应用内部时的转换口;测试里也直接调用它来构造期望结果,方便和刷新后的列表比较。
调用图:被 1 处调用(list_all_connectors_refreshes_when_only_directory_disk_cache_exists);外部调用 1 个(new)。
connector_install_url431–434 ↗
fn connector_install_url(name: &str, connector_id: &str) -> String
作用:给连接器生成一个 ChatGPT 应用安装页面链接。没有服务器给的安装链接时,就用这个函数补一个。
数据流:进去的是连接器名字和 id → 它先用 connector_name_slug 把名字变成适合 URL 的短串,再拼成 https://chatgpt.com/apps/{slug}/{id} → 出来是一条安装网址字符串。
调用关系:list_all_connectors_with_options 在 AppInfo 缺少 install_url 时会调用它;测试里也用它生成期望链接,保证规则一致。
调用图:调用 1 个内部函数(connector_name_slug);被 2 处调用(list_all_connectors_with_options, list_all_connectors_refreshes_when_only_directory_disk_cache_exists);外部调用 1 个(format!)。
connector_name_slug436–451 ↗
fn connector_name_slug(name: &str) -> String
作用:把连接器名字变成能放进网址里的 slug,也就是简短、规整的路径片段。
数据流:进去的是名字字符串 → 它把英文字母和数字保留并转小写,其它字符都变成横杠,再去掉首尾横杠;如果最后什么都不剩,就用 app → 出来是 URL 友好的名字片段。
调用关系:connector_install_url 依赖它生成安装链接里的中间那段;它是一个小工具,不关心连接器其它信息。
调用图:被 1 处调用(connector_install_url);外部调用 1 个(with_capacity)。
normalize_connector_name453–460 ↗
fn normalize_connector_name(name: &str, connector_id: &str) -> String
作用:清理连接器名字,避免展示一堆前后空格或空名字。名字为空时,就退回使用连接器 id。
数据流:进去的是原名字和连接器 id → 它先 trim,也就是去掉前后空白;如果剩下为空,就返回 id,否则返回清理后的名字 → 出来是可展示的名字字符串。
调用关系:list_all_connectors_with_options 在最终整理每个连接器时调用它,确保排序和展示用的名字稳定可读。
调用图:被 1 处调用(list_all_connectors_with_options)。
normalize_connector_value462–467 ↗
fn normalize_connector_value(value: Option<&str>) -> Option<String>
作用:清理可选文本字段,比如描述。空白描述会被当成没有描述,而不是显示空字符串。
数据流:进去的是一个可能存在的字符串引用 → 它去掉前后空白,过滤掉空结果,再复制成 String → 出来是 Some(干净文本) 或 None。
调用关系:list_all_connectors_with_options 用它清理连接器描述;它让后续界面不用再分辨“空字符串”和“真的没有值”。
调用图:被 1 处调用(list_all_connectors_with_options)。
tests::cache_key482–489 ↗
tests::cache_context491–493 ↗
fn cache_context(codex_home: &TempDir, id: &str) -> ConnectorDirectoryCacheContext
作用:测试专用的小帮手,把临时目录和缓存钥匙组成缓存上下文。它让测试不用真的碰用户机器上的缓存目录。
数据流:进去的是临时目录和测试 id → 它取出临时目录路径,调用 tests::cache_key 生成 key,再创建 ConnectorDirectoryCacheContext → 出来是测试用缓存上下文。
调用关系:多个测试在调用 list_all_connectors_with_options 或 cached_directory_connectors 前先用它准备环境。
tests::clear_directory_memory_cache495–500 ↗
fn clear_directory_memory_cache()
作用:测试专用:清空全局内存缓存。这样可以模拟程序刚启动、内存里什么都没有的情况。
数据流:进去不需要参数 → 它锁住全局缓存,把里面的值设成 None → 出来没有返回值,但内存缓存被清空。
调用关系:磁盘缓存相关测试会调用它,确认读取结果不是来自内存,而是真正从磁盘缓存恢复。
tests::app502–515 ↗
fn app(id: &str, name: &str) -> DirectoryApp
作用:测试专用:快速造一个最简单的 DirectoryApp。这样测试可以只关心 id 和名字,不必每次填一堆无关字段。
数据流:进去的是应用 id 和名字 → 它创建 DirectoryApp,把其它可选字段都设为空 → 出来是一条测试用目录应用。
调用关系:多个测试用它构造服务器返回的假应用,再交给 list_all_connectors_with_options 或其它函数验证整理结果。
tests::list_all_connectors_merges_and_normalizes_directory_apps567–638 ↗
async fn list_all_connectors_merges_and_normalizes_directory_apps() -> anyhow::Result<()>
作用:验证主流程会合并重复应用、过滤隐藏应用,并把名字、描述、安装链接整理好。
数据流:进去是测试运行环境 → 它模拟普通目录返回 alpha 和 beta,工作区目录返回另一个 alpha 和一个隐藏应用 → 调用 list_all_connectors_with_options 后检查只剩 alpha、beta,alpha 的描述和品牌被合并,名字被修剪,安装链接被补上。
调用关系:这个测试把 list_all_connectors_with_options、list_workspace_connectors、merge_directory_apps、normalize_connector_name、connector_install_url 等关键步骤串起来验证。
调用图:调用 1 个内部函数(list_all_connectors_with_options);外部调用 6 个(clone, new, new, new, assert_eq!, cache_context)。
tests::cached_directory_connectors_reads_directory_disk_cache645–677 ↗
async fn cached_directory_connectors_reads_directory_disk_cache() -> anyhow::Result<()>
作用:验证磁盘缓存能在内存缓存清空后被读回来。也就是说,缓存不只是存在内存里。
数据流:进去是测试运行环境 → 它先通过 list_all_connectors_with_options 拉取并写缓存;然后清空内存缓存;再调用 cached_directory_connectors → 最后确认没有再次请求服务器,读回的列表和第一次一样。
调用关系:这个测试连接了刷新写入流程和 cached_directory_connectors 的读取流程,重点证明 load_cached_directory_connectors_from_disk 这条路径可用。
调用图:调用 2 个内部函数(cached_directory_connectors, list_all_connectors_with_options);外部调用 7 个(clone, new, new, new, assert_eq!, cache_context, clear_directory_memory_cache)。
tests::list_all_connectors_refreshes_when_only_directory_disk_cache_exists684–744 ↗
async fn list_all_connectors_refreshes_when_only_directory_disk_cache_exists() -> anyhow::Result<()>
作用:验证正式刷新不会只满足于磁盘旧缓存。只有磁盘缓存、没有新鲜内存缓存时,主流程仍会去服务器拿新列表。
数据流:进去是测试运行环境 → 它先拉取 alpha 写入缓存,再清空内存;确认 cached_directory_connectors 能读到 alpha;随后再次调用 list_all_connectors_with_options,让服务器返回 beta → 最后确认请求次数增加,并且结果变成 beta。
调用关系:这个测试区分了 cached_directory_connectors 的“读旧缓存”和 list_all_connectors_with_options 的“需要新鲜结果”两种用途,同时用 connector_install_url 和 directory_app_to_app_info 构造期望值。
调用图:调用 3 个内部函数(connector_install_url, directory_app_to_app_info, list_all_connectors_with_options);外部调用 8 个(clone, new, new, new, assert_eq!, app, cache_context, clear_directory_memory_cache)。
tests::cached_directory_connectors_drops_stale_disk_schema747–766 ↗
async fn cached_directory_connectors_drops_stale_disk_schema() -> anyhow::Result<()>
作用:验证磁盘缓存格式太旧时不会被继续使用。旧格式可能让程序误读数据,所以要丢掉。
数据流:进去是测试运行环境 → 它清空内存,手写一个 schema_version 为 0 的旧缓存文件 → 调用 cached_directory_connectors → 最后确认返回 None,并且旧缓存文件被删除。
调用关系:这个测试主要覆盖 cached_directory_connectors 走磁盘读取时对旧 schema 的防护,确保 directory_cache 模块不会把过期格式当成正常缓存。
调用图:外部调用 9 个(new, assert!, assert_eq!, cache_context, clear_directory_memory_cache, json!, to_vec_pretty, create_dir_all, write)。
tests::list_directory_connectors_omits_tier_for_all_pages769–814 ↗
async fn list_directory_connectors_omits_tier_for_all_pages() -> anyhow::Result<()>
作用:验证普通目录分页请求的 URL 形状正确,特别是不带 tier 参数,并且下一页 token 会被正确编码。
数据流:进去是测试运行环境 → 它记录 list_directory_connectors 请求过的路径;第一页返回 alpha 和带空格的 next_token,第二页检查 token 是否变成 page%202 并返回 beta → 最后确认应用顺序和请求路径都符合预期。
调用关系:这个测试直接针对 list_directory_connectors,确保分页翻页这颗小齿轮不会因为 URL 拼错而漏掉后续连接器。
调用图:调用 1 个内部函数(list_directory_connectors);外部调用 5 个(clone, new, new, new, assert_eq!)。
rmcp-client/src/rmcp_client.rs源码 ↗
MCP(Model Context Protocol,可以理解成“让 AI 程序调用外部工具和资源的一套通信规矩”)服务器可能在本进程里,也可能是一个命令行子进程,还可能是远程 HTTP 服务。这个文件把这些连接方式包成同一个 RmcpClient,让上层不用关心底下到底怎么连。它先按配置做出“待连接的通道”,初始化握手成功后保存可用服务;之后所有列工具、读资源、调用工具的请求都走统一入口。它还会在请求前刷新 OAuth(授权登录令牌),请求后保存新令牌;遇到 HTTP 会话过期的 404,会自动重新建连接再试一次。另一个重要点是“elicitation”(服务器向用户追问信息)期间,普通超时计时会暂停,避免用户还没回答就被误判超时。
ElicitationPauseState::new147–153 ↗
fn new() -> Self
作用:创建一个“暂停计时”的共享状态。它用来记录当前有没有服务器正在等用户回答问题。
数据流:进去没有参数 → 它建立一个计数器和一个通知通道,初始状态是“不暂停” → 出来一个 ElicitationPauseState,后续请求可以订阅它,知道是否该暂停超时计时。
调用关系:创建各种 RmcpClient 时都会调用它,测试里也会调用它。后面 create_elicitation 进入等待用户回答时,会通过同一个状态通知超时逻辑暂停。
调用图:被 4 处调用(new_in_process_client, new_stdio_client, new_streamable_http_client, active_time_timeout_pauses_while_elicitation_is_pending);外部调用 3 个(new, new, channel)。
ElicitationPauseState::enter155–162 ↗
fn enter(&self) -> ElicitationPauseGuard
作用:标记“现在进入等待用户回答”的阶段。只要还有这样的等待,客户端的活动超时就不应该继续扣时间。
数据流:进去是当前暂停状态 → 它把活跃等待数加一;如果这是第一个等待,就广播“暂停” → 出来一个 ElicitationPauseGuard,等这个守卫被丢弃时会自动退出暂停。
调用关系:create_elicitation 在向界面发起追问时使用它。它和 ElicitationPauseGuard::drop 配成一对,像进门拿牌、出门还牌。
调用图:被 1 处调用(create_elicitation);外部调用 1 个(send_replace)。
ElicitationPauseState::subscribe164–166 ↗
fn subscribe(&self) -> watch::Receiver<bool>
作用:让某个操作订阅“是否暂停计时”的变化。这样超时控制可以实时知道用户是否正在被追问。
数据流:进去是暂停状态 → 它从内部通知通道复制一个接收端 → 出来一个接收器,调用者可以用它监听暂停和恢复。
调用关系:run_service_operation_once 会调用它,把接收器交给 active_time_timeout,让单次服务请求按“活动时间”而不是墙上钟表时间来计时。
调用图:被 1 处调用(run_service_operation_once);外部调用 1 个(subscribe)。
ElicitationPauseGuard::drop174–178 ↗
fn drop(&mut self)
作用:在等待用户回答结束时,自动撤销一次暂停标记。这样调用者不需要手动恢复,减少忘记恢复导致永久暂停的风险。
数据流:进去是即将被销毁的守卫 → 它把活跃等待数减一;如果这是最后一个等待,就广播“恢复计时” → 没有返回值,但会改变暂停状态。
调用关系:它由 Rust 的 Drop 机制自动触发。ElicitationPauseState::enter 创建它,等待流程结束或出错离开作用域时,它会自动把状态收尾。
active_time_timeout181–225 ↗
async fn active_time_timeout(
duration: Duration,
mut pause_state: watch::Receiver<bool>,
operation: Fut,
) -> std::result::Result<T, ()>
作用:给一个异步操作加超时,但只计算“没有暂停”的时间。它解决用户正在回答问题时,请求被机械超时的问题。
数据流:进去是总时长、暂停状态接收器、要执行的操作 → 它一边跑操作,一边看暂停信号;暂停时不扣剩余时间,恢复后继续倒计时 → 出来成功结果,或在活动时间用完后返回超时标记。
调用关系:run_service_operation_once 用它包住实际请求。测试 active_time_timeout_pauses_while_elicitation_is_pending 专门验证暂停期间不会消耗超时时间。
调用图:被 2 处调用(run_service_operation_once, active_time_timeout_pauses_while_elicitation_is_pending);外部调用 4 个(now, borrow_and_update, pin!, select!)。
remaining_operation_timeout235–252 ↗
fn remaining_operation_timeout(
label: &str,
timeout: Option<Duration>,
deadline: Option<Instant>,
) -> std::result::Result<Option<Duration>, ClientOperationError>
作用:计算一次重试还剩多少时间可用。它保证多次重试加起来不会超过用户给的总超时时间。
数据流:进去是操作名称、原始超时、最终截止时间 → 它用当前时间减出剩余时长;如果已经没时间,就生成带名称的超时错误 → 出来是剩余时长,或者超时错误。
调用关系:run_service_operation_with_transient_retries 每次重试前都会问它还剩多少预算,再把这个预算交给单次执行。
调用图:被 1 处调用(run_service_operation_with_transient_retries);外部调用 1 个(now)。
ElicitationResponse::from266–272 ↗
fn from(value: CreateElicitationResult) -> Self
作用:把 rmcp 库里的追问结果转换成本项目自己的 ElicitationResponse。这样外层代码可以用统一的数据形状。
数据流:进去是 CreateElicitationResult → 它复制用户动作和内容,并把 meta 设为空 → 出来是 ElicitationResponse。
调用关系:这是类型转换函数,主要在需要把 SDK 的结果交给项目内部或界面层时使用。它不发网络请求,只改数据包装。
CreateElicitationResult::from276–282 ↗
fn from(value: ElicitationResponse) -> Self
作用:把本项目的 ElicitationResponse 转回 rmcp 库需要的结果类型。它用于把界面回答交还给 MCP 通信层。
数据流:进去是 ElicitationResponse → 它取出动作和内容,并把 meta 设为空 → 出来是 CreateElicitationResult。
调用关系:这是和 ElicitationResponse::from 相反方向的转换。它让自定义界面响应能重新进入 rmcp SDK 的协议流程。
RmcpClient::new_in_process_client314–332 ↗
async fn new_in_process_client(
factory: Arc<dyn InProcessTransportFactory>,
) -> io::Result<Self>
作用:创建一个连接到“同进程 MCP 服务器”的客户端。同进程就像两个人在同一间屋里递纸条,不需要启动外部程序或走网络。
数据流:进去是一个能打开内部通道的 factory → 它保存连接配方,创建待连接通道,初始化状态、恢复锁和暂停状态 → 出来一个还没完成握手的 RmcpClient。
调用关系:它会调用 create_pending_transport 准备通道,并调用 ElicitationPauseState::new 准备超时暂停功能。之后 initialize 才会真正完成 MCP 握手。
调用图:调用 1 个内部函数(new);外部调用 3 个(new, create_pending_transport, new)。
RmcpClient::new_stdio_client334–366 ↗
async fn new_stdio_client(
program: OsString,
args: Vec<OsString>,
env: Option<HashMap<OsString, OsString>>,
env_vars: &[McpServerEnvVar],
cwd: Option<PathBuf>,
作用:创建一个通过标准输入输出连接的 MCP 客户端。常见场景是启动一个子进程,然后通过它的 stdin/stdout 说话。
数据流:进去是程序名、参数、环境变量、工作目录和启动器 → 它组装启动命令,启动或准备 stdio 通道,保存子进程句柄 → 出来一个等待初始化的 RmcpClient。
调用关系:make_rmcp_client 等上层构造流程会用它。它调用 create_pending_transport 打开通道,shutdown 后会用保存的进程句柄杀掉这个 stdio 服务器。
调用图:调用 2 个内部函数(new, new);被 4 处调用(make_rmcp_client, drop_kills_wrapper_process_group, shutdown_kills_initialized_stdio_server_with_in_flight_operation, rmcp_client_can_list_and_read_resources);外部调用 4 个(new, create_pending_transport, new, to_vec)。
RmcpClient::new_streamable_http_client369–402 ↗
async fn new_streamable_http_client(
server_name: &str,
url: &str,
bearer_token: Option<String>,
http_headers: Option<HashMap<String, String>>,
env_http_headers
作用:创建一个通过 Streamable HTTP 连接远程 MCP 服务器的客户端。它适合服务器在网络另一端的情况。
数据流:进去是服务器名、地址、令牌、HTTP 头、OAuth 保存方式、HTTP 客户端等 → 它保存这些连接配方并创建待连接 HTTP 通道 → 出来一个等待初始化的 RmcpClient。
调用关系:make_rmcp_client、create_remote_client 等远程客户端构造流程会用它。后续 initialize 会拿这个通道完成真正握手。
调用图:调用 1 个内部函数(new);被 4 处调用(make_rmcp_client, oauth_startup_child, create_client_with_http_client, create_remote_client);外部调用 3 个(new, create_pending_transport, new)。
RmcpClient::initialize406–469 ↗
async fn initialize(
&self,
params: InitializeRequestParams,
timeout: Option<Duration>,
send_elicitation: SendElicitation,
) -> Result<InitializeResult>
作用:和 MCP 服务器做初始化握手。没有这一步,客户端不知道服务器支持什么能力,也不能安全地发后续请求。
数据流:进去是初始化参数、可选超时、以及向界面追问用户的函数 → 它创建客户端服务,取出待连接通道,带重试连接并握手,保存初始化上下文和可用服务 → 出来服务器返回的 InitializeResult。
调用关系:initialize_client 会调用它。它把连接状态从 Connecting 推到 Ready,并在 OAuth 令牌变化时尝试保存;后续 list_tools、call_tool 等都依赖它成功完成。
调用图:调用 1 个内部函数(new);被 1 处调用(initialize_client);外部调用 5 个(clone, anyhow!, matches!, clone, warn!)。
RmcpClient::list_tools471–485 ↗
async fn list_tools(
&self,
params: Option<PaginatedRequestParams>,
timeout: Option<Duration>,
) -> Result<ListToolsResult>
作用:向服务器询问“你有哪些工具可以调用”。这相当于拿一张功能菜单。
数据流:进去是分页参数和可选超时 → 它先刷新 OAuth 令牌,再通过统一服务操作发送 tools/list,请求结束后保存可能更新的令牌 → 出来工具列表结果。
调用关系:它把实际通信交给 run_service_operation。这个统一入口会处理超时、短暂错误重试,以及会话过期后的恢复。
调用图:调用 3 个内部函数(persist_oauth_tokens, refresh_oauth_if_needed, run_service_operation)。
RmcpClient::list_tools_with_connector_ids487–522 ↗
async fn list_tools_with_connector_ids(
&self,
params: Option<PaginatedRequestParams>,
timeout: Option<Duration>,
) -> Result<ListToolsWithConnectorIdResult>
作用:列出工具,同时从工具的附加信息里提取连接器 ID、名称和说明。这样界面可以知道工具来自哪个外部连接器。
数据流:进去是分页参数和可选超时 → 它先刷新令牌并请求 tools/list,再逐个读取工具 meta 里的 connector 字段 → 出来带连接器信息的工具列表。
调用关系:它和 list_tools 一样走 run_service_operation,但额外调用 meta_string 清洗元数据字段。最后也会保存 OAuth 令牌。
调用图:调用 3 个内部函数(persist_oauth_tokens, refresh_oauth_if_needed, run_service_operation)。
RmcpClient::meta_string524–530 ↗
fn meta_string(meta: Option<&rmcp::model::Meta>, key: &str) -> Option<String>
作用:从工具的 meta 附加信息里安全取出一个非空字符串。它避免把空白字符串当成有效连接器信息。
数据流:进去是可选 meta 和字段名 → 它查字段、确认是字符串、去掉前后空格、过滤空值 → 出来是可用字符串,或者没有值。
调用关系:list_tools_with_connector_ids 用它提取 connector_id、connector_name、connector_description 等字段。它是一个小工具函数,不碰网络。
RmcpClient::list_resources532–546 ↗
async fn list_resources(
&self,
params: Option<PaginatedRequestParams>,
timeout: Option<Duration>,
) -> Result<ListResourcesResult>
作用:向服务器询问有哪些资源可以读取。资源可以理解成服务器暴露出来的文件、数据项或内容入口。
数据流:进去是分页参数和可选超时 → 它刷新 OAuth,发送 resources/list,完成后保存新令牌 → 出来资源列表。
调用关系:它把请求交给 run_service_operation,复用同一套超时、重试和会话恢复机制。
调用图:调用 3 个内部函数(persist_oauth_tokens, refresh_oauth_if_needed, run_service_operation)。
RmcpClient::list_resource_templates548–562 ↗
async fn list_resource_templates(
&self,
params: Option<PaginatedRequestParams>,
timeout: Option<Duration>,
) -> Result<ListResourceTemplatesResult>
作用:向服务器询问有哪些资源模板。模板就像带占位符的地址,之后可以填参数读取具体资源。
数据流:进去是分页参数和可选超时 → 它刷新 OAuth,发送 resources/templates/list,结束后保存令牌 → 出来资源模板列表。
调用关系:它是资源发现流程的一部分,底层同样依赖 run_service_operation 来完成实际请求。
调用图:调用 3 个内部函数(persist_oauth_tokens, refresh_oauth_if_needed, run_service_operation)。
RmcpClient::read_resource564–578 ↗
async fn read_resource(
&self,
params: ReadResourceRequestParams,
timeout: Option<Duration>,
) -> Result<ReadResourceResult>
作用:读取某个具体资源的内容。前面列资源像看目录,这个函数像打开其中一项。
数据流:进去是读取资源的参数和可选超时 → 它刷新 OAuth,经统一操作入口发送 resources/read,请求后保存令牌 → 出来资源内容结果。
调用关系:它由上层在用户或模型需要查看资源内容时调用。实际通信、超时和恢复都交给 run_service_operation。
调用图:调用 3 个内部函数(persist_oauth_tokens, refresh_oauth_if_needed, run_service_operation)。
RmcpClient::call_tool580–636 ↗
async fn call_tool(
&self,
name: String,
arguments: Option<serde_json::Value>,
meta: Option<serde_json::Value>,
timeout: Option<Duration>,
) -> Result<CallT
作用:调用 MCP 服务器提供的某个工具。它是客户端真正让外部能力干活的入口。
数据流:进去是工具名、参数 JSON、请求 meta 和可选超时 → 它先确认参数和 meta 必须是 JSON 对象,再组装 MCP 调用请求,发送后检查返回类型 → 出来工具调用结果,或参数格式错误、响应类型不对等错误。
调用关系:call_echo_tool 等测试或上层功能会调用它。它调用 refresh_oauth_if_needed、run_service_operation、persist_oauth_tokens;真正发请求时使用 rmcp 的 peer 请求接口。
调用图:调用 3 个内部函数(persist_oauth_tokens, refresh_oauth_if_needed, run_service_operation);被 1 处调用(call_echo_tool);外部调用 3 个(new, anyhow!, Meta)。
RmcpClient::send_custom_notification638–666 ↗
async fn send_custom_notification(
&self,
method: &str,
params: Option<serde_json::Value>,
) -> Result<()>
作用:发送一个自定义通知给 MCP 服务器。通知是“我告诉你一件事”,通常不等服务器返回业务结果。
数据流:进去是方法名和可选 JSON 参数 → 它刷新 OAuth,组装 CustomNotification,通过服务发送,完成后保存令牌 → 出来成功或错误。
调用关系:它走 run_service_operation,但没有设置超时。适合上层需要发送协议扩展通知时使用。
调用图:调用 3 个内部函数(persist_oauth_tokens, refresh_oauth_if_needed, run_service_operation)。
RmcpClient::send_custom_request668–689 ↗
async fn send_custom_request(
&self,
method: &str,
params: Option<serde_json::Value>,
) -> Result<ServerResult>
作用:发送一个自定义请求给 MCP 服务器,并等待服务器回复。它给协议扩展留了一个通用入口。
数据流:进去是方法名和可选 JSON 参数 → 它刷新 OAuth,组装 CustomRequest,通过服务发送并等待结果,之后保存令牌 → 出来服务器返回的 ServerResult。
调用关系:它和 send_custom_notification 类似,但这是有回复的请求。底层仍交给 run_service_operation 统一处理连接状态和错误。
调用图:调用 3 个内部函数(persist_oauth_tokens, refresh_oauth_if_needed, run_service_operation)。
RmcpClient::service691–698 ↗
async fn service(&self) -> Result<Arc<RunningService<RoleClient, ElicitationClientService>>>
作用:取出当前已经初始化好的运行服务。如果客户端还没初始化或已经关闭,它会明确报错。
数据流:进去是客户端自身 → 它锁住状态查看当前是 Ready、Connecting 还是 Closed → 出来可共享的服务引用,或“未初始化/已关闭”的错误。
调用关系:run_service_operation 每次发请求前都会调用它。它是防止在错误生命周期里乱发请求的关卡。
调用图:被 1 处调用(run_service_operation);外部调用 2 个(clone, anyhow!)。
RmcpClient::oauth_persistor700–709 ↗
async fn oauth_persistor(&self) -> Option<OAuthPersistor>
作用:取出当前 OAuth 令牌保存器。没有 OAuth 的连接会返回空。
数据流:进去是客户端自身 → 它查看 Ready 状态里是否带有 OAuthPersistor → 出来保存器副本,或者 None。
调用关系:persist_oauth_tokens 和 refresh_oauth_if_needed 都靠它拿到令牌运行时。它把 OAuth 相关逻辑和普通请求逻辑隔开。
调用图:被 2 处调用(persist_oauth_tokens, refresh_oauth_if_needed)。
RmcpClient::shutdown712–725 ↗
RmcpClient::persist_oauth_tokens729–735 ↗
async fn persist_oauth_tokens(&self)
作用:把刷新后的 OAuth 令牌保存起来。这样下次启动时不用重新登录,或者能继续用新令牌。
数据流:进去是客户端自身 → 它取 OAuth 保存器;如果存在,就要求它在有变化时写入存储 → 没有返回值,保存失败只打警告。
调用关系:list_tools、read_resource、call_tool、自定义请求等操作完成后都会调用它。它负责把请求过程中可能发生的令牌刷新落盘。
调用图:调用 1 个内部函数(oauth_persistor);被 8 处调用(call_tool, list_resource_templates, list_resources, list_tools, list_tools_with_connector_ids, read_resource, send_custom_notification, send_custom_request);外部调用 1 个(warn!)。
RmcpClient::refresh_oauth_if_needed737–743 ↗
async fn refresh_oauth_if_needed(&self)
作用:在发请求前检查 OAuth 令牌是否快过期,必要时先刷新。这样可以减少请求发到一半才因令牌失效失败的情况。
数据流:进去是客户端自身 → 它取 OAuth 保存器;如果存在,就尝试刷新令牌 → 没有返回值,刷新失败只记录警告,后续请求仍会按实际情况执行。
调用关系:所有主要请求入口都会先调用它,包括 list_tools、call_tool、读资源和自定义请求。
调用图:调用 1 个内部函数(oauth_persistor);被 8 处调用(call_tool, list_resource_templates, list_resources, list_tools, list_tools_with_connector_ids, read_resource, send_custom_notification, send_custom_request);外部调用 1 个(warn!)。
RmcpClient::create_pending_transport745–852 ↗
async fn create_pending_transport(
transport_recipe: &TransportRecipe,
) -> Result<PendingTransport>
作用:按连接配方创建“待握手的通信通道”。它把同进程、stdio、HTTP、带 OAuth 的 HTTP 这些差异藏在一个统一出口后面。
数据流:进去是一份 TransportRecipe → 它根据类型打开内部通道、启动子进程,或创建 HTTP 传输;HTTP 情况下还会合并默认头、读取已保存 OAuth 令牌,必要时构建 OAuth 传输 → 出来 PendingTransport。
调用关系:各个 new_* 构造函数和会话恢复流程都会调用它。它会调用 create_oauth_transport_and_runtime 来处理已有 OAuth 令牌的远程连接。
调用图:调用 3 个内部函数(new, create_oauth_transport_and_runtime, build_default_headers);外部调用 5 个(clone, with_client, with_uri, load_oauth_tokens, warn!)。
RmcpClient::connect_pending_transport854–912 ↗
async fn connect_pending_transport(
pending_transport: PendingTransport,
client_service: ElicitationClientService,
timeout: Option<Duration>,
) -> Result<(
Arc<Runn
作用:把待连接通道真正接上 rmcp 服务,并执行握手等待。它把“有一根线”变成“可以发 MCP 请求的服务”。
数据流:进去是 PendingTransport、客户端服务和可选超时 → 它选择对应传输交给 rmcp 的 serve_client;如果设置超时就限时等待握手;失败时尽量保存 OAuth 令牌 → 出来运行中的服务和可选 OAuth 保存器。
调用关系:初始化和恢复流程会通过带重试的连接流程使用它。它位于 create_pending_transport 之后、客户端进入 Ready 状态之前。
RmcpClient::run_service_operation914–950 ↗
async fn run_service_operation(
&self,
label: &str,
timeout: Option<Duration>,
operation: F,
) -> Result<T>
作用:所有常规 MCP 请求的统一执行入口。它负责拿服务、跑请求、识别会话过期,并在需要时重连后再试。
数据流:进去是操作名称、可选超时和具体请求函数 → 它先取当前服务,调用带短暂错误重试的执行器;如果发现 HTTP 会话过期 404,就重新初始化连接后再执行一次 → 出来请求结果或最终错误。
调用关系:list_tools、read_resource、call_tool、自定义请求等都调用它。它把恢复逻辑交给 reinitialize_after_session_expiry,把短暂错误重试交给 run_service_operation_with_transient_retries。
调用图:调用 2 个内部函数(reinitialize_after_session_expiry, service);被 8 处调用(call_tool, list_resource_templates, list_resources, list_tools, list_tools_with_connector_ids, read_resource, send_custom_notification, send_custom_request);外部调用 4 个(clone, is_session_expired_404, run_service_operation_with_transient_retries, clone)。
RmcpClient::run_service_operation_with_transient_retries952–1006 ↗
async fn run_service_operation_with_transient_retries(
service: Arc<RunningService<RoleClient, ElicitationClientService>>,
label: &str,
timeout: Option<Duration>,
pause
作用:对某些短暂失败做重试,尤其是 Streamable HTTP 的 tools/list 临时错误。它像“第一次门铃没响,等一下再按”。
数据流:进去是服务、操作名、总超时、暂停状态和具体操作 → 它按预设延迟循环尝试;每次先计算剩余超时,再执行一次;遇到可重试错误就睡一会儿再来 → 出来成功结果,或最后一次错误/超时。
调用关系:run_service_operation 调用它。它会调用 remaining_operation_timeout、run_service_operation_once、sleep_with_retry_deadline 和 is_retryable_tools_list_error。
调用图:调用 2 个内部函数(remaining_operation_timeout, sleep_with_retry_deadline);外部调用 8 个(clone, from_millis, is_retryable_tools_list_error, run_service_operation_once, clone, once, unreachable!, warn!)。
RmcpClient::run_service_operation_once1008–1031 ↗
async fn run_service_operation_once(
service: Arc<RunningService<RoleClient, ElicitationClientService>>,
label: &str,
timeout: Option<Duration>,
pause_state: Elicitatio
作用:执行一次具体的服务请求。它不管重试,只负责这一次要不要套超时。
数据流:进去是服务、操作名、可选超时、暂停状态和请求函数 → 如果有超时,就订阅暂停状态并用 active_time_timeout 包住请求;如果没有超时,就直接执行 → 出来请求结果或服务错误/超时错误。
调用关系:run_service_operation_with_transient_retries 每次尝试都会调用它。它是重试循环里的“单次执行单元”。
调用图:调用 2 个内部函数(subscribe, active_time_timeout)。
RmcpClient::is_retryable_tools_list_error1033–1047 ↗
fn is_retryable_tools_list_error(label: &str, error: &ClientOperationError) -> bool
作用:判断一个错误是不是 tools/list 可以重试的临时 HTTP 错误。它避免对不该重试的操作盲目重试。
数据流:进去是操作名和错误 → 它先确认操作名必须是 tools/list,再确认错误来自 Streamable HTTP 发送阶段,并交给 HTTP 错误判断逻辑 → 出来 true 或 false。
调用关系:run_service_operation_with_transient_retries 用它决定是否等待后重试。它只做判断,不修改状态。
RmcpClient::is_session_expired_4041049–1067 ↗
fn is_session_expired_404(error: &ClientOperationError) -> bool
作用:判断错误是不是远程 HTTP 会话过期导致的 404。这个 404 不是普通“找不到”,而是提示客户端该重新建会话了。
数据流:进去是一次操作错误 → 它检查错误是否来自传输发送,再向下查看是否是 SessionExpired404 → 出来 true 或 false。
调用关系:run_service_operation 用它识别恢复时机。为 true 时会触发 reinitialize_after_session_expiry。
RmcpClient::reinitialize_after_session_expiry1069–1128 ↗
async fn reinitialize_after_session_expiry(
&self,
failed_service: &Arc<RunningService<RoleClient, ElicitationClientService>>,
) -> Result<()>
作用:在 HTTP 会话过期后重新初始化客户端。它让用户不用手动重启就能继续请求。
数据流:进去是刚失败的服务引用 → 它先拿恢复锁,确认当前服务确实还是那个失败服务;然后取旧初始化上下文,重建传输并重新握手,最后替换 Ready 服务并保存 OAuth 令牌 → 出来成功或恢复失败错误。
调用关系:run_service_operation 在识别到会话过期 404 后调用它。恢复锁保证同一时间只有一个任务真正重连,其他任务不会一起乱重建。
调用图:被 1 处调用(run_service_operation);外部调用 6 个(ptr_eq, create_pending_transport, acquire, anyhow!, matches!, warn!)。
create_oauth_transport_and_runtime1131–1190 ↗
async fn create_oauth_transport_and_runtime(
server_name: &str,
url: &str,
initial_tokens: StoredOAuthTokens,
credentials_store: OAuthCredentialsStoreMode,
keyring_backend_kind: Au
作用:用已经保存的 OAuth 令牌创建带自动授权能力的 HTTP 传输,并准备令牌保存器。它让远程 MCP 客户端能继续使用上次登录状态。
数据流:进去是服务器名、URL、已有令牌、保存方式、默认 HTTP 头和 HTTP 客户端 → 它构建 OAuth 元数据客户端,加载凭据到 OAuth 状态,创建 AuthClient 和 Streamable HTTP 传输,再创建 OAuthPersistor → 出来带授权的传输和令牌保存器。
调用关系:create_pending_transport 在发现本地有可用 OAuth 令牌时调用它。如果服务器不支持 OAuth 元数据,外层会选择退回到 bearer token 方式。
调用图:调用 3 个内部函数(new, new, apply_default_headers);被 1 处调用(create_pending_transport);外部调用 7 个(new, new, with_client, with_uri, anyhow!, builder, maybe_build_rustls_client_config_with_custom_ca)。
tests::active_time_timeout_pauses_while_elicitation_is_pending1202–1218 ↗
async fn active_time_timeout_pauses_while_elicitation_is_pending()
作用:测试“等待用户回答时不扣超时时间”这个规则。它防止以后改代码时把这个细节弄坏。
数据流:进去没有外部输入 → 它创建暂停状态,先进入暂停,延迟一段时间后解除;同时运行一个总耗时超过普通超时的任务 → 出来断言结果仍然成功为 done。
调用关系:它调用 ElicitationPauseState::new 和 active_time_timeout。这个测试证明 elicitation 暂停机制和活动时间超时能正确配合。
调用图:调用 2 个内部函数(new, active_time_timeout);外部调用 4 个(from_millis, assert_eq!, sleep, spawn)。
chatgpt/src/workspace_settings.rs源码 ↗
这个文件解决的是“插件开关到底开没开”的问题。普通个人账号、没有登录、或者不是工作区账号时,它默认允许使用;只有 ChatGPT 的工作区账号,才需要去后台读取工作区设置。它会拿账号 ID 和 ChatGPT 服务地址组成一个缓存钥匙,先看看本地有没有 15 分钟内查过的结果。缓存可以理解成便利贴:刚问过的答案先贴在桌上,短时间内不用再打电话确认。如果没有可用缓存,它会把账号 ID 安全地放进网址路径里,再用 10 秒超时去请求 /accounts/.../settings。服务器返回的 beta_settings 里如果有 enable_plugins,就按这个值决定;如果没写,默认当作允许。这里还用了读写锁(一把允许多人读、但写入时独占的锁),保证多个任务同时查询时不会把缓存改乱。
WorkspaceSettingsCache::get_codex_plugins_enabled41–68 ↗
fn get_codex_plugins_enabled(&self, key: &WorkspaceSettingsCacheKey) -> Option<bool>
作用:这个函数从本地缓存里取“Codex 插件是否开启”的答案。它的用处是:如果刚刚已经查过同一个工作区,就直接复用结果,不必再发网络请求。
数据流:进去的是一个缓存钥匙,里面包含 ChatGPT 服务地址和账号 ID。函数先用读锁查看当前缓存:如果缓存存在、没过期、而且钥匙完全一样,就返回里面保存的开关值。否则它再用写锁清掉已经过期或不属于这个账号的旧缓存,最后返回空,表示需要重新去服务器查。
调用关系:它在 codex_plugins_enabled_for_workspace 准备发请求前被调用,相当于先问本地“有没有现成答案”。它自己不请求网络,只读取当前时间来判断缓存是否还有效。
调用图:外部调用 1 个(now)。
WorkspaceSettingsCache::set_codex_plugins_enabled70–80 ↗
fn set_codex_plugins_enabled(&self, key: WorkspaceSettingsCacheKey, enabled: bool)
作用:这个函数把刚从服务器查到的插件开关结果放进缓存。这样接下来 15 分钟内,同一个账号和同一个服务地址就可以直接用这个答案。
数据流:进去的是缓存钥匙和一个布尔值,也就是开或关。函数拿到写锁后,把缓存内容替换成新的记录,并把过期时间设为现在往后 15 分钟。出来没有额外返回值,但缓存已经被更新。
调用关系:它在 codex_plugins_enabled_for_workspace 成功拿到服务器设置后被调用。它接过网络查询的结果,把它保存起来,供下一次 WorkspaceSettingsCache::get_codex_plugins_enabled 直接读取。
调用图:外部调用 1 个(now)。
codex_plugins_enabled_for_workspace83–132 ↗
async fn codex_plugins_enabled_for_workspace(
config: &Config,
auth: Option<&CodexAuth>,
cache: Option<&WorkspaceSettingsCache>,
) -> anyhow::Result<bool>
作用:这是本文件最核心的判断函数:给定配置、登录信息和可选缓存,它告诉调用方当前工作区是否允许使用 Codex 插件。它把“不需要查服务器的情况”和“必须查工作区设置的情况”都处理掉。
数据流:进去的是程序配置、可选的认证信息,以及可选的缓存。它先排除几类默认允许的情况:没登录、不是 ChatGPT 登录、不是工作区账号、账号 ID 为空。然后用服务地址和账号 ID 组成缓存钥匙,能从缓存拿到就直接返回。拿不到时,它先调用 encode_path_segment 把账号 ID 转成适合放进网址的安全文本,再调用 chatgpt_get_request_with_timeout 请求工作区设置。返回数据里如果 enable_plugins 存在,就用它的值;如果不存在,就默认 true。最后把结果写入缓存,并把最终的开关值返回给调用方。
调用关系:它被 workspace_codex_plugins_enabled 调用,用在需要知道工作区插件权限的时候。它自己负责串起整个流程:先查身份条件,再查缓存,再整理网址路径,最后把网络请求交给 chatgpt_get_request_with_timeout。
调用图:调用 2 个内部函数(chatgpt_get_request_with_timeout, encode_path_segment);被 3 处调用(workspace_codex_plugins_enabled, workspace_codex_plugins_enabled, workspace_codex_plugins_enabled);外部调用 1 个(format!)。
encode_path_segment134–144 ↗
fn encode_path_segment(value: &str) -> String
作用:这个小工具函数把一段文字变成可以安全放进网址路径里的形式。它防止账号 ID 里如果有空格、斜杠或特殊符号时,把网址结构弄乱。
数据流:进去的是原始字符串。函数逐个查看里面的字节:英文字母、数字和少数安全符号会原样保留;其他字节会改写成 %XX 这种网址编码形式。出来的是编码后的字符串,可以放心拼到请求路径中。
调用关系:它只被 codex_plugins_enabled_for_workspace 使用,专门在发起工作区设置请求前处理账号 ID。它不接触网络,也不读配置,只做安全的文本转换。
调用图:被 1 处调用(codex_plugins_enabled_for_workspace);外部调用 3 个(new, format!, matches!)。
chatgpt/src/connectors.rs源码 ↗
连接器可以理解成让 ChatGPT 接入外部服务的小插件,比如某个应用或工具。这个文件解决的问题是:用户到底能看到哪些连接器、哪些已经可用、哪些因为账号或来源限制不能显示。它先检查配置和登录状态,确认当前账号是否允许使用应用功能;然后按需要从缓存或远端目录拿连接器列表;同时也会读取本机 MCP 工具能访问的连接器。MCP 可以简单理解成一套让程序发现外部工具的接口。拿到几份名单后,它会把重复项合并,把插件声明的连接器补进去,再过滤掉当前来源不允许使用的连接器。一个重要细节是:如果完整目录已经加载完,它会丢掉“不在完整目录里但声称可访问”的项,避免显示不存在或过期的应用;如果目录还没加载完,则会先保留这些可访问项,让界面在加载中也能显示有用信息。
apps_enabled29–36 ↗
async fn apps_enabled(config: &Config) -> bool
作用:检查当前配置和登录账号是否允许使用连接器应用。有人要列应用之前,会先用它挡一下,避免没权限时还去读缓存或请求网络。
数据流:输入是一份 Config 配置;它从配置里拿到共享的 AuthManager,也就是登录信息管理器,再读取当前认证信息;然后看这个账号是不是走 Codex 后端,以及配置里的应用功能是否对这个账号开启;最后输出 true 或 false。
调用关系:list_connectors、list_all_connectors_with_options 和 list_cached_all_connectors 都会先问它一句“应用功能能不能用”。它自己把读取登录信息的活交给 shared_from_config。
调用图:调用 1 个内部函数(shared_from_config);被 3 处调用(list_all_connectors_with_options, list_cached_all_connectors, list_connectors)。
connector_auth38–50 ↗
async fn connector_auth(config: &Config) -> anyhow::Result<CodexAuth>
作用:拿到可用于连接器目录的 ChatGPT/Codex 登录凭证,并确认它真的适合访问 Codex 后端。没有它,后面请求连接器目录时就不知道该代表哪个用户。
数据流:输入是 Config;它创建或取得 AuthManager,读取当前认证信息;如果没有登录,就返回错误;如果登录方式不是 Codex 后端,也返回错误;成功时输出 CodexAuth,也就是后续查目录和建缓存键要用的账号身份。
调用关系:list_all_connectors_with_options 和 list_cached_all_connectors 在需要真实账号身份时调用它。它依赖 shared_from_config 拿登录管理器,并用 ensure! 做条件检查。
调用图:调用 1 个内部函数(shared_from_config);被 2 处调用(list_all_connectors_with_options, list_cached_all_connectors);外部调用 1 个(ensure!)。
list_connectors52–68 ↗
async fn list_connectors(config: &Config) -> anyhow::Result<Vec<AppInfo>>
作用:给调用者返回最终可展示的连接器列表,包括目录里的应用,以及当前本机工具实际可访问的状态。它是“给界面看连接器总览”的主要入口之一。
数据流:输入是 Config;它先检查应用功能是否开启;没开就直接输出空列表。开启后,它同时做两件事:拉取完整连接器目录,并列出 MCP 工具可访问的连接器;两边都成功后,它把两份名单合并,再根据配置补上是否启用的状态;最后输出 Vec<AppInfo>,也就是应用信息列表。
调用关系:它调用 apps_enabled 做总开关检查;用并发 join! 同时跑 list_all_connectors 和 list_accessible_connectors_from_mcp_tools;随后把合并工作交给 merge_connectors_with_accessible,再交给 with_app_enabled_state 补启用状态。
调用图:调用 3 个内部函数(apps_enabled, merge_connectors_with_accessible, with_app_enabled_state);外部调用 2 个(new, join!)。
list_all_connectors70–72 ↗
async fn list_all_connectors(config: &Config) -> anyhow::Result<Vec<AppInfo>>
作用:用默认选项列出完整连接器目录。它是一个简单包装,让调用者不用关心“是否强制刷新”和“插件应用列表”这些额外参数。
数据流:输入是 Config;它把 force_refetch 设为 false,把 plugin_apps 设为空数组,然后调用更完整的 list_all_connectors_with_options;最后把那边的结果原样返回。
调用关系:list_connectors 会调用它来拿完整目录。真正的认证、缓存、远端请求和过滤都由 list_all_connectors_with_options 完成。
调用图:调用 1 个内部函数(list_all_connectors_with_options)。
list_cached_all_connectors74–86 ↗
async fn list_cached_all_connectors(
config: &Config,
plugin_apps: &[AppConnectorId],
) -> Option<Vec<AppInfo>>
作用:只从本地缓存里拿连接器目录,不主动访问网络。它适合在需要快速显示或不能等待远端请求时使用。
数据流:输入是 Config 和插件声明的 AppConnectorId 列表;它先检查应用功能是否开启,没开就返回 Some(空列表)。如果开启,它尝试拿登录凭证;拿不到就返回 None。拿到后,它构造缓存上下文,从本地缓存读取目录;如果缓存存在,就把插件连接器合进去并过滤不允许的项,最后返回 Some(列表);缓存没有则返回 None。
调用关系:plugin_apps_needing_auth_for_install、remote_plugin_install_response 和 load_plugin_app_summaries 会用它先尝试读缓存。它调用 apps_enabled、connector_auth、connector_directory_cache_context,并把最后整理名单的工作交给 merge_and_filter_plugin_connectors。
调用图:调用 4 个内部函数(apps_enabled, connector_auth, connector_directory_cache_context, merge_and_filter_plugin_connectors);被 3 处调用(plugin_apps_needing_auth_for_install, remote_plugin_install_response, load_plugin_app_summaries);外部调用 2 个(new, cached_directory_connectors)。
list_all_connectors_with_options88–113 ↗
async fn list_all_connectors_with_options(
config: &Config,
force_refetch: bool,
plugin_apps: &[AppConnectorId],
) -> anyhow::Result<Vec<AppInfo>>
作用:按指定选项获取完整连接器目录,可以选择是否强制重新从远端拉取,也可以把插件声明的连接器一起算进去。这是实际“拿目录”的核心函数。
数据流:输入是 Config、force_refetch 标志和 plugin_apps;它先确认应用功能开启,再拿认证信息;然后根据账号和服务器地址创建缓存上下文;接着调用 codex_connectors 的目录加载函数:它会根据缓存和 force_refetch 决定是否请求远端。真正请求远端时,这里提供一个带 60 秒超时的 ChatGPT GET 请求函数。拿到目录后,它合并插件连接器并过滤不允许的项,最后输出 AppInfo 列表。
调用关系:apps_list_response、load_plugin_app_summaries 和 list_all_connectors 会调用它。它把认证交给 connector_auth,把缓存键生成交给 connector_directory_cache_context,把远端 HTTP 请求交给 chatgpt_get_request_with_timeout,把收尾整理交给 merge_and_filter_plugin_connectors。
调用图:调用 4 个内部函数(apps_enabled, connector_auth, connector_directory_cache_context, merge_and_filter_plugin_connectors);被 3 处调用(apps_list_response, load_plugin_app_summaries, list_all_connectors);外部调用 2 个(new, list_all_connectors_with_options)。
connector_directory_cache_context115–128 ↗
fn connector_directory_cache_context(
config: &Config,
auth: &CodexAuth,
) -> ConnectorDirectoryCacheContext
作用:生成连接器目录缓存所需的“地址牌”。这样同一台机器上不同服务器、不同用户、个人账号和工作区账号不会混用缓存。
数据流:输入是 Config 和 CodexAuth;它读取本地 codex_home 目录、ChatGPT 基础地址、账号 ID、ChatGPT 用户 ID、是否工作区账号;把这些拼成 ConnectorDirectoryCacheContext;输出这个上下文,供读写缓存使用。
调用关系:list_all_connectors_with_options 和 list_cached_all_connectors 在访问目录缓存前都会调用它。它从认证对象里取 get_account_id、get_chatgpt_user_id 和 is_workspace_account。
调用图:调用 5 个内部函数(new, new, get_account_id, get_chatgpt_user_id, is_workspace_account);被 2 处调用(list_all_connectors_with_options, list_cached_all_connectors)。
merge_and_filter_plugin_connectors130–141 ↗
fn merge_and_filter_plugin_connectors(
connectors: Vec<AppInfo>,
plugin_apps: &[AppConnectorId],
) -> Vec<AppInfo>
作用:把远端目录里的连接器和插件声明的连接器合在一起,然后剔除当前来源不允许显示的连接器。它负责把“可选补充项”和“安全过滤”一次做完。
数据流:输入是已有 connectors 和 plugin_apps;它先把插件 ID 转成普通字符串,交给 merge_plugin_connectors 合入列表;再用当前 originator,也就是请求来源标识,调用 filter_disallowed_connectors 过滤;最后输出干净的连接器列表。
调用关系:list_all_connectors_with_options 和 list_cached_all_connectors 拿到目录后会调用它。合并规则在 merge_plugin_connectors 里,过滤规则在 filter_disallowed_connectors 里。
调用图:调用 3 个内部函数(filter_disallowed_connectors, merge_plugin_connectors, originator);被 2 处调用(list_all_connectors_with_options, list_cached_all_connectors);外部调用 1 个(iter)。
connectors_for_plugin_apps143–163 ↗
fn connectors_for_plugin_apps(
connectors: Vec<AppInfo>,
plugin_apps: &[AppConnectorId],
) -> Vec<AppInfo>
作用:从一堆连接器里挑出插件明确请求的那些,并且会为插件声明但目录里没有的连接器补出条目。它常用于安装插件或展示插件摘要时,只关心这个插件需要的应用。
数据流:输入是 connectors 和 plugin_apps;它先把插件连接器合入列表,再过滤掉不允许的项;然后按连接器 ID 建一个临时表;最后按 plugin_apps 的顺序逐个取出匹配项,取过就移除,所以重复请求不会重复输出同一个目录项;输出只包含插件请求且允许显示的连接器。
调用关系:plugin_apps_needing_auth_for_install、remote_plugin_install_response 和 load_plugin_app_summaries 会调用它,测试也覆盖它。它依赖 merge_plugin_connectors 补插件项,依赖 filter_disallowed_connectors 做来源过滤。
调用图:调用 3 个内部函数(filter_disallowed_connectors, merge_plugin_connectors, originator);被 5 处调用(plugin_apps_needing_auth_for_install, remote_plugin_install_response, load_plugin_app_summaries, connectors_for_plugin_apps_filters_disallowed_plugin_apps, connectors_for_plugin_apps_returns_only_requested_plugin_apps);外部调用 1 个(iter)。
merge_connectors_with_accessible165–184 ↗
fn merge_connectors_with_accessible(
connectors: Vec<AppInfo>,
accessible_connectors: Vec<AppInfo>,
all_connectors_loaded: bool,
) -> Vec<AppInfo>
作用:把“完整目录列表”和“实际可访问列表”合成一份列表,并给应用标上是否可访问。它避免界面把不存在、被禁用或来源不允许的连接器展示出来。
数据流:输入是 connectors、accessible_connectors,以及 all_connectors_loaded;如果完整目录已经加载完,它先建立完整目录 ID 集合,然后丢掉不在目录里的可访问项;如果目录还在加载,就暂时保留可访问项。接着调用 merge_connectors 合并两份信息,最后按当前来源过滤不允许的项,输出最终列表。
调用关系:list_connectors 用它合并远端目录和 MCP 可访问结果;merge_loaded_apps 也会用它合并已加载应用。测试函数验证了目录已加载和未加载两种情况下的差异。
调用图:调用 3 个内部函数(filter_disallowed_connectors, merge_connectors, originator);被 4 处调用(merge_loaded_apps, list_connectors, excludes_accessible_connectors_not_in_all_when_all_loaded, keeps_accessible_connectors_not_in_all_while_all_loading)。
tests::app193–209 ↗
fn app(id: &str) -> AppInfo
作用:在测试里快速造一个最简单的 AppInfo。这样每个测试不用重复写一大串字段。
数据流:输入是一个 id 字符串;它用这个 id 同时填应用 ID 和名称,其他说明、图标、元数据等字段大多留空;默认设置为不可访问但已启用;输出一个 AppInfo 测试对象。
调用关系:多个测试用它准备输入数据。它只服务测试,不参与真实运行流程。
调用图:外部调用 1 个(new)。
tests::merged_app211–227 ↗
fn merged_app(id: &str, is_accessible: bool) -> AppInfo
作用:在测试里快速造一个“合并后应该长这样”的 AppInfo。它用来和实际合并结果做对比。
数据流:输入是 id 和 is_accessible;它填好基础字段,并用 connector_install_url 生成安装链接;把可访问状态设置成传入值;输出预期的 AppInfo。
调用关系:合并相关测试用它构造期望结果。它调用 connector_install_url,是为了匹配真实合并函数会补上的安装地址。
调用图:调用 1 个内部函数(connector_install_url);外部调用 1 个(new)。
tests::excludes_accessible_connectors_not_in_all_when_all_loaded230–237 ↗
fn excludes_accessible_connectors_not_in_all_when_all_loaded()
作用:验证当完整目录已经加载完成时,不在完整目录里的“可访问连接器”会被排除。这个测试防止界面显示目录里不存在的应用。
数据流:输入是测试里手工造的目录:完整目录只有 alpha,可访问列表有 alpha 和 beta;它调用 merge_connectors_with_accessible,并把 all_connectors_loaded 设为 true;最后断言输出只剩 alpha,且 alpha 被标成可访问。
调用关系:它专门测试 merge_connectors_with_accessible 的“完整目录已加载”分支。测试失败就说明合并规则可能会放进过期或不该显示的连接器。
调用图:调用 1 个内部函数(merge_connectors_with_accessible);外部调用 2 个(assert_eq!, vec!)。
tests::keeps_accessible_connectors_not_in_all_while_all_loading240–253 ↗
fn keeps_accessible_connectors_not_in_all_while_all_loading()
作用:验证当完整目录还没加载完时,不在目录里的可访问连接器会先保留下来。这样加载过程中不会把用户已经能用的应用突然藏起来。
数据流:输入是完整目录暂时只有 alpha,可访问列表有 alpha 和 beta;它调用 merge_connectors_with_accessible,并把 all_connectors_loaded 设为 false;最后断言输出包含 alpha 和 beta,二者都标成可访问。
调用关系:它测试 merge_connectors_with_accessible 的“目录仍在加载”分支,和另一个测试形成对照,确保两个时机的行为不同。
调用图:调用 1 个内部函数(merge_connectors_with_accessible);外部调用 2 个(assert_eq!, vec!)。
tests::connectors_for_plugin_apps_returns_only_requested_plugin_apps256–269 ↗
fn connectors_for_plugin_apps_returns_only_requested_plugin_apps()
作用:验证 connectors_for_plugin_apps 只返回插件请求的连接器,并保持插件请求顺序,同时不会把没请求的目录项混进去。
数据流:输入是目录里的 alpha、beta,以及插件请求 gmail、alpha、gmail;它调用 connectors_for_plugin_apps;结果应该先得到补出来的 gmail,再得到已有的 alpha,beta 不出现,重复的 gmail 也不会出现第二次。
调用关系:它覆盖 connectors_for_plugin_apps 的核心筛选规则:插件声明可以补项,已有目录项可以被选中,未请求项要排除,重复请求不会重复输出。
调用图:调用 1 个内部函数(connectors_for_plugin_apps);外部调用 3 个(assert_eq!, new, vec!)。
tests::connectors_for_plugin_apps_filters_disallowed_plugin_apps272–280 ↗
fn connectors_for_plugin_apps_filters_disallowed_plugin_apps()
作用:验证插件请求了被禁止的连接器时,结果会被过滤为空。这个测试保证来源限制不会因为插件补项而被绕过。
数据流:输入是空的目录列表,以及一个特定的被禁止插件连接器 ID;它调用 connectors_for_plugin_apps;过滤后输出空列表,并用断言确认这一点。
调用关系:它测试 connectors_for_plugin_apps 与 filter_disallowed_connectors 的配合,确保插件安装或摘要流程也遵守同一套禁止规则。
调用图:调用 1 个内部函数(connectors_for_plugin_apps);外部调用 3 个(new, assert_eq!, new)。
启动内容刷新
这些文件同步精选插件内容,并公开一个专用后端任务获取功能,用于启动邻近的远程准备流程。
core-plugins/src/startup_sync.rs源码 ↗
这个文件像一个“启动时自动补货员”。程序需要一份官方插件仓库放在本地目录里,后面插件市场、安装插件等功能都会读它。它优先用 git 拉取 GitHub 上的 openai/plugins;如果 git 不行,就改用 GitHub 的 HTTP 接口下载压缩包;如果本地还完全没有快照,再尝试一个备用导出压缩包。同步前会加文件锁(一把锁,防止两个程序同时改同一个目录),下载时先放到临时目录,确认里面有插件清单后,再一次性替换正式目录。这样就算中途失败,也不容易留下坏目录。它还记录 sha(可以理解成版本号)和指标,方便知道同步用了哪条路、成功还是失败。
curated_plugins_repo_path58–60 ↗
fn curated_plugins_repo_path(codex_home: &Path) -> PathBuf
作用:算出官方插件仓库在本机应该放在哪里。别人只要给它 Codex 的家目录,它就返回固定的插件目录路径。
数据流:输入是 codex_home 这个根目录 → 它在后面拼上 .tmp/plugins → 输出完整的本地官方插件仓库路径,不改动磁盘。
调用关系:这是很多流程的共同路标;同步流程、插件市场读取流程和测试都会用它,避免各处自己猜目录。
调用图:被 34 处调用(does_not_expand_local_plugins_by_installed_apps, does_not_expand_local_sales_apps, does_not_read_local_plugins_for_loaded_apps, does_not_reload_marketplace_per_plugin, ignores_missing_marketplace_plugin, includes_openai_curated_when_remote_enabled, normalizes_description, omits_installed_curated_plugins, omits_not_available_curated_plugins, returns_api_curated_fallback_plugins_for_direct_provider_auth (+15 more));外部调用 1 个(join)。
curated_plugins_api_marketplace_path62–64 ↗
fn curated_plugins_api_marketplace_path(codex_home: &Path) -> PathBuf
作用:给出官方插件仓库里 API 插件市场清单的位置。这个路径用于让插件市场知道该去哪里读清单文件。
数据流:输入是 codex_home → 先调用 curated_plugins_repo_path 找到仓库目录 → 再拼上 .agents/plugins/api_marketplace.json,输出这个 JSON 文件路径。
调用关系:它被 marketplace_roots 使用,相当于告诉市场加载器:官方 API 插件清单在这里。
调用图:调用 1 个内部函数(curated_plugins_repo_path);被 1 处调用(marketplace_roots)。
read_curated_plugins_sha66–68 ↗
fn read_curated_plugins_sha(codex_home: &Path) -> Option<String>
作用:读取本地官方插件快照对应的版本号。安装插件时可以用它判断当前用的是哪一版官方插件数据。
数据流:输入是 codex_home → 它先算出 sha 文件位置,再读取并去掉空白 → 输出 Some(版本号) 或 None,不修改文件。
调用关系:install_resolved_plugin 会用它读取当前插件快照版本;它把具体读文件的活交给 read_sha_file。
调用图:调用 2 个内部函数(curated_plugins_sha_path, read_sha_file);被 1 处调用(install_resolved_plugin)。
curated_plugins_sha_path70–72 ↗
fn curated_plugins_sha_path(codex_home: &Path) -> PathBuf
作用:算出保存官方插件版本号的文件路径。这个文件通常放在 .tmp/plugins.sha。
数据流:输入是 codex_home → 拼接固定文件名 .tmp/plugins.sha → 输出路径,不碰磁盘。
调用关系:read_curated_plugins_sha 和备用压缩包同步流程会用它,保证读写的是同一个版本记录文件。
调用图:被 2 处调用(read_curated_plugins_sha, sync_openai_plugins_repo_via_backup_archive);外部调用 1 个(join)。
sync_openai_plugins_repo74–81 ↗
fn sync_openai_plugins_repo(codex_home: &Path) -> Result<String, String>
作用:这是外部最常调用的同步入口。调用它就会尝试把官方插件仓库更新到本机。
数据流:输入是 codex_home → 它使用默认 git 命令、默认 GitHub 地址和默认备用地址 → 输出同步后的版本号,失败时输出错误文字。
调用关系:它只是公开入口,真正的分支判断和失败兜底都交给 sync_openai_plugins_repo_with_transport_overrides。
调用图:调用 1 个内部函数(sync_openai_plugins_repo_with_transport_overrides)。
sync_openai_plugins_repo_with_transport_overrides83–146 ↗
fn sync_openai_plugins_repo_with_transport_overrides(
codex_home: &Path,
git_binary: &str,
api_base_url: &str,
backup_archive_api_url: &str,
) -> Result<String, String>
作用:按顺序尝试多种同步办法:先 git,再 GitHub HTTP,必要时再用备用导出压缩包。它负责决定失败后该不该换路线。
数据流:输入是本地目录、git 命令名、GitHub API 地址和备用压缩包地址 → 先加锁,再逐个尝试同步方式,并记录成功失败指标 → 输出最终拿到的版本号,或把各阶段错误合成一段说明。
调用关系:sync_openai_plugins_repo 调它;它调 lock_curated_plugins_startup_sync 防并发,调三个具体同步函数干活,并用指标函数把结果报出去。
调用图:调用 7 个内部函数(emit_curated_plugins_startup_sync_final_metric, emit_curated_plugins_startup_sync_metric, has_local_curated_plugins_snapshot, lock_curated_plugins_startup_sync, sync_openai_plugins_repo_via_backup_archive, sync_openai_plugins_repo_via_git, sync_openai_plugins_repo_via_http);被 1 处调用(sync_openai_plugins_repo);外部调用 2 个(format!, warn!)。
lock_curated_plugins_startup_sync148–162 ↗
fn lock_curated_plugins_startup_sync(codex_home: &Path) -> Result<File, String>
作用:给启动同步加一把文件锁,防止多个 Codex 进程同时更新同一份插件目录。没有它,两个进程可能互相覆盖文件。
数据流:输入是 codex_home → 创建 .tmp 目录,打开 plugins.sync.lock 文件并锁住 → 输出打开着的 File;这个 File 活着时锁就还在。
调用关系:sync_openai_plugins_repo_with_transport_overrides 一开始就调用它;后面的 git、HTTP、备用同步都在这把锁保护下运行。
调用图:被 1 处调用(sync_openai_plugins_repo_with_transport_overrides);外部调用 3 个(options, join, create_dir_all)。
sync_openai_plugins_repo_via_git164–206 ↗
fn sync_openai_plugins_repo_via_git(codex_home: &Path, git_binary: &str) -> Result<String, String>
作用:用 git 方式同步官方插件仓库,这是首选方式。git 能精确拿到远端版本,也适合复用已有仓库。
数据流:输入是 codex_home 和 git 命令 → 查询远端 HEAD 版本,比较本地版本;需要更新时创建临时仓库、拉取指定提交、重置工作区、检查清单、替换正式目录、写入 sha → 输出远端版本号。
调用关系:它由总调度函数优先调用;内部会调用 git_ls_remote_head_sha、fetch_curated_plugins_commit、reset_curated_plugins_checkout、activate_curated_repo 等函数完成整套 git 更新。
调用图:调用 12 个内部函数(activate_curated_repo, curated_plugins_repo_path, ensure_marketplace_manifest_exists, fetch_curated_plugins_commit, fetch_curated_plugins_commit_from_source, git_head_sha, git_ls_remote_head_sha, prepare_curated_repo_parent_and_temp_dir, read_local_git_or_sha_file, reset_curated_plugins_checkout (+2 more));被 1 处调用(sync_openai_plugins_repo_with_transport_overrides);外部调用 2 个(join, format!)。
fetch_curated_plugins_commit208–220 ↗
fn fetch_curated_plugins_commit(
repo_path: &Path,
remote_sha: &str,
git_binary: &str,
) -> Result<(), String>
作用:从官方 GitHub 仓库拉取指定的插件提交。它是 git 同步里真正“去远处拿货”的一步。
数据流:输入是仓库目录、目标 sha 和 git 命令 → 使用固定的 openai/plugins Git 地址去 fetch 那个提交 → 成功返回空结果,失败返回错误。
调用关系:sync_openai_plugins_repo_via_git 会调用它;它把通用 fetch 细节交给 fetch_curated_plugins_commit_from。
调用图:调用 1 个内部函数(fetch_curated_plugins_commit_from);被 1 处调用(sync_openai_plugins_repo_via_git)。
fetch_curated_plugins_commit_from_source222–235 ↗
fn fetch_curated_plugins_commit_from_source(
repo_path: &Path,
source_repo_path: &Path,
remote_sha: &str,
git_binary: &str,
) -> Result<(), String>
作用:从已有的本地仓库复制一个已经抓到的提交到临时仓库。这样可以少走一次网络。
数据流:输入是目标临时仓库、来源仓库、版本引用和 git 命令 → 从来源仓库 fetch 到目标仓库的固定引用 → 成功返回空结果。
调用关系:当本地旧仓库已经存在时,sync_openai_plugins_repo_via_git 会先让旧仓库抓远端,再用它把提交复制到新临时仓库。
调用图:调用 1 个内部函数(fetch_curated_plugins_commit_from);被 1 处调用(sync_openai_plugins_repo_via_git)。
fetch_curated_plugins_commit_from237–257 ↗
fn fetch_curated_plugins_commit_from(
repo_path: &Path,
source: &Path,
source_revision: &str,
git_binary: &str,
context: &str,
) -> Result<(), String>
作用:执行通用的 git fetch 命令,把某个来源的某个版本抓到固定引用名下。它把“从哪里抓”和“抓哪个版本”参数化了。
数据流:输入是仓库路径、来源路径或 URL、来源版本、git 命令和上下文说明 → 拼出 fetch refspec,带超时运行 git fetch → 检查 git 是否成功,输出成功或错误。
调用关系:fetch_curated_plugins_commit 和 fetch_curated_plugins_commit_from_source 都调用它;它再依赖 run_git_command_with_timeout 和 ensure_git_success。
调用图:调用 2 个内部函数(ensure_git_success, run_git_command_with_timeout);被 2 处调用(fetch_curated_plugins_commit, fetch_curated_plugins_commit_from_source);外部调用 2 个(new, format!)。
reset_curated_plugins_checkout259–272 ↗
fn reset_curated_plugins_checkout(repo_path: &Path, git_binary: &str) -> Result<(), String>
作用:把临时仓库的文件内容切到刚抓到的插件版本,并清掉多余文件。可以理解成把货架整理到目标版本的干净状态。
数据流:输入是仓库目录和 git 命令 → 运行 git reset --hard 到固定引用,再运行 git clean -fdx 清理未跟踪文件 → 成功返回空结果。
调用关系:git 同步抓完提交后调用它;它通过 run_git_in_repo 执行两条 git 命令。
调用图:调用 1 个内部函数(run_git_in_repo);被 1 处调用(sync_openai_plugins_repo_via_git)。
run_git_in_repo274–290 ↗
fn run_git_in_repo(
repo_path: &Path,
git_binary: &str,
args: &[&str],
context: &str,
) -> Result<(), String>
作用:在某个仓库目录里运行一条 git 命令,并检查它有没有成功。它是 git 操作的统一小工具。
数据流:输入是仓库路径、git 命令、参数和说明文字 → 带超时启动 git,并收集输出 → 如果退出码正常就返回成功,否则返回包含错误输出的说明。
调用关系:sync_openai_plugins_repo_via_git 和 reset_curated_plugins_checkout 会用它;它底层调用 run_git_command_with_timeout 和 ensure_git_success。
调用图:调用 2 个内部函数(ensure_git_success, run_git_command_with_timeout);被 2 处调用(reset_curated_plugins_checkout, sync_openai_plugins_repo_via_git);外部调用 1 个(new)。
sync_openai_plugins_repo_via_http292–316 ↗
fn sync_openai_plugins_repo_via_http(
codex_home: &Path,
api_base_url: &str,
) -> Result<String, String>
作用:当 git 不可用或失败时,用 GitHub HTTP 接口下载插件仓库压缩包。它是第一条备用路线。
数据流:输入是 codex_home 和 GitHub API 地址 → 建一个小型异步运行环境,查远端 sha,若本地已是最新版就跳过;否则下载 zip、解压到临时目录、检查清单、替换正式目录、写 sha → 输出版本号。
调用关系:总调度在 git 失败后调用它;它依赖 fetch_curated_repo_remote_sha、fetch_curated_repo_zipball、extract_zipball_to_dir 和 activate_curated_repo。
调用图:调用 9 个内部函数(activate_curated_repo, curated_plugins_repo_path, ensure_marketplace_manifest_exists, extract_zipball_to_dir, fetch_curated_repo_remote_sha, fetch_curated_repo_zipball, prepare_curated_repo_parent_and_temp_dir, read_sha_file, write_curated_plugins_sha);被 1 处调用(sync_openai_plugins_repo_with_transport_overrides);外部调用 2 个(join, new_current_thread)。
sync_openai_plugins_repo_via_backup_archive318–339 ↗
fn sync_openai_plugins_repo_via_backup_archive(
codex_home: &Path,
backup_archive_api_url: &str,
) -> Result<String, String>
作用:在 GitHub HTTP 也失败、且本地还没有插件快照时,用备用导出压缩包做一次“救急安装”。它只用于从无到有,不用于刷新已有快照。
数据流:输入是 codex_home 和备用接口地址 → 下载备用压缩包,解压到临时目录,检查插件清单,尽量从压缩包里的 .git 读版本号,读不到就用 export-backup,最后替换正式目录并写 sha → 输出版本标识。
调用关系:总调度只在没有本地快照时调用它;它会调用 fetch_curated_repo_backup_archive_zip、read_extracted_backup_archive_git_sha、activate_curated_repo 等函数。
调用图:调用 9 个内部函数(activate_curated_repo, curated_plugins_repo_path, curated_plugins_sha_path, ensure_marketplace_manifest_exists, extract_zipball_to_dir, fetch_curated_repo_backup_archive_zip, prepare_curated_repo_parent_and_temp_dir, read_extracted_backup_archive_git_sha, write_curated_plugins_sha);被 1 处调用(sync_openai_plugins_repo_with_transport_overrides);外部调用 1 个(new_current_thread)。
has_local_curated_plugins_snapshot341–346 ↗
fn has_local_curated_plugins_snapshot(codex_home: &Path) -> bool
作用:判断本机是否已经有一份可用的官方插件快照。它用来决定是否允许走备用导出压缩包。
数据流:输入是 codex_home → 检查插件市场清单文件和 sha 文件是否都存在 → 输出 true 或 false,不改动任何东西。
调用关系:sync_openai_plugins_repo_with_transport_overrides 在 GitHub HTTP 失败后调用它;如果已经有本地快照,就宁愿报错也不拿可能滞后的备用包覆盖。
调用图:调用 1 个内部函数(curated_plugins_repo_path);被 1 处调用(sync_openai_plugins_repo_with_transport_overrides);外部调用 1 个(join)。
prepare_curated_repo_parent_and_temp_dir348–373 ↗
fn prepare_curated_repo_parent_and_temp_dir(repo_path: &Path) -> Result<TempDir, String>
作用:准备一个安全的临时目录,用来先下载或解压新插件仓库。这样正式目录不会在下载一半时被破坏。
数据流:输入是正式仓库路径 → 找到父目录,创建它,清理太旧的临时目录,再新建一个 plugins-clone- 开头的临时目录 → 输出 TempDir。
调用关系:git、HTTP、备用压缩包三种同步方式都会先调用它;它把旧临时目录清理交给 remove_stale_curated_repo_temp_dirs。
调用图:调用 1 个内部函数(remove_stale_curated_repo_temp_dirs);被 3 处调用(sync_openai_plugins_repo_via_backup_archive, sync_openai_plugins_repo_via_git, sync_openai_plugins_repo_via_http);外部调用 4 个(parent, format!, create_dir_all, new)。
remove_stale_curated_repo_temp_dirs375–458 ↗
fn remove_stale_curated_repo_temp_dirs(parent: &Path, max_age: Duration)
作用:删除很久以前遗留下来的插件临时目录。比如上次程序崩了,临时目录可能没来得及自动清理。
数据流:输入是父目录和最大年龄 → 遍历里面 plugins-clone- 开头的目录,读取修改时间,超过时间就尝试删除 → 没有返回值,失败只写警告。
调用关系:prepare_curated_repo_parent_and_temp_dir 在创建新临时目录前调用它,避免 .tmp 目录越堆越多。
调用图:被 1 处调用(prepare_curated_repo_parent_and_temp_dir);外部调用 3 个(read_dir, remove_dir_all, warn!)。
emit_curated_plugins_startup_sync_metric460–466 ↗
fn emit_curated_plugins_startup_sync_metric(transport: &'static str, status: &'static str)
作用:记录某一种同步路线的一次尝试结果,比如 git 成功或 HTTP 失败。指标用于以后观察启动同步是否健康。
数据流:输入是 transport 和 status 两个标签 → 把它们交给通用计数函数 → 没有业务返回值。
调用关系:总调度函数每条路线尝试完都会调用它;它再调用 emit_curated_plugins_startup_sync_counter。
调用图:调用 1 个内部函数(emit_curated_plugins_startup_sync_counter);被 1 处调用(sync_openai_plugins_repo_with_transport_overrides)。
emit_curated_plugins_startup_sync_final_metric468–474 ↗
fn emit_curated_plugins_startup_sync_final_metric(transport: &'static str, status: &'static str)
作用:记录最终采用哪条路线以及最终是否成功。它和普通尝试指标不同,表示最后结局。
数据流:输入是最终路线名和状态 → 写入最终结果计数器 → 不改变同步结果本身。
调用关系:sync_openai_plugins_repo_with_transport_overrides 在决定最终成功或最终失败时调用它;底层同样走 emit_curated_plugins_startup_sync_counter。
调用图:调用 1 个内部函数(emit_curated_plugins_startup_sync_counter);被 1 处调用(sync_openai_plugins_repo_with_transport_overrides)。
emit_curated_plugins_startup_sync_counter476–486 ↗
fn emit_curated_plugins_startup_sync_counter(
metric_name: &str,
transport: &'static str,
status: &'static str,
)
作用:真正把同步指标加一。它是上面两个指标函数共用的底层出口。
数据流:输入是指标名、同步方式和状态 → 如果全局指标系统存在,就带标签给计数器加 1;如果指标系统没启动,就直接返回。
调用关系:emit_curated_plugins_startup_sync_metric 和 emit_curated_plugins_startup_sync_final_metric 都调用它,避免重复写同样的指标代码。
调用图:被 2 处调用(emit_curated_plugins_startup_sync_final_metric, emit_curated_plugins_startup_sync_metric);外部调用 1 个(global)。
ensure_marketplace_manifest_exists488–496 ↗
fn ensure_marketplace_manifest_exists(repo_path: &Path) -> Result<(), String>
作用:确认下载下来的插件仓库里真的有市场清单文件。没有这个文件,后面插件市场就没法正常工作。
数据流:输入是仓库目录 → 检查 .agents/plugins/marketplace.json 是否存在 → 存在则成功,缺失则返回明确错误。
调用关系:三种同步方式在激活新目录前都会调用它;它是把坏压缩包或错误仓库挡在正式目录外的关卡。
调用图:被 3 处调用(sync_openai_plugins_repo_via_backup_archive, sync_openai_plugins_repo_via_git, sync_openai_plugins_repo_via_http);外部调用 2 个(join, format!)。
activate_curated_repo498–552 ↗
fn activate_curated_repo(repo_path: &Path, staged_repo_dir: TempDir) -> Result<(), String>
作用:把已经准备好的临时插件目录切换成正式目录。它尽量做到“要么换成功,要么把旧目录放回去”。
数据流:输入是正式目录路径和临时目录 → 如果旧目录存在,先移到备份目录,再把新目录改名到正式位置;如果新目录移动失败,尝试回滚旧目录 → 输出成功或详细错误。
调用关系:git、HTTP、备用压缩包同步在确认新内容没问题后都会调用它;它是最后真正改动本地插件仓库的关键一步。
调用图:被 3 处调用(sync_openai_plugins_repo_via_backup_archive, sync_openai_plugins_repo_via_git, sync_openai_plugins_repo_via_http);外部调用 6 个(exists, parent, path, format!, rename, new)。
write_curated_plugins_sha554–569 ↗
fn write_curated_plugins_sha(sha_path: &Path, remote_sha: &str) -> Result<(), String>
作用:把当前插件快照的版本号写到 sha 文件里。这个小文件让下次启动能判断本地是不是已经最新。
数据流:输入是 sha 文件路径和版本字符串 → 确保父目录存在,再把版本号加换行写入文件 → 输出成功或写文件错误。
调用关系:三种同步方式在激活仓库后都会调用它;read_sha_file 和 read_curated_plugins_sha 后续会读取这个结果。
调用图:被 3 处调用(sync_openai_plugins_repo_via_backup_archive, sync_openai_plugins_repo_via_git, sync_openai_plugins_repo_via_http);外部调用 4 个(parent, format!, create_dir_all, write)。
read_local_git_or_sha_file571–583 ↗
fn read_local_git_or_sha_file(
repo_path: &Path,
sha_path: &Path,
git_binary: &str,
) -> Option<String>
作用:读取本地插件快照的版本号,优先问 git,问不到再读 sha 文件。这样兼容 git 仓库和纯解压目录两种形态。
数据流:输入是仓库路径、sha 文件路径和 git 命令 → 如果仓库里有 .git 且 git rev-parse 成功,就返回 HEAD sha;否则读取 sha 文件 → 输出可选版本号。
调用关系:sync_openai_plugins_repo_via_git 用它决定本地是否已经和远端一致;它会调用 git_head_sha 或 read_sha_file。
调用图:调用 2 个内部函数(git_head_sha, read_sha_file);被 1 处调用(sync_openai_plugins_repo_via_git);外部调用 1 个(join)。
git_ls_remote_head_sha585–610 ↗
fn git_ls_remote_head_sha(git_binary: &str) -> Result<String, String>
作用:询问远端 GitHub 仓库当前 HEAD 是哪个提交。HEAD 可以理解成默认最新版本的指针。
数据流:输入是 git 命令 → 运行 git ls-remote openai/plugins HEAD,检查命令成功,解析第一行的 sha → 输出远端版本号。
调用关系:git 同步一开始调用它获取目标版本;它用 run_git_command_with_timeout 控制超时,用 ensure_git_success 判断 git 是否成功。
调用图:调用 2 个内部函数(ensure_git_success, run_git_command_with_timeout);被 1 处调用(sync_openai_plugins_repo_via_git);外部调用 3 个(from_utf8_lossy, new, format!)。
git_head_sha612–636 ↗
fn git_head_sha(repo_path: &Path, git_binary: &str) -> Result<String, String>
作用:读取某个本地 git 仓库当前检出的提交号。它用来确认本地目录实际处在哪个版本。
数据流:输入是仓库路径和 git 命令 → 运行 git rev-parse HEAD,检查成功,取标准输出并去空白 → 输出 sha 字符串。
调用关系:read_local_git_or_sha_file 用它读取旧仓库版本;sync_openai_plugins_repo_via_git 在抓取后也用它确认临时仓库确实是目标版本。
调用图:调用 1 个内部函数(ensure_git_success);被 2 处调用(read_local_git_or_sha_file, sync_openai_plugins_repo_via_git);外部调用 3 个(from_utf8_lossy, new, format!)。
run_git_command_with_timeout638–690 ↗
fn run_git_command_with_timeout(
command: &mut Command,
context: &str,
timeout: Duration,
) -> Result<Output, String>
作用:运行一个 git 子进程,并给它设置最长等待时间。这样启动同步不会因为 git 卡住而无限等下去。
数据流:输入是命令对象、说明文字和超时时长 → 启动子进程,循环检查是否结束;正常结束就返回输出,超时就杀掉进程并返回错误 → 可能改变的是外部 git 命令造成的文件状态。
调用关系:git_ls_remote_head_sha、fetch_curated_plugins_commit_from 和 run_git_in_repo 都靠它执行 git;ensure_git_success 随后负责解释退出码。
调用图:被 3 处调用(fetch_curated_plugins_commit_from, git_ls_remote_head_sha, run_git_in_repo);外部调用 8 个(from_millis, null, piped, from_utf8_lossy, stdin, format!, sleep, now)。
ensure_git_success692–705 ↗
fn ensure_git_success(output: &Output, context: &str) -> Result<(), String>
作用:检查 git 命令是否成功退出,并把失败原因整理成人能看懂的错误。它避免调用者只看到一个冷冰冰的退出码。
数据流:输入是 git 输出和上下文说明 → 看退出状态;成功返回 Ok,失败则读取 stderr 并拼成错误文字 → 不改动文件。
调用关系:所有关键 git 命令执行后都会调用它,包括 fetch、rev-parse、ls-remote 和 reset/clean。
调用图:被 4 处调用(fetch_curated_plugins_commit_from, git_head_sha, git_ls_remote_head_sha, run_git_in_repo);外部调用 2 个(from_utf8_lossy, format!)。
fetch_curated_repo_remote_sha707–735 ↗
async fn fetch_curated_repo_remote_sha(api_base_url: &str) -> Result<String, String>
作用:通过 GitHub API 查官方插件仓库默认分支当前的提交号。这是 HTTP 同步路线的第一步。
数据流:输入是 GitHub API 基地址 → 请求仓库信息,解析默认分支名,再请求该分支的 git ref,解析里面的 sha → 输出远端版本号。
调用关系:sync_openai_plugins_repo_via_http 调用它;它用 fetch_github_text 发带 GitHub 头的请求。
调用图:调用 2 个内部函数(fetch_github_text, build_reqwest_client);被 1 处调用(sync_openai_plugins_repo_via_http);外部调用 2 个(format!, from_str)。
fetch_curated_repo_zipball737–746 ↗
async fn fetch_curated_repo_zipball(
api_base_url: &str,
remote_sha: &str,
) -> Result<Vec<u8>, String>
作用:通过 GitHub API 下载某个指定版本的插件仓库 zip 压缩包。
数据流:输入是 GitHub API 基地址和 remote_sha → 拼出 zipball 地址,创建 HTTP 客户端,下载字节内容 → 输出 zip 文件的字节数组。
调用关系:sync_openai_plugins_repo_via_http 在确认需要更新后调用它;下载到的字节随后交给 extract_zipball_to_dir 解压。
调用图:调用 2 个内部函数(fetch_github_bytes, build_reqwest_client);被 1 处调用(sync_openai_plugins_repo_via_http);外部调用 1 个(format!)。
fetch_curated_repo_backup_archive_zip748–776 ↗
async fn fetch_curated_repo_backup_archive_zip(
backup_archive_api_url: &str,
) -> Result<Vec<u8>, String>
作用:从备用导出服务拿插件压缩包。它先拿元数据,再按里面给出的下载地址取真正的 zip。
数据流:输入是备用接口地址 → 请求一段 JSON 元数据,解析 download_url,确认不为空,再下载该地址的字节 → 输出 zip 字节数组。
调用关系:sync_openai_plugins_repo_via_backup_archive 调用它;它使用 fetch_public_text 和 fetch_public_bytes,因为这不是 GitHub API 请求。
调用图:调用 3 个内部函数(fetch_public_bytes, fetch_public_text, build_reqwest_client);被 1 处调用(sync_openai_plugins_repo_via_backup_archive);外部调用 2 个(format!, from_str)。
read_extracted_backup_archive_git_sha778–805 ↗
fn read_extracted_backup_archive_git_sha(repo_path: &Path) -> Result<Option<String>, String>
作用:尝试从备用压缩包解压出的 .git 目录里读出版本号。这样备用包也能尽量带上真实来源版本。
数据流:输入是解压后的仓库目录 → 如果没有 .git 就返回 None;如果有,就读 HEAD,若 HEAD 指向某个 ref 就验证路径并解析 ref,否则直接把 HEAD 当 sha → 输出可选版本号。
调用关系:备用同步流程调用它;它会把 ref 安全检查交给 validate_backup_archive_git_ref,把 ref 到 sha 的解析交给 read_git_ref_sha。
调用图:调用 2 个内部函数(read_git_ref_sha, validate_backup_archive_git_ref);被 1 处调用(sync_openai_plugins_repo_via_backup_archive);外部调用 3 个(join, format!, read_to_string)。
validate_backup_archive_git_ref807–833 ↗
fn validate_backup_archive_git_ref(reference: &str) -> Result<&str, String>
作用:检查备用压缩包里的 git 引用路径是否安全。它防止引用写成绝对路径或带奇怪的上级目录,从而读到不该读的文件。
数据流:输入是 ref 字符串 → 要求它以 refs/ 开头、是相对路径、每段都是普通目录名 → 通过则原样返回,不通过则返回错误。
调用关系:read_extracted_backup_archive_git_sha 在跟随 HEAD 的 ref 前调用它;这是读取压缩包内 git 信息的安全门。
调用图:被 1 处调用(read_extracted_backup_archive_git_sha);外部调用 2 个(new, format!)。
read_git_ref_sha835–866 ↗
fn read_git_ref_sha(git_dir: &Path, reference: &str) -> Result<String, String>
作用:根据 git 引用名找到对应的提交号。它兼顾普通 ref 文件和 packed-refs 这种 git 打包存储方式。
数据流:输入是 .git 目录和 ref 名 → 先尝试读取 .git/refs/... 文件;读不到就扫描 packed-refs;找到非空 sha 就返回,否则报错 → 不修改文件。
调用关系:read_extracted_backup_archive_git_sha 在 HEAD 指向 ref 时调用它,用来把“分支指针”变成具体版本号。
调用图:被 1 处调用(read_extracted_backup_archive_git_sha);外部调用 3 个(join, format!, read_to_string)。
fetch_github_text868–881 ↗
async fn fetch_github_text(client: &Client, url: &str, context: &str) -> Result<String, String>
作用:向 GitHub API 发请求并拿回文本内容。失败时会把 HTTP 状态码和响应内容写进错误里。
数据流:输入是 HTTP 客户端、URL 和上下文说明 → 用 github_request 配好 GitHub 请求头,发送请求,读取文本 → 成功输出字符串,失败输出错误说明。
调用关系:fetch_curated_repo_remote_sha 用它读取仓库信息和分支引用信息;它依赖 github_request 统一设置 GitHub 请求格式。
调用图:调用 1 个内部函数(github_request);被 1 处调用(fetch_curated_repo_remote_sha);外部调用 1 个(format!)。
fetch_github_bytes883–900 ↗
async fn fetch_github_bytes(client: &Client, url: &str, context: &str) -> Result<Vec<u8>, String>
作用:向 GitHub API 下载二进制内容,比如 zip 压缩包。它和 fetch_github_text 类似,只是返回字节。
数据流:输入是 HTTP 客户端、URL 和说明 → 发送带 GitHub 头的请求,读取响应字节,检查状态码 → 成功输出字节数组,失败输出带响应正文的错误。
调用关系:fetch_curated_repo_zipball 调用它下载插件仓库压缩包;请求构造由 github_request 负责。
调用图:调用 1 个内部函数(github_request);被 1 处调用(fetch_curated_repo_zipball);外部调用 2 个(from_utf8_lossy, format!)。
fetch_public_text902–917 ↗
async fn fetch_public_text(client: &Client, url: &str, context: &str) -> Result<String, String>
作用:从普通公开 URL 读取文本,不加 GitHub 专用请求头。它用于备用导出服务的元数据。
数据流:输入是 HTTP 客户端、URL 和说明 → 发 GET 请求并设置备用服务超时,读取文本,检查状态码 → 输出字符串或错误。
调用关系:fetch_curated_repo_backup_archive_zip 先调用它拿包含 download_url 的 JSON。
调用图:被 1 处调用(fetch_curated_repo_backup_archive_zip);外部调用 2 个(get, format!)。
fetch_public_bytes919–938 ↗
async fn fetch_public_bytes(client: &Client, url: &str, context: &str) -> Result<Vec<u8>, String>
作用:从普通公开 URL 下载二进制内容。这里主要用来下载备用导出压缩包。
数据流:输入是 HTTP 客户端、URL 和说明 → 发 GET 请求,读取响应字节,检查状态码 → 输出字节数组或错误。
调用关系:fetch_curated_repo_backup_archive_zip 在拿到 download_url 后调用它;下载结果随后会被解压。
调用图:被 1 处调用(fetch_curated_repo_backup_archive_zip);外部调用 3 个(from_utf8_lossy, get, format!)。
github_request940–946 ↗
fn github_request(client: &Client, url: &str) -> reqwest::RequestBuilder
作用:创建一个带 GitHub API 必要请求头和超时设置的 GET 请求。这样所有 GitHub 请求都长得一致。
数据流:输入是 HTTP 客户端和 URL → 调用 client.get,设置超时、Accept 头和 GitHub API 版本头 → 输出还没发送的请求构造器。
调用关系:fetch_github_text 和 fetch_github_bytes 都用它,避免每个 GitHub 请求重复写相同的头和超时。
调用图:被 2 处调用(fetch_github_bytes, fetch_github_text);外部调用 1 个(get)。
read_sha_file948–953 ↗
fn read_sha_file(sha_path: &Path) -> Option<String>
作用:读取一个保存版本号的小文本文件。空文件或不存在都会被当成没有版本号。
数据流:输入是文件路径 → 尝试读成字符串,去掉首尾空白,过滤空字符串 → 输出 Some(版本号) 或 None。
调用关系:read_curated_plugins_sha、read_local_git_or_sha_file 和 HTTP 同步流程都会用它读取本地版本记录。
调用图:被 3 处调用(read_curated_plugins_sha, read_local_git_or_sha_file, sync_openai_plugins_repo_via_http);外部调用 1 个(read_to_string)。
extract_zipball_to_dir955–1028 ↗
fn extract_zipball_to_dir(bytes: &[u8], destination: &Path) -> Result<(), String>
作用:把下载到的插件 zip 压缩包安全解压到目标目录。它会防止压缩包里的路径逃出目标目录。
数据流:输入是 zip 字节和目标目录 → 创建目标目录,逐个读取压缩包条目,去掉 GitHub zip 顶层目录,把文件写到目标位置,并在支持的平台上恢复权限 → 输出成功或具体错误。
调用关系:HTTP 同步和备用压缩包同步都会调用它;每写完一个文件会调用 apply_zip_permissions 处理权限。
调用图:调用 2 个内部函数(apply_zip_permissions, new);被 2 处调用(sync_openai_plugins_repo_via_backup_archive, sync_openai_plugins_repo_via_http);外部调用 7 个(join, new, new, format!, create, create_dir_all, copy)。
apply_zip_permissions1046–1051 ↗
fn apply_zip_permissions(
_entry: &zip::read::ZipFile<'_>,
_output_path: &Path,
) -> Result<(), String>
作用:把 zip 文件里记录的 Unix 文件权限应用到解压出来的文件上。比如脚本是否可执行,就靠这些权限保留下来。
数据流:输入是 zip 条目和输出文件路径 → 在 Unix 系统上读取条目的权限位并设置到文件;非 Unix 系统上直接成功返回 → 输出成功或权限设置错误。
调用关系:extract_zipball_to_dir 在写出每个普通文件后调用它,让解压结果尽量保持原仓库里的文件属性。
调用图:被 1 处调用(extract_zipball_to_dir);外部调用 3 个(unix_mode, from_mode, set_permissions)。
chatgpt/src/get_task.rs源码 ↗
这个文件像一个“取件窗口”:给它一个任务编号,它就去服务器上取这个任务的详情。服务器返回的是一大包数据,但这里并不关心全部内容,只挑出当前任务回合里的输出项,尤其是类型为 pr 的输出,因为里面有真正的 diff,也就是代码改动前后的差异文本。文件里的几个结构体,比如 GetTaskResponse、AssistantTurn、OutputItem、PrOutputItem、OutputDiff,就是把服务器返回的 JSON 数据翻译成 Rust 程序能安全读取的形状。serde 的 Deserialize 表示“能从 JSON 自动填进这些结构”。如果返回里有不认识的输出类型,OutputItem 会把它归为 Other,而不是直接报错,这样程序更耐用。
get_task37–40 ↗
async fn get_task(config: &Config, task_id: String) -> anyhow::Result<GetTaskResponse>
作用:这个函数根据任务编号去后端拿任务详情。调用者用它来取得某个任务里生成的代码差异,之后才能把这些改动应用到本地。
数据流:进去的是配置 config 和任务编号 task_id。它先把任务编号拼成服务器路径,比如 /wham/tasks/某个编号,然后把配置和路径交给网络请求函数 chatgpt_get_request。出来的是一个 GetTaskResponse,里面可能带有当前任务回合和 diff 内容;如果请求失败,就返回错误。
调用关系:它通常由 run_apply_command 调用,也就是用户想把某个远端任务的结果应用到本地时会走到这里。它自己不直接处理网络细节,而是把真正的 GET 请求交给 chatgpt_get_request;它只负责拼出正确的接口地址,并声明期望把返回内容解析成 GetTaskResponse。
调用图:调用 1 个内部函数(chatgpt_get_request);被 1 处调用(run_apply_command);外部调用 1 个(format!)。
更新与速率限制检查
这些文件查询后端速率限制重置状态,根据可用余量控制内存启动,并协调 CLI 和 TUI 的更新发现。
backend-client/src/client/rate_limit_resets.rs源码 ↗
这个文件是后端客户端的一小块“通信窗口”。限流可以理解成游乐场门口的闸机:一段时间内只能进这么多人;重置额度就像一张特殊通行券,可以把某个限制恢复一次。这里的 Client 提供了两类动作:先去服务器查当前使用限制和剩余重置券,再在需要时消耗一张券。它会根据不同的接口风格拼出不同的网址,比如 Codex API 和 ChatGPT API 的路径不一样;然后用 HTTP 请求(一种客户端和服务器说话的网络方式)发给后端;返回后再把 JSON(一种常见的数据文本格式)解成程序能用的结构。一个重要点是:查询接口拿到的是后端原始状态,公开方法还会把其中的限流快照整理成调用方更好用的形状。
Client::get_rate_limits_with_reset_credits19–25 ↗
async fn get_rate_limits_with_reset_credits(&self) -> Result<RateLimitsWithResetCredits>
作用:这是给外部代码用的查询入口,用来一次性拿到“限流状态”和“可用的重置额度”。调用方不用关心后端原始数据长什么样,只拿到整理后的结果。
数据流:进去的是 Client 自己保存的服务器地址、认证头和接口风格等信息 → 它先调用 get_rate_limit_status 去后端拿原始状态,再把原始的限流列表转换成更适合展示或判断的快照,同时保留重置额度信息 → 出来的是 RateLimitsWithResetCredits,里面包含整理后的限流信息和剩余重置额度;如果网络或解析失败,就返回错误。
调用关系:它是这一组功能里的高层读取方法。需要展示用量、判断是否还能继续请求,或者提示用户是否有重置额度时,会先走到这里;它把真正联网取数据的活交给 Client::get_rate_limit_status,再借助 rate_limit_snapshots_from_payload 把后端返回的原始内容整理一下。
调用图:调用 1 个内部函数(get_rate_limit_status);外部调用 1 个(rate_limit_snapshots_from_payload)。
Client::get_rate_limit_status27–32 ↗
async fn get_rate_limit_status(&self) -> Result<RateLimitStatusWithResetCredits>
作用:这个函数负责真正去后端拉取限流状态。它更偏底层,拿到的是带重置额度的原始状态数据。
数据流:进去的是 Client 里的基础网址、请求头和 HTTP 客户端 → 它先用 rate_limit_status_url 拼出正确的网址,然后发 GET 请求,也就是“只读取、不修改”的网络请求;收到响应后读取正文和内容类型,再把 JSON 解码成 RateLimitStatusWithResetCredits → 出来的是服务器返回的限流状态;如果服务器没响应、返回内容不对或解码失败,就把错误交给调用方。
调用关系:它被 Client::get_rate_limits_with_reset_credits 调用,是高层查询方法下面的实际联网步骤。它自己先找 Client::rate_limit_status_url 要正确地址,然后完成请求和解码。
调用图:调用 1 个内部函数(rate_limit_status_url);被 1 处调用(get_rate_limits_with_reset_credits)。
Client::consume_rate_limit_reset_credit34–47 ↗
async fn consume_rate_limit_reset_credit(
&self,
redeem_request_id: &str,
) -> Result<ConsumeRateLimitResetCreditResponse>
作用:这个函数用来消耗一张“限流重置额度”。调用方给它一个兑换请求编号,它就把这个编号发给后端,请后端执行这次重置消耗。
数据流:进去的是 Client 的连接信息,以及一个 redeem_request_id,也就是这次兑换请求的唯一编号 → 它用 consume_rate_limit_reset_credit_url 拼出提交地址,发 POST 请求,也就是“我要做一次动作”的网络请求;请求体是 JSON,里面放着这个兑换编号,并明确告诉服务器内容类型是 application/json → 出来的是 ConsumeRateLimitResetCreditResponse,表示后端处理这次消耗后的结果;失败时返回错误。
调用关系:它是消耗重置额度的公开入口。用户确认要用掉一次重置机会时,上层代码会调用它;它先让 Client::consume_rate_limit_reset_credit_url 提供正确地址,再把兑换编号包装成请求发给后端。
调用图:调用 1 个内部函数(consume_rate_limit_reset_credit_url);外部调用 1 个(from_static)。
Client::rate_limit_status_url49–54 ↗
fn rate_limit_status_url(&self) -> String
作用:这个小函数专门拼“查询限流状态”的网址。因为同一个客户端可能连不同风格的后端接口,所以不能把路径写死在调用处。
数据流:进去的是 Client 里的 base_url 和 path_style;base_url 是服务器根地址,path_style 表示要用哪套接口路径 → 如果是 Codex API,就拼成 /api/codex/usage;如果是 ChatGPT API,就拼成 /wham/usage → 出来的是完整 URL 字符串,供后续 GET 请求使用。
调用关系:它只服务于 Client::get_rate_limit_status。查询状态前,后者会先来这里拿地址,避免把“不同接口该走哪条路”的判断散落在联网代码里。
调用图:被 1 处调用(get_rate_limit_status);外部调用 1 个(format!)。
Client::consume_rate_limit_reset_credit_url56–68 ↗
fn consume_rate_limit_reset_credit_url(&self) -> String
作用:这个小函数专门拼“消耗重置额度”的网址。它把接口风格差异集中放在一个地方,减少写错路径的风险。
数据流:进去的是 Client 里的服务器根地址和接口风格 → 如果是 Codex API,就拼成 /api/codex/rate-limit-reset-credits/consume;如果是 ChatGPT API,就拼成 /wham/rate-limit-reset-credits/consume → 出来的是完整 URL 字符串,供后续 POST 请求使用。
调用关系:它被 Client::consume_rate_limit_reset_credit 调用。真正发起消耗请求前,那个函数先通过这里拿到正确的提交地址,再继续组装 JSON 请求并发送。
调用图:被 1 处调用(consume_rate_limit_reset_credit);外部调用 1 个(format!)。
memories/write/src/guard.rs源码 ↗
这个文件像一个开工前的“油量检查员”。记忆写入功能启动前,它会先通过登录信息判断用户是不是在用 Codex 后端;如果不是,就不拦着。若是,它会创建一个后端客户端,去服务器取当前的限额信息,也就是还剩多少可用额度、有没有触发限流。然后它会挑出 Codex 对应的那组限额数据,和配置里的最低剩余额度比例作比较。只要主窗口或次窗口里已经用掉太多,或者服务器明确说已经限流,它就返回“不建议启动”。一个重要细节是:如果检查过程中拿不到登录、建客户端失败、请求失败等,它不会强硬阻止启动,而是默认放行。这是为了避免因为检查服务临时出问题,导致整个记忆功能完全起不来。
rate_limits_ok9–13 ↗
async fn rate_limits_ok(auth_manager: &AuthManager, config: &Config) -> bool
作用:这是给外部调用的入口,用来问一句:“现在额度情况允许启动记忆写入吗?”它把真正的检查包起来,并且在检查失败或没有结果时默认回答“可以”。
数据流:它拿到登录管理器和配置 → 调用内部的 rate_limits_check 去查额度 → 如果内部返回明确的 true 或 false,就照着返回;如果内部返回空结果,比如没法检查,就当作 true 返回,不阻塞启动。
调用关系:start_memories_startup_task 在准备启动记忆任务时会调用它。它自己不做细查,而是把活交给 rate_limits_check,自己负责把“没查到结果”变成“默认允许”。
调用图:调用 1 个内部函数(rate_limits_check);被 1 处调用(start_memories_startup_task)。
rate_limits_check15–47 ↗
async fn rate_limits_check(auth_manager: &AuthManager, config: &Config) -> Option<bool>
作用:这个函数是真正做额度检查的地方。它会拿登录信息、连后端、取限额数据,再判断是否低于配置要求。
数据流:它先从 AuthManager 读取当前认证信息 → 如果用户不是走 Codex 后端,就返回空结果,表示不用检查 → 如果是,就用配置里的后端地址和认证信息创建 BackendClient → 向后端请求多组限额快照 → 找到 Codex 对应的限额,找不到就用第一组 → 读取配置中的最低剩余额度百分比 → 调用 snapshot_allows_startup 判断能不能启动 → 如果不能启动,就写一条日志说明跳过原因 → 最后返回 Some(true) 或 Some(false)。中途如果缺少认证、创建客户端失败、请求失败,都会记录警告或直接返回空结果。
调用关系:它只由 rate_limits_ok 调用,是这道“启动闸门”的核心流程。它会调用 auth 取得登录状态,调用外部的 from_auth 创建后端客户端,拿到数据后再交给 snapshot_allows_startup 做纯判断;如果决定不允许启动,还会通过 info! 记录原因。
调用图:调用 2 个内部函数(auth, snapshot_allows_startup);被 1 处调用(rate_limits_ok);外部调用 2 个(from_auth, info!)。
snapshot_allows_startup49–57 ↗
fn snapshot_allows_startup(snapshot: &RateLimitSnapshot, min_remaining_percent: i64) -> bool
作用:这个函数只看一份限额快照,判断它是否允许启动。它不负责联网,也不管登录,只负责把限额数字变成“能不能开工”的结论。
数据流:它收到一份 RateLimitSnapshot 和最低剩余额度百分比 → 如果快照里已经标明触发了限流,就直接返回 false → 否则把“最低剩余百分比”换算成“最多允许已使用百分比” → 分别检查 primary 和 secondary 两个限额窗口 → 两个都没超线才返回 true,否则返回 false。
调用关系:rate_limits_check 拿到服务器返回的限额快照后,会调用它来做判断。它再把每个窗口的细节判断交给 window_allows_startup,自己负责把整体规则串起来。
调用图:调用 1 个内部函数(window_allows_startup);被 1 处调用(rate_limits_check)。
window_allows_startup59–64 ↗
fn window_allows_startup(window: Option<&RateLimitWindow>, max_used_percent: f64) -> bool
作用:这个函数检查单个限额窗口是否还算安全。限额窗口可以理解成一段时间里的额度统计,比如短时间额度或长时间额度。
数据流:它收到一个可能存在的 RateLimitWindow 和最大允许已使用百分比 → 如果窗口存在,就比较 used_percent 是否小于等于允许值 → 如果窗口不存在,就认为没有这项限制,返回 true。
调用关系:它由 snapshot_allows_startup 调用,用来分别检查 primary 和 secondary 两个窗口。它是最底层的小判断函数,只回答单个窗口有没有超过线。
调用图:被 1 处调用(snapshot_allows_startup)。
cli/src/doctor/updates.rs源码 ↗
这个文件像是 Codex 更新路径的“体检医生”。它先看当前程序从哪里启动,再判断安装方式,比如 npm、Bun、Homebrew、独立安装包或未知方式。然后它把本地缓存的版本信息读出来,作为线索放进检查报告里。如果是 npm 管理的安装,它还会确认 npm 全局更新会指向正在运行的那个包;否则用户执行更新命令时,可能更新到别处。最后它会有限度地联网查询最新版本:Homebrew 安装查 Homebrew 的接口,其他安装查 GitHub 最新发布。联网失败不会直接判定体检失败,只会给警告,因为版本新不新是辅助信息,不该盖过更严重的安装问题。
updates_check33–108 ↗
fn updates_check(config: &Config) -> DoctorCheck
作用:这是本文件的主入口,用来生成一条“更新状态”的体检结果。它告诉用户当前更新设置是否合理、是否可能更新错地方、是否能查到新版本。
数据流:进去的是全局配置 Config,里面有 Codex 主目录和是否启动时检查更新等信息。它读取当前可执行文件路径,推断安装方式,读取版本缓存;如果是 npm 安装,还检查 npm 全局目录是否和当前运行的包一致;接着尝试查询最新版本并和当前版本比较。出来的是一个 DoctorCheck,也就是一条体检报告,里面有状态、摘要、细节,以及必要时的修复建议。
调用关系:它是这组更新检查的总调度员。它会请 doctor_install_context 判断安装来源,请 push_cached_version_details 补充本地版本缓存,请 npm_global_root_check 检查 npm 目标,再通过 fetch_latest_version 查询线上最新版本,并用 is_newer 判断是否有更新。最后它把这些信息装进 DoctorCheck。
调用图:调用 4 个内部函数(new, fetch_latest_version, is_newer, push_cached_version_details);外部调用 7 个(env!, format!, current_exe, doctor_install_context, doctor_managed_by_npm, npm_global_root_check, vec!)。
push_cached_version_details110–130 ↗
fn push_cached_version_details(details: &mut Vec<String>, version_file: &Path)
作用:这个函数负责把本地保存的版本缓存写进体检详情里。缓存就像上次看病留下的病历,能告诉用户上次查到的最新版本、检查时间、以及用户是否忽略过某个版本。
数据流:进去的是一组可追加的详情文字 details,以及 version.json 文件路径。它先记录缓存文件在哪里,然后尝试读文件;读到了就按 JSON 解析成 VersionInfo,并追加最新版本、上次检查时间、已忽略版本等文字;文件不存在就写“缺失”,读不了或解析不了就把错误写进去。它不返回新对象,而是直接改动传入的 details。
调用关系:它只服务于 updates_check。updates_check 在组装更新体检报告时调用它,让报告不只看当前网络结果,也能说明本地历史缓存的状态。
调用图:被 1 处调用(updates_check);外部调用 2 个(format!, read_to_string)。
update_action_label132–140 ↗
fn update_action_label(context: &InstallContext) -> &'static str
作用:这个函数把安装方式翻译成用户看得懂的更新命令或说明。比如 npm 安装就显示 npm install -g @openai/codex,Homebrew 安装就显示 brew upgrade --cask codex。
数据流:进去的是 InstallContext,也就是“这个 Codex 是怎么装的”的判断结果。它查看其中的安装方法,然后返回一段固定文字,说明应该用哪种更新动作。它不改任何外部状态。
调用关系:updates_check 会用它生成体检详情里的“update action”。测试函数 tests::update_action_labels_install_contexts 会检查常见安装方式对应的文字没有写错。
fetch_latest_version142–150 ↗
fn fetch_latest_version(context: &InstallContext) -> Result<String, String>
作用:这个函数根据安装方式选择去哪里查最新版本。Homebrew 安装查 Homebrew 的数据源,其他安装方式查 GitHub 最新发布。
数据流:进去的是 InstallContext。它看安装方法:如果是 Brew,就调用 fetch_homebrew_cask_version;如果是 npm、Bun、独立安装或未知方式,就调用 fetch_latest_github_release_version。出来的是最新版本字符串,或者一段错误文字。
调用关系:updates_check 需要知道线上最新版本时会调用它。它自己不直接处理网络细节,而是把具体查询交给 fetch_homebrew_cask_version 或 fetch_latest_github_release_version。
调用图:调用 2 个内部函数(fetch_homebrew_cask_version, fetch_latest_github_release_version);被 1 处调用(updates_check)。
fetch_latest_github_release_version152–163 ↗
fn fetch_latest_github_release_version() -> Result<String, String>
作用:这个函数去 GitHub 查询 Codex 最新发布版本。它适合 npm、Bun、独立安装和未知安装方式,因为这些方式都以 GitHub 发布版本作为参考。
数据流:它没有外部参数,会访问固定的 GitHub latest release 地址,拿到 JSON 数据后读取 tag_name。然后它要求标签名以 rust-v 开头,并把这个前缀去掉,得到纯版本号;如果格式不符合,就返回解析失败的错误。
调用关系:fetch_latest_version 在非 Homebrew 路径下会调用它。它把“下载并解析 JSON”的通用工作交给 http_get_json,自己只关心 GitHub 返回的标签如何变成版本号。
调用图:被 1 处调用(fetch_latest_version)。
fetch_homebrew_cask_version165–172 ↗
fn fetch_homebrew_cask_version() -> Result<String, String>
作用:这个函数去 Homebrew 的官方接口查询 Codex cask 的最新版本。cask 可以理解成 Homebrew 管理图形或二进制应用的一种安装包说明。
数据流:它没有外部参数,会访问固定的 Homebrew cask API 地址,拿到 JSON 后读取 version 字段。成功时返回版本号字符串,失败时返回错误文字。
调用关系:fetch_latest_version 发现当前安装方式是 Brew 时会调用它。它同样把实际联网和 JSON 解析交给 http_get_json,只负责从 Homebrew 数据里取出版本字段。
调用图:被 1 处调用(fetch_latest_version)。
http_get_json174–180 ↗
fn http_get_json(url: &str) -> Result<T, String>
作用:这是一个小工具:给它一个网址,它用 curl 下载内容,并把返回内容按 JSON 解析成调用者想要的结构。JSON 可以理解成一种常见的文本数据格式,很多网页接口都用它传数据。
数据流:进去的是 URL 字符串,以及调用者指定的目标数据类型。它执行 curl -fsSL --max-time 5,也就是最多等 5 秒、失败就报错、安静地下载内容;然后用 serde_json 把文本转成结构化数据。出来的是解析好的对象,或者错误字符串。
调用关系:它是 fetch_latest_github_release_version 和 fetch_homebrew_cask_version 背后的通用联网取数零件。真正执行外部命令的活交给 run_command,所以这个函数不用自己实现进程启动细节。
调用图:外部调用 1 个(run_command)。
is_newer182–187 ↗
fn is_newer(latest: &str, current: &str) -> Option<bool>
作用:这个函数比较两个版本号,判断 latest 是否比 current 新。它只处理像 1.2.3 这样的普通三段版本号。
数据流:进去的是两个版本字符串。它先分别交给 parse_version 拆成数字三元组,例如 1.2.3 变成 1、2、3;两个都能拆出来时就按数字大小比较。出来的是 Some(true)、Some(false),或者 None;None 表示至少有一个版本号格式看不懂,比如带 beta 后缀。
调用关系:updates_check 查到最新版本后会调用它,决定报告里写“有新版本”还是“当前版本不旧”。tests::is_newer_compares_plain_semver 会验证它对普通版本和预发布版本的判断。
调用图:调用 1 个内部函数(parse_version);被 1 处调用(updates_check)。
parse_version189–195 ↗
fn parse_version(value: &str) -> Option<(u64, u64, u64)>
作用:这个函数把版本号字符串拆成三个数字,方便比较大小。它只认“主版本.次版本.补丁版本”这种简单格式,比如 1.2.3。
数据流:进去的是一个字符串。它去掉前后空白,再按点号分成几段,依次尝试把前三段转成整数。成功时返回一个三数字组合;只要缺段或数字解析失败,就返回 None。
调用关系:它是 is_newer 的底层小零件。is_newer 不自己拆字符串,而是调用它把文字版本先变成可比较的数字。
调用图:被 1 处调用(is_newer)。
tests::is_newer_compares_plain_semver211–215 ↗
fn is_newer_compares_plain_semver()
作用:这个测试确认 is_newer 对普通版本号的比较是对的,同时确认带 beta 这类后缀的版本不会被硬猜。
数据流:它没有输入参数。测试里给 is_newer 三组例子:新版本大于当前版本、低于当前版本、以及包含 beta 后缀的版本。输出不是业务数据,而是测试断言;如果结果不符合预期,测试会失败。
调用关系:它在测试阶段运行,用来保护 is_newer 和 parse_version 的行为不被以后改坏。它通过 assert_eq! 对比实际结果和期望结果。
调用图:外部调用 1 个(assert_eq!)。
tests::update_action_labels_install_contexts218–233 ↗
fn update_action_labels_install_contexts()
作用:这个测试确认不同安装方式会显示正确的更新提示文字。这样用户在 doctor 报告里看到的命令不会被误改。
数据流:它构造两个 InstallContext:一个表示 npm 安装,一个表示未知安装。然后分别调用 update_action_label,检查返回文字是否等于预期。它不产生业务输出;断言不通过时测试失败。
调用关系:它在测试阶段运行,专门保护 update_action_label 的映射表。也就是说,如果以后有人改安装方式提示,这个测试会提醒他别把 npm 或未知安装的说明改错。
调用图:外部调用 1 个(assert_eq!)。
tui/src/updates.rs源码 ↗
这个文件只在正式发布版里启用,调试版不会用它。它解决的问题很实际:用户打开 TUI(文字界面)时,程序要知道是否该提示升级,但又不能每次启动都卡着等网络。它的做法像看快递状态:先翻一下本地记事本里上次查到的最新版本,如果这个记录还算新,就直接用;如果超过大约 20 小时没查,就悄悄开一个后台任务去 GitHub、npm 或 Homebrew 查询最新版本,并把结果写回缓存文件,留给下次启动使用。它还会根据用户的安装方式选择合适的来源,比如 Homebrew 安装就看 Homebrew cask,因为 Homebrew 可能比 GitHub 发布稍慢。另一个重要点是“忽略此版本”:如果用户已经关掉过某个版本的升级弹窗,这里会记住,不再反复弹同一个提示。
get_upgrade_version24–54 ↗
fn get_upgrade_version(config: &Config) -> Option<String>
作用:这个函数判断当前是否有值得提示用户升级的新版本。它主要给启动流程用,而且会避免因为联网检查而拖慢界面打开。
数据流:进去的是用户配置,它会先看配置是否允许启动时检查更新,以及当前版本是不是源码自编译版本;如果不该检查,就直接返回空。然后它找到缓存文件,读取上次查到的版本和检查时间;如果缓存太旧,就启动一个后台任务去刷新缓存。最后它把缓存里的最新版本和当前程序版本比较,如果缓存里的版本更新,就返回这个版本号,否则返回空。
调用关系:它会被主运行流程 run 使用,也会被 get_upgrade_version_for_popup 再包一层使用。它自己会询问 get_update_action 来知道该用哪种升级方式,读取 version_filepath 和 read_version_info 找缓存;需要刷新时,它把真正联网和写文件的活儿交给 check_for_update,并通过 spawn 放到后台执行。
调用图:调用 5 个内部函数(get_update_action, is_source_build_version, check_for_update, read_version_info, version_filepath);被 2 处调用(run, get_upgrade_version_for_popup);外部调用 3 个(hours, now, spawn)。
check_for_update70–113 ↗
async fn check_for_update(version_file: &Path, action: Option<UpdateAction>) -> anyhow::Result<()>
作用:这个函数是真正去网上查最新版本、再把结果写进本地缓存的人。它通常不直接挡在用户面前,而是被后台任务调用。
数据流:进去的是缓存文件路径和可能的升级方式。它根据升级方式选择查询来源:Homebrew 安装就查 Homebrew 的接口;npm 或 bun 全局安装会先查 GitHub 最新发布,再确认 npm 包里这个版本已经准备好;独立安装或未知方式就直接查 GitHub。拿到最新版本后,它会读取旧缓存,保留用户之前忽略过的版本号,再写入新的最新版本、当前检查时间和忽略记录,最后确保目录存在并把 JSON 文本写到缓存文件。
调用关系:它由 get_upgrade_version 在缓存过期时放进后台调用。它会用 create_client 发网络请求;需要 GitHub 最新版本时会交给 fetch_latest_github_release_version;如果走 npm 路线,还会调用 ensure_version_ready 确认 npm 上真的能安装这个版本。最后它通过读写缓存文件,把结果留给后续启动或弹窗逻辑使用。
调用图:调用 4 个内部函数(create_client, ensure_version_ready, fetch_latest_github_release_version, read_version_info);被 1 处调用(get_upgrade_version);外部调用 5 个(parent, now, format!, create_dir_all, write)。
fetch_latest_github_release_version115–126 ↗
async fn fetch_latest_github_release_version() -> anyhow::Result<String>
作用:这个函数专门去 GitHub 查询项目最新发布版的版本号。它把 GitHub 返回的标签名转换成程序能比较的普通版本号。
数据流:它不需要外部传入版本号,只会发请求到 GitHub 的“最新 release”接口。接口返回的数据里有一个 tag_name,也就是发布标签;函数拿到这个标签后,用 extract_version_from_latest_tag 去掉多余格式,得到干净的版本号字符串并返回。如果网络、接口状态或格式有问题,就返回错误。
调用关系:它被 check_for_update 调用,是获取 GitHub 最新版本的一个小帮手。check_for_update 会根据安装方式决定是否用它;它自己负责网络请求和标签解析,不负责缓存、不负责决定是否提示用户。
调用图:调用 2 个内部函数(create_client, extract_version_from_latest_tag);被 1 处调用(check_for_update)。
get_upgrade_version_for_popup130–144 ↗
fn get_upgrade_version_for_popup(config: &Config) -> Option<String>
作用:这个函数判断是否应该弹出升级提示窗口。它不仅看有没有新版本,还会尊重用户之前点过的“别再提示这个版本”。
数据流:进去的是用户配置。它先和普通检查一样,确认配置允许检查更新,且当前不是源码自编译版本;然后调用 get_upgrade_version 得到可升级版本。如果没有新版本,就返回空。若有新版本,它再读取缓存文件,看看用户是否已经忽略过这个 exact 版本;如果忽略过,就返回空,否则返回这个新版本号给弹窗使用。
调用关系:它由 run_update_prompt_if_needed 调用,位置更靠近“是否显示弹窗”的界面流程。它把查新版本的工作交给 get_upgrade_version,自己额外检查 read_version_info 里的 dismissed_version,用来避免同一个升级弹窗反复打扰用户。
调用图:调用 4 个内部函数(is_source_build_version, get_upgrade_version, read_version_info, version_filepath);被 1 处调用(run_update_prompt_if_needed)。