Codex 系统手册

功能标志、提供商目录和内置资产安装

stage-4.233 个文件

这一阶段像开门营业前的备货,发生在会话和界面启动之前。它先读功能开关,兼容旧名字,并用策略锁住不能乱改的项;也让用户在终端里勾选实验功能。然后安装和加载技能,放好记忆模板,按配置决定谁启用。插件市场、插件清单和 MCP 通道会被整理好。模型供应商、模型说明、协作模式、审批预设,以及界面用的模型清单、更新方式、宠物素材,也都提前准备妥当。

本阶段的文件33

功能标志解析

这些文件定义功能架构、旧版映射、规范解析、托管强制执行,以及用于查看和保存实验性开关的 UI。

features/src/feature_configs.rs源码 ↗
configconfig load

这个文件像一张“配置表格说明书”。它告诉系统:代码模式、多智能体模式、网络代理这些功能,各自有哪些可配置项。比如代码模式可以开关,也可以排除某些工具命名空间;多智能体模式可以限制同一会话里同时跑多少线程、等待多久、提示文字怎么写;网络代理可以设置代理地址、SOCKS5、哪些域名或 Unix socket 允许访问。这里大量使用 Option,意思是“这个字段可以不填”,不填时由别处决定默认值。serde 用来把 TOML 和 Rust 结构互相转换,schemars 用来生成配置校验用的 JSON Schema。deny_unknown_fields 表示不接受陌生字段,能及早发现拼错配置名。几个 enabled 和 set_enabled 方法实现了统一的 FeatureConfig 接口,让外层代码不用关心具体是哪种功能,都能用同一种方式读取或修改开关。

函数细节6
CodeModeConfigToml::enabled18–20 ↗
fn enabled(&self) -> Option<bool>

作用:读取代码模式这个功能有没有被明确打开或关闭。它不自己决定默认值,只回答配置里有没有写 enabled,以及写的是 true 还是 false。

数据流:进去的是一个 CodeModeConfigToml 配置对象 → 它查看对象里的 enabled 字段 → 出来的是 Option<bool>:有值表示用户明确设置了开或关,没有值表示用户没写这个开关。

调用关系:它是 FeatureConfig 这套统一接口的一部分。外层功能开关系统在判断代码模式是否启用时,会通过这个方法拿到用户配置,然后再结合默认策略做最终决定。

CodeModeConfigToml::set_enabled22–24 ↗
fn set_enabled(&mut self, enabled: bool)

作用:把代码模式的开关写进配置对象里。有人需要用程序方式强制打开或关闭代码模式时,会用它。

数据流:进去的是一个可修改的 CodeModeConfigToml 和一个布尔值 enabled → 它把配置里的 enabled 字段改成 Some(enabled) → 出来没有单独返回值,但这个配置对象已经记录了明确的开关状态。

调用关系:它和 enabled 配成一对,都是 FeatureConfig 接口要求的动作。外层代码如果要统一地修改某个功能是否启用,不需要知道这是代码模式配置,只要调用这个方法即可。

MultiAgentV2ConfigToml::enabled62–64 ↗
fn enabled(&self) -> Option<bool>

作用:读取多智能体 V2 功能有没有被配置成开启或关闭。这里的“多智能体”可以理解成让多个助手线程协同工作的能力。

数据流:进去的是一个 MultiAgentV2ConfigToml 配置对象 → 它只读取 enabled 字段 → 出来的是 Option<bool>,表示用户是否明确写了这个功能开关,以及写成了什么。

调用关系:它把多智能体配置接入通用的 FeatureConfig 流程。外层在加载功能列表或决定是否展示多智能体工具时,会用同一种方式询问它的开关状态。

MultiAgentV2ConfigToml::set_enabled66–68 ↗
fn set_enabled(&mut self, enabled: bool)

作用:把多智能体 V2 功能设置为开启或关闭。它用于程序内部统一改功能开关,而不是手写字段名。

数据流:进去的是一个可修改的 MultiAgentV2ConfigToml 和 true 或 false → 它把 enabled 字段设置为 Some(true) 或 Some(false) → 出来没有返回值,但配置对象已经带上了明确的启用状态。

调用关系:它是 FeatureConfig 接口的写入端。外层如果用统一逻辑批量启用、禁用功能,多智能体配置会通过这个方法被更新。

NetworkProxyConfigToml::enabled110–112 ↗
fn enabled(&self) -> Option<bool>

作用:读取网络代理功能是否被明确启用或禁用。网络代理就是让程序访问网络时先经过一个指定的中转通道。

数据流:进去的是一个 NetworkProxyConfigToml 配置对象 → 它查看 enabled 字段 → 出来的是 Option<bool>,有值表示配置里明确写了代理开关,没有值表示没有写。

调用关系:它让网络代理配置也能被 FeatureConfig 统一识别。外层启动网络相关能力前,可以先通过这个方法知道用户是否要求开启代理。

NetworkProxyConfigToml::set_enabled114–116 ↗
fn set_enabled(&mut self, enabled: bool)

作用:把网络代理功能标记为开启或关闭。它不会检查代理地址是否可用,只负责记录这个开关。

数据流:进去的是一个可修改的 NetworkProxyConfigToml 和一个布尔值 → 它把 enabled 字段改成 Some(enabled) → 出来没有返回值,但配置对象中的网络代理开关已经被明确设置。

调用关系:它是网络代理配置实现 FeatureConfig 的一部分。外层统一配置流程如果需要改网络代理是否启用,会调用这个方法,其他字段如代理 URL、域名权限仍由配置对象里的字段保存。

features/src/legacy.rs源码 ↗
configconfig load

项目里的功能开关有一套新的正式名字,但以前的版本可能用过别的名字,比如把某个功能叫作“web_search”或“connectors”。这个文件就像一张“旧门牌到新门牌”的对照表:读到旧名字时,能找到它现在对应的真正功能。这样用户升级软件后,不会因为配置文件里还写着旧字段就丢掉原来的设置。它还会写日志提醒:你用了旧写法,建议改成新的 [features] 写法。文件里有两种用法:一种是按旧 key 查对应功能,另一种是把旧结构体里的开关值应用到当前 Features 上。真正改开关时,它只做一件简单的事:如果值是 true 就启用,如果是 false 就禁用;如果根本没填,就什么也不动。

函数细节6
legacy_feature_keys50–52 ↗
fn legacy_feature_keys() -> impl Iterator<Item = &'static str>

作用:给外部提供所有还被承认的旧功能开关键名。有人需要检查或合并配置时,可以用它知道哪些旧名字仍然有效。

数据流:进去不需要参数 → 它读取文件里固定写好的旧名对照表 ALIASES → 出来一个可以逐个取出旧 key 的列表式迭代器,不修改任何状态。

调用关系:它在解析功能配置时被 materialize_resolved_enabled 使用。那个流程需要知道哪些旧 key 也算功能开关,于是来这里拿完整清单。

调用图:被 1 处调用(materialize_resolved_enabled)。

feature_for_key54–62 ↗
fn feature_for_key(key: &str) -> Option<Feature>

作用:把一个旧的功能开关键名翻译成现在真正使用的 Feature。如果用户配置里还写旧名字,这个函数负责找出它实际指的是哪个新功能。

数据流:进去一个字符串 key → 它在旧名对照表 ALIASES 里查找完全相同的旧 key;找到了就记录一次“用了旧名字”的提示,并取出对应的 Feature;找不到就返回空 → 出来是 Some(Feature)None

调用关系:它处在“读到一个功能名后判断它是什么”的环节。调用图显示它被 feature_for_key 这一查询流程使用;实际作用是把旧入口接到新的功能枚举上,并在命中旧名时顺手触发提示日志。

调用图:被 1 处调用(feature_for_key)。

LegacyFeatureToggles::apply70–77 ↗
fn apply(self, features: &mut Features)

作用:把旧格式结构体里保存的开关,真正套用到当前功能集合上。它是“旧配置生效”的入口。

数据流:进去的是一个 LegacyFeatureToggles,里面的字段可能是有值或没值;同时拿到可修改的 Features → 它检查旧字段 experimental_use_unified_exec_tool,如果用户确实填了,就交给 set_if_some 去启用或禁用对应的新功能 → 出来没有返回值,但 Features 可能已经被改动。

调用关系:它通常在配置加载后运行,把已经反序列化出来的旧字段应用到新功能系统里。它不自己判断怎么改开关,而是把具体动作交给 set_if_some

调用图:调用 1 个内部函数(set_if_some)。

set_if_some80–91 ↗
fn set_if_some(
    features: &mut Features,
    feature: Feature,
    maybe_value: Option<bool>,
    alias_key: &'static str,
)

作用:只在用户真的写了旧开关值时才动手。这样可以避免“没配置”被误当成“关闭”或“开启”。

数据流:进去的是可修改的 Features、要改的 Feature、一个可能存在的布尔值、以及旧 key 名 → 如果布尔值存在,就用 set_feature 按 true/false 修改功能状态,再写一条旧名提示日志,并记录这次旧配置的使用;如果布尔值不存在,就什么都不做 → 出来没有返回值,但可能改变功能开关并留下记录。

调用关系:它由 LegacyFeatureToggles::apply 调用,是旧配置应用过程里的小分发器。具体开关动作交给 set_feature,提示交给 log_alias,同时还调用 record_legacy_usage 让系统知道用户仍在使用旧写法。

调用图:调用 3 个内部函数(record_legacy_usage, log_alias, set_feature);被 1 处调用(apply)。

set_feature93–99 ↗
fn set_feature(features: &mut Features, feature: Feature, enabled: bool)

作用:根据 true 或 false,实际打开或关闭某个功能。它把“布尔值”翻译成对 Features 的具体操作。

数据流:进去的是可修改的 Features、一个具体 Feature、以及 enabled 布尔值 → 如果 enabled 是 true,就调用 enable;如果是 false,就调用 disable → 出来没有返回值,但目标功能在 Features 里的状态已经被更新。

调用关系:它被 set_if_some 调用,位于旧配置真正落地的最后一步。上层负责判断要不要改,它只负责把开或关执行到功能集合上。

调用图:调用 2 个内部函数(disable, enable);被 1 处调用(set_if_some)。

log_alias101–111 ↗
fn log_alias(alias: &str, feature: Feature)

作用:在用户用了旧功能名时写一条提醒日志,告诉用户最好改用新的正式名字。这样旧配置不会立刻坏掉,但用户也能知道该迁移了。

数据流:进去的是旧名字 alias 和对应的 Feature → 它先从 Feature 取出正式 key;如果旧名字和正式名字一样,就不提示;如果不一样,就通过日志系统写出“检测到旧功能开关,请改用正式写法”的信息 → 出来没有返回值,只可能产生一条日志。

调用关系:它被 set_if_some 调用,用在旧配置真正生效时的提醒步骤。它还会用到 Featurekey 方法拿正式名称,并通过外部日志宏 info! 把提示发出去。

调用图:调用 1 个内部函数(key);被 1 处调用(set_if_some);外部调用 1 个(info!)。

features/src/lib.rs源码 ↗
configconfig load / startup / cross-cutting

项目里有很多功能,有的稳定,有的还在试验,有的已经删掉但老配置里还可能出现。这个文件就像一张总闸表:先登记所有功能的名字、状态和默认值,再把用户配置、旧名字、命令行覆盖项合并成最终可用的一组功能。它还会处理兼容问题,比如老开关不再生效时给出提醒;也会补上依赖关系,比如某个功能打开后必须顺手打开另一个功能。最后,它能把开启了“开发中”功能的情况变成警告事件,提醒用户这些功能可能不稳定。

函数细节37
Stage::experimental_menu_name49–54 ↗
fn experimental_menu_name(self) -> Option<&'static str>

作用:取出实验功能在“实验菜单”里显示的名字。只有真正标成 Experimental 的功能才有这个名字。

数据流:进去的是一个功能阶段 → 它判断这个阶段是不是 Experimental → 出来的是菜单名,或者没有值。

调用关系:它是 Stage 这个状态枚举的小查询工具,供界面或配置展示代码在需要显示实验菜单时使用。

Stage::experimental_menu_description56–63 ↗
fn experimental_menu_description(self) -> Option<&'static str>

作用:取出实验功能在菜单里显示的说明文字,让用户知道这个实验项大概做什么。

数据流:进去的是一个功能阶段 → 如果里面带有实验菜单说明,就把说明拿出来 → 否则返回没有值。

调用关系:它和 experimental_menu_name 配套使用,帮助外层界面把实验功能展示成人能看懂的选项。

Stage::experimental_announcement65–73 ↗
fn experimental_announcement(self) -> Option<&'static str>

作用:取出实验功能的公告文案,比如“新功能上线,可以试试”。空公告会被当作没有公告。

数据流:进去的是一个功能阶段 → 它检查是否是 Experimental,并且公告文字是否为空 → 出来的是公告文字,或者没有值。

调用关系:它服务于通知或菜单展示流程,让外层代码只在确实有公告时才提示用户。

Feature::key277–279 ↗
fn key(self) -> &'static str

作用:把程序内部的功能枚举,变成配置文件里使用的字符串名字。比如内部功能要和 [features] 里的开关对上,就靠它。

数据流:进去的是一个 Feature → 它通过 info 找到这项功能的登记资料 → 出来的是固定的配置键名字符串。

调用关系:record_legacy_usage、log_alias 和 legacy_usage_notice 会用它拿到正式名字,用来判断旧名字、生成提示文案。

调用图:调用 1 个内部函数(info);被 3 处调用(record_legacy_usage, log_alias, legacy_usage_notice)。

Feature::stage281–283 ↗
fn stage(self) -> Stage

作用:查询某个功能现在处于什么阶段,比如稳定、实验中、开发中、已废弃或已移除。

数据流:进去的是一个 Feature → 它通过 info 找到登记资料 → 出来的是 Stage 阶段值。

调用关系:它是功能元数据查询入口之一,外层代码可以用它决定是否展示实验菜单或发出风险提醒。

调用图:调用 1 个内部函数(info)。

Feature::default_enabled285–287 ↗
fn default_enabled(self) -> bool

作用:查询某个功能在没有用户配置时是否默认开启。

数据流:进去的是一个 Feature → 它通过 info 找到登记资料 → 出来的是 true 或 false。

调用关系:它依赖 info 读总登记表,适合外层代码在需要知道默认策略时调用。

调用图:调用 1 个内部函数(info)。

Feature::info289–294 ↗
fn info(self) -> &'static FeatureSpec

作用:在总功能表 FEATURES 里找到某个 Feature 对应的完整登记资料。它是 key、stage、default_enabled 这些查询的共同底层入口。

数据流:进去的是一个 Feature → 它遍历总表,找到 id 相同的 FeatureSpec → 出来的是这条登记资料;如果总表缺项,程序会认为这是代码错误。

调用关系:Feature::key、Feature::stage 和 Feature::default_enabled 都把查表工作交给它,避免每个函数重复找资料。

调用图:被 3 处调用(default_enabled, key, stage)。

FeatureOverrides::apply324–333 ↗
fn apply(self, features: &mut Features)

作用:把少量“额外覆盖”的开关应用到最终功能集合里。目前主要处理 web_search_request 这个旧式覆盖项。

数据流:进去的是覆盖配置和一份可修改的 Features → 如果覆盖项有值,就按 true 开启、false 关闭对应功能,并记录这是旧用法 → 改完后的 Features 留在原地。

调用关系:Features::from_sources 在合并基础配置和 profile 配置之后调用它,让最后的显式覆盖拥有最终决定权。

调用图:调用 3 个内部函数(disable, enable, record_legacy_usage);被 1 处调用(from_sources)。

Features::with_defaults338–349 ↗
fn with_defaults() -> Self

作用:创建一份只包含内置默认值的功能集合。也就是用户什么都没配时,系统应该长什么样。

数据流:没有外部输入 → 它新建一个有序集合,遍历 FEATURES,把 default_enabled 为 true 的功能放进去 → 出来的是一份默认 Features。

调用关系:Features::from_sources 会先用它打底,很多测试和启动配置流程也从这一步开始。

调用图:被 60 处调用(web_search_mode_defaults_to_none_if_unset, web_search_mode_disabled_overrides_legacy_request, web_search_mode_prefers_config_over_legacy_flags, codex_apps_auth_elicitation_disallowed_by_policy_returns_original_result, codex_apps_auth_elicitation_feature_enabled_requests_elicitation, codex_apps_auth_elicitation_granular_mcp_disabled_returns_original_result, codex_apps_auth_elicitation_non_host_owned_server_returns_original_result, default_available_modes, default_mode_enabled_available_modes, elevated_flag_works_by_itself (+15 more));外部调用 1 个(new)。

Features::enabled351–353 ↗
fn enabled(&self, f: Feature) -> bool

作用:检查某个功能现在是不是开启。其他模块在决定能不能用某项能力前,通常会问它。

数据流:进去的是 Features 和一个 Feature → 它查看 enabled 集合里有没有这个功能 → 出来的是 true 或 false。

调用关系:它被很多流程使用,比如校验配置、解析网页搜索模式、判断应用鉴权、上报指标、补齐依赖、写回配置。

调用图:被 12 处调用(validate_pinned_features_constraint, resolve_web_search_mode, from_features, apps_enabled_for_auth, emit_metrics, normalize_dependencies, use_legacy_landlock, materialize_resolved_enabled, unstable_features_warning_event, shell_command_backend_for_features (+2 more));外部调用 1 个(contains)。

Features::apps_enabled_for_auth355–357 ↗
fn apps_enabled_for_auth(&self, has_chatgpt_auth: bool) -> bool

作用:判断 Apps 功能在鉴权场景下是否真的可用。它不只看功能开关,还要求用户有 ChatGPT 登录凭据。

数据流:进去的是当前功能集合和 has_chatgpt_auth → 它先查 Apps 是否开启,再和登录状态做与运算 → 出来的是是否允许使用 Apps 鉴权相关能力。

调用关系:它把普通功能开关和登录条件绑在一起,供认证相关流程在做决定时使用。

调用图:调用 1 个内部函数(enabled)。

Features::use_legacy_landlock359–361 ↗
fn use_legacy_landlock(&self) -> bool

作用:判断是否要使用旧版 Landlock Linux 沙箱方案。沙箱可以理解成把程序关在安全隔间里运行。

数据流:进去的是当前 Features → 它查询 UseLegacyLandlock 这个功能是否开启 → 出来的是 true 或 false。

调用关系:它是沙箱选择代码的便捷入口,内部只是调用 enabled,避免外层到处写具体功能名。

调用图:调用 1 个内部函数(enabled)。

Features::enable363–366 ↗
fn enable(&mut self, f: Feature) -> &mut Self

作用:手动打开某个功能,并返回自己,方便连续设置多个开关。

数据流:进去的是可修改的 Features 和一个 Feature → 它把这个功能插入 enabled 集合 → 出来的是同一个 Features 的可继续修改引用。

调用关系:FeatureOverrides::apply、Features::apply_map、normalize_dependencies、set_enabled 以及测试辅助 set_feature 都会用它来打开功能。

调用图:被 5 处调用(apply, apply_map, normalize_dependencies, set_enabled, set_feature);外部调用 1 个(insert)。

Features::disable368–371 ↗
fn disable(&mut self, f: Feature) -> &mut Self

作用:手动关闭某个功能,并返回自己,方便继续修改。

数据流:进去的是可修改的 Features 和一个 Feature → 它从 enabled 集合里移除这个功能 → 出来的是同一个 Features 的可继续修改引用。

调用关系:FeatureOverrides::apply、Features::apply_map、set_enabled 和测试辅助 set_feature 会用它来关闭功能。

调用图:被 4 处调用(apply, apply_map, set_enabled, set_feature);外部调用 1 个(remove)。

Features::set_enabled373–379 ↗
fn set_enabled(&mut self, f: Feature, enabled: bool) -> &mut Self

作用:按一个布尔值统一设置功能开关:true 就打开,false 就关闭。

数据流:进去的是一个 Feature 和 enabled 布尔值 → 它根据布尔值转交给 enable 或 disable → 出来的是修改后的同一个 Features 引用。

调用关系:normalize_candidate 会用它把候选配置调整成明确的开启或关闭状态。

调用图:调用 2 个内部函数(disable, enable);被 1 处调用(normalize_candidate)。

Features::record_legacy_usage_force381–389 ↗
fn record_legacy_usage_force(&mut self, alias: &str, feature: Feature)

作用:强制记录一次旧开关或旧名字的使用情况,用来之后给用户提示“这个写法过时了”。

数据流:进去的是旧名字 alias 和真实 Feature → 它调用 legacy_usage_notice 生成摘要和详情,再插入 legacy_usages 集合 → Features 多了一条兼容提醒记录。

调用关系:Features::apply_map 在遇到明确过时的配置时会直接调用它;record_legacy_usage 也会把真正需要记录的情况交给它。

调用图:调用 1 个内部函数(legacy_usage_notice);被 2 处调用(apply_map, record_legacy_usage);外部调用 1 个(insert)。

Features::record_legacy_usage391–396 ↗
fn record_legacy_usage(&mut self, alias: &str, feature: Feature)

作用:记录旧功能名的使用,但如果用户用的本来就是正式名字,就不记录,避免误报。

数据流:进去的是 alias 和 Feature → 它先用 Feature::key 拿正式名字比较 → 如果不同,就调用 record_legacy_usage_force 写入提醒。

调用关系:FeatureOverrides::apply、Features::apply_map 和 set_if_some 会在兼容旧配置时用它留下提示。

调用图:调用 2 个内部函数(key, record_legacy_usage_force);被 3 处调用(apply, apply_map, set_if_some)。

Features::legacy_feature_usages398–400 ↗
fn legacy_feature_usages(&self) -> impl Iterator<Item = &LegacyFeatureUsage> + '_

作用:让外层代码读取已经记录的旧开关使用情况,通常用于展示警告或迁移建议。

数据流:进去的是 Features → 它返回 legacy_usages 的迭代器,也就是一条条可读取的旧用法记录 → 不修改原数据。

调用关系:它是对外查看兼容提醒的窗口,其他模块不用知道内部集合怎么存。

调用图:外部调用 1 个(iter)。

Features::emit_metrics402–418 ↗
fn emit_metrics(&self, otel: &SessionTelemetry)

作用:把“哪些功能被用户改成非默认状态”上报到遥测系统。遥测可以理解成程序运行时的统计打点。

数据流:进去的是 Features 和 SessionTelemetry → 它遍历所有未移除的功能,比较当前状态和默认状态 → 对有变化的功能调用 counter 上报一次计数。

调用关系:它在会话运行中帮助项目了解功能开关的实际使用情况,内部依赖 enabled 判断当前状态。

调用图:调用 2 个内部函数(enabled, counter);外部调用 1 个(matches!)。

Features::apply_map421–496 ↗
fn apply_map(&mut self, m: &BTreeMap<String, bool>)

作用:把配置文件里的 [features] 键值表应用到当前功能集合。这里会同时处理新名字、旧名字、已删除但仍要兼容的名字。

数据流:进去的是一张字符串到布尔值的表 → 它逐项识别功能名,跳过已无意义的旧开关,记录需要提醒的旧用法,按布尔值开启或关闭功能;未知名字会写警告日志 → 当前 Features 被更新。

调用关系:Features::apply_toml 会先把 TOML 配置整理成普通表,再把真正开关应用工作交给它。

调用图:调用 5 个内部函数(disable, enable, record_legacy_usage, record_legacy_usage_force, feature_for_key);被 1 处调用(apply_toml);外部调用 2 个(matches!, warn!)。

Features::from_sources498–520 ↗
fn from_sources(
        base: FeatureConfigSource<'_>,
        profile: FeatureConfigSource<'_>,
        overrides: FeatureOverrides,
    ) -> Self

作用:把基础配置、profile 配置和额外覆盖项合并成最终功能集合。这是配置解析后最核心的汇总入口。

数据流:进去的是 base、profile 和 overrides → 它先用 with_defaults 建默认集合,再依次应用旧开关、TOML 功能表和覆盖项,最后补齐依赖关系 → 出来的是最终 Features。

调用关系:配置加载、启动鉴权后端选择、功能需求校验和多项测试都会调用它,因为它代表“最终到底哪些功能开着”。

调用图:调用 2 个内部函数(apply, with_defaults);被 12 处调用(load_config_with_layer_stack, resolve_bootstrap_auth_keyring_backend_kind, validate_feature_requirements_in_config_toml, from_sources_applies_base_profile_and_overrides, from_sources_ignores_removed_apply_patch_freeform_feature_key, from_sources_ignores_removed_image_detail_original_feature_key, from_sources_ignores_removed_js_repl_feature_keys, from_sources_ignores_removed_plugin_hooks_feature_key, from_sources_ignores_removed_terminal_resize_reflow_feature_key, from_sources_ignores_removed_undo_feature_key (+2 more))。

Features::enabled_features522–524 ↗
fn enabled_features(&self) -> Vec<Feature>

作用:列出当前所有已开启的功能,方便外层一次性查看完整结果。

数据流:进去的是 Features → 它遍历 enabled 集合并复制出每个 Feature → 出来的是 Feature 列表。

调用关系:它是批量读取当前开关状态的出口,适合展示、调试或进一步处理。

调用图:外部调用 1 个(iter)。

Features::normalize_dependencies526–533 ↗
fn normalize_dependencies(&mut self)

作用:自动补齐功能之间的依赖。比如一个功能必须靠另一个基础功能工作,就不能只开一半。

数据流:进去的是可修改的 Features → 它检查 SpawnCsv 是否需要 Collab、CodeModeOnly 是否需要 CodeMode → 如果缺少依赖就自动 enable → Features 被修正成更一致的状态。

调用关系:Features::from_sources 在所有配置合并完后调用它;normalize_candidate 也会用它修正候选功能集合。

调用图:调用 2 个内部函数(enable, enabled);被 1 处调用(normalize_candidate)。

legacy_usage_notice536–584 ↗
fn legacy_usage_notice(alias: &str, feature: Feature) -> (String, Option<String>)

作用:为旧配置写法生成给用户看的提醒文案。它告诉用户哪里过时了,以及该怎么改。

数据流:进去的是旧名字 alias 和真实 Feature → 它根据不同功能定制说明,必要时调用 web_search_details 或 Feature::key 拿正式名字 → 出来的是摘要和可选详情。

调用关系:Features::record_legacy_usage_force 专门调用它,把生成的文案保存到 legacy_usages 里。

调用图:调用 2 个内部函数(key, web_search_details);被 1 处调用(record_legacy_usage_force);外部调用 1 个(format!)。

web_search_details586–588 ↗
fn web_search_details() -> &'static str

作用:返回网页搜索旧开关的迁移说明,告诉用户现在应该用新的 web_search 配置方式。

数据流:没有输入 → 它返回一段固定说明文字 → 不修改任何状态。

调用关系:legacy_usage_notice 在处理 WebSearchRequest 或 WebSearchCached 的旧配置提醒时会调用它。

调用图:被 1 处调用(legacy_usage_notice)。

feature_for_key591–598 ↗
fn feature_for_key(key: &str) -> Option<Feature>

作用:把配置文件里的功能键名转换成程序内部的 Feature。它既认正式名字,也会尝试认旧名字。

数据流:进去的是字符串 key → 它先查 FEATURES 总表里的正式 key,找不到再交给 legacy 模块查旧 key → 出来的是对应 Feature,或没有值。

调用关系:Features::apply_map 用它识别配置项;is_known_feature_key 也靠它判断某个字符串是不是已知功能名。

调用图:调用 1 个内部函数(feature_for_key);被 2 处调用(apply_map, is_known_feature_key)。

canonical_feature_for_key600–605 ↗
fn canonical_feature_for_key(key: &str) -> Option<Feature>

作用:只按正式功能名查找 Feature,不接受旧别名。适合需要严格区分“新写法”和“旧写法”的地方。

数据流:进去的是字符串 key → 它只遍历 FEATURES 总表并匹配 spec.key → 出来的是对应 Feature,或没有值。

调用关系:它和 feature_for_key 形成对照:一个只认正式名,一个兼容旧名。

is_known_feature_key608–610 ↗
fn is_known_feature_key(key: &str) -> bool

作用:判断一个字符串是不是系统认识的功能开关名。

数据流:进去的是 key 字符串 → 它调用 feature_for_key 查正式名和旧名 → 出来的是 true 或 false。

调用关系:它是校验入口的小包装,让调用方不用关心具体查找规则。

调用图:调用 1 个内部函数(feature_for_key)。

Features::apply_toml629–632 ↗
fn apply_toml(&mut self, features: &FeaturesToml)

作用:把反序列化后的 TOML 功能配置应用到 Features。TOML 是一种常见配置文件格式。

数据流:进去的是 FeaturesToml → 它先调用 entries 把复杂配置整理成普通开关表,再调用 apply_map 应用 → 当前 Features 被更新。

调用关系:Features::from_sources 在读取到某个配置来源的 features 表后,会通过它进入真正的开关应用流程。

调用图:调用 2 个内部函数(apply_map, entries)。

FeaturesToml::clear_removed_compatibility_entries638–641 ↗
fn clear_removed_compatibility_entries(&mut self)

作用:清理那些只为兼容老配置而保留、但现在不该再写回新配置的项目。

数据流:进去的是可修改的 FeaturesToml → 它把 removed_apps_mcp_path_override 清空,并从 entries 里移除 apps_mcp_path_override → 配置对象变得更干净。

调用关系:FeaturesToml::materialize_resolved_enabled 在把最终状态写回配置前会调用它,避免把废弃项重新生成出来。

调用图:被 1 处调用(materialize_resolved_enabled)。

FeaturesToml::entries643–655 ↗
fn entries(&self) -> BTreeMap<String, bool>

作用:把 FeaturesToml 里不同形态的配置统一整理成“功能名 → 是否开启”的表。

数据流:进去的是 FeaturesToml → 它复制普通 entries,再从 code_mode、multi_agent_v2、network_proxy 这些可能带额外配置的项目里取 enabled 值并塞进表 → 出来的是统一的 BTreeMap。

调用关系:Features::apply_toml 调用它,然后把整理好的表交给 apply_map。

调用图:被 1 处调用(apply_toml)。

FeaturesToml::materialize_resolved_enabled657–681 ↗
fn materialize_resolved_enabled(&mut self, features: &Features)

作用:把已经算好的最终功能状态写回到 FeaturesToml 里,形成一份明确、规范的新配置。

数据流:进去的是可修改的 FeaturesToml 和最终 Features → 它先清掉废弃兼容项,再移除旧 key,然后遍历 FEATURES,把每个功能的最终开关写入对应位置;带复杂配置的功能会保留结构并只更新 enabled → FeaturesToml 被改成解析后的明确状态。

调用关系:它用于配置规范化或保存场景,内部调用 enabled、clear_removed_compatibility_entries、legacy_feature_keys 和 materialize_resolved_feature_enabled。

调用图:调用 4 个内部函数(enabled, clear_removed_compatibility_entries, legacy_feature_keys, materialize_resolved_feature_enabled)。

materialize_resolved_feature_enabled684–692 ↗
fn materialize_resolved_feature_enabled(
    feature: &mut Option<FeatureToml<T>>,
    enabled: bool,
)

作用:专门更新那些既可以写成布尔值、也可以写成详细配置块的功能开关。

数据流:进去的是一个可选 FeatureToml 和最终 enabled 值 → 如果原来有配置,就只改里面的 enabled;如果原来没有,就新建一个布尔开关 → 原位置被补齐或更新。

调用关系:FeaturesToml::materialize_resolved_enabled 在处理 code_mode、multi_agent_v2、network_proxy 这类复杂功能时会调用它。

调用图:被 1 处调用(materialize_resolved_enabled);外部调用 1 个(Enabled)。

FeaturesToml::from695–700 ↗
fn from(entries: BTreeMap<String, bool>) -> Self

作用:把一张普通的功能开关表快速包装成 FeaturesToml,方便测试或其他代码构造配置对象。

数据流:进去的是 BTreeMap<String, bool> → 它把这张表放进 entries,其它字段用默认值 → 出来的是 FeaturesToml。

调用关系:多个配置相关测试和启动鉴权后端解析流程会用它快速制造功能表。

调用图:被 11 处调用(resolve_bootstrap_auth_keyring_backend_kind_uses_secret_auth_storage_feature, feature_table_overrides_legacy_flags, memory_tool_makes_memories_root_readable_without_creating_or_widening_writes, responses_websocket_features_do_not_change_wire_api, resolve_windows_sandbox_mode_falls_back_to_legacy_keys, from_sources_ignores_removed_apply_patch_freeform_feature_key, from_sources_ignores_removed_image_detail_original_feature_key, from_sources_ignores_removed_js_repl_feature_keys, from_sources_ignores_removed_plugin_hooks_feature_key, from_sources_ignores_removed_terminal_resize_reflow_feature_key (+1 more));外部调用 1 个(default)。

FeatureToml::enabled713–718 ↗
fn enabled(&self) -> Option<bool>

作用:从一个功能配置项里取出“是否开启”。不管它是简单布尔值,还是带详细配置的结构,都用同一个入口读取。

数据流:进去的是 FeatureToml → 如果是 Enabled(bool),直接返回这个 bool;如果是 Config,就调用具体配置对象的 enabled 方法 → 出来的是可选布尔值。

调用关系:FeaturesToml::entries 依靠它把复杂配置统一折算成开关状态。

FeatureToml::set_enabled720–725 ↗
fn set_enabled(&mut self, enabled: bool)

作用:修改一个功能配置项里的“是否开启”,同时保留其它详细配置。

数据流:进去的是可修改的 FeatureToml 和 enabled 值 → 如果是简单布尔值就直接替换;如果是详细配置,就调用配置对象自己的 set_enabled → 原配置被更新。

调用关系:materialize_resolved_feature_enabled 会调用它,把最终解析出的开关状态写回复杂功能配置。

unstable_features_warning_event1278–1325 ↗
fn unstable_features_warning_event(
    effective_features: Option<&Table>,
    suppress_unstable_features_warning: bool,
    features: &Features,
    config_path: &str,
) -> Option<Event>

作用:如果用户显式开启了“开发中”的功能,就生成一个警告事件。这样用户能知道这些功能还不完整,可能会有怪问题。

数据流:进去的是有效 features 配置表、是否压制警告、最终 Features 和配置文件路径 → 如果警告被压制就直接返回空;否则找出配置里开启且最终也生效的 UnderDevelopment 功能,排序后拼成消息 → 出来的是 Warning 事件,或者没有事件。

调用关系:它通常在配置加载完成后运行,把风险提示交给事件系统;内部用 enabled 确认功能最终确实开启。

调用图:调用 1 个内部函数(enabled);外部调用 5 个(new, new, format!, matches!, Warning)。

core/src/config/managed_features.rs源码 ↗
configconfig load / startup / runtime feature mutation

项目里有很多功能开关,比如某个实验功能是否启用。这个文件定义了 ManagedFeatures,可以把它理解成“带规则的功能开关盒子”。普通 Features 只记录哪些功能开、哪些功能关;ManagedFeatures 还会记住哪些功能被要求固定,也就是 pinned features。创建它时,它会先读取 feature requirements,也就是外部写下的强制要求;再把用户配置的功能开关修正到符合要求;最后检查有没有冲突。以后如果有人想改功能开关,也要先经过同样的检查。这样做的好处是:配置加载、启动警告、运行时修改都走同一套规则,不会一处允许、一处拒绝。文件还提供了两个配置校验函数,用来在读 TOML 配置时提前发现“用户显式设置”和“系统强制要求”打架的问题。

函数细节22
ManagedFeatures::default30–38 ↗
fn default() -> Self

作用:创建一个最宽松的默认功能开关盒子。它没有任何被强制固定的功能,里面放的是默认的 Features。

数据流:进去没有外部输入 → 它生成默认 Features,并包上一层“允许任意值”的约束 → 出来一个空规则的 ManagedFeatures,不会记录来源,也不会固定任何功能。

调用关系:这是兜底起点。别的代码需要一个还没加载配置、也没有策略限制的功能集合时,可以先用它。它依赖底层的 ConstrainedWithSource 和 Constrained 来包装默认值。

调用图:调用 2 个内部函数(new, allow_any);外部调用 2 个(new, default)。

ManagedFeatures::from_configured42–51 ↗
fn from_configured(
        configured_features: Features,
        feature_requirements: Option<Sourced<FeatureRequirementsToml>>,
    ) -> std::io::Result<Self>

作用:用已经读到的功能配置,加上可选的强制要求,构造一个受约束的 ManagedFeatures。它适合不需要收集启动警告的场景。

数据流:进去的是用户配置出的 Features,以及可能存在的 feature requirements → 它把真正工作交给带可选警告参数的内部函数 → 出来是通过规范化和校验后的 ManagedFeatures,或者一个输入数据错误。

调用关系:这是常用入口之一。配置校验、认证相关初始化、以及测试场景会调用它;它自己不做细活,而是转交给 ManagedFeatures::from_configured_with_optional_warnings。

调用图:被 3 处调用(resolve_bootstrap_auth_keyring_backend_kind, validate_feature_requirements_in_config_toml, guardian_review_session_config_allows_pinned_disabled_feature);外部调用 1 个(from_configured_with_optional_warnings)。

ManagedFeatures::from_configured_with_warnings53–63 ↗
fn from_configured_with_warnings(
        configured_features: Features,
        feature_requirements: Option<Sourced<FeatureRequirementsToml>>,
        startup_warnings: &mut Vec<String>,
    ) -> st

作用:和 from_configured 类似,但会把启动时值得提醒用户的消息收集到一个列表里。比如配置里用了旧名字或未知功能名。

数据流:进去的是配置好的 Features、可选的强制要求、以及一个可追加文字的警告列表 → 它转交给内部构造函数,并把警告列表传进去 → 出来是 ManagedFeatures,警告列表可能被追加了内容。

调用关系:配置加载主流程 load_config_with_layer_stack 会用它,因为启动时需要把配置里的问题集中展示给用户。实际解析和校验仍由 ManagedFeatures::from_configured_with_optional_warnings 完成。

调用图:被 1 处调用(load_config_with_layer_stack);外部调用 1 个(from_configured_with_optional_warnings)。

ManagedFeatures::from_configured_with_optional_warnings65–87 ↗
fn from_configured_with_optional_warnings(
        configured_features: Features,
        feature_requirements: Option<Sourced<FeatureRequirementsToml>>,
        startup_warnings: Option<&mut Vec<Stri

作用:这是构造 ManagedFeatures 的核心步骤。它读取强制要求,修正功能开关,然后确认最终结果真的满足要求。

数据流:进去的是已配置的 Features、可能带来源的 feature requirements、以及可选警告列表 → 它把要求解析成“某功能必须开或关”的表,按这张表覆盖候选功能,再处理功能之间的依赖,最后校验固定要求是否被满足 → 出来是保存了最终 Features、来源和固定规则的 ManagedFeatures;如果冲突就返回错误。

调用关系:from_configured 和 from_configured_with_warnings 都把活交给它。它会调用 parse_feature_requirements 读规则,调用 normalize_candidate 修正候选值,再调用 validate_pinned_features 把不合法情况变成配置读取错误。

调用图:调用 5 个内部函数(new, allow_any, normalize_candidate, parse_feature_requirements, validate_pinned_features);外部调用 1 个(new)。

ManagedFeatures::get89–91 ↗
fn get(&self) -> &Features

作用:取出当前真正生效的 Features。调用者只读它,不直接绕过规则去改它。

数据流:进去的是 ManagedFeatures 自身 → 它从内部受约束的容器里拿出当前 Features 的引用 → 出来是只读的功能开关状态,不改变任何东西。

调用关系:这是读取当前状态的统一小窗口。deref 会通过它让 ManagedFeatures 像 Features 一样被读取;set_enabled 也先通过它复制当前状态,再做修改。

调用图:被 2 处调用(deref, set_enabled);外部调用 1 个(get)。

ManagedFeatures::normalize_and_validate93–102 ↗
fn normalize_and_validate(&self, candidate: Features) -> ConstraintResult<Features>

作用:把一个准备设置的新功能状态先整理好,再检查它能不能被接受。它是“改功能前的安检口”。

数据流:进去的是候选 Features → 它先按固定规则覆盖对应功能,再整理功能依赖,然后问内部约束是否允许这个值,并确认固定功能没有被依赖关系意外改掉 → 出来是整理后的 Features,或一个说明为什么不允许的约束错误。

调用关系:can_set 用它做试检查,set 用它做真正设置前的检查。它会调用 normalize_candidate、底层 can_set,以及 validate_pinned_features_constraint。

调用图:调用 2 个内部函数(normalize_candidate, validate_pinned_features_constraint);被 2 处调用(can_set, set);外部调用 1 个(can_set)。

ManagedFeatures::can_set104–106 ↗
fn can_set(&self, candidate: &Features) -> ConstraintResult<()>

作用:只检查某个功能开关组合能不能设置,不真的改当前值。适合调用方想先试探一下是否会被规则拒绝。

数据流:进去的是一个候选 Features 引用 → 它复制一份候选值,送去 normalize_and_validate 检查 → 出来只表示成功或失败;ManagedFeatures 本身保持不变。

调用关系:它是 set 的“预演版”。需要在界面、命令或策略判断里提前知道某个修改是否可行时,会用到它;具体规则判断交给 normalize_and_validate。

调用图:调用 1 个内部函数(normalize_and_validate);外部调用 1 个(clone)。

ManagedFeatures::set108–111 ↗
fn set(&mut self, candidate: Features) -> ConstraintResult<()>

作用:真正替换当前功能开关状态,但只有通过固定规则和依赖规则检查后才会替换。

数据流:进去的是新的 Features → 它先调用 normalize_and_validate 得到合法且整理过的版本 → 然后写入内部容器 → 出来是成功标记;如果不合法,当前值不会被改动。

调用关系:set_enabled 会调用它来保存单个功能的开关变化。它是运行中修改整个 Features 时必须经过的关卡。

调用图:调用 1 个内部函数(normalize_and_validate);被 1 处调用(set_enabled)。

ManagedFeatures::set_enabled113–117 ↗
fn set_enabled(&mut self, feature: Feature, enabled: bool) -> ConstraintResult<()>

作用:打开或关闭某一个指定功能。它比直接 set 整个 Features 更方便,也仍然遵守所有固定要求。

数据流:进去的是某个 Feature 和目标状态 true 或 false → 它复制当前 Features,改掉这个功能的状态,再把整份新状态交给 set → 出来是成功或约束错误;成功时当前功能集合被更新。

调用关系:enable 和 disable 都只是它的简化包装。它先通过 get 读取当前状态,再通过 set 走完整校验流程。

调用图:调用 2 个内部函数(get, set);被 2 处调用(disable, enable)。

ManagedFeatures::enable119–121 ↗
fn enable(&mut self, feature: Feature) -> ConstraintResult<()>

作用:把某个功能打开。它是 set_enabled(feature, true) 的更好读的写法。

数据流:进去的是要打开的 Feature → 它把目标状态固定为 true,交给 set_enabled → 出来是成功或规则拒绝的错误;成功时该功能处于开启状态。

调用关系:这是给调用方使用的便捷方法。真正的读取、修改和校验都由 set_enabled 继续完成。

调用图:调用 1 个内部函数(set_enabled)。

ManagedFeatures::disable123–125 ↗
fn disable(&mut self, feature: Feature) -> ConstraintResult<()>

作用:把某个功能关闭。它是 set_enabled(feature, false) 的更好读的写法。

数据流:进去的是要关闭的 Feature → 它把目标状态固定为 false,交给 set_enabled → 出来是成功或规则拒绝的错误;成功时该功能处于关闭状态。

调用关系:这是便捷方法。它不自己处理规则,而是统一走 set_enabled,避免关闭功能时绕开固定要求。

调用图:调用 1 个内部函数(set_enabled)。

ManagedFeatures::from132–140 ↗
fn from(features: Features) -> Self

作用:这是测试专用的转换方法,用普通 Features 快速造出 ManagedFeatures。它不带任何强制要求。

数据流:进去的是一份 Features → 它包成允许任意值的受约束对象,并使用空的 pinned_features → 出来是没有来源、没有固定规则的 ManagedFeatures。

调用关系:它只在测试编译时存在。若干认证引导相关测试会用它快速准备功能开关环境,而不用模拟完整配置加载流程。

调用图:调用 2 个内部函数(new, allow_any);被 4 处调用(codex_apps_auth_elicitation_disallowed_by_policy_returns_original_result, codex_apps_auth_elicitation_feature_enabled_requests_elicitation, codex_apps_auth_elicitation_granular_mcp_disabled_returns_original_result, codex_apps_auth_elicitation_non_host_owned_server_returns_original_result);外部调用 1 个(new)。

ManagedFeatures::deref146–148 ↗
fn deref(&self) -> &Self::Target

作用:让 ManagedFeatures 在只读时可以像 Features 一样使用。这样调用方读功能状态时更顺手。

数据流:进去的是 ManagedFeatures 自身 → 它调用 get 取出内部 Features 引用 → 出来是 Features 的只读引用,不改变状态。

调用关系:这是 Rust 的 Deref 机制,也就是“把外壳当里面的东西来读”的接口。它把实际读取交给 ManagedFeatures::get。

调用图:调用 1 个内部函数(get)。

normalize_candidate151–160 ↗
fn normalize_candidate(
    mut candidate: Features,
    pinned_features: &BTreeMap<Feature, bool>,
) -> Features

作用:把一份候选功能开关整理成符合固定要求和依赖关系的最终候选值。可以理解成先按规定改正,再让相关联的开关对齐。

数据流:进去的是候选 Features 和一张固定功能表 → 它逐个把固定功能设成要求的开或关,然后调用 normalize_dependencies 整理功能之间的依赖 → 出来是一份被修正过的 Features。

调用关系:构造 ManagedFeatures 和运行中修改功能时都会用它。from_configured_with_optional_warnings 用它处理启动配置,normalize_and_validate 用它处理后续候选修改。

调用图:调用 2 个内部函数(normalize_dependencies, set_enabled);被 2 处调用(from_configured_with_optional_warnings, normalize_and_validate)。

validate_pinned_features_constraint162–187 ↗
fn validate_pinned_features_constraint(
    normalized_features: &Features,
    pinned_features: &BTreeMap<Feature, bool>,
    source: Option<&RequirementSource>,
) -> ConstraintResult<()>

作用:检查最终功能状态有没有违背强制固定要求。比如要求 A 必须关,但整理后 A 变成开,这里就会报错。

数据流:进去的是已经整理过的 Features、固定功能表、以及可选的规则来源 → 如果没有来源就直接通过;如果有来源,就逐项比较实际状态和要求状态 → 出来是成功,或一个带字段名、候选值、允许值和来源的约束错误。

调用关系:normalize_and_validate 用它阻止运行中非法修改;validate_pinned_features 也用它把启动配置错误转换成标准输入输出错误。它会调用 feature_requirements_display 生成给人看的允许值说明。

调用图:调用 2 个内部函数(feature_requirements_display, enabled);被 2 处调用(normalize_and_validate, validate_pinned_features);外部调用 1 个(format!)。

validate_pinned_features189–196 ↗
fn validate_pinned_features(
    normalized_features: &Features,
    pinned_features: &BTreeMap<Feature, bool>,
    source: Option<&RequirementSource>,
) -> std::io::Result<()>

作用:在配置加载阶段检查固定功能要求,并把约束错误包装成普通的配置读取错误。这样上层能按“配置文件不合法”来处理。

数据流:进去的是整理后的 Features、固定功能表、规则来源 → 它调用 validate_pinned_features_constraint 做实际比较 → 出来是 std::io::Result;失败时错误类型是 InvalidData。

调用关系:from_configured_with_optional_warnings 在创建 ManagedFeatures 前会调用它。它是启动配置校验和通用约束校验之间的适配层。

调用图:调用 1 个内部函数(validate_pinned_features_constraint);被 1 处调用(from_configured_with_optional_warnings)。

feature_requirements_display198–204 ↗
fn feature_requirements_display(feature_requirements: &BTreeMap<Feature, bool>) -> String

作用:把固定功能要求变成人能看懂的一串文字。比如把内部表显示成类似 [feature_a=true, feature_b=false]。

数据流:进去的是固定功能表 → 它逐项取功能的配置键名和要求状态,拼成字符串列表 → 出来是一段用于错误消息的文字。

调用关系:当需要告诉用户“允许的值是什么”时会用它。validate_pinned_features_constraint 和 validate_explicit_feature_settings_in_config_toml 都会调用它来生成错误提示。

调用图:被 2 处调用(validate_explicit_feature_settings_in_config_toml, validate_pinned_features_constraint);外部调用 1 个(format!)。

parse_feature_requirements206–242 ↗
fn parse_feature_requirements(
    feature_requirements: FeatureRequirementsToml,
    source: &RequirementSource,
    mut startup_warnings: Option<&mut Vec<String>>,
) -> BTreeMap<Feature, bool>

作用:把 TOML 里写的功能强制要求,解析成程序内部认得的 Feature 到 true/false 的表。它还会处理旧名字和未知名字。

数据流:进去的是 FeatureRequirementsToml、这份要求来自哪里、以及可选启动警告列表 → 它逐条看配置键:特殊的 auto_review 会映射到 GuardianApproval;标准名字直接接受;旧名字会接受但发警告;完全不认识的名字会忽略并发警告 → 出来是一张固定功能表。

调用关系:ManagedFeatures 构造时会用它读取规则;显式配置校验也会用它知道哪些功能被强制固定。遇到需要提醒用户的情况,它会把消息交给 push_feature_requirement_warning。

调用图:调用 1 个内部函数(push_feature_requirement_warning);被 2 处调用(from_configured_with_optional_warnings, validate_explicit_feature_settings_in_config_toml);外部调用 4 个(new, canonical_feature_for_key, feature_for_key, format!)。

push_feature_requirement_warning244–252 ↗
fn push_feature_requirement_warning(
    startup_warnings: &mut Option<&mut Vec<String>>,
    message: String,
)

作用:发出一条关于功能要求配置的警告,并在需要时把这条警告存进启动警告列表。

数据流:进去的是可选警告列表和一段消息 → 它先通过日志系统记录警告;如果调用方提供了列表,就把同一段消息追加进去 → 出来没有返回值,但日志和列表可能发生变化。

调用关系:parse_feature_requirements 在遇到旧功能名或未知功能名时会调用它。这样既能给开发/运维日志留下记录,也能在启动提示里集中展示给用户。

调用图:被 1 处调用(parse_feature_requirements);外部调用 1 个(warn!)。

explicit_feature_settings_in_config254–272 ↗
fn explicit_feature_settings_in_config(cfg: &ConfigToml) -> Vec<(String, Feature, bool)>

作用:从主配置文件里找出用户明确写过的功能开关。它只收集“用户真的写了”的设置,方便之后检查是否和强制要求冲突。

数据流:进去的是 ConfigToml → 它查看 cfg.features 里的每个键,并把能识别的功能记录下来;还会单独检查 experimental_use_unified_exec_tool 这个旧式/独立配置项 → 出来是一个列表,每项包含配置路径、对应 Feature、以及用户设置的 true/false。

调用关系:validate_explicit_feature_settings_in_config_toml 会调用它。它负责把零散的配置写法统一整理成可比较的列表。

调用图:被 1 处调用(validate_explicit_feature_settings_in_config_toml);外部调用 3 个(new, feature_for_key, format!)。

validate_explicit_feature_settings_in_config_toml274–314 ↗
fn validate_explicit_feature_settings_in_config_toml(
    cfg: &ConfigToml,
    feature_requirements: Option<&Sourced<FeatureRequirementsToml>>,
) -> std::io::Result<()>

作用:检查用户在 TOML 里明确写的功能开关,是否和系统强制要求打架。它能尽早指出具体是哪一项配置不允许。

数据流:进去的是 ConfigToml 和可选的 feature requirements → 如果没有强制要求就直接通过;否则先解析固定功能表,再收集用户显式写过的功能设置,逐个比较 → 出来是成功,或一个 InvalidData 错误,错误里会写明冲突的配置路径、用户值、允许值和规则来源。

调用关系:更上层的 validate_feature_requirements_for_config_toml 会调用它做配置文件级检查。它会用 parse_feature_requirements 读规则,用 explicit_feature_settings_in_config 找用户设置,用 feature_requirements_display 组织错误提示。

调用图:调用 3 个内部函数(explicit_feature_settings_in_config, feature_requirements_display, parse_feature_requirements);被 1 处调用(validate_feature_requirements_for_config_toml);外部调用 2 个(new, format!)。

validate_feature_requirements_in_config_toml316–329 ↗
fn validate_feature_requirements_in_config_toml(
    cfg: &ConfigToml,
    feature_requirements: Option<&Sourced<FeatureRequirementsToml>>,
) -> std::io::Result<()>

作用:检查整份 TOML 配置算出来的最终功能状态,是否满足强制要求。它不是只看用户写了什么,而是看合并默认值、配置值和覆盖值后的结果。

数据流:进去的是 ConfigToml 和可选 feature requirements → 它先用 Features::from_sources 从配置内容生成实际 Features,再用 ManagedFeatures::from_configured 套上强制要求并校验 → 出来是成功,或配置数据不合法的错误。

调用关系:上层配置校验流程 validate_feature_requirements_for_config_toml 会调用它。它把普通配置转换成 Features 后,交给 ManagedFeatures 的标准构造流程,确保启动时最终功能状态合法。

调用图:调用 2 个内部函数(from_configured, from_sources);被 1 处调用(validate_feature_requirements_for_config_toml);外部调用 2 个(default, default)。

tui/src/bottom_pane/experimental_features_view.rs源码 ↗
domain_logic用户打开实验功能弹窗期间,处理按键、刷新画面,并在退出时提交更新

这个文件解决一个很实际的问题:实验功能不能只靠改配置文件,用户需要一个看得见、能操作的开关面板。它实现了一个终端用户界面(TUI,就是在命令行窗口里画出来的界面)的小弹窗。每个实验功能会显示名字、说明和一个类似复选框的标记,光标所在行用箭头提示。用户可以用上下键、翻页键、跳到顶部/底部来浏览,用空格切换开关,用回车或取消键结束。结束时,它不会自己直接写文件,而是发出一个应用事件,告诉外层程序“这些功能开关变了,请保存”。渲染部分负责把标题、列表和底部提示分成几块画到屏幕上,并处理窗口太小、没有实验功能等情况。整体像一个临时设置面板:打开时展示当前状态,操作时只改内存里的勾选,关闭时一次性提交。

函数细节18
ExperimentalFeaturesView::new52–74 ↗
fn new(
        features: Vec<ExperimentalFeatureItem>,
        app_event_tx: AppEventSender,
        keymap: ListKeymap,
    ) -> Self

作用:创建一个新的实验功能弹窗,把功能列表、发事件的通道和按键规则装进去。它也准备好标题和底部提示,并默认选中第一项。

数据流:进去的是一组实验功能、一个可以向应用发送消息的发送器、以及按键映射规则;它生成标题区域、提示文字和滚动状态;出来的是一个可显示、可响应按键的 ExperimentalFeaturesView。

调用关系:当外层要打开实验功能弹窗时会调用它,比如 open_experimental_popup 和相关测试。它内部会调用 experimental_popup_hint_line 生成底部提示,并调用 initialize_selection 让列表一开始有合理的选中项。

调用图:调用 3 个内部函数(experimental_popup_hint_line, new, new);被 3 处调用(open_experimental_popup, experimental_features_popup_snapshot, experimental_features_toggle_saves_on_exit);外部调用 2 个(new, from)。

ExperimentalFeaturesView::initialize_selection76–82 ↗
fn initialize_selection(&mut self)

作用:决定弹窗刚出现时应该选中哪一行。没有功能时不选中,有功能但还没选中时选第一行。

数据流:它读取当前功能数量和已有的 selected_idx;如果列表为空,就把选中位置设为 None;如果列表不空且还没有选中位置,就设为 0;它不返回新值,只修改内部滚动选择状态。

调用关系:它在 ExperimentalFeaturesView::new 里被调用,是弹窗初始化的一步。它依赖 visible_len 来知道列表里到底有多少项。

调用图:调用 1 个内部函数(visible_len)。

ExperimentalFeaturesView::visible_len84–86 ↗
fn visible_len(&self) -> usize

作用:告诉其他代码当前列表里有多少个可以显示的实验功能。这里没有过滤,所以就是功能数组的长度。

数据流:它读取 self.features 的长度;不改任何状态;返回一个数字,表示列表项数量。

调用关系:初始化选中项、上下移动、翻页、跳到顶部和底部时都会问它列表有多长。它是这些导航函数避免越界的基础。

调用图:被 7 处调用(initialize_selection, jump_bottom, jump_top, move_down, move_up, page_down, page_up)。

ExperimentalFeaturesView::build_rows88–107 ↗
fn build_rows(&self) -> Vec<GenericDisplayRow>

作用:把内部的功能数据变成适合画到屏幕上的行。它会给当前选中项加箭头,也会用 [x] 或 [ ] 表示开关状态。

数据流:它读取功能列表和当前选中的行号;逐项拼出显示用的名称和说明;返回一组 GenericDisplayRow,供渲染和高度计算使用,不修改功能本身。

调用关系:render 要靠它拿到真正要画的列表内容,desired_height 也要靠它估算弹窗需要多高。它把原始数据变成通用的“可显示行”,再交给 measure_rows_height 和 render_rows。

调用图:被 2 处调用(desired_height, render);外部调用 3 个(default, with_capacity, format!)。

ExperimentalFeaturesView::move_up109–116 ↗
fn move_up(&mut self)

作用:把光标往上一行移。到顶时会绕到最后一行,像菜单循环选择一样。

数据流:它先读取列表长度;如果没有项目就什么也不做;否则更新 selected_idx,并调整滚动位置,确保选中的行仍然在可见范围内。

调用关系:handle_key_event 在用户按“向上”对应按键时调用它。它把具体移动交给 ScrollState 的 move_up_wrap 和 ensure_visible。

调用图:调用 3 个内部函数(visible_len, ensure_visible, move_up_wrap);被 1 处调用(handle_key_event)。

ExperimentalFeaturesView::move_down118–125 ↗
fn move_down(&mut self)

作用:把光标往下一行移。到底时会绕回第一行。

数据流:它读取列表长度;空列表直接返回;非空时更新选中行,并调整滚动窗口,保证新选中的行能显示出来。

调用关系:handle_key_event 在用户按“向下”对应按键时调用它。它依赖 ScrollState 完成循环移动和可见性修正。

调用图:调用 3 个内部函数(visible_len, ensure_visible, move_down_wrap);被 1 处调用(handle_key_event)。

ExperimentalFeaturesView::page_up127–131 ↗
fn page_up(&mut self)

作用:向上翻一页,让用户不用一行一行移动。页大小受弹窗最多显示行数限制。

数据流:它读取列表长度,算出当前最多能显示多少行;然后让滚动状态把选中位置向上移动一页,并限制在有效范围内;不返回值。

调用关系:handle_key_event 在用户按翻页上键时调用它。真正的边界处理交给 ScrollState 的 page_up_clamped。

调用图:调用 2 个内部函数(visible_len, page_up_clamped);被 1 处调用(handle_key_event)。

ExperimentalFeaturesView::page_down133–137 ↗
fn page_down(&mut self)

作用:向下翻一页,适合实验功能较多时快速浏览。

数据流:它读取列表长度,算出一页能显示的行数;然后更新滚动和选中位置,最多移动到列表底部;不返回值。

调用关系:handle_key_event 在用户按翻页下键时调用它。它把具体计算交给 ScrollState 的 page_down_clamped。

调用图:调用 2 个内部函数(visible_len, page_down_clamped);被 1 处调用(handle_key_event)。

ExperimentalFeaturesView::jump_top139–143 ↗
fn jump_top(&mut self)

作用:直接跳到列表第一项。列表很长时,这是快速回到开头的快捷操作。

数据流:它读取列表长度和可见行数;然后把选中位置和滚动位置调整到顶部;不返回值。

调用关系:handle_key_event 在用户按“跳到顶部”的快捷键时调用它。它使用 ScrollState 的 jump_top 做实际位置更新。

调用图:调用 2 个内部函数(visible_len, jump_top);被 1 处调用(handle_key_event)。

ExperimentalFeaturesView::jump_bottom145–149 ↗
fn jump_bottom(&mut self)

作用:直接跳到列表最后一项。列表很长时,用户可以不用一直按向下键。

数据流:它读取列表长度和可见行数;然后把选中位置和滚动位置调整到底部;不返回值。

调用关系:handle_key_event 在用户按“跳到底部”的快捷键时调用它。它使用 ScrollState 的 jump_bottom 做实际位置更新。

调用图:调用 2 个内部函数(visible_len, jump_bottom);被 1 处调用(handle_key_event)。

ExperimentalFeaturesView::toggle_selected151–159 ↗
fn toggle_selected(&mut self)

作用:切换当前选中实验功能的开关状态。开着就关掉,关着就打开。

数据流:它读取当前选中行号;如果没有选中项就直接结束;如果找到了对应功能,就把 enabled 布尔值取反;不立刻保存,只改弹窗里的临时状态。

调用关系:handle_key_event 在用户按空格时调用它。之后用户退出弹窗时,on_ctrl_c 会把这些临时改动整理成更新事件发出去。

调用图:被 1 处调用(handle_key_event)。

ExperimentalFeaturesView::rows_width161–163 ↗
fn rows_width(total_width: u16) -> u16

作用:算出列表行实际可用的宽度。它会从总宽度里扣掉左右留白。

数据流:进去的是弹窗内容的总宽度;它安全地减去 2,避免宽度太小时出现负数;出来的是列表渲染时使用的宽度。

调用关系:render 和 desired_height 需要知道行宽,才能正确换行、测量高度和画列表。这个小函数把宽度计算规则集中在一处。

ExperimentalFeaturesView::handle_key_event167–187 ↗
fn handle_key_event(&mut self, key_event: KeyEvent)

作用:处理用户在弹窗里按下的键。它像前台接待员一样,把不同按键分派给移动、翻页、切换或退出保存。

数据流:进去的是一个按键事件;它根据 keymap 和具体按键判断用户意图;然后调用对应函数修改选择、切换开关,或调用 on_ctrl_c 提交并关闭;本身不返回操作结果。

调用关系:这是 BottomPaneView 接口的一部分,外层主循环收到键盘输入后会调用它。它把工作分给 move_up、move_down、page_up、page_down、jump_top、jump_bottom、toggle_selected 和 on_ctrl_c。

调用图:调用 8 个内部函数(jump_bottom, jump_top, move_down, move_up, on_ctrl_c, page_down, page_up, toggle_selected)。

ExperimentalFeaturesView::is_complete189–191 ↗
fn is_complete(&self) -> bool

作用:告诉外层这个弹窗是不是已经结束了。结束后外层就可以把它关掉。

数据流:它读取 complete 这个内部标记;不修改任何东西;返回 true 或 false。

调用关系:这是 BottomPaneView 接口的一部分。外层界面循环会用它判断这个底部面板是否还要继续显示。

ExperimentalFeaturesView::on_ctrl_c193–207 ↗
fn on_ctrl_c(&mut self) -> CancellationEvent

作用:结束这个弹窗,并把用户改过的实验功能开关提交出去。名字里虽然有 Ctrl+C,但这里也会被确认键或取消键触发。

数据流:它读取所有功能的 feature 标识和 enabled 状态;如果列表不空,就打包成 UpdateFeatureFlags 事件,通过 app_event_tx 发给应用;然后把 complete 设为 true,并返回“已处理取消事件”。

调用关系:handle_key_event 在用户按确认或取消相关按键时调用它。它不自己写 config.toml,而是通过 send 把保存请求交给应用的事件系统。

调用图:调用 1 个内部函数(send);被 1 处调用(handle_key_event)。

ExperimentalFeaturesView::render211–267 ↗
fn render(&self, area: Rect, buf: &mut Buffer)

作用:把实验功能弹窗画到终端屏幕上。它负责标题、列表、空状态提示和底部快捷键说明的位置。

数据流:进去的是一块屏幕区域和一个绘图缓冲区;它先检查区域是否为空,再切分内容区和底部提示区,生成要显示的行,计算高度,然后把背景、标题、列表和提示写进缓冲区;它不改变功能开关状态。

调用关系:这是 Renderable 接口的一部分,界面刷新时会调用它。它会用 build_rows 准备列表,用 measure_rows_height 算高度,用 render_rows 真正画列表,并使用 user_message_style 统一外观。

调用图:调用 5 个内部函数(build_rows, measure_rows_height, render_rows, vh, user_message_style);外部调用 7 个(default, Fill, Length, Max, vertical, clone, rows_width)。

ExperimentalFeaturesView::desired_height269–282 ↗
fn desired_height(&self, width: u16) -> u16

作用:估算这个弹窗在给定宽度下需要多高。外层布局可以用它决定给弹窗留多少空间。

数据流:进去的是可用宽度;它构造显示行,算出列表在这个宽度下需要的高度,再加上标题、间距和底部提示;出来的是期望高度。

调用关系:这是 Renderable 接口的一部分,布局计算时会调用它。它和 render 使用同样的 build_rows、rows_width 和 measure_rows_height,保证“估算的高度”和“真正画出来的内容”尽量一致。

调用图:调用 2 个内部函数(build_rows, measure_rows_height);外部调用 1 个(rows_width)。

experimental_popup_hint_line285–293 ↗
fn experimental_popup_hint_line() -> Line<'static>

作用:生成弹窗底部那行操作提示,告诉用户按空格选择、按回车保存到下一次对话。

数据流:它不需要输入;内部拼出一段带按键样式的文字;返回一个 Line,供弹窗底部渲染使用。

调用关系:ExperimentalFeaturesView::new 在创建弹窗时调用它,把生成的提示存进 footer_hint。之后 render 会把这行提示画出来。

调用图:被 1 处调用(new);外部调用 2 个(from, vec!)。

技能配置与安装

这些文件描述技能相关配置,安装内置系统技能,初始化扩展资源,并加载最终过滤后的运行时技能集。

config/src/skills_config.rs源码 ↗
configconfig load

这个文件像是在给“技能配置”做一张固定表格。没有这张表格,配置文件里写的技能开关、技能路径、技能名字就可能各写各的,程序也不知道该按什么规则理解。这里主要有三层:单个技能的 SkillConfig,可以用路径或名字选中一个技能,并写明 enabled 是否启用;整体的 SkillsConfig,装着内置技能设置、是否自动加入技能说明、以及一组具体技能配置;BundledSkillsConfig 则专门表示“随程序一起带来的内置技能”是否打开。它还支持序列化和反序列化,也就是能在程序数据和配置文件格式之间来回转换,并生成 JSON Schema(一份机器可读的配置说明书),帮助检查配置写得对不对。

函数细节3
default_enabled8–10 ↗
fn default_enabled() -> bool

作用:这个小函数给“是否启用”提供默认值:默认是打开。这样用户没写这个开关时,内置技能不会莫名其妙被关掉。

数据流:它不需要任何输入,也不读取外部信息;被调用时直接给出 true;结果就是相关配置字段在缺省情况下会被当作“启用”。

调用关系:它主要服务于 BundledSkillsConfig 里的 enabled 字段。当配置文件没有明确写 enabled 时,反序列化过程会用它来补上默认值。

BundledSkillsConfig::default46–48 ↗
fn default() -> Self

作用:这个函数说明 BundledSkillsConfig 的默认状态是什么:内置技能默认启用。有人创建默认配置时,会靠它得到一个合理的初始值。

数据流:进去不需要参数;它创建一个 BundledSkillsConfig,并把 enabled 设为 true;出来的是一个可直接使用的默认内置技能配置。

调用关系:它是 Rust 里 Default 默认值机制的一部分。凡是代码需要一个默认的 BundledSkillsConfig,而不是从配置文件细读出来时,就会走到这里。

SkillsConfig::try_from54–56 ↗
fn try_from(value: toml::Value) -> Result<Self, Self::Error>

作用:这个函数把 TOML 配置值转换成程序能用的 SkillsConfig。TOML 是常见的人类可读配置格式,比如 key = value 这种写法。

数据流:输入是一段已经被读成 toml::Value 的配置数据;它把这份数据交给反序列化 deserialize,也就是按 SkillsConfig 的结构去解析和检查;成功时输出 SkillsConfig,失败时输出 TOML 解析错误,告诉调用方配置不符合要求。

调用关系:它站在“配置文件内容”和“程序内部配置对象”之间。外部加载配置后,可以调用它完成转换;它自己不手写解析规则,而是把具体解析工作交给 serde 的 deserialize。

调用图:外部调用 1 个(deserialize)。

ext/skills/src/config.rs源码 ↗
configconfig load / cross-cutting

这个文件很小,但作用很直接:它像一张设置表,告诉 skills extension(技能扩展,也就是给系统增加额外能力的一块功能)该怎么工作。SkillsExtensionConfig 里有两个布尔值开关,布尔值就是“是/否”。include_instructions 决定是否把可用技能目录放进模型能看到的上下文里;如果打开,模型更容易知道自己能用哪些技能。bundled_skills_enabled 决定内置技能是否可以被发现;如果关闭,就算程序里带着这些技能,也不会拿出来用。没有这个配置结构,宿主程序就很难用统一、清楚的方式控制技能扩展的行为。

skills/src/lib.rs源码 ↗
io_transportstartup

这个文件解决的是“内置资源怎么落到用户电脑上”的问题。系统技能本来被打包在程序里,像随身带着的一份说明书和脚本包;但很多运行流程需要它们以普通文件夹的形式存在,所以这里会把它们写到 CODEX_HOME/skills/.system。为了不每次启动都删了重写,它会先计算内置技能目录的“指纹”(可以理解成内容身份证),再读磁盘上的标记文件。如果标记吻合,就说明磁盘上的版本已经是最新的,直接跳过。否则它会清空旧目录,重新写入所有内置文件,并保存新的标记。文件里的 SystemSkillsError 用来把读写文件时的失败包装成更清楚的错误,比如“创建目录失败”或“写技能文件失败”。

函数细节8
system_cache_root_dir18–22 ↗
fn system_cache_root_dir(codex_home: &AbsolutePathBuf) -> AbsolutePathBuf

作用:这个函数算出系统技能应该放在哪个文件夹里。别人只要给它 CODEX_HOME,它就能拼出 CODEX_HOME/skills/.system 这个固定位置。

数据流:进去的是一个绝对路径形式的 CODEX_HOME → 它在后面依次接上 skills 和 .system 两段目录名 → 出来的是系统技能缓存目录的完整路径,不会实际创建文件夹。

调用关系:install_system_skills 在真正安装前会先调用它,弄清楚目标目录在哪里;它自己只做路径拼接,把后续的磁盘读写工作留给安装流程。

调用图:调用 1 个内部函数(join);被 1 处调用(install_system_skills)。

install_system_skills32–56 ↗
fn install_system_skills(codex_home: &AbsolutePathBuf) -> Result<(), SystemSkillsError>

作用:这是安装系统技能的主入口。它确保用户目录里有一份最新的内置技能文件,如果已经是最新的,就什么都不做,节省启动时间。

数据流:进去的是 CODEX_HOME → 它先创建 skills 根目录,再算出 .system 目录位置,计算当前内置技能的指纹,并尝试读取已有标记 → 如果标记匹配就直接成功返回;如果不匹配,就删除旧的 .system,重新写入内置目录,最后写入新的标记文件 → 出来是成功结果,或者带有具体动作说明的错误。

调用关系:它是这个文件的总调度者:调用 system_cache_root_dir 找位置,调用 embedded_system_skills_fingerprint 算版本指纹,调用 read_marker 看磁盘版本,必要时调用 write_embedded_dir 真正落盘。外部代码通常只需要调用它,不必关心里面的细节。

调用图:调用 5 个内部函数(embedded_system_skills_fingerprint, read_marker, system_cache_root_dir, write_embedded_dir, join);外部调用 4 个(format!, create_dir_all, remove_dir_all, write)。

read_marker58–63 ↗
fn read_marker(path: &AbsolutePathBuf) -> Result<String, SystemSkillsError>

作用:这个函数读取系统技能目录里的标记文件。标记文件里存的是上次安装时的内容指纹,用来判断要不要重新安装。

数据流:进去的是标记文件路径 → 它把文件内容读成字符串,去掉前后的空白和换行 → 出来的是干净的指纹字符串;如果读文件失败,就返回说明“读取系统技能标记失败”的错误。

调用关系:install_system_skills 会在决定是否跳过安装时调用它。它只负责读标记,不判断对不对;比较工作由 install_system_skills 完成。

调用图:调用 1 个内部函数(as_path);被 1 处调用(install_system_skills);外部调用 1 个(read_to_string)。

embedded_system_skills_fingerprint65–77 ↗
fn embedded_system_skills_fingerprint() -> String

作用:这个函数给程序内置的系统技能目录算一个“指纹”。只要里面的文件名、目录名或文件内容变了,算出来的值通常也会变。

数据流:它不接收外部输入,而是读取编译进程序里的系统技能目录 → 先让 collect_fingerprint_items 收集所有目录和文件的信息,再排序,保证顺序稳定,然后把版本盐值、路径和内容哈希一起喂给哈希器 → 出来的是一个十六进制字符串,用作当前内置技能版本的身份证。

调用关系:install_system_skills 用它来得到“应该安装的版本号”。它把遍历目录的细活交给 collect_fingerprint_items,自己负责把这些信息合成最终指纹。

调用图:调用 1 个内部函数(collect_fingerprint_items);被 1 处调用(install_system_skills);外部调用 3 个(new, new, format!)。

collect_fingerprint_items79–96 ↗
fn collect_fingerprint_items(dir: &Dir<'_>, items: &mut Vec<(String, Option<u64>)>)

作用:这个函数递归走完整个内置技能目录,收集每个目录和文件的身份信息。它是计算指纹前的“清点清单”步骤。

数据流:进去的是一个内置目录和一个可追加的列表 → 它逐个查看目录项:遇到子目录就记录路径并继续往里走,遇到文件就对文件内容算一个小哈希并记录路径和内容哈希 → 出来时,传入的列表被填满了所有目录和文件的记录。

调用关系:embedded_system_skills_fingerprint 依赖它提供完整清单;测试函数 tests::fingerprint_traverses_nested_entries 也会调用它,确认它真的能走到嵌套目录里的文件。

调用图:被 2 处调用(embedded_system_skills_fingerprint, fingerprint_traverses_nested_entries);外部调用 2 个(new, entries)。

write_embedded_dir101–128 ↗
fn write_embedded_dir(dir: &Dir<'_>, dest: &AbsolutePathBuf) -> Result<(), SystemSkillsError>

作用:这个函数把程序里打包好的内置目录写到真实磁盘上。它会保留原来的文件夹结构,就像把压缩包展开到目标目录。

数据流:进去的是一个内置目录和目标磁盘目录 → 它先确保目标目录存在,然后遍历里面的每个目录和文件;遇到目录就创建对应文件夹并继续处理,遇到文件就确保父目录存在,再把文件内容写进去 → 出来是写入完成的成功结果,或者某一步文件操作失败的错误。

调用关系:install_system_skills 在确认需要重新安装时调用它。它是实际干体力活的部分,负责创建目录和写文件;错误会通过 SystemSkillsError::io 包装后交回上层。

调用图:调用 2 个内部函数(as_path, join);被 1 处调用(install_system_skills);外部调用 3 个(entries, create_dir_all, write)。

SystemSkillsError::io141–143 ↗
fn io(action: &'static str, source: std::io::Error) -> Self

作用:这个小函数把普通的文件读写错误包装成 SystemSkillsError,并附上当时正在做什么。这样报错时不会只看到“失败了”,还能知道是在创建目录、删除目录还是写文件时失败。

数据流:进去的是一段固定的动作说明和原始的系统 IO 错误,也就是读写磁盘时产生的错误 → 它把两者放进 SystemSkillsError::Io 这个错误值里 → 出来的是更有上下文的错误对象。

调用关系:这个文件里所有磁盘操作失败时都会用它来生成错误。它不主动做读写,只负责把底层错误翻译成上层更好懂的错误。

tests::fingerprint_traverses_nested_entries152–168 ↗
fn fingerprint_traverses_nested_entries()

作用:这是一个测试,确认收集指纹信息时不会只看第一层目录,而是能进入更深的子目录。没有这个保证,某些深层技能脚本变了可能不会被发现。

数据流:进去的是测试环境中同一份内置系统技能目录 → 它调用 collect_fingerprint_items 收集路径,把路径排序,然后检查两个嵌套文件路径确实在清单里 → 出来是测试通过;如果找不到这些路径,测试会失败。

调用关系:它专门验证 collect_fingerprint_items 的递归行为。虽然运行时不会调用这个测试,但它帮助开发者防止以后改代码时破坏指纹计算。

调用图:调用 1 个内部函数(collect_fingerprint_items);外部调用 2 个(new, assert!)。

core-skills/src/system.rs源码 ↗
orchestrationsetup/teardown

这里的“系统技能”可以理解成程序自带的一组能力包,可能会被安装到一个缓存目录里,方便运行时使用。这个文件一方面把外部库里的安装函数和缓存目录函数转手提供给本模块内部用,另一方面补了一个卸载函数。卸载的做法很直接:先根据 codex_home,也就是这个程序的主目录,算出系统技能缓存目录;然后把整个目录删掉。它像是一个清理按钮:不关心里面具体有哪些文件,只要是系统技能缓存,就整包移走。一个重要细节是,删除失败会被忽略,比如目录本来就不存在,也不会报错打断流程。这适合“尽力清理”的场景,但也意味着调用方不会从这里知道到底有没有删成功。

函数细节1
uninstall_system_skills6–8 ↗
fn uninstall_system_skills(codex_home: &AbsolutePathBuf)

作用:这个函数用来卸载或清理已经缓存到本地的系统技能。有人会在需要重建、限制或重置系统技能环境时调用它,让旧缓存先消失。

数据流:进去的是 codex_home,也就是程序用来存放自身数据的主目录路径。函数先用 system_cache_root_dir 算出系统技能缓存文件夹在哪里,然后调用 remove_dir_all 把这个文件夹连同里面的内容一起删除。出来没有返回值;它对磁盘做了改动,但删除结果被丢弃,所以失败也不会通知调用者。

调用关系:它被 new_with_restriction_product 调用,通常是在创建某种受限制的产品环境时,先把系统技能缓存清掉,避免旧内容影响新环境。它自己不负责判断该不该删,只负责把目录位置算出来,并把真正的删除动作交给文件系统函数 remove_dir_all。

调用图:被 1 处调用(new_with_restriction_product);外部调用 2 个(system_cache_root_dir, remove_dir_all)。

memories/write/src/extensions/ad_hoc.rs源码 ↗
io_transportstartup / extension initialization

这个文件解决的是“新扩展一开始没有说明文件”的问题。它把项目里预先打包好的说明文字,也就是 INSTRUCTIONS,写到用户的 memory 目录下面的 extensions/ad_hoc/instructions.md。流程很简单:先根据传进来的 memory_root 找到扩展目录;再确保这个目录存在;然后尝试新建 instructions.md。这里有个重要细节:它用的是“只在文件不存在时创建”的方式,所以如果用户已经改过这份说明,它不会覆盖,避免把人的配置或笔记冲掉。可以把它想成酒店房间第一次入住前放一本说明册,但如果客人已经自己写了备注,服务员不会把它扔掉重放一本新的。

函数细节1
seed_instructions8–26 ↗
async fn seed_instructions(memory_root: &Path) -> std::io::Result<()>

作用:这个函数负责把 ad_hoc 扩展的默认说明文件放到指定的记忆目录里。它通常在初始化扩展时使用,确保新环境里有一份可读的 instructions.md,但不会覆盖已经存在的文件。

数据流:输入是一个 memory_root 路径,也就是记忆系统的根目录。函数先通过 memory_extensions_root 找到扩展总目录,再拼出 ad_hoc 子目录和 instructions.md 文件路径;接着创建目录;然后尝试新建文件并写入内置模板 INSTRUCTIONS。成功时返回空结果;如果文件已经存在,也当作成功;如果遇到其他磁盘错误,就把错误返回给调用者。

调用关系:它是扩展初始化流程中的一个小步骤,由 seed_extension_instructions 调用。它自己不决定什么时候初始化,而是在被叫到时完成落盘写文件的工作;它会借助 memory_extensions_root 找到正确位置,并使用 tokio 的异步文件操作去创建目录、打开文件和写入内容。

调用图:被 1 处调用(seed_extension_instructions);外部调用 3 个(memory_extensions_root, new, create_dir_all)。

memories/write/src/extensions/mod.rs源码 ↗
orchestrationstartup

这个文件本身不做复杂工作,更像一个前台接待台:外面的人不用知道扩展功能分别藏在哪个房间,只要从这里进来就行。它声明了两个内部模块:ad_hoc 和 prune。ad_hoc 里有给扩展写入初始说明的动作,prune 里有清理旧扩展资源的动作。这里的 seed_extension_instructions 会在给定的记忆根目录下准备扩展说明;实际活儿交给 ad_hoc::seed_instructions 去做。最后一行把 prune_old_extension_resources 重新导出,意思是别的代码可以从 extensions 这个总入口直接拿到清理功能,而不用关心它在 prune 文件里。

函数细节1
seed_extension_instructions6–8 ↗
async fn seed_extension_instructions(memory_root: &Path) -> std::io::Result<()>

作用:这个函数用于在记忆系统启动时,给扩展功能准备好默认说明文件或说明内容。调用者只需要给它一个记忆根目录,它会把具体初始化工作转交给 ad_hoc 模块。

数据流:进去的是 memory_root,也就是记忆数据存放的根目录路径。函数没有自己加工太多,而是把这个路径原样交给 seed_instructions,并等待它完成。出来的是一个 std::io::Result,表示磁盘读写这类操作是成功了,还是出了文件系统错误。

调用关系:它是扩展初始化的统一入口。启动任务 start_memories_startup_task 会在系统启动阶段调用它,测试 run_memory_phase_two_model_request_test 也会用它来准备测试环境。它自己不直接写文件,而是把真正的准备工作交给 seed_instructions。

调用图:调用 1 个内部函数(seed_instructions);被 2 处调用(start_memories_startup_task, run_memory_phase_two_model_request_test)。

core-skills/src/config_rules.rs源码 ↗
configconfig load / plugin loading

这个文件解决的是“技能开关到底听谁的”这个问题。项目里的技能可以按名字选,也可以按文件路径选;配置还可能来自用户配置、临时会话参数等不同来源。这里会按优先级从低到高读取这些配置,只保留真正能影响技能的层。每条配置会先被转成一个选择器:要么是名字,要么是路径;如果写得含糊,比如同时写了名字和路径,文件会直接警告并忽略。之后它保留规则的顺序,因为后面的规则可以覆盖前面的规则,就像后来贴上的便签盖住了旧便签。最后,给定已经加载到的技能清单,它会把“按名字禁用”和“按路径禁用”统一算成一组实际文件路径,告诉后续加载流程哪些技能不要用。

函数细节3
skill_config_rules_from_stack30–69 ↗
fn skill_config_rules_from_stack(config_layer_stack: &ConfigLayerStack) -> SkillConfigRules

作用:这个函数从一叠配置里挑出和 skills 有关的开关规则,并整理成统一格式。它有用的地方在于:后面的代码不用再关心这些规则来自哪个配置文件、哪个会话参数,只看整理好的结果就行。

数据流:进去的是一个 ConfigLayerStack,也就是多层配置叠在一起的结构。函数按“低优先级先看,高优先级后看”的顺序读取配置,只接受用户配置和会话参数里的 skills 项;然后把每个技能配置交给 skill_config_rule_selector 转成“按名字选”或“按路径选”。如果同一个选择器之前出现过,它会先删掉旧规则,再放入新规则,这样后来的配置就能覆盖前面的配置。出来的是 SkillConfigRules,里面是一串已经排好顺序、可直接使用的启用/禁用规则;遇到无效配置时不会崩溃,只会记录警告并跳过。

调用关系:它是技能配置进入系统后的整理入口。load_plugins_from_layer_stack、plugins_for_config_with_force_reload、skills_for_config、skills_for_cwd 等加载技能或插件的流程会先调用它,拿到干净的规则表。它自己会把每条原始配置交给 skill_config_rule_selector 判断该按名字还是路径匹配。

调用图:调用 2 个内部函数(get_layers, skill_config_rule_selector);被 9 处调用(load_plugins_from_layer_stack, plugins_for_config_with_force_reload, read_plugin_detail_for_marketplace_plugin, skills_for_config, skills_for_cwd, disabled_paths_for_skills_allows_name_selector_to_override_path_selector, disabled_paths_for_skills_allows_session_flags_to_disable_user_enabled_skill, disabled_paths_for_skills_allows_session_flags_to_override_user_layer, disabled_paths_for_skills_disables_matching_name_selectors);外部调用 3 个(new, matches!, warn!)。

resolve_disabled_skill_paths71–103 ↗
fn resolve_disabled_skill_paths(
    skills: &[SkillMetadata],
    rules: &SkillConfigRules,
) -> HashSet<AbsolutePathBuf>

作用:这个函数把抽象的禁用规则,真正换算成一组“要禁用的技能文件路径”。有人会用它,是因为后续加载技能时最终需要知道的是哪些具体文件不能启用,而不是一堆名字或配置项。

数据流:进去的是已经发现的技能列表,以及前一步整理出的 SkillConfigRules。函数从空的禁用路径集合开始,按规则顺序一条条执行:如果规则按路径选,就直接把这个路径加入或移出禁用集合;如果规则按名字选,就先在技能列表里找到同名技能,再把这些技能对应的文件路径加入或移出禁用集合。出来的是一个 HashSet,也就是不重复的路径集合,表示最终被禁用的技能文件;如果某条后来的规则重新启用了技能,它也会把路径从集合里移走。

调用关系:它位于“配置已经读完”和“真正决定加载哪些技能”之间。load_plugin_skills、build_skill_outcome 会调用它,把规则落到具体技能文件上。它不再读取配置,也不判断配置是否合法,只负责把已有规则应用到已发现的技能清单上。

调用图:被 2 处调用(load_plugin_skills, build_skill_outcome);外部调用 2 个(new, iter)。

skill_config_rule_selector105–128 ↗
fn skill_config_rule_selector(entry: &SkillConfig) -> Option<SkillConfigRuleSelector>

作用:这个小函数负责看懂单条技能配置到底是在指哪个技能:是按文件路径指,还是按技能名字指。它也负责挡住写错的配置,避免错误规则悄悄生效。

数据流:进去的是一条 SkillConfig。函数检查里面有没有 path 和 name:只有 path 时,会把路径尽量转成规范路径,方便后面准确比较;只有 name 时,会去掉前后空格,空名字会被警告并忽略;如果 path 和 name 同时存在,或者两者都没有,也会警告并返回空结果。出来的是一个可用的 SkillConfigRuleSelector,或者 None,表示这条配置不能用。

调用关系:它是 skill_config_rules_from_stack 的助手。主函数在读取每条 skills.config 配置时都会叫它来做“选择器翻译”。它不处理启用还是禁用,只回答“这条规则要匹配哪个技能”。

调用图:被 1 处调用(skill_config_rules_from_stack);外部调用 3 个(Name, Path, warn!)。

core-skills/src/loader.rs源码 ↗
orchestrationconfig load / startup / skill discovery

可以把这个文件想成“技能图书管理员”。它先根据配置、当前项目目录、用户目录、插件目录等地方,算出应该去哪些书架找技能。然后它在这些目录里往下扫描,专门寻找名叫 SKILL.md 的文件。找到后,它会读取文件开头用 --- 包起来的 YAML 信息(YAML 是一种人写起来比较像清单的配置格式),拿到技能名、描述等内容;如果旁边还有 agents/openai.yaml,它还会读取图标、默认提示词、依赖工具和使用策略。它也会做安全和质量检查,比如限制名字长度、忽略隐藏目录、防止图标路径乱跑到不该访问的地方、避免目录扫描太深或太多。最后输出一个 SkillLoadOutcome,也就是“加载结果”:里面有成功加载的技能、出错信息、每个技能来自哪个根目录和文件系统。

函数细节30
SkillParseError::fmt138–150 ↗
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result

作用:把技能文件解析失败的原因,转换成普通人能读懂的一句话错误信息。这样上层展示错误时,不会只看到难懂的内部错误类型。

数据流:进去的是一个具体错误,比如读文件失败、缺少 YAML 开头信息、字段太长等;它按错误种类挑一句说明文字;出来的是写进格式化器里的错误消息文本。

调用关系:它是 SkillParseError 这个错误类型的显示出口。parse_skill_file 等地方产生错误后,上层把错误转成字符串时,就会走到这里。

调用图:外部调用 1 个(write!)。

load_skills_from_roots163–233 ↗
async fn load_skills_from_roots(roots: I) -> SkillLoadOutcome

作用:从一批已经确定好的技能根目录里加载所有技能,并把重复项去掉、排序、记录来源。调用者用它拿到最终可用的技能列表。

数据流:进去的是多个 SkillRoot,每个包含目录、范围和文件系统;它先把目录路径标准化,再逐个调用 discover_skills_under_root 扫描技能,记录技能来自哪个根目录和哪个文件系统,最后去重并按范围、名称、路径排序;出来的是 SkillLoadOutcome,里面有技能、错误、根目录映射和文件系统映射。

调用关系:它是实际加载流程的总入口之一。load_plugin_skills、build_skill_outcome、list 等会调用它;它把具体扫描工作交给 discover_skills_under_root,把路径统一交给 canonicalize_for_skill_identity。

调用图:调用 3 个内部函数(canonicalize_for_skill_identity, discover_skills_under_root, new);被 4 处调用(load_plugin_skills, build_skill_outcome, list, skill_loading_and_reads_use_the_supplied_executor_file_system);外部调用 5 个(new, new, new, new, default)。

skill_roots235–253 ↗
async fn skill_roots(
    fs: Option<Arc<dyn ExecutorFileSystem>>,
    config_layer_stack: &ConfigLayerStack,
    cwd: &AbsolutePathBuf,
    plugin_skill_roots: Vec<PluginSkillRoot>,
    extra_skill_r

作用:根据配置和当前工作目录,算出系统应该去哪些地方找技能。它是“先找书架,再找书”的第一步。

数据流:进去的是可选文件系统、配置层、当前目录、插件技能目录和额外目录;它读取用户 home 目录,再把这些信息交给 skill_roots_with_home_dir;出来的是一组 SkillRoot。

调用关系:skill_roots_for_config 和 skills_for_cwd 会调用它。它本身不扫描技能文件,只负责准备根目录,并把主要工作交给 skill_roots_with_home_dir。

调用图:调用 1 个内部函数(skill_roots_with_home_dir);被 2 处调用(skill_roots_for_config, skills_for_cwd);外部调用 1 个(home_dir)。

skill_roots_with_home_dir255–281 ↗
async fn skill_roots_with_home_dir(
    fs: Option<Arc<dyn ExecutorFileSystem>>,
    config_layer_stack: &ConfigLayerStack,
    cwd: &AbsolutePathBuf,
    home_dir: Option<&AbsolutePathBuf>,
    plugi

作用:把所有可能的技能来源合并成一张根目录清单,包括配置里的目录、插件目录、额外传入目录和项目里的 .agents/skills。

数据流:进去的是配置、当前目录、home 目录、插件根目录和额外根目录;它先从配置层生成根目录,再追加插件和额外目录,再查找仓库里的技能目录,最后按路径去重;出来的是不会重复的 SkillRoot 列表。

调用关系:skill_roots 和测试辅助 skill_roots_from_layer_stack 都会调用它。它向下调用 skill_roots_from_layer_stack_inner、repo_agents_skill_roots 和 dedupe_skill_roots_by_path。

调用图:调用 3 个内部函数(dedupe_skill_roots_by_path, repo_agents_skill_roots, skill_roots_from_layer_stack_inner);被 2 处调用(skill_roots, skill_roots_from_layer_stack)。

skill_roots_from_layer_stack_inner283–362 ↗
fn skill_roots_from_layer_stack_inner(
    config_layer_stack: &ConfigLayerStack,
    home_dir: Option<&AbsolutePathBuf>,
    repo_fs: Option<Arc<dyn ExecutorFileSystem>>,
) -> Vec<SkillRoot>

作用:从配置层里提取技能目录。不同配置来源会变成不同范围的技能,比如项目技能、用户技能、系统技能或管理员技能。

数据流:进去的是配置层栈、home 目录和仓库文件系统;它按优先级遍历配置层,看每层的配置文件夹在哪里,并按来源拼出 skills 目录;出来的是一批 SkillRoot。

调用关系:它被 skill_roots_with_home_dir 调用,是根目录收集流程里的“读配置”部分。它还会用 system_cache_root_dir 找系统内置技能缓存目录。

调用图:调用 1 个内部函数(get_layers);被 1 处调用(skill_roots_with_home_dir);外部调用 3 个(clone, new, system_cache_root_dir)。

repo_agents_skill_roots364–398 ↗
async fn repo_agents_skill_roots(
    fs: Option<Arc<dyn ExecutorFileSystem>>,
    config_layer_stack: &ConfigLayerStack,
    cwd: &AbsolutePathBuf,
) -> Vec<SkillRoot>

作用:查找当前项目里从项目根到当前目录之间的 .agents/skills 目录。这样子目录可以有更贴近当前位置的项目技能。

数据流:进去的是仓库文件系统、配置层和当前目录;它先确定项目根标记,再找到项目根,然后列出项目根到当前目录之间的每一级目录,检查里面有没有 .agents/skills;出来的是存在且确实是目录的 SkillRoot 列表。

调用关系:它被 skill_roots_with_home_dir 调用。它会先问 project_root_markers_from_stack 要项目根标记,再用 find_project_root 找根目录,用 dirs_between_project_root_and_cwd 列出路径范围。

调用图:调用 4 个内部函数(dirs_between_project_root_and_cwd, find_project_root, project_root_markers_from_stack, from_abs_path);被 1 处调用(skill_roots_with_home_dir);外部调用 3 个(clone, new, warn!)。

project_root_markers_from_stack400–420 ↗
fn project_root_markers_from_stack(config_layer_stack: &ConfigLayerStack) -> Vec<String>

作用:决定用哪些文件或目录名来识别“项目根目录”,比如类似 .git 这样的标记。

数据流:进去的是配置层栈;它把非项目层的配置合并起来,读取 project_root_markers,如果配置无效就警告并使用默认标记;出来的是一组标记名称字符串。

调用关系:repo_agents_skill_roots 在找项目根之前会调用它。它依赖配置合并和解析函数,把分散配置变成一个明确的查找规则。

调用图:调用 1 个内部函数(get_layers);被 1 处调用(repo_agents_skill_roots);外部调用 7 个(Table, default_project_root_markers, merge_toml_values, project_root_markers_from_config, matches!, new, warn!)。

find_project_root422–449 ↗
async fn find_project_root(
    fs: &dyn ExecutorFileSystem,
    cwd: &AbsolutePathBuf,
    project_root_markers: &[String],
) -> AbsolutePathBuf

作用:从当前目录一路往上找,找到第一个带有项目根标记的目录。找不到时,就把当前目录当作根。

数据流:进去的是文件系统、当前目录和项目根标记列表;它遍历当前目录的每个父目录,检查标记路径是否存在;出来的是找到的项目根目录,或者当前目录的副本。

调用关系:repo_agents_skill_roots 调用它来圈定查找 .agents/skills 的范围。它通过文件系统的 get_metadata 做存在性检查。

调用图:调用 2 个内部函数(ancestors, from_abs_path);被 1 处调用(repo_agents_skill_roots);外部调用 3 个(get_metadata, warn!, clone)。

dirs_between_project_root_and_cwd451–470 ↗
fn dirs_between_project_root_and_cwd(
    cwd: &AbsolutePathBuf,
    project_root: &AbsolutePathBuf,
) -> Vec<AbsolutePathBuf>

作用:列出从项目根到当前目录之间的所有目录,方便逐层检查有没有本地技能目录。

数据流:进去的是当前目录和项目根目录;它从当前目录往上收集父目录,直到项目根为止,再反转顺序;出来的是从上到下排列的目录列表。

调用关系:repo_agents_skill_roots 调用它。它不碰文件系统,只做路径拆分和排序。

调用图:调用 1 个内部函数(ancestors);被 1 处调用(repo_agents_skill_roots)。

dedupe_skill_roots_by_path472–475 ↗
fn dedupe_skill_roots_by_path(roots: &mut Vec<SkillRoot>)

作用:删除路径重复的技能根目录,避免同一个目录被扫描两遍。

数据流:进去的是可修改的 SkillRoot 列表;它用一个集合记住见过的路径,只保留第一次出现的;出来时原列表被原地改成去重后的版本。

调用关系:skill_roots_with_home_dir 在合并所有来源后调用它,保证后续 load_skills_from_roots 不做多余扫描。

调用图:被 1 处调用(skill_roots_with_home_dir);外部调用 1 个(new)。

canonicalize_for_skill_identity477–486 ↗
async fn canonicalize_for_skill_identity(
    fs: &dyn ExecutorFileSystem,
    path: &AbsolutePathBuf,
) -> AbsolutePathBuf

作用:把路径尽量转换成系统认可的真实路径,用来判断两个路径是不是同一个技能。比如符号链接指向同一处时,这一步很重要。

数据流:进去的是文件系统和一个绝对路径;它请求文件系统做 canonicalize,也就是解析真实路径;如果成功就返回真实绝对路径,失败就退回原路径。

调用关系:load_skills_from_roots、discover_skills_under_root 和 parse_skill_file 都会调用它。它是去重、记录身份、处理符号链接时的共同小工具。

调用图:调用 1 个内部函数(from_abs_path);被 3 处调用(discover_skills_under_root, load_skills_from_roots, parse_skill_file);外部调用 1 个(canonicalize)。

discover_skills_under_root488–637 ↗
async fn discover_skills_under_root(
    fs: &dyn ExecutorFileSystem,
    root: &AbsolutePathBuf,
    scope: SkillScope,
    plugin_id: Option<&str>,
    plugin_root: Option<&AbsolutePathBuf>,
    out

作用:在某个技能根目录下面实际扫描文件,找到所有 SKILL.md,并尝试解析成技能。

数据流:进去的是文件系统、根目录、技能范围、插件信息和可修改的加载结果;它先确认根目录存在且是目录,然后用队列逐层扫描,跳过隐藏项,限制深度和目录数量,按规则处理符号链接,遇到 SKILL.md 就调用 parse_skill_file;出来时 outcome 里新增成功技能,非系统技能的解析错误也会被记录进去。

调用关系:load_skills_from_roots 调用它做核心扫描。它把单个文件解析交给 parse_skill_file,把路径标准化交给 canonicalize_for_skill_identity。

调用图:调用 3 个内部函数(canonicalize_for_skill_identity, parse_skill_file, from_abs_path);被 1 处调用(load_skills_from_roots);外部调用 8 个(new, from, error!, get_metadata, read_directory, matches!, warn!, clone)。

parse_skill_file639–705 ↗
async fn parse_skill_file(
    fs: &dyn ExecutorFileSystem,
    path: &AbsolutePathBuf,
    scope: SkillScope,
    plugin_id: Option<&str>,
    plugin_root: Option<&AbsolutePathBuf>,
) -> Result<Skill

作用:读取并解析单个 SKILL.md,把它变成程序内部的 SkillMetadata。它会判断这个技能文件是不是写得合格。

数据流:进去的是文件系统、SKILL.md 路径、技能范围和插件信息;它读取文件文本,提取 YAML frontmatter,解析名字和描述,补默认名字,加插件命名空间,读取可选的 openai.yaml 元数据,并检查长度;出来是 SkillMetadata,失败时返回 SkillParseError。

调用关系:discover_skills_under_root 找到 SKILL.md 后调用它。它再调用 extract_frontmatter、namespaced_skill_name、load_skill_metadata、validate_len 和 canonicalize_for_skill_identity。

调用图:调用 7 个内部函数(canonicalize_for_skill_identity, extract_frontmatter, load_skill_metadata, namespaced_skill_name, validate_len, read_file_text, from_abs_path);被 1 处调用(discover_skills_under_root);外部调用 1 个(from_str)。

default_skill_name707–717 ↗
fn default_skill_name(path: &AbsolutePathBuf) -> String

作用:当 SKILL.md 里没有写技能名时,用所在文件夹名当默认技能名。这样少写一个字段时仍然尽量可用。

数据流:进去的是 SKILL.md 的路径;它取父目录的文件夹名,清理成单行文本;如果拿不到或为空,就返回 skill;出来的是一个默认名称字符串。

调用关系:parse_skill_file 在 frontmatter 没有有效 name 时使用它。它是解析流程里的兜底命名规则。

调用图:调用 1 个内部函数(parent)。

namespaced_skill_name719–728 ↗
async fn namespaced_skill_name(
    fs: &dyn ExecutorFileSystem,
    path: &AbsolutePathBuf,
    base_name: &str,
) -> String

作用:给插件里的技能名加上插件命名空间,避免不同插件的技能重名。命名空间可以理解成“姓氏”。

数据流:进去的是文件系统、技能路径和基础名字;它询问 plugin_namespace_for_skill_path 是否能找到插件命名空间;如果有,就拼成 namespace:base_name,否则保持原名;出来的是最终技能名。

调用关系:parse_skill_file 调用它生成 SkillMetadata.name。它把插件识别交给外部的 plugin_namespace_for_skill_path。

调用图:被 1 处调用(parse_skill_file);外部调用 1 个(plugin_namespace_for_skill_path)。

load_skill_metadata730–799 ↗
async fn load_skill_metadata(
    fs: &dyn ExecutorFileSystem,
    skill_path: &AbsolutePathBuf,
    plugin_root: Option<&AbsolutePathBuf>,
) -> LoadedSkillMetadata

作用:读取 SKILL.md 旁边的 agents/openai.yaml,补充界面、依赖工具和策略等可选信息。这个文件坏了不会阻止技能加载。

数据流:进去的是文件系统、技能文件路径和插件根目录;它找到技能所在目录,检查 openai.yaml 是否存在并是文件,读取并解析 YAML,然后分别解析 interface、dependencies、policy;出来是 LoadedSkillMetadata,出错时返回空的默认元数据并记录警告。

调用关系:parse_skill_file 调用它。它向下调用 resolve_interface、resolve_dependencies 和 resolve_policy,把原始配置变成安全的内部结构。

调用图:调用 7 个内部函数(resolve_dependencies, resolve_interface, resolve_policy, read_file_text, parent, new, from_abs_path);被 1 处调用(parse_skill_file);外部调用 4 个(default, get_metadata, from_str, warn!)。

resolve_interface801–844 ↗
fn resolve_interface(
    interface: Option<Interface>,
    skill_dir: &AbsolutePathBuf,
    plugin_root: Option<&AbsolutePathBuf>,
) -> Option<SkillInterface>

作用:把界面展示相关配置整理成 SkillInterface,比如显示名、短描述、图标、品牌颜色和默认提示词。

数据流:进去的是可选 Interface、技能目录和插件根目录;它逐项清理字符串、校验颜色格式、解析图标路径,并丢掉无效字段;如果最后一个有效字段都没有,就返回 None,否则返回 SkillInterface。

调用关系:load_skill_metadata 在解析 openai.yaml 后调用它。它把字符串交给 resolve_str,把颜色交给 resolve_color_str,把图标路径交给 resolve_asset_path。

调用图:调用 3 个内部函数(resolve_asset_path, resolve_color_str, resolve_str);被 1 处调用(load_skill_metadata)。

resolve_dependencies846–858 ↗
fn resolve_dependencies(dependencies: Option<Dependencies>) -> Option<SkillDependencies>

作用:把技能声明的外部工具依赖整理成内部可用的依赖列表。没有有效工具时,就当作没有依赖。

数据流:进去的是可选 Dependencies;它遍历 tools,把每个工具交给 resolve_dependency_tool 校验和清理,过滤掉不合格的;出来是 SkillDependencies,或者 None。

调用关系:load_skill_metadata 调用它处理 openai.yaml 里的 dependencies 部分。它是依赖配置进入系统前的过滤器。

调用图:被 1 处调用(load_skill_metadata)。

resolve_policy860–865 ↗
fn resolve_policy(policy: Option<Policy>) -> Option<SkillPolicy>

作用:把技能的使用策略配置转成内部结构,比如是否允许隐式调用、适用于哪些产品。

数据流:进去的是可选 Policy;它把字段原样放进 SkillPolicy;如果没有策略配置,就返回 None。

调用关系:load_skill_metadata 调用它处理 openai.yaml 里的 policy 部分。它比较简单,主要负责类型转换。

调用图:被 1 处调用(load_skill_metadata)。

resolve_dependency_tool867–903 ↗
fn resolve_dependency_tool(tool: DependencyTool) -> Option<SkillToolDependency>

作用:校验并整理一个工具依赖条目。必须有类型和取值,否则这个工具依赖会被忽略。

数据流:进去的是一个 DependencyTool;它检查 type 和 value 是否存在且不超长,再清理 description、transport、command、url 等可选字段;出来是 SkillToolDependency,或者因为缺关键字段而返回 None。

调用关系:resolve_dependencies 在遍历工具列表时使用它。它把具体字段清理工作交给 resolve_required_str 和 resolve_str。

调用图:调用 2 个内部函数(resolve_required_str, resolve_str)。

resolve_asset_path905–952 ↗
fn resolve_asset_path(
    skill_dir: &AbsolutePathBuf,
    plugin_root: Option<&AbsolutePathBuf>,
    field: &'static str,
    path: Option<PathBuf>,
) -> Option<AbsolutePathBuf>

作用:检查图标路径是否安全,并把它变成绝对路径。它防止配置把图标指到技能目录外的随便位置。

数据流:进去的是技能目录、插件根目录、字段名和一个相对路径;它拒绝空路径和绝对路径,要求普通技能图标必须在 assets/ 下;如果路径里有 ..,只有插件技能在能解析到插件级 assets 目录下时才允许;出来是安全的绝对路径,或 None。

调用关系:resolve_interface 调用它解析 icon_small 和 icon_large。遇到插件共享资源的情况,它会把判断交给 resolve_plugin_shared_asset_path。

调用图:调用 2 个内部函数(resolve_plugin_shared_asset_path, join);被 1 处调用(resolve_interface);外部调用 2 个(new, warn!)。

resolve_plugin_shared_asset_path954–978 ↗
fn resolve_plugin_shared_asset_path(
    skill_dir: &AbsolutePathBuf,
    plugin_root: Option<&AbsolutePathBuf>,
    field: &'static str,
    path: &Path,
) -> Option<AbsolutePathBuf>

作用:专门处理插件技能引用插件公共 assets 目录里的图标。它允许有限度的 ..,但必须仍留在插件 assets 里。

数据流:进去的是技能目录、插件根目录、字段名和原始路径;它先把插件 assets 目录和目标路径做字面规范化,再检查目标是否位于插件 assets 下,最后转成绝对路径;出来是允许的绝对路径,或 None。

调用关系:resolve_asset_path 在发现图标路径包含 .. 时调用它。它依赖 lexically_normalize 来先把路径里的 . 和 .. 算清楚。

调用图:调用 3 个内部函数(lexically_normalize, join, try_from);被 1 处调用(resolve_asset_path);外部调用 1 个(warn!)。

lexically_normalize980–994 ↗
fn lexically_normalize(path: &Path) -> PathBuf

作用:只按路径文字规则清理 . 和 ..,不访问磁盘。可以理解成把“向上一级、当前目录”这些路标先算一遍。

数据流:进去的是一个路径;它逐个查看路径组件,跳过 .,遇到 .. 就弹出上一段,其他部分照常保留;出来的是清理后的 PathBuf。

调用关系:resolve_plugin_shared_asset_path 调用它来判断插件图标路径是否还在允许目录下。它不负责检查文件是否真的存在。

调用图:被 1 处调用(resolve_plugin_shared_asset_path);外部调用 2 个(components, new)。

sanitize_single_line996–998 ↗
fn sanitize_single_line(raw: &str) -> String

作用:把用户写的多空格、多换行文本压成干净的一行。这样名字和描述不会带奇怪换行。

数据流:进去的是原始字符串;它按空白字符切开,再用单个空格拼回去;出来的是一行规整文本。

调用关系:resolve_str 会调用它,parse_skill_file 中的名称和描述清理也依赖同样规则。它是字段清洗的基础小工具。

调用图:被 1 处调用(resolve_str)。

validate_len1000–1015 ↗
fn validate_len(
    value: &str,
    max_len: usize,
    field_name: &'static str,
) -> Result<(), SkillParseError>

作用:检查必填字段是否为空、是否超过最大长度。它用于让 SKILL.md 的核心信息保持可用和可展示。

数据流:进去的是字段值、最大字符数和字段名;它先拒绝空值,再按字符数检查长度;出来是 Ok,或者 MissingField / InvalidField 错误。

调用关系:parse_skill_file 在生成技能元数据前调用它。它负责硬性校验,失败会让这个 SKILL.md 加载失败。

调用图:被 1 处调用(parse_skill_file);外部调用 2 个(MissingField, format!)。

resolve_str1017–1029 ↗
fn resolve_str(value: Option<String>, max_len: usize, field: &'static str) -> Option<String>

作用:清理一个可选字符串字段,并在空值或太长时忽略它。它用于可选元数据,原则是“坏了就跳过,不影响整个技能”。

数据流:进去的是可选字符串、最大长度和字段名;它有值时先压成单行,再检查是否为空或过长;出来是清理后的字符串,或 None,并可能写警告日志。

调用关系:resolve_interface、resolve_dependency_tool 和 resolve_required_str 都会调用它。它是 openai.yaml 可选文本字段的统一清洗器。

调用图:调用 1 个内部函数(sanitize_single_line);被 3 处调用(resolve_dependency_tool, resolve_interface, resolve_required_str);外部调用 1 个(warn!)。

resolve_required_str1031–1041 ↗
fn resolve_required_str(
    value: Option<String>,
    max_len: usize,
    field: &'static str,
) -> Option<String>

作用:读取一个必须存在的字符串字段,缺失或不合格时返回 None。它常用于依赖工具里的关键字段。

数据流:进去的是可选字符串、最大长度和字段名;它先检查是否存在,不存在就警告;存在时交给 resolve_str 做清理和长度检查;出来是可用字符串或 None。

调用关系:resolve_dependency_tool 调用它检查 dependencies.tools.type 和 dependencies.tools.value。它让缺少关键字段的工具依赖被安全丢弃。

调用图:调用 1 个内部函数(resolve_str);被 1 处调用(resolve_dependency_tool);外部调用 1 个(warn!)。

resolve_color_str1043–1057 ↗
fn resolve_color_str(value: Option<String>, field: &'static str) -> Option<String>

作用:校验品牌颜色是不是标准的 #RRGGBB 格式。比如 #FF0000 表示红色。

数据流:进去的是可选颜色字符串和字段名;它去掉首尾空格,拒绝空值,并检查是否是井号加六位十六进制字符;出来是合法颜色字符串,或 None。

调用关系:resolve_interface 调用它处理 interface.brand_color。它只负责格式检查,不解释颜色含义。

调用图:被 1 处调用(resolve_interface);外部调用 1 个(warn!)。

extract_frontmatter1059–1080 ↗
fn extract_frontmatter(contents: &str) -> Option<String>

作用:从 SKILL.md 文件开头提取用 --- 包住的 YAML 配置块。没有这个块,技能就缺少机器可读的基本信息。

数据流:进去的是整个文件内容;它要求第一行是 ---,然后一直收集到下一个 ---,中间不能为空;出来是 YAML 文本字符串,或 None。

调用关系:parse_skill_file 读取 SKILL.md 后马上调用它。它提取出来的文本会交给 YAML 解析器变成 SkillFrontmatter。

调用图:被 1 处调用(parse_skill_file);外部调用 2 个(new, matches!)。

skill_roots_from_layer_stack1082–1097 ↗
async fn skill_roots_from_layer_stack(
    fs: Arc<dyn ExecutorFileSystem>,
    config_layer_stack: &ConfigLayerStack,
    cwd: &AbsolutePathBuf,
    home_dir: Option<&AbsolutePathBuf>,
) -> Vec<Skill

作用:这是测试专用入口,用来在测试里直接生成技能根目录列表。它方便验证配置层到技能目录的转换是否正确。

数据流:进去的是测试提供的文件系统、配置层、当前目录和 home 目录;它把插件目录和额外目录设为空,调用 skill_roots_with_home_dir;出来的是 SkillRoot 列表。

调用关系:它只在测试编译时存在。它复用正式的 skill_roots_with_home_dir,避免测试自己写一套不同逻辑。

调用图:调用 1 个内部函数(skill_roots_with_home_dir);外部调用 1 个(new)。

core-skills/src/manager.rs源码 ↗
orchestrationcross-cutting

这里的“技能”可以理解成给系统加能力的小插件。这个文件里的 SkillsManager 像一个前台经理:先看当前目录、用户配置、插件目录、额外指定目录,再决定从哪里找技能;然后调用真正的加载器把技能读出来;接着按产品限制和配置规则过滤,比如某些系统内置技能是否允许、某些路径是否被禁用。它还做缓存,也就是把上次结果先记在本子上,下次条件一样就不用重查。这里用了 RwLock(读写锁,一把防止多人同时改同一份缓存的锁)保护缓存和额外目录列表。一个重要细节是:按完整配置加载时,用“技能目录加配置规则”做缓存钥匙,避免两个在同一目录但配置不同的会话互相串味;按当前目录加载时,则在合适情况下用当前目录做缓存钥匙。

函数细节15
SkillsLoadInput::new37–49 ↗
fn new(
        cwd: AbsolutePathBuf,
        effective_skill_roots: Vec<PluginSkillRoot>,
        config_layer_stack: ConfigLayerStack,
        bundled_skills_enabled: bool,
    ) -> Self

作用:把加载技能所需的几样信息打包成一个输入对象,方便后面统一传给管理器。调用者不用一项项散着传,减少漏传或传错的机会。

数据流:进去的是当前工作目录、已经算好的插件技能根目录、配置层叠信息,以及“是否启用内置技能”的开关;它只是把这些值放进 SkillsLoadInput 这个包里;出来的是一个完整的加载请求说明,不会额外读文件或改状态。

调用关系:它通常在准备加载技能前被调用,比如线程配置注册、测试场景或从配置生成加载输入时。后续 SkillsManager::skills_for_config 和 SkillsManager::skills_for_cwd 会拿着这个包继续干活。

调用图:被 8 处调用(register_thread_config, set_extra_roots_replaces_runtime_roots_and_clears_cache, skills_for_config_ignores_cwd_cache_when_session_flags_reenable_skill, skills_for_config_with_stack, skills_for_cwd_loads_repo_and_user_roots_with_local_fs, skills_for_cwd_uses_cached_result_until_force_reload, skills_for_cwd_without_fs_skips_repo_roots, skills_load_input_from_config)。

SkillsManager::new61–63 ↗
fn new(codex_home: AbsolutePathBuf, bundled_skills_enabled: bool) -> Self

作用:用最常见的方式创建一个技能管理器,并默认把产品限制设为 Codex。外部一般用它来得到一个可以加载和缓存技能的管理对象。

数据流:进去的是 Codex 的主目录,以及是否启用随程序附带的内置技能;它把产品限制补成 Codex 后交给更通用的构造函数;出来的是初始化好的 SkillsManager。

调用关系:它是普通入口,自己不做复杂初始化,而是把工作交给 SkillsManager::new_with_restriction_product。测试和实际代码都会用它来启动技能管理这一套。

调用图:被 17 处调用(new_with_disabled_bundled_skills_removes_stale_cached_system_skills, set_extra_roots_applies_to_config_loads_and_empty_clears, set_extra_roots_replaces_runtime_roots_and_clears_cache, skills_for_config_disables_plugin_skills_by_name, skills_for_config_excludes_bundled_skills_when_disabled_in_config, skills_for_config_ignores_cwd_cache_when_session_flags_reenable_skill, skills_for_config_reuses_cache_for_same_effective_config, skills_for_cwd_loads_repo_and_user_roots_with_local_fs, skills_for_cwd_uses_cached_result_until_force_reload, skills_for_cwd_without_fs_skips_repo_roots (+7 more));外部调用 1 个(new_with_restriction_product)。

SkillsManager::new_with_restriction_product65–85 ↗
fn new_with_restriction_product(
        codex_home: AbsolutePathBuf,
        bundled_skills_enabled: bool,
        restriction_product: Option<Product>,
    ) -> Self

作用:创建技能管理器的完整版本,可以指定技能要按哪个产品来过滤。它还会在启动时安装或清理系统内置技能。

数据流:进去的是 Codex 主目录、内置技能开关、可选的产品限制;它建立额外目录列表和两套缓存,并根据开关安装 system skills,或尽力删除旧的系统技能缓存;出来的是可用的 SkillsManager。如果安装失败,只记录错误,不直接让创建失败。

调用关系:SkillsManager::new 会调用它。更底层的测试工具也可以直接调用它,以便指定不同产品限制。它会把安装和卸载系统技能的活交给 install_system_skills 和 uninstall_system_skills。

调用图:调用 1 个内部函数(uninstall_system_skills);被 2 处调用(new, with_models_provider_home_and_state_for_tests);外部调用 5 个(new, new, new, install_system_skills, error!)。

SkillsManager::set_extra_roots87–96 ↗
fn set_extra_roots(&self, extra_roots: Vec<AbsolutePathBuf>)

作用:替换运行时额外指定的技能目录。比如用户临时告诉系统“也去这些地方找技能”,就会用到它。

数据流:进去的是一组新的绝对路径;它用写锁安全地替换旧列表;之后清空缓存,因为技能来源变了,旧结果不再可信;出来没有返回值,但管理器内部的额外目录和缓存状态已经改变。

调用关系:它是调整运行时搜索范围的入口。替换目录后会调用 SkillsManager::clear_cache,避免后续加载还拿到旧目录算出来的技能结果。

调用图:调用 1 个内部函数(clear_cache)。

SkillsManager::skills_for_config105–124 ↗
async fn skills_for_config(
        &self,
        input: &SkillsLoadInput,
        fs: Option<Arc<dyn ExecutorFileSystem>>,
    ) -> SkillLoadOutcome

作用:按一个已经准备好的配置来加载技能,并且用更精确的缓存方式避免会话之间互相影响。适合配置已经确定、不想再重新读配置层的时候使用。

数据流:进去的是 SkillsLoadInput 和可选的文件系统接口;它先算出本次应该查哪些技能根目录,再从配置里提取启用/禁用规则,组成缓存钥匙;如果缓存里已有结果就直接返回,否则加载技能、过滤禁用项、保存缓存;出来的是 SkillLoadOutcome,也就是本次可用技能、禁用路径和隐式调用索引等结果。

调用关系:它会调用 SkillsManager::skill_roots_for_config 找目录,调用 skill_config_rules_from_stack 读规则,调用 config_skills_cache_key 做缓存钥匙,必要时调用 SkillsManager::build_skill_outcome 真正加载。它比按目录缓存更安全,因为同目录但配置不同的会话不会共用错结果。

调用图:调用 5 个内部函数(skill_config_rules_from_stack, build_skill_outcome, cached_outcome_for_config, skill_roots_for_config, config_skills_cache_key);被 1 处调用(skills_for_config_with_stack)。

SkillsManager::skill_roots_for_config126–143 ↗
async fn skill_roots_for_config(
        &self,
        input: &SkillsLoadInput,
        fs: Option<Arc<dyn ExecutorFileSystem>>,
    ) -> Vec<SkillRoot>

作用:根据当前配置算出应该从哪些“根目录”寻找技能。它只负责列出地点,不负责读取技能内容。

数据流:进去的是加载输入和可选文件系统接口;它把配置、当前目录、插件根目录、运行时额外根目录交给 skill_roots 计算;如果输入说不启用内置技能,就把系统范围的根目录删掉;出来是一组 SkillRoot。

调用关系:它被 SkillsManager::skills_for_config 使用,是“先找地点,再读技能”这条路的第一步。它会读取 SkillsManager::extra_roots 提供的运行时额外目录。

调用图:调用 2 个内部函数(skill_roots, extra_roots);被 1 处调用(skills_for_config)。

SkillsManager::skills_for_cwd145–180 ↗
async fn skills_for_cwd(
        &self,
        input: &SkillsLoadInput,
        force_reload: bool,
        fs: Option<Arc<dyn ExecutorFileSystem>>,
    ) -> SkillLoadOutcome

作用:按当前工作目录加载技能,适合“我在这个项目目录里,需要知道能用哪些技能”的场景。它可以按目录缓存,除非调用者要求强制重载。

数据流:进去的是加载输入、是否强制重载、可选文件系统接口;如果允许用目录缓存且不是强制重载,它先查缓存;没有命中时就重新计算技能根目录,按配置决定是否去掉系统内置技能,再加载和过滤技能;出来是 SkillLoadOutcome,并在可缓存时把结果存回按目录的缓存。

调用关系:它直接串起 skill_roots、bundled_skills_enabled_from_stack、skill_config_rules_from_stack 和 SkillsManager::build_skill_outcome。它会用 SkillsManager::cached_outcome_for_cwd 读旧结果,也会用 SkillsManager::extra_roots 加入额外目录。

调用图:调用 6 个内部函数(skill_config_rules_from_stack, skill_roots, build_skill_outcome, cached_outcome_for_cwd, extra_roots, bundled_skills_enabled_from_stack)。

SkillsManager::build_skill_outcome183–194 ↗
async fn build_skill_outcome(
        &self,
        roots: Vec<SkillRoot>,
        skill_config_rules: &SkillConfigRules,
    ) -> SkillLoadOutcome

作用:把“技能目录列表”变成最终可用的技能结果。它是加载流程里真正把原材料加工成成品的一步。

数据流:进去的是一组 SkillRoot 和技能配置规则;它先从这些目录读取技能,再按产品限制过滤不适合当前产品的技能,然后根据配置算出被禁用的路径,最后补齐隐式调用需要的索引;出来的是整理好的 SkillLoadOutcome。

调用关系:SkillsManager::skills_for_config 和 SkillsManager::skills_for_cwd 在缓存没命中时都会调用它。它把读取工作交给 load_skills_from_roots,把禁用规则交给 resolve_disabled_skill_paths,把最终收尾交给 finalize_skill_outcome。

调用图:调用 3 个内部函数(resolve_disabled_skill_paths, load_skills_from_roots, finalize_skill_outcome);被 2 处调用(skills_for_config, skills_for_cwd);外部调用 1 个(filter_skill_load_outcome_for_product)。

SkillsManager::clear_cache196–217 ↗
fn clear_cache(&self)

作用:清空所有已经记住的技能加载结果。只要技能来源或关键条件变了,就需要它来防止旧结果继续被使用。

数据流:进去没有业务输入;它分别拿到按目录缓存和按配置缓存的写锁,记录各自有多少条,然后清空;出来没有返回值,但缓存已被清空,并会写一条日志说明清了多少项。

调用关系:SkillsManager::set_extra_roots 会调用它,因为额外技能目录改变后,原来的缓存结果就不可靠了。它不重新加载技能,只负责把旧账本擦掉。

调用图:被 1 处调用(set_extra_roots);外部调用 1 个(info!)。

SkillsManager::cached_outcome_for_cwd219–224 ↗
fn cached_outcome_for_cwd(&self, cwd: &AbsolutePathBuf) -> Option<SkillLoadOutcome>

作用:按当前工作目录查找之前缓存过的技能结果。这样同一个目录下重复请求时,可以少做一次扫描和解析。

数据流:进去的是一个当前目录路径;它读取按目录缓存,如果找到对应结果就克隆一份返回;如果锁曾经因为线程异常而“中毒”,它也会取回内部数据继续读;出来是可选的 SkillLoadOutcome,可能有也可能没有。

调用关系:它只被 SkillsManager::skills_for_cwd 使用,位于加载前的快速通道。命中时,后面的找目录和读技能步骤都可以跳过。

调用图:被 1 处调用(skills_for_cwd)。

SkillsManager::cached_outcome_for_config226–234 ↗
fn cached_outcome_for_config(
        &self,
        cache_key: &ConfigSkillsCacheKey,
    ) -> Option<SkillLoadOutcome>

作用:按“技能目录加配置规则”这把更精确的钥匙查缓存。它用来避免不同配置的会话因为在同一目录而误用同一份结果。

数据流:进去的是 ConfigSkillsCacheKey;它读取按配置缓存,找到就克隆返回,找不到就返回空;如果读锁中毒,也会尽量取回数据继续读;出来是可选的 SkillLoadOutcome。

调用关系:它被 SkillsManager::skills_for_config 调用。这个流程先算缓存钥匙,再通过它看有没有现成结果;没有才会进入真正加载。

调用图:被 1 处调用(skills_for_config)。

SkillsManager::extra_roots236–241 ↗
fn extra_roots(&self) -> Vec<AbsolutePathBuf>

作用:安全地取出当前运行时额外技能目录列表。它给加载流程提供“除了配置里写的目录,还要去哪儿找”的补充信息。

数据流:进去没有参数;它读取 extra_roots 这份受锁保护的列表并复制一份;出来是一组绝对路径,调用者拿到的是副本,不会直接改到管理器内部状态。

调用关系:SkillsManager::skill_roots_for_config 和 SkillsManager::skills_for_cwd 会调用它,然后把这些额外目录交给 skill_roots 一起计算最终搜索范围。

调用图:被 2 处调用(skill_roots_for_config, skills_for_cwd)。

bundled_skills_enabled_from_stack250–270 ↗
fn bundled_skills_enabled_from_stack(
    config_layer_stack: &codex_config::ConfigLayerStack,
) -> bool

作用:从配置层叠结果里判断“随程序附带的内置技能”是否启用。配置没写或写坏时,它选择默认启用,以免因为配置解析失败把功能意外关掉。

数据流:进去的是 ConfigLayerStack,也就是多层配置合并后的来源;它取出最终生效配置里的 skills 表,尝试转换成 SkillsConfig;如果没有配置就返回 true,如果配置无效就记录警告并返回 true,如果有效就读 bundled.enabled;出来是一个布尔值,表示内置技能是否启用。

调用关系:SkillsManager::skills_for_cwd 会用它决定是否排除系统技能根目录。另一个名为 bundled_skills_enabled 的调用方也会用它作为配置判断工具。

调用图:调用 1 个内部函数(effective_config);被 2 处调用(skills_for_cwd, bundled_skills_enabled);外部调用 1 个(warn!)。

config_skills_cache_key272–291 ↗
fn config_skills_cache_key(
    roots: &[SkillRoot],
    skill_config_rules: &SkillConfigRules,
) -> ConfigSkillsCacheKey

作用:把本次技能加载的关键条件做成一把缓存钥匙。只有目录、目录身份和禁用规则都一样时,才会复用同一份结果。

数据流:进去的是技能根目录列表和技能配置规则;它把每个根目录转成路径、范围等级和插件编号,并复制配置规则;出来的是 ConfigSkillsCacheKey,可以放进哈希表里查缓存。

调用关系:它被 SkillsManager::skills_for_config 调用。这个缓存钥匙随后会交给 SkillsManager::cached_outcome_for_config 查询,或者作为新加载结果的保存位置。

调用图:被 1 处调用(skills_for_config);外部调用 2 个(clone, iter)。

finalize_skill_outcome293–303 ↗
fn finalize_skill_outcome(
    mut outcome: SkillLoadOutcome,
    disabled_paths: HashSet<AbsolutePathBuf>,
) -> SkillLoadOutcome

作用:给已经加载出的技能结果做最后整理,让它不仅知道哪些技能可用,也知道哪些路径禁用,以及哪些技能可以被自动触发。

数据流:进去的是一个初步的 SkillLoadOutcome 和一组被禁用路径;它先把禁用路径写进结果,再从允许隐式调用的技能中建立两张索引:按脚本目录找、按文档路径找;出来的是补全后的 SkillLoadOutcome。

调用关系:它被 SkillsManager::build_skill_outcome 作为最后一步调用。它会使用 build_implicit_skill_path_indexes 生成快速查找表,方便后续系统根据文件路径自动匹配技能。

调用图:调用 1 个内部函数(allowed_skills_for_implicit_invocation);被 1 处调用(build_skill_outcome);外部调用 2 个(new, build_implicit_skill_path_indexes)。

插件市场与 MCP 目录

这些文件解析市场根目录,描述插件加载结果,规范化插件 MCP 声明,并构建最终的运行时 MCP 服务器目录。

core-plugins/src/installed_marketplaces.rs源码 ↗
domain_logicconfig load

插件市场就像一个“应用商店目录”,里面会列出可安装的插件。这个文件做的事很具体:先规定默认安装位置是 Codex 主目录下面的 .tmp/marketplaces,再根据用户配置判断每个市场实际在哪里。如果用户写的是本地目录,就用用户给的路径;如果不是本地目录,就默认认为它在 .tmp/marketplaces/市场名 下面。它还会检查配置是不是表格、市场名字是不是安全合法的路径片段,并确认目录里真的有市场清单文件。写错的配置不会让程序崩掉,而是打警告并跳过。最后,它把找到的目录转成“绝对路径”(完整路径,不依赖当前所在目录),并排序,保证后续处理顺序稳定。

函数细节3
marketplace_install_root13–15 ↗
fn marketplace_install_root(codex_home: &Path) -> PathBuf

作用:算出系统默认用来存放已安装市场的目录。有人要安装、删除、查找市场时,都需要先知道这个“仓库根目录”在哪里。

数据流:进去的是 Codex 的主目录路径 → 它在后面拼上固定的 .tmp/marketplaces → 出来的是默认市场安装根目录;它不读文件,也不改任何东西,只是组装路径。

调用关系:这是很多流程都会先问的一句“默认放哪儿”。安装市场、写入市场、删除市场、查看配置快照,以及 installed_marketplace_roots_from_layer_stack 在解析配置时,都会用它得到统一的默认位置。

调用图:被 18 处调用(marketplace_remove_deletes_config_and_installed_root, write_installed_marketplace, configured_marketplace_sources_by_root, configured_marketplace_snapshot_issues, marketplace_add_local_directory_source, marketplace_remove_json_prints_remove_outcome, write_installed_marketplace, installed_marketplace_roots_from_layer_stack, list_marketplaces_ignores_installed_roots_missing_from_config, list_marketplaces_includes_installed_marketplace_roots (+8 more));外部调用 1 个(join)。

installed_marketplace_roots_from_layer_stack17–61 ↗
fn installed_marketplace_roots_from_layer_stack(
    config_layer_stack: &ConfigLayerStack,
    codex_home: &Path,
) -> Vec<AbsolutePathBuf>

作用:从用户配置里找出所有真正可用的已安装市场目录。它的作用像门卫:只放行格式正确、名字安全、并且目录里确实有市场清单的项目。

数据流:进去的是配置层叠对象和 Codex 主目录 → 它先取最终生效的用户配置,再找 marketplaces 这一块;如果没有或格式不对,就返回空列表或警告。接着它逐个检查市场配置:条目必须是表格,名字必须合法;然后算出目录位置,并确认里面能找到市场清单文件。最后把路径转成绝对路径、排序 → 出来的是一组稳定顺序的可用市场根目录;过程中只记录警告,不修改配置或磁盘。

调用关系:它被 marketplace_roots 调用,是“把配置变成实际市场目录列表”的关键步骤。它内部会用 marketplace_install_root 得到默认安装目录,也会依赖路径解析和清单查找来过滤掉不可用的市场。

调用图:调用 2 个内部函数(effective_user_config, marketplace_install_root);被 1 处调用(marketplace_roots);外部调用 2 个(new, warn!)。

resolve_configured_marketplace_root63–76 ↗
fn resolve_configured_marketplace_root(
    marketplace_name: &str,
    marketplace: &toml::Value,
    default_install_root: &Path,
) -> Option<PathBuf>

作用:根据单个市场的配置,判断这个市场目录到底应该在哪里。它把“本地目录”和“默认安装目录”两种情况分清楚。

数据流:进去的是市场名字、这个市场对应的配置块、以及默认安装根目录 → 它查看配置里的 source_type;如果是 local,就读取非空的 source 字段当作路径;否则就把默认安装根目录和市场名字拼起来 → 出来的是一个目录路径,或者在本地配置缺少有效路径时返回空。

调用关系:它是多个功能共用的路径判定小零件。配置快照检查、按名字查找市场根目录、根据来源找已安装市场位置时都会调用它,避免每个地方各自写一套判断规则。

调用图:被 3 处调用(configured_marketplace_snapshot_issues, find_marketplace_root_by_name, installed_marketplace_root_for_source);外部调用 2 个(join, get)。

cli/src/marketplace_cmd.rs源码 ↗
orchestration用户执行 codex plugin marketplace 子命令时活跃

这个文件是插件市场相关命令的“前台接待员”。用户在命令行里输入 add、list、upgrade、remove 后,它先把参数读懂,再把真正的活交给底层插件模块去做。比如 add 会找到 Codex 的主目录,把本地路径或 Git 仓库加入配置;list 会读取配置,找出当前能用的市场,并检查有没有加载失败;upgrade 会刷新 Git 类型的市场;remove 会把某个市场从配置里移走。它还支持 JSON 输出,方便脚本读取结果。可以把它想成一个服务窗口:用户说一句人话命令,它翻译成内部请求,调用后端干活,再把结果用适合人看或机器看的格式打印出来。

函数细节15
MarketplaceCli::run124–142 ↗
async fn run(self) -> Result<()>

作用:这是整个 marketplace 命令的分发入口。它看用户选的是添加、列表、升级还是删除,然后把任务交给对应的小函数。

数据流:进去的是已经解析好的命令行参数和配置覆盖项 → 它先把配置覆盖项转换成内部可用的格式 → 根据子命令分别调用 run_addrun_listrun_upgraderun_remove → 成功时没有额外产物,只表示命令完成;失败时把错误往外传。

调用关系:它是这个文件里所有具体操作的总开关。上层 CLI 框架解析完命令后会调用它,它自己不做添加或删除的细活,只负责把不同子命令送到对应处理函数。

调用图:调用 4 个内部函数(run_add, run_list, run_remove, run_upgrade)。

run_add145–187 ↗
async fn run_add(args: AddMarketplaceArgs) -> Result<()>

作用:这个函数处理“添加插件市场”。用户给一个本地路径或 Git 地址,它把这个来源登记到 Codex 里,并告诉用户装到了哪里。

数据流:进去的是添加命令的参数,包括来源地址、可选 Git 分支或标签、可选稀疏路径、是否 JSON 输出 → 它先找到 Codex 的主目录,再调用 add_marketplace 完成真正添加 → 出来的是终端上的提示文字或 JSON;同时 Codex 的插件市场配置和安装目录可能被更新。

调用关系:它由 MarketplaceCli::run 在用户执行 add 时调用。真正的安装和配置修改交给 add_marketplace,如果用户要 JSON,它再用 JsonMarketplaceAddOutput::from_outcome 把结果整理成机器容易读的格式。

调用图:调用 3 个内部函数(from_outcome, add_marketplace, find_codex_home);被 1 处调用(run);外部调用 1 个(println!)。

JsonMarketplaceAddOutput::from_outcome198–204 ↗
fn from_outcome(outcome: MarketplaceAddOutcome) -> Self

作用:这个函数把“添加市场”的内部结果改造成 JSON 输出用的小对象。这样脚本不需要解析人类提示语,也能稳定拿到市场名、安装目录和是否已经添加过。

数据流:进去的是 MarketplaceAddOutcome,里面有市场名、安装路径、是否重复添加等信息 → 它挑出 JSON 需要的字段,并把路径转成字符串 → 出来的是 JsonMarketplaceAddOutput,后面会被序列化成 JSON 打印。

调用关系:它只在 run_add 需要 JSON 输出时使用。它不负责添加市场,只负责把后端结果包装成对外输出格式。

调用图:被 1 处调用(run_add)。

run_list207–295 ↗
async fn run_list(overrides: Vec<(String, toml::Value)>, args: ListMarketplaceArgs) -> Result<()>

作用:这个函数处理“列出当前插件市场”。它让用户知道 Codex 现在会从哪些市场找插件,以及这些市场在本机的具体目录。

数据流:进去的是配置覆盖项和列表命令参数 → 它加载配置,创建插件管理器,设置认证方式,读取插件市场配置并发现可用市场 → 如果发现市场快照加载失败,会整理错误并中止;否则输出表格或 JSON → 出来的是终端列表或 JSON 列表,不修改市场本身。

调用关系:它由 MarketplaceCli::run 在用户执行 list 时调用。它会调用插件管理器发现市场,调用 configured_marketplace_snapshot_issues 汇总加载问题,普通输出时用 marketplace_root_dir 算根目录,JSON 输出时还会交给 configured_marketplace_sources_by_rootJsonMarketplaceListOutput::from_marketplaces 整理数据。

调用图:调用 6 个内部函数(from_marketplaces, configured_marketplace_sources_by_root, configured_marketplace_snapshot_issues, load_cli_auth_mode, new, marketplace_root_dir);被 1 处调用(run);外部调用 5 个(new, new, load_with_cli_overrides, bail!, println!)。

JsonMarketplaceListOutput::from_marketplaces304–325 ↗
fn from_marketplaces(
        marketplaces: Vec<codex_core_plugins::marketplace::Marketplace>,
        marketplace_sources: &HashMap<PathBuf, JsonMarketplaceSource>,
    ) -> Self

作用:这个函数把发现到的市场列表整理成 JSON 输出。它会去掉重复的市场根目录,避免同一个市场被打印多次。

数据流:进去的是插件市场对象列表,以及“目录对应来源信息”的表 → 它逐个计算市场根目录,跳过算不出来或重复的项目,再填入名称、根目录和可选来源信息 → 出来的是 JsonMarketplaceListOutput,可直接转成 JSON。

调用关系:它被 run_list--json 模式下调用。它依赖 marketplace_root_dir 的结果来判断每个市场真正属于哪个根目录,并把 configured_marketplace_sources_by_root 准备好的来源信息合进去。

调用图:被 1 处调用(run_list);外部调用 1 个(new)。

configured_marketplace_sources_by_root337–365 ↗
fn configured_marketplace_sources_by_root(
    codex_home: &Path,
    plugins_input: &PluginsConfigInput,
) -> HashMap<PathBuf, JsonMarketplaceSource>

作用:这个函数把“用户配置里的市场来源”改成按安装目录查找的表。这样列 JSON 时,可以把某个市场目录和它来自哪里对应起来。

数据流:进去的是 Codex 主目录和插件配置输入 → 它先读取用户有效配置里的 marketplaces 表,再取得每个市场的来源说明,并计算这个市场应该安装到哪个根目录 → 出来的是一个映射表:安装目录到来源信息;如果没有用户配置或没有 marketplaces,就返回空表。

调用关系:它被 run_list 的 JSON 输出路径调用。它会借助 configured_marketplace_sources 解析来源信息,用 marketplace_install_root 得到默认安装位置,再用 resolve_configured_marketplace_root 算每个市场的最终目录。

调用图:调用 2 个内部函数(configured_marketplace_sources, marketplace_install_root);被 1 处调用(run_list);外部调用 1 个(new)。

run_upgrade367–389 ↗
async fn run_upgrade(
    overrides: Vec<(String, toml::Value)>,
    args: UpgradeMarketplaceArgs,
) -> Result<()>

作用:这个函数处理“升级插件市场”。它主要用于刷新配置里的 Git 市场,让本机快照跟上配置指定的最新版本。

数据流:进去的是配置覆盖项,以及可选市场名和是否 JSON 输出 → 它加载配置,找到 Codex 主目录,创建插件管理器,然后请求升级一个指定市场或全部 Git 市场 → 出来的是人类可读提示或 JSON;本机已安装的市场快照可能被更新。

调用关系:它由 MarketplaceCli::run 在用户执行 upgrade 时调用。升级动作由 PluginsManager 完成,输出阶段再根据参数交给 print_upgrade_outcomeprint_upgrade_outcome_json

调用图:调用 4 个内部函数(print_upgrade_outcome, print_upgrade_outcome_json, new, find_codex_home);被 1 处调用(run);外部调用 1 个(load_with_cli_overrides)。

run_remove391–418 ↗
async fn run_remove(args: RemoveMarketplaceArgs) -> Result<()>

作用:这个函数处理“删除一个已配置的插件市场”。它会把指定市场从 Codex 的市场来源里移走,并可能删除对应的已安装目录。

数据流:进去的是要删除的市场名和是否 JSON 输出 → 它找到 Codex 主目录,调用 remove_marketplace 做实际删除 → 出来的是删除成功提示或 JSON;配置和本地安装目录可能被改变。

调用关系:它由 MarketplaceCli::run 在用户执行 remove 时调用。真正删除由 remove_marketplace 完成;如果需要 JSON,它用 JsonMarketplaceRemoveOutput::from_outcome 包装结果。

调用图:调用 3 个内部函数(from_outcome, remove_marketplace, find_codex_home);被 1 处调用(run);外部调用 1 个(println!)。

JsonMarketplaceRemoveOutput::from_outcome428–435 ↗
fn from_outcome(outcome: MarketplaceRemoveOutcome) -> Self

作用:这个函数把“删除市场”的内部结果转成 JSON 输出对象。它让外部脚本能清楚知道删掉的是哪个市场,以及有没有删除本地安装目录。

数据流:进去的是 MarketplaceRemoveOutcome,里面有市场名和可选的已删除目录 → 它把路径转成字符串,并保留可能为空的目录字段 → 出来的是 JsonMarketplaceRemoveOutput

调用关系:它只被 run_remove 的 JSON 分支使用。它不碰配置和磁盘,只负责把删除结果整理成稳定的输出格式。

调用图:被 1 处调用(run_remove)。

print_upgrade_outcome_json438–452 ↗
fn print_upgrade_outcome_json(outcome: &PluginMarketplaceUpgradeOutcome) -> Result<()>

作用:这个函数用 JSON 格式打印升级结果。它也会先检查有没有升级失败,失败时把错误写到标准错误并让命令失败。

数据流:进去的是升级结果对象 → 它先逐条打印失败信息到错误输出;如果有任何失败,就返回错误并停止;如果全部成功,就用 JsonMarketplaceUpgradeOutput::from_outcome 生成 JSON 并打印 → 出来的是 JSON 文本或失败错误。

调用关系:它被 run_upgrade 在用户加了 --json 时调用。它把结果转换工作交给 JsonMarketplaceUpgradeOutput::from_outcome,同时用 all_succeeded 判断能不能继续输出成功结果。

调用图:调用 1 个内部函数(from_outcome);被 1 处调用(run_upgrade);外部调用 4 个(all_succeeded, bail!, eprintln!, println!)。

JsonMarketplaceUpgradeOutput::from_outcome463–480 ↗
fn from_outcome(outcome: &PluginMarketplaceUpgradeOutcome) -> Self

作用:这个函数把升级结果整理成 JSON 对象。内容包括这次选中了哪些市场、哪些目录被更新、有哪些错误。

数据流:进去的是 PluginMarketplaceUpgradeOutcome 的引用 → 它复制市场名列表,把目录路径转成字符串,把错误拆成市场名和消息 → 出来的是 JsonMarketplaceUpgradeOutput,后面会被打印成 JSON。

调用关系:它被 print_upgrade_outcome_json 调用。它只做数据格式转换,不判断成功失败,也不执行升级。

调用图:被 1 处调用(print_upgrade_outcome_json)。

print_upgrade_outcome490–526 ↗
fn print_upgrade_outcome(
    outcome: &PluginMarketplaceUpgradeOutcome,
    marketplace_name: Option<&str>,
) -> Result<()>

作用:这个函数用普通人能读的文字打印升级结果。它会区分“没有可升级市场”“已经是最新”“升级了一个”“升级了多个”等情况。

数据流:进去的是升级结果,以及用户是否指定了某个市场名 → 它先把所有错误打印到错误输出;如果有失败就返回错误;如果成功,就根据选中市场和更新目录的数量选择合适的话术 → 出来的是终端提示文字或失败错误。

调用关系:它被 run_upgrade 在非 JSON 模式下调用。它不负责刷新市场,只负责把 PluginsManager 返回的结果翻译成用户看得懂的提示。

调用图:被 1 处调用(run_upgrade);外部调用 4 个(all_succeeded, bail!, eprintln!, println!)。

tests::sparse_paths_parse_before_or_after_source534–561 ↗
fn sparse_paths_parse_before_or_after_source()

作用:这个测试确认 add 命令里的 --sparse 参数放在来源前后都能被正确识别。sparse 可以理解成只取 Git 仓库里的某些子目录。

数据流:进去的是几组模拟命令行参数 → 测试用 clap 的解析器把它们解析成 AddMarketplaceArgs → 再检查 source 和 sparse_paths 是否符合预期;它只验证解析结果,不改真实配置。

调用关系:这是测试代码,只在运行测试时执行。它保护 run_add 之前的参数解析阶段,避免用户因为参数顺序不同而命令失效。

调用图:外部调用 2 个(assert_eq!, try_parse_from)。

tests::upgrade_subcommand_parses_optional_marketplace_name564–570 ↗
fn upgrade_subcommand_parses_optional_marketplace_name()

作用:这个测试确认 upgrade 命令的市场名是可选的。不写名字表示升级全部,写了名字表示只升级一个。

数据流:进去的是两组模拟命令:一个只有 upgrade,一个是 upgrade debug → 解析后分别检查 marketplace_name 是空还是 debug → 出来的是测试通过或失败。

调用关系:这是测试代码,只在测试阶段运行。它保证 run_upgrade 收到的参数含义稳定,不会把“升级全部”和“升级指定市场”搞混。

调用图:外部调用 2 个(assert_eq!, try_parse_from)。

tests::remove_subcommand_parses_marketplace_name573–576 ↗
fn remove_subcommand_parses_marketplace_name()

作用:这个测试确认 remove 命令必须能正确读到要删除的市场名。没有这个保障,删除命令可能删错对象或无法执行。

数据流:进去的是模拟命令 remove debug → 解析成 RemoveMarketplaceArgs → 检查 marketplace_name 是否等于 debug → 出来的是测试通过或失败。

调用关系:这是测试代码,只在测试阶段运行。它保护 run_remove 的入口参数,确保删除逻辑拿到的是用户真正输入的市场名。

调用图:外部调用 2 个(assert_eq!, try_parse_from)。

plugin/src/provider.rs源码 ↗
domain_logic插件发现和加载准备阶段

插件通常来自某个环境的文件系统,比如一个沙盒、工作区或远端环境。这个文件要解决的问题是:找到插件以后,不能只记一堆裸路径,因为裸路径看不出它属于谁,也容易误用。这里用 PluginResourceLocator 给每个资源贴上“来自哪个环境”的标签;用 ResolvedPluginLocation 记录插件包根目录在哪里;用 ResolvedPlugin 保存插件清单、清单文件路径和被选中的能力根 ID。创建 ResolvedPlugin 时会检查所有资源路径必须在插件包根目录下面,像门卫一样,不让清单把手伸到包外。最后,PluginProvider 是一个统一接口:不同来源的插件提供者都实现它,按一个被选中的根目录去解析插件,但只生成描述,不真正启动插件。

函数细节6
ResolvedPlugin::from_environment51–70 ↗
fn from_environment(
        selected_root_id: String,
        environment_id: String,
        root: AbsolutePathBuf,
        manifest_path: AbsolutePathBuf,
        manifest: PluginManifest<AbsoluteP

作用:把一个已经读到、已经验证过格式的插件清单,包装成一个带环境归属的插件描述。它最重要的作用是给清单里的每个文件路径都贴上环境标签,并确认这些路径没有跑出插件包目录。

数据流:进去的是被选中的根 ID、环境 ID、插件包根目录、清单文件路径,以及清单里原本的绝对路径。它先用 environment_resource 检查清单文件本身在不在包根目录下,再通过 try_map_resources 把清单中的每个资源路径也逐个检查并转换成带环境 ID 的 PluginResourceLocator。成功后出来的是 ResolvedPlugin;如果发现某个路径在包外,就出来一个 ResolvedPluginError。

调用关系:这是把“刚解析出来的清单”变成“系统可安全传递的插件描述”的入口。resolve_plugin_root 这类解析流程会在找到清单后调用它,测试也用它确认主机和执行端解析一致、资源会被正确绑定、包外资源会被拒绝。它把具体的单个路径检查交给 environment_resource。

调用图:调用 2 个内部函数(try_map_resources, environment_resource);被 5 处调用(host_and_executor_sources_parse_the_same_manifest, resolve_plugin_root, resolved_plugin, environment_descriptor_binds_every_manifest_resource, environment_descriptor_rejects_resources_outside_package_root)。

ResolvedPlugin::selected_root_id73–75 ↗
fn selected_root_id(&self) -> &str

作用:取出这个插件对应的能力根 ID。能力根可以理解为系统允许插件看到的一块起点目录或资源范围,这个 ID 用来回头认出它是哪一块。

数据流:进去的是一个已经构造好的 ResolvedPlugin。函数不改任何东西,只把里面保存的 selected_root_id 作为字符串引用交出去。

调用关系:load_from_file_system 会用它知道当前插件来自哪个被选中的根。它只是一个安全的读取窗口,不参与解析,也不触碰文件系统。

调用图:被 1 处调用(load_from_file_system)。

ResolvedPlugin::location78–80 ↗
fn location(&self) -> &ResolvedPluginLocation

作用:取出插件包的实际位置,并且这个位置带着“属于哪个环境”的信息。这样后续代码不会把不同环境里的同名路径混在一起。

数据流:进去的是 ResolvedPlugin。函数直接返回内部的 ResolvedPluginLocation 引用,不复制文件,也不改状态。

调用关系:load_from_file_system 会读取它来知道插件包根目录和环境归属。它配合 manifest、selected_root_id 等读取函数,让加载流程拿到足够信息但不能随意破坏描述对象。

调用图:被 1 处调用(load_from_file_system)。

ResolvedPlugin::manifest_path83–85 ↗
fn manifest_path(&self) -> &PluginResourceLocator

作用:取出当初用来解析这个插件的清单文件位置。这个位置同样带有环境标签,方便排错、记录日志或之后需要定位清单时使用。

数据流:进去的是 ResolvedPlugin。它把内部保存的 manifest_path 引用返回出去,不做额外计算,也不会改变插件描述。

调用关系:它是 ResolvedPlugin 对外暴露信息的一扇小窗口。虽然调用图里没有显示当前流程直接使用它,但它让其他代码可以在不拆开结构体的情况下知道清单文件来自哪里。

ResolvedPlugin::manifest88–90 ↗
fn manifest(&self) -> &PluginManifest<PluginResourceLocator>

作用:取出插件清单内容。这里的清单不是普通裸路径版本,而是资源字段都已经带上来源环境的安全版本。

数据流:进去的是 ResolvedPlugin。函数返回内部 manifest 的只读引用,调用者可以查看插件元数据和资源位置,但不能通过这个函数修改它。

调用关系:load_from_file_system 会用它继续加载插件需要的元信息。它依赖 from_environment 之前已经把清单里的资源路径转换好,所以后续流程拿到的是带来源归属的清单。

调用图:被 1 处调用(load_from_file_system)。

environment_resource93–108 ↗
fn environment_resource(
    environment_id: &str,
    root: &AbsolutePathBuf,
    path: AbsolutePathBuf,
) -> Result<PluginResourceLocator, ResolvedPluginError>

作用:把一个绝对路径变成“某个环境里的资源定位符”,同时检查它是否真的在插件包根目录下面。它就是防止清单引用包外文件的那道关。

数据流:进去的是环境 ID、插件包根目录和一个资源绝对路径。它把路径和根目录都当作文件系统路径来比较:如果资源路径不是以根目录开头,就返回 ResourceOutsideRoot 错误,并带上根目录和问题路径;如果检查通过,就输出 PluginResourceLocator::Environment,里面包含环境 ID 和原路径。

调用关系:它只被 ResolvedPlugin::from_environment 调用,用来检查清单文件路径以及清单里列出的每个资源路径。from_environment 负责整体组装,它负责单个路径的安全判定。

调用图:调用 1 个内部函数(as_path);被 1 处调用(from_environment);外部调用 1 个(clone)。

plugin/src/load_outcome.rs源码 ↗
domain_logicconfig load / runtime capability lookup

插件加载不是简单地“读到文件就算成功”。一个插件可能被禁用,可能加载时报错,也可能没有任何实际能力。这个文件就像一张插件清单的整理表:先用 LoadedPlugin 记录每个插件的原始情况,比如名字、路径、是否启用、技能目录、MCP 服务器配置和错误信息;再用 PluginLoadOutcome 把这些插件筛一遍,只把真正可用的能力拿出来。这里还会生成给模型看的能力摘要,并把描述文字压平、截短,避免把太长或乱格式的说明塞进提示词。它还处理重复项:比如多个插件指向同一个技能目录时,只保留第一个插件的归属信息。这样后面的系统不用反复判断插件是否有效,只要问这个文件提供的“有效结果”即可。

函数细节17
LoadedPlugin::is_active34–36 ↗
fn is_active(&self) -> bool

作用:判断一个插件现在算不算“可用”。只有插件被启用,而且加载时没有错误,才算可用。

数据流:输入是一个已经加载出来的插件记录 → 它查看 enabled 是否为真、error 是否为空 → 输出一个 true 或 false,不改动插件本身。

调用关系:生成插件能力摘要时会先调用它。这样后续摘要和有效能力列表不会把禁用或出错的插件算进去。

调用图:被 1 处调用(plugin_capability_summary_from_loaded)。

LoadedPlugin::display_name38–40 ↗
fn display_name(&self) -> &str

作用:给插件找一个适合展示的名字。优先用插件清单里的正式名称;如果没有,就用配置里的名字兜底。

数据流:输入是插件记录 → 它查看 manifest_name 有没有值 → 输出一个可显示的字符串引用,不创建新的插件数据。

调用关系:插件能力摘要需要一个给人和模型看的名字,所以 plugin_capability_summary_from_loaded 会调用它。

调用图:被 1 处调用(plugin_capability_summary_from_loaded)。

plugin_capability_summary_from_loaded43–66 ↗
fn plugin_capability_summary_from_loaded(
    plugin: &LoadedPlugin<M>,
) -> Option<PluginCapabilitySummary>

作用:把一个可用插件压缩成一份“能力摘要”。这份摘要告诉系统和模型:这个插件有什么技能、哪些 MCP 服务、哪些应用连接器。

数据流:输入是一个 LoadedPlugin → 它先用 is_active 排除禁用或出错的插件,再整理 MCP 服务名、显示名、安全版描述、技能标记和应用连接器 ID → 如果插件没有任何能力就输出 None,否则输出 PluginCapabilitySummary。

调用关系:PluginLoadOutcome::from_plugins 在汇总所有插件时会用它。它会调用 display_name、is_active、prompt_safe_plugin_description,并借助 app_connector_ids_from_declarations 把应用声明转成连接器 ID。

调用图:调用 3 个内部函数(display_name, is_active, prompt_safe_plugin_description);外部调用 1 个(app_connector_ids_from_declarations)。

prompt_safe_plugin_description69–84 ↗
fn prompt_safe_plugin_description(description: Option<&str>) -> Option<String>

作用:把插件描述整理成适合放进模型提示词里的短文本。它会去掉多余空白,并限制长度,避免描述太乱或太长。

数据流:输入是一个可选的描述字符串 → 如果没有描述就直接返回 None;如果有,就把连续空白整理成单个空格,空文本丢弃,再最多保留 1024 个字符 → 输出整理后的可选字符串。

调用关系:plugin_capability_summary_from_loaded 在生成能力摘要时调用它。它是摘要进入模型上下文前的一道清洁工。

调用图:被 1 处调用(plugin_capability_summary_from_loaded)。

PluginLoadOutcome::default96–98 ↗
fn default() -> Self

作用:创建一个空的插件加载结果。适合系统还没有插件,或者加载失败需要一个安全空结果时使用。

数据流:没有输入 → 它创建一个空插件列表,并交给 from_plugins 生成完整的 PluginLoadOutcome → 输出一个不含插件、不含能力摘要的结果。

调用关系:plugins_for_config_with_force_reload 和 plugins_for_layer_stack 会在需要默认加载结果时用它。它把实际构造工作交给 from_plugins。

调用图:被 2 处调用(plugins_for_config_with_force_reload, plugins_for_layer_stack);外部调用 2 个(from_plugins, new)。

PluginLoadOutcome::from_plugins102–111 ↗
fn from_plugins(plugins: Vec<LoadedPlugin<M>>) -> Self

作用:从一批已加载插件生成最终的运行时结果。它不仅保存原始插件列表,还提前算好给模型看的能力摘要。

数据流:输入是一组 LoadedPlugin → 它逐个尝试生成能力摘要,只留下可用且确实有能力的插件摘要 → 输出 PluginLoadOutcome,里面同时保存插件原文和摘要列表。

调用关系:resolve_loaded_plugins_for_auth 会用它把加载结果封装起来;一些测试也用它检查能力索引和技能目录去重行为。

调用图:被 3 处调用(resolve_loaded_plugins_for_auth, capability_index_filters_inactive_and_zero_capability_plugins, effective_plugin_skill_roots_preserves_first_plugin_for_shared_root)。

PluginLoadOutcome::effective_mcp_servers144–154 ↗
fn effective_mcp_servers(&self) -> HashMap<String, M>

作用:取出当前真正生效的 MCP 服务器配置。MCP 服务器可以理解成插件提供给系统调用的外部工具服务。

数据流:输入是整个插件加载结果 → 它只看可用插件,遍历每个插件声明的 MCP 服务;如果服务名重复,就保留先遇到的配置 → 输出一个服务名到配置的映射表。

调用关系:sorted_effective_mcp_server_names 会用它拿到当前有效服务,再做排序展示或检查。它内部会新建一个映射表来装结果。

调用图:被 1 处调用(sorted_effective_mcp_server_names);外部调用 1 个(new)。

PluginLoadOutcome::effective_apps156–163 ↗
fn effective_apps(&self) -> Vec<AppConnectorId>

作用:取出当前可用插件提供的应用连接器 ID。应用连接器可以理解成插件声明的“能连接哪些外部应用”的标识。

数据流:输入是插件加载结果 → 它筛掉不可用插件,把剩余插件的应用声明合在一起 → 通过 app_connector_ids_from_declarations 转成连接器 ID 列表并输出。

调用关系:它在需要知道当前有哪些应用连接能力时使用,并把“声明转 ID”的细节交给 app_connector_ids_from_declarations。

调用图:外部调用 1 个(app_connector_ids_from_declarations)。

PluginLoadOutcome::effective_plugin_hook_sources165–171 ↗
fn effective_plugin_hook_sources(&self) -> Vec<PluginHookSource>

作用:收集所有可用插件提供的钩子来源。钩子就是系统在某些时机自动触发的小扩展点。

数据流:输入是插件加载结果 → 它只看可用插件,把这些插件的 hook_sources 复制出来并合并 → 输出一个钩子来源列表。

调用关系:运行时如果要注册或执行插件钩子,会从这里拿到已经过滤好的来源,避免误用禁用或出错插件的钩子。

PluginLoadOutcome::effective_plugin_hook_warnings173–179 ↗
fn effective_plugin_hook_warnings(&self) -> Vec<String>

作用:收集可用插件在加载钩子时产生的警告信息。这样系统可以把“钩子有问题但插件仍可用”的情况告诉用户或日志。

数据流:输入是插件加载结果 → 它筛选可用插件,复制这些插件的 hook_load_warnings → 输出警告字符串列表。

调用关系:它服务于诊断和提示场景,让调用方不用自己遍历插件并判断哪些警告该显示。

PluginLoadOutcome::capability_summaries181–183 ↗
fn capability_summaries(&self) -> &[PluginCapabilitySummary]

作用:返回已经算好的插件能力摘要列表。调用方可以直接用它给模型或界面展示插件能力。

数据流:输入是 PluginLoadOutcome 自身 → 它直接借出内部的 capability_summaries 列表 → 输出只读引用,不复制也不修改数据。

调用关系:它是读取摘要的窗口。摘要在 from_plugins 时已经生成,所以这里不用重新计算。

PluginLoadOutcome::plugins185–187 ↗
fn plugins(&self) -> &[LoadedPlugin<M>]

作用:返回原始的已加载插件列表。适合需要查看完整插件信息,而不只是摘要或有效能力的调用方。

数据流:输入是 PluginLoadOutcome 自身 → 它直接借出内部 plugins 列表 → 输出只读引用,不改变任何插件。

调用关系:它给其他模块一个安全查看加载结果的入口,同时避免外部随便改动内部插件列表。

PluginLoadOutcome::effective_skill_roots199–201 ↗
fn effective_skill_roots(&self) -> Vec<AbsolutePathBuf>

作用:取出当前真正生效的技能目录路径。技能目录就是插件放置可被系统发现和使用的技能文件的地方。

数据流:输入是插件加载结果 → 它只看可用插件,收集技能目录路径,排序并去掉重复路径 → 输出干净的路径列表。

调用关系:它也通过 EffectiveSkillRoots 这个接口暴露给不想关心 MCP 配置类型的调用方。这样像技能系统这样的模块只需要问“有哪些技能目录”,不用知道插件内部细节。

PluginLoadOutcome::effective_plugin_skill_roots203–205 ↗
fn effective_plugin_skill_roots(&self) -> Vec<PluginSkillRoot>

作用:取出当前生效的技能目录,并保留每个目录来自哪个插件。相比只拿路径,它还能回答“这个技能目录归谁”。

数据流:输入是插件加载结果 → 它只看可用插件,逐个检查技能目录;同一路径第一次出现时记录路径、插件 ID 和插件根目录,后面重复的跳过;最后按路径排序 → 输出 PluginSkillRoot 列表。

调用关系:EffectiveSkillRoots 接口会把同名请求转到这里。测试 effective_plugin_skill_roots_preserves_first_plugin_for_shared_root 专门检查:如果两个插件共享一个目录,归属要保留第一个插件。

调用图:外部调用 2 个(new, new)。

tests::test_path212–215 ↗
fn test_path(name: &str) -> AbsolutePathBuf

作用:给测试造一个临时目录下的绝对路径。测试需要稳定的路径对象,但不真的关心磁盘里是否有这些插件。

数据流:输入是一个名字 → 它把系统临时目录和这个名字拼起来,再检查并转换成 AbsolutePathBuf → 输出一个测试用的绝对路径。

调用关系:测试里的 loaded_plugin 和 effective_plugin_skill_roots_preserves_first_plugin_for_shared_root 会调用它,避免每个测试重复写路径构造代码。

调用图:调用 1 个内部函数(from_absolute_path_checked);外部调用 1 个(temp_dir)。

tests::loaded_plugin217–233 ↗
fn loaded_plugin(config_name: &str, skill_roots: Vec<AbsolutePathBuf>) -> LoadedPlugin<()>

作用:快速造一个测试用的可用插件。这样测试可以专注检查技能目录行为,而不用手动填写一大堆插件字段。

数据流:输入是插件配置名和技能目录列表 → 它填入默认的启用状态、空 MCP 服务、空应用、空钩子、无错误等字段,并用 test_path 生成插件根目录 → 输出一个 LoadedPlugin<()>。

调用关系:effective_plugin_skill_roots_preserves_first_plugin_for_shared_root 用它创建两个共享同一技能目录的插件,来验证去重时保留谁。

调用图:外部调用 4 个(new, new, new, test_path)。

tests::effective_plugin_skill_roots_preserves_first_plugin_for_shared_root236–251 ↗
fn effective_plugin_skill_roots_preserves_first_plugin_for_shared_root()

作用:验证一个关键规则:如果两个插件声明了同一个技能目录,结果里只保留第一次出现的插件作为归属方。

数据流:它先造一个共享技能目录,再造两个插件 zeta@test 和 alpha@test,都指向这个目录 → 用 from_plugins 生成加载结果,再调用 effective_plugin_skill_roots → 最后断言结果只包含 zeta@test 的归属信息。

调用关系:这个测试保护 effective_plugin_skill_roots 的去重语义,防止以后改代码时不小心让后面的插件覆盖前面的插件。

调用图:调用 1 个内部函数(from_plugins);外部调用 3 个(assert_eq!, test_path, vec!)。

codex-mcp/src/plugin_config.rs源码 ↗
configconfig load

MCP 可以理解成“让 Codex 连接外部工具的一套接口”。插件可以自己声明要启动或连接哪些 MCP 服务器,但这些声明可能有两种外层写法,也可能带相对目录、旧字段名,甚至某个服务器写错了。这个文件就像一个收货员:先拆开配置文件,认出里面每台服务器;再逐台检查和整理。能用的服务器会放进结果里,写坏的服务器只记录错误,不会把同一个文件里的其他好配置一起丢掉。它还会处理一个重要场景:当服务器由某个执行环境托管时,会给配置绑定 environment_id,给 stdio 服务器补默认工作目录,并检查环境变量应该来自本地还是远端,避免插件越界读路径或拿错环境变量。

函数细节6
PluginMcpFile::into_mcp_servers50–55 ↗
fn into_mcp_servers(self) -> BTreeMap<String, JsonValue>

作用:这个函数把插件 MCP 配置的两种外层写法统一成同一种“服务器名字到服务器配置”的表。这样后面的代码不用关心用户到底用了哪种格式。

数据流:进去的是已经从 JSON 读出来的 PluginMcpFile;它判断这个文件是包在 mcpServers 字段里,还是顶层本身就是服务器表;出来的是一张 BTreeMap,也就是按名字排列的服务器配置清单。

调用关系:它是 parse_plugin_mcp_config 拆开文件后的第一步。parse_plugin_mcp_config 先把文本解析成 PluginMcpFile,再交给它取出真正的服务器列表,然后逐个送去 normalize_plugin_mcp_server 整理。

parse_plugin_mcp_config62–82 ↗
fn parse_plugin_mcp_config(
    plugin_root: &Path,
    contents: &str,
    placement: PluginMcpServerPlacement<'_>,
) -> Result<PluginMcpConfigParseOutcome, serde_json::Error>

作用:这是外部最主要会调用的入口:给它插件目录、配置文件内容和放置方式,它会返回“哪些服务器可用、哪些服务器写错了”。它不会因为一个服务器错了就放弃整个文件,除非 JSON 顶层本身就坏到读不懂。

数据流:进去的是插件根目录、配置文本和 placement(放置方式,说明保留原声明还是绑定到某个环境);它先把 JSON 文本解析成内部结构,再取出服务器表;每个服务器都交给 normalize_plugin_mcp_server 规范化。成功的放进 servers,失败的把服务器名和错误信息放进 errors;最后出来一个 PluginMcpConfigParseOutcome。

调用关系:它串起整个文件的流程,是其他模块读取插件 MCP 配置时会用的函数。它把具体整理工作交给 normalize_plugin_mcp_server,自己负责收集结果,并用 default 创建一个空的结果篮子。

调用图:调用 1 个内部函数(normalize_plugin_mcp_server);外部调用 1 个(default)。

normalize_plugin_mcp_server84–120 ↗
fn normalize_plugin_mcp_server(
    plugin_root: &Path,
    value: JsonValue,
    placement: PluginMcpServerPlacement<'_>,
) -> Result<McpServerConfig, String>

作用:这个函数负责把单个插件服务器配置变成 Codex 运行时标准的 McpServerConfig。它会补环境信息、处理工作目录,并把不合法的配置变成清楚的错误文字。

数据流:进去的是插件根目录、一段 JSON 服务器配置和 placement;它先调用 normalize_plugin_mcp_server_value 做字段级清理。如果 placement 表示服务器属于某个环境,它会写入 environment_id;如果是 stdio 这类需要启动本地命令的服务器,还会把 cwd(工作目录)补成插件根目录或安全的绝对路径。然后它把整理后的 JSON 转成 McpServerConfig;如果需要环境绑定,还会调用 bind_environment_env_vars 检查环境变量来源。出来的是标准配置,或者一条错误信息。

调用关系:它由 parse_plugin_mcp_config 对每个服务器逐个调用。它自己再把活分给 normalize_plugin_mcp_server_value、executor_plugin_cwd 和 bind_environment_env_vars:前者清字段,中间那个算安全目录,后者管环境变量来源。

调用图:调用 3 个内部函数(bind_environment_env_vars, executor_plugin_cwd, normalize_plugin_mcp_server_value);被 1 处调用(parse_plugin_mcp_config);外部调用 4 个(Object, String, to_string_lossy, matches!)。

executor_plugin_cwd122–139 ↗
fn executor_plugin_cwd(plugin_root: &Path, configured_cwd: &str) -> Result<PathBuf, String>

作用:这个函数用来计算插件服务器实际运行时的工作目录,并防止相对路径跑出插件目录。简单说,它保证插件说“在子目录里运行”时,不能偷偷写成“跑到上级目录去”。

数据流:进去的是插件根目录和配置里写的 cwd 字符串;如果 cwd 已经是绝对路径,就原样返回;如果是相对路径,它检查里面有没有 ..、根路径或平台前缀这类可能越界的部分。安全的话,就把它拼到插件根目录下面;不安全的话,出来一条错误信息。

调用关系:它只在 normalize_plugin_mcp_server 处理执行环境托管的 stdio 服务器时被调用。normalize_plugin_mcp_server 需要给服务器确定一个可靠的启动目录,所以把路径安全检查交给它。

调用图:被 1 处调用(normalize_plugin_mcp_server);外部调用 3 个(join, new, format!)。

bind_environment_env_vars141–175 ↗
fn bind_environment_env_vars(config: &mut McpServerConfig) -> Result<(), String>

作用:这个函数检查并修正 stdio MCP 服务器的环境变量来源。环境变量就是程序启动时能读到的一些外部值;这里要分清它们来自本地机器还是远端执行环境,避免拿错地方的秘密或配置。

数据流:进去的是一个已经解析好的 McpServerConfig,并且会直接修改它;它先判断这个服务器是不是本地环境。如果服务器不是 stdio 传输,就什么也不做。对于 stdio 的 env_vars,它会把远端环境里只写名字的变量补成 source 为 remote;如果发现本地环境却声明 remote,或远端托管插件却声明 local,就返回错误。成功时出来 Ok,并且配置里的环境变量可能已经被补全。

调用关系:它由 normalize_plugin_mcp_server 在环境托管模式下调用。normalize_plugin_mcp_server 负责大流程,但环境变量的细规则比较容易出错,所以集中交给这个函数判断。它会用 McpServerConfig 的 is_local_environment 来决定规则。

调用图:调用 1 个内部函数(is_local_environment);被 1 处调用(normalize_plugin_mcp_server);外部调用 3 个(format!, take, unreachable!)。

normalize_plugin_mcp_server_value177–228 ↗
fn normalize_plugin_mcp_server_value(
    plugin_root: &Path,
    value: JsonValue,
    placement: PluginMcpServerPlacement<'_>,
) -> JsonMap<String, JsonValue>

作用:这个函数做最早一层的“字段清洗”:把插件配置里 Codex 不直接使用或名字不统一的字段整理好。它还会对可疑但不致命的写法打警告,而不是直接让整个配置失败。

数据流:进去的是插件根目录、一段 JSON 值和 placement;如果这段值不是对象,就返回一个空对象。它会移除 type 字段,并对未知传输类型写日志警告;会整理 oauth(登录授权配置)里的 callbackPort 和 clientId,把 clientId 改成 Codex 期待的 client_id;如果 placement 是 Declared,并且 cwd 是相对路径,它会把 cwd 拼到插件根目录下。出来的是整理后的 JSON 对象。

调用关系:它是 normalize_plugin_mcp_server 的第一道处理工序。normalize_plugin_mcp_server 先让它把松散的插件写法变规整,再继续做环境绑定、路径安全检查和最终的 McpServerConfig 解析。

调用图:被 1 处调用(normalize_plugin_mcp_server);外部调用 7 个(new, Object, String, join, new, matches!, warn!)。

codex-mcp/src/catalog.rs源码 ↗
domain_logic配置合并、插件加载后、真正使用 MCP 服务器前

MCP 可以理解成一种让程序接入外部工具服务器的约定。这个文件像一个“报名表裁判”:插件、用户配置、兼容层、扩展都可能声明同一个服务器,也可能要求禁用或移除它。文件先把这些声明统一包装成动作,再按优先级排序:普通插件最低,用户配置更高,扩展更高。同名时,高优先级覆盖低优先级;同一档次冲突时,靠加入顺序决定胜负,并把冲突记录下来,方便之后提示或排查。它还保留插件来源信息,用来知道某个工具到底来自哪个插件。最后产出一个不可随便改的 ResolvedMcpCatalog,也就是“已经裁决好的 MCP 服务器清单”。

函数细节28
McpPluginAttribution::new16–21 ↗
fn new(plugin_id: String, display_name: String) -> Self

作用:创建一份插件归属信息,记录插件的内部 ID 和给人看的名字。之后系统能说清楚某个 MCP 工具是哪个插件带来的。

数据流:输入插件 ID 和显示名称 → 把两段文字放进 McpPluginAttribution 结构里 → 返回这份可携带、可复制的插件身份信息,不改动外部数据。

调用关系:插件注册 MCP 服务器时会先用它做一张“来源标签”。后面的 McpServerRegistration::from_plugin 和 McpServerRegistration::from_selected_plugin 会把这张标签塞进服务器声明里。

调用图:被 6 处调用(plugin, selected_mcp_attribution_does_not_join_an_unrelated_local_summary, tool_plugin_provenance_collects_app_and_mcp_sources, to_mcp_config_with_plugin_registrations, selected_plugin_wins_after_discovered_plugin_requirements, runtime_config_with_context)。

McpPluginAttribution::plugin_id23–25 ↗
fn plugin_id(&self) -> &str

作用:取出插件的内部 ID。这个 ID 通常给程序识别插件用,不一定是给用户看的名字。

数据流:输入是一份插件归属信息 → 只读取其中的 plugin_id 字段 → 返回这段 ID 的只读引用,不复制也不修改。

调用关系:当外部代码需要按插件 ID 做记录、比较或展示来源时会调用它。它不再把工作交给别的函数。

McpPluginAttribution::display_name27–29 ↗
fn display_name(&self) -> &str

作用:取出插件的显示名称。这个名字更适合给人看,比如日志、界面或提示里展示。

数据流:输入是一份插件归属信息 → 只读取 display_name 字段 → 返回显示名称的只读引用,不改变清单内容。

调用关系:它通常在需要解释“这个服务器来自哪个插件”时被调用。它只是一个安全的读窗口,不参与优先级裁决。

McpServerSource::disabled_registration_is_name_veto49–53 ↗
fn disabled_registration_is_name_veto(&self) -> bool

作用:判断一个“被禁用”的服务器名字,是否应该继续挡住后来同名的运行时声明。简单说,就是这个禁用有没有“一票否决同名服务器”的效果。

数据流:输入是服务器来源 → 检查它是不是 SelectedPlugin,也就是当前线程特意选择的插件 → 如果不是 SelectedPlugin 就返回 true,表示禁用会按名字延续;如果是就返回 false。

调用关系:McpCatalogBuilder::build 在处理禁用服务器时会调用它。这样可以避免一个临时选择的插件禁用项,误伤别的更高层运行时来源。

调用图:外部调用 1 个(matches!)。

RegistrationPrecedence::tier66–74 ↗
fn tier(self) -> u8

作用:把详细的优先级变成一个粗略档位,用来判断哪些动作属于同一层冲突。比如普通插件是一档,配置是一档,扩展是一档。

数据流:输入一个 RegistrationPrecedence 值 → 按来源种类映射成 0 到 4 的数字 → 返回这个数字,数字越大代表档位越高。

调用关系:McpCatalogBuilder::build 用它把同名、同档的动作分组。只有同一档里出现多个动作,才会记录成冲突。

McpServerRegistration::from_config87–94 ↗
fn from_config(name: String, config: McpServerConfig) -> Self

作用:把用户或应用配置里的 MCP 服务器配置,包装成一条标准服务器声明。配置来源的优先级高于插件。

数据流:输入服务器名字和 McpServerConfig 配置 → 标记来源为 Config,并设置配置来源的优先级 → 返回一条 McpServerRegistration。

调用关系:测试和配置转换代码会用它把普通配置加入清单。它内部交给 McpServerRegistration::new 统一创建结构,之后通常会被 McpCatalogBuilder::register 收集。

调用图:被 5 处调用(disabled_winner_remains_a_veto_when_the_catalog_is_extended, selected_plugins_override_discovered_plugins_but_not_config, source_precedence_preserves_the_winning_registration, effective_mcp_servers_preserve_runtime_servers, to_mcp_config_with_plugin_registrations);外部调用 1 个(new)。

McpServerRegistration::from_plugin96–108 ↗
fn from_plugin(
        name: String,
        attribution: McpPluginAttribution,
        plugin_order: usize,
        config: McpServerConfig,
    ) -> Self

作用:把自动发现的插件提供的 MCP 服务器,包装成一条标准声明。它还带上插件归属,方便以后追踪工具来源。

数据流:输入服务器名字、插件归属、插件顺序和服务器配置 → 标记来源为 Plugin,并把插件顺序转成优先级的一部分 → 返回服务器声明。

调用关系:插件扫描或测试会调用它。它通过 McpServerRegistration::new 完成实际组装,随后交给 McpCatalogBuilder::register 进入裁决流程。

调用图:被 6 处调用(disabled_discovered_plugin_remains_a_veto_for_runtime_overlays, earlier_plugin_wins_with_an_explicit_conflict, selected_plugins_override_discovered_plugins_but_not_config, source_precedence_preserves_the_winning_registration, tool_plugin_provenance_collects_app_and_mcp_sources, to_mcp_config_with_plugin_registrations);外部调用 4 个(new, Plugin, Plugin, Reverse)。

McpServerRegistration::from_selected_plugin111–123 ↗
fn from_selected_plugin(
        name: String,
        attribution: McpPluginAttribution,
        selection_order: usize,
        config: McpServerConfig,
    ) -> Self

作用:为“当前线程明确选择的插件”创建 MCP 服务器声明。它的优先级高于普通发现的插件,但低于用户配置。

数据流:输入服务器名字、插件归属、选择顺序和配置 → 标记来源为 SelectedPlugin,并记录选择顺序 → 返回标准服务器声明。

调用关系:当某次运行或某个上下文临时选择了插件时会用它。它内部仍交给 McpServerRegistration::new,之后进入 McpCatalogBuilder::register。

调用图:被 5 处调用(disabled_selected_plugin_does_not_veto_runtime_overlays, selected_plugins_override_discovered_plugins_but_not_config, selected_mcp_attribution_does_not_join_an_unrelated_local_summary, selected_plugin_wins_after_discovered_plugin_requirements, runtime_config_with_context);外部调用 4 个(new, SelectedPlugin, SelectedPlugin, Reverse)。

McpServerRegistration::from_compatibility125–136 ↗
fn from_compatibility(
        name: String,
        id: impl Into<String>,
        config: McpServerConfig,
    ) -> Self

作用:创建来自兼容层的 MCP 服务器声明。兼容层一般是为了支持旧方式或旧接口,让新旧系统能接上。

数据流:输入服务器名字、兼容项 ID 和配置 → 把 ID 转成字符串,标记来源为 Compatibility → 返回带兼容层优先级的服务器声明。

调用关系:运行时构建兼容配置或相关测试会调用它。它用 McpServerRegistration::new 统一生成声明,再由 builder 参与合并。

调用图:被 3 处调用(equal_precedence_uses_insertion_order_not_source_identity, source_precedence_preserves_the_winning_registration, runtime_config_with_context);外部调用 2 个(into, new)。

McpServerRegistration::from_extension138–150 ↗
fn from_extension(
        name: String,
        id: impl Into<String>,
        contribution_order: usize,
        config: McpServerConfig,
    ) -> Self

作用:创建来自扩展贡献的 MCP 服务器声明。扩展来源的优先级最高,并且还会用贡献顺序处理同级先后。

数据流:输入服务器名字、扩展 ID、贡献顺序和配置 → 把 ID 转成字符串,标记来源为 Extension,并记录顺序 → 返回服务器声明。

调用关系:扩展系统提供或覆盖 MCP 服务器时会调用它。它内部调用 McpServerRegistration::new,之后交给 McpCatalogBuilder::register 统一裁决。

调用图:被 6 处调用(disabled_discovered_plugin_remains_a_veto_for_runtime_overlays, disabled_selected_plugin_does_not_veto_runtime_overlays, disabled_veto_only_disables_the_winning_registration, disabled_winner_remains_a_veto_when_the_catalog_is_extended, source_precedence_preserves_the_winning_registration, runtime_config_with_context);外部调用 3 个(into, new, Extension)。

McpServerRegistration::new152–164 ↗
fn new(
        name: String,
        source: McpServerSource,
        config: McpServerConfig,
        precedence: RegistrationPrecedence,
    ) -> Self

作用:这是创建服务器声明的底层小工厂。前面那些 from_config、from_plugin 等函数都会用它,避免每个地方重复拼结构。

数据流:输入名字、来源、配置和优先级 → 原样放进 McpServerRegistration 的字段里 → 返回一条完整声明。

调用关系:它是各种 from_ 构造函数的共同终点。外部通常不直接用它,因为不同来源需要不同优先级,交给 from_ 更不容易出错。

CatalogAction::name194–199 ↗
fn name(&self) -> &str

作用:取出一个清单动作对应的服务器名字。无论动作是注册还是移除,都能用同一种方式拿到名字。

数据流:输入一个 CatalogAction → 如果是 Register 就读 registration.name,如果是 Remove 就读 remove 里的 name → 返回名字的只读引用。

调用关系:McpCatalogBuilder::build 在按名字分组和选胜者时反复调用它。它让注册动作和移除动作可以被放在同一套流程里处理。

CatalogAction::precedence201–206 ↗
fn precedence(&self) -> RegistrationPrecedence

作用:取出一个动作的优先级。裁决同名服务器时,系统靠这个判断谁覆盖谁。

数据流:输入一个 CatalogAction → 如果是注册,就读注册里的 precedence;如果是移除,就读移除动作里的 precedence → 返回优先级值。

调用关系:McpCatalogBuilder::build 会先用它排序所有动作,再用它判断冲突档位。它是整个“谁说了算”规则的入口之一。

CatalogAction::conflict_action208–215 ↗
fn conflict_action(&self) -> McpServerConflictAction

作用:把内部动作转成对外可读的冲突记录。它会说明某个来源是在注册服务器,还是在移除服务器。

数据流:输入一个 CatalogAction → 读取动作来源 → 输出 McpServerConflictAction::Register 或 McpServerConflictAction::Remove,并带上来源副本。

调用关系:McpCatalogBuilder::build 在发现同名同档冲突时调用它。这样最终的 conflicts 列表不是一堆内部数据,而是能解释冲突双方在做什么。

调用图:外部调用 2 个(Register, Remove)。

McpCatalogBuilder::register226–229 ↗
fn register(&mut self, registration: McpServerRegistration)

作用:把一条服务器声明加入待裁决清单。它只是先收集材料,还不马上决定胜负。

数据流:输入 McpServerRegistration → 包成 CatalogAction::Register → 放进 builder 的 actions 列表里,返回空结果但改变 builder 内部状态。

调用关系:各种来源创建好 McpServerRegistration 后会调用它。真正排序、覆盖、禁用和冲突记录都等到 McpCatalogBuilder::build 再做。

调用图:外部调用 2 个(new, Register)。

McpCatalogBuilder::disable232–234 ↗
fn disable(&mut self, name: String)

作用:按名字标记一个 MCP 服务器为禁用。可以把它想成在名单上给某个名字贴了“不要启用”的便签。

数据流:输入服务器名字 → 放入 disabled_server_names 集合 → 之后 build 时看到这个名字,就会把对应胜出服务器的 enabled 改成 false。

调用关系:旧式按名字禁用的配置会走这里。McpCatalogBuilder::build 会读取这份禁用名单,并结合来源规则决定禁用是否继续影响后续同名服务器。

调用图:外部调用 1 个(insert)。

McpCatalogBuilder::remove_compatibility236–242 ↗
fn remove_compatibility(&mut self, name: String, id: impl Into<String>)

作用:加入一条“兼容层要移除某个服务器”的动作。它不是直接删除,而是参与同一套优先级裁决。

数据流:输入服务器名字和兼容项 ID → 转成 Compatibility 来源的 Remove 动作 → 放进 actions 列表,等待 build 时和注册动作比较。

调用关系:兼容层需要撤掉某个服务器时会调用它。McpCatalogBuilder::build 会把这个移除动作和注册动作一起排序,最后可能让它成为胜出动作。

调用图:外部调用 1 个(into)。

McpCatalogBuilder::remove_extension244–255 ↗
fn remove_extension(
        &mut self,
        name: String,
        id: impl Into<String>,
        contribution_order: usize,
    )

作用:加入一条“扩展要移除某个服务器”的动作。扩展移除也按扩展优先级和贡献顺序参与裁决。

数据流:输入服务器名字、扩展 ID 和贡献顺序 → 转成 Extension 来源的 Remove 动作 → 放进 actions 列表,等待最终构建。

调用关系:扩展想覆盖或撤销某个 MCP 服务器时会用它。McpCatalogBuilder::build 最后决定这条移除是否赢过其他注册动作。

调用图:外部调用 2 个(into, Extension)。

McpCatalogBuilder::build257–322 ↗
fn build(mut self) -> ResolvedMcpCatalog

作用:这是本文件的核心裁判。它把之前收集到的注册、移除、禁用全部算一遍,产出最终可用的 MCP 服务器清单。

数据流:输入 builder 里累计的 actions 和 disabled_server_names → 先按优先级稳定排序,再按服务器名选出最后胜出的动作,同时记录同名同档冲突 → 对胜出的注册项应用禁用规则,过滤掉胜出的移除项 → 输出 ResolvedMcpCatalog,里面有最终服务器、冲突记录、原始动作和延续后的禁用名单。

调用关系:所有 register、disable、remove_* 都是在给它准备材料。它会调用 CatalogAction::name、CatalogAction::precedence、RegistrationPrecedence::tier、CatalogAction::conflict_action 和 McpServerSource::disabled_registration_is_name_veto,最后生成只读的 ResolvedMcpCatalog 给运行时查询。

调用图:外部调用 3 个(new, new, new)。

ResolvedMcpServer::source333–335 ↗
fn source(&self) -> &McpServerSource

作用:读取一个最终胜出服务器的来源。比如它来自配置、插件、扩展,还是兼容层。

数据流:输入一个 ResolvedMcpServer → 只读取 source 字段 → 返回来源的只读引用,不改变服务器配置。

调用关系:ResolvedMcpCatalog::plugin_attributions_by_server_name 和 selected_plugin_server_names 会用它判断服务器来源。外部代码也可用它展示来源或做策略判断。

ResolvedMcpServer::config337–339 ↗
fn config(&self) -> &McpServerConfig

作用:读取一个最终胜出服务器的配置。调用者可以拿它去真正启动或连接 MCP 服务器。

数据流:输入一个 ResolvedMcpServer → 只读取 config 字段 → 返回配置的只读引用,不修改配置。

调用关系:外部运行时查询某个服务器时通常会先通过 ResolvedMcpCatalog::server 拿到对象,再用它取配置。ResolvedMcpCatalog::configured_servers 也会读取这些配置并复制出去。

ResolvedMcpCatalog::builder352–354 ↗
fn builder() -> McpCatalogBuilder

作用:创建一个空的清单构建器。调用者从这里开始收集 MCP 服务器声明。

数据流:没有业务输入 → 使用默认值创建 McpCatalogBuilder,里面动作列表为空、禁用名单为空 → 返回这个 builder。

调用关系:许多测试和运行时代码都从它开始。之后会陆续调用 register、disable、remove_*,最后调用 McpCatalogBuilder::build 得到解析后的清单。

调用图:被 12 处调用(disabled_discovered_plugin_remains_a_veto_for_runtime_overlays, disabled_selected_plugin_does_not_veto_runtime_overlays, disabled_veto_only_disables_the_winning_registration, disabled_winner_remains_a_veto_when_the_catalog_is_extended, earlier_plugin_wins_with_an_explicit_conflict, equal_precedence_uses_insertion_order_not_source_identity, selected_plugins_override_discovered_plugins_but_not_config, source_precedence_preserves_the_winning_registration, effective_mcp_servers_preserve_runtime_servers, selected_mcp_attribution_does_not_join_an_unrelated_local_summary (+2 more));外部调用 1 个(default)。

ResolvedMcpCatalog::to_builder356–361 ↗
fn to_builder(&self) -> McpCatalogBuilder

作用:把已经解析好的清单,重新变回一个可继续添加动作的 builder。适合在现有结果上叠加新的运行时覆盖。

数据流:输入当前 ResolvedMcpCatalog → 复制原来的 actions 和 disabled_server_names → 返回新的 McpCatalogBuilder,不直接改变原清单。

调用关系:当系统已有一份基础 MCP 清单,又要追加扩展或运行时变化时会用它。追加完后再调用 McpCatalogBuilder::build 重新裁决。

调用图:外部调用 1 个(clone)。

ResolvedMcpCatalog::server363–365 ↗
fn server(&self, name: &str) -> Option<&ResolvedMcpServer>

作用:按名字查找最终胜出的 MCP 服务器。找不到就说明这个名字没有可用的胜出注册。

数据流:输入服务器名字 → 在 servers 映射表里查找 → 找到则返回 ResolvedMcpServer 的只读引用,找不到返回 None。

调用关系:运行时准备启动某个具体 MCP 服务器时会调用它。它只查询 build 之后的最终结果,不再重新计算优先级。

ResolvedMcpCatalog::configured_servers367–372 ↗
fn configured_servers(&self) -> HashMap<String, McpServerConfig>

作用:导出所有最终服务器的配置表。调用者可以拿到一个普通的名字到配置的映射,方便交给其他模块使用。

数据流:输入当前 resolved catalog → 遍历所有胜出服务器,复制服务器名字和配置 → 返回 HashMap<String, McpServerConfig>。

调用关系:需要把 MCP 清单交给实际启动或配置消费方时会调用它。它不带来源信息,只给出最终配置。

ResolvedMcpCatalog::plugin_attributions_by_server_name375–388 ↗
fn plugin_attributions_by_server_name(&self) -> HashMap<String, McpPluginAttribution>

作用:列出哪些最终服务器来自插件,以及分别来自哪个插件。这样工具调用或界面展示时能说明来源。

数据流:输入当前 resolved catalog → 遍历所有胜出服务器,筛出 Plugin 和 SelectedPlugin 来源 → 复制服务器名和插件归属信息 → 返回名字到插件归属的映射。

调用关系:工具归属统计、来源展示和相关测试会用它。它会调用 ResolvedMcpServer::source 来判断每个服务器是不是插件带来的。

ResolvedMcpCatalog::selected_plugin_server_names391–395 ↗
fn selected_plugin_server_names(&self) -> impl Iterator<Item = &str>

作用:找出最终清单中由“当前线程选择的插件”提供的服务器名字。它只关心这个特殊来源。

数据流:输入当前 resolved catalog → 遍历所有服务器,检查 source 是否为 SelectedPlugin → 输出一个迭代器,逐个给出符合条件的服务器名。

调用关系:运行时需要知道本次上下文临时选中了哪些插件服务器时会用它。它同样依靠 ResolvedMcpServer::source 判断来源。

ResolvedMcpCatalog::conflicts397–399 ↗
fn conflicts(&self) -> &[McpServerConflict]

作用:读取构建过程中记录的冲突列表。这里的冲突指同一个名字、同一个优先级档位里出现了多个注册或移除动作。

数据流:输入当前 resolved catalog → 只读取 conflicts 字段 → 返回冲突列表的只读切片,不修改任何结果。

调用关系:日志、诊断、测试或用户提示会调用它,解释为什么某个服务器最后由某个来源胜出。冲突是在 McpCatalogBuilder::build 里提前算好的。

core/src/mcp.rs源码 ↗
orchestrationconfig load / runtime server resolution

这个文件的核心是 McpManager,像一个“总调度员”。用户配置里可能写了 MCP 服务器,插件也可能带来服务器,宿主程序安装的扩展还能临时添加、覆盖或删除服务器;另外还有一个兼容旧版本的内置 Codex Apps 服务器。没有这个文件,各处就得自己拼配置,很容易出现同名服务器互相覆盖、插件来源说不清、登录权限没过滤等问题。它先收集扩展给出的动作:添加、删除,或者把某个插件选中的服务器登记进来;再把普通配置和插件配置转成统一的 MCP 配置;然后按顺序应用内置兼容项和扩展“覆盖层”。如果多个来源抢同一个名字,它不会直接崩掉,而是按目录的规则得出结果,并写警告日志。最后,它还提供三种视角:只看静态配置、看运行时扩展后的配置、以及经过登录权限过滤后真正可用的服务器。

函数细节8
McpManager::new44–49 ↗
fn new(plugins_manager: Arc<PluginsManager>) -> Self

作用:创建一个最普通的 MCP 管理器,只带插件管理器,不带宿主扩展。常用于命令行、测试或还不需要扩展参与的场景。

数据流:进去的是一个 PluginsManager,也就是插件清单和插件能力的入口;函数给扩展部分放入一个空的扩展注册表;出来的是一个 McpManager,之后它就能基于配置和插件计算 MCP 服务器。

调用关系:它会调用 empty_extension_registry(ext) 准备一个“没有扩展”的默认环境。run_get、run_list、run_login、run_logout 以及一些会话和测试辅助流程会用它来快速得到一个可工作的 MCP 管理器。

调用图:被 14 处调用(run_get, run_list, run_login, run_logout, list_accessible_connectors_from_mcp_tools_with_environment_manager, guardian_subagent_does_not_inherit_parent_exec_policy_rules, make_session_and_context, make_session_and_context_with_auth_config_home_and_rx, make_session_with_config_and_rx, make_session_with_history_source_and_agent_control_and_rx (+4 more));外部调用 1 个(empty_extension_registry)。

McpManager::new_with_extensions52–60 ↗
fn new_with_extensions(
        plugins_manager: Arc<PluginsManager>,
        extensions: Arc<ExtensionRegistry<Config>>,
    ) -> Self

作用:创建一个带扩展注册表的 MCP 管理器。也就是说,除了用户配置和插件,它还会考虑宿主程序安装的扩展贡献出来的 MCP 服务器。

数据流:进去的是插件管理器和扩展注册表;函数只是把这两样保存进 McpManager;出来的是一个后续能读取扩展贡献的管理器实例。

调用关系:它用于需要扩展参与的初始化路径,比如 installed_manager,或者测试“后来的扩展能删除同名注册项”的场景。之后真正合并扩展贡献的活儿会交给 runtime_config_with_context。

调用图:被 3 处调用(new, installed_manager, later_extension_can_remove_same_name_registration)。

McpManager::runtime_config64–67 ↗
async fn runtime_config(&self, config: &Config) -> McpConfig

作用:生成全局运行时 MCP 配置。它会把静态配置、插件、扩展临时贡献和兼容内置项都算进去。

数据流:进去的是项目的 Config;函数把它交给 runtime_config_with_context,并且不传线程专属初始化数据;出来的是完整的 McpConfig,也就是运行时要参考的 MCP 总配置。

调用关系:它是 runtime_servers 和 effective_servers 的前置步骤。这两个函数都先用它拿到运行时配置,再分别提取服务器列表或做权限过滤。

调用图:调用 1 个内部函数(runtime_config_with_context);被 2 处调用(effective_servers, runtime_servers)。

McpManager::runtime_config_for_thread69–76 ↗
async fn runtime_config_for_thread(
        &self,
        config: &Config,
        thread_init: &ExtensionDataInit,
    ) -> McpConfig

作用:生成某个具体线程或会话专用的运行时 MCP 配置。它适合扩展需要根据当前线程初始化数据来决定贡献哪些服务器的情况。

数据流:进去的是全局 Config 和 ExtensionDataInit,也就是线程启动时给扩展看的初始化资料;函数把这些交给 runtime_config_with_context;出来的是带线程上下文影响的 McpConfig。

调用关系:它和 runtime_config 用的是同一套核心算法,只是多传了线程上下文。真正的收集、合并、冲突处理都在 runtime_config_with_context 里完成。

调用图:调用 1 个内部函数(runtime_config_with_context)。

McpManager::runtime_config_with_context78–183 ↗
async fn runtime_config_with_context(
        &self,
        config: &Config,
        thread_init: Option<&ExtensionDataInit>,
    ) -> McpConfig

作用:这是本文件最核心的函数:真正把配置、插件、扩展动作和旧版兼容内置服务器合成一份最终运行时 MCP 配置。

数据流:进去的是 Config,以及可选的线程初始化数据;它先建立扩展贡献时需要看的上下文,再询问每个扩展要添加、删除或选择哪些服务器;接着把用户配置和插件选择转成 McpConfig;然后按顺序加入或移除内置 Codex Apps 服务器和扩展覆盖项;最后检查同名冲突并写警告,出来的是已经合并好的 McpConfig。

调用关系:runtime_config 和 runtime_config_for_thread 都把工作交给它。它会调用 for_thread 或 global 创建扩展上下文,调用 to_mcp_config_with_plugin_registrations(ext) 合并配置和插件,调用 from_selected_plugin、from_compatibility、from_extension 生成不同来源的服务器登记项。它是整个 MCP 运行时配置流程的中枢。

调用图:调用 6 个内部函数(new, from_compatibility, from_extension, from_selected_plugin, for_thread, global);被 2 处调用(runtime_config, runtime_config_for_thread);外部调用 4 个(new, to_mcp_config_with_plugin_registrations, codex_apps_mcp_server_config, warn!)。

McpManager::configured_servers186–189 ↗
async fn configured_servers(&self, config: &Config) -> HashMap<String, McpServerConfig>

作用:只返回用户配置和插件本身带来的 MCP 服务器,不加入运行时扩展贡献。适合想看“静态配置里到底有什么”的场景。

数据流:进去的是 Config;函数先通过 to_mcp_config(ext) 把配置和插件转成 MCP 配置;再用 configured_mcp_servers(ext) 提取服务器映射;出来的是按名字索引的 McpServerConfig 列表。

调用关系:它不走 runtime_config_with_context,所以不会应用扩展覆盖层,也不会加入运行时临时贡献。它给调用者一个更朴素、更接近配置文件本身的视角。

调用图:外部调用 2 个(to_mcp_config, configured_mcp_servers)。

McpManager::runtime_servers192–195 ↗
async fn runtime_servers(&self, config: &Config) -> HashMap<String, McpServerConfig>

作用:返回运行时能看到的 MCP 服务器,但还没有按登录权限做最后过滤。它适合想看“配置加扩展之后有哪些候选服务器”的场景。

数据流:进去的是 Config;函数先调用 runtime_config 得到合并后的运行时配置;再用 configured_mcp_servers(ext) 把其中的服务器取出来;出来的是服务器名字到配置的映射。

调用关系:它站在 runtime_config 后面,是运行时配置的一个简化视图。和 effective_servers 不同,它不接收 auth,也不判断当前用户是否真的有权使用某些服务器。

调用图:调用 1 个内部函数(runtime_config);外部调用 1 个(configured_mcp_servers)。

McpManager::effective_servers198–205 ↗
async fn effective_servers(
        &self,
        config: &Config,
        auth: Option<&CodexAuth>,
    ) -> HashMap<String, EffectiveMcpServer>

作用:返回当前用户真正可用的 MCP 服务器。它会在运行时配置的基础上,再结合登录认证信息做权限过滤。

数据流:进去的是 Config 和可选的 CodexAuth,CodexAuth 可以理解成当前登录身份和凭据;函数先调用 runtime_config 得到完整候选配置;再用 effective_mcp_servers(ext) 根据认证状态筛掉不可用项;出来的是按名字索引的 EffectiveMcpServer,也就是实际可连接的服务器信息。

调用关系:它通常是启动连接外部工具前最实用的一步。它依赖 runtime_config 完成配置合并,再把最后的权限判断交给 effective_mcp_servers(ext)。

调用图:调用 1 个内部函数(runtime_config);外部调用 1 个(effective_mcp_servers)。

模型与提供方预设

这些文件定义提供方注册表、模型管理器配置和助手、协作预设、审批预设,以及围绕模型数据和更新操作的轻量 TUI 封装。

model-provider-info/src/lib.rs源码 ↗
configconfig load / startup / request preparation

Codex 要和不同的模型服务说话,比如 OpenAI、Amazon Bedrock、本机 Ollama、LM Studio。这个文件定义了“供应商配置”长什么样,也给出几套默认配置。它会检查配置有没有互相打架,比如 AWS 签名认证不能同时再配 API Key;也会把用户写在配置文件里的内容变成真正发请求时需要的地址、请求头、重试和超时设置。可以把它想成出门前整理好的联系人卡片:名字、地址、钥匙放哪、特殊备注都写清楚。一个重要行为是:旧的 chat 通讯方式和 ollama-chat 已经不再支持,会明确报错提示用户改法。它还会把用户自定义供应商合并进默认列表,但对内置 Bedrock 只允许改 AWS profile 和 region,防止用户不小心把内置规则改坏。

函数细节20
WireApi::fmt60–65 ↗
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result

作用:把 WireApi 这个内部枚举值转成给人看的文字。目前只有 responses,也就是 Codex 现在支持的模型接口协议。

数据流:进去的是一个 WireApi 值和一个输出缓冲区 → 它判断这个值对应的字符串 → 把 responses 写进输出缓冲区,不改别的配置。

调用关系:当日志、错误信息或配置展示需要把通讯协议打印出来时会用到它;它只负责把值写成文字,真正的协议选择由供应商配置决定。

调用图:外部调用 1 个(write_str)。

WireApi::deserialize69–79 ↗
fn deserialize(deserializer: D) -> Result<Self, D::Error>

作用:把配置文件里的协议文字读成程序内部能用的 WireApi。它还专门拦截旧的 chat 写法,给用户一个清楚的迁移提示。

数据流:进去的是配置里的字符串 → 它先读出文字,如果是 responses 就变成内部值;如果是 chat 就返回“已不支持”的错误;其他乱写的值返回“未知选项”错误 → 出来的是可用协议或错误信息。

调用关系:配置文件被读取时会经过这里;它不负责发网络请求,只负责在配置入口处把不支持的老写法挡住,避免后面运行时才出怪问题。

调用图:外部调用 3 个(deserialize, custom, unknown_variant)。

ModelProviderInfo::validate150–208 ↗
fn validate(&self) -> std::result::Result<(), String>

作用:检查一个模型供应商配置是不是自相矛盾。它的作用是尽早发现“同一个服务同时用了两套认证方式”这类危险配置。

数据流:进去的是一个供应商配置 → 它查看 AWS 认证、命令认证、环境变量 API Key、明文 token、OpenAI 登录等字段是否冲突,也检查命令认证的命令不能为空 → 出来是成功,或一段说明哪里冲突的错误文字。

调用关系:通常在加载或接受用户配置后使用;它不生成新配置,也不连网络,只是在配置进入正式使用前当一道安全检查门。

调用图:外部调用 2 个(new, format!)。

ModelProviderInfo::build_header_map210–235 ↗
fn build_header_map(&self) -> CodexResult<HeaderMap>

作用:把配置里的 HTTP 请求头整理成真正发网络请求时能用的格式。HTTP 请求头可以理解成随请求一起递上的小纸条,比如组织 ID、项目 ID 或服务要求的特殊标记。

数据流:进去的是供应商配置里的固定请求头和“从环境变量读取的请求头” → 它逐个检查名字和值是否合法,读取需要的环境变量,并跳过空值或非法值 → 出来是一张可直接放进网络请求里的请求头表。

调用关系:它被 ModelProviderInfo::to_api_provider 调用,是把配置变成实际 API 连接信息的一步;它只准备请求头,不决定请求地址和重试规则。

调用图:被 1 处调用(to_api_provider);外部调用 4 个(with_capacity, try_from, try_from, var)。

ModelProviderInfo::to_api_provider237–273 ↗
fn to_api_provider(&self, auth_mode: Option<AuthMode>) -> CodexResult<ApiProvider>

作用:把这里的通用供应商配置,转换成底层 API 客户端真正要用的连接对象。也就是从“配置卡片”变成“可以出发拨号的电话簿条目”。

数据流:进去的是供应商配置和可选的登录方式 → 它选择默认网址,生成请求头,计算请求重试次数和流式响应超时 → 出来是 ApiProvider,里面包含名称、地址、查询参数、请求头、重试策略和超时设置。

调用关系:外部的 list_models 会在列模型时调用它;它内部把请求头准备交给 ModelProviderInfo::build_header_map,把重试和超时交给对应的小函数计算。

调用图:调用 3 个内部函数(build_header_map, request_max_retries, stream_idle_timeout);被 1 处调用(list_models);外部调用 2 个(from_millis, matches!)。

ModelProviderInfo::api_key278–294 ↗
fn api_key(&self) -> CodexResult<Option<String>>

作用:从环境变量里取这个供应商需要的 API Key。API Key 可以理解成访问服务的钥匙,放环境变量里比直接写配置文件更安全。

数据流:进去的是供应商配置里的 env_key 名字 → 如果配置要求某个环境变量,它就去系统环境里读,空值也算没有;读不到时返回带说明的错误;如果没要求环境变量,就返回没有 key → 它不修改配置。

调用关系:它会被 realtime_api_keybearer_auth_for_provider 调用,用在真正需要给请求加认证信息的时候;它只找钥匙,不负责发送请求。

调用图:被 2 处调用(realtime_api_key, bearer_auth_for_provider);外部调用 1 个(var)。

ModelProviderInfo::request_max_retries297–301 ↗
fn request_max_retries(&self) -> u64

作用:算出普通 HTTP 请求最多重试几次。这样服务短暂出错时 Codex 可以再试,但不会无限重试卡死。

数据流:进去的是配置里可选的重试次数 → 如果没填就用默认值;如果填得太大就压到安全上限 100 → 出来是最终生效的次数。

调用关系:它被 ModelProviderInfo::to_api_provider 调用,用来组装底层 API 客户端的重试策略。

调用图:被 1 处调用(to_api_provider)。

ModelProviderInfo::stream_max_retries304–308 ↗
fn stream_max_retries(&self) -> u64

作用:算出流式响应断线后最多重连几次。流式响应就是模型一边生成一边往外吐内容,像边说边传。

数据流:进去的是配置里可选的流式重连次数 → 没填就用默认值;填得过大就限制到 100 → 出来是最终重连次数。

调用关系:这个值供处理流式连接的代码使用;本文件只负责给出安全的最终数字,不负责实际重连。

ModelProviderInfo::stream_idle_timeout311–315 ↗
fn stream_idle_timeout(&self) -> Duration

作用:算出流式响应多久没动静就算连接可能坏了。这样 Codex 不会一直傻等一个已经断掉的连接。

数据流:进去的是配置里的毫秒数 → 如果填了就转成程序使用的时间长度;没填就用默认 300 秒 → 出来是一个 Duration 时间对象。

调用关系:它被 ModelProviderInfo::to_api_provider 调用,放进底层 API 连接配置里,供后续网络请求判断流是否闲置太久。

调用图:被 1 处调用(to_api_provider);外部调用 1 个(from_millis)。

ModelProviderInfo::websocket_connect_timeout318–322 ↗
fn websocket_connect_timeout(&self) -> Duration

作用:算出建立 WebSocket 连接最多等多久。WebSocket 是一种长连接,适合双方持续来回传消息。

数据流:进去的是配置里的 WebSocket 连接超时毫秒数 → 填了就使用它,没填就用默认 15 秒 → 出来是一个时间长度。

调用关系:支持 WebSocket 的连接代码会用这个值;本函数只负责把配置变成可用时间,不实际打开连接。

调用图:外部调用 1 个(from_millis)。

ModelProviderInfo::create_openai_provider324–359 ↗
fn create_openai_provider(base_url: Option<String>) -> ModelProviderInfo

作用:创建内置的 OpenAI 供应商配置。这样用户不写任何复杂配置,Codex 也知道怎么连 OpenAI 或 ChatGPT Codex 后端。

数据流:进去的是可选的基础网址 → 它填好名字、网址、默认协议、版本请求头、可从环境变量读取的组织和项目请求头,并标记需要 OpenAI 登录且支持 WebSocket → 出来是一份完整的 OpenAI 配置。

调用关系:它被默认供应商列表和很多测试使用;built_in_model_providers 会调用它,把 OpenAI 放进 Codex 开箱即用的供应商目录。

调用图:被 17 处调用(model_client_with_counting_attestation, test_model_client_session, installed_extension_contributes_web_run_when_enabled, test_personal_access_token_uses_chatgpt_codex_base_url, test_supports_remote_compaction_for_openai, test_validate_provider_aws_rejects_conflicting_auth, test_validate_provider_aws_rejects_websockets, openai_provider_rejects_bedrock_api_key_auth, provider_info_with_command_auth, provider_without_command_auth_reports_no_command_auth (+7 more));外部调用 1 个(env!)。

ModelProviderInfo::create_amazon_bedrock_provider361–389 ↗
fn create_amazon_bedrock_provider(
        aws: Option<ModelProviderAwsAuthInfo>,
    ) -> ModelProviderInfo

作用:创建内置的 Amazon Bedrock 供应商配置。Bedrock 使用 AWS 的认证方式,所以它和普通 API Key 供应商的规则不一样。

数据流:进去的是可选的 AWS profile 和 region 配置 → 它填好 Bedrock 默认地址、AWS 认证信息、专用请求头,并关闭 OpenAI 登录和 WebSocket 支持 → 出来是一份完整的 Bedrock 配置。

调用关系:它被默认供应商列表、模型创建逻辑和测试使用;后续 merge_configured_model_providers 允许用户只覆盖它的 AWS profile 和 region。

调用图:被 14 处调用(guardian_review_session_config_keeps_bedrock_provider_for_bedrock_gpt_5_4, use_bedrock_provider, test_amazon_bedrock_provider_adds_mantle_client_agent_header, api_provider_for_bedrock_bearer_token_uses_configured_region_endpoint, approval_review_preferred_model_uses_bedrock_gpt_5_4, capabilities_disable_unsupported_hosted_tools, managed_auth_takes_precedence_over_aws_auth, openai_auth_is_not_exposed_to_bedrock, amazon_bedrock_provider_creates_static_models_manager, amazon_bedrock_provider_returns_bedrock_account_state (+4 more));外部调用 1 个(from)。

ModelProviderInfo::is_openai391–393 ↗
fn is_openai(&self) -> bool

作用:判断这个供应商是不是内置 OpenAI。很多功能只对 OpenAI 或类似服务开放,需要一个简单判断。

数据流:进去的是供应商配置 → 它比较配置里的名字是否等于 OpenAI 的标准名字 → 出来是真或假,不改任何数据。

调用关系:它被 realtime_api_keyModelProviderInfo::supports_remote_compaction 使用;作为小开关,帮助其他流程决定是否走 OpenAI 专用能力。

调用图:被 2 处调用(realtime_api_key, supports_remote_compaction)。

ModelProviderInfo::is_amazon_bedrock395–397 ↗
fn is_amazon_bedrock(&self) -> bool

作用:判断这个供应商是不是内置 Amazon Bedrock。这样创建模型客户端时可以走 Bedrock 专门的认证和端点规则。

数据流:进去的是供应商配置 → 它比较名字是否等于 Amazon Bedrock 的标准名字 → 出来是真或假。

调用关系:它被外部的 create_model_provider 使用;这个判断本身很小,但能让后面的创建流程选择正确分支。

调用图:被 1 处调用(create_model_provider)。

ModelProviderInfo::supports_remote_compaction399–401 ↗
fn supports_remote_compaction(&self) -> bool

作用:判断这个供应商是否支持远程压缩上下文。远程压缩可以理解成把太长的对话整理变短,好让模型继续处理。

数据流:进去的是供应商配置 → 它先看是不是 OpenAI,再看是不是 Azure 上兼容 Responses API 的供应商 → 出来是真或假。

调用关系:它被 should_use_remote_compact_task 调用,用来决定是否把压缩工作交给远端服务;它内部复用 ModelProviderInfo::is_openai,并调用外部的 Azure 判断工具。

调用图:调用 1 个内部函数(is_openai);被 1 处调用(should_use_remote_compact_task);外部调用 1 个(is_azure_responses_provider)。

ModelProviderInfo::has_command_auth403–405 ↗
fn has_command_auth(&self) -> bool

作用:判断这个供应商是否使用“运行一个命令来拿 token”的认证方式。适合某些企业或代理环境,钥匙不是固定写死的。

数据流:进去的是供应商配置 → 它查看 auth 字段有没有内容 → 出来是真或假。

调用关系:它被外部同名流程 has_command_auth 调用;本函数只是提供一个清晰的小判断,让上层知道是否需要执行认证命令。

调用图:被 1 处调用(has_command_auth)。

built_in_model_providers415–441 ↗
fn built_in_model_providers(
    openai_base_url: Option<String>,
) -> HashMap<String, ModelProviderInfo>

作用:生成 Codex 自带的供应商清单。没有它,用户第一次运行时就得手动写 OpenAI、Bedrock、Ollama、LM Studio 等基础配置。

数据流:进去的是可选的 OpenAI 基础网址 → 它创建 OpenAI、Amazon Bedrock、本机 Ollama、本机 LM Studio 四类默认供应商 → 出来是一张以供应商 ID 为键的配置表。

调用关系:启动或加载配置时会先拿到这张默认表;它会调用 ModelProviderInfo::create_openai_providerModelProviderInfo::create_amazon_bedrock_providercreate_oss_provider 来拼出完整目录。

调用图:调用 1 个内部函数(create_oss_provider);外部调用 2 个(create_amazon_bedrock_provider, create_openai_provider)。

merge_configured_model_providers448–479 ↗
fn merge_configured_model_providers(
    mut model_providers: HashMap<String, ModelProviderInfo>,
    configured_model_providers: HashMap<String, ModelProviderInfo>,
) -> Result<HashMap<String, ModelP

作用:把用户配置文件里的供应商合并进内置供应商列表。它既允许用户添加新服务,也保护关键内置服务不被随便改坏。

数据流:进去的是内置供应商表和用户配置的供应商表 → 它逐个处理:普通新 ID 只在内置没有时加入;Amazon Bedrock 只允许覆盖 AWS profile 和 region,其他字段一旦改动就报错 → 出来是合并后的供应商表或错误。

调用关系:配置加载阶段会用它把默认值和用户设置拼在一起;它不创建网络客户端,只决定最终有哪些供应商配置可用。

调用图:外部调用 2 个(format!, default)。

create_oss_provider481–498 ↗
fn create_oss_provider(default_provider_port: u16, wire_api: WireApi) -> ModelProviderInfo

作用:创建本机开源模型服务的默认配置,比如 Ollama 或 LM Studio。它会优先看实验性的环境变量,让用户不用改配置文件也能换端口或地址。

数据流:进去的是默认端口和通讯协议 → 它读取 CODEX_OSS_PORTCODEX_OSS_BASE_URL,没有就拼出 http://localhost:端口/v1 → 然后交给 create_oss_provider_with_base_url 生成完整配置。

调用关系:它被 built_in_model_providers 调用,用来生成 Ollama 和 LM Studio 的默认条目;它负责决定地址,具体填配置字段的活交给 create_oss_provider_with_base_url

调用图:调用 1 个内部函数(create_oss_provider_with_base_url);被 1 处调用(built_in_model_providers);外部调用 2 个(format!, var)。

create_oss_provider_with_base_url500–520 ↗
fn create_oss_provider_with_base_url(base_url: &str, wire_api: WireApi) -> ModelProviderInfo

作用:用指定地址创建一个开源模型供应商配置。它适合在已经知道服务地址时直接生成标准配置。

数据流:进去的是基础网址和通讯协议 → 它填好名字 gpt-oss、地址、协议,并关闭 API Key、OpenAI 登录、WebSocket 等不需要的功能 → 出来是一份本机开源模型供应商配置。

调用关系:它被 create_oss_provider 调用,是创建开源模型供应商的最后一步;上层负责算地址,它负责把地址装进统一的供应商结构里。

调用图:被 1 处调用(create_oss_provider)。

models-manager/src/config.rs源码 ↗
configconfig load / cross-cutting

这个文件很小,但很重要。它只定义了一个配置结构 ModelsManagerConfig,可以把它理解成一张“模型使用说明表”。这张表里记录了模型最多能看多少内容、什么时候该自动压缩对话、工具输出最多保留多少、默认给模型的基础指令是什么,以及这个模型是否支持“推理摘要”等能力。很多字段都用 Option 包起来,意思是“这个值可以有,也可以没有”;如果没有,程序后面可能会使用默认值或从别处补上。它还可以带一个模型目录 model_catalog,也就是可用模型及其信息的清单。这个文件本身不执行动作,只提供统一的数据形状,让其他代码能用同一种方式读取这些设置。

models-manager/src/model_presets.rs源码 ↗
configconfig load / migration compatibility

这个文件很小,但作用是“别让旧用户的配置突然失效”。过去系统里可能有一些写死的模型预设,也会用配置项记录某些迁移提示是否已经隐藏。现在模型列表不再靠代码里写死,而是从当前启用的模型目录里动态拿。但老版本配置文件里可能还保存着这些开关名字。这里把两个旧的配置键作为常量留下来,就像给旧钥匙继续保留一个锁孔:新系统不一定主动使用旧机制,但看到旧配置时仍然知道它是什么意思。这样升级时更稳,不会因为改名或删除字符串导致重复弹提示、迁移状态丢失,或者老配置无法识别。

models-manager/src/lib.rs源码 ↗
othercross-cutting

这个文件像一本书的目录页加服务台。上面先声明了模型缓存、配置、管理器、模型信息、预设等模块,说明这个库由哪些零件组成;同时把 AuthMode 和 ModelsManagerConfig 重新公开出去,让调用方可以从这里直接拿到常用类型,不必知道它们原本藏在哪个文件里。它还提供两个小函数:一个把随程序一起打包的 models.json 读出来,并解析成“模型列表响应”;另一个把当前软件版本整理成只含主版本、次版本、补丁版本的形式,比如去掉 alpha 这类预发布后缀。没有这个文件,外部代码要使用模型管理功能会更分散、更容易依赖错内部细节。

函数细节2
bundled_models_response13–16 ↗
fn bundled_models_response() -> std::result::Result<codex_protocol::openai_models::ModelsResponse, serde_json::Error>

作用:读取程序自带的模型目录文件,并把它变成代码里能直接使用的模型列表。有人需要默认模型清单时,就会用它。

数据流:进去没有外部参数;它读取编译时塞进程序里的 models.json 文本,也就是随软件打包的模型清单;然后用 JSON 解析工具把这段文字转成 ModelsResponse 结构。出来的是解析成功后的模型列表,或者 JSON 格式不对时返回错误;它不改动外部状态。

调用关系:它处在“提供默认模型目录”的入口位置。函数内部先通过 include_str! 把外部文件内容嵌进程序,再交给 serde_json 的 from_str 做解析;调用它的上层代码不用关心文件在哪里,也不用自己解析 JSON。

调用图:外部调用 2 个(include_str!, from_str)。

client_version_to_whole19–26 ↗
fn client_version_to_whole() -> String

作用:生成当前客户端的完整三段式版本号,比如 1.2.3。它会去掉 alpha、beta 这类额外标签,方便和只认正式版本号的地方对接。

数据流:进去没有参数;它读取编译时由 Cargo 提供的版本号三部分:主版本、次版本、补丁版本;再用 format! 拼成“主.次.补”的字符串。出来的是一个新的版本号文本,不读取配置,也不修改任何东西。

调用关系:它是一个很小的版本号整理工具。需要上报、比较或显示客户端基础版本时,上层代码可以直接调用它;它自己只把编译期版本信息交给 format! 拼接,不再把工作转给项目里的其他函数。

调用图:外部调用 1 个(format!)。

models-manager/src/model_info.rs源码 ↗
domain_logicconfig load / model selection

系统在调用某个 AI 模型前,需要先知道这个模型的“使用说明”:比如上下文窗口有多大,也就是一次能记住多少内容;工具输出太长时按字节还是按令牌截断;要不要加入默认的 Codex 行为提示词。这个文件就像给每个模型办一张工作证。正常情况下,它会拿已有的模型资料,再按配置文件里的要求做覆盖;如果遇到一个不认识的模型代号,它也不会直接崩掉,而是造一份保守的备用资料,让系统还能继续工作。文件里还专门给少数本地人格模型准备了不同语气的提示词模板,比如更友好或更务实。一个重要细节是:如果用户自己提供了基础指令,它会关闭内置的人格消息,避免两套提示词互相打架。

函数细节3
with_config_overrides23–63 ↗
fn with_config_overrides(mut model: ModelInfo, config: &ModelsManagerConfig) -> ModelInfo

作用:这个函数把配置文件里的用户偏好,套到一份已有的模型资料上。有人会用它来确保最终拿去调用模型的参数,既尊重模型原本的能力边界,也尊重本地配置。

数据流:进去的是一份 ModelInfo 模型资料和一份 ModelsManagerConfig 配置。它会检查配置里有没有指定推理摘要支持、上下文窗口、自动压缩限制、工具输出长度限制、基础指令等;有就改,没有就保留原值。比如工具输出限制如果按“令牌”配置,它会根据模型原来的截断模式,转换成字节限制或令牌限制。出来的是改好后的 ModelInfo,原来的那份模型资料被按规则更新后返回。

调用关系:它通常在 construct_model_info_from_candidates 选出候选模型资料后被调用,作为“最后修订”步骤。修订过程中,它会借助 approx_bytes_for_tokens 把令牌数量粗略换算成字节数,也会调用 TruncationPolicyConfig::bytes 或 TruncationPolicyConfig::tokens 来生成新的截断规则。

调用图:调用 2 个内部函数(bytes, tokens);被 1 处调用(construct_model_info_from_candidates);外部调用 2 个(approx_bytes_for_tokens, try_from)。

model_info_from_slug66–108 ↗
fn model_info_from_slug(slug: &str) -> ModelInfo

作用:这个函数在只知道模型代号、但找不到完整模型资料时,临时造一份最小可用的备用资料。它的价值是让系统不要因为“不认识这个模型名”就立刻停工。

数据流:进去的是一个模型 slug,也就是模型的短名字或代号。函数先记录一条警告,告诉维护者这是未知模型,正在使用备用资料;然后把这个代号填进模型名称和显示名称,给它设置一组保守默认值,比如默认提示词、272000 的上下文窗口、10000 字节的工具输出截断限制、默认输入类型等。出来的是一份 ModelInfo,并且 used_fallback_model_metadata 会标成 true,表示这不是官方完整资料,而是兜底资料。

调用关系:当 model_with_default_service_tier、remote_model 或 construct_model_info_from_candidates 等流程发现没有现成模型资料时,会调用它兜底。它内部还会把活儿交给 local_personality_messages_for_slug,看看这个模型代号是不是需要本地人格提示词;同时调用 default_input_modalities 填入默认输入能力,并用 bytes 创建按字节截断的规则。

调用图:调用 3 个内部函数(local_personality_messages_for_slug, bytes, default_input_modalities);被 5 处调用(model_with_default_service_tier, remote_model, build_stage_one_input_message_truncates_rollout_using_model_context_window, build_stage_one_input_message_uses_default_limit_when_model_context_window_missing, construct_model_info_from_candidates);外部调用 2 个(new, warn!)。

local_personality_messages_for_slug110–124 ↗
fn local_personality_messages_for_slug(slug: &str) -> Option<ModelMessages>

作用:这个函数判断某个模型代号是否需要本地“人格提示词”。简单说,就是给特定模型准备不同说话风格的提示模板。

数据流:进去的是模型 slug。它会匹配少数已知代号,比如 gpt-5.2-codex 或 exp-codex-personality;如果匹配上,就拼出一套 ModelMessages,里面包含基础身份说明、人格占位符和 Codex 的完整基础指令,还提供“友好”和“务实”两种人格文本。如果没有匹配上,就返回 None,表示这个模型不使用这套本地人格消息。

调用关系:它只被 model_info_from_slug 调用,是备用模型资料生成过程中的一个小分支。model_info_from_slug 负责造整张模型说明卡,而这个函数只负责回答一个问题:这张卡里要不要附带本地人格提示词。

调用图:被 1 处调用(model_info_from_slug);外部调用 2 个(new, format!)。

models-manager/src/collaboration_mode_presets.rs源码 ↗
configconfig load / startup

这个文件像一张“出厂菜单”。系统里有不同的协作模式,简单说就是让 AI 用不同方式和用户配合:有的偏正常对话,有的偏先做计划。这里把内置模式包装成 CollaborationModeMask,也就是一份“模式配置清单”,里面写了模式名字、模式类型、是否指定模型、推理力度,以及要给模型看的开发者说明。默认模式的说明不是直接拼死字符串,而是用 Template(模板,把占位符替换成真实内容的文本工具)生成;它会把当前界面可见的模式名字填进去。LazyLock(一种用到时才初始化的静态缓存)保证模板只解析一次,既省事也避免重复开销。如果模板解析或渲染失败,程序会直接报错停止,因为这些内置模板坏了就说明发布包本身有问题。

函数细节5
builtin_collaboration_mode_presets16–18 ↗
fn builtin_collaboration_mode_presets() -> Vec<CollaborationModeMask>

作用:给外部一次性拿到所有系统内置的协作模式预设。别人想显示模式列表、筛选模式,或者建立默认配置时,会从这里取这份“出厂清单”。

数据流:进去不需要参数 → 它创建一个列表,把计划模式预设和默认模式预设放进去 → 出来的是一个 CollaborationModeMask 列表,表示系统自带的可用协作模式;它不改动外部状态。

调用关系:它处在这组功能的门口。调用图里显示,list_collaboration_modes 和 filtered_presets 这类列出、筛选模式的流程会来问它要内置预设;图里还记录了它被自身引用的关系,但从文件内容看,它的核心作用就是把各个预设打包成一个列表。

调用图:被 4 处调用(builtin_collaboration_mode_presets, list_collaboration_modes, list_collaboration_modes, filtered_presets);外部调用 1 个(vec!)。

plan_preset20–28 ↗
fn plan_preset() -> CollaborationModeMask

作用:生成“计划模式”的内置配置。这个模式会让 AI 更偏向先规划步骤,并把推理力度设为中等。

数据流:进去不需要参数 → 它读取 ModeKind::Plan 的显示名,填入计划模式类型,不指定具体模型,把 reasoning_effort 设成 Medium,并放入计划模式专用说明文本 → 出来是一份 CollaborationModeMask,代表“计划模式”这张预设卡片。

调用关系:它是内置预设列表里的一个零件。builtin_collaboration_mode_presets 需要组装完整列表时会用到它;它本身不再把工作交给别的本文件函数,只是把已有常量和枚举值装进配置结构里。

default_preset30–38 ↗
fn default_preset() -> CollaborationModeMask

作用:生成“默认模式”的内置配置。它代表用户没有特别选择时,系统该怎么和模型协作。

数据流:进去不需要参数 → 它读取默认模式的显示名和模式类型,不指定模型,也不强行设置推理力度,然后调用 default_mode_instructions 生成默认模式要给模型看的说明文字 → 出来是一份 CollaborationModeMask,代表“默认模式”的预设。

调用关系:它是内置预设列表中的另一个核心零件。builtin_collaboration_mode_presets 会把它放进返回列表;它把“说明文字怎么生成”这件事交给 default_mode_instructions,这样配置外壳和文本生成分开,比较清楚。

调用图:调用 1 个内部函数(default_mode_instructions)。

default_mode_instructions40–45 ↗
fn default_mode_instructions() -> String

作用:生成默认协作模式的开发者说明,也就是系统会交给模型看的行为提示。它会把“当前有哪些可见模式”填进默认模板里。

数据流:进去不需要参数 → 它先读取 TUI_VISIBLE_COLLABORATION_MODES,也就是终端界面里可见的协作模式列表;再调用 format_mode_names 把这些模式名变成一句人能读的文字;最后把这段文字塞进默认模板的 KNOWN_MODE_NAMES 占位符 → 出来是一整段默认模式说明文本。如果模板渲染失败,程序会直接报错。

调用关系:它被 default_preset 调用,专门负责准备默认模式的说明书。它往下把“模式名字怎么排成文字”交给 format_mode_names,再用已经懒加载解析好的模板生成最终文本。

调用图:调用 1 个内部函数(format_mode_names);被 1 处调用(default_preset)。

format_mode_names47–55 ↗
fn format_mode_names(modes: &[ModeKind]) -> String

作用:把一组模式名字整理成适合放进句子里的文字。比如没有模式就写“none”,两个模式就写“A and B”,多个模式就用逗号隔开。

数据流:进去的是一个 ModeKind 切片,也就是一小段模式列表 → 它逐个取出每个模式的显示名,再根据数量选择不同写法:空列表、一个、两个、多个分别处理 → 出来是一段普通字符串,用来嵌进默认模式模板。

调用关系:它是 default_mode_instructions 的小帮手,只管把列表变成好读的文字。它内部会遍历列表并做字符串格式化,不知道模板是什么,也不关心最终说明给谁用。

调用图:被 1 处调用(default_mode_instructions);外部调用 2 个(format!, iter)。

utils/approval-presets/src/lib.rs源码 ↗
configstartup / config load

这个文件定义了 Codex 的内置审批预设。审批,就是程序在做敏感事前要不要先问用户;权限配置,就是它能读写哪些文件、能不能联网。这里把两者绑成一个个套餐:只读、默认、完全访问。比如“只读”允许看当前工作区文件,但改文件或联网要先得到同意;“完全访问”则很宽松,所以描述里特意提醒要小心。文件里的 ApprovalPreset 是每个套餐的说明卡片,包含给机器用的固定编号、给人看的名字和说明、审批策略,以及真正要启用的权限配置。builtin_approval_presets 返回完整菜单;另一个函数则能根据一个内置权限编号,找回对应的具体权限配置。它刻意不写任何界面相关内容,这样命令行界面、终端界面或服务器都能共用同一套安全选项。

函数细节2
builtin_approval_presets28–61 ↗
fn builtin_approval_presets() -> Vec<ApprovalPreset>

作用:这个函数生成内置的审批和权限套餐列表,给界面展示,也给程序实际套用。有人需要显示“只读、默认、完全访问”这些选项时,就会用它拿到统一版本。

数据流:它不需要外部输入;函数内部直接写死三张预设卡片,每张卡片都有编号、名称、说明、审批规则和权限规则;最后输出一个列表。它不会修改外部状态,只是临时组装并返回这些预设。

调用关系:它处在配置准备阶段,通常由界面或服务启动时调用,用来拿到可选安全模式。它只借助 vec! 这个 Rust 的列表创建工具把三项预设装进列表,不再把工作交给别的复杂流程。

调用图:外部调用 1 个(vec!)。

builtin_permission_profile_for_active_permission_profile64–77 ↗
fn builtin_permission_profile_for_active_permission_profile(
    active_permission_profile: &ActivePermissionProfile,
) -> Option<PermissionProfile>

作用:这个函数根据一个“当前启用的内置权限编号”,换算出真正可执行的权限配置。它像查表:看到编号是只读,就返回只读规则;看到编号是工作区可写,就返回工作区写入规则。

数据流:输入是一份当前权限标识。函数先检查它是不是扩展出来的自定义配置;如果是,就不处理,返回空结果。然后它看编号属于哪一种内置权限:只读会生成只读配置,工作区会生成可写当前工作区的配置,完全访问会返回禁用权限限制的配置;如果编号不认识,也返回空结果。

调用关系:它在程序需要把“配置里保存的名字”变成“实际权限规则”时使用。它会调用 read_onlyworkspace_write 来创建对应的具体权限配置;对于完全访问,它直接返回不启用限制的权限状态。

调用图:调用 2 个内部函数(read_only, workspace_write)。

tui/src/model_catalog.rs源码 ↗
data_modelstartup 和界面运行期间需要展示或切换模型时

这个文件定义了一个叫 ModelCatalog 的小容器。你可以把它理解成一张菜单:里面每一项都是一个 ModelPreset,也就是某个模型的预设信息。它做的事很克制:创建时把模型列表收进去;别人想看列表时,就给出一份拷贝。这里返回拷贝很重要,因为外面拿到菜单后怎么改,都不会直接弄乱原始菜单。try_list_models 的返回类型里有 Infallible,意思是“按设计不会失败的错误类型”。也就是说,这个版本的模型清单只是从内存里读数据,不需要联网、不读文件,所以列出模型不会出错。它主要是给 TUI(文字用户界面)里选择模型、测试模型切换等地方当稳定的数据来源。

函数细节2
ModelCatalog::new10–12 ↗
fn new(models: Vec<ModelPreset>) -> Self

作用:创建一个新的模型清单,把传进来的模型预设列表包进 ModelCatalog 里。调用者用它来准备一份之后可以查询的“模型菜单”。

数据流:进去的是一组 ModelPreset 模型预设 → 函数把这组数据原样放进 ModelCatalog 的内部字段里 → 出来的是一个新的 ModelCatalog 对象,没有额外读取文件或修改外部东西。

调用关系:它是这份清单的入口。运行流程 run 会用它准备真实界面要用的模型列表;一些测试,比如 set_fast_mode_test_catalog、test_model_catalog、model_switch_recomputes_catalog_default_service_tier 和 service_tier_commands_lowercase_catalog_names,也会用它造出测试用的模型清单。

调用图:被 5 处调用(run, set_fast_mode_test_catalog, test_model_catalog, model_switch_recomputes_catalog_default_service_tier, service_tier_commands_lowercase_catalog_names)。

ModelCatalog::try_list_models14–16 ↗
fn try_list_models(&self) -> Result<Vec<ModelPreset>, Infallible>

作用:把当前保存的模型列表拿出来给别人看。名字里有 try,是因为接口形式允许返回错误;但这里实际不会失败。

数据流:进去的是已有的 ModelCatalog,也就是内部已经保存好的模型列表 → 函数复制一份 models 列表 → 出来的是 Ok(复制后的 Vec<ModelPreset>);原来的清单保持不变,调用者改返回值也不会影响它。

调用关系:它是界面或其他代码读取模型菜单时会走的出口。它不再把工作交给别的函数,只负责安全地把内部列表复制出来,让上层可以展示模型、计算默认选项,或执行模型切换相关逻辑。

tui/src/update_action.rs源码 ↗
domain_logicafter TUI exit / update prompt

这个文件像一个“更新方式转换表”。Codex 可能是用 npm、bun、Homebrew,或者官方独立安装脚本装上的;不同来源要用不同命令更新。这里先用 UpdateAction 这个枚举(枚举就是一组选项)列出所有支持的更新动作,比如 npm 全局安装最新版、brew 升级、Unix 或 Windows 重新跑安装脚本。发布版里,它会读取当前安装环境,然后选出合适的动作;如果安装来源不认识,就不自动更新。选好以后,它还能给出两种形式:一种是程序真正执行用的“命令名 + 参数列表”,另一种是给人看的完整命令字符串。文件末尾的测试会检查这些映射和独立安装版的命令没有写错,因为这里一旦错了,用户点击更新时就可能执行错误命令。

函数细节6
UpdateAction::from_install_context25–36 ↗
fn from_install_context(context: &InstallContext) -> Option<Self>

作用:根据 Codex 当前的安装方式,判断应该使用哪一种更新动作。有人会用它来避免“明明是 brew 安装,却拿 npm 去更新”这种错配。

数据流:输入是一份安装环境信息,里面写着安装方法。它查看 method 字段:npm 就变成 NpmGlobalLatest,bun 就变成 BunGlobalLatest,brew 就变成 BrewUpgrade,独立安装版再按 Unix 或 Windows 分开;如果是 Other,也就是不知道来源,就返回空,表示不建议自动更新。

调用关系:它是选择更新路线的核心判断。get_update_action 会先拿到当前安装环境,再把这份环境交给它,由它给出最终的 UpdateAction 或者告诉上层“没有合适动作”。测试函数 tests::maps_install_context_to_update_action 也会直接喂给它各种假安装环境,确认每种情况都映射正确。

调用图:被 1 处调用(get_update_action)。

UpdateAction::command_args39–61 ↗
fn command_args(self) -> (&'static str, &'static [&'static str])

作用:把一个更新动作变成程序可以直接执行的命令和参数。比如把“用 npm 更新”变成 npm 这个程序名,加上 install、-g、@openai/codex 这些参数。

数据流:输入是一个 UpdateAction 选项。它按选项查表,输出两部分:第一部分是要启动的命令名,第二部分是参数数组。它不真正运行命令,只负责把命令准备成安全、清楚的结构。

调用关系:它位于“已经决定怎么更新”和“真正执行更新”之间。run_update_action 会用它拿到可执行命令;command_str 也会调用它,把同一组命令参数拼成给用户看的文字。测试 tests::standalone_update_commands_rerun_latest_installer 会检查独立安装版生成的命令是否正是重新运行官方安装脚本。

调用图:被 2 处调用(run_update_action, command_str)。

UpdateAction::command_str64–68 ↗
fn command_str(self) -> String

作用:把更新命令整理成一行适合展示给人的字符串。它常用于界面提示,让用户知道系统准备执行什么命令。

数据流:输入是一个 UpdateAction。它先调用 command_args 拿到命令名和参数,再用 shlex::try_join 把它们按 shell 命令的写法安全拼接起来;如果拼接失败,就退一步用简单空格连接。输出是一段完整命令文字,不会实际执行任何东西。

调用关系:它服务于展示层和执行前提示。run_update_action 可能用它显示将要运行的更新命令,render_ref 也会用它把命令渲染到界面文本里。它自己不判断更新方式,而是依赖 command_args 提供准确的命令零件。

调用图:调用 1 个内部函数(command_args);被 2 处调用(run_update_action, render_ref);外部调用 2 个(try_join, once)。

get_update_action72–74 ↗
fn get_update_action() -> Option<UpdateAction>

作用:在发布版运行时,读取当前 Codex 的真实安装情况,并给出推荐的更新动作。它是外部代码获取“现在该怎么更新”的入口。

数据流:它不需要调用者传入参数。它先调用 InstallContext::current 读取当前安装环境,再把结果交给 UpdateAction::from_install_context;最后输出一个可选的 UpdateAction,有值表示可以自动更新,没值表示安装来源不明确或不支持自动选择。

调用关系:它把“读取环境”和“选择动作”串起来。run、run_update_prompt_if_needed、get_upgrade_version 会在需要判断是否能更新、如何提示更新时调用它。真正的判断工作交给 UpdateAction::from_install_context。

调用图:调用 2 个内部函数(current, from_install_context);被 3 处调用(run, run_update_prompt_if_needed, get_upgrade_version)。

tests::maps_install_context_to_update_action83–138 ↗
fn maps_install_context_to_update_action()

作用:这个测试确认各种安装来源都会被翻译成正确的更新动作。它防止以后改代码时,把 npm、bun、brew 或独立安装版的映射弄乱。

数据流:它先准备一个临时的绝对路径,用来模拟独立安装版的目录。然后构造多份假的 InstallContext,分别代表未知来源、npm、bun、brew、Unix 独立安装、Windows 独立安装;每份都送进 UpdateAction::from_install_context,再用断言检查输出是否符合预期。

调用关系:它是 UpdateAction::from_install_context 的安全网。平时用户不会调用它,只有跑测试时会执行;如果映射规则被误改,这个测试会失败,提醒开发者更新选择逻辑出问题了。

调用图:调用 1 个内部函数(from_absolute_path);外部调用 2 个(assert_eq!, temp_dir)。

tests::standalone_update_commands_rerun_latest_installer141–164 ↗
fn standalone_update_commands_rerun_latest_installer()

作用:这个测试确认独立安装版的更新命令确实会重新运行官方最新版安装脚本。这样可以避免把 Unix 或 Windows 的安装命令写错。

数据流:它不构造复杂输入,只直接取 StandaloneUnix 和 StandaloneWindows 两个更新动作,调用 command_args 得到命令名和参数,然后和预期命令逐项比较。测试通过表示这两类平台会拿到正确的脚本下载和执行命令。

调用关系:它专门保护 UpdateAction::command_args 里最容易出错的两条长命令。用户不会直接碰到这个函数;开发者跑测试时,它会检查独立安装更新路径是否仍然可用。

调用图:外部调用 1 个(assert_eq!)。

内置宠物资源

这些文件定义内置宠物目录,获取并验证缓存的 spritesheets,并从内置或用户提供的来源加载规范化的宠物清单。

tui/src/pets/catalog.rs源码 ↗
data_modelcross-cutting

这个文件像一张固定的商品目录,只不过商品是终端界面里的小宠物。它先规定了每张宠物动画图的标准尺寸:一帧多宽多高、整张 spritesheet(把很多动画帧拼在一张大图里的图片)有几列几行。然后用 BuiltinPet 这个小结构保存每只宠物的基本资料,比如 id、显示名、描述、图片文件名。BUILTIN_PETS 就是内置宠物列表。程序如果拿到一个宠物 id,比如 “codex”,就可以通过 builtin_pet 找到完整资料。测试时还提供了 write_test_spritesheet,用来临时生成一张符合尺寸的空图片,方便测试流程不用依赖真正的美术资源。

函数细节2
builtin_pet69–71 ↗
fn builtin_pet(id: &str) -> Option<BuiltinPet>

作用:按宠物的 id 查内置宠物资料。别人只要给它一个像 “codex” 这样的名字,它就帮忙在内置清单里找对应的小宠物。

数据流:进去的是一个字符串 id;函数读取固定的 BUILTIN_PETS 列表,一个个比较每只宠物的 id;如果找到匹配项,就出来一个 BuiltinPet,里面有显示名、描述和图片文件名,如果找不到就出来空值 Option::None。

调用关系:它是内置宠物资料的查询入口。builtin_pet_url_uses_public_cdn_path 会用它确认内置宠物的公开资源路径,ensure_builtin_pack_for_pet 会用它为某只内置宠物准备资源包,load_with_codex_home 会在加载配置或资源时用它把宠物 id 转成完整资料。

调用图:被 3 处调用(builtin_pet_url_uses_public_cdn_path, ensure_builtin_pack_for_pet, load_with_codex_home)。

write_test_spritesheet74–77 ↗
fn write_test_spritesheet(path: &std::path::Path)

作用:专门给测试用,生成一张尺寸正确的空白宠物动画图。这样测试不需要真的带一整套宠物图片,也能检查文件写入和加载流程是否能跑通。

数据流:进去的是一个文件路径;函数根据内置规定的整张 spritesheet 宽高创建一张新的透明图片;然后把这张图片保存到给定路径。结果是磁盘上多了一个测试用图片文件,如果保存失败会直接报错停止测试。

调用关系:它只在测试编译时存在。write_test_pack、write_pet_manifest、write_legacy_avatar 和 write_pet 这些测试辅助流程会调用它来造假图片;它内部把创建图片的工作交给 image::RgbaImage::new,也就是调用图里的 new(ext),最后再保存成文件。

调用图:被 4 处调用(write_test_pack, write_pet_manifest, write_legacy_avatar, write_pet);外部调用 1 个(new)。

tui/src/pets/asset_pack.rs源码 ↗
io_transport按需加载内置宠物素材时活跃,也会在测试准备素材时使用

内置宠物的图片没有直接打包在 TUI 程序里,而是第一次需要时从公网下载。这个文件就像一个“宠物素材保管员”:先算出素材应该放在 CODEX_HOME 下面哪个缓存目录;如果文件已经存在,就检查它是不是能读、宽高是不是项目规定的尺寸;如果不存在或坏了,就用 HTTPS 下载,并限制最多 4MB、最多等 60 秒,避免下载到奇怪的大文件或卡死。下载后不会马上覆盖旧文件,而是先写到一个临时文件,检查通过后再改名安装,这样可以减少半截文件污染缓存的风险。它只负责做到“这里有一张合格的内置宠物图”,至于什么时候允许下载、什么时候把宠物写进配置,是更上层代码的事。

函数细节11
builtin_spritesheet_path33–35 ↗
fn builtin_spritesheet_path(codex_home: &Path, file: &str) -> PathBuf

作用:根据用户的 Codex 主目录和图片文件名,算出某个内置宠物 spritesheet 应该放在哪里。spritesheet 可以理解成“一张大图里排着很多帧动画”。

数据流:输入是 codex_home 和文件名 → 它先通过 pack_dir 找到当前版本的宠物缓存目录,再拼上 assets 和文件名 → 输出一个完整的本地文件路径,不会真的读写文件。

调用关系:ensure_builtin_pet 会用它先定位要检查或下载的目标文件;测试 write_test_pack_installs_all_builtins 也用它确认测试素材确实写到了预期位置。

调用图:调用 1 个内部函数(pack_dir);被 2 处调用(ensure_builtin_pet, write_test_pack_installs_all_builtins)。

ensure_builtin_pet45–83 ↗
fn ensure_builtin_pet(codex_home: &Path, pet: catalog::BuiltinPet) -> Result<()>

作用:确保某个内置宠物的图片文件已经在本地,而且结构正确。调用者只要调用它,就能把“本地缓存可能没有、可能坏了”的麻烦交给这里处理。

数据流:输入是 Codex 主目录和一个内置宠物信息 → 它算出目标路径,先检查现有文件;如果可用就直接结束;如果不可用,就生成官方 CDN 地址,下载字节,创建 assets 目录,把内容写到临时文件,检查临时文件尺寸,最后用改名的方式安装到正式位置 → 成功时本地缓存里会有合格图片,失败时返回错误,并尽量清理临时文件或坏文件。

调用关系:它是这个文件的核心入口,由上层 ensure_builtin_pack_for_pet 在需要内置宠物素材时调用。它把具体小活分给 builtin_spritesheet_path、builtin_pet_url、download_bytes_with_limit、validate_cached_spritesheet 和 install_downloaded_spritesheet。

调用图:调用 5 个内部函数(builtin_pet_url, builtin_spritesheet_path, download_bytes_with_limit, install_downloaded_spritesheet, validate_cached_spritesheet);被 1 处调用(ensure_builtin_pack_for_pet);外部调用 4 个(format!, create_dir_all, remove_file, write)。

builtin_pet_url85–89 ↗
fn builtin_pet_url(pet: catalog::BuiltinPet) -> Result<String>

作用:为某个内置宠物拼出官方下载地址,并确认这个地址是安全的 HTTPS 地址。

数据流:输入是内置宠物信息,主要用里面的 spritesheet 文件名 → 它把固定 CDN 基础地址和文件名拼起来,再交给 validate_download_url 检查协议 → 输出一个可用于下载的 URL 字符串。

调用关系:ensure_builtin_pet 在需要联网下载时会调用它;测试 builtin_pet_url_uses_public_cdn_path 用它确认生成的地址没有跑偏。

调用图:调用 1 个内部函数(validate_download_url);被 2 处调用(ensure_builtin_pet, builtin_pet_url_uses_public_cdn_path);外部调用 1 个(format!)。

pack_dir91–93 ↗
fn pack_dir(codex_home: &Path) -> PathBuf

作用:算出宠物素材包的缓存根目录,并带上版本号。这样以后素材包升级时,可以和旧缓存分开。

数据流:输入是 codex_home → 它拼出 cache/tui-pets/v1 这样的子目录路径 → 输出这个目录路径,不创建目录也不检查是否存在。

调用关系:builtin_spritesheet_path 用它继续拼出具体图片路径;测试辅助函数 write_test_pack 用它找到该把假素材写到哪里。

调用图:被 2 处调用(builtin_spritesheet_path, write_test_pack);外部调用 1 个(join)。

download_bytes_with_limit95–121 ↗
fn download_bytes_with_limit(url: &str, max_bytes: u64) -> Result<Vec<u8>>

作用:从指定地址下载宠物图片,同时限制地址必须是 HTTPS、下载时间不能太久、文件不能太大。这样可以避免不安全链接、超大文件和网络卡死。

数据流:输入是 URL 和最大字节数 → 它先检查 URL,创建一个带 60 秒超时的 HTTP 客户端,请求文件,确认服务器返回成功;如果服务器声明的长度太大就立刻报错;读取内容时也最多多读 1 个字节来判断是否超限 → 输出下载到的字节数组,或返回错误。

调用关系:ensure_builtin_pet 在本地缓存不可用时调用它拿到远端文件内容。它内部还会再次用 validate_download_url 检查最终响应地址,防止重定向到非 HTTPS 地址。

调用图:调用 1 个内部函数(validate_download_url);被 1 处调用(ensure_builtin_pet);外部调用 3 个(new, builder, bail!)。

install_downloaded_spritesheet123–125 ↗
fn install_downloaded_spritesheet(staging: &Path, destination: &Path) -> Result<()>

作用:把已经下载并验证过的临时图片文件安装到正式缓存位置。这里用的是改名操作,尽量让安装过程像“一次性换牌子”一样干净。

数据流:输入是临时文件路径 staging 和正式目标路径 destination → 它调用文件系统改名,把临时文件移动成正式文件 → 成功后正式路径出现新素材,临时路径消失;失败时返回错误。

调用关系:ensure_builtin_pet 在临时文件验证通过后调用它。如果第一次安装失败,ensure_builtin_pet 还会检查是不是别的进程已经把目标文件装好了,必要时再清理旧目标并重试。

调用图:被 1 处调用(ensure_builtin_pet);外部调用 1 个(rename)。

validate_download_url127–133 ↗
fn validate_download_url(value: &str) -> Result<()>

作用:检查下载地址是否合法,并且只允许 HTTPS。HTTPS 可以简单理解成带加密和身份校验的网页连接,比普通 HTTP 更安全。

数据流:输入是一个 URL 字符串 → 它尝试解析这个字符串;如果不是合法 URL 或协议不是 https,就返回错误 → 成功时不产出新数据,只表示这个地址通过检查。

调用关系:builtin_pet_url 用它检查自己拼出来的官方地址;download_bytes_with_limit 在下载前和处理服务器最终响应地址时也会用它,形成一道安全门。

调用图:被 2 处调用(builtin_pet_url, download_bytes_with_limit);外部调用 2 个(parse, bail!)。

validate_cached_spritesheet135–149 ↗
fn validate_cached_spritesheet(path: &Path) -> Result<()>

作用:检查本地图片是不是一张符合项目要求的宠物 spritesheet。它主要看图片能不能读,以及宽高是不是固定规格。

数据流:输入是图片文件路径 → 它读取图片尺寸;如果读不了,或宽高不是 catalog 里规定的 SPRITESHEET_WIDTH 和 SPRITESHEET_HEIGHT,就返回错误 → 成功时不修改文件,只表示这张图合格。

调用关系:ensure_builtin_pet 用它判断现有缓存能不能直接用,也用它检查刚下载的临时文件。测试 write_test_pack_installs_all_builtins 也用它确认测试生成的内置宠物图片都是合格的。

调用图:被 2 处调用(ensure_builtin_pet, write_test_pack_installs_all_builtins);外部调用 2 个(bail!, image_dimensions)。

write_test_pack152–159 ↗
fn write_test_pack(codex_home: &Path)

作用:这是测试专用函数,用来在临时 Codex 主目录里写入一整套假的内置宠物素材。这样测试不用真的访问网络。

数据流:输入是测试用的 codex_home → 它用 pack_dir 找到缓存目录,创建 assets 目录,然后遍历所有内置宠物,为每个文件写一张测试 spritesheet → 结果是磁盘上出现一套可被正常验证的测试素材。

调用关系:测试 write_test_pack_installs_all_builtins 会直接调用它检查写入效果;其他测试比如 load_builtin_pet_uses_app_catalog_storage 也会用它提前准备好内置宠物素材环境。

调用图:调用 2 个内部函数(pack_dir, write_test_spritesheet);被 2 处调用(write_test_pack_installs_all_builtins, load_builtin_pet_uses_app_catalog_storage);外部调用 1 个(create_dir_all)。

tests::builtin_pet_url_uses_public_cdn_path167–176 ↗
fn builtin_pet_url_uses_public_cdn_path()

作用:这个测试确认内置宠物 dewey 的下载地址确实指向预期的公共 CDN 路径。它防止有人改代码时不小心把地址拼错。

数据流:它先从 catalog 里取出名为 dewey 的内置宠物 → 调用 builtin_pet_url 生成地址 → 把结果和写死的正确地址比较;一致则测试通过,不一致则失败。

调用关系:它专门覆盖 builtin_pet_url 的行为,保证下载入口会去正确的官方位置找素材。

调用图:调用 2 个内部函数(builtin_pet_url, builtin_pet);外部调用 1 个(assert_eq!)。

tests::write_test_pack_installs_all_builtins179–189 ↗
fn write_test_pack_installs_all_builtins()

作用:这个测试确认测试素材包会为每个内置宠物都生成文件,而且生成的文件尺寸也正确。

数据流:它创建一个临时目录 → 调用 write_test_pack 写入测试素材 → 遍历所有内置宠物,用 builtin_spritesheet_path 算路径,检查文件存在,再用 validate_cached_spritesheet 验证图片规格 → 所有宠物都通过才算成功。

调用关系:它把 write_test_pack、builtin_spritesheet_path 和 validate_cached_spritesheet 串起来测了一遍,保证测试环境准备工具本身是可靠的。

调用图:调用 3 个内部函数(builtin_spritesheet_path, validate_cached_spritesheet, write_test_pack);外部调用 2 个(assert!, tempdir)。

tui/src/pets/model.rs源码 ↗
domain_logic宠物加载时、界面准备显示宠物前;测试中也会反复使用

这个文件像宠物系统的“验货员”和“登记员”。用户可能输入内置宠物名、自定义宠物名、旧版头像名,或者直接给一个本地文件夹路径;这里会把这些不同说法统一整理成一个 Pet。Pet 里面有宠物名字、说明、精灵图路径、每一帧的大小、总帧数,以及各种动画怎么播放。它还会严格检查:图片必须存在,尺寸必须是应用支持的大小;帧网格必须刚好铺满整张图;动画不能引用不存在的帧;动画播放速度不能离谱。这样后面的绘制代码就不用猜,也不用一边播放一边处理烂配置。可以把它理解成:宠物进场前先过安检,合格后才发一张标准通行证。

函数细节47
Animation::total_duration46–51 ↗
fn total_duration(&self) -> Duration

作用:算出一段动画从头播到尾一共要多久。播放代码需要这个总时长,才能知道当前时间落在哪一帧。

数据流:它读取动画里的每个小帧,每个小帧都有自己的停留时间;然后把这些时间加起来;最后返回一个总时长,不改动动画本身。

调用关系:它被 current_animation_frame 使用。也就是说,外面的播放逻辑先问这段动画总共有多长,再决定现在该显示哪一张小图。

调用图:被 1 处调用(current_animation_frame)。

Pet::load_with_codex_home82–96 ↗
fn load_with_codex_home(value: &str, codex_home: Option<&Path>) -> Result<Self>

作用:这是加载宠物的总入口。它接收用户输入的宠物选择,比如“dewey”“custom:cat”或一个文件夹路径,然后判断该走哪条加载路线。

数据流:输入是一段选择文字和可选的 CODEX_HOME 目录;它先判断这段文字像不像路径,再判断是不是 custom: 开头,再查内置宠物目录;最后返回一个完整 Pet,或者给出清楚的错误。

调用关系:很多测试和上层加载函数都会从这里开始。它自己不读完整配置,而是按情况把工作交给 load_pet_path、load_custom_pet 或 load_builtin_pet。

调用图:调用 5 个内部函数(builtin_pet, load_builtin_pet, load_custom_pet, load_pet_path, path_like);被 9 处调用(load, custom_pet_rejects_spritesheet_path_escape, custom_pet_selector_falls_back_to_legacy_avatar_manifest, custom_pet_selector_loads_codex_home_pet_manifest, load_builtin_pet_uses_app_catalog_storage, load_pet_error_from_dir, load_pet_from_dir, load_pet_json_path_uses_containing_directory, custom_pet_entries)。

Pet::frame_count98–100 ↗
fn frame_count(&self) -> usize

作用:返回这个宠物一共有多少张可用的小帧。准备图片缓存或切图时会用到这个数字。

数据流:它读取 Pet 里已经算好的 frame_count 字段;不重新计算;直接把这个数字返回。

调用关系:prepare_png_frames 会调用它,用来知道应该准备多少张切好的图片帧。

调用图:被 1 处调用(prepare_png_frames)。

Pet::frame_cache_key102–110 ↗
fn frame_cache_key(&self) -> Result<String>

作用:给当前宠物图片生成一个缓存钥匙。只要图片内容或切帧规格变了,钥匙就会变,旧缓存就不会被误用。

数据流:它读取精灵图文件的全部字节,算 SHA-256 哈希值(可以理解成文件内容的指纹),再把帧宽高、列数、行数拼进去;最后返回一串字符串。

调用关系:这个函数不参与选择宠物,只在需要缓存切好的帧时使用。它依赖磁盘读取和哈希计算,保证同名文件改了内容也能被发现。

调用图:外部调用 3 个(digest, format!, read)。

FrameSpec::default139–146 ↗
fn default() -> Self

作用:提供默认的切帧规格。宠物配置文件没写 frame 时,就按应用内置宠物的标准尺寸来切。

数据流:它从 catalog 里的默认宽、高、列数、行数取值;组装成一个 FrameSpec;返回给加载流程使用。

调用关系:load_pet_manifest 读取配置后,如果没有 frame 字段,就会间接用到它,避免每个宠物配置都必须重复写标准尺寸。

custom_pet_selector149–151 ↗
fn custom_pet_selector(id: &str) -> String

作用:把普通自定义宠物 id 包装成程序识别的选择字符串。比如 chefito 会变成 custom:chefito。

数据流:输入一个 id;函数在前面加上 custom: 前缀;输出新的选择字符串。

调用关系:测试和自定义宠物列表会用它生成标准写法。生成后的字符串通常再交给 Pet::load_with_codex_home 判断并加载。

调用图:被 4 处调用(custom_pet_rejects_spritesheet_path_escape, custom_pet_selector_falls_back_to_legacy_avatar_manifest, custom_pet_selector_loads_codex_home_pet_manifest, custom_pet_entries);外部调用 1 个(format!)。

load_builtin_pet164–183 ↗
fn load_builtin_pet(pet: catalog::BuiltinPet, codex_home: Option<&Path>) -> Result<Pet>

作用:加载应用自带的宠物。它不下载资源,只确认资源已经在本地,然后用固定规格生成 Pet。

数据流:输入一个内置宠物资料和 CODEX_HOME;它算出内置精灵图应该放在哪里,检查文件是否存在;然后填入默认帧规格和默认动画,返回 Pet。

调用关系:Pet::load_with_codex_home 查到用户输入是内置宠物后会调用它。它会用 default_frame_count 和 default_animations 补齐内置宠物的播放信息。

调用图:调用 2 个内部函数(default_animations, default_frame_count);被 1 处调用(load_with_codex_home);外部调用 2 个(bail!, builtin_spritesheet_path)。

load_custom_pet185–203 ↗
fn load_custom_pet(value: &str, codex_home: Option<&Path>) -> Result<Pet>

作用:按自定义宠物 id 在用户目录里找宠物配置。它兼容新位置 pets,也兼容旧位置 avatars。

数据流:输入自定义 id 和 CODEX_HOME;它先找 CODEX_HOME/pets/id/pet.json,找不到再找 CODEX_HOME/avatars/id/avatar.json;找到就交给 load_pet_manifest,找不到就报 unknown pet。

调用关系:Pet::load_with_codex_home 在遇到 custom: 前缀或非内置名字时会调用它。它负责定位目录,真正读配置和校验交给 load_pet_manifest。

调用图:调用 2 个内部函数(custom_pet_cache_id, load_pet_manifest);被 1 处调用(load_with_codex_home);外部调用 1 个(bail!)。

load_pet_path205–230 ↗
fn load_pet_path(value: &str) -> Result<Pet>

作用:从用户直接给出的本地路径加载宠物。这个路径可以是宠物文件夹,也可以是 pet.json/avatar.json 文件本身。

数据流:输入一段路径文字;它先展开 ~,再查看路径是文件还是目录,得到所在目录;然后在目录里找 pet.json 或 avatar.json;最后调用 load_pet_manifest 返回 Pet。

调用关系:Pet::load_with_codex_home 发现输入看起来像路径时会调用它。它主要负责把路径变成标准宠物目录。

调用图:调用 2 个内部函数(expand_path, load_pet_manifest);被 1 处调用(load_with_codex_home);外部调用 2 个(bail!, metadata)。

load_pet_manifest232–294 ↗
fn load_pet_manifest(
    pet_dir: &Path,
    manifest_file: &str,
    fallback_id: &str,
    cache_id: &str,
) -> Result<Pet>

作用:读取并校验一个宠物配置文件,这是自定义宠物真正变成 Pet 的核心步骤。

数据流:输入宠物目录、配置文件名、备用 id 和缓存 id;它读取 JSON 配置,整理名字和说明,解析精灵图路径,检查图片存在和尺寸,检查切帧规格,再加载动画;最后返回完整 Pet。

调用关系:load_custom_pet 和 load_pet_path 都会调用它。它再把安全路径检查交给 resolve_spritesheet_path,把图片尺寸检查交给 validate_app_spritesheet_dimensions,把帧和动画检查交给对应函数。

调用图:调用 4 个内部函数(load_animations, resolve_spritesheet_path, validate_app_spritesheet_dimensions, validate_frame_spec);被 2 处调用(load_custom_pet, load_pet_path);外部调用 4 个(join, bail!, read_to_string, from_str)。

resolve_spritesheet_path302–312 ↗
fn resolve_spritesheet_path(pet_dir: &Path, spritesheet_path: &str) -> Result<PathBuf>

作用:把配置里的精灵图路径变成真实路径,同时防止它逃出宠物文件夹。这样一个宠物配置不能偷偷读取别处的文件。

数据流:输入宠物目录和配置里的图片路径;它拒绝绝对路径、.. 上级目录、Windows 盘符这类危险写法;安全时返回 pet_dir 加上相对路径后的结果。

调用关系:load_pet_manifest 在确定图片位置时调用它。它是自定义宠物的安全边界之一。

调用图:被 1 处调用(load_pet_manifest);外部调用 3 个(join, new, bail!)。

validate_app_spritesheet_dimensions314–325 ↗
fn validate_app_spritesheet_dimensions(path: &Path) -> Result<(u32, u32)>

作用:检查精灵图整张图片的像素尺寸是不是应用支持的固定大小。尺寸不对,后面切图就会错位。

数据流:输入图片路径;它读取图片宽高;如果宽高等于 catalog 规定的尺寸,就返回宽高,否则报错。

调用关系:load_pet_manifest 找到图片后会调用它。它确保进入后续切帧步骤的图片大小可靠。

调用图:被 1 处调用(load_pet_manifest);外部调用 2 个(bail!, image_dimensions)。

validate_frame_spec327–359 ↗
fn validate_frame_spec(
    frame: &FrameSpec,
    spritesheet_width: u32,
    spritesheet_height: u32,
) -> Result<usize>

作用:检查每帧大小和网格数量是否合理。它保证所有小格子刚好铺满整张精灵图。

数据流:输入帧规格和整张图片宽高;它拒绝 0 尺寸,检查宽乘列、高乘行是否刚好等于图片尺寸,再算总帧数并限制最大值;最后返回总帧数。

调用关系:load_pet_manifest 在读到 frame 配置后调用它。通过它以后,动画才可以安全地用帧编号引用图片。

调用图:被 1 处调用(load_pet_manifest);外部调用 2 个(bail!, try_from)。

custom_pet_cache_id361–363 ↗
fn custom_pet_cache_id(id: &str) -> String

作用:为自定义宠物生成缓存用 id。这样自定义宠物和内置宠物不容易重名冲突。

数据流:输入一个自定义 id;它在前面加 custom-;输出缓存 id 字符串。

调用关系:load_custom_pet 找到自定义宠物目录前会调用它,并把结果传给 load_pet_manifest,用来决定 Pet 的 id。

调用图:被 1 处调用(load_custom_pet);外部调用 1 个(format!)。

path_like365–374 ↗
fn path_like(value: &str) -> bool

作用:判断用户输入是不是像文件路径,而不是普通宠物名字。比如 ./cat、~/pets/cat、带斜杠的字符串都会被当成路径。

数据流:输入一段文字;它检查点号、~、相对路径、绝对路径、斜杠和反斜杠;返回 true 或 false。

调用关系:Pet::load_with_codex_home 最先调用它。这个判断决定后面是走本地路径加载,还是按宠物 id 去查目录或内置目录。

调用图:被 1 处调用(load_with_codex_home);外部调用 1 个(new)。

expand_path376–386 ↗
fn expand_path(value: &str) -> Result<PathBuf>

作用:把带 ~ 的路径展开成用户家目录。这样用户可以写 ~/pets/cat,而不用写完整路径。

数据流:输入路径文字;如果是 ~ 或 ~/ 开头,就读取 HOME 环境变量并替换;否则直接转成 PathBuf;最后返回路径。

调用关系:load_pet_path 在访问磁盘前调用它。它只处理路径文字,不负责判断配置文件是否存在。

调用图:被 1 处调用(load_pet_path);外部调用 2 个(from, var_os)。

load_animations388–452 ↗
fn load_animations(
    specs: HashMap<String, AnimationSpec>,
    frame_count: usize,
) -> Result<HashMap<String, Animation>>

作用:把配置文件里的动画写法变成程序可播放的 Animation。没有自定义动画时,它会使用默认动画。

数据流:输入动画配置表和宠物总帧数;它先准备默认动画,如果配置为空就校验默认动画;否则逐个检查帧列表、帧编号、fps 播放速度、是否循环和 fallback 退回动画;最后返回动画表。

调用关系:load_pet_manifest 在构造 Pet 时调用它。它还会调用 validate_animation_indices 做最后总检查,确保动画不会引用不存在的帧或不存在的 fallback。

调用图:调用 2 个内部函数(default_animations, validate_animation_indices);被 2 处调用(load_pet_manifest, custom_animation_specs_keep_manifest_fps_and_loop_shape);外部调用 2 个(from_secs_f64, bail!)。

validate_animation_indices454–478 ↗
fn validate_animation_indices(
    animations: &HashMap<String, Animation>,
    frame_count: usize,
) -> Result<()>

作用:统一检查动画表有没有坏引用。坏引用包括空动画、帧编号越界、退回动画不存在。

数据流:输入全部动画和总帧数;它逐个动画、逐帧检查;如果发现问题就报错;全部通过则返回成功,不改动动画。

调用关系:load_animations 在返回动画表前调用它。它像最后一道验收,保证播放时不会突然找不到帧或 fallback。

调用图:被 1 处调用(load_animations);外部调用 1 个(bail!)。

default_frame_count480–482 ↗
fn default_frame_count() -> usize

作用:算出内置标准精灵图默认有多少帧。它就是默认列数乘默认行数。

数据流:它读取 catalog 的默认列数和行数;相乘后转成 usize;返回总帧数。

调用关系:load_builtin_pet 用它给内置宠物填 frame_count。测试自定义动画时也用它提供标准帧数。

调用图:被 2 处调用(load_builtin_pet, custom_animation_specs_keep_manifest_fps_and_loop_shape)。

default_animations484–582 ↗
fn default_animations() -> HashMap<String, Animation>

作用:生成应用默认的一整套宠物动画,比如待机、奔跑、挥手、失败、等待等。

数据流:它调用 idle_animation 生成待机动画,又多次调用 app_state_animation 生成各状态动画;最后把名字和动画放进 HashMap 返回。

调用关系:load_builtin_pet 直接使用它;load_animations 也先拿它当基础,再用自定义配置覆盖或补充。多个测试会检查这些默认动画的帧是否符合预期。

调用图:调用 2 个内部函数(app_state_animation, idle_animation);被 5 处调用(load_animations, load_builtin_pet, app_idle_animation_uses_calm_loop, app_notification_states_use_expected_rows, app_running_animation_repeats_then_settles_into_idle)。

idle_animation584–596 ↗
fn idle_animation() -> Animation

作用:生成默认待机动画。待机动画是宠物什么都不做时的基础循环。

数据流:它写死了一组帧编号和每帧停留毫秒数;把它们变成 AnimationFrame;最后返回一个会从第 0 帧开始循环、fallback 也是 idle 的 Animation。

调用关系:default_animations 会把它作为 idle 动画。app_state_animation 也会把它接在动作动画后面,让宠物动作播完后回到待机。

调用图:被 2 处调用(app_state_animation, default_animations)。

app_state_animation598–627 ↗
fn app_state_animation(
    row_index: usize,
    frame_count: usize,
    frame_duration_ms: u64,
    final_frame_duration_ms: u64,
) -> Animation

作用:按某一行精灵图生成一个应用状态动画。比如奔跑、等待、失败这类动作都用同一种模板。

数据流:输入行号、帧数、普通帧时长和最后一帧时长;它计算这一行每一列对应的帧编号,把动作重复三遍,再接上 idle 动画;最后返回一个 Animation。

调用关系:default_animations 多次调用它生成不同状态。它内部调用 idle_animation,把短动作和待机循环接起来。

调用图:调用 1 个内部函数(idle_animation);被 1 处调用(default_animations)。

tests::write_minimal_pet633–642 ↗
fn write_minimal_pet() -> tempfile::TempDir

作用:测试用的小工具,快速创建一个最基本、合法的宠物目录。

数据流:它准备一段包含 id、显示名、说明和图片路径的 JSON;交给写配置的辅助函数;返回临时目录。

调用关系:很多加载测试都会先调用它,得到一个干净的宠物样本,再交给加载函数验证行为。

调用图:外部调用 1 个(write_pet_manifest)。

tests::write_pet_manifest644–649 ↗
fn write_pet_manifest(manifest: &str) -> tempfile::TempDir

作用:测试用的小工具,把给定的宠物配置写进临时目录,并放一张测试精灵图。

数据流:输入一段 manifest JSON 字符串;它创建临时目录,写 pet.json,再调用 catalog 的测试函数写 spritesheet.webp;最后返回目录。

调用关系:tests::write_minimal_pet 和许多错误场景测试都会调用它。它让测试不依赖真实用户文件。

调用图:调用 1 个内部函数(write_test_spritesheet);外部调用 2 个(write, tempdir)。

tests::load_pet_from_dir651–653 ↗
fn load_pet_from_dir(dir: &tempfile::TempDir) -> Pet

作用:测试用的小包装,直接从临时宠物目录加载 Pet,并假定应该成功。

数据流:输入临时目录;它把目录路径转成字符串,调用 Pet::load_with_codex_home;成功时返回 Pet,失败会让测试崩掉。

调用关系:成功场景测试用它减少重复代码。它走的就是正常路径加载流程。

调用图:调用 1 个内部函数(load_with_codex_home);外部调用 1 个(path)。

tests::load_pet_error_from_dir655–657 ↗
fn load_pet_error_from_dir(dir: &tempfile::TempDir) -> anyhow::Error

作用:测试用的小包装,直接从临时宠物目录加载 Pet,并假定应该失败。

数据流:输入临时目录;它调用 Pet::load_with_codex_home;如果得到错误就返回错误,如果居然成功就让测试失败。

调用关系:各种“坏配置应该被拒绝”的测试会用它,方便检查错误信息是否符合预期。

调用图:调用 1 个内部函数(load_with_codex_home);外部调用 1 个(path)。

tests::load_builtin_pet_uses_app_catalog_storage660–678 ↗
fn load_builtin_pet_uses_app_catalog_storage()

作用:测试内置宠物会从应用规定的位置加载,并带有正确的基本资料。

数据流:它创建临时 CODEX_HOME,写入测试资源包,加载 dewey;然后检查 id、显示名、说明、图片路径和默认帧规格。

调用关系:它覆盖 Pet::load_with_codex_home 到 load_builtin_pet 的路线,确保内置目录规则没有被改坏。

调用图:调用 2 个内部函数(write_test_pack, load_with_codex_home);外部调用 2 个(assert_eq!, tempdir)。

tests::app_idle_animation_uses_calm_loop681–688 ↗
fn app_idle_animation_uses_calm_loop()

作用:测试默认待机动画是不是使用预期的平静循环。

数据流:它生成默认动画表,取出 idle;检查帧编号、每帧时长和循环起点。

调用关系:它直接检查 default_animations 生成的 idle 内容,防止默认动画节奏被无意改掉。

调用图:调用 1 个内部函数(default_animations);外部调用 1 个(assert_eq!)。

tests::app_running_animation_repeats_then_settles_into_idle691–708 ↗
fn app_running_animation_repeats_then_settles_into_idle()

作用:测试默认 running 动画会先重复跑步动作,再进入待机循环。

数据流:它生成默认动画,取 running;检查前三段都是同一组跑步帧,后面接 idle 帧,并检查时长和 loop_start。

调用关系:它验证 default_animations 和 app_state_animation 的配合方式,确保状态动作播放完不会卡住。

调用图:调用 1 个内部函数(default_animations);外部调用 2 个(assert_eq!, vec!)。

tests::app_notification_states_use_expected_rows711–726 ↗
fn app_notification_states_use_expected_rows()

作用:测试等待、审核、失败这些通知状态使用了精灵图中正确的行。

数据流:它生成默认动画表,分别取 waiting、review、failed;检查开头帧编号是否对应预期行。

调用关系:它保护 default_animations 里的行号映射,避免 UI 状态显示成错误动作。

调用图:调用 1 个内部函数(default_animations);外部调用 1 个(assert_eq!)。

tests::custom_animation_specs_keep_manifest_fps_and_loop_shape729–749 ↗
fn custom_animation_specs_keep_manifest_fps_and_loop_shape()

作用:测试自定义动画会尊重配置里的 fps 和是否循环。

数据流:它手工构造一个 custom 动画配置,帧是 1、2,fps 是 2,不循环;调用 load_animations 后检查帧、时长、loop_start 和 fallback。

调用关系:它直接测试 load_animations 的自定义配置解析,不需要完整宠物文件。

调用图:调用 2 个内部函数(default_frame_count, load_animations);外部调用 3 个(from, assert_eq!, vec!)。

tests::load_pet_directory_uses_app_pet_manifest_defaults752–765 ↗
fn load_pet_directory_uses_app_pet_manifest_defaults()

作用:测试一个最小宠物配置在没写 frame 和 animations 时,会套用应用默认值。

数据流:它创建最小宠物目录,加载 Pet;检查 id、显示名、默认帧宽高、列行、总帧数,以及 idle 动画存在。

调用关系:它通过 write_minimal_pet 和 load_pet_from_dir 覆盖 load_pet_manifest 的默认值逻辑。

调用图:外部调用 4 个(assert!, assert_eq!, load_pet_from_dir, write_minimal_pet)。

tests::frame_cache_key_changes_with_spritesheet_contents768–783 ↗
fn frame_cache_key_changes_with_spritesheet_contents()

作用:测试图片内容变化时缓存钥匙也会变化。这样不会继续使用旧切图。

数据流:它加载宠物并记录第一次缓存 key;然后改写精灵图像素,再重新加载并生成 key;最后确认两个 key 不一样。

调用关系:它验证 Pet::frame_cache_key 把图片内容纳入了指纹,而不只是看文件名。

调用图:外部调用 5 个(assert_ne!, Rgba, from_pixel, load_pet_from_dir, write_minimal_pet)。

tests::frame_cache_key_changes_with_frame_spec786–802 ↗
fn frame_cache_key_changes_with_frame_spec()

作用:测试切帧规格变化时缓存钥匙也会变化。即使图片内容一样,切法不同也不能共用缓存。

数据流:它分别加载默认规格宠物和自定义 frame 规格宠物;生成两个缓存 key;最后确认它们不同。

调用关系:它验证 Pet::frame_cache_key 把帧宽高、列数、行数也算进缓存标识。

调用图:外部调用 4 个(assert_ne!, load_pet_from_dir, write_minimal_pet, write_pet_manifest)。

tests::load_pet_json_path_uses_containing_directory805–816 ↗
fn load_pet_json_path_uses_containing_directory()

作用:测试用户直接给 pet.json 文件路径时,会使用它所在的文件夹作为宠物目录。

数据流:它创建最小宠物目录,把 pet.json 的路径传给 Pet::load_with_codex_home;然后检查精灵图路径解析到同目录下的文件。

调用关系:它覆盖 load_pet_path 对“文件路径而不是目录路径”的处理。

调用图:调用 1 个内部函数(load_with_codex_home);外部调用 2 个(assert_eq!, write_minimal_pet)。

tests::custom_pet_selector_loads_codex_home_pet_manifest819–839 ↗
fn custom_pet_selector_loads_codex_home_pet_manifest()

作用:测试 custom: 写法能从 CODEX_HOME/pets 里加载新式自定义宠物。

数据流:它创建 CODEX_HOME/pets/chefito,复制配置和图片进去;用 custom_pet_selector 生成选择字符串并加载;最后检查 id 和图片路径。

调用关系:它验证 custom_pet_selector、Pet::load_with_codex_home、load_custom_pet 和 load_pet_manifest 的新式自定义宠物路线。

调用图:调用 2 个内部函数(load_with_codex_home, custom_pet_selector);外部调用 5 个(assert_eq!, copy, create_dir_all, tempdir, write_minimal_pet)。

tests::custom_pet_selector_falls_back_to_legacy_avatar_manifest842–862 ↗
fn custom_pet_selector_falls_back_to_legacy_avatar_manifest()

作用:测试 custom: 写法在 pets 目录找不到时,还能兼容旧的 avatars/avatar.json。

数据流:它创建 CODEX_HOME/avatars/legacy,放入 avatar.json 和图片;加载 custom:legacy;最后检查 id 和显示名。

调用关系:它验证 load_custom_pet 的旧版兼容分支,保证老用户数据还能用。

调用图:调用 2 个内部函数(load_with_codex_home, custom_pet_selector);外部调用 5 个(assert_eq!, copy, create_dir_all, tempdir, write_minimal_pet)。

tests::custom_pet_rejects_spritesheet_path_escape865–888 ↗
fn custom_pet_rejects_spritesheet_path_escape()

作用:测试自定义宠物不能把 spritesheetPath 指到自己目录外面。这是安全检查。

数据流:它写一个 spritesheetPath 为 ../spritesheet.webp 的配置;尝试加载后拿到错误;最后确认错误提示包含必须留在目录内。

调用关系:它覆盖 resolve_spritesheet_path 的防逃逸逻辑,防止未来修改时放松安全限制。

调用图:调用 2 个内部函数(load_with_codex_home, custom_pet_selector);外部调用 4 个(assert!, create_dir_all, write, tempdir)。

tests::custom_pet_rejects_zero_frame_dimensions891–906 ↗
fn custom_pet_rejects_zero_frame_dimensions()

作用:测试帧宽高或网格数量为 0 时会被拒绝。

数据流:它写一个 frame.width 为 0 的配置;加载时应该失败;最后检查错误信息说明帧尺寸和网格数不能为零。

调用关系:它验证 validate_frame_spec 的基础合法性检查。

调用图:外部调用 3 个(assert!, load_pet_error_from_dir, write_pet_manifest)。

tests::custom_pet_rejects_frame_grid_that_does_not_cover_spritesheet909–924 ↗
fn custom_pet_rejects_frame_grid_that_does_not_cover_spritesheet()

作用:测试帧网格不能只覆盖图片的一部分。否则切图会切错。

数据流:它写一个列数少一列的 frame 配置;加载时应该失败;最后检查错误信息说明网格必须刚好覆盖整张图。

调用关系:它验证 validate_frame_spec 对总宽高匹配的检查。

调用图:外部调用 3 个(assert!, load_pet_error_from_dir, write_pet_manifest)。

tests::custom_pet_rejects_excessive_frame_count927–939 ↗
fn custom_pet_rejects_excessive_frame_count()

作用:测试帧数太多的配置会被拒绝,避免异常配置消耗太多资源。

数据流:它写一个非常密集的 frame 网格;加载时应该失败;最后确认错误包含超过最大值。

调用关系:它验证 validate_frame_spec 的最大帧数限制。

调用图:外部调用 3 个(assert!, load_pet_error_from_dir, write_pet_manifest)。

tests::custom_pet_rejects_empty_animation_frames942–959 ↗
fn custom_pet_rejects_empty_animation_frames()

作用:测试动画必须至少有一帧。空动画没有东西可播放。

数据流:它写一个 idle 动画但 frames 为空;加载时应该失败;最后检查错误信息。

调用关系:它验证 load_animations 对自定义动画帧列表的检查。

调用图:外部调用 3 个(assert!, load_pet_error_from_dir, write_pet_manifest)。

tests::custom_pet_rejects_animation_frame_outside_grid962–979 ↗
fn custom_pet_rejects_animation_frame_outside_grid()

作用:测试动画不能引用宠物根本没有的帧编号。

数据流:它写一个引用第 72 帧的 idle 动画,而默认帧编号只到 71;加载时应该失败;最后检查错误信息。

调用关系:它验证 load_animations 和 validate_animation_indices 对帧编号越界的防护。

调用图:外部调用 3 个(assert!, load_pet_error_from_dir, write_pet_manifest)。

tests::custom_pet_rejects_invalid_animation_fps982–999 ↗
fn custom_pet_rejects_invalid_animation_fps()

作用:测试动画播放速度 fps 不能太快、为零、负数或不是正常数字。

数据流:它写一个 fps 为 120 的动画配置;加载时应该失败;最后确认错误说明 fps 必须在允许范围内。

调用关系:它验证 load_animations 对播放速度上限和合法性的检查。

调用图:外部调用 3 个(assert!, load_pet_error_from_dir, write_pet_manifest)。

tests::custom_pet_rejects_animation_fallback_to_missing_animation1002–1019 ↗
fn custom_pet_rejects_animation_fallback_to_missing_animation()

作用:测试一次性动画的 fallback 必须指向存在的动画。否则播完后不知道回到哪里。

数据流:它写一个 wave 动画,fallback 是 missing;加载时应该失败;最后检查错误提示 fallback 不存在。

调用关系:它验证 validate_animation_indices 对动画之间引用关系的检查。

调用图:外部调用 3 个(assert!, load_pet_error_from_dir, write_pet_manifest)。

tests::sprite_indices1021–1027 ↗
fn sprite_indices(animation: &Animation) -> Vec<usize>

作用:测试用的小工具,把动画里的帧编号单独抽出来,方便断言。

数据流:输入一个 Animation;它遍历 frames,取每个 AnimationFrame 的 sprite_index;返回编号列表。

调用关系:多个动画测试用它把复杂的 Animation 变成简单数字列表,方便比较预期。

tests::durations_ms1029–1035 ↗
fn durations_ms(animation: &Animation) -> Vec<u128>

作用:测试用的小工具,把动画里每帧时长转成毫秒列表,方便检查节奏。

数据流:输入一个 Animation;它遍历 frames,把 Duration 转成毫秒;返回毫秒数字列表。

调用关系:默认动画和自定义动画测试用它确认每一帧停留时间是否正确。