Codex 系统手册

插件与连接器生态管理

stage-14.3.234 个文件

这一阶段像插件和连接器的“应用商店加仓库管理员”,属于系统运行时的幕后支撑,也给命令行和聊天界面提供入口。它先从插件市场、本地目录、Git 仓库或远端服务找货源,再读取 plugin.json 清单,检查路径和格式是否安全。加载器把技能、工具服务器、应用声明和钩子整理给主程序用。管理器负责安装、卸载、升级、缓存同步和分享。连接器策略会判断外部应用能不能用、是否要确认。最后,推荐工具和安装请求工具把可装内容展示出来,但真正安装前仍要让用户点头。

本阶段的文件34

插件子系统基础

这些文件定义核心 crate 接口,以及底层 manifest、provider、routing、marketplace、loader 和 manager 层,插件与连接器管理中的其他内容都构建在其上。

core-plugins/src/lib.rs源码 ↗
othercross-cutting

可以把这个文件想成一家商场的总服务台。插件安装、卸载、升级、远程市场、启动同步、加载器等具体柜台都在别的文件里;这里不亲自干复杂活,而是告诉外面“哪些柜台存在、哪些服务可以直接来这里问”。它还定义了几个 OpenAI 官方插件市场的固定名字,避免各处手写字符串写错。LoadedPluginPluginLoadOutcome 是给插件加载结果取的统一别名,让外部代码不用关心底层泛型细节。最后,大量 pub use 把内部模块里的重要类型重新导出,相当于把分散的入口集中到一个清晰的公共 API。如果没有这个文件,外部代码就得到处找内部模块,既容易依赖错地方,也更难维护。

函数细节1
is_openai_curated_marketplace_name26–29 ↗
fn is_openai_curated_marketplace_name(marketplace_name: &str) -> bool

作用:这个函数用来判断一个插件市场名字是不是 OpenAI 官方精选市场。有人需要区分“官方精选来源”和普通第三方来源时,就会用它。

数据流:进去的是一个市场名字字符串。函数把它分别和两个固定官方名字比较:openai-curatedopenai-api-curated。如果匹配其中任何一个,就出来 true;否则出来 false。它不改动任何数据,只做一次简单判断。

调用关系:它是这个库对外提供的小工具函数,配合文件里定义的官方市场常量使用。其他插件管理、市场读取或展示流程在需要识别官方精选市场时,可以调用它,而它本身不再把任务交给别的函数。

core-plugins/src/provider.rs源码 ↗
domain_logicplugin discovery / request handling

这个文件解决的是“插件到底从哪里来”的问题。系统里可能有不同的执行环境,每个环境都有自己的文件系统,就像不同的房间有各自的柜子。用户选中一个能力根目录后,这里会先确认这个目录路径是合法的绝对路径,再找到对应的执行环境,然后只通过这个环境提供的文件系统去检查目录、寻找插件清单文件、读取清单内容,并把清单解析成系统能理解的插件描述。它还把插件描述和当时使用的文件系统绑在一起保存,方便后面加载插件文件时继续用同一个“柜子”,不会拿错地方。文件里定义了很细的错误类型,比如路径不合法、环境不存在、根目录不是文件夹、清单读不了或解析失败,这样出错时能清楚告诉用户是哪一步坏了。

函数细节7
ResolvedExecutorPlugin::plugin89–91 ↗
fn plugin(&self) -> &ResolvedPlugin

作用:返回已经解析好的插件描述。别人需要知道这个插件叫什么、入口在哪里、声明了什么能力时,会用它来取出这些信息。

数据流:进去的是这个已绑定插件对象本身 → 它不做复制,也不重新读取文件,只是把里面保存的插件描述借出来 → 出来的是一个只读的插件描述引用,原对象没有变化。

调用关系:它通常在后续的 load 流程里被调用。前面 resolve_bound 已经把插件解析好并放进 ResolvedExecutorPlugin,load 再通过这个函数拿到插件说明,继续决定怎么加载。

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

ResolvedExecutorPlugin::file_system94–96 ↗
fn file_system(&self) -> &dyn ExecutorFileSystem

作用:返回解析这个插件时用过的那个文件系统。这样后续读取插件包里的文件时,还会从同一个执行环境里读,不会跑到别的地方。

数据流:进去的是这个已绑定插件对象本身 → 它取出内部保存的文件系统对象,并把它当作通用的 ExecutorFileSystem 使用 → 出来的是一个只读的文件系统引用,方便后面继续读文件。

调用关系:它也会在 load 流程里被调用。plugin 函数给出“插件说明”,file_system 函数给出“去哪里读插件文件”,两者一起保证加载阶段既知道读什么,也知道从哪里读。

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

ExecutorPluginProvider::new101–105 ↗
fn new(environment_manager: Arc<EnvironmentManager>) -> Self

作用:创建一个插件提供者,并把执行环境管理器交给它。执行环境管理器可以理解成一张登记表,记录当前有哪些可用环境。

数据流:进去的是一个共享的 EnvironmentManager → 它把这个管理器保存到 ExecutorPluginProvider 里面 → 出来的是一个新的提供者对象,以后可以靠它查环境、找插件。

调用关系:这是使用这个提供者前的准备步骤。测试里会直接调用它来搭建场景,正常系统启动或组装组件时也会创建它,然后后续通过 resolve 或 resolve_bound 去真正解析插件。

调用图:被 6 处调用(host_and_executor_sources_parse_the_same_manifest, executor_root_must_be_an_explicit_absolute_path, malformed_preferred_manifest_does_not_fall_through_to_alternate, standalone_capability_root_is_not_a_plugin, unavailable_environment_does_not_fall_back_to_host_filesystem, new)。

ExecutorPluginProvider::resolve_bound108–129 ↗
async fn resolve_bound(
        &self,
        selected_root: &SelectedCapabilityRoot,
    ) -> Result<Option<ResolvedExecutorPlugin>, ExecutorPluginProviderError>

作用:根据用户选中的根目录,解析出一个插件,并且把插件和读取它的文件系统绑在一起返回。它的重点是“找到插件”之外,还记住“从哪个环境读到的”。

数据流:进去的是一个 SelectedCapabilityRoot,也就是用户选中的能力根目录信息 → 它先调用 selected_plugin_root 检查并转换路径,再用环境编号从 EnvironmentManager 找到对应环境,拿到该环境的文件系统,然后把目录和文件系统交给 resolve_plugin_root 查找并解析清单 → 出来的是可能存在的 ResolvedExecutorPlugin;如果目录里没有插件清单,就返回空;如果环境不存在或读取失败,就返回明确错误。

调用关系:它是这个文件的核心入口之一。PluginProvider 的 resolve 会调用它,只是最后丢掉文件系统绑定;resolve_snapshot 这类需要保留文件系统的流程也会调用它。它自己把路径检查交给 selected_plugin_root,把实际看目录、找清单、读清单、构造插件描述的工作交给 resolve_plugin_root。

调用图:调用 2 个内部函数(resolve_plugin_root, selected_plugin_root);被 2 处调用(resolve, resolve_snapshot)。

ExecutorPluginProvider::resolve135–142 ↗
async fn resolve(
        &self,
        selected_root: &SelectedCapabilityRoot,
    ) -> Result<Option<ResolvedPlugin>, Self::Error>

作用:实现通用的 PluginProvider 接口,返回普通的插件描述。它适合只关心“有没有插件、插件是什么”,不需要继续持有文件系统的调用者。

数据流:进去的是用户选中的能力根目录 → 它调用 resolve_bound 完成真正解析 → 如果解析到了已绑定插件,就只取出里面的 ResolvedPlugin 返回;如果没有插件就返回空;如果出错就把错误原样传出去。

调用关系:这是对外接口的一层适配。真正干活的是 resolve_bound;它存在的原因是让 ExecutorPluginProvider 能符合 codex_plugin::PluginProvider 这个通用约定,被其他只认识 PluginProvider 的代码直接使用。

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

selected_plugin_root145–165 ↗
fn selected_plugin_root(
    selected_root: &SelectedCapabilityRoot,
) -> Result<AbsolutePathBuf, ExecutorPluginProviderError>

作用:检查用户选中的插件根目录路径是否合格,并把它变成系统内部认可的绝对路径。绝对路径就是从磁盘根部开始写清楚的位置,避免相对路径造成“到底从哪儿算起”的混乱。

数据流:进去的是 SelectedCapabilityRoot,里面带有根目录编号和路径字符串 → 它取出环境位置里的 path,先确认这个路径是绝对路径,再用 from_absolute_path_checked 做更严格的合法性检查 → 出来的是 AbsolutePathBuf;如果路径不是绝对路径或格式不合法,就返回 InvalidRootPath 错误,并带上根目录编号和原因。

调用关系:resolve_bound 一开始就会调用它。只有路径先过关,后面 resolve_plugin_root 才会去访问文件系统;这样可以把“路径本身不可信”的问题挡在最前面。

调用图:调用 1 个内部函数(from_absolute_path_checked);被 1 处调用(resolve_bound);外部调用 1 个(from)。

resolve_plugin_root167–246 ↗
async fn resolve_plugin_root(
    selected_root: &SelectedCapabilityRoot,
    plugin_root: AbsolutePathBuf,
    file_system: &dyn ExecutorFileSystem,
) -> Result<Option<ResolvedPlugin>, ExecutorPlugin

作用:在指定执行环境的文件系统里,检查一个目录是否是插件包,并把它变成系统可用的插件描述。它负责真正的“进目录找清单、读清单、解析清单”。

数据流:进去的是选中的根目录信息、已经确认过的插件根路径、以及对应环境的文件系统 → 它先把路径转成文件 URI(一种把文件路径写成统一地址的格式),检查根路径是不是文件夹;然后按预设的清单文件位置逐个尝试,找到第一个真实文件;接着读取清单文本,调用 parse_plugin_manifest 解析内容;最后用 ResolvedPlugin::from_environment 把根目录、环境编号、清单路径和清单内容组合成插件描述 → 出来的是 Some 插件描述;如果没有任何清单文件,出来的是 None;如果检查、读取、解析或构造失败,就返回对应的详细错误。

调用关系:它由 resolve_bound 调用,是解析插件的实际工作台。它会依次使用文件系统的 get_metadata 检查目录和候选清单,用 read_file_text 读取清单,用 parse_plugin_manifest 理解清单内容,再用 from_environment 生成最终的插件描述。

调用图:调用 5 个内部函数(parse_plugin_manifest, read_file_text, from_environment, join, from_abs_path);被 1 处调用(resolve_bound);外部调用 1 个(get_metadata)。

core-plugins/src/manifest.rs源码 ↗
domain_logic插件发现和加载时

插件就像一个小应用,系统要先看它的“说明书”才知道它叫什么、有哪些功能、图标在哪、有没有 hooks(插件在特定时机要运行的动作)等。这个文件做的事,就是找到这本说明书,读出来,检查格式,再转成内部统一的 PluginManifest。它特别在意路径安全:清单里的文件路径必须写成 ./xxx,不能偷偷写到插件目录外面,也不能用 .. 往上钻。它还会清理一些展示用信息,比如默认提示词会去掉多余空格、限制最多 3 条、每条最长 128 个字符。写错的字段不会让整个插件一定崩掉,很多时候只是打警告然后忽略,避免一个小问题拖垮插件加载流程。文件末尾的测试则像验收清单,确认旧格式、新格式、备用清单路径、版本号修剪等行为都符合预期。

函数细节20
load_plugin_manifest111–124 ↗
fn load_plugin_manifest(plugin_root: &Path) -> Option<PluginManifest>

作用:从本机磁盘上的某个插件目录里,找到并读取插件清单文件。别人想知道一个插件的基本信息时,通常会先调用它。

数据流:输入是插件根目录路径。它先找清单文件的位置,再把文件内容读成文字,然后交给 parse_plugin_manifest 解析;如果找不到、读不了或解析失败,就返回空值,并在解析失败时记一条警告。

调用关系:它是这个文件对外最常用的入口。插件加载、读取插件应用、读取插件 MCP 服务器、生成遥测信息、市场页详情等流程都会用它;它自己把真正的理解和整理工作交给 parse_plugin_manifest。

调用图:调用 1 个内部函数(parse_plugin_manifest);被 12 处调用(load_declared_plugin_mcp_servers, load_plugin, load_plugin_apps, plugin_telemetry_metadata_from_root, load_sources, read_plugin_detail_for_marketplace_plugin, host_and_executor_sources_parse_the_same_manifest, load_manifest, resolve_marketplace_plugin_entry, extract_remote_plugin_bundle_to_path (+2 more));外部调用 3 个(find_plugin_manifest_path, read_to_string, warn!)。

parse_plugin_manifest126–230 ↗
fn parse_plugin_manifest(
    plugin_root: &Path,
    manifest_path: &Path,
    contents: &str,
) -> Result<PluginManifest, serde_json::Error>

作用:把 plugin.json 的原始文字,变成系统内部使用的插件清单对象。它不只是“读 JSON”,还会补默认值、清理字段、检查路径。

数据流:输入是插件根目录、清单文件路径和 JSON 文本。它先按宽松的原始结构读出字段,再决定插件名、修剪版本号、整理界面信息、解析默认提示词和各种文件路径,最后输出一个 PluginManifest。

调用关系:load_plugin_manifest 会在读到文件后调用它;其他解析插件根目录的流程也会直接用它。它把路径检查交给 resolve_manifest_path、resolve_manifest_path_value、resolve_manifest_hooks 等小函数,把默认提示词整理交给 resolve_default_prompts。

调用图:调用 3 个内部函数(resolve_manifest_hooks, resolve_manifest_path, resolve_manifest_path_value);被 3 处调用(load_plugin_manifest, resolve_plugin_root, plugin_root_resolution_uses_supplied_executor_file_system);外部调用 1 个(file_name)。

resolve_manifest_hooks232–260 ↗
fn resolve_manifest_hooks(
    plugin_root: &Path,
    hooks: Option<RawPluginManifestHooks>,
) -> Option<PluginManifestHooks>

作用:整理清单里的 hooks 字段。hooks 可以理解成“插件在某些时机自动执行的规则”,这个函数负责接受几种写法并转成统一格式。

数据流:输入是插件根目录和原始 hooks 值。它根据 hooks 是单个路径、多个路径、内联对象、对象数组还是错误格式来处理:路径会被安全解析,内联内容会直接包装起来,错误格式会被警告并忽略;输出是可用的 hooks 配置或空值。

调用关系:parse_plugin_manifest 在组装最终清单时调用它。它遇到路径时继续交给 resolve_manifest_path 检查,遇到明显不合规的内容则只记录警告,不阻断整个清单解析。

调用图:调用 1 个内部函数(resolve_manifest_path);被 1 处调用(parse_plugin_manifest);外部调用 4 个(Inline, Paths, warn!, vec!)。

resolve_interface_asset_path262–268 ↗
fn resolve_interface_asset_path(
    plugin_root: &Path,
    field: &'static str,
    path: Option<&str>,
) -> Option<AbsolutePathBuf>

作用:解析界面资源文件的路径,比如插件图标、Logo、截图。它存在是为了让这些展示资源也走同一套安全路径规则。

数据流:输入是插件根目录、字段名和用户写的路径字符串。它直接把这些信息交给 resolve_manifest_path,得到插件目录内的绝对路径,或者在路径不合规时得到空值。

调用关系:parse_plugin_manifest 整理 interface 展示信息时会用它。它本身只是一个薄包装,让图标、Logo、截图这些字段的代码读起来更清楚。

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

resolve_default_prompts270–325 ↗
fn resolve_default_prompts(
    manifest_path: &Path,
    value: Option<&RawPluginManifestDefaultPrompt>,
) -> Option<Vec<String>>

作用:整理插件界面里的默认提示词。默认提示词就是用户打开插件时可以直接点选或预填的一小段话。

数据流:输入是清单文件路径和 defaultPrompt 原始值。它支持旧的单字符串写法,也支持字符串数组;每条会交给 resolve_default_prompt_str 清理和检查,遇到数字、对象等错误内容会警告,超过最多 3 条就停止;输出是整理好的提示词列表或空值。

调用关系:parse_plugin_manifest 在处理 interface.defaultPrompt 时调用它。它负责整体形状判断,具体每条文字的空白清理和长度限制交给 resolve_default_prompt_str,错误提示交给 warn_invalid_default_prompt。

调用图:调用 2 个内部函数(resolve_default_prompt_str, warn_invalid_default_prompt);外部调用 2 个(new, format!)。

resolve_default_prompt_str327–342 ↗
fn resolve_default_prompt_str(manifest_path: &Path, field: &str, prompt: &str) -> Option<String>

作用:检查并清理一条默认提示词。它保证展示给用户的提示词不是空的,也不会长到不适合放在界面里。

数据流:输入是清单路径、字段名和原始提示词。它会把连续空白压成普通单空格,去掉前后多余空白;如果结果为空或超过 128 个字符,就记录警告并返回空值;合格时返回清理后的文字。

调用关系:resolve_default_prompts 会对每一条候选提示词调用它。它发现问题时不自己拼复杂日志,而是交给 warn_invalid_default_prompt 统一记录。

调用图:调用 1 个内部函数(warn_invalid_default_prompt);被 1 处调用(resolve_default_prompts);外部调用 1 个(format!)。

warn_invalid_default_prompt344–349 ↗
fn warn_invalid_default_prompt(manifest_path: &Path, field: &str, message: &str)

作用:专门记录默认提示词格式不对的警告。这样相关警告的格式统一,排查问题时更容易看懂。

数据流:输入是清单文件路径、出问题的字段名和说明文字。它把这些信息写进日志;没有返回有业务意义的数据,也不会修改清单。

调用关系:resolve_default_prompts 和 resolve_default_prompt_str 发现默认提示词不合格时会调用它。它是错误提示的小出口,负责把“哪里错了”讲清楚。

调用图:被 2 处调用(resolve_default_prompt_str, resolve_default_prompts);外部调用 1 个(warn!)。

json_value_type351–360 ↗
fn json_value_type(value: &JsonValue) -> &'static str

作用:把 JSON 值的真实类型翻译成容易写进警告里的英文词,比如 string、array、object。这样日志能说清楚“期望什么,实际来了什么”。

数据流:输入是一个 JSON 值。它判断这个值是空、布尔值、数字、字符串、数组还是对象,然后返回对应的类型名称字符串。

调用关系:多个校验函数在发现字段类型不对时会用它来生成警告文字。它不参与解析主流程,只负责让报错信息更明白。

resolve_manifest_path_value362–377 ↗
fn resolve_manifest_path_value(
    plugin_root: &Path,
    field: &'static str,
    path: Option<&RawPluginManifestPath>,
) -> Option<AbsolutePathBuf>

作用:处理那种“应该是一个路径字符串”的清单字段,比如 skills。它先确认字段确实是字符串,再交给统一路径检查。

数据流:输入是插件根目录、字段名和原始路径值。值如果是字符串,就调用 resolve_manifest_path 解析成安全的绝对路径;如果是别的 JSON 类型,就记录警告并返回空值。

调用关系:parse_plugin_manifest 在处理 skills 字段时调用它。它是原始 JSON 类型和安全路径解析之间的过渡层。

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

resolve_manifest_path379–420 ↗
fn resolve_manifest_path(
    plugin_root: &Path,
    field: &'static str,
    path: Option<&str>,
) -> Option<AbsolutePathBuf>

作用:把清单里的相对路径变成安全的绝对路径。它是防止插件路径乱指、越界访问的重要守门员。

数据流:输入是插件根目录、字段名和路径字符串。它要求路径必须以 ./ 开头,不能只是 ./,不能包含 ..,也不能包含会逃出插件目录的特殊路径片段;通过检查后,把它拼到插件根目录下并转成绝对路径,失败则记录警告并返回空值。

调用关系:parse_plugin_manifest、resolve_interface_asset_path、resolve_manifest_hooks、resolve_manifest_path_value 都依赖它。凡是清单里提到本地文件的位置,基本都要经过这道安全检查。

调用图:调用 1 个内部函数(try_from);被 4 处调用(parse_plugin_manifest, resolve_interface_asset_path, resolve_manifest_hooks, resolve_manifest_path_value);外部调用 4 个(join, new, new, warn!)。

tests::write_manifest444–460 ↗
fn write_manifest(plugin_root: &Path, version: Option<&str>, interface: &str)

作用:测试用的小帮手,用来快速在临时插件目录里写一个标准位置的 plugin.json。这样每个测试不用重复写建目录和写文件的代码。

数据流:输入是插件根目录、可选版本号和 interface JSON 片段。它创建 .codex-plugin 目录,把这些内容拼成完整清单文件并写到磁盘上;输出不返回清单,只改变测试临时目录里的文件。

调用关系:多个测试在准备测试插件时调用它。后续测试再通过 tests::load_manifest 或 load_plugin_manifest 读取它刚写好的文件。

调用图:外部调用 4 个(join, format!, create_dir_all, write)。

tests::write_alternate_plugin_manifest462–467 ↗
fn write_alternate_plugin_manifest(plugin_root: &Path, contents: &str)

作用:测试用来写备用位置的插件清单文件。它验证系统不只认识默认清单路径,也认识另一种可发现路径。

数据流:输入是插件根目录和清单内容。它创建备用清单所在目录,把内容写入 .claude-plugin/plugin.json;输出主要是磁盘上多了这个测试文件。

调用关系:tests::plugin_manifest_uses_alternate_discoverable_path 会调用它,然后再调用加载流程确认备用路径能被发现。

调用图:外部调用 3 个(join, create_dir_all, write)。

tests::load_manifest469–471 ↗
fn load_manifest(plugin_root: &Path) -> PluginManifest

作用:测试里的简化读取函数。它把 load_plugin_manifest 的空值情况直接当成测试失败,方便测试代码更短。

数据流:输入是插件根目录。它调用 load_plugin_manifest,期望一定能得到 PluginManifest;如果没得到,测试会失败;成功时返回清单对象。

调用关系:多个测试用它读取前面写好的临时清单。它是测试代码和正式加载函数之间的一层方便包装。

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

tests::plugin_interface_accepts_legacy_default_prompt_string474–493 ↗
fn plugin_interface_accepts_legacy_default_prompt_string()

作用:确认旧版写法还能用:defaultPrompt 如果是单个字符串,也会被接受。这样老插件不会因为格式升级突然坏掉。

数据流:它创建临时目录,写入一个 defaultPrompt 为字符串的清单,然后加载清单,最后检查提示词被清理成单元素列表,并且多余空格被压掉。

调用关系:这个测试通过 write_manifest 准备文件,通过 tests::load_manifest 走正式加载流程,最终验证 resolve_default_prompts 和 resolve_default_prompt_str 的效果。

调用图:外部调用 4 个(assert_eq!, load_manifest, write_manifest, tempdir)。

tests::plugin_interface_normalizes_default_prompt_array496–530 ↗
fn plugin_interface_normalizes_default_prompt_array()

作用:确认 defaultPrompt 数组会被正确筛选和清理。它覆盖了正常文字、错误类型、太长、空字符串、超过数量上限等情况。

数据流:它写入一个混合了多种情况的提示词数组,加载后检查结果只保留 3 条合格提示词,并且每条都被整理掉多余空白。

调用关系:这个测试主要验证 resolve_default_prompts 的列表处理规则,也间接验证单条提示词清理函数和最多 3 条的限制。

调用图:外部调用 5 个(assert_eq!, load_manifest, write_manifest, format!, tempdir)。

tests::plugin_interface_ignores_invalid_default_prompt_shape533–549 ↗
fn plugin_interface_ignores_invalid_default_prompt_shape()

作用:确认 defaultPrompt 如果写成对象这类不支持的形状,会被忽略而不是误解析。这样坏字段不会污染最终清单。

数据流:它写入一个 defaultPrompt 为对象的清单,加载后取出 interface,检查 default_prompt 是空值。

调用关系:这个测试走 write_manifest 和 tests::load_manifest,重点覆盖 resolve_default_prompts 对整体类型错误的处理。

调用图:外部调用 4 个(assert_eq!, load_manifest, write_manifest, tempdir)。

tests::plugin_manifest_reads_trimmed_version552–566 ↗
fn plugin_manifest_reads_trimmed_version()

作用:确认版本号两边的空格会被去掉。这样插件作者不小心多打空格时,系统内部仍拿到干净版本号。

数据流:它写入带前后空格的 version 字段,加载清单,然后断言最终 version 是修剪后的 1.2.3-beta+7。

调用关系:这个测试通过正式加载流程检查 parse_plugin_manifest 对 version 字段的清理逻辑。

调用图:外部调用 4 个(assert_eq!, load_manifest, write_manifest, tempdir)。

tests::plugin_manifest_reads_keywords569–588 ↗
fn plugin_manifest_reads_keywords()

作用:确认清单里的 keywords 关键字列表能被读出来。关键字通常用于描述插件用途,方便搜索、分类或展示。

数据流:它手动创建清单目录并写入带 keywords 的 JSON,加载后检查最终清单里的关键字列表和文件内容一致。

调用关系:这个测试直接准备文件,再通过 tests::load_manifest 进入正式读取流程,验证 parse_plugin_manifest 没有漏掉 keywords 字段。

调用图:外部调用 5 个(assert_eq!, load_manifest, create_dir_all, write, tempdir)。

tests::plugin_manifest_uses_alternate_discoverable_path591–615 ↗
fn plugin_manifest_uses_alternate_discoverable_path()

作用:确认系统能从备用清单路径发现插件。这样兼容不同来源或历史格式的插件目录。

数据流:它在备用位置写入 plugin.json,里面包含版本号和显示名;加载后检查版本号被修剪,显示名也被正确读出。

调用关系:这个测试调用 write_alternate_plugin_manifest 准备备用路径文件,再通过 tests::load_manifest 间接验证 load_plugin_manifest 使用的清单发现逻辑。

调用图:外部调用 4 个(assert_eq!, load_manifest, write_alternate_plugin_manifest, tempdir)。

tests::host_and_executor_sources_parse_the_same_manifest618–657 ↗
async fn host_and_executor_sources_parse_the_same_manifest()

作用:确认主机端和执行器端看到的是同一个插件结果。简单说,就是不同入口读同一份插件清单,不能读出两套不一样的解释。

数据流:它创建临时插件并写入清单,先用 load_plugin_manifest 得到主机端清单;再创建执行器插件提供者,让它解析同一个插件目录;最后手工构造期望的 ResolvedPlugin,并比较两边完全一致。

调用关系:这是跨模块的集成测试。它既调用本文件的 load_plugin_manifest,也调用 ExecutorPluginProvider 的解析流程,用来保证这个文件的清单解析规则和执行器那边使用的规则保持一致。

调用图:调用 5 个内部函数(load_plugin_manifest, new, default_for_tests, from_environment, from_absolute_path_checked);外部调用 4 个(new, assert_eq!, write_manifest, tempdir)。

core-plugins/src/app_mcp_routing.rs源码 ↗
domain_logicplugin load and plugin detail resolution

这个文件像一个交通分流员。项目里既可能有“AppDeclaration(应用声明,告诉系统某个应用能力存在)”,也可能有“MCP server(Model Context Protocol 服务器,一种让模型调用外部工具的服务入口)”。如果当前认证方式不是走 Codex 后端,这类应用路由就不能用,文件会把应用声明清空,避免系统展示或使用不该用的入口。如果应用路由可用,并且插件功能已经启用,它会检查应用声明的名字,然后从 MCP 服务器列表里删掉同名项。这样做是为了防止同一个插件既作为应用出现,又作为 MCP 服务器出现,造成重复、冲突或让用户看到两份一样的能力。整体逻辑很短,但位置很关键:它是在插件加载、市场插件详情生成、认证后的插件解析等流程里统一套用的一条路由规则。

函数细节2
apps_route_available6–8 ↗
fn apps_route_available(auth_mode: Option<AuthMode>) -> bool

作用:这个函数判断当前登录或认证方式是否支持“应用路由”。简单说,就是看系统现在是不是能走 Codex 后端;能走才允许这些应用声明继续存在。

数据流:输入是一份可能存在也可能不存在的 AuthMode(认证模式,也就是系统怎么确认用户身份和后端能力)。函数检查它是否存在,并询问这个认证模式是否使用 Codex 后端。最后输出一个布尔值:true 表示应用路由可用,false 表示不可用。

调用关系:它是这条规则的第一道门槛。apply_app_mcp_routing_policy 会先调用它决定要不要清空应用声明;load_plugin_mcp_servers 也会用它判断加载插件 MCP 服务器时是否要考虑应用路由。

调用图:被 2 处调用(apply_app_mcp_routing_policy, load_plugin_mcp_servers)。

apply_app_mcp_routing_policy10–28 ↗
fn apply_app_mcp_routing_policy(
    apps: &mut Vec<AppDeclaration>,
    mcp_servers: &mut HashMap<String, M>,
    auth_mode: Option<AuthMode>,
    plugin_active: bool,
)

作用:这个函数把“应用声明”和“MCP 服务器”之间的冲突处理掉。它会在应用路由不可用时清空应用声明;在可用且插件启用时,删除和应用同名的 MCP 服务器,避免重复登记。

数据流:输入包括一份可修改的应用声明列表、一份可修改的 MCP 服务器表、当前认证模式,以及插件是否启用。它先用 apps_route_available 判断应用路由能不能用;不能用就把应用列表清空。能用时,如果插件启用且应用列表不为空,它会收集所有应用名字,再把 MCP 服务器表里同名的条目删掉。结果是:应用列表和 MCP 服务器表被原地改好,不额外返回新对象。

调用关系:它是插件加载和展示前的一道统一整理步骤。load_plugin_mcp_servers、read_plugin_detail_for_marketplace_plugin、resolve_loaded_plugins_for_auth、build_remote_plugin_detail 会在不同场景下调用它,确保无论是加载插件、读取市场详情,还是按认证状态解析插件,最终都遵守同一套去重和可用性规则。它自己只把“是否可用”的判断交给 apps_route_available。

调用图:调用 1 个内部函数(apps_route_available);被 4 处调用(load_plugin_mcp_servers, read_plugin_detail_for_marketplace_plugin, resolve_loaded_plugins_for_auth, build_remote_plugin_detail)。

core-plugins/src/marketplace.rs源码 ↗
domain_logicconfig load / marketplace discovery / plugin install

可以把这个文件理解成插件市场的“导购加验货员”。插件市场用一个 marketplace.json 文件列出插件,本文件先在几个固定位置找这个文件,再把里面的 JSON 内容读出来。读到每个插件后,它会检查来源:本地路径必须老老实实待在市场根目录里,Git 地址会被整理成标准写法,奇怪或不支持的来源会被跳过并写警告。它还会读取本地插件自己的 manifest(插件说明书),补上版本、关键词、界面分类等信息。安装前,它会再看安装策略和产品限制,确认这个插件是不是允许装。文件里还定义了 MarketplaceError 这类错误,方便把“文件不存在”“格式错”“插件不可安装”等情况说清楚。

函数细节31
PluginInstallPolicy::from112–118 ↗
fn from(value: MarketplacePluginInstallPolicy) -> Self

作用:把本文件内部使用的安装策略,转换成应用服务器协议里要用的安装策略。这样市场读出来的规则可以直接交给外层接口展示或传输。

数据流:进去的是一个市场里的安装规则,比如不可用、可用、默认安装;函数按同名含义做一一对应;出来的是协议层认识的 PluginInstallPolicy,不改动其他数据。

调用关系:它是两个类型之间的翻译器。市场解析代码先得到内部规则,之后当这些规则要发给应用服务器协议时,就靠这个转换保持说法一致。

PluginAuthPolicy::from122–127 ↗
fn from(value: MarketplacePluginAuthPolicy) -> Self

作用:把市场里的认证策略转换成协议层的认证策略。认证策略就是说明插件是在安装时要授权,还是使用时才授权。

数据流:进去的是内部 MarketplacePluginAuthPolicy;函数把 OnInstall 或 OnUse 映射成协议里的同等值;出来的是 PluginAuthPolicy,不读取文件也不产生副作用。

调用关系:它和 PluginInstallPolicy::from 类似,负责把市场模块的内部说法翻译成外部接口能理解的说法。

MarketplaceError::io167–169 ↗
fn io(context: &'static str, source: io::Error) -> Self

作用:快速包装一个文件读写错误,并附上一句固定的上下文说明。这样报错时不是只说“失败”,而是能说清楚是在读市场文件时失败。

数据流:进去的是一段简短场景文字和底层 io::Error(输入输出错误,比如读文件失败);函数把它们装进 MarketplaceError::Io;出来的是统一的市场错误对象。

调用关系:它被 load_raw_marketplace_manifest 用来把系统文件错误变成市场模块自己的错误格式,方便上层统一处理和展示。

find_marketplace_plugin172–195 ↗
fn find_marketplace_plugin(
    marketplace_path: &AbsolutePathBuf,
    plugin_name: &str,
) -> Result<ResolvedMarketplacePlugin, MarketplaceError>

作用:在指定的市场清单里按名字找一个插件,并把它整理成系统能使用的完整插件信息。别人想从某个市场拿到某个插件时,会先用它。

数据流:进去的是市场文件路径和插件名;它先调用 load_raw_marketplace_manifest 读取原始清单,再逐个插件比较名字,命中后交给 resolve_marketplace_plugin_entry 校验和整理来源、策略、界面信息;出来的是 ResolvedMarketplacePlugin,找不到就返回 PluginNotFound 错误。

调用关系:它是按名查插件的入口。read_plugin_for_config 和 find_installable_marketplace_plugin 会调用它;它自己把读文件交给 load_raw_marketplace_manifest,把单个插件的细节整理交给 resolve_marketplace_plugin_entry。

调用图:调用 2 个内部函数(load_raw_marketplace_manifest, resolve_marketplace_plugin_entry);被 2 处调用(read_plugin_for_config, find_installable_marketplace_plugin)。

find_installable_marketplace_plugin197–220 ↗
fn find_installable_marketplace_plugin(
    marketplace_path: &AbsolutePathBuf,
    plugin_name: &str,
    restriction_product: Option<Product>,
) -> Result<ResolvedMarketplacePlugin, MarketplaceError

作用:找插件的同时确认它真的允许安装。它会检查插件市场写的安装规则,以及这个插件是否允许当前产品使用。

数据流:进去的是市场路径、插件名和可选的产品限制;它先用 find_marketplace_plugin 找到插件,再看安装策略是不是 NotAvailable,并检查 products 列表是否匹配当前产品;通过就原样返回插件,不通过就返回 PluginNotAvailable。

调用关系:安装流程 install_plugin 和 install_plugin_with_remote_sync 会用它做最后把关。它站在 find_marketplace_plugin 之后,相当于“找到了不等于能装,还要过许可检查”。

调用图:调用 1 个内部函数(find_marketplace_plugin);被 2 处调用(install_plugin, install_plugin_with_remote_sync)。

list_marketplaces222–226 ↗
fn list_marketplaces(
    additional_roots: &[AbsolutePathBuf],
) -> Result<MarketplaceListOutcome, MarketplaceError>

作用:列出系统能发现的所有插件市场。调用者只要给额外的搜索位置,它会自动把用户主目录也算进去。

数据流:进去的是额外根目录列表;它调用 home_dir 找用户主目录,再调用 list_marketplaces_with_home 做实际发现和加载;出来的是 MarketplaceListOutcome,里面有成功加载的市场和加载失败的错误列表。

调用关系:它是对外常用的市场列表入口。刷新插件缓存、按配置发现市场、列出市场配置时会调用它;真正的细活交给 list_marketplaces_with_home。

调用图:调用 2 个内部函数(home_dir, list_marketplaces_with_home);被 3 处调用(refresh_non_curated_plugin_cache_with_mode, discover_marketplaces_for_config, list_marketplaces_for_config)。

home_dir228–236 ↗
fn home_dir() -> Option<PathBuf>

作用:找当前用户的主目录,也就是通常放用户级配置的地方。它尽量兼容不同系统和环境变量。

数据流:它不需要参数;先看 HOME 和 USERPROFILE 环境变量,要求值非空且是绝对路径,找不到再问系统库 dirs::home_dir;出来的是一个可选路径,找不到就是 None。

调用关系:list_marketplaces 用它把用户主目录纳入市场搜索范围,checkout_remote_plugin_share 也会用它决定远程插件共享内容放在哪里。

调用图:被 2 处调用(list_marketplaces, checkout_remote_plugin_share)。

validate_marketplace_root238–247 ↗
fn validate_marketplace_root(root: &Path) -> Result<String, MarketplaceError>

作用:检查一个目录是不是合法的插件市场根目录,并返回这个市场的名字。它常用于用户配置或升级时,防止把普通文件夹误当成市场。

数据流:进去的是一个目录路径;它先用 find_marketplace_manifest_path 看目录下有没有支持的 marketplace.json 位置,再用 load_marketplace 真正读取并校验;出来的是市场名,失败时返回说明清楚的 MarketplaceError。

调用关系:查找、安装、升级已配置市场时会调用它。它先找清单位置,再把完整加载工作交给 load_marketplace。

调用图:调用 2 个内部函数(find_marketplace_manifest_path, load_marketplace);被 4 处调用(find_marketplace_root_by_name, installed_marketplace_root_for_source, validate_marketplace_source_root, upgrade_configured_git_marketplace);外部调用 1 个(to_path_buf)。

find_marketplace_manifest_path249–259 ↗
fn find_marketplace_manifest_path(root: &Path) -> Option<AbsolutePathBuf>

作用:在一个根目录下面寻找支持的市场清单文件。它知道系统认可的几个固定相对位置。

数据流:进去的是根目录;它依次拼出 .agents/plugins/marketplace.json、.agents/plugins/api_marketplace.json、.claude-plugin/marketplace.json,哪个真实存在就转成绝对路径返回;都不存在就返回 None。

调用关系:导入插件、检查配置快照、发现市场路径、验证市场根目录、升级市场配置都会用它。它是“这个目录里有没有市场清单”的基础探测器。

调用图:被 5 处调用(import_plugins, configured_marketplace_snapshot_issues, discover_marketplace_paths_from_roots, validate_marketplace_root, upgrade_configured_git_marketplace)。

supported_marketplace_manifest_path261–272 ↗
fn supported_marketplace_manifest_path(path: &Path) -> Option<AbsolutePathBuf>

作用:判断传入的路径本身是不是一个受支持位置上的市场清单文件。它比只看文件存在更严格,还会检查目录布局对不对。

数据流:进去的是一个文件路径;它先确认这是文件,再用受支持的布局规则反推市场根目录,最后转成绝对路径;符合就返回 Some,失败就返回 None。

调用关系:discover_marketplace_paths_from_roots 会先用它处理那些直接指向 marketplace.json 的输入。这样用户既可以给目录,也可以给清单文件。

调用图:调用 1 个内部函数(try_from);被 1 处调用(discover_marketplace_paths_from_roots);外部调用 2 个(is_file, to_path_buf)。

invalid_marketplace_layout_error274–279 ↗
fn invalid_marketplace_layout_error(path: &AbsolutePathBuf) -> MarketplaceError

作用:生成“市场文件放错位置了”的错误。它把这种布局问题统一包装成 InvalidMarketplaceFile。

数据流:进去的是市场清单路径;函数复制这个路径,并附上固定提示“marketplace file is not in a supported location”;出来的是 MarketplaceError。

调用关系:marketplace_root_dir 在无法从清单路径推出合法市场根目录时会调用它,避免各处手写不一致的错误消息。

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

marketplace_root_from_layout281–294 ↗
fn marketplace_root_from_layout(marketplace_path: &Path, relative_path: &str) -> Option<PathBuf>

作用:根据一个已知的清单相对位置,反推出市场的根目录。简单说,就是从 marketplace.json 往上倒着数目录。

数据流:进去的是市场文件路径和一种支持的相对布局;它从相对路径的最后一段开始,逐段检查文件名和目录名是否吻合,吻合就一路往父目录走;出来的是根目录路径,不吻合就 None。

调用关系:marketplace_root_dir 用它尝试每一种支持布局。它是判断“这个 marketplace.json 是不是摆在规定位置”的核心小工具。

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

load_marketplace296–341 ↗
fn load_marketplace(path: &AbsolutePathBuf) -> Result<Marketplace, MarketplaceError>

作用:把一个市场清单文件加载成对外可用的 Marketplace 对象。它不仅读 JSON,还会过滤坏插件、补齐本地插件信息。

数据流:进去的是 marketplace.json 的绝对路径;它调用 load_raw_marketplace_manifest 读原始清单,再对每个插件调用 resolve_marketplace_plugin_entry,坏插件会被警告并跳过;它从本地插件 manifest 中取版本和关键词,最后调用 resolve_marketplace_interface 整理市场显示信息;出来的是 Marketplace。

调用关系:刷新精选插件缓存、列出市场、验证市场根目录都会用它。它是从“文件里的原始清单”变成“系统可展示市场”的主加工线。

调用图:调用 3 个内部函数(load_raw_marketplace_manifest, resolve_marketplace_interface, resolve_marketplace_plugin_entry);被 3 处调用(refresh_curated_plugin_cache, list_marketplaces_with_home, validate_marketplace_root);外部调用 3 个(new, clone, warn!)。

list_marketplaces_with_home344–368 ↗
fn list_marketplaces_with_home(
    additional_roots: &[AbsolutePathBuf],
    home_dir: Option<&Path>,
) -> Result<MarketplaceListOutcome, MarketplaceError>

作用:按指定的用户主目录和额外位置发现并加载所有市场。这个函数把“发现路径”和“读取市场”串起来,还会收集失败原因。

数据流:进去的是额外根目录列表和可选主目录;它先用 discover_marketplace_paths_from_roots 找到候选清单,再逐个调用 load_marketplace;成功的放进 marketplaces,失败的写警告并放进 errors;出来的是 MarketplaceListOutcome。

调用关系:list_marketplaces 调用它做实际工作。它像一个批处理器:路径发现交给 discover_marketplace_paths_from_roots,单个市场读取交给 load_marketplace。

调用图:调用 2 个内部函数(discover_marketplace_paths_from_roots, load_marketplace);被 1 处调用(list_marketplaces);外部调用 2 个(default, warn!)。

discover_marketplace_paths_from_roots370–407 ↗
fn discover_marketplace_paths_from_roots(
    additional_roots: &[AbsolutePathBuf],
    home_dir: Option<&Path>,
) -> Vec<AbsolutePathBuf>

作用:从用户主目录和一批额外路径里找出所有可能的市场清单文件,并去重。它支持给根目录、给清单文件、也支持从 Git 仓库根目录推断。

数据流:进去的是额外根目录列表和可选主目录;它先在主目录找默认市场,再逐个处理额外路径:如果路径本身是支持的清单就加入,如果目录里有清单就加入,否则尝试找 Git 仓库根目录再找清单;出来的是不重复的绝对清单路径列表。

调用关系:list_marketplaces_with_home 调用它作为第一步。它把各种用户可能给出的路径形式整理成统一的 marketplace.json 路径。

调用图:调用 3 个内部函数(find_marketplace_manifest_path, supported_marketplace_manifest_path, try_from);被 1 处调用(list_marketplaces_with_home);外部调用 2 个(new, get_git_repo_root)。

load_raw_marketplace_manifest409–425 ↗
fn load_raw_marketplace_manifest(
    path: &AbsolutePathBuf,
) -> Result<RawMarketplaceManifest, MarketplaceError>

作用:读取并解析 marketplace.json 的原始内容。它只负责把文件变成 RawMarketplaceManifest,不做太多业务判断。

数据流:进去的是清单绝对路径;它用 read_to_string 读文件,文件不存在会变成 MarketplaceNotFound,其他读取失败会包装成 Io;读到字符串后用 serde_json 解析 JSON,格式错就返回 InvalidMarketplaceFile;出来的是原始清单结构。

调用关系:find_marketplace_plugin 和 load_marketplace 都从它开始。它是所有市场信息进入程序的文件读取入口。

调用图:调用 1 个内部函数(as_path);被 2 处调用(find_marketplace_plugin, load_marketplace);外部调用 2 个(read_to_string, from_str)。

resolve_marketplace_plugin_entry427–466 ↗
fn resolve_marketplace_plugin_entry(
    marketplace_path: &AbsolutePathBuf,
    marketplace_name: &str,
    plugin: RawMarketplaceManifestPlugin,
) -> Result<Option<ResolvedMarketplacePlugin>, Market

作用:把清单里一个原始插件条目,整理成系统真正能理解的插件条目。它会处理来源、插件 ID、策略、界面分类和本地 manifest。

数据流:进去的是市场路径、市场名和一个原始插件条目;它先用 resolve_supported_plugin_source 判断来源是否支持并规范化,本地插件会调用 load_plugin_manifest 读取插件说明书,然后用 plugin_interface_with_marketplace_category 合并分类;最后创建 PluginId,并组装 ResolvedMarketplacePlugin;如果来源不支持则返回 None。

调用关系:find_marketplace_plugin 和 load_marketplace 都会对命中的或遍历到的插件调用它。它把来源解析交给 resolve_supported_plugin_source,把界面分类合并交给 plugin_interface_with_marketplace_category。

调用图:调用 4 个内部函数(load_plugin_manifest, plugin_interface_with_marketplace_category, resolve_supported_plugin_source, new);被 2 处调用(find_marketplace_plugin, load_marketplace)。

resolve_supported_plugin_source468–495 ↗
fn resolve_supported_plugin_source(
    marketplace_path: &AbsolutePathBuf,
    plugin_name: &str,
    source: RawMarketplaceManifestPluginSource,
) -> Option<MarketplacePluginSource>

作用:判断插件来源是不是系统支持的,并把可支持的来源解析出来。遇到不支持或解析失败的来源,它不会让整个市场崩掉,而是跳过这个插件。

数据流:进去的是市场路径、插件名和原始来源;如果来源是不认识的 JSON,就写警告并返回 None;否则调用 resolve_plugin_source 做具体解析,成功返回 Some,失败也写警告并返回 None。

调用关系:resolve_marketplace_plugin_entry 会先用它过滤来源。它把“能不能处理这个来源”的温和失败逻辑包起来,避免一个坏插件拖垮整个市场。

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

resolve_plugin_source497–541 ↗
fn resolve_plugin_source(
    marketplace_path: &AbsolutePathBuf,
    source: RawMarketplaceManifestPluginSource,
) -> Result<MarketplacePluginSource, MarketplaceError>

作用:把插件来源转换成统一的内部表示:要么是本地路径,要么是 Git 仓库地址。它负责识别清单里几种不同写法。

数据流:进去的是市场路径和原始来源;本地路径会交给 resolve_local_plugin_source_path 变成绝对路径,Git 地址会交给 normalize_git_plugin_source_url 整理,子目录交给 normalize_remote_plugin_subdir 检查,ref 和 sha 交给 normalize_optional_git_selector 清理;出来的是 MarketplacePluginSource。

调用关系:resolve_supported_plugin_source 在确认来源类型可处理后调用它。它是插件来源解析的分岔路口,把不同来源分别交给专门的小函数。

调用图:调用 4 个内部函数(normalize_git_plugin_source_url, normalize_optional_git_selector, normalize_remote_plugin_subdir, resolve_local_plugin_source_path);被 1 处调用(resolve_supported_plugin_source);外部调用 1 个(unreachable!)。

resolve_local_plugin_source_path543–574 ↗
fn resolve_local_plugin_source_path(
    marketplace_path: &AbsolutePathBuf,
    path: &str,
) -> Result<AbsolutePathBuf, MarketplaceError>

作用:把本地插件的相对路径变成安全的绝对路径。它会防止清单用 ../ 之类的写法跑到市场目录外面。

数据流:进去的是市场清单路径和本地来源字符串;它要求路径必须以 ./ 开头,去掉 ./ 后不能为空,并且每一段都必须是普通目录名;然后调用 marketplace_root_dir 找市场根目录,再拼出插件绝对路径;失败时返回 InvalidMarketplaceFile。

调用关系:resolve_plugin_source 在遇到本地插件来源时调用它。它承担安全边界检查,确保本地插件只能来自市场根目录内部。

调用图:调用 2 个内部函数(marketplace_root_dir, to_path_buf);被 1 处调用(resolve_plugin_source);外部调用 1 个(new)。

normalize_remote_plugin_subdir576–599 ↗
fn normalize_remote_plugin_subdir(
    marketplace_path: &AbsolutePathBuf,
    path: &str,
) -> Result<String, MarketplaceError>

作用:整理 Git 仓库里插件所在的子目录,并检查它不会越界。它处理的是“仓库下载下来后,插件在里面哪个文件夹”。

数据流:进去的是市场路径和子目录字符串;它先去掉首尾空白,也允许开头的 ./,然后要求剩下内容不为空,且不能包含 .、.. 这类特殊路径段;出来的是干净的子目录字符串。

调用关系:resolve_plugin_source 在 Git 来源带 path 或 git-subdir 写法时调用它。它防止远程插件路径指向仓库根目录之外。

调用图:调用 1 个内部函数(to_path_buf);被 1 处调用(resolve_plugin_source);外部调用 1 个(new)。

normalize_git_plugin_source_url601–636 ↗
fn normalize_git_plugin_source_url(
    marketplace_path: &AbsolutePathBuf,
    url: &str,
) -> Result<String, MarketplaceError>

作用:把清单里的 Git 来源地址整理成系统能使用的形式,并拒绝明显不合法的地址。它支持完整网址、相对路径、本地 file、SSH、以及 GitHub 简写。

数据流:进去的是市场路径和 URL 字符串;它先 trim 去空白,空字符串报错;http/https 会交给 normalize_github_git_url 补 .git,./ 或 ../ 开头会交给 normalize_relative_git_plugin_source_url,本地 file、绝对路径、SSH 直接接受,像 owner/repo 的 GitHub 简写交给 normalize_github_shorthand_url;都不符合就返回错误。

调用关系:resolve_plugin_source 在解析 Git 插件来源时调用它。它把各种用户友好写法统一成后续下载代码能理解的地址。

调用图:调用 4 个内部函数(normalize_github_git_url, normalize_github_shorthand_url, normalize_relative_git_plugin_source_url, to_path_buf);被 1 处调用(resolve_plugin_source);外部调用 1 个(format!)。

normalize_relative_git_plugin_source_url638–659 ↗
fn normalize_relative_git_plugin_source_url(
    marketplace_path: &AbsolutePathBuf,
    url: &str,
) -> Result<String, MarketplaceError>

作用:把相对的 Git 来源路径转换成市场根目录内的本地路径字符串。它明确禁止 ..,防止路径跳出市场目录。

数据流:进去的是市场清单路径和相对 URL;它先用 marketplace_root_dir 找到市场根目录,再按 / 或 \ 分段拼路径,忽略空段和 .,遇到 .. 就报错;出来的是拼好的路径字符串。

调用关系:normalize_git_plugin_source_url 在发现 URL 是相对路径时调用它。它专门处理“这个 Git 来源就在市场目录旁边”的情况。

调用图:调用 2 个内部函数(marketplace_root_dir, to_path_buf);被 1 处调用(normalize_git_plugin_source_url)。

normalize_optional_git_selector661–667 ↗
fn normalize_optional_git_selector(value: &Option<String>) -> Option<String>

作用:清理可选的 Git 选择器,比如分支名、标签名或提交号。空白值会被当成没有填写。

数据流:进去的是 Option<String>;如果有值就去掉首尾空白,空了就丢弃,不空就转成新的 String;出来的是 Option<String>。

调用关系:resolve_plugin_source 在处理 Git 来源的 ref 和 sha 时调用它。它避免把空字符串误当成有效的分支或提交。

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

normalize_github_git_url669–675 ↗
fn normalize_github_git_url(url: &str) -> String

作用:把 GitHub 的 HTTPS 仓库地址补成更标准的 .git 结尾。其他地址保持不变。

数据流:进去的是一个 URL 字符串;如果它以 https://github.com/ 开头且没有 .git 结尾,就在末尾加 .git;否则原样复制;出来的是整理后的字符串。

调用关系:normalize_git_plugin_source_url 在处理 http 或 https 地址时调用它。它让 GitHub 地址更适合后续 Git 克隆使用。

调用图:被 1 处调用(normalize_git_plugin_source_url);外部调用 1 个(format!)。

normalize_github_shorthand_url677–689 ↗
fn normalize_github_shorthand_url(source: &str) -> Option<String>

作用:把 owner/repo 这种 GitHub 简写变成完整的 HTTPS Git 地址。比如 openai/example 会变成 https://github.com/openai/example.git。

数据流:进去的是来源字符串;它先用 looks_like_github_shorthand 判断是不是刚好两段合法名字,再取出 owner 和 repo,去掉 repo 末尾可能已有的 .git,最后拼成完整 GitHub 地址;不符合就返回 None。

调用关系:normalize_git_plugin_source_url 在其他 URL 形式都不匹配时会尝试调用它。它让清单作者可以写更短的 GitHub 地址。

调用图:调用 1 个内部函数(looks_like_github_shorthand);被 1 处调用(normalize_git_plugin_source_url);外部调用 1 个(format!)。

looks_like_github_shorthand691–699 ↗
fn looks_like_github_shorthand(source: &str) -> bool

作用:判断一个字符串看起来是不是 GitHub 的 owner/repo 简写。它只接受两段,中间一个斜杠,没有多余段。

数据流:进去的是字符串;它按 / 分成 owner、repo 和可能的 extra,要求 owner 和 repo 都通过 is_github_shorthand_segment 检查,并且没有第三段;出来的是 true 或 false。

调用关系:normalize_github_shorthand_url 先用它做外形检查。它负责挡掉不像 GitHub 简写的字符串,避免误转换。

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

is_github_shorthand_segment701–706 ↗
fn is_github_shorthand_segment(segment: &str) -> bool

作用:检查 GitHub 简写里的某一段名字是否合法。它要求非空,并且只包含常见的字母、数字、横线、下划线和点。

数据流:进去的是 owner 或 repo 这一小段字符串;它逐个字符检查是否是 ASCII 字母数字,或 -、_、.;全部合格就返回 true,否则 false。

调用关系:looks_like_github_shorthand 用它分别检查 owner 和 repo。它是 GitHub 简写判断里最小的字符级校验。

plugin_interface_with_marketplace_category708–719 ↗
fn plugin_interface_with_marketplace_category(
    mut interface: Option<PluginManifestInterface>,
    category: Option<String>,
) -> Option<PluginManifestInterface>

作用:把市场清单里的分类合并进插件界面信息里,并且市场分类优先。这样市场可以覆盖插件自己 manifest 里的分类。

数据流:进去的是可选的插件界面信息和可选分类;如果市场给了分类,它会确保 interface 存在,然后把 category 设置成市场分类;如果市场没给分类,就原样返回原来的 interface。

调用关系:resolve_marketplace_plugin_entry 会在整理插件时调用它,read_plugin_detail_for_marketplace_plugin 也会用它。它解决“插件自己说一个分类,市场又给一个分类时听谁的”的问题。

调用图:被 2 处调用(read_plugin_detail_for_marketplace_plugin, resolve_marketplace_plugin_entry)。

marketplace_root_dir722–735 ↗
fn marketplace_root_dir(
    marketplace_path: &AbsolutePathBuf,
) -> Result<AbsolutePathBuf, MarketplaceError>

作用:从 marketplace.json 的路径反推出市场根目录。因为清单文件可能放在几个固定子目录下,很多相对路径都要先知道根目录才能算。

数据流:进去的是市场清单绝对路径;它遍历支持的清单相对位置,逐个调用 marketplace_root_from_layout 尝试反推根目录,成功后转成 AbsolutePathBuf;全部失败就调用 invalid_marketplace_layout_error 返回布局错误。

调用关系:resolve_local_plugin_source_path 和 normalize_relative_git_plugin_source_url 会用它把相对路径变成根目录内路径,run_list 也会用它。它是所有“相对市场根目录”计算的基础。

调用图:调用 4 个内部函数(invalid_marketplace_layout_error, marketplace_root_from_layout, as_path, try_from);被 3 处调用(run_list, normalize_relative_git_plugin_source_url, resolve_local_plugin_source_path)。

resolve_marketplace_interface806–817 ↗
fn resolve_marketplace_interface(
    interface: Option<RawMarketplaceManifestInterface>,
) -> Option<MarketplaceInterface>

作用:把原始市场界面信息整理成对外可用的 MarketplaceInterface。当前它只保留有实际内容的显示名。

数据流:进去的是可选的 RawMarketplaceManifestInterface;没有 interface 就返回 None,有 interface 但 display_name 也是空的也返回 None;如果有 display_name,就包装成 MarketplaceInterface 返回。

调用关系:load_marketplace 在组装最终 Marketplace 时调用它。它负责把 JSON 里的界面小配置清理成更干净的内部结构。

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

core-plugins/src/loader.rs源码 ↗
orchestrationstartup, config load, cache refresh, plugin inspection

插件像一个个外接配件,但每个配件里可能有技能说明、MCP 服务器配置、应用连接信息、钩子配置等不同文件。MCP 可以理解成“让程序调用外部工具的一套协议”;钩子就是“某些时机自动触发的动作”。这个文件负责把这些散落的信息找出来、读出来、过滤掉不该用的部分,并把错误变成警告或加载失败信息。它还处理插件缓存:精选市场插件会按版本刷新,非精选市场插件可能来自本地目录或 Git 仓库。整体上,它像仓库管理员:先看清单,再找货架,再验货,最后把可用能力登记给主程序。

函数细节46
log_plugin_load_errors83–93 ↗
fn log_plugin_load_errors(plugins: &[LoadedPlugin<McpServerConfig>])

作用:把已经加载过的插件里出现的错误写到日志里,方便启动时排查哪个插件坏了。它不修复问题,只负责把问题说清楚。

数据流:输入是一组已加载插件 → 它逐个查看有没有 error 字段 → 对有错误的插件输出警告日志,不改变插件内容。

调用关系:在插件重新加载流程中被上层调用;它站在加载结束之后,专门负责把 load_plugin 等步骤留下的失败原因展示出来。

调用图:被 1 处调用(plugins_for_config_with_force_reload);外部调用 2 个(iter, warn!)。

load_plugins_from_layer_stack110–129 ↗
async fn load_plugins_from_layer_stack(
    config_layer_stack: &ConfigLayerStack,
    extra_plugins: HashMap<String, PluginConfig>,
    store: &PluginStore,
    restriction_product: Option<Product>,

作用:按当前配置一次性加载所有插件的完整能力,包括技能、MCP 工具服务器、应用和钩子。它是外部最常用的插件加载入口之一。

数据流:输入是配置层、额外插件配置、插件仓库、产品限制和冲突偏好 → 先从配置层算出技能启停规则 → 再交给更底层的加载函数 → 输出已加载插件列表。

调用关系:它被插件配置加载、强制重载和测试流程使用;自己不逐个读文件,而是把范围设成“加载全部能力”后交给 load_plugins_from_layer_stack_with_scope。

调用图:调用 2 个内部函数(load_plugins_from_layer_stack_with_scope, skill_config_rules_from_stack);被 3 处调用(plugins_for_config_with_force_reload, plugins_for_layer_stack, load_plugins_ignores_project_config_files)。

load_plugins_from_layer_stack_with_scope131–167 ↗
async fn load_plugins_from_layer_stack_with_scope(
    config_layer_stack: &ConfigLayerStack,
    extra_plugins: HashMap<String, PluginConfig>,
    store: &PluginStore,
    prefer_remote_curated_confl

作用:按指定范围加载插件:可以加载全部能力,也可以只加载钩子。它负责把“要加载哪些插件”和“每个插件怎么加载”串起来。

数据流:输入是配置、额外插件、插件仓库、冲突偏好和加载范围 → 合并配置插件与远端已安装插件,排序,逐个调用 load_plugin → 输出加载结果列表,并记录重复的 MCP 服务器名警告。

调用关系:load_plugins_from_layer_stack 和 load_plugin_hooks_from_layer_stack 都靠它干实际编排;它再把单个插件的细活交给 load_plugin。

调用图:调用 3 个内部函数(configured_plugins_from_stack, load_plugin, merge_configured_plugins_with_remote_installed);被 2 处调用(load_plugin_hooks_from_layer_stack, load_plugins_from_layer_stack);外部调用 3 个(new, with_capacity, warn!)。

load_plugin_hooks_from_layer_stack170–196 ↗
async fn load_plugin_hooks_from_layer_stack(
    config_layer_stack: &ConfigLayerStack,
    extra_plugins: HashMap<String, PluginConfig>,
    store: &PluginStore,
    prefer_remote_curated_conflicts:

作用:只加载已启用插件的钩子,不加载技能、MCP 服务器或应用。这样启动时需要钩子信息时,可以更快、更轻地完成。

数据流:输入是配置层、额外插件、插件仓库和冲突偏好 → 用“只加载钩子”的范围读取插件 → 从活跃插件里收集钩子来源和警告 → 输出 PluginHookLoadOutcome。

调用关系:被插件钩子汇总流程调用;它复用 load_plugins_from_layer_stack_with_scope,但刻意跳过大部分能力加载。

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

merge_configured_plugins_with_remote_installed198–244 ↗
fn merge_configured_plugins_with_remote_installed(
    mut configured_plugins: HashMap<String, PluginConfig>,
    extra_plugins: HashMap<String, PluginConfig>,
    store: &PluginStore,
    prefer_remo

作用:把用户本地配置的插件和远端同步来的已安装插件合成一份清单。它还处理“同一个精选插件本地版和远端版打架”时该保留谁。

数据流:输入是本地配置、额外插件配置、插件仓库和冲突偏好 → 找出已安装的本地精选插件,再逐个合并额外插件 → 输出最终插件配置表,可能移除或跳过冲突项。

调用关系:在 load_plugins_from_layer_stack_with_scope 开始逐个加载前使用;它会调用 installed_plugin_name_for_marketplace 来判断远端精选插件是否和本地精选插件对应。

调用图:调用 3 个内部函数(installed_plugin_name_for_marketplace, active_plugin_version, parse);被 1 处调用(load_plugins_from_layer_stack_with_scope);外部调用 2 个(new, is_openai_curated_marketplace_name)。

installed_plugin_name_for_marketplace246–257 ↗
fn installed_plugin_name_for_marketplace(
    plugin_key: &str,
    marketplace_name: &str,
    store: &PluginStore,
) -> Option<String>

作用:判断一个插件配置键是否属于指定市场,并且本地确实已经安装。符合条件时返回插件名。

数据流:输入是插件键、市场名和插件仓库 → 解析插件 ID,检查市场名和本地活跃安装目录 → 输出插件名,或在不匹配时输出空。

调用关系:只被 merge_configured_plugins_with_remote_installed 使用,用来识别远端精选插件和本地精选插件之间的冲突。

调用图:调用 2 个内部函数(active_plugin_root, parse);被 1 处调用(merge_configured_plugins_with_remote_installed)。

remote_installed_plugins_to_config259–292 ↗
fn remote_installed_plugins_to_config(
    plugins: &[RemoteInstalledPlugin],
    store: &PluginStore,
) -> HashMap<String, PluginConfig>

作用:把服务器告诉本机的“远端已安装插件列表”转换成普通插件配置。这样后续加载流程不用关心这些插件来自远端还是用户手写配置。

数据流:输入是远端插件列表和插件仓库 → 检查插件名是否合法、插件包是否已经在本地缓存中 → 输出插件键到 PluginConfig 的映射,坏数据会被跳过并记警告。

调用关系:被远端插件配置生成流程调用;它产出的配置之后会进入合并和加载流程。

调用图:被 1 处调用(remote_installed_plugin_configs);外部调用 1 个(iter)。

refresh_curated_plugin_cache294–383 ↗
fn refresh_curated_plugin_cache(
    codex_home: &Path,
    plugin_version: &str,
    configured_curated_plugin_ids: &[PluginId],
) -> Result<bool, String>

作用:刷新 OpenAI 精选市场插件的本地缓存,确保用户配置的精选插件对应当前程序版本里的插件包。它也会清理市场里已经不存在的旧插件。

数据流:输入是 codex_home、插件版本号和已配置的精选插件 ID → 找到精选市场文件,读取市场清单,比较本地缓存版本 → 必要时安装新版或卸载过期缓存 → 输出是否真的刷新过。

调用关系:这是精选插件缓存同步的主流程;它依赖 curated_marketplace_paths_for_cache_refresh 找市场文件,依赖 curated_plugin_cache_version 生成缓存版本,并通过插件仓库执行安装和卸载。

调用图:调用 5 个内部函数(curated_marketplace_paths_for_cache_refresh, curated_plugin_cache_version, load_marketplace, try_new, new);外部调用 4 个(new, new, to_path_buf, warn!)。

curated_marketplace_paths_for_cache_refresh385–407 ↗
fn curated_marketplace_paths_for_cache_refresh(
    codex_home: &Path,
) -> Result<Vec<AbsolutePathBuf>, String>

作用:找出刷新精选插件缓存时要读取的市场清单文件路径。它知道默认清单和可选 API 清单放在哪里。

数据流:输入是 codex_home → 拼出本地精选 marketplace.json 路径,并在存在时加入 api_marketplace.json → 输出绝对路径列表,路径不合法时返回错误。

调用关系:只在 refresh_curated_plugin_cache 中使用;它给后续 load_marketplace 提供要读取的文件位置。

调用图:调用 1 个内部函数(try_from);被 1 处调用(refresh_curated_plugin_cache);外部调用 2 个(join, vec!)。

curated_plugin_cache_version409–415 ↗
fn curated_plugin_cache_version(plugin_version: &str) -> String

作用:把插件版本号整理成适合缓存目录使用的版本字符串。完整 Git 提交号太长时,它只取前 8 位。

数据流:输入是版本字符串 → 判断它是否像 40 位完整 Git SHA → 如果是就截短,否则原样返回。

调用关系:刷新精选缓存和安装已解析插件时都会用它;它内部靠 is_full_git_sha 判断是否需要截短。

调用图:调用 1 个内部函数(is_full_git_sha);被 2 处调用(refresh_curated_plugin_cache, install_resolved_plugin)。

refresh_non_curated_plugin_cache417–426 ↗
fn refresh_non_curated_plugin_cache(
    codex_home: &Path,
    additional_roots: &[AbsolutePathBuf],
) -> Result<bool, String>

作用:刷新非精选市场插件缓存,但只有在插件版本变化时才重新安装。适合后台循环里温和地保持插件最新。

数据流:输入是 codex_home 和额外市场根目录 → 带着“版本变了才刷新”的模式调用通用刷新函数 → 输出是否有缓存被更新。

调用关系:被非精选插件缓存刷新循环调用;它只是给 refresh_non_curated_plugin_cache_with_mode 选择普通模式。

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

refresh_non_curated_plugin_cache_force_reinstall428–437 ↗
fn refresh_non_curated_plugin_cache_force_reinstall(
    codex_home: &Path,
    additional_roots: &[AbsolutePathBuf],
) -> Result<bool, String>

作用:强制重装非精选市场插件缓存,不管版本有没有变化。适合升级或修复缓存时使用。

数据流:输入是 codex_home 和额外市场根目录 → 带着“强制重装”的模式调用通用刷新函数 → 输出是否有缓存被更新。

调用关系:被刷新循环和市场升级流程调用;它和 refresh_non_curated_plugin_cache 共用同一个底层实现,只是模式不同。

调用图:调用 1 个内部函数(refresh_non_curated_plugin_cache_with_mode);被 2 处调用(run_non_curated_plugin_cache_refresh_loop, upgrade_configured_marketplaces_for_config)。

refresh_non_curated_plugin_cache_with_mode439–526 ↗
fn refresh_non_curated_plugin_cache_with_mode(
    codex_home: &Path,
    additional_roots: &[AbsolutePathBuf],
    mode: NonCuratedCacheRefreshMode,
) -> Result<bool, String>

作用:真正执行非精选插件缓存刷新的核心函数。它会从用户配置里找插件,再从发现到的市场里找到来源并安装到缓存。

数据流:输入是 codex_home、额外市场根目录和刷新模式 → 读取配置中的非精选插件,列出市场,定位每个插件来源,必要时把 Git 来源临时拉下来,再按版本安装 → 输出是否刷新过。

调用关系:普通刷新和强制刷新两个公开函数都调用它;它会用 configured_plugins_from_codex_home 读配置,用 materialize_marketplace_plugin_source 准备插件源码。

调用图:调用 7 个内部函数(configured_plugins_from_codex_home, materialize_marketplace_plugin_source, non_curated_plugin_ids_from_config_keys, list_marketplaces, try_new, plugin_version_for_source, new);被 2 处调用(refresh_non_curated_plugin_cache, refresh_non_curated_plugin_cache_force_reinstall);外部调用 4 个(new, to_path_buf, is_openai_curated_marketplace_name, warn!)。

configured_plugins_from_stack528–535 ↗
fn configured_plugins_from_stack(
    config_layer_stack: &ConfigLayerStack,
) -> HashMap<String, PluginConfig>

作用:从当前配置层里取出用户最终生效的插件配置。配置层可以理解成多份配置叠加后的结果。

数据流:输入是 ConfigLayerStack → 取出有效用户配置,如果没有就返回空表 → 有的话交给 configured_plugins_from_user_config_value 解析 plugins 部分。

调用关系:load_plugins_from_layer_stack_with_scope 在决定加载哪些插件前调用它;它把配置系统和插件加载系统接起来。

调用图:调用 2 个内部函数(effective_user_config, configured_plugins_from_user_config_value);被 1 处调用(load_plugins_from_layer_stack_with_scope);外部调用 1 个(new)。

is_full_git_sha537–539 ↗
fn is_full_git_sha(value: &str) -> bool

作用:判断一个字符串是不是 40 位十六进制 Git 提交号。Git 提交号可以理解成代码版本的身份证。

数据流:输入是字符串 → 检查长度是否为 40,且每个字符都是十六进制字符 → 输出 true 或 false。

调用关系:curated_plugin_cache_version 用它决定是否把版本号截短。

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

configured_plugins_from_user_config_value541–554 ↗
fn configured_plugins_from_user_config_value(
    user_config: &toml::Value,
) -> HashMap<String, PluginConfig>

作用:从用户配置内容里解析 plugins 这一段。解析失败时不会让程序崩掉,而是记录警告并当作没有插件配置。

数据流:输入是 TOML 配置值 → 找 plugins 字段并尝试转换成插件配置表 → 成功输出配置表,缺失或失败则输出空表。

调用关系:configured_plugins_from_stack 和 configured_plugins_from_codex_home 都调用它;它是配置文本变成插件配置对象的共同入口。

调用图:被 2 处调用(configured_plugins_from_codex_home, configured_plugins_from_stack);外部调用 3 个(new, get, warn!)。

configured_plugins_from_codex_home556–588 ↗
fn configured_plugins_from_codex_home(
    codex_home: &Path,
    read_error_message: &str,
    parse_error_message: &str,
) -> HashMap<String, PluginConfig>

作用:直接从 codex_home 下的 config.toml 读取插件配置。它用于不经过完整配置层、只需要本地用户配置的场景。

数据流:输入是 codex_home 和两段错误提示文字 → 读取 config.toml,解析成 TOML,再提取 plugins → 输出插件配置表;文件不存在、读取失败或解析失败时返回空表并可能记警告。

调用关系:精选插件 ID 提取和非精选缓存刷新都会调用它;它把磁盘上的配置文件交给 configured_plugins_from_user_config_value 继续处理。

调用图:调用 1 个内部函数(configured_plugins_from_user_config_value);被 2 处调用(configured_curated_plugin_ids_from_codex_home, refresh_non_curated_plugin_cache_with_mode);外部调用 4 个(new, join, read_to_string, warn!)。

configured_plugin_ids590–608 ↗
fn configured_plugin_ids(
    configured_plugins: HashMap<String, PluginConfig>,
    invalid_plugin_key_message: &str,
) -> Vec<PluginId>

作用:把插件配置表里的字符串键转换成结构化的 PluginId。无效的插件键会被跳过并写警告。

数据流:输入是插件配置表和无效键提示语 → 逐个解析配置键 → 输出成功解析出的 PluginId 列表。

调用关系:curated_plugin_ids_from_config_keys 和 non_curated_plugin_ids_from_config_keys 都用它做基础解析,然后再按市场类型筛选。

调用图:被 2 处调用(curated_plugin_ids_from_config_keys, non_curated_plugin_ids_from_config_keys)。

curated_plugin_ids_from_config_keys610–622 ↗
fn curated_plugin_ids_from_config_keys(
    configured_plugins: HashMap<String, PluginConfig>,
) -> Vec<PluginId>

作用:从配置键里筛出 OpenAI 精选市场插件的 ID,并排好序。这样刷新精选缓存时处理顺序稳定。

数据流:输入是插件配置表 → 先解析成 PluginId → 只保留精选市场的插件 → 按插件键排序后输出列表。

调用关系:configured_curated_plugin_ids_from_codex_home 调用它;它建立在 configured_plugin_ids 的通用解析结果之上。

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

non_curated_plugin_ids_from_config_keys624–636 ↗
fn non_curated_plugin_ids_from_config_keys(
    configured_plugins: HashMap<String, PluginConfig>,
) -> Vec<PluginId>

作用:从配置键里筛出非精选市场插件的 ID,并排好序。它用于刷新第三方或自定义市场插件缓存。

数据流:输入是插件配置表 → 先解析成 PluginId → 去掉精选市场插件 → 按插件键排序后输出列表。

调用关系:refresh_non_curated_plugin_cache_with_mode 调用它;它和精选筛选函数是同一套解析逻辑的两个分支。

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

configured_curated_plugin_ids_from_codex_home638–644 ↗
fn configured_curated_plugin_ids_from_codex_home(codex_home: &Path) -> Vec<PluginId>

作用:从本地 config.toml 里直接拿到已配置的精选插件 ID。它是刷新精选插件缓存前的准备工具。

数据流:输入是 codex_home → 读取用户配置中的插件表 → 筛选精选市场插件 ID → 输出排序后的 PluginId 列表。

调用关系:它把 configured_plugins_from_codex_home 和 curated_plugin_ids_from_config_keys 串起来,供缓存同步前使用。

调用图:调用 2 个内部函数(configured_plugins_from_codex_home, curated_plugin_ids_from_config_keys)。

load_plugin646–759 ↗
async fn load_plugin(
    config_name: String,
    plugin: &PluginConfig,
    store: &PluginStore,
    scope: &PluginLoadScope<'_>,
) -> LoadedPlugin<McpServerConfig>

作用:加载单个插件,把它的各种能力读成一个 LoadedPlugin。它负责处理禁用、未安装、配置损坏、manifest 缺失等常见情况。

数据流:输入是配置名、插件配置、插件仓库和加载范围 → 解析插件 ID,定位安装目录,读取 plugin.json,再按范围加载技能、MCP 服务器、应用和钩子 → 输出 LoadedPlugin,里面可能带错误信息。

调用关系:load_plugins_from_layer_stack_with_scope 逐个插件调用它;它再分派给 load_plugin_skills、load_mcp_servers_from_file、load_plugin_apps、load_plugin_hooks 等专门函数。

调用图:调用 10 个内部函数(apply_plugin_mcp_server_policy, load_mcp_servers_from_file, load_plugin_apps, load_plugin_hooks, load_plugin_skills, plugin_mcp_config_paths, plugin_skill_roots, load_plugin_manifest, plugin_data_root, parse);被 1 处调用(load_plugins_from_layer_stack_with_scope);外部调用 4 个(new, new, new, warn!)。

apply_plugin_mcp_server_policy761–778 ↗
fn apply_plugin_mcp_server_policy(config: &mut McpServerConfig, policy: &PluginMcpServerConfig)

作用:把用户配置里的 MCP 服务器开关和工具审批规则覆盖到插件声明的服务器配置上。简单说,就是让用户的偏好生效。

数据流:输入是可修改的 MCP 服务器配置和用户策略 → 覆盖启用状态、默认审批模式、允许或禁用的工具,以及单个工具的审批设置 → 输出通过原地修改后的配置。

调用关系:load_plugin 在读到插件 MCP 服务器后调用它;它夹在“插件声明的默认配置”和“最终运行配置”之间。

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

ResolvedPluginSkills::has_enabled_skills788–794 ↗
fn has_enabled_skills(&self) -> bool

作用:判断插件是否还有可用技能。即使加载技能时出过错,也会认为需要显示“有技能相关内容”,避免把问题悄悄藏掉。

数据流:输入是已解析技能结果自身 → 如果曾有错误直接返回 true,否则检查是否存在未被禁用的技能 → 输出布尔值。

调用关系:load_plugin 在加载技能后用它设置 has_enabled_skills;它帮助上层知道插件是否应该被当成有技能能力。

load_plugin_skills797–828 ↗
async fn load_plugin_skills(
    plugin_root: &AbsolutePathBuf,
    plugin_id: &PluginId,
    manifest_paths: &PluginManifestPaths,
    restriction_product: Option<Product>,
    skill_config_rules: &S

作用:读取一个插件里的技能文件,并按当前产品和用户规则过滤。技能可以理解成插件提供给助手的“做事说明书”。

数据流:输入是插件根目录、插件 ID、manifest 路径设置、产品限制和技能配置规则 → 找技能目录,加载技能元数据,过滤不适用于当前产品的技能,再算出被禁用的技能路径 → 输出 ResolvedPluginSkills。

调用关系:load_plugin 和市场插件详情读取流程会调用它;它依赖 plugin_skill_roots 找目录,并把真正读技能的工作交给 load_skills_from_roots。

调用图:调用 3 个内部函数(plugin_skill_roots, resolve_disabled_skill_paths, load_skills_from_roots);被 2 处调用(load_plugin, read_plugin_detail_for_marketplace_plugin)。

plugin_skill_roots830–841 ↗
fn plugin_skill_roots(
    plugin_root: &AbsolutePathBuf,
    manifest_paths: &PluginManifestPaths,
) -> Vec<AbsolutePathBuf>

作用:找出一个插件可能存放技能的目录。它会同时考虑默认 skills 文件夹和 manifest 里明确写的路径。

数据流:输入是插件根目录和 manifest 路径配置 → 先找默认 skills 目录,再追加 manifest 指定目录,排序去重 → 输出技能根目录列表。

调用关系:load_plugin、load_plugin_skills 和遥测信息生成都会用它;它底层会调用 default_skill_roots。

调用图:调用 1 个内部函数(default_skill_roots);被 3 处调用(load_plugin, load_plugin_skills, plugin_telemetry_metadata_from_root)。

default_skill_roots843–850 ↗
fn default_skill_roots(plugin_root: &AbsolutePathBuf) -> Vec<AbsolutePathBuf>

作用:检查插件根目录下有没有默认的 skills 文件夹。有就把它当作技能目录,没有就返回空。

数据流:输入是插件根目录 → 拼出 root/skills 并检查是否为目录 → 输出一个包含该目录的列表或空列表。

调用关系:只被 plugin_skill_roots 调用;它提供“约定俗成的默认位置”。

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

plugin_mcp_config_paths852–860 ↗
fn plugin_mcp_config_paths(
    plugin_root: &Path,
    manifest_paths: &PluginManifestPaths,
) -> Vec<AbsolutePathBuf>

作用:找出插件的 MCP 配置文件路径。manifest 指定了就用指定路径,否则找默认 .mcp.json。

数据流:输入是插件根目录和 manifest 路径配置 → 如果 manifest 有 mcp_servers 路径就返回它 → 否则调用 default_mcp_config_paths 找默认文件。

调用关系:load_plugin、load_declared_plugin_mcp_servers 和遥测信息生成都会用它;它决定哪些 MCP 配置文件会被读取。

调用图:调用 1 个内部函数(default_mcp_config_paths);被 3 处调用(load_declared_plugin_mcp_servers, load_plugin, plugin_telemetry_metadata_from_root);外部调用 1 个(vec!)。

default_mcp_config_paths862–873 ↗
fn default_mcp_config_paths(plugin_root: &Path) -> Vec<AbsolutePathBuf>

作用:查找插件根目录下默认的 .mcp.json 文件。这个文件声明插件提供哪些 MCP 工具服务器。

数据流:输入是插件根目录 → 拼出 .mcp.json,确认它是文件并转成绝对路径 → 排序去重后输出路径列表。

调用关系:plugin_mcp_config_paths 在 manifest 没有显式路径时调用它。

调用图:调用 1 个内部函数(try_from);被 1 处调用(plugin_mcp_config_paths);外部调用 2 个(join, new)。

load_plugin_apps875–884 ↗
async fn load_plugin_apps(plugin_root: &Path) -> Vec<AppDeclaration>

作用:读取插件声明的应用连接信息。应用声明告诉系统插件和哪些外部应用或连接器有关。

数据流:输入是插件根目录 → 如果能读到 manifest,就按 manifest 或默认路径找应用配置;否则直接找默认 .app.json → 读取并解析后输出 AppDeclaration 列表。

调用关系:load_plugin、load_plugin_mcp_servers 和市场插件详情读取都会调用它;它把文件读取交给 load_apps_from_paths。

调用图:调用 4 个内部函数(default_app_config_paths, load_apps_from_paths, plugin_app_config_paths, load_plugin_manifest);被 3 处调用(load_plugin, load_plugin_mcp_servers, read_plugin_detail_for_marketplace_plugin)。

plugin_app_declarations_from_value886–894 ↗
fn plugin_app_declarations_from_value(value: &JsonValue) -> Vec<AppDeclaration>

作用:从一段 JSON 值里直接解析应用声明。它适合配置内容已经在内存里、不需要再读文件的场景。

数据流:输入是 JSON 值 → 尝试解析成 PluginAppFile,再转换成 AppDeclaration → 去掉重复 connector_id 后输出应用列表。

调用关系:被根据应用 ID 查询分类的流程调用;它复用 app_declarations_from_file 做实际转换。

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

plugin_app_config_paths896–904 ↗
fn plugin_app_config_paths(
    plugin_root: &Path,
    manifest_paths: &PluginManifestPaths,
) -> Vec<AbsolutePathBuf>

作用:确定插件应用配置文件在哪里。manifest 指定了就用指定文件,否则找默认 .app.json。

数据流:输入是插件根目录和 manifest 路径配置 → 优先返回 manifest 中的 apps 路径 → 没有则调用 default_app_config_paths。

调用关系:load_plugin_apps 和遥测信息生成都会调用它;它决定 load_apps_from_paths 接下来读哪些文件。

调用图:调用 1 个内部函数(default_app_config_paths);被 2 处调用(load_plugin_apps, plugin_telemetry_metadata_from_root);外部调用 1 个(vec!)。

default_app_config_paths906–917 ↗
fn default_app_config_paths(plugin_root: &Path) -> Vec<AbsolutePathBuf>

作用:查找插件根目录下默认的 .app.json 文件。这个文件保存插件的应用连接声明。

数据流:输入是插件根目录 → 拼出 .app.json,确认存在且是文件后转成绝对路径 → 排序去重后输出路径列表。

调用关系:plugin_app_config_paths 和 load_plugin_apps 在没有 manifest 指定路径时会用它。

调用图:调用 1 个内部函数(try_from);被 2 处调用(load_plugin_apps, plugin_app_config_paths);外部调用 2 个(join, new)。

load_plugin_hooks922–976 ↗
fn load_plugin_hooks(
    plugin_root: &AbsolutePathBuf,
    plugin_id: &PluginId,
    plugin_data_root: &AbsolutePathBuf,
    manifest_paths: &PluginManifestPaths,
) -> (Vec<PluginHookSource>, Vec<St

作用:读取插件自带的钩子配置。钩子可以让插件在特定时机插入动作,比如启动或执行前后。

数据流:输入是插件根目录、插件 ID、插件数据目录和 manifest 路径配置 → 根据 manifest 的 hooks 字段读取文件或内联配置;没有声明时尝试默认 hooks/hooks.json → 输出钩子来源列表和加载警告。

调用关系:load_plugin 和市场插件详情读取会调用它;读取单个钩子文件的工作交给 append_plugin_hook_file。

调用图:调用 3 个内部函数(append_plugin_hook_file, as_path, join);被 2 处调用(load_plugin, read_plugin_detail_for_marketplace_plugin);外部调用 5 个(new, find_plugin_manifest_path, format!, clone, clone)。

append_plugin_hook_file980–1027 ↗
fn append_plugin_hook_file(
    plugin_root: &AbsolutePathBuf,
    plugin_id: &PluginId,
    plugin_data_root: &AbsolutePathBuf,
    path: &AbsolutePathBuf,
    sources: &mut Vec<PluginHookSource>,

作用:读取并解析一个具体的钩子配置文件,然后把它加入钩子来源列表。读取或解析失败时,它把问题放进警告列表。

数据流:输入是插件路径信息、钩子文件路径、可追加的来源列表和警告列表 → 读 JSON,解析 HooksFile,跳过空钩子 → 成功时追加 PluginHookSource,失败时追加警告。

调用关系:只被 load_plugin_hooks 调用;它负责把“一个文件”变成运行时可报告、可使用的钩子来源。

调用图:调用 1 个内部函数(as_path);被 1 处调用(load_plugin_hooks);外部调用 4 个(format!, read_to_string, clone, clone)。

load_apps_from_paths1029–1052 ↗
async fn load_apps_from_paths(
    plugin_root: &Path,
    app_config_paths: Vec<AbsolutePathBuf>,
) -> Vec<AppDeclaration>

作用:从一组应用配置文件路径里读取应用声明。坏文件会被跳过,不会阻止其他文件继续加载。

数据流:输入是插件根目录和应用配置路径列表 → 逐个异步读取文件,解析 JSON,再转换为应用声明 → 输出合并后的 AppDeclaration 列表。

调用关系:load_plugin_apps 和遥测信息生成会调用它;它把每个解析成功的文件交给 app_declarations_from_file 转换。

调用图:调用 1 个内部函数(app_declarations_from_file);被 2 处调用(load_plugin_apps, plugin_telemetry_metadata_from_root);外部调用 3 个(new, read_to_string, warn!)。

app_declarations_from_file1054–1079 ↗
fn app_declarations_from_file(
    parsed: PluginAppFile,
    plugin_root: Option<&Path>,
) -> Vec<AppDeclaration>

作用:把已经解析好的应用配置变成程序统一使用的应用声明。它会丢掉缺少应用 ID 的条目。

数据流:输入是 PluginAppFile 和可选插件根目录 → 遍历 apps,把非空 id 转成 AppConnectorId,并清理分类字段 → 输出 AppDeclaration 列表;缺 id 时可能写警告。

调用关系:load_apps_from_paths 和 plugin_app_declarations_from_value 都调用它;它是应用配置格式到内部模型的转换点。

调用图:被 2 处调用(load_apps_from_paths, plugin_app_declarations_from_value)。

cleaned_app_category1081–1085 ↗
fn cleaned_app_category(category: Option<String>) -> Option<String>

作用:清理应用分类字符串,去掉前后空白,并把空字符串当成没有分类。这样后续看到的是干净数据。

数据流:输入是可选分类字符串 → 如果存在就 trim,若结果为空则丢弃 → 输出清理后的可选字符串。

调用关系:app_declarations_from_file 在生成每个应用声明时使用它。

plugin_telemetry_metadata_from_root1087–1128 ↗
async fn plugin_telemetry_metadata_from_root(
    plugin_id: &PluginId,
    plugin_root: &AbsolutePathBuf,
) -> PluginTelemetryMetadata

作用:从插件目录生成遥测元数据,也就是用于统计和事件上报的插件能力摘要。它尽量只收集名称、能力概况等信息。

数据流:输入是插件 ID 和插件根目录 → 读取 manifest,检查是否有技能,读取 MCP 服务器名和应用连接器 ID → 输出 PluginTelemetryMetadata;manifest 缺失时退回只含插件 ID 的信息。

调用关系:安装插件和查询已安装插件遥测信息时调用它;它复用技能路径、MCP 配置路径和应用配置路径等加载辅助函数。

调用图:调用 9 个内部函数(load_apps_from_paths, load_mcp_servers_from_file, plugin_app_config_paths, plugin_mcp_config_paths, plugin_skill_roots, load_plugin_manifest, from_plugin_id, as_key, as_path);被 2 处调用(installed_plugin_telemetry_metadata, install_resolved_plugin);外部调用 3 个(new, app_connector_ids_from_declarations, clone)。

load_plugin_mcp_servers1130–1147 ↗
async fn load_plugin_mcp_servers(
    plugin_root: &Path,
    auth_mode: Option<AuthMode>,
) -> HashMap<String, McpServerConfig>

作用:读取插件声明的 MCP 服务器,并根据登录/授权模式决定是否应用应用路由策略。路由策略会影响某些应用相关工具如何暴露。

数据流:输入是插件根目录和可选授权模式 → 先读取声明的 MCP 服务器;如果应用路由可用且有服务器,再加载应用声明并调整服务器配置 → 输出 MCP 服务器配置表。

调用关系:市场插件详情读取会调用它;它先用 load_declared_plugin_mcp_servers 读原始声明,再视情况交给 apply_app_mcp_routing_policy。

调用图:调用 4 个内部函数(apply_app_mcp_routing_policy, apps_route_available, load_declared_plugin_mcp_servers, load_plugin_apps);被 1 处调用(read_plugin_detail_for_marketplace_plugin)。

load_declared_plugin_mcp_servers1149–1163 ↗
async fn load_declared_plugin_mcp_servers(plugin_root: &Path) -> HashMap<String, McpServerConfig>

作用:只读取插件自己声明的 MCP 服务器,不应用运行时路由策略。它提供一份原始服务器清单。

数据流:输入是插件根目录 → 读取 manifest,找 MCP 配置路径,逐个解析服务器配置 → 输出名称到 McpServerConfig 的映射,重复名称保留先看到的。

调用关系:load_plugin_mcp_servers 调用它作为第一步;它依赖 load_mcp_servers_from_file 解析具体文件。

调用图:调用 3 个内部函数(load_mcp_servers_from_file, plugin_mcp_config_paths, load_plugin_manifest);被 1 处调用(load_plugin_mcp_servers);外部调用 1 个(new)。

installed_plugin_telemetry_metadata1165–1181 ↗
async fn installed_plugin_telemetry_metadata(
    codex_home: &Path,
    plugin_id: &PluginId,
) -> PluginTelemetryMetadata

作用:为一个已安装插件生成遥测信息。它会先从本地插件仓库找到当前活跃安装目录。

数据流:输入是 codex_home 和插件 ID → 打开插件仓库,定位活跃插件目录 → 找到则调用 plugin_telemetry_metadata_from_root,失败或未安装则返回只含插件 ID 的默认信息。

调用关系:插件启停事件和卸载流程会调用它;它把“插件 ID”转换成“插件目录上的能力摘要”。

调用图:调用 3 个内部函数(plugin_telemetry_metadata_from_root, try_new, from_plugin_id);被 2 处调用(emit_plugin_toggle_events, uninstall_plugin_id);外部调用 2 个(to_path_buf, warn!)。

load_mcp_servers_from_file1183–1213 ↗
async fn load_mcp_servers_from_file(
    plugin_root: &Path,
    mcp_config_path: &AbsolutePathBuf,
) -> PluginMcpDiscovery

作用:读取并解析一个 MCP 配置文件,得到插件声明的工具服务器列表。解析失败时返回空结果并写警告。

数据流:输入是插件根目录和 MCP 配置文件路径 → 异步读取文件内容,调用 MCP 配置解析器,记录每个服务器解析错误 → 输出 PluginMcpDiscovery。

调用关系:load_plugin、load_declared_plugin_mcp_servers 和遥测信息生成都会用它;它是 MCP 配置文件进入系统的实际入口。

调用图:调用 1 个内部函数(as_path);被 3 处调用(load_declared_plugin_mcp_servers, load_plugin, plugin_telemetry_metadata_from_root);外部调用 4 个(parse_plugin_mcp_config, default, read_to_string, warn!)。

materialize_marketplace_plugin_source1226–1279 ↗
fn materialize_marketplace_plugin_source(
    codex_home: &Path,
    source: &MarketplacePluginSource,
) -> Result<MaterializedMarketplacePluginSource, String>

作用:把市场里的插件来源变成本地可读取的目录。本地来源直接使用;Git 来源会先克隆到临时目录。

数据流:输入是 codex_home 和市场插件来源 → 如果是本地路径,直接包装返回;如果是 Git,创建暂存临时目录,克隆仓库并可选进入子路径 → 输出 MaterializedMarketplacePluginSource,临时目录会随对象保留。

调用关系:非精选插件缓存刷新时调用它;Git 克隆的细节交给 clone_git_plugin_source。

调用图:调用 2 个内部函数(clone_git_plugin_source, try_from);被 1 处调用(refresh_non_curated_plugin_cache_with_mode);外部调用 3 个(join, create_dir_all, new)。

clone_git_plugin_source1281–1322 ↗
fn clone_git_plugin_source(
    url: &str,
    ref_name: Option<&str>,
    sha: Option<&str>,
    sparse_checkout_path: Option<&str>,
    destination: &Path,
) -> Result<(), String>

作用:把 Git 仓库里的插件源码克隆到指定目录。它支持稀疏检出,也就是只拿仓库里的某个子目录。

数据流:输入是 Git 地址、可选分支或引用、可选提交号、可选子路径和目标目录 → 组合 git clone、sparse-checkout、checkout 命令 → 成功返回空结果,失败返回错误字符串。

调用关系:materialize_marketplace_plugin_source 在处理 Git 来源时调用它;实际执行外部命令的工作交给 run_git。

调用图:调用 1 个内部函数(run_git);被 1 处调用(materialize_marketplace_plugin_source);外部调用 1 个(to_string_lossy)。

run_git1324–1346 ↗
fn run_git(args: &[&str], cwd: Option<&Path>) -> Result<(), String>

作用:执行一条 git 命令,并把失败时的状态、标准输出和错误输出整理成可读错误。它还关闭终端交互提示,避免后台流程卡住等用户输入。

数据流:输入是 git 参数和可选工作目录 → 启动 git 进程,设置 GIT_TERMINAL_PROMPT=0,等待输出 → 成功返回 Ok,失败返回包含命令输出的错误文本。

调用关系:clone_git_plugin_source 用它执行 clone、sparse-checkout 和 checkout;它是这个文件里和外部 git 程序打交道的最底层函数。

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

core-plugins/src/manager.rs源码 ↗
orchestrationstartup、request handling、background refresh、cross-cutting

可以把这个文件想成“插件管家”。用户配置里写了哪些插件、市场里有哪些插件、远程账号装了哪些插件、本地缓存是不是过期,都由这里串起来。它一边读配置和插件市场,一边调用加载器把插件的技能、钩子、应用连接器和 MCP 服务准备好;还会根据登录方式隐藏或保留某些能力。它也负责安装、卸载、升级市场、同步 OpenAI 官方插件仓库,以及后台刷新远程插件目录。为了不反复做重活,它用了缓存和后台任务;为了避免两个任务同时改同一份状态,它用读写锁和信号量做排队。这个文件本身不实现每种插件能力的细节,而是把市场、存储、远程服务、配置和加载器这些零件按正确顺序接起来。

函数细节69
PluginsConfigInput::new102–114 ↗
fn new(
        config_layer_stack: ConfigLayerStack,
        plugins_enabled: bool,
        remote_plugin_enabled: bool,
        chatgpt_base_url: String,
    ) -> Self

作用:创建一份插件运行所需的配置输入,里面包括配置层、插件总开关、远程插件开关和服务地址。

数据流:进去的是配置层栈、两个开关和 ChatGPT 服务地址;函数把它们原样装进一个 PluginsConfigInput;出来的是后续所有插件管理流程都能使用的配置包。

调用关系:它通常由配置加载流程调用,用来把零散设置整理成插件管家能看懂的一份输入。

调用图:被 2 处调用(load_plugins_config, plugins_config_input)。

remote_plugin_service_config201–205 ↗
fn remote_plugin_service_config(config: &PluginsConfigInput) -> RemotePluginServiceConfig

作用:把插件配置里和远程服务有关的部分抽出来,做成远程插件 API 能用的小配置。

数据流:进去的是完整 PluginsConfigInput;它取出 chatgpt_base_url;出来的是 RemotePluginServiceConfig,供远程安装状态、推荐插件、精选插件等请求使用。

调用关系:很多远程相关函数都会先调用它,相当于每次联系服务器前先写好收件地址。

调用图:被 9 处调用(build_and_cache_remote_installed_plugin_marketplaces, cached_global_remote_discoverable_plugins_for_config, featured_plugin_ids_for_config, install_plugin_with_remote_sync, maybe_start_global_remote_catalog_cache_refresh, maybe_start_plugin_startup_tasks_for_config, maybe_start_remote_installed_plugin_bundle_sync, maybe_start_remote_installed_plugins_cache_refresh_with_notify, uninstall_plugin_with_remote_sync)。

PluginCapabilitySummary::from313–327 ↗
fn from(value: PluginDetail) -> Self

作用:把详细插件信息压缩成一份能力摘要,方便给提示词或上层界面使用。

数据流:进去的是 PluginDetail;它检查是否还有未禁用的技能,清理描述文本,并带上 MCP 服务名和应用连接器 ID;出来的是 PluginCapabilitySummary。

调用关系:它是类型转换入口,会调用 prompt_safe_plugin_description 把描述变成更安全、适合放进提示里的文字。

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

PluginsManager::new370–372 ↗
fn new(codex_home: PathBuf) -> Self

作用:用默认限制创建插件管家,适合大多数 Codex 场景直接使用。

数据流:进去的是 codex_home 路径;它交给 new_with_options,并默认指定产品限制;出来的是初始化好的 PluginsManager。

调用关系:很多命令行和运行流程都会从这里拿到插件管家,再去列插件、安装插件或加载插件。

调用图:被 84 处调用(detect_migrations, import_plugins, run_list, run_upgrade, run_get, run_list, run_login, run_logout, load_plugin_command_context, deduplicates_configured_marketplace_plugin (+15 more));外部调用 1 个(new_with_options)。

PluginsManager::new_with_options374–409 ↗
fn new_with_options(
        codex_home: PathBuf,
        restriction_product: Option<Product>,
        auth_mode: Option<AuthMode>,
    ) -> Self

作用:创建可定制的插件管家,可以指定产品限制和当前登录模式。

数据流:进去的是主目录、可选产品限制、可选认证模式;它建立插件存储、各种缓存、后台刷新状态、锁和信号量;出来的是完整的 PluginsManager。

调用关系:new 会调用它;测试和特殊运行场景也会直接用它来控制认证模式或产品限制。

调用图:调用 1 个内部函数(new);被 14 处调用(featured_plugin_ids_for_config_defaults_query_param_to_codex, featured_plugin_ids_for_config_uses_restriction_product_query_param, load_plugins_from_config, plugin_auth_projection_hides_apps_without_chatgpt_auth, plugin_auth_projection_hides_dual_surface_mcp_with_agent_identity_apps_route, plugin_auth_projection_hides_matching_mcp_with_chatgpt_apps_route, plugin_auth_projection_keeps_non_conflicting_mcp_with_chatgpt_apps_route, plugin_auth_projection_preserves_duplicate_connector_declaration_names, plugin_auth_projection_reprojects_cached_plugins_when_auth_changes, plugins_manager_tracks_auth_mode (+4 more));外部调用 9 个(new, clone, new, new, default, default, default, default, default)。

PluginsManager::set_auth_mode411–421 ↗
fn set_auth_mode(&self, auth_mode: Option<AuthMode>) -> bool

作用:更新当前登录/认证模式,并告诉调用者这个值有没有真的变。

数据流:进去的是新的 AuthMode;它拿写锁比较旧值和新值,不同就替换;出来的是布尔值,true 表示发生变化。

调用关系:认证模式会影响应用和 MCP 能力的展示,所以更新后上层可以决定是否刷新插件能力。

PluginsManager::auth_mode423–428 ↗
fn auth_mode(&self) -> Option<AuthMode>

作用:读取当前认证模式。

数据流:进去不需要业务参数;它从读写锁里取出 AuthMode;出来的是可选认证模式。

调用关系:加载插件能力和读取插件详情时会调用它,用来决定哪些应用或 MCP 服务应该露出来。

调用图:被 2 处调用(read_plugin_detail_for_marketplace_plugin, resolve_loaded_plugins_for_auth)。

PluginsManager::set_analytics_events_client430–436 ↗
fn set_analytics_events_client(&self, analytics_events_client: AnalyticsEventsClient)

作用:设置 analytics 事件客户端,也就是用来记录安装、卸载等统计事件的对象。

数据流:进去的是 AnalyticsEventsClient;它写入管理器内部;出来没有返回值,但之后安装卸载插件时会发送统计事件。

调用关系:安装和卸载内部流程会读取这个客户端,存在时就上报插件事件。

PluginsManager::restriction_product_matches438–446 ↗
fn restriction_product_matches(&self, products: Option<&[Product]>) -> bool

作用:判断某个插件声明的产品限制是否允许当前产品使用。

数据流:进去的是插件允许的产品列表;没有限制就允许,空列表就拒绝,有列表则和管理器当前产品比较;出来是能不能使用的布尔值。

调用关系:列插件、读插件详情时会用它过滤不该出现在当前产品里的插件。

调用图:被 2 处调用(read_plugin_detail_for_marketplace_plugin, read_plugin_for_config)。

PluginsManager::plugins_for_config448–451 ↗
async fn plugins_for_config(&self, config: &PluginsConfigInput) -> PluginLoadOutcome

作用:按当前配置加载可用插件,是外部最常用的插件加载入口。

数据流:进去的是 PluginsConfigInput;它默认不强制刷新,转交给 plugins_for_config_with_force_reload;出来的是 PluginLoadOutcome,里面是可生效的插件能力。

调用关系:构建 MCP 配置和钩子配置时会调用它,是运行时拿插件能力的主门。

调用图:调用 1 个内部函数(plugins_for_config_with_force_reload);被 2 处调用(to_mcp_config_with_plugin_registrations, build_hooks_for_config)。

PluginsManager::plugins_for_config_with_force_reload458–495 ↗
async fn plugins_for_config_with_force_reload(
        &self,
        config: &PluginsConfigInput,
        force_reload: bool,
    ) -> PluginLoadOutcome

作用:真正执行插件加载,并支持绕过缓存强制重载。

数据流:进去的是配置和 force_reload;它先看插件开关,再用配置、技能规则、远程开关组成缓存键,能命中就直接返回;否则排队加载插件、记录错误、写缓存,再按认证模式整理能力;出来是 PluginLoadOutcome。

调用关系:plugins_for_config 调用它;它把实际加载交给 load_plugins_from_layer_stack,并用缓存相关小函数保护性能和一致性。

调用图:调用 10 个内部函数(load_plugins_from_layer_stack, log_plugin_load_errors, cache_loaded_plugins_if_current, cached_loaded_plugins, loaded_plugins_cache_generation, remote_installed_plugin_configs, resolve_loaded_plugins_for_auth, configured_plugins_from_stack, skill_config_rules_from_stack, default);被 1 处调用(plugins_for_config);外部调用 2 个(acquire, warn!)。

PluginsManager::resolve_loaded_plugins_for_auth497–509 ↗
fn resolve_loaded_plugins_for_auth(&self, mut plugins: Vec<LoadedPlugin>) -> PluginLoadOutcome

作用:根据当前认证模式调整已加载插件的应用和 MCP 服务。

数据流:进去的是一组 LoadedPlugin;它读取 auth_mode,对每个插件应用路由策略,可能隐藏或保留某些能力;出来的是 PluginLoadOutcome。

调用关系:插件从缓存取出或刚加载完成后都会经过它,确保同一份插件在不同登录状态下表现正确。

调用图:调用 3 个内部函数(apply_app_mcp_routing_policy, auth_mode, from_plugins);被 2 处调用(plugins_for_config_with_force_reload, plugins_for_layer_stack)。

PluginsManager::clear_cache511–518 ↗
fn clear_cache(&self)

作用:清掉插件加载缓存和精选插件缓存,让下次重新计算。

数据流:进去无业务参数;它先清 loaded plugins 缓存,再把 featured ids 缓存置空;出来没有返回值,但内部缓存变干净。

调用关系:市场升级、非官方缓存刷新等发现本地内容变了时会调用它。

调用图:调用 1 个内部函数(clear_loaded_plugins_cache);被 2 处调用(run_non_curated_plugin_cache_refresh_loop, upgrade_configured_marketplaces_for_config)。

PluginsManager::clear_loaded_plugins_cache533–540 ↗
fn clear_loaded_plugins_cache(&self)

作用:只清掉已加载插件缓存,并把代数加一,防止旧加载结果回写覆盖新状态。

数据流:进去无业务参数;它拿写锁,把 generation 加一并删除缓存条目;出来没有返回值。

调用关系:远程安装状态变化、本地缓存变化、clear_cache 都会调用它。

调用图:被 3 处调用(clear_cache, clear_remote_installed_plugins_cache, write_remote_installed_plugins_cache)。

PluginsManager::plugins_for_layer_stack543–560 ↗
async fn plugins_for_layer_stack(
        &self,
        config_layer_stack: &ConfigLayerStack,
        config: &PluginsConfigInput,
    ) -> PluginLoadOutcome

作用:按指定配置层加载插件,但不使用也不改动主缓存。

数据流:进去的是配置层栈和配置输入;插件关闭就返回空结果,否则直接加载插件,再按认证模式调整;出来是 PluginLoadOutcome。

调用关系:effective_skill_roots_for_layer_stack 调用它,适合临时查看某套配置下的插件能力。

调用图:调用 4 个内部函数(load_plugins_from_layer_stack, remote_installed_plugin_configs, resolve_loaded_plugins_for_auth, default);被 1 处调用(effective_skill_roots_for_layer_stack)。

PluginsManager::plugin_hooks_for_layer_stack563–578 ↗
async fn plugin_hooks_for_layer_stack(
        &self,
        config_layer_stack: &ConfigLayerStack,
        config: &PluginsConfigInput,
    ) -> PluginHookLoadOutcome

作用:只加载插件钩子,不加载技能、应用等其他能力。

数据流:进去的是配置层栈和配置输入;插件关闭则返回空钩子结果,否则读取远程安装配置并加载钩子;出来是 PluginHookLoadOutcome。

调用关系:它把工作交给 load_plugin_hooks_from_layer_stack,用于只关心事件钩子的场景。

调用图:调用 2 个内部函数(load_plugin_hooks_from_layer_stack, remote_installed_plugin_configs);外部调用 1 个(default)。

PluginsManager::effective_skill_roots_for_layer_stack581–589 ↗
async fn effective_skill_roots_for_layer_stack(
        &self,
        config_layer_stack: &ConfigLayerStack,
        config: &PluginsConfigInput,
    ) -> Vec<PluginSkillRoot>

作用:拿到某套配置下真正生效的插件技能目录。

数据流:进去的是配置层栈和配置输入;它先临时加载插件,再从加载结果里提取有效技能根目录;出来是 PluginSkillRoot 列表。

调用关系:它建立在 plugins_for_layer_stack 之上,专门服务需要技能路径的调用者。

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

PluginsManager::cached_loaded_plugins591–605 ↗
fn cached_loaded_plugins(&self, key: &PluginLoadCacheKey) -> Option<Vec<LoadedPlugin>>

作用:尝试从已加载插件缓存里取结果。

数据流:进去的是缓存键;它读缓存,如果键完全一致就克隆插件列表返回,否则返回空;不改动状态。

调用关系:plugins_for_config_with_force_reload 在加载前后都会查它,减少重复加载。

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

PluginsManager::loaded_plugins_cache_generation607–612 ↗
fn loaded_plugins_cache_generation(&self) -> u64

作用:读取当前加载缓存的代数,用来判断后面能不能安全写回缓存。

数据流:进去无业务参数;它读锁里取 generation;出来是一个数字。

调用关系:plugins_for_config_with_force_reload 在开始慢加载前记下它,加载完交给 cache_loaded_plugins_if_current 判断缓存是否已被别人清过。

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

PluginsManager::cache_loaded_plugins_if_current614–627 ↗
fn cache_loaded_plugins_if_current(
        &self,
        generation: u64,
        key: PluginLoadCacheKey,
        plugins: Vec<LoadedPlugin>,
    )

作用:只有缓存代数没变时,才把新加载的插件写入缓存。

数据流:进去的是旧代数、缓存键和插件列表;它拿写锁确认 generation 仍相同,相同就保存,不同就丢弃;出来没有返回值。

调用关系:plugins_for_config_with_force_reload 用它避免后台刷新期间旧结果覆盖新状态。

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

PluginsManager::remote_installed_plugin_configs629–639 ↗
fn remote_installed_plugin_configs(&self) -> HashMap<String, PluginConfig>

作用:把远程账号安装的插件缓存转换成本地加载器能读懂的插件配置。

数据流:进去无业务参数;它读取 remote_installed_plugins_cache,没有缓存就返回空表,有缓存就调用 remote_installed_plugins_to_config;出来是插件配置映射。

调用关系:加载插件和钩子时会把它作为额外配置,让远程安装状态也能参与本地能力加载。

调用图:调用 1 个内部函数(remote_installed_plugins_to_config);被 3 处调用(plugin_hooks_for_layer_stack, plugins_for_config_with_force_reload, plugins_for_layer_stack);外部调用 1 个(new)。

PluginsManager::build_remote_installed_plugin_marketplaces_from_cache641–656 ↗
fn build_remote_installed_plugin_marketplaces_from_cache(
        &self,
        visible_marketplaces: &[&str],
    ) -> Option<Vec<crate::remote::RemoteMarketplace>>

作用:把缓存里的远程已安装插件按市场分组,做成界面可展示的市场列表。

数据流:进去的是可见市场名列表;它读取远程安装缓存,没有缓存返回 None,有缓存就按市场分组;出来是远程市场列表。

调用关系:它不联网,只复用已有缓存,适合快速展示远程安装状态。

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

PluginsManager::cached_global_remote_discoverable_plugins_for_config658–681 ↗
fn cached_global_remote_discoverable_plugins_for_config(
        &self,
        config: &PluginsConfigInput,
        auth: Option<&CodexAuth>,
    ) -> Vec<crate::remote::RemoteDiscoverablePlugin>

作用:读取已经缓存的全球远程可发现插件列表。

数据流:进去的是配置和登录信息;它先检查插件、远程插件、后端登录、账号 ID 是否满足;满足后从本地远程目录缓存读数据;出来是插件列表,条件不满足则为空。

调用关系:它依赖 remote_plugin_service_config,并调用 remote 模块的缓存读取函数,不主动刷新网络数据。

调用图:调用 2 个内部函数(remote_plugin_service_config, cached_global_remote_discoverable_plugins);外部调用 2 个(as_path, new)。

PluginsManager::build_and_cache_remote_installed_plugin_marketplaces683–704 ↗
async fn build_and_cache_remote_installed_plugin_marketplaces(
        &self,
        config: &PluginsConfigInput,
        auth: Option<&CodexAuth>,
        visible_marketplaces: &[&str],
        on_e

作用:联网获取远程已安装插件,写入缓存,并返回按市场整理后的结果。

数据流:进去的是配置、登录信息、可见市场和变化回调;它请求远程安装列表,分组成市场,写入本地缓存;如果缓存变了就触发回调;出来是市场列表或错误。

调用关系:它用于需要立即拿到最新远程安装状态的路径,内部会调用 write_remote_installed_plugins_cache。

调用图:调用 4 个内部函数(write_remote_installed_plugins_cache, remote_plugin_service_config, fetch_remote_installed_plugins, group_remote_installed_plugins_by_marketplaces)。

PluginsManager::write_remote_installed_plugins_cache706–718 ↗
fn write_remote_installed_plugins_cache(&self, plugins: Vec<RemoteInstalledPlugin>) -> bool

作用:更新远程已安装插件缓存,并在内容变化时让已加载插件缓存失效。

数据流:进去的是远程插件列表;如果和旧缓存一样就返回 false,否则写入新列表、清掉 loaded plugins 缓存;出来是是否发生变化。

调用关系:远程安装刷新循环和手动构建远程市场都会调用它。

调用图:调用 1 个内部函数(clear_loaded_plugins_cache);被 2 处调用(build_and_cache_remote_installed_plugin_marketplaces, run_remote_installed_plugins_cache_refresh_loop)。

PluginsManager::clear_remote_installed_plugins_cache720–732 ↗
fn clear_remote_installed_plugins_cache(&self) -> bool

作用:清空远程已安装插件缓存,并让插件加载结果重新计算。

数据流:进去无业务参数;缓存本来为空就返回 false,否则置空并清 loaded plugins 缓存;出来是是否真的清掉了东西。

调用关系:远程刷新遇到未登录或认证不支持时会调用它。

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

PluginsManager::maybe_start_remote_plugin_caches_refresh734–754 ↗
fn maybe_start_remote_plugin_caches_refresh(
        self: &Arc<Self>,
        config: &PluginsConfigInput,
        auth: Option<CodexAuth>,
        on_effective_plugins_changed: Option<Arc<dyn Fn() +

作用:在后台启动远程插件相关缓存刷新,但只在需要时排任务。

数据流:进去的是配置、登录信息和变化回调;它安排远程已安装插件刷新,并异步刷新推荐插件模式;立刻返回,不等待网络完成。

调用关系:插件列表后台任务会调用它;它进一步调用 maybe_start_remote_installed_plugins_cache_refresh_with_notify。

调用图:调用 1 个内部函数(maybe_start_remote_installed_plugins_cache_refresh_with_notify);被 1 处调用(maybe_start_plugin_list_background_tasks_for_config);外部调用 3 个(clone, clone, spawn)。

PluginsManager::maybe_start_remote_installed_plugins_cache_refresh_after_mutation756–768 ↗
fn maybe_start_remote_installed_plugins_cache_refresh_after_mutation(
        self: &Arc<Self>,
        config: &PluginsConfigInput,
        auth: Option<CodexAuth>,
        on_effective_plugins_chang

作用:远程插件安装或卸载后,安排一次更严格的已安装缓存刷新。

数据流:进去的是配置、登录信息和回调;它要求刷新成功后通知,即使安装集合看起来没变也通知;出来没有同步结果。

调用关系:远程 bundle 同步或远程变更后使用它,确保 MCP 刷新顺序在远程安装缓存之后。

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

PluginsManager::maybe_start_remote_installed_plugins_cache_refresh_with_notify770–789 ↗
fn maybe_start_remote_installed_plugins_cache_refresh_with_notify(
        self: &Arc<Self>,
        config: &PluginsConfigInput,
        auth: Option<CodexAuth>,
        notify: RemoteInstalledPlugin

作用:统一安排远程已安装插件缓存刷新,并决定刷新后何时通知。

数据流:进去的是配置、登录信息、通知策略和回调;插件关闭就不做事,否则组装刷新请求并提交调度;出来没有返回值。

调用关系:普通远程缓存刷新和变更后刷新都会走它,再交给 schedule_remote_installed_plugins_cache_refresh。

调用图:调用 2 个内部函数(schedule_remote_installed_plugins_cache_refresh, remote_plugin_service_config);被 2 处调用(maybe_start_remote_installed_plugins_cache_refresh_after_mutation, maybe_start_remote_plugin_caches_refresh)。

PluginsManager::maybe_start_remote_installed_plugin_bundle_sync791–818 ↗
fn maybe_start_remote_installed_plugin_bundle_sync(
        self: &Arc<Self>,
        config: &PluginsConfigInput,
        auth: Option<CodexAuth>,
        on_effective_plugins_changed: Option<Arc<dyn

作用:启动远程已安装插件本地包的同步,让远程插件在本机也有可加载的文件。

数据流:进去的是配置、登录信息和变化回调;插件关闭就退出,否则注册本地缓存变化后的回调,并调用 remote 模块启动同步;出来没有立即结果。

调用关系:插件列表后台任务和启动任务会调用它;本地包变动后会反过来触发远程安装缓存刷新。

调用图:调用 1 个内部函数(remote_plugin_service_config);被 1 处调用(maybe_start_plugin_list_background_tasks_for_config);外部调用 5 个(clone, new, clone, clone, maybe_start_remote_installed_plugin_bundle_sync)。

PluginsManager::maybe_start_global_remote_catalog_cache_refresh820–833 ↗
fn maybe_start_global_remote_catalog_cache_refresh(
        self: &Arc<Self>,
        config: &PluginsConfigInput,
        auth: Option<CodexAuth>,
    )

作用:在后台刷新全球远程插件目录缓存。

数据流:进去的是配置和登录信息;只有插件与远程插件都开启才组装请求并提交;出来没有同步结果。

调用关系:插件列表后台任务可按选项调用它,然后由调度器启动刷新循环。

调用图:调用 2 个内部函数(schedule_global_remote_catalog_cache_refresh, remote_plugin_service_config);被 1 处调用(maybe_start_plugin_list_background_tasks_for_config)。

PluginsManager::maybe_start_plugin_list_background_tasks_for_config835–857 ↗
fn maybe_start_plugin_list_background_tasks_for_config(
        self: &Arc<Self>,
        config: &PluginsConfigInput,
        auth: Option<CodexAuth>,
        roots: &[AbsolutePathBuf],
        optio

作用:列插件时顺手启动一批后台维护任务,让列表和缓存逐步变新。

数据流:进去的是配置、登录信息、市场根目录、选项和回调;它依次安排非官方缓存刷新、可选远程目录刷新、远程安装缓存刷新和远程包同步;出来立即返回。

调用关系:这是插件列表流程的后台管家,负责把多个刷新任务串起来。

调用图:调用 4 个内部函数(maybe_start_global_remote_catalog_cache_refresh, maybe_start_non_curated_plugin_cache_refresh, maybe_start_remote_installed_plugin_bundle_sync, maybe_start_remote_plugin_caches_refresh)。

PluginsManager::install_plugin1011–1021 ↗
async fn install_plugin(
        &self,
        request: PluginInstallRequest,
    ) -> Result<PluginInstallOutcome, PluginInstallError>

作用:安装一个市场里的插件,只做本地安装流程。

数据流:进去的是插件名和市场路径;它先在市场里找到可安装插件,再交给 install_resolved_plugin;出来是安装结果或错误。

调用关系:它把查找交给 find_installable_marketplace_plugin,把真正复制和配置修改交给 install_resolved_plugin。

调用图:调用 2 个内部函数(install_resolved_plugin, find_installable_marketplace_plugin)。

PluginsManager::install_plugin_with_remote_sync1023–1044 ↗
async fn install_plugin_with_remote_sync(
        &self,
        config: &PluginsConfigInput,
        auth: Option<&CodexAuth>,
        request: PluginInstallRequest,
    ) -> Result<PluginInstallOutc

作用:安装插件前先通知远程后端启用该插件,然后再做本地安装。

数据流:进去的是配置、登录信息和安装请求;它解析市场插件,调用远程启用接口,成功后执行本地安装;出来是安装结果或错误。

调用关系:这是旧版远程同步安装路径,先调用 enable_remote_plugin,再复用 install_resolved_plugin。

调用图:调用 4 个内部函数(install_resolved_plugin, remote_plugin_service_config, find_installable_marketplace_plugin, enable_remote_plugin)。

PluginsManager::install_resolved_plugin1046–1104 ↗
async fn install_resolved_plugin(
        &self,
        resolved: ResolvedMarketplacePlugin,
    ) -> Result<PluginInstallOutcome, PluginInstallError>

作用:执行已经解析好的插件安装:准备源文件、写入插件存储、打开用户配置,并记录统计。

数据流:进去的是 ResolvedMarketplacePlugin;它判断官方精选插件版本,在线程池中物化源代码并安装到 store,然后把用户配置设为启用,最后可选上报安装事件;出来是插件 ID、版本、安装路径和认证策略。

调用关系:install_plugin 和 install_plugin_with_remote_sync 都会调用它,它是安装流程的核心落地点。

调用图:调用 3 个内部函数(curated_plugin_cache_version, plugin_telemetry_metadata_from_root, read_curated_plugins_sha);被 2 处调用(install_plugin, install_plugin_with_remote_sync);外部调用 6 个(as_path, clone, set_user_plugin_enabled, clone, is_openai_curated_marketplace_name, spawn_blocking)。

PluginsManager::uninstall_plugin1106–1109 ↗
async fn uninstall_plugin(&self, plugin_id: String) -> Result<(), PluginUninstallError>

作用:按字符串形式的插件 ID 卸载插件,只做本地卸载。

数据流:进去的是 plugin_id 字符串;它先解析成 PluginId,再交给 uninstall_plugin_id;出来是成功或卸载错误。

调用关系:它是本地卸载的外部入口,把实际工作交给 uninstall_plugin_id。

调用图:调用 2 个内部函数(uninstall_plugin_id, parse)。

PluginsManager::uninstall_plugin_with_remote_sync1111–1130 ↗
async fn uninstall_plugin_with_remote_sync(
        &self,
        config: &PluginsConfigInput,
        auth: Option<&CodexAuth>,
        plugin_id: String,
    ) -> Result<(), PluginUninstallError>

作用:卸载插件前先通知远程后端,再清理本地。

数据流:进去的是配置、登录信息和插件 ID 字符串;它解析 ID,调用远程卸载接口,成功后执行本地卸载;出来是成功或错误。

调用关系:这是旧版远程同步卸载路径,先调用 uninstall_remote_plugin,再复用 uninstall_plugin_id。

调用图:调用 4 个内部函数(uninstall_plugin_id, remote_plugin_service_config, uninstall_remote_plugin, parse)。

PluginsManager::uninstall_plugin_id1132–1159 ↗
async fn uninstall_plugin_id(&self, plugin_id: PluginId) -> Result<(), PluginUninstallError>

作用:真正执行本地插件卸载,并清理用户配置和统计记录。

数据流:进去的是 PluginId;它先准备卸载统计信息,在线程池里从 store 删除插件,再从用户配置清掉插件项,最后可选上报卸载事件;出来是成功或错误。

调用关系:uninstall_plugin 和 uninstall_plugin_with_remote_sync 都调用它。

调用图:调用 3 个内部函数(installed_plugin_telemetry_metadata, active_plugin_root, as_key);被 2 处调用(uninstall_plugin, uninstall_plugin_with_remote_sync);外部调用 5 个(as_path, clear_user_plugin, clone, clone, spawn_blocking)。

PluginsManager::list_marketplaces_for_config1161–1250 ↗
fn list_marketplaces_for_config(
        &self,
        config: &PluginsConfigInput,
        additional_roots: &[AbsolutePathBuf],
        include_openai_curated: bool,
    ) -> Result<ConfiguredMarke

作用:列出当前配置下可见的插件市场和插件,并标出哪些已安装、哪些已启用。

数据流:进去的是配置、额外市场根目录和是否包含官方精选市场;插件关闭返回空结果,否则收集配置状态和市场路径,读取市场文件,过滤重复插件和产品不匹配插件,补充版本与启用信息;出来是市场列表和读取错误列表。

调用关系:安装验证、查找插件市场、界面列市场等流程会调用它。

调用图:调用 3 个内部函数(configured_plugin_states, marketplace_roots, list_marketplaces);被 3 处调用(configured_marketplace_plugins, find_marketplace_for_plugin, verified_plugin_install_completed);外部调用 2 个(new, default)。

PluginsManager::discover_marketplaces_for_config1252–1266 ↗
fn discover_marketplaces_for_config(
        &self,
        config: &PluginsConfigInput,
        additional_roots: &[AbsolutePathBuf],
    ) -> Result<MarketplaceListOutcome, MarketplaceError>

作用:发现当前配置下有哪些市场文件或市场目录。

数据流:进去的是配置和额外根目录;插件关闭返回空结果,否则计算市场根目录并读取市场;出来是 MarketplaceListOutcome。

调用关系:它比 list_marketplaces_for_config 更轻,主要用于发现市场本身。

调用图:调用 2 个内部函数(marketplace_roots, list_marketplaces);外部调用 1 个(default)。

PluginsManager::read_plugin_for_config1268–1325 ↗
async fn read_plugin_for_config(
        &self,
        config: &PluginsConfigInput,
        request: &PluginReadRequest,
    ) -> Result<PluginReadOutcome, MarketplaceError>

作用:读取某个市场插件的完整详情。

数据流:进去的是配置和读插件请求;插件关闭会报错,否则找到市场插件,检查产品限制,判断安装和启用状态,再交给详情读取函数;出来是 PluginReadOutcome。

调用关系:它是外部读单个插件详情的入口,内部调用 read_plugin_detail_for_marketplace_plugin。

调用图:调用 5 个内部函数(configured_plugin_states, read_plugin_detail_for_marketplace_plugin, restriction_product_matches, find_marketplace_plugin, active_plugin_version)。

PluginsManager::read_plugin_detail_for_marketplace_plugin1328–1476 ↗
async fn read_plugin_detail_for_marketplace_plugin(
        &self,
        config: &PluginsConfigInput,
        marketplace_name: &str,
        plugin: ConfiguredMarketplacePlugin,
    ) -> Result<Plu

作用:把一个市场插件展开成详细信息,包括描述、技能、钩子、应用、MCP 服务等。

数据流:进去的是配置、市场名和市场插件摘要;它检查产品限制,构造插件 ID,必要时提示远程 Git 插件需先安装;否则确定源目录、读取 manifest、加载技能、钩子、应用和 MCP 服务,并按认证模式过滤;出来是 PluginDetail。

调用关系:read_plugin_for_config 调用它;它把具体读取工作分发给 loader、hooks、skills 和 app/MCP 相关函数。

调用图:调用 14 个内部函数(apply_app_mcp_routing_policy, load_plugin_apps, load_plugin_hooks, load_plugin_mcp_servers, load_plugin_skills, auth_mode, restriction_product_matches, remote_plugin_install_required_description, load_plugin_manifest, plugin_interface_with_marketplace_category (+4 more));被 1 处调用(read_plugin_for_config);外部调用 9 个(new, new, clone, new, plugin_hook_declarations, app_connector_ids_from_declarations, InvalidPlugin, matches!, spawn_blocking)。

PluginsManager::maybe_start_plugin_startup_tasks_for_config1478–1591 ↗
fn maybe_start_plugin_startup_tasks_for_config(
        self: &Arc<Self>,
        config: &PluginsConfigInput,
        auth_manager: Arc<AuthManager>,
        on_effective_plugins_changed: Option<Arc<

作用:应用启动时启动插件系统的后台维护任务。

数据流:进去的是配置、认证管理器和变化回调;插件开启时,它启动官方精选仓库同步、配置市场自动升级、远程安装缓存刷新、远程包同步、远程目录预热和精选插件预热;出来立即返回。

调用关系:这是启动阶段的总入口,会调用 start_curated_repo_sync,并派生多个后台线程或异步任务。

调用图:调用 3 个内部函数(start_curated_repo_sync, remote_plugin_service_config, fetch_and_cache_global_remote_plugin_catalog);外部调用 5 个(clone, clone, new, spawn, warn!)。

PluginsManager::upgrade_configured_marketplaces_for_config1593–1637 ↗
fn upgrade_configured_marketplaces_for_config(
        &self,
        config: &PluginsConfigInput,
        marketplace_name: Option<&str>,
    ) -> Result<ConfiguredMarketplaceUpgradeOutcome, String>

作用:升级配置中的 Git 插件市场,并刷新由这些市场安装出的插件缓存。

数据流:进去的是配置和可选市场名;它先确认指定市场确实是 Git 市场,再执行升级;如果有根目录升级,就强制重装/刷新非官方缓存,成功时清缓存,失败时记录错误;出来是升级结果或字符串错误。

调用关系:启动自动升级线程会调用它,也可被手动升级命令使用。

调用图:调用 4 个内部函数(refresh_non_curated_plugin_cache_force_reinstall, clear_cache, configured_git_marketplace_names, upgrade_configured_git_marketplaces);外部调用 2 个(as_path, format!)。

PluginsManager::maybe_start_non_curated_plugin_cache_refresh1639–1647 ↗
fn maybe_start_non_curated_plugin_cache_refresh(
        self: &Arc<Self>,
        roots: &[AbsolutePathBuf],
    )

作用:安排非官方插件缓存刷新。

数据流:进去的是市场根目录列表;它选择“版本变化才刷新”的模式,交给调度函数;出来没有同步结果。

调用关系:列插件后台任务会调用它,实际去重和开线程由 schedule_non_curated_plugin_cache_refresh 做。

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

PluginsManager::schedule_remote_installed_plugins_cache_refresh1649–1689 ↗
fn schedule_remote_installed_plugins_cache_refresh(
        self: &Arc<Self>,
        mut request: RemoteInstalledPluginsCacheRefreshRequest,
    )

作用:把远程已安装插件刷新请求放入队列,并保证同一时间只有一个刷新循环在跑。

数据流:进去的是刷新请求;它合并已有请求的通知策略和回调,记录最新请求,如果没有任务在跑就启动异步循环;出来没有返回值。

调用关系:maybe_start_remote_installed_plugins_cache_refresh_with_notify 调用它,后台工作由 run_remote_installed_plugins_cache_refresh_loop 完成。

调用图:被 1 处调用(maybe_start_remote_installed_plugins_cache_refresh_with_notify);外部调用 3 个(clone, matches!, spawn)。

PluginsManager::schedule_global_remote_catalog_cache_refresh1691–1716 ↗
fn schedule_global_remote_catalog_cache_refresh(
        self: &Arc<Self>,
        request: GlobalRemoteCatalogCacheRefreshRequest,
    )

作用:安排全球远程插件目录缓存刷新,并避免重复开多个刷新任务。

数据流:进去的是刷新请求;它保存请求,若当前没有 in_flight 任务就 spawn 一个刷新循环;出来没有返回值。

调用关系:maybe_start_global_remote_catalog_cache_refresh 调用它,实际联网由 run_global_remote_catalog_cache_refresh_loop 做。

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

PluginsManager::schedule_non_curated_plugin_cache_refresh1718–1780 ↗
fn schedule_non_curated_plugin_cache_refresh(
        self: &Arc<Self>,
        roots: &[AbsolutePathBuf],
        mode: NonCuratedCacheRefreshMode,
    )

作用:安排非官方插件缓存刷新,并合并重复请求。

数据流:进去的是根目录列表和刷新模式;它排序去重,空列表直接返回;然后根据当前状态判断是否重复、是否已有更强的强制刷新请求,必要时启动后台线程;出来没有返回值。

调用关系:maybe_start_non_curated_plugin_cache_refresh 调用它,后台线程运行 run_non_curated_plugin_cache_refresh_loop。

调用图:被 1 处调用(maybe_start_non_curated_plugin_cache_refresh);外部调用 7 个(clone, new, dedup, is_empty, sort_unstable, to_vec, warn!)。

PluginsManager::start_curated_repo_sync1782–1822 ↗
fn start_curated_repo_sync(self: &Arc<Self>)

作用:启动官方精选插件仓库同步,而且全进程只启动一次。

数据流:进去无业务参数;它用原子布尔值防重复,开线程同步仓库,成功后刷新精选插件缓存,缓存变化则清管理器缓存;失败会重置启动标记并记录警告。

调用关系:启动任务 maybe_start_plugin_startup_tasks_for_config 会调用它。

调用图:被 1 处调用(maybe_start_plugin_startup_tasks_for_config);外部调用 4 个(clone, clone, new, warn!)。

PluginsManager::run_remote_installed_plugins_cache_refresh_loop1824–1882 ↗
async fn run_remote_installed_plugins_cache_refresh_loop(self: Arc<Self>)

作用:后台循环处理远程已安装插件刷新请求。

数据流:进去的是 Arc<Self>;它不断取出最新请求,联网拉取远程安装列表,成功则写缓存并按策略通知,认证问题则清缓存,其他错误只记录;没有请求时标记空闲并退出。

调用关系:由 schedule_remote_installed_plugins_cache_refresh 启动,是远程安装缓存真正工作的后台循环。

调用图:调用 3 个内部函数(clear_remote_installed_plugins_cache, write_remote_installed_plugins_cache, fetch_remote_installed_plugins);外部调用 2 个(matches!, warn!)。

PluginsManager::run_global_remote_catalog_cache_refresh_loop1884–1920 ↗
async fn run_global_remote_catalog_cache_refresh_loop(self: Arc<Self>)

作用:后台循环刷新全球远程插件目录缓存。

数据流:进去的是 Arc<Self>;它不断取请求并调用远程目录获取与缓存函数,认证类错误忽略,其他错误记录;请求用完后退出并标记空闲。

调用关系:由 schedule_global_remote_catalog_cache_refresh 启动。

调用图:调用 1 个内部函数(fetch_and_cache_global_remote_plugin_catalog);外部调用 2 个(as_path, warn!)。

PluginsManager::run_non_curated_plugin_cache_refresh_loop1922–1979 ↗
fn run_non_curated_plugin_cache_refresh_loop(self: Arc<Self>)

作用:后台刷新非官方插件缓存,并根据是否有新请求决定是否继续跑。

数据流:进去的是 Arc<Self>;它读取当前请求,按模式执行普通刷新或强制重装刷新;成功且缓存变化就清缓存,失败也清缓存并警告;最后更新刷新状态,没有新请求则退出。

调用关系:由 schedule_non_curated_plugin_cache_refresh 开线程运行。

调用图:调用 3 个内部函数(refresh_non_curated_plugin_cache, refresh_non_curated_plugin_cache_force_reinstall, clear_cache);外部调用 2 个(as_path, warn!)。

PluginsManager::configured_plugin_states1981–2000 ↗
fn configured_plugin_states(
        &self,
        config: &PluginsConfigInput,
    ) -> (HashSet<String>, HashSet<String>)

作用:从用户配置和本地存储里算出哪些插件已安装、哪些插件已启用。

数据流:进去的是配置输入;它读取用户配置里的插件项,逐个检查 store 里是否安装,再筛出 enabled 为 true 的插件;出来是两个字符串集合。

调用关系:列市场和读插件详情前都会调用它,用来给插件打状态标记。

调用图:调用 1 个内部函数(configured_plugins_from_stack);被 2 处调用(list_marketplaces_for_config, read_plugin_for_config)。

PluginsManager::marketplace_roots2002–2041 ↗
fn marketplace_roots(
        &self,
        config: &PluginsConfigInput,
        additional_roots: &[AbsolutePathBuf],
        include_openai_curated: bool,
    ) -> Vec<AbsolutePathBuf>

作用:整理当前应该扫描的插件市场位置。

数据流:进去的是配置、额外根目录和是否包含官方精选市场;它合并额外根目录、配置里的市场根目录,以及按认证模式选择的官方精选市场路径;最后排序去重;出来是绝对路径列表。

调用关系:discover_marketplaces_for_config 和 list_marketplaces_for_config 都靠它决定去哪里找市场。

调用图:调用 4 个内部函数(installed_marketplace_roots_from_layer_stack, curated_plugins_api_marketplace_path, curated_plugins_repo_path, try_from);被 2 处调用(discover_marketplaces_for_config, list_marketplaces_for_config);外部调用 3 个(as_path, matches!, to_vec)。

remote_plugin_install_required_description2044–2070 ↗
fn remote_plugin_install_required_description(source: &MarketplacePluginSource) -> String

作用:为尚未安装的跨仓库插件生成一段说明,告诉用户需要先安装才能看详情。

数据流:进去的是插件来源;它把 Git URL、子路径、ref、sha 或本地路径拼成可读描述;出来是一段英文提示文字。

调用关系:read_plugin_detail_for_marketplace_plugin 遇到未安装的 Git 来源插件时会调用它。

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

PluginInstallError::join2091–2093 ↗
fn join(source: tokio::task::JoinError) -> Self

作用:把后台安装任务的 JoinError 包装成插件安装错误。

数据流:进去的是 tokio 任务 JoinError;它封装成 PluginInstallError::Join;出来是统一错误类型。

调用关系:install_resolved_plugin 在线程池任务失败连接时用它做错误转换。

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

PluginInstallError::is_invalid_request2095–2106 ↗
fn is_invalid_request(&self) -> bool

作用:判断安装失败是不是因为用户请求本身不合法。

数据流:进去的是安装错误;它匹配市场不存在、插件不存在、插件不可用、插件无效或存储无效等情况;出来是布尔值。

调用关系:上层可以用它把“用户传错了”和“系统内部失败”区分开。

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

PluginUninstallError::join2128–2130 ↗
fn join(source: tokio::task::JoinError) -> Self

作用:把后台卸载任务的 JoinError 包装成插件卸载错误。

数据流:进去的是 tokio 任务 JoinError;它封装成 PluginUninstallError::Join;出来是统一错误类型。

调用关系:uninstall_plugin_id 在线程池卸载失败连接时会用它。

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

PluginUninstallError::is_invalid_request2132–2134 ↗
fn is_invalid_request(&self) -> bool

作用:判断卸载失败是不是因为插件 ID 不合法。

数据流:进去的是卸载错误;它只检查是否为 InvalidPluginId;出来是布尔值。

调用关系:上层可用它给用户返回更明确的“插件 ID 写错了”。

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

configured_plugins_from_stack2137–2145 ↗
fn configured_plugins_from_stack(
    config_layer_stack: &ConfigLayerStack,
) -> HashMap<String, PluginConfig>

作用:从配置层栈里取出用户配置中的插件设置。

数据流:进去的是 ConfigLayerStack;它只读取 effective_user_config,没用户配置就返回空表,有则继续解析 plugins 字段;出来是插件配置映射。

调用关系:插件加载和状态计算都会调用它;它把解析细节交给 configured_plugins_from_user_config_value。

调用图:调用 2 个内部函数(effective_user_config, configured_plugins_from_user_config_value);被 2 处调用(configured_plugin_states, plugins_for_config_with_force_reload);外部调用 1 个(new)。

configured_plugins_from_user_config_value2147–2160 ↗
fn configured_plugins_from_user_config_value(
    user_config: &toml::Value,
) -> HashMap<String, PluginConfig>

作用:把用户配置文件里的 plugins 字段解析成程序可用的插件配置表。

数据流:进去的是 TOML 配置值;它取 plugins 字段并尝试转换成 HashMap,失败会记录警告并返回空表;出来是插件配置映射。

调用关系:configured_plugins_from_stack 调用它,是用户插件配置从文本变成结构化数据的最后一步。

调用图:被 1 处调用(configured_plugins_from_stack);外部调用 3 个(new, get, warn!)。

Marketplace 生命周期

这些文件涵盖从本地或 git 支持来源添加、跟踪、升级和移除插件 marketplace。

core-plugins/src/marketplace_add/metadata.rs源码 ↗
domain_logicmarketplace add flow

安装插件市场时,程序不能只把文件拷贝过去,还得记住它的“来历”:是哪个 Git 地址、哪个分支或标签、本地哪个目录,以及是否只取了仓库里的部分路径。这个文件就像给每个市场办一张“来源身份证”。安装完成后,它会把这张身份证写进用户的 config.toml 配置文件;下次再添加同一个来源时,它会先翻配置,看看有没有已经安装好的可用目录。它还会检查找到的目录是不是真的像一个合法市场,避免配置里写了但文件已经坏掉或不存在。文件里也包含生成 UTC 时间戳的小工具,用来记录“最后更新时间”。整体上,它把“来源信息”“配置文件记录”“已安装目录查找”这几件事连在一起,让添加市场的流程更稳,不会盲目重复下载或相信错误配置。

函数细节16
record_added_marketplace_entry32–53 ↗
fn record_added_marketplace_entry(
    codex_home: &Path,
    marketplace_name: &str,
    install_metadata: &MarketplaceInstallMetadata,
) -> Result<(), MarketplaceAddError>

作用:把刚添加的插件市场写进用户的 config.toml 配置文件。这样程序以后能知道这个市场从哪里来、什么时候添加或更新过。

数据流:输入用户目录、市场名字和安装来源信息 → 从来源信息里取出类型、地址、分支名和稀疏路径,再生成当前 UTC 时间 → 组装成一条配置更新记录并写入用户配置;成功时不返回额外内容,失败时返回带说明的添加错误。

调用关系:它在添加市场成功后由 add_marketplace_sync_with_cloner 调用,属于收尾登记步骤。它把取来源字段的小活交给 MarketplaceInstallMetadata::config_source、config_source_type、ref_name、sparse_paths,把取当前时间交给 utc_timestamp_now,最后调用外部的 record_user_marketplace 真正写配置。测试 installed_marketplace_root_for_source_uses_local_source_root 也用它先制造一条配置记录。

调用图:调用 5 个内部函数(config_source, config_source_type, ref_name, sparse_paths, utc_timestamp_now);被 2 处调用(add_marketplace_sync_with_cloner, installed_marketplace_root_for_source_uses_local_source_root);外部调用 1 个(record_user_marketplace)。

installed_marketplace_root_for_source55–96 ↗
fn installed_marketplace_root_for_source(
    codex_home: &Path,
    install_root: &Path,
    install_metadata: &MarketplaceInstallMetadata,
) -> Result<Option<PathBuf>, MarketplaceAddError>

作用:根据一个市场的来源信息,去用户配置里找“这个来源是不是已经安装过”。如果找到了,并且目录有效,就返回那个目录。

数据流:输入用户目录、安装根目录和来源身份证 → 读取 config.toml,解析成 TOML(一种常见配置文件格式)结构 → 遍历里面的 marketplaces 配置,逐条比较来源是否一致,再把配置里的信息解析成真实目录并验证目录合法 → 找到就返回目录,找不到返回空;配置读不出来或格式坏了则返回错误。

调用关系:它在 add_marketplace_sync_with_cloner 里用于决定是否可以复用已有安装,而不是重新安装。它用 MarketplaceInstallMetadata::matches_config 判断配置条目是不是同一个来源,用 resolve_configured_marketplace_root 把配置变成路径,用 validate_marketplace_root 检查这个路径确实是可用市场。两个测试分别验证它遇到配置读取错误会报错,以及本地来源能被正确找回。

调用图:调用 3 个内部函数(resolve_configured_marketplace_root, validate_marketplace_root, matches_config);被 3 处调用(add_marketplace_sync_with_cloner, installed_marketplace_root_for_source_propagates_config_read_errors, installed_marketplace_root_for_source_uses_local_source_root);外部调用 5 个(join, Internal, format!, read_to_string, from_str)。

find_marketplace_root_by_name98–138 ↗
fn find_marketplace_root_by_name(
    codex_home: &Path,
    install_root: &Path,
    marketplace_name: &str,
) -> Result<Option<PathBuf>, MarketplaceAddError>

作用:按市场名字去配置里找对应的已安装目录。它解决的是“我知道名字,但不知道目录在哪”的问题。

数据流:输入用户目录、安装根目录和市场名字 → 读取并解析 config.toml → 在 marketplaces 表里按名字取出那一项 → 把这项配置解析成实际根目录并检查目录是否合法 → 合法就返回路径,不存在或不合法就返回空,读取或解析配置失败则返回错误。

调用关系:它由 add_marketplace_sync_with_cloner 调用,通常用于安装流程里按名字确认已有市场位置。和 installed_marketplace_root_for_source 不同,它不是按来源匹配,而是直接按名字查找;找到配置后同样把路径解析交给 resolve_configured_marketplace_root,把合法性检查交给 validate_marketplace_root。

调用图:调用 2 个内部函数(resolve_configured_marketplace_root, validate_marketplace_root);被 1 处调用(add_marketplace_sync_with_cloner);外部调用 5 个(join, Internal, format!, read_to_string, from_str)。

MarketplaceInstallMetadata::from_source141–153 ↗
fn from_source(source: &MarketplaceSource, sparse_paths: &[String]) -> Self

作用:把用户提供的市场来源,转换成这个文件内部使用的“安装来源身份证”。这一步让后续写配置、匹配配置时有统一格式。

数据流:输入 MarketplaceSource,也就是外部传来的来源信息,以及稀疏路径列表 → 如果是 Git 来源,就保存 URL、分支或标签名、稀疏路径;如果是本地来源,就保存本地路径字符串 → 输出一个 MarketplaceInstallMetadata 对象。

调用关系:它在 add_marketplace_sync_with_cloner 开始处理来源时被调用,是后面所有元数据操作的起点。测试里也用它快速构造来源信息,然后交给 installed_marketplace_root_for_source 或 record_added_marketplace_entry 使用。

调用图:被 3 处调用(add_marketplace_sync_with_cloner, installed_marketplace_root_for_source_propagates_config_read_errors, installed_marketplace_root_for_source_uses_local_source_root)。

MarketplaceInstallMetadata::config_source_type155–160 ↗
fn config_source_type(&self) -> &'static str

作用:把内部来源类型翻译成写进配置文件的文字:Git 来源写成“git”,本地来源写成“local”。

数据流:输入当前 MarketplaceInstallMetadata 自己保存的来源 → 查看它是 Git 还是本地 → 输出一个固定字符串,供配置文件保存或比较。

调用关系:record_added_marketplace_entry 用它写入配置,matches_config 用它和配置里的 source_type 字段对照。它是一个小翻译器,保证写入和比较时用同一套词。

调用图:被 2 处调用(matches_config, record_added_marketplace_entry)。

MarketplaceInstallMetadata::config_source162–167 ↗
fn config_source(&self) -> String

作用:取出配置文件里要记录的“来源地址”。Git 来源就是仓库 URL,本地来源就是本地路径。

数据流:输入当前安装来源信息 → 如果来源是 Git,就复制 Git URL;如果是本地,就复制路径字符串 → 输出这个来源字符串。

调用关系:record_added_marketplace_entry 用它把来源写进配置,matches_config 用它检查配置里的 source 字段是不是同一个来源。它让外层代码不用关心来源内部到底是哪种类型。

调用图:被 2 处调用(matches_config, record_added_marketplace_entry)。

MarketplaceInstallMetadata::ref_name169–174 ↗
fn ref_name(&self) -> Option<&str>

作用:取出 Git 的分支名、标签名或提交引用;本地来源没有这个概念,所以返回空。

数据流:输入当前安装来源信息 → 如果是 Git,就返回里面保存的 ref_name;如果是本地,就返回 None,也就是没有值 → 输出可选的字符串引用。

调用关系:record_added_marketplace_entry 用它把 Git 引用写进配置,matches_config 用它确认配置里的 ref 是否一致。它帮助区分同一个仓库的不同分支或标签,避免误认为是同一个安装。

调用图:被 2 处调用(matches_config, record_added_marketplace_entry)。

MarketplaceInstallMetadata::sparse_paths176–181 ↗
fn sparse_paths(&self) -> &[String]

作用:取出 Git 稀疏路径列表。稀疏路径就是只安装仓库里指定的几个子目录,而不是整仓库;本地来源没有这个设置。

数据流:输入当前安装来源信息 → 如果是 Git,就返回保存的稀疏路径列表;如果是本地,就返回空列表 → 输出一个字符串列表的引用。

调用关系:record_added_marketplace_entry 用它写配置,matches_config 用它比较配置。它让“同一个 Git 仓库但只取不同目录”的情况也能被正确区分。

调用图:被 2 处调用(matches_config, record_added_marketplace_entry)。

MarketplaceInstallMetadata::matches_config183–190 ↗
fn matches_config(&self, marketplace: &toml::Value) -> bool

作用:判断配置文件里的某个市场条目,是否和当前这份安装来源信息完全对应。

数据流:输入当前安装来源信息和一段 TOML 配置值 → 读取配置里的 source_type、source、ref、sparse_paths → 分别和当前对象里的来源类型、来源地址、引用名、稀疏路径比较 → 全部一致就返回 true,否则返回 false。

调用关系:installed_marketplace_root_for_source 遍历所有已配置市场时会调用它,用来筛出“来源相同”的那一项。它内部调用 config_source_type、config_source、ref_name、sparse_paths 取当前来源字段,并调用 config_sparse_paths 解析配置里的稀疏路径。

调用图:调用 5 个内部函数(config_source, config_source_type, ref_name, sparse_paths, config_sparse_paths);被 1 处调用(installed_marketplace_root_for_source);外部调用 1 个(get)。

config_sparse_paths193–205 ↗
fn config_sparse_paths(marketplace: &toml::Value) -> Vec<String>

作用:从一个市场配置条目里读出 sparse_paths,也就是 Git 稀疏安装时指定的子路径列表。

数据流:输入一段 TOML 市场配置 → 查找 sparse_paths 字段,看它是不是数组 → 把数组里能当字符串的值收集起来 → 输出字符串列表;如果字段不存在或不是数组,就输出空列表。

调用关系:它只被 MarketplaceInstallMetadata::matches_config 调用,专门负责把配置里的稀疏路径整理成可比较的普通列表。这样 matches_config 不用关心 TOML 结构的细节。

调用图:被 1 处调用(matches_config);外部调用 1 个(get)。

utc_timestamp_now207–214 ↗
fn utc_timestamp_now() -> Result<String, MarketplaceAddError>

作用:生成当前时间的 UTC 时间戳,用来写进配置里的 last_updated。UTC 是统一世界时间,可以避免不同时区造成混乱。

数据流:输入不来自参数,而是读取系统当前时间 → 计算它距离 Unix epoch(1970-01-01 00:00:00 UTC,很多系统计时的起点)过去了多少秒 → 把秒数交给 format_utc_timestamp 变成类似 2026-04-10T00:00:00Z 的字符串;如果系统时钟早于 1970 年,就返回错误。

调用关系:record_added_marketplace_entry 在写配置前调用它,给这次记录盖上时间戳。它把格式化工作交给 format_utc_timestamp,自己主要负责从系统拿当前时间并处理异常。

调用图:调用 1 个内部函数(format_utc_timestamp);被 1 处调用(record_added_marketplace_entry);外部调用 1 个(now)。

format_utc_timestamp216–225 ↗
fn format_utc_timestamp(seconds_since_epoch: i64) -> String

作用:把“从 1970 年起过了多少秒”转换成人能看懂、机器也好读的 UTC 时间字符串。

数据流:输入一个秒数 → 拆成天数和一天内的时、分、秒 → 调用 civil_from_days 把天数换成年、月、日 → 拼成 RFC3339 风格的字符串,例如 1970-01-01T00:00:00Z → 输出这个字符串。

调用关系:utc_timestamp_now 用它把系统时间变成配置文件里的文本。测试 utc_timestamp_formats_unix_epoch_as_rfc3339_utc 直接检查它对几个固定秒数的输出,确保时间格式不会悄悄变错。

调用图:调用 1 个内部函数(civil_from_days);被 1 处调用(utc_timestamp_now);外部调用 1 个(format!)。

civil_from_days227–240 ↗
fn civil_from_days(days_since_epoch: i64) -> (i64, i64, i64)

作用:把距离 1970-01-01 的天数换算成公历的年、月、日。它是时间戳格式化里的日历计算小零件。

数据流:输入天数 → 用一套纯数学公式处理闰年、世纪年、月份偏移等日历规则 → 输出年、月、日三个数字。

调用关系:它只被 format_utc_timestamp 调用。format_utc_timestamp 负责时分秒和最终字符串,而 civil_from_days 只负责最容易出错的日期换算部分。

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

tests::utc_timestamp_formats_unix_epoch_as_rfc3339_utc249–258 ↗
fn utc_timestamp_formats_unix_epoch_as_rfc3339_utc()

作用:测试时间格式化是否正确,特别是 Unix 计时起点和另一个固定日期。这样能防止配置里的更新时间变成错误格式。

数据流:输入两个固定秒数写在测试里 → 调用 format_utc_timestamp 得到字符串 → 和预期的 UTC 字符串比较 → 如果不一致,测试失败,不改动外部状态。

调用关系:它是 format_utc_timestamp 的保护网。时间格式一旦被改坏,这个测试会提醒开发者。

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

tests::installed_marketplace_root_for_source_propagates_config_read_errors261–287 ↗
fn installed_marketplace_root_for_source_propagates_config_read_errors()

作用:测试读取用户配置失败时,查找已安装市场的函数不会假装没事,而是把错误说清楚。

数据流:测试先创建一个临时用户目录,再故意把 config.toml 的位置建成目录,让读取文件必然失败 → 构造一个 Git 来源元数据 → 调用 installed_marketplace_root_for_source → 期望得到错误,并检查错误文字里包含读取配置失败的信息。

调用关系:它验证 installed_marketplace_root_for_source 的错误处理路径,也用 MarketplaceInstallMetadata::from_source 构造测试输入。这个测试保证真实用户遇到坏配置路径时能看到明确报错。

调用图:调用 2 个内部函数(from_source, installed_marketplace_root_for_source);外部调用 3 个(new, assert!, create_dir)。

tests::installed_marketplace_root_for_source_uses_local_source_root290–314 ↗
fn installed_marketplace_root_for_source_uses_local_source_root()

作用:测试本地来源的市场能被正确记录并重新找到。也就是说,配置里写的是本地路径时,程序应该返回这个原始目录。

数据流:测试创建临时目录和一个看起来合法的本地市场文件结构 → 用 from_source 构造本地来源元数据 → 调用 record_added_marketplace_entry 写入配置 → 再调用 installed_marketplace_root_for_source 查找 → 期望返回的路径就是原来的本地来源目录。

调用关系:它把 record_added_marketplace_entry 和 installed_marketplace_root_for_source 串起来测了一遍,确认“写配置”和“按来源找回目录”这两个步骤能配合工作。

调用图:调用 3 个内部函数(from_source, installed_marketplace_root_for_source, record_added_marketplace_entry);外部调用 4 个(new, assert_eq!, create_dir_all, write)。

core-plugins/src/marketplace_add/source.rs源码 ↗
domain_logic添加 marketplace 时解析来源、准备 staging、校验根目录;测试时也会运行

添加 marketplace 时,用户可能输入 owner/repo、https://...、ssh://...、./local/path,甚至还会带 main 这样的分支名。这个文件就像入口处的检票员:先把输入修剪干净,再拆出来源和版本引用,然后判断它到底是本地目录还是 Git 仓库。如果是 Git,会把 GitHub 简写补成完整地址;如果是本地路径,会展开 ~/,转成真实绝对路径,并确认它不是普通文件。后面真正下载或校验 marketplace 前,都依赖这里给出的统一结果。这样其他代码不用反复猜“用户到底想表达什么”。

函数细节28
parse_marketplace_source18–65 ↗
fn parse_marketplace_source(
    source: &str,
    explicit_ref: Option<String>,
) -> Result<MarketplaceSource, MarketplaceAddError>

作用:把用户写的一段来源文字,翻译成程序能理解的 MarketplaceSource:要么是 Git 仓库,要么是本地目录。它也会顺手检查明显不合法的写法,避免后面下载或读取时才爆错。

数据流:进去的是原始 source 字符串和一个可选的 explicit_ref 分支/标签名;它先去掉空白,拆出 # 或 @ 后面的引用,再判断像不像本地路径、Git URL,或 GitHub 的 owner/repo 简写;出来的是规范化后的 MarketplaceSource,或者一条告诉用户哪里写错的 MarketplaceAddError。

调用关系:这是 add_marketplace_sync_with_cloner 添加 marketplace 时最早用到的解析关口。它把细节活交给 split_source_ref、looks_like_local_path、resolve_local_source_path、is_git_url、is_ssh_git_url、looks_like_github_shorthand 和 normalize_git_url;很多测试也直接调用它,确认各种输入都会被分到正确类型。

调用图:调用 7 个内部函数(is_git_url, is_ssh_git_url, looks_like_github_shorthand, looks_like_local_path, normalize_git_url, resolve_local_source_path, split_source_ref);被 6 处调用(add_marketplace_sync_with_cloner, file_url_source_is_rejected, github_shorthand_and_git_url_normalize_to_same_source, local_file_source_is_rejected, local_path_source_parses, non_git_sources_reject_ref_override);外部调用 2 个(InvalidRequest, format!)。

stage_marketplace_source67–90 ↗
fn stage_marketplace_source(
    source: &MarketplaceSource,
    sparse_paths: &[String],
    staged_root: &Path,
    clone_source: F,
) -> Result<(), MarketplaceAddError>

作用:在需要把远程 marketplace 先放到临时目录时,这个函数决定能不能 staging,并调用外部传进来的克隆函数。staging 可以理解成“先把货卸到临时仓库里检查”。

数据流:进去的是已经解析好的来源、稀疏路径列表、临时目录 staged_root,以及一个 clone_source 回调;它先拒绝“本地路径还要求稀疏下载”的情况,然后对 Git 来源调用 clone_source;出来通常是成功或错误,本身不直接下载,而是把下载动作交出去。

调用关系:add_marketplace_sync_with_cloner 会在准备远程 Git marketplace 时调用它。它不处理本地 marketplace,因为本地来源不需要复制安装根目录;测试 non_git_sources_reject_sparse_checkout 专门确认本地来源不能用 --sparse。

调用图:被 2 处调用(add_marketplace_sync_with_cloner, non_git_sources_reject_sparse_checkout);外部调用 3 个(InvalidRequest, matches!, unreachable!)。

validate_marketplace_source_root92–98 ↗
fn validate_marketplace_source_root(root: &Path) -> Result<String, MarketplaceAddError>

作用:检查一个 marketplace 根目录是不是真的像一个合法 marketplace,并取出它的名字。它防止用户给了一个目录,但里面结构不对或名字不能当插件标识。

数据流:进去的是一个目录路径 root;它先调用 validate_marketplace_root 检查目录内容并拿到 marketplace 名字,再用 validate_plugin_segment 检查这个名字是否适合做标识;出来是合法的名字字符串,或转成 MarketplaceAddError 的错误。

调用关系:add_marketplace_sync_with_cloner 在拿到本地目录或克隆完远程仓库后会用它做最后验收。它连接了 marketplace 根目录校验和插件命名规则校验,保证后续安装看到的是干净、可信的输入。

调用图:调用 1 个内部函数(validate_marketplace_root);被 1 处调用(add_marketplace_sync_with_cloner);外部调用 1 个(validate_plugin_segment)。

split_source_ref100–111 ↗
fn split_source_ref(source: &str) -> (String, Option<String>)

作用:从来源字符串里拆出“仓库/路径本体”和“想要的分支或标签”。比如 owner/repo@main 会被拆成 owner/repo 和 main。

数据流:进去的是一整段 source;它优先用 # 拆引用,如果不是带协议的 URL、也不是 SSH Git 地址,再允许用 @ 拆引用;出来是 base_source 字符串和可选 ref_name。

调用关系:parse_marketplace_source 一开始就调用它。它内部会问 is_ssh_git_url,避免把 git@host:path 这种 SSH 地址里的 @ 错当成分支分隔符,并把候选引用交给 non_empty_ref 清理。

调用图:调用 2 个内部函数(is_ssh_git_url, non_empty_ref);被 1 处调用(parse_marketplace_source)。

non_empty_ref113–116 ↗
fn non_empty_ref(ref_name: &str) -> Option<String>

作用:把引用名收拾一下,只留下真正有内容的分支或标签名。空的引用不会被当成有效值。

数据流:进去的是可能带空格的 ref_name;它去掉前后空白,检查是否为空;出来是 Some(干净字符串) 或 None。

调用关系:split_source_ref 在拆出 # 或 @ 后面的内容时调用它。这样 parse_marketplace_source 后面看到的引用要么是真有名字,要么就是没有引用。

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

normalize_git_url118–125 ↗
fn normalize_git_url(url: &str) -> String

作用:把 Git 地址整理成更统一的样子,尤其是 GitHub HTTPS 地址。这样 owner/repo 简写和完整 GitHub URL 最后能对齐。

数据流:进去的是一个 Git URL;它先去掉末尾多余的斜杠,如果是 https://github.com/... 且没有 .git,就补上 .git;出来是规范化后的 URL 字符串。

调用关系:parse_marketplace_source 在确认来源是 Git URL 后调用它。它让后面的比较、显示和克隆少受用户输入格式差异影响。

调用图:被 1 处调用(parse_marketplace_source);外部调用 1 个(format!)。

looks_like_local_path127–137 ↗
fn looks_like_local_path(source: &str) -> bool

作用:判断一段输入是不是更像本地路径,而不是仓库名或网址。这样 ./marketplace、~/marketplace、C:\... 这类输入不会被误当成 GitHub 简写。

数据流:进去的是 source 字符串;它检查 Unix 绝对路径、Windows 绝对路径、相对路径前缀、~ 家目录、单独的 . 或 ..;出来是 true 或 false。

调用关系:parse_marketplace_source 用它决定是否走本地目录解析。它把 Windows 路径判断交给 looks_like_windows_absolute_path,保证即使程序跑在非 Windows 系统上也能识别 Windows 风格路径。

调用图:调用 1 个内部函数(looks_like_windows_absolute_path);被 1 处调用(parse_marketplace_source);外部调用 1 个(new)。

looks_like_windows_absolute_path139–146 ↗
fn looks_like_windows_absolute_path(source: &str) -> bool

作用:专门识别 Windows 绝对路径,比如 C:\Users\alice 或 \\server\share。它让路径判断不只适合当前操作系统。

数据流:进去的是 source 字符串;它查看字节形式,判断是不是盘符加冒号加斜杠,或者 UNC 网络路径开头;出来是 true 或 false。

调用关系:looks_like_local_path 会调用它补足 Windows 情况。测试 windows_absolute_paths_look_like_local_paths_on_every_host 覆盖了这类判断。

调用图:被 1 处调用(looks_like_local_path);外部调用 1 个(matches!)。

resolve_local_source_path148–167 ↗
fn resolve_local_source_path(source: &str) -> Result<PathBuf, MarketplaceAddError>

作用:把用户写的本地路径变成真实、可用的绝对路径。它会发现路径不存在、无法访问等问题,并把这些变成用户能看到的错误。

数据流:进去的是本地路径字符串;它先展开 ~/,如果还是相对路径就接到当前工作目录后面,然后 canonicalize,也就是解析成系统确认存在的真实路径;出来是 PathBuf,或说明无法解析的 MarketplaceAddError。

调用关系:parse_marketplace_source 认定输入是本地路径后会调用它。它把家目录展开交给 expand_tilde_path,并在读取当前目录失败时返回内部错误。

调用图:调用 1 个内部函数(expand_tilde_path);被 1 处调用(parse_marketplace_source);外部调用 1 个(current_dir)。

expand_tilde_path169–177 ↗
fn expand_tilde_path(source: &str) -> PathBuf

作用:把 ~/xxx 这种写法换成用户家目录下的真实路径。用户常用这种缩写,这个函数让程序也懂。

数据流:进去的是路径字符串;如果不是 ~/ 开头,就原样变成 PathBuf;如果是,就读取 HOME 或 USERPROFILE 环境变量,把后半段拼到家目录后面;出来是一个 PathBuf。

调用关系:resolve_local_source_path 在处理本地路径前调用它。它只做轻量转换,不检查路径是否存在,真正的存在性检查由后面的 canonicalize 完成。

调用图:被 1 处调用(resolve_local_source_path);外部调用 2 个(from, var_os)。

is_ssh_git_url179–181 ↗
fn is_ssh_git_url(source: &str) -> bool

作用:判断输入是不是 SSH 形式的 Git 地址。SSH 可以理解成用钥匙登录服务器的仓库地址,常见写法有 ssh://... 或 git@github.com:owner/repo.git。

数据流:进去的是 source 字符串;它检查是否以 ssh:// 开头,或者是否以 git@ 开头且包含冒号;出来是 true 或 false。

调用关系:parse_marketplace_source 用它识别 Git 来源,split_source_ref 用它避免错误拆分 git@... 里的 @。

调用图:被 2 处调用(parse_marketplace_source, split_source_ref)。

is_git_url183–185 ↗
fn is_git_url(source: &str) -> bool

作用:判断输入是不是 HTTP 或 HTTPS 形式的 Git 地址。这里不证明仓库真的存在,只判断格式像不像。

数据流:进去的是 source 字符串;它检查是否以 http:// 或 https:// 开头;出来是 true 或 false。

调用关系:parse_marketplace_source 在本地路径判断之后调用它。若返回 true,来源会被当作 Git 仓库并送去 normalize_git_url 整理。

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

looks_like_github_shorthand187–195 ↗
fn looks_like_github_shorthand(source: &str) -> bool

作用:判断输入是不是 GitHub 简写,也就是 owner/repo 这种两段式写法。这样用户不用每次都写完整 https://github.com/owner/repo.git。

数据流:进去的是 source 字符串;它按 / 拆成段,要求刚好两段,并且每段字符都合法;出来是 true 或 false。

调用关系:parse_marketplace_source 在排除本地路径和完整 Git URL 后调用它。段名是否合法由 is_github_shorthand_segment 判断。

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

is_github_shorthand_segment197–202 ↗
fn is_github_shorthand_segment(segment: &str) -> bool

作用:检查 GitHub 简写中的一段名字是否合法,比如 owner 或 repo。它要求不能为空,只能用常见的字母、数字、横线、下划线和点。

数据流:进去的是一个 segment 字符串;它逐个字符检查是否属于允许范围;出来是 true 或 false。

调用关系:它是 looks_like_github_shorthand 的小零件,用来分别检查 owner 和 repo 两段。虽然调用图没有单独列出调用者,但代码中它服务于 GitHub 简写判断。

MarketplaceSource::display205–213 ↗
fn display(&self) -> String

作用:把已经解析好的 MarketplaceSource 再变成适合显示给人的文字。它用于日志、错误提示或界面展示时,让用户知道当前来源是什么。

数据流:进去的是一个 MarketplaceSource;如果是 Git,就输出 URL,并在有引用时拼上 #ref;如果是本地目录,就输出路径字符串;出来是可读的 String。

调用关系:这是 MarketplaceSource 这个类型自带的展示方法。它不参与解析,只在别的流程需要把来源说明白时提供统一格式。

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

tests::github_shorthand_parses_ref_suffix223–231 ↗
fn github_shorthand_parses_ref_suffix()

作用:测试 owner/repo@main 这种 GitHub 简写带分支名的输入能被正确识别。它保护的是用户最省事的写法。

数据流:进去的是测试里写死的 owner/repo@main;测试期望解析后变成 GitHub 的完整 .git 地址,并带 main 引用;出来是测试通过或断言失败。

调用关系:它属于本文件的测试模块,用 assert_eq! 对照期望结果。它覆盖 parse_marketplace_source 对 GitHub 简写和 @ 引用后缀的处理。

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

tests::git_url_parses_fragment_ref234–246 ↗
fn git_url_parses_fragment_ref()

作用:测试完整 Git URL 后面带 #v1 时,# 后面的内容会被当成分支或标签。这样用户可以直接在地址里指定版本。

数据流:进去的是 https://example.com/team/repo.git#v1;测试期望得到不带 #v1 的仓库地址和 ref_name=v1;出来是测试通过或断言失败。

调用关系:它用 assert_eq! 验证解析结果,主要保护 split_source_ref 对 # 分隔符的行为,以及 parse_marketplace_source 对普通 HTTPS Git 地址的接受。

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

tests::explicit_ref_overrides_source_ref249–257 ↗
fn explicit_ref_overrides_source_ref()

作用:测试命令行单独传入的 --ref 会覆盖来源字符串里写的 @main。这样规则明确,不会出现两个版本名打架。

数据流:进去的是 owner/repo@main 和 explicit_ref=release;测试期望最终 ref_name 是 release,而不是 main;出来是测试通过或断言失败。

调用关系:它验证 parse_marketplace_source 里 explicit_ref.or(parsed_ref) 这条优先级规则。这个规则对调用方 add_marketplace_sync_with_cloner 很重要,因为命令参数应该比字符串后缀更明确。

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

tests::github_shorthand_and_git_url_normalize_to_same_source260–276 ↗
fn github_shorthand_and_git_url_normalize_to_same_source()

作用:测试 owner/repo 和 https://github.com/owner/repo.git 最后会被看成同一个来源。这样不同写法不会造成重复或比较错误。

数据流:进去的是 GitHub 简写和完整 URL 两种字符串;测试分别解析它们,再比较结果是否相等,并确认最终 URL 是带 .git 的标准形式;出来是测试通过或断言失败。

调用关系:调用图显示它会调用 parse_marketplace_source,并用 assert_eq! 比较。它主要保护 looks_like_github_shorthand 和 normalize_git_url 的配合。

调用图:调用 1 个内部函数(parse_marketplace_source);外部调用 1 个(assert_eq!)。

tests::github_url_with_trailing_slash_normalizes_without_extra_path_segment279–288 ↗
fn github_url_with_trailing_slash_normalizes_without_extra_path_segment()

作用:测试 GitHub URL 末尾多写一个斜杠时,不会变成奇怪的多一层路径。用户复制地址时经常会带这个斜杠。

数据流:进去的是 https://github.com/owner/repo/;测试期望输出 https://github.com/owner/repo.git;出来是测试通过或断言失败。

调用关系:它用 assert_eq! 保护 normalize_git_url 的行为,确保 parse_marketplace_source 在处理 GitHub URL 时先去掉末尾斜杠再补 .git。

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

tests::non_github_https_source_parses_as_git_url291–300 ↗
fn non_github_https_source_parses_as_git_url()

作用:测试非 GitHub 的 HTTPS 地址也能当作 Git 仓库来源。也就是说这里不是只支持 GitHub。

数据流:进去的是 https://gitlab.com/owner/repo;测试期望它被解析成 Git 来源,URL 原样保留;出来是测试通过或断言失败。

调用关系:它用 assert_eq! 验证 is_git_url 的宽松判断和 normalize_git_url 的谨慎行为:只有 GitHub 地址才会自动补 .git。

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

tests::file_url_source_is_rejected303–313 ↗
fn file_url_source_is_rejected()

作用:测试 file:///tmp/... 这种 file URL 会被拒绝。这样用户不会误以为可以用文件协议当 marketplace 来源。

数据流:进去的是 file:///tmp/marketplace.git;测试期望解析失败,并且错误信息包含 invalid marketplace source format;出来是测试通过或断言失败。

调用关系:调用图显示它调用 parse_marketplace_source 和 assert!。它保护 parse_marketplace_source 的格式边界:只接受 owner/repo、Git URL 或本地路径,不接受 file://。

调用图:调用 1 个内部函数(parse_marketplace_source);外部调用 1 个(assert!)。

tests::local_path_source_parses316–323 ↗
fn local_path_source_parses()

作用:测试 . 这种当前目录写法会被识别成本地 marketplace 路径。它保证本地开发调试时可以直接添加当前目录。

数据流:进去的是字符串 .;测试解析后要求结果是 Local,并且路径已经变成绝对路径;如果不是 Local 就主动 panic;出来是测试通过或失败。

调用关系:调用图显示它调用 parse_marketplace_source、assert! 和 panic!。它覆盖 looks_like_local_path 与 resolve_local_source_path 的基本配合。

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

tests::windows_absolute_paths_look_like_local_paths_on_every_host326–331 ↗
fn windows_absolute_paths_look_like_local_paths_on_every_host()

作用:测试 Windows 风格绝对路径在任何系统上都能被识别成本地路径。这样 Linux 或 macOS 上运行测试时也不会漏掉 Windows 用户场景。

数据流:进去的是几种 Windows 路径字符串,包括盘符路径和网络共享路径;测试期望真正的绝对路径返回 true,C:relative\path 这种相对写法返回 false;出来是测试通过或断言失败。

调用关系:它用 assert! 保护 looks_like_windows_absolute_path 和 looks_like_local_path 的跨平台判断。

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

tests::local_file_source_is_rejected334–347 ↗
fn local_file_source_is_rejected()

作用:测试如果用户给的是一个本地文件,而不是目录,会被明确拒绝。marketplace 来源需要是目录,因为里面要有完整结构。

数据流:进去的是临时目录里创建出的 marketplace.json 文件路径;测试解析它,期望失败,并检查错误信息说明“必须是目录不是文件”;出来是测试通过或失败。

调用关系:调用图显示它会创建临时目录、写文件、调用 parse_marketplace_source,并用 assert! 检查错误。它保护 parse_marketplace_source 中 path.is_file() 的防呆检查。

调用图:调用 1 个内部函数(parse_marketplace_source);外部调用 3 个(new, assert!, write)。

tests::non_git_sources_reject_ref_override350–358 ↗
fn non_git_sources_reject_ref_override()

作用:测试本地路径不能使用 --ref。--ref 是给 Git 分支或标签用的,本地目录没有这个概念。

数据流:进去的是 ./marketplace 加 explicit_ref=main;测试期望解析失败,并且错误信息说明 --ref 只支持 Git marketplace 来源;出来是测试通过或失败。

调用关系:调用图显示它调用 parse_marketplace_source 和 assert!。它保护 parse_marketplace_source 对本地路径和 Git 专属参数的边界检查。

调用图:调用 1 个内部函数(parse_marketplace_source);外部调用 1 个(assert!)。

tests::non_git_sources_reject_sparse_checkout361–376 ↗
fn non_git_sources_reject_sparse_checkout()

作用:测试本地来源不能使用 --sparse。sparse checkout 是 Git 的“只取一部分目录”能力,本地目录不需要也不支持这个步骤。

数据流:进去的是一个 Local 来源、一个稀疏路径 plugins/foo、临时 staged_root 和假的 clone 回调;测试期望 stage_marketplace_source 直接报错;出来是测试通过或失败。

调用关系:调用图显示它调用 stage_marketplace_source、Path::new、current_dir 和 assert!。它保护 stage_marketplace_source 在调用 clone_source 之前的参数检查。

调用图:调用 1 个内部函数(stage_marketplace_source);外部调用 3 个(new, assert!, current_dir)。

tests::ssh_url_parses_as_git_url379–391 ↗
fn ssh_url_parses_as_git_url()

作用:测试 ssh://git@github.com/...#main 这种 SSH Git 地址能被识别,并能拆出 main 引用。它覆盖使用 SSH 密钥访问仓库的用户。

数据流:进去的是带 #main 的 SSH URL;测试期望输出同一个 SSH 仓库地址和 ref_name=main;出来是测试通过或断言失败。

调用关系:它用 assert_eq! 保护 is_ssh_git_url、split_source_ref 和 parse_marketplace_source 的组合行为,尤其是不要把 SSH 地址里的 git@ 部分误拆。

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

core-plugins/src/marketplace_add.rs源码 ↗
orchestration用户执行添加插件市场命令时,或导入插件时触发

可以把这个文件理解成“新增插件市场”的接待员和搬运工。用户给它一个来源:可能是本机文件夹,也可能是 Git 地址。它先把这个来源看懂,再检查一些规则,比如本地来源不能用 sparse checkout(只拉仓库里一部分内容的 Git 功能),官方保留名字不能被外部来源冒用,已经添加过的市场不要重复装。对本地目录,它不会复制文件,只是在配置里登记这个目录。对 Git 来源,它会先下载到临时暂存区,确认里面真的有 marketplace.json 这类市场描述文件,再搬到正式安装目录。最后它会写入配置;如果写配置失败,还会尽量把刚搬进去的目录退回去,避免系统留下一半成功、一半失败的脏状态。

函数细节10
add_marketplace50–57 ↗
async fn add_marketplace(
    codex_home: PathBuf,
    request: MarketplaceAddRequest,
) -> Result<MarketplaceAddOutcome, MarketplaceAddError>

作用:这是对外使用的异步入口,用来添加一个插件市场。它把真正耗时的文件操作和 Git 操作放到后台线程里跑,避免卡住异步运行环境。

数据流:进去的是 Codex 的主目录路径和一个添加请求,里面有来源地址、可选分支名、可选 sparse 路径。它把这些交给同步版本执行,并等待结果。出来的是添加结果;如果后台线程出错,就包装成“添加失败”的内部错误。

调用关系:它会被 import_plugins 和 run_add 这类上层流程调用。自己不直接做安装细节,而是通过 spawn_blocking 把活儿交给 add_marketplace_sync 这一套同步流程。

调用图:被 2 处调用(import_plugins, run_add);外部调用 1 个(spawn_blocking)。

is_local_marketplace_source59–67 ↗
fn is_local_marketplace_source(
    source: &str,
    explicit_ref: Option<String>,
) -> Result<bool, MarketplaceAddError>

作用:这个函数只回答一个问题:给定的 marketplace 来源是不是本地目录。上层可以用它提前判断接下来该走本地路径逻辑还是远程 Git 逻辑。

数据流:进去的是用户输入的来源字符串和可选的引用名。它先调用解析器把字符串变成结构化来源,再看解析结果是不是 Local。本地就返回 true,不是就返回 false;解析失败就返回错误。

调用关系:它是一个小型判断工具。它依赖 parse_marketplace_source 来读懂来源格式,但不参与真正安装,也不会改动磁盘或配置。

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

add_marketplace_sync69–74 ↗
fn add_marketplace_sync(
    codex_home: &Path,
    request: MarketplaceAddRequest,
) -> Result<MarketplaceAddOutcome, MarketplaceAddError>

作用:这是同步版本的添加入口。它使用默认的 Git 克隆函数,完成真正的 marketplace 添加流程。

数据流:进去的是 Codex 主目录和添加请求。它把请求原样交给更通用的 add_marketplace_sync_with_cloner,并指定默认的 clone_git_source 作为下载 Git 仓库的方法。出来的是添加结果或错误。

调用关系:它被 add_marketplace 间接调用。它本身像一个薄薄的转接头,把普通调用转到可测试、可替换 Git 克隆器的核心函数。

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

add_marketplace_sync_with_cloner76–210 ↗
fn add_marketplace_sync_with_cloner(
    codex_home: &Path,
    request: MarketplaceAddRequest,
    clone_source: F,
) -> Result<MarketplaceAddOutcome, MarketplaceAddError>

作用:这是本文件最核心的流程函数,真正决定一个 marketplace 怎么被添加。它同时处理本地目录、Git 仓库、重复添加、名称冲突、临时目录安装和配置写入。

数据流:进去的是 Codex 主目录、添加请求,以及一个“怎么克隆 Git”的函数。它先解析来源,拒绝不合规请求;然后创建安装目录,生成安装元数据,检查同一个来源是否已经登记过。若是本地目录,它验证目录内容后只写配置。若是 Git 来源,它创建暂存目录,把源码放进去,验证市场名称,算出安全的安装目录,搬到正式位置,再写配置。出来的是 MarketplaceAddOutcome;过程中会创建目录、复制或移动文件、写配置,失败时返回清楚的错误,配置写失败还会尝试回滚已安装目录。

调用关系:add_marketplace_sync 会调用它,测试也直接调用它并传入假的克隆函数,方便不用真的访问网络。它把具体小任务分给 marketplace_install_root、marketplace_staging_root、stage_marketplace_source、validate_marketplace_source_root、record_added_marketplace_entry 等函数,自己负责把这些步骤按正确顺序串起来。

调用图:调用 13 个内部函数(marketplace_install_root, ensure_marketplace_destination_is_inside_install_root, marketplace_staging_root, replace_marketplace_root, safe_marketplace_dir_name, from_source, find_marketplace_root_by_name, installed_marketplace_root_for_source, record_added_marketplace_entry, parse_marketplace_source (+3 more));被 5 处调用(add_marketplace_sync, add_marketplace_sync_installs_local_directory_source_and_updates_config, add_marketplace_sync_installs_marketplace_and_updates_config, add_marketplace_sync_rejects_sparse_checkout_for_local_directory_source, add_marketplace_sync_treats_existing_local_directory_source_as_already_added);外部调用 8 个(new, Internal, InvalidRequest, is_openai_curated_marketplace_name, format!, create_dir_all, rename, matches!)。

tests::add_marketplace_sync_installs_marketplace_and_updates_config220–254 ↗
fn add_marketplace_sync_installs_marketplace_and_updates_config() -> Result<()>

作用:这个测试确认:从 Git 来源添加 marketplace 时,系统会把内容安装到本地,并把来源信息写进配置文件。

数据流:它先创建临时 Codex 主目录和一份假的 marketplace 源目录,再用一个假的克隆函数把源目录复制到目标位置。然后调用 add_marketplace_sync_with_cloner。最后检查结果里的市场名、来源显示、是否不是重复添加,以及安装目录和配置文件内容是否符合预期。

调用关系:这是核心安装流程的“远程来源”测试。它直接调用 add_marketplace_sync_with_cloner,并借助 write_marketplace_source 准备假市场、copy_dir_all 模拟 Git 克隆。

调用图:调用 1 个内部函数(add_marketplace_sync_with_cloner);外部调用 6 个(new, new, assert!, assert_eq!, write_marketplace_source, read_to_string)。

tests::add_marketplace_sync_installs_local_directory_source_and_updates_config257–298 ↗
fn add_marketplace_sync_installs_local_directory_source_and_updates_config() -> Result<()>

作用:这个测试确认:如果 marketplace 来源是本地目录,系统不会复制它,而是直接把这个目录登记到配置里。

数据流:它创建临时目录,写入一份假的 marketplace,然后把这个本地路径作为来源传入。假的 Git 克隆函数如果被调用就会直接报错。调用结束后,它检查返回的安装路径就是原本的本地目录,正式安装目录里没有复制品,并确认配置里写的是 local 来源。

调用关系:它覆盖 add_marketplace_sync_with_cloner 的本地目录分支。通过让克隆函数 panic,它证明本地来源不会走 Git 下载流程。

调用图:调用 2 个内部函数(add_marketplace_sync_with_cloner, from_absolute_path);外部调用 7 个(new, new, assert!, assert_eq!, write_marketplace_source, read_to_string, from_str)。

tests::add_marketplace_sync_rejects_sparse_checkout_for_local_directory_source301–330 ↗
fn add_marketplace_sync_rejects_sparse_checkout_for_local_directory_source() -> Result<()>

作用:这个测试确认:本地目录来源不能使用 sparse checkout 参数。因为 sparse checkout 是 Git 才有的“只取部分文件”能力,本地目录没有这个概念。

数据流:它准备一个本地 marketplace,然后在请求里故意加入 sparse 路径。调用核心添加函数后,它期待拿到错误,并检查错误文字是否说明 sparse 只支持 Git 来源。同时它确认配置文件没有被创建,说明失败时没有留下登记记录。

调用关系:它测试 add_marketplace_sync_with_cloner 一开始的请求校验。这个测试不应该触发 Git 克隆,也不应该改写配置。

调用图:调用 1 个内部函数(add_marketplace_sync_with_cloner);外部调用 5 个(new, assert!, assert_eq!, write_marketplace_source, vec!)。

tests::add_marketplace_sync_treats_existing_local_directory_source_as_already_added333–360 ↗
fn add_marketplace_sync_treats_existing_local_directory_source_as_already_added() -> Result<()>

作用:这个测试确认:同一个本地 marketplace 添加第二次时,不会被当成冲突,而是返回“已经添加过”。

数据流:它准备一个本地 marketplace,用同一个请求调用核心添加函数两次。第一次应当是新添加,第二次应当标记 already_added 为 true,并且两次返回的安装路径一致。

调用关系:它验证 add_marketplace_sync_with_cloner 对重复来源的处理。这个场景依赖已写入的安装元数据,让第二次调用能认出“这是同一个来源”。

调用图:调用 1 个内部函数(add_marketplace_sync_with_cloner);外部调用 5 个(new, new, assert!, assert_eq!, write_marketplace_source)。

tests::write_marketplace_source362–386 ↗
fn write_marketplace_source(source: &Path, marker: &str) -> std::io::Result<()>

作用:这是测试用的造数据工具,用来快速生成一个看起来合法的 marketplace 目录。

数据流:进去的是目标目录和一个标记字符串。它在目标目录里创建 marketplace 需要的文件夹,写入 marketplace.json、插件 plugin.json,以及一个 marker.txt。出来没有业务结果,但磁盘上多了一套测试用市场文件。

调用关系:多个测试先调用它来准备假 marketplace。它不参与正式产品流程,只服务测试,让测试不用手写一大堆重复文件准备代码。

调用图:外部调用 3 个(join, create_dir_all, write)。

tests::copy_dir_all388–401 ↗
fn copy_dir_all(source: &Path, destination: &Path) -> std::io::Result<()>

作用:这是测试用的递归复制工具,用来把一个目录完整复制到另一个目录。测试用它模拟 Git 克隆后的文件落地效果。

数据流:进去的是源目录和目标目录。它先创建目标目录,再逐个读取源目录里的文件和子目录;遇到子目录就继续递归复制,遇到普通文件就复制文件。出来没有单独返回数据,但目标目录会变成源目录的一份拷贝。

调用关系:它主要被远程来源安装测试里的假克隆函数使用。这样测试可以验证安装流程,而不需要真的连 GitHub 或执行 git 命令。

调用图:外部调用 5 个(join, copy_dir_all, copy, create_dir_all, read_dir)。

core-plugins/src/marketplace_upgrade/git.rs源码 ↗
io_transportmarketplace upgrade 时运行,主要发生在下载、校验和准备 Git 来源代码阶段

marketplace 升级时,系统需要从一个 Git 地址拿到代码,就像去仓库取货:先确认要取哪一版,再把货搬到指定地方。这个文件专门做这件事。它不会自己实现 Git,而是调用电脑上的 git 命令,并把容易出问题的地方包好:比如 Git 卡住不退出、远端地址要输入密码、命令失败但错误不清楚、Windows 路径格式让 Git 不认识。主要入口有两个:git_remote_revision 用来查某个分支或标签实际对应的提交号;clone_git_source 用来把仓库克隆到本地,必要时只下载指定目录,减少下载量。下面的辅助函数像安全护栏:统一创建不弹交互提示的 Git 命令、检查命令是否成功、超时后杀掉卡住的进程、读取当前工作目录的真实提交号。

函数细节15
git_remote_revision8–41 ↗
fn git_remote_revision(
    source: &str,
    ref_name: Option<&str>,
    timeout: Duration,
) -> Result<String, String>

作用:查出 Git 远端仓库里某个分支、标签或 HEAD 当前对应的完整提交号。这样升级流程可以知道自己到底要安装哪一版,而不是只拿到一个会变化的名字。

数据流:输入是仓库地址 source、可选的 ref_name(比如分支名、标签名或提交号)和超时时间。它先看 ref_name 是否已经是 40 位完整 Git 提交号;如果是,就直接返回。否则它运行 git ls-remote 去远端查询,把输出第一行按制表符拆开,取前面的提交号;如果输出为空、格式不对或命令失败,就返回说明原因的错误文字。

调用关系:它由 upgrade_configured_git_marketplace 在升级 Git 类型 marketplace 时调用。它自己把创建 Git 命令的事交给 git_command,把执行和超时交给 run_git_command_with_timeout,把成功检查交给 ensure_git_success,并用 is_full_git_sha 避免对已经明确的提交号再查远端。

调用图:调用 4 个内部函数(ensure_git_success, git_command, is_full_git_sha, run_git_command_with_timeout);被 1 处调用(upgrade_configured_git_marketplace);外部调用 2 个(from_utf8_lossy, format!)。

clone_git_source43–110 ↗
fn clone_git_source(
    source: &str,
    ref_name: Option<&str>,
    sparse_paths: &[String],
    destination: &Path,
    timeout: Duration,
) -> Result<String, String>

作用:把指定 Git 仓库下载到本地目录,并切到需要的分支、标签或提交。它还支持“稀疏检出”,也就是只拿仓库里的某些路径,避免下载不需要的内容。

数据流:输入是仓库地址、可选版本名、要保留的路径列表、目标目录和超时时间。它先把目标路径整理成 Git 能接受的形式。如果没有指定稀疏路径,就普通 clone,必要时 checkout 到指定版本,然后读取当前提交号返回。如果指定了稀疏路径,它会先用不检出文件的方式克隆,再设置 sparse-checkout,最后 checkout 到目标版本,并返回最终工作区的提交号。过程中任何 Git 命令失败都会变成带上下文的错误。

调用关系:它由 upgrade_configured_git_marketplace 调用,是实际把代码搬到本地的主流程。它反复使用 git_command 创建命令,用 run_git_command_with_timeout 执行,用 ensure_git_success 判断成败,用 git_path_arg 处理路径兼容性,最后交给 git_worktree_revision 确认克隆出来的真实版本。

调用图:调用 5 个内部函数(ensure_git_success, git_command, git_path_arg, git_worktree_revision, run_git_command_with_timeout);被 1 处调用(upgrade_configured_git_marketplace)。

git_worktree_revision112–130 ↗
fn git_worktree_revision(destination: &Path, timeout: Duration) -> Result<String, String>

作用:读取本地 Git 工作目录当前指向的提交号。克隆或切换版本之后,用它确认“现在这个目录里到底是哪一版代码”。

数据流:输入是本地仓库目录和超时时间。它在该目录里运行 git rev-parse HEAD,检查命令是否成功,然后把标准输出转成文字并去掉空白;如果结果为空,就返回错误,否则返回提交号字符串。

调用关系:它只被 clone_git_source 调用,位于下载流程的最后一步。它仍然通过 git_command 生成 Git 命令,通过 run_git_command_with_timeout 避免卡死,通过 ensure_git_success 统一处理失败。

调用图:调用 3 个内部函数(ensure_git_success, git_command, run_git_command_with_timeout);被 1 处调用(clone_git_source);外部调用 1 个(from_utf8_lossy)。

is_full_git_sha132–134 ↗
fn is_full_git_sha(value: &str) -> bool

作用:判断一段文字是不是完整的 Git 提交号。完整提交号通常是 40 个十六进制字符,也就是 0-9 和 a-f/A-F。

数据流:输入是一段字符串。它检查长度是否正好为 40,并且每个字符是不是十六进制字符;满足就返回 true,不满足就返回 false,不修改任何外部东西。

调用关系:它被 git_remote_revision 用在最开头。这样如果调用者已经给了明确的提交号,就不必再联网执行 git ls-remote。测试 tests::full_git_sha_ref_is_already_a_remote_revision 会验证这个判断。

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

git_command136–142 ↗
fn git_command() -> Command

作用:创建一个统一配置好的 git 命令模板。它让后续所有 Git 调用都从系统路径里找 git,并且尽量不弹出交互式提示。

数据流:它不需要输入。它创建 Command::new("git"),并设置两个环境变量:GIT_OPTIONAL_LOCKS=0 用来减少可选锁带来的干扰,GIT_TERMINAL_PROMPT=0 用来禁止终端里询问用户名密码;最后返回这个还没执行的命令对象。

调用关系:git_remote_revision、clone_git_source 和 git_worktree_revision 都用它来起步创建 Git 命令。测试 tests::git_command_uses_path_lookup_with_stable_noninteractive_env 会检查它确实使用 git 名称查找,并带上稳定的非交互环境设置。

调用图:被 4 处调用(clone_git_source, git_remote_revision, git_worktree_revision, git_command_uses_path_lookup_with_stable_noninteractive_env);外部调用 1 个(new)。

git_path_arg152–154 ↗
fn git_path_arg(path: &Path) -> PathBuf

作用:把本地路径转换成适合传给 git 命令的路径参数。它主要是为 Windows 上一种特殊长路径格式兜底,避免 Git 看不懂。

数据流:输入是一个 Path。普通系统上它直接复制这个路径返回;Windows 上它会先把路径转成文字,尝试去掉类似 \\?\ 这样的特殊前缀,如果能去掉就返回改写后的路径,否则返回原路径。

调用关系:它被 clone_git_source 在克隆前调用,用来准备目标目录参数。在 Windows 或测试环境下,它会把具体的前缀处理交给 strip_windows_verbatim_path_prefix。

调用图:调用 1 个内部函数(strip_windows_verbatim_path_prefix);被 1 处调用(clone_git_source);外部调用 2 个(to_path_buf, to_string_lossy)。

strip_windows_verbatim_path_prefix157–164 ↗
fn strip_windows_verbatim_path_prefix(path: &str) -> Option<String>

作用:去掉 Windows 路径里的“verbatim”前缀,也就是类似 \\?\ 的特殊写法,让路径更像 Git 通常能识别的样子。

数据流:输入是一段路径文字。它先检查是否以 \\?\ 开头;没有就返回 None,表示不用改。有的话会去掉这个前缀;如果后面是 UNC 网络路径格式,还会改回 \\server\share 这种常见写法;最后返回改好的路径文字。

调用关系:它被 git_path_arg 使用,是 Windows 路径兼容的小零件。相关测试 tests::strips_windows_verbatim_disk_prefix_for_git、tests::strips_windows_verbatim_unc_prefix_for_git 和 tests::leaves_non_verbatim_path_without_rewrite 分别检查磁盘路径、网络路径和无需改写的路径。

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

run_git_command_with_timeout166–207 ↗
fn run_git_command_with_timeout(
    command: &mut Command,
    context: &str,
    timeout: Duration,
) -> Result<Output, String>

作用:运行一个外部 Git 命令,并保证它不会无限卡住。超时后它会杀掉进程,并把错误输出一起写进错误信息里,方便排查。

数据流:输入是准备好的 Command、这次命令的人类可读说明 context,以及超时时间。它启动子进程,关闭标准输入,收集标准输出和标准错误;然后每 100 毫秒查看进程是否结束。正常结束就返回 Output;如果启动、等待或轮询失败就返回错误;如果超过时间还没结束,就杀掉进程,读取 stderr,并返回“某某命令超时”的错误。

调用关系:git_remote_revision、clone_git_source 和 git_worktree_revision 都把实际执行 Git 的工作交给它。它是这个文件里的安全阀,保证网络慢、认证卡住或 Git 异常时,升级流程不会永远挂在那里。

调用图:被 3 处调用(clone_git_source, git_remote_revision, git_worktree_revision);外部调用 8 个(from_millis, null, piped, from_utf8_lossy, stdin, format!, sleep, now)。

ensure_git_success209–222 ↗
fn ensure_git_success(output: &Output, context: &str) -> Result<(), String>

作用:检查 Git 命令的退出状态是不是成功,并把失败原因整理成好懂的错误信息。

数据流:输入是 Git 命令的 Output 和本次命令的 context。它先看退出状态是否成功;成功就返回 Ok。失败时,它读取 stderr;如果 stderr 为空,就只报告状态码,如果不为空,就把状态码和 Git 输出的错误文字一起返回。

调用关系:git_remote_revision、clone_git_source 和 git_worktree_revision 在每次 Git 命令执行完后都会调用它。run_git_command_with_timeout 只负责“命令跑完没跑完”,而它负责判断“跑完的结果是不是成功”。

调用图:被 3 处调用(clone_git_source, git_remote_revision, git_worktree_revision);外部调用 2 个(from_utf8_lossy, format!)。

tests::full_git_sha_ref_is_already_a_remote_revision233–237 ↗
fn full_git_sha_ref_is_already_a_remote_revision()

作用:测试完整 Git 提交号的识别规则是否正确。它确保 40 位十六进制字符串会被当成明确版本,而普通分支名或短号不会被误判。

数据流:输入是测试里写死的几个字符串。它调用 is_full_git_sha 做判断,然后用断言确认完整提交号返回 true,main 和短提交号返回 false;测试不产生业务输出,只在失败时让测试框架报错。

调用关系:这是 is_full_git_sha 的单元测试。它保护 git_remote_revision 的快捷路径,避免未来改动导致已经明确的提交号还被拿去查远端。

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

tests::git_command_uses_path_lookup_with_stable_noninteractive_env240–253 ↗
fn git_command_uses_path_lookup_with_stable_noninteractive_env()

作用:测试 git_command 创建的命令是否符合预期:使用系统里的 git,并设置好不弹交互提示的环境变量。

数据流:它调用 git_command 得到一个命令对象,然后检查程序名是 git,检查 GIT_OPTIONAL_LOCKS 和 GIT_TERMINAL_PROMPT 的值都是 0,并确认没有额外改写 PATH。结果通过断言表达,断言失败就说明命令模板被破坏了。

调用关系:它直接覆盖 git_command,并使用 tests::command_env 读取命令对象里的环境变量。这个测试能防止 Git 调用在自动升级时突然需要人工输入或找 git 的方式发生意外变化。

调用图:调用 1 个内部函数(git_command);外部调用 1 个(assert_eq!)。

tests::strips_windows_verbatim_disk_prefix_for_git256–261 ↗
fn strips_windows_verbatim_disk_prefix_for_git()

作用:测试 Windows 普通磁盘路径里的 \\?\ 前缀会被正确去掉。这样 C 盘这类路径传给 Git 时更稳妥。

数据流:输入是测试里写死的 \\?\C:\... 路径。它调用 strip_windows_verbatim_path_prefix,期待得到 C:\... 形式的路径文字;如果结果不同,断言失败。

调用关系:这是 strip_windows_verbatim_path_prefix 的测试之一,间接保护 git_path_arg 在 Windows 上给 clone_git_source 准备路径参数的行为。

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

tests::strips_windows_verbatim_unc_prefix_for_git264–269 ↗
fn strips_windows_verbatim_unc_prefix_for_git()

作用:测试 Windows 网络共享路径里的特殊前缀会被改成常见 UNC 路径格式。UNC 可以理解为类似 \\服务器\共享目录 的网络路径。

数据流:输入是 \\?\UNC\server\share\marketplace 形式的路径。它调用 strip_windows_verbatim_path_prefix,期待输出 \\server\share\marketplace;不同就让断言失败。

调用关系:这是 strip_windows_verbatim_path_prefix 的网络路径测试。它确保 git_path_arg 不只照顾本机磁盘路径,也照顾网络共享目录。

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

tests::leaves_non_verbatim_path_without_rewrite272–274 ↗
fn leaves_non_verbatim_path_without_rewrite()

作用:测试普通 Windows 路径不会被乱改。只有带特殊前缀的路径才应该被处理。

数据流:输入是 C:\Users\alice 这种普通路径。它调用 strip_windows_verbatim_path_prefix,期待得到 None,意思是“不需要改写”;如果返回了别的内容,断言失败。

调用关系:这是 strip_windows_verbatim_path_prefix 的边界测试。它防止路径清理逻辑过度积极,影响 git_path_arg 传给 Git 的正常路径。

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

tests::command_env276–284 ↗
fn command_env(
        command: &'a std::process::Command,
        name: &str,
    ) -> Option<Option<&'a OsStr>>

作用:在测试里读取 Command 对象上某个环境变量的设置。它是测试辅助函数,不参与实际升级流程。

数据流:输入是一个 Command 引用和环境变量名字。它遍历命令对象里显式设置过的环境变量,找到名字匹配的那一项并返回它的值;找不到就返回 None。

调用关系:它被 tests::git_command_uses_path_lookup_with_stable_noninteractive_env 使用,用来检查 git_command 设置了哪些环境变量。它只是测试工具,不会被生产代码调用。

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

core-plugins/src/marketplace_upgrade.rs源码 ↗
orchestrationstartup / marketplace auto-upgrade

这里的“插件市场”可以理解成一包插件目录;Git 是常见的代码仓库工具,用来从远程地址取最新内容。这个文件先从用户配置里找出哪些市场是 Git 来源,再逐个检查远程仓库有没有新版本。如果本地已经是同一版本,并且安装记录也对得上,就不动它;否则先下载到临时“候场区”,验证里面确实是合法市场、名字也和配置一致,再写入安装记录。最后它才把临时目录切换成正式目录,并把新版本号写回 config.toml。这样做像先把新货验收好,再上架,避免半成品直接覆盖正在用的市场。

函数细节9
ConfiguredMarketplaceUpgradeOutcome::all_succeeded51–53 ↗
fn all_succeeded(&self) -> bool

作用:这个小方法用来快速判断这次市场升级是不是全都成功了。只要错误列表是空的,它就认为没有失败。

数据流:进去的是一次升级结果对象,里面有已选择的市场、已升级路径和错误列表。它只看错误列表有没有内容,不改任何数据。出来的是一个布尔值:true 表示没有错误,false 表示至少有一个市场升级失败。

调用关系:它通常在外层流程拿到 ConfiguredMarketplaceUpgradeOutcome 之后使用,用来决定要不要向用户报告失败,或者继续后面的步骤。它不调用别的函数,也不触发升级。

configured_git_marketplace_names56–63 ↗
fn configured_git_marketplace_names(config_layer_stack: &ConfigLayerStack) -> Vec<String>

作用:这个函数列出当前配置里所有 Git 来源的插件市场名字。外层可以用它告诉用户“哪些市场会参与自动升级”。

数据流:进去的是配置层叠对象,也就是系统把默认配置、用户配置等合在一起后的视图。它先取出 Git 市场列表,再只留下名字,并按字母顺序排好。出来的是一个干净、有序的名字列表。

调用关系:它把真正读取和筛选配置的活交给 configured_git_marketplaces。调用图里显示,upgrade_configured_marketplaces_for_config 会用它来了解配置里有哪些 Git 市场。

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

upgrade_configured_git_marketplaces65–103 ↗
fn upgrade_configured_git_marketplaces(
    codex_home: &Path,
    config_layer_stack: &ConfigLayerStack,
    marketplace_name: Option<&str>,
) -> ConfiguredMarketplaceUpgradeOutcome

作用:这是批量升级入口:根据配置找到要升级的 Git 市场,可以升级全部,也可以只升级指定名字的一个。它会把成功和失败都收集起来,而不是遇到一个失败就让整个流程崩掉。

数据流:进去的是 Codex 的家目录、配置层叠对象,以及可选的市场名字。它读取配置、筛选目标市场、算出安装根目录,然后逐个调用单个市场升级函数。出来的是 ConfiguredMarketplaceUpgradeOutcome:包含选中了哪些市场、哪些目录真的升级了、哪些市场报错;同时它可能会改动磁盘上的市场目录和用户 config.toml。

调用关系:它是外层 upgrade_configured_marketplaces_for_config 调进来的主干流程。它先用 configured_git_marketplaces 找目标,再用 marketplace_install_root 定位安装位置,最后把每个市场交给 upgrade_configured_git_marketplace 做实际下载、验证和启用。

调用图:调用 3 个内部函数(configured_git_marketplaces, marketplace_install_root, upgrade_configured_git_marketplace);被 1 处调用(upgrade_configured_marketplaces_for_config);外部调用 2 个(new, default)。

marketplace_install_root105–107 ↗
fn marketplace_install_root(codex_home: &Path) -> PathBuf

作用:这个函数统一规定自动安装的插件市场放在哪里。这样其他代码不用各自拼路径,避免装到不同地方。

数据流:进去的是 Codex 家目录路径。它在后面接上固定目录“.tmp/marketplaces”。出来的是市场安装根目录路径,不会创建目录,也不会访问磁盘。

调用关系:它被 upgrade_configured_git_marketplaces 调用,用来给后续每个市场升级提供共同的安装位置。真正创建目录和写文件是在后面的升级函数里做。

调用图:被 1 处调用(upgrade_configured_git_marketplaces);外部调用 1 个(join)。

configured_git_marketplaces109–135 ↗
fn configured_git_marketplaces(
    config_layer_stack: &ConfigLayerStack,
) -> Vec<ConfiguredGitMarketplace>

作用:这个函数从用户配置里挑出“来源类型是 Git”的插件市场,并整理成内部好用的格式。没有配置、配置格式不对,都会安全地返回空列表或跳过,并记录警告。

数据流:进去的是配置层叠对象。它读取生效的用户配置,找 marketplaces 这一段,把它转换成市场配置表,再过滤出 Git 来源的条目并排序。出来的是 ConfiguredGitMarketplace 列表,里面包含名字、仓库地址、分支或标签、只下载哪些路径、上次版本号等信息;它不修改配置文件。

调用关系:它是“配置变成升级任务”的入口,被 configured_git_marketplace_names 用来列名字,也被 upgrade_configured_git_marketplaces 用来决定真正要升级哪些市场。遇到坏配置时,它只发出 warn! 警告,不把整个升级流程打断。

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

configured_git_marketplace_from_config137–166 ↗
fn configured_git_marketplace_from_config(
    name: String,
    marketplace: MarketplaceConfig,
) -> Option<ConfiguredGitMarketplace>

作用:这个函数把单个市场配置翻译成内部的 Git 市场记录。它会排除不是 Git 来源的市场,也会拒绝没有仓库地址的 Git 市场。

数据流:进去的是市场名字和一份 MarketplaceConfig。它拆出来源类型、仓库地址、版本引用、稀疏路径和上次版本号;如果来源不是 Git,就返回 None;如果是 Git 但没有 source 地址,也返回 None 并写警告。出来的是 Some(ConfiguredGitMarketplace) 或 None。

调用关系:它是配置解析里的小转换器。read_configured_git_marketplace 会用它把刚从 config.toml 读出的单个市场重新转成可比较的内部记录,用来确认升级过程中配置没有被别人改掉。

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

upgrade_configured_git_marketplace168–243 ↗
fn upgrade_configured_git_marketplace(
    codex_home: &Path,
    install_root: &Path,
    marketplace: &ConfiguredGitMarketplace,
) -> Result<Option<AbsolutePathBuf>, String>

作用:这是单个 Git 插件市场升级的核心流程。它负责检查远程版本、下载、验证、写安装记录、切换目录,并更新用户配置。

数据流:进去的是 Codex 家目录、安装根目录和一个市场的内部配置。它先验证市场名能安全当目录名使用,再问远程 Git 仓库当前版本号;如果本地清单存在、版本号一样、安装元数据也匹配,就直接返回 Ok(None)。否则它创建临时目录,克隆仓库内容,验证下载来的目录确实是合法市场且名字正确,写入安装元数据,然后在启用前再次确认用户配置没变,最后记录新版本到 config.toml 并把临时目录激活成正式目录。出来的是 Ok(Some(绝对路径)) 表示真的升级了,Ok(None) 表示无需升级,Err(文字) 表示失败原因;过程中会读写磁盘、访问 Git、修改配置。

调用关系:它由 upgrade_configured_git_marketplaces 逐个调用。它把不同步骤分给专门函数:git_remote_revision 查远程版本,clone_git_source 下载,validate_marketplace_root 验证市场目录,installed_marketplace_metadata_matches 判断本地是否已匹配,write_installed_marketplace_metadata 写安装记录,activate_marketplace_root 做最终切换;在切换前还通过 ensure_configured_git_marketplace_unchanged 防止配置被并发改动。

调用图:调用 8 个内部函数(find_marketplace_manifest_path, validate_marketplace_root, activate_marketplace_root, installed_marketplace_metadata_matches, write_installed_marketplace_metadata, clone_git_source, git_remote_revision, try_from);被 1 处调用(upgrade_configured_git_marketplaces);外部调用 6 个(join, now, validate_plugin_segment, format!, create_dir_all, new)。

ensure_configured_git_marketplace_unchanged244–260 ↗
fn ensure_configured_git_marketplace_unchanged(
    codex_home: &Path,
    expected: &ConfiguredGitMarketplace,
) -> Result<(), String>

作用:这个函数是升级前最后一道保险:确认用户配置里的这个市场,和开始升级时看到的是同一份。它防止“下载过程中用户改了配置,但旧任务还把结果写回去”的问题。

数据流:进去的是 Codex 家目录和预期的市场配置。它重新读取当前 config.toml 里的同名 Git 市场;如果当前配置和预期完全一样,就返回成功;如果变了、删了,或不再是 Git 市场,就返回错误文字。它只读取配置,不写入文件。

调用关系:upgrade_configured_git_marketplace 在激活新目录、写回配置之前会调用它。它把读取当前配置的细节交给 read_configured_git_marketplace,自己只负责把“当前”和“预期”做比较并给出清楚的失败原因。

调用图:调用 1 个内部函数(read_configured_git_marketplace);外部调用 1 个(format!)。

read_configured_git_marketplace262–297 ↗
fn read_configured_git_marketplace(
    codex_home: &Path,
    marketplace_name: &str,
) -> Result<Option<ConfiguredGitMarketplace>, String>

作用:这个函数直接从用户的 config.toml 里读取某个市场当前的 Git 配置。它用于获得“此刻磁盘上的真实配置”,而不是之前缓存的配置视图。

数据流:进去的是 Codex 家目录和市场名字。它拼出 config.toml 路径,读取文本;文件不存在就返回 Ok(None),读取失败或 TOML 格式解析失败就返回错误。解析成功后,它找 marketplaces 表,取出指定市场,并用 configured_git_marketplace_from_config 转成内部记录。出来的是 Ok(Some(市场记录))、Ok(None) 或 Err(错误文字);它不修改磁盘。

调用关系:它被 ensure_configured_git_marketplace_unchanged 调用,用来在升级临门一脚前重新核对用户配置。它负责底层的文件读取和 TOML 解析,解析出的单个市场再交给 configured_git_marketplace_from_config 判断是不是有效的 Git 市场。

调用图:调用 1 个内部函数(configured_git_marketplace_from_config);被 1 处调用(ensure_configured_git_marketplace_unchanged);外部调用 4 个(join, format!, read_to_string, from_str)。

core-plugins/src/marketplace_remove.rs源码 ↗
domain_logicrequest handling

可以把 marketplace 理解成一个插件货架。这个文件负责把某个货架从用户家目录里干净移走:先检查名字是否合法,防止奇怪路径或危险名字;再从用户的 config.toml 配置文件里删掉对应记录;然后去本地安装位置删除实际文件夹或文件。它还很小心:如果配置里的名字大小写和用户输入不完全一致,就拒绝删除,避免误删;如果配置文件坏了,删配置失败,就不会继续删安装目录,防止只删一半。对外的 remove_marketplace 是异步接口,会把真正的磁盘删除工作交给阻塞任务执行,避免卡住主运行流程。

函数细节9
remove_marketplace29–38 ↗
async fn remove_marketplace(
    codex_home: PathBuf,
    request: MarketplaceRemoveRequest,
) -> Result<MarketplaceRemoveOutcome, MarketplaceRemoveError>

作用:这是给外部调用的异步删除入口。有人发起“移除 marketplace”命令时,会走这里;它把可能较慢的磁盘操作放到后台阻塞任务里做,避免拖慢异步主流程。

数据流:输入是 Codex 的用户目录路径和要删除的 marketplace 名字 → 它把这些交给同步版本 remove_marketplace_sync 去真正检查配置、删配置、删文件 → 输出成功结果,或把后台任务失败包装成 MarketplaceRemoveError::Internal 错误。

调用关系:它由 run_remove 在执行删除命令时调用。它自己不直接碰配置和文件,而是通过 spawn_blocking 把活交给 remove_marketplace_sync,这样命令层可以继续用异步方式等待结果。

调用图:被 1 处调用(run_remove);外部调用 1 个(spawn_blocking)。

remove_marketplace_sync40–74 ↗
fn remove_marketplace_sync(
    codex_home: &Path,
    request: MarketplaceRemoveRequest,
) -> Result<MarketplaceRemoveOutcome, MarketplaceRemoveError>

作用:这是实际执行删除的核心函数。它负责确认名字安全、删除配置记录、删除本地安装目录,并判断这次删除到底有没有删到东西。

数据流:输入是用户目录和删除请求 → 它取出 marketplace_name,先用 validate_plugin_segment 检查名字是否合法;再用 marketplace_install_root 算出本地安装位置;接着调用 remove_user_marketplace_config 删除 config.toml 里的记录;如果大小写不匹配就报错;然后调用 remove_marketplace_root 删除安装目录或文件 → 输出 MarketplaceRemoveOutcome,里面说明删的是哪个 marketplace,以及是否删掉了本地安装根目录;如果既没配置也没安装,就返回“找不到”的请求错误。

调用关系:remove_marketplace 会把实际工作交给它。它是整个删除流程的中轴:上游给它请求,下游它分别调用配置删除函数和 remove_marketplace_root。测试函数也直接调用它,验证各种正常和异常场景。

调用图:调用 2 个内部函数(marketplace_install_root, remove_marketplace_root);被 6 处调用(remove_marketplace_sync_keeps_installed_root_when_config_removal_fails, remove_marketplace_sync_rejects_case_mismatched_configured_name, remove_marketplace_sync_rejects_unknown_marketplace, remove_marketplace_sync_removes_config_and_installed_root, remove_marketplace_sync_removes_file_installed_root, remove_marketplace_sync_removes_inline_config_entry);外部调用 4 个(remove_user_marketplace_config, validate_plugin_segment, InvalidRequest, format!)。

remove_marketplace_root76–105 ↗
fn remove_marketplace_root(root: &Path) -> Result<Option<AbsolutePathBuf>, MarketplaceRemoveError>

作用:这个函数只负责删除本地安装位置。它不管配置文件,只看指定路径是否存在,存在就把它删掉,并告诉调用者删掉的是哪个绝对路径。

数据流:输入是一个可能存在的安装路径 → 如果路径不存在,直接返回 None;如果存在,先转成 AbsolutePathBuf(绝对路径,避免含糊的相对位置),再读取路径信息;如果它是目录就递归删除整个目录,如果是普通文件就删除文件 → 输出 Some(被删除的绝对路径),或在检查、转换、删除失败时返回内部错误。

调用关系:它只被 remove_marketplace_sync 调用,是删除流程里的“清扫本地文件”零件。remove_marketplace_sync 先处理配置,再叫它清理安装根目录。

调用图:调用 1 个内部函数(try_from);被 1 处调用(remove_marketplace_sync);外部调用 5 个(exists, to_path_buf, remove_dir_all, remove_file, symlink_metadata)。

tests::remove_marketplace_sync_removes_config_and_installed_root116–156 ↗
fn remove_marketplace_sync_removes_config_and_installed_root()

作用:这个测试确认最常见的成功情况:配置里有记录,本地也有安装目录时,删除后两边都干净了。

数据流:它先创建临时用户目录,写入一个 debug marketplace 配置,再造出对应安装目录和文件 → 调用 remove_marketplace_sync → 检查返回结果里的名字和被删路径正确,同时确认 config.toml 不再包含 debug 配置,安装目录也不存在了。

调用关系:这是对 remove_marketplace_sync 的主成功路径测试。它会用 record_user_marketplace 准备配置,用 marketplace_install_root 找安装位置,然后验证核心删除函数确实同时清理配置和磁盘。

调用图:调用 2 个内部函数(marketplace_install_root, remove_marketplace_sync);外部调用 7 个(new, assert!, assert_eq!, record_user_marketplace, create_dir_all, read_to_string, write)。

tests::remove_marketplace_sync_rejects_unknown_marketplace159–174 ↗
fn remove_marketplace_sync_rejects_unknown_marketplace()

作用:这个测试确认:如果用户要删的 marketplace 既没有配置,也没有安装目录,程序会明确报错,而不是假装成功。

数据流:它创建一个空的临时用户目录 → 调用 remove_marketplace_sync 删除 debug → 得到错误 → 检查错误文字是“debug 没有配置或安装”。

调用关系:它直接测试 remove_marketplace_sync 的兜底判断:配置没删到,本地目录也没删到时,应该返回 InvalidRequest,而不是静默通过。

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

tests::remove_marketplace_sync_rejects_case_mismatched_configured_name177–211 ↗
fn remove_marketplace_sync_rejects_case_mismatched_configured_name()

作用:这个测试确认大小写必须完全一致。比如配置里是 debug,用户输入 Debug,程序会拒绝,避免在大小写敏感和不敏感的系统上产生误删。

数据流:它先写入名为 debug 的配置,并创建 debug 安装目录 → 调用 remove_marketplace_sync 时故意传入 Debug → 得到错误 → 再检查安装目录还在,配置也还在。

调用关系:它验证 remove_marketplace_sync 处理 remove_user_marketplace_config 返回的 NameCaseMismatch 分支时,会停止流程,不会继续调用删除安装目录造成副作用。

调用图:调用 2 个内部函数(marketplace_install_root, remove_marketplace_sync);外部调用 6 个(new, assert!, assert_eq!, record_user_marketplace, create_dir_all, read_to_string)。

tests::remove_marketplace_sync_keeps_installed_root_when_config_removal_fails214–237 ↗
fn remove_marketplace_sync_keeps_installed_root_when_config_removal_fails()

作用:这个测试确认一个安全保护:如果配置文件坏了,配置删除失败,就不能继续删本地安装目录。

数据流:它创建临时用户目录,写入一份格式错误的 config.toml,再创建 debug 安装目录 → 调用 remove_marketplace_sync → 得到包含“删除配置失败”的错误 → 检查安装目录仍然存在。

调用关系:它测试 remove_marketplace_sync 的顺序很关键:先删配置,配置失败就立刻返回错误,不会把工作交给 remove_marketplace_root。

调用图:调用 2 个内部函数(marketplace_install_root, remove_marketplace_sync);外部调用 4 个(new, assert!, create_dir_all, write)。

tests::remove_marketplace_sync_removes_file_installed_root240–280 ↗
fn remove_marketplace_sync_removes_file_installed_root()

作用:这个测试确认安装根位置就算不是目录、而是一个异常的普通文件,也能被删除。这样可以清理损坏或不完整的安装状态。

数据流:它先写入 debug 配置,然后在本该是安装目录的位置写入一个普通文件 → 调用 remove_marketplace_sync → 检查返回结果包含被删路径,文件已经不存在,配置记录也被移除了。

调用关系:它覆盖 remove_marketplace_root 的文件删除分支。remove_marketplace_sync 调用它时,不要求安装根必须是正常目录,也能清掉坏状态。

调用图:调用 2 个内部函数(marketplace_install_root, remove_marketplace_sync);外部调用 7 个(new, assert!, assert_eq!, record_user_marketplace, create_dir_all, read_to_string, write)。

tests::remove_marketplace_sync_removes_inline_config_entry283–312 ↗
fn remove_marketplace_sync_removes_inline_config_entry()

作用:这个测试确认配置写成内联形式时也能删除。内联形式就是把 marketplace 配置写在一行里的紧凑写法。

数据流:它手动写入一段内联 marketplace 配置,再创建对应安装目录 → 调用 remove_marketplace_sync → 检查返回名字和删除路径正确,安装目录没了,配置文件里也不再出现 debug。

调用关系:它验证 remove_marketplace_sync 依赖的 remove_user_marketplace_config 不只会删普通表格形式,也能处理内联配置;核心删除流程不需要关心配置具体写法。

调用图:调用 2 个内部函数(marketplace_install_root, remove_marketplace_sync);外部调用 6 个(new, assert!, assert_eq!, create_dir_all, read_to_string, write)。

远程包与共享

这些文件实现远程插件传输、包安装与同步、旧后端兼容,以及工作区插件共享和 checkout 流程。

core-plugins/src/remote_bundle.rs源码 ↗
domain_logic插件安装、远程插件同步、远程插件检出时活跃

远程插件不是直接信任后端一句话就装上去。这个文件先把后端返回的插件名、版本号、下载地址整理成一份已验证的安装计划;只接受 HTTPS 下载地址,调试测试时才允许本机 HTTP。真正下载时,它会限制等待时间和文件大小,失败时只读取一小段错误正文,防止服务端返回巨量内容。下载完成后,它把压缩包放到临时目录里解开,确认根目录里有 plugin.json,也就是插件的“身份证”。全局远程市场的插件还会用后端给的版本和应用清单覆盖包里的内容,保证本地看到的信息和远端一致。最后它通过 PluginStore(插件仓库,本地保存已安装插件的地方)完成安装。文件末尾的测试用各种坏包、超大包、路径穿越等情况,确保这个流程不会轻易被绕过。

函数细节40
RemotePluginBundleInstallError::io131–133 ↗
fn io(context: &'static str, source: io::Error) -> Self

作用:把普通的文件读写错误包装成这个文件统一使用的远程插件安装错误。这样上层看到错误时,会知道是哪一步文件操作出了问题。

数据流:进去的是一段固定的错误背景说明和系统返回的 io 错误 → 函数把它们装进 RemotePluginBundleInstallError::Io → 出来的是带上下文的统一错误值,不改动文件。

调用关系:它是很多磁盘操作失败时的“翻译器”。创建目录、读取清单、写 JSON、重命名目录等地方都会借它把底层错误变成安装流程能理解的错误。

validate_remote_plugin_bundle136–196 ↗
fn validate_remote_plugin_bundle(
    remote_plugin_id: &str,
    remote_marketplace_name: &str,
    plugin_name: &str,
    release_version: Option<&str>,
    bundle_download_url: Option<&str>,
    ap

作用:把后端返回的远程插件信息先验一遍,确认插件名、版本号和下载地址都能安全使用。只有通过这里的检查,后面才会真的下载和安装。

数据流:进去的是远程插件编号、市场名、插件名、可选版本号、可选下载地址和可选应用清单 → 它生成本地插件 ID,清理并检查版本号,解析下载地址,并确认地址协议安全 → 出来的是 ValidatedRemotePluginBundle,也就是一份可以继续执行的安装计划;如果缺字段或不安全,就返回明确错误。

调用关系:它通常在远程安装响应、同步远程已安装插件、检出共享插件时先被调用。它会借 validate_plugin_version_segment 检查版本号,借 is_allowed_bundle_download_url 判断链接能不能用。后续的下载安装函数只接收它产出的“已验证包信息”。

调用图:调用 4 个内部函数(allow_test_loopback_http_bundle_downloads, is_allowed_bundle_download_url, validate_plugin_version_segment, new);被 10 处调用(remote_plugin_install_response, sync_remote_installed_plugin_bundles_once, checkout_remote_plugin_share, install_preserves_non_global_bundle_manifest_metadata, valid_remote_plugin_bundle, validate_remote_plugin_bundle_rejects_invalid_release_version, validate_remote_plugin_bundle_rejects_missing_download_url, validate_remote_plugin_bundle_rejects_missing_release_version, validate_remote_plugin_bundle_rejects_unsupported_download_url_scheme, validate_remote_plugin_bundle_uses_detail_name_for_local_plugin_id);外部调用 1 个(parse)。

allow_test_loopback_http_bundle_downloads198–207 ↗
fn allow_test_loopback_http_bundle_downloads() -> bool

作用:判断测试环境里是否临时允许从本机 HTTP 地址下载插件包。平时返回 false,避免普通明文 HTTP 下载带来风险。

数据流:进去没有参数 → 在调试构建下读取环境变量 CODEX_TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS → 如果值是 1 就返回 true,否则返回 false;不修改任何状态。

调用关系:它被 validate_remote_plugin_bundle 和 download_remote_plugin_bundle_with_limit 调用。前者用它判断初始下载地址,后者用它判断跳转后的最终地址,保证测试方便但正式场景安全。

调用图:被 2 处调用(download_remote_plugin_bundle_with_limit, validate_remote_plugin_bundle);外部调用 1 个(var)。

is_allowed_bundle_download_url209–215 ↗
fn is_allowed_bundle_download_url(url: &Url, allow_loopback_http: bool) -> bool

作用:判断一个插件包下载地址是否允许使用。规则很简单:HTTPS 永远允许;HTTP 只在测试开关打开且地址是本机时允许。

数据流:进去的是一个 URL 和是否允许本机 HTTP 的布尔值 → 它查看 URL 的协议,如果是 HTTP 还会检查是不是 localhost 或回环 IP → 出来 true 或 false,不产生副作用。

调用关系:它是下载地址安全检查的核心小门卫。validate_remote_plugin_bundle 用它挡住后端给来的坏地址,download_remote_plugin_bundle_with_limit 用它挡住重定向后的坏地址。

调用图:调用 1 个内部函数(is_loopback_url);被 2 处调用(download_remote_plugin_bundle_with_limit, validate_remote_plugin_bundle);外部调用 1 个(scheme)。

is_loopback_url217–224 ↗
fn is_loopback_url(url: &Url) -> bool

作用:判断一个 URL 是否指向本机。这里的“本机”包括 localhost、127.0.0.1 这类 IPv4 回环地址和 ::1 这类 IPv6 回环地址。

数据流:进去的是一个 URL → 它读取主机名部分并分类判断 → 出来 true 表示只访问本机,false 表示不是本机或没有主机名。

调用关系:它只被 is_allowed_bundle_download_url 使用。作用是让测试时的 HTTP 例外足够窄,只放行本机地址,而不是放行互联网上的明文 HTTP。

调用图:被 1 处调用(is_allowed_bundle_download_url);外部调用 1 个(host)。

download_and_install_remote_plugin_bundle226–244 ↗
async fn download_and_install_remote_plugin_bundle(
    codex_home: PathBuf,
    bundle: ValidatedRemotePluginBundle,
) -> Result<PluginInstallResult, RemotePluginBundleInstallError>

作用:完成“下载远程插件包并安装”的完整动作。它把网络下载和本地解压安装串起来,是上层安装远程插件时会调用的主入口之一。

数据流:进去的是 Codex 主目录路径和已验证的插件包信息 → 它先按大小限制下载压缩包字节,再把耗时的解压和安装放到阻塞任务里执行 → 出来的是 PluginInstallResult,里面说明装到了哪里、版本是什么;失败则返回安装错误。

调用关系:它被远程插件安装响应和远程同步流程调用。它先交给 download_remote_plugin_bundle_with_limit 做网络下载,再交给 install_remote_plugin_bundle 做本地落盘和注册。

调用图:调用 1 个内部函数(download_remote_plugin_bundle_with_limit);被 2 处调用(remote_plugin_install_response, sync_remote_installed_plugin_bundles_once);外部调用 1 个(spawn_blocking)。

download_and_extract_remote_plugin_bundle_to_path246–264 ↗
async fn download_and_extract_remote_plugin_bundle_to_path(
    bundle: ValidatedRemotePluginBundle,
    destination: AbsolutePathBuf,
) -> Result<AbsolutePathBuf, RemotePluginBundleInstallError>

作用:把远程插件包下载下来,并解压到指定目录,但不走普通安装仓库流程。它适合“检出”共享插件这种需要放到某个固定位置的场景。

数据流:进去的是已验证的插件包信息和目标绝对路径 → 它下载压缩包,再在后台阻塞任务中解压、检查并移动到目标目录 → 出来的是目标路径;如果目标已存在或包不合规就报错。

调用关系:它被 checkout_remote_plugin_share 使用。它和安装入口一样先下载,但后半段交给 extract_remote_plugin_bundle_to_path,而不是 PluginStore。

调用图:调用 1 个内部函数(download_remote_plugin_bundle_with_limit);被 1 处调用(checkout_remote_plugin_share);外部调用 1 个(spawn_blocking)。

download_remote_plugin_bundle_with_limit266–307 ↗
async fn download_remote_plugin_bundle_with_limit(
    bundle_download_url: &str,
    max_bytes: u64,
) -> Result<Vec<u8>, RemotePluginBundleInstallError>

作用:真正执行 HTTP 请求下载插件压缩包,并严格限制超时、地址安全和文件大小。这样远端服务器就不能无限拖延或塞一个特别大的文件过来。

数据流:进去的是下载 URL 和最大字节数 → 它创建网络客户端,发 GET 请求,检查重定向后的最终地址和 HTTP 状态;失败状态会读取有限长度的错误正文;成功时读取正文并持续检查大小 → 出来是完整的压缩包字节数组。

调用关系:它被两个高层函数共用:一个用于下载后安装,一个用于下载后检出。它内部调用 read_response_body_with_limit 读响应体,并使用 is_allowed_bundle_download_url 复查跳转后的地址。

调用图:调用 4 个内部函数(allow_test_loopback_http_bundle_downloads, is_allowed_bundle_download_url, read_response_body_with_limit, build_reqwest_client);被 2 处调用(download_and_extract_remote_plugin_bundle_to_path, download_and_install_remote_plugin_bundle);外部调用 1 个(from_utf8_lossy)。

read_response_body_with_limit309–334 ↗
async fn read_response_body_with_limit(
    mut response: Response,
    url: &str,
    max_bytes: u64,
) -> Result<Vec<u8>, RemotePluginBundleInstallError>

作用:一边读取网络响应内容,一边检查是否超过大小上限。它防止服务器谎报或不报文件大小时,程序把内存吃爆。

数据流:进去的是网络响应、对应 URL 和最大字节数 → 它先看响应头里的 content-length,如果有就先检查;然后一块一块读取内容,每读一块都累加长度并检查 → 出来是读完的字节数组,或在超限/读取失败时返回错误。

调用关系:它只由 download_remote_plugin_bundle_with_limit 调用。这个函数把“怎么安全读完整响应体”的细活从下载主流程里拆出来。

调用图:调用 1 个内部函数(enforce_download_size_limit);被 1 处调用(download_remote_plugin_bundle_with_limit);外部调用 3 个(new, chunk, content_length)。

enforce_download_size_limit336–348 ↗
fn enforce_download_size_limit(
    url: &str,
    bytes: u64,
    max_bytes: u64,
) -> Result<(), RemotePluginBundleInstallError>

作用:检查某个下载当前大小是否超过允许上限。它是下载大小限制的最小判断单元。

数据流:进去的是 URL、当前字节数和最大字节数 → 它比较当前值和上限 → 如果没超过就返回成功;超过就返回 DownloadTooLarge 错误,不修改任何数据。

调用关系:read_response_body_with_limit 在读响应头和每个数据块后都会调用它。测试 download_size_limit_rejects_oversized_bundle 也直接调用它,确认超限会被拦住。

调用图:被 2 处调用(read_response_body_with_limit, download_size_limit_rejects_oversized_bundle)。

install_remote_plugin_bundle350–385 ↗
fn install_remote_plugin_bundle(
    codex_home: PathBuf,
    bundle: ValidatedRemotePluginBundle,
    bundle_bytes: Vec<u8>,
) -> Result<PluginInstallResult, RemotePluginBundleInstallError>

作用:把已经下载好的插件压缩包解开、检查、必要时修正清单,然后安装进本地插件仓库。它是本地落盘安装的核心流程。

数据流:进去的是 Codex 主目录、已验证插件信息和压缩包字节 → 它创建临时解压目录,解压压缩包,确认根目录有 plugin.json,按需要覆盖版本和应用清单,再创建 PluginStore 并安装指定版本 → 出来是安装结果,同时本地插件目录会发生变化。

调用关系:download_and_install_remote_plugin_bundle 下载完成后会把活交给它。它继续调用 extract_plugin_bundle_tar_gz、find_extracted_plugin_root、prepare_extracted_remote_plugin_root,最后交给 PluginStore 写入正式插件仓库。多个测试会直接调用它验证坏包和元数据行为。

调用图:调用 5 个内部函数(extract_plugin_bundle_tar_gz, find_extracted_plugin_root, prepare_extracted_remote_plugin_root, try_new, try_from);被 3 处调用(install_preserves_non_global_bundle_manifest_metadata, install_rejects_bundle_without_standard_plugin_root, install_rejects_invalid_tar_gz_bundle);外部调用 3 个(join, create_dir_all, new)。

extract_remote_plugin_bundle_to_path387–442 ↗
fn extract_remote_plugin_bundle_to_path(
    bundle: ValidatedRemotePluginBundle,
    bundle_bytes: Vec<u8>,
    destination: AbsolutePathBuf,
) -> Result<AbsolutePathBuf, RemotePluginBundleInstallErr

作用:把插件压缩包解压并移动到一个指定目录,用于检出插件而不是安装到仓库。它会确认目标目录不能已存在,避免覆盖用户已有文件。

数据流:进去的是已验证插件信息、压缩包字节和目标绝对路径 → 它检查目标不存在并创建父目录,在临时目录解压,确认有合法 plugin.json,并检查清单里的 name 和远程插件名一致 → 最后把临时目录改名成目标目录,返回该路径。

调用关系:download_and_extract_remote_plugin_bundle_to_path 在后台任务里调用它。它复用 extract_plugin_bundle_tar_gz 和 find_extracted_plugin_root 的安全解压检查,也会读取插件清单确认身份。

调用图:调用 4 个内部函数(load_plugin_manifest, extract_plugin_bundle_tar_gz, find_extracted_plugin_root, as_path);外部调用 5 个(InvalidBundle, format!, create_dir_all, rename, new)。

prepare_extracted_remote_plugin_root444–457 ↗
fn prepare_extracted_remote_plugin_root(
    plugin_root: &Path,
    bundle: &ValidatedRemotePluginBundle,
) -> Result<(), RemotePluginBundleInstallError>

作用:在插件安装前,对刚解压出来的插件根目录做必要的修正。主要是全局远程市场的插件,需要以服务端返回的信息为准。

数据流:进去的是解压后的插件根目录和已验证插件信息 → 如果市场不是全局远程市场,它什么都不改;如果是,就覆盖 plugin.json 里的 version,并在有应用清单时写入应用清单 → 出来是成功或错误,可能会改写插件目录里的 JSON 文件。

调用关系:install_remote_plugin_bundle 在确认包根目录后调用它。它把具体写清单的事交给 overwrite_plugin_manifest_version 和 overwrite_plugin_app_manifest。

调用图:调用 2 个内部函数(overwrite_plugin_app_manifest, overwrite_plugin_manifest_version);被 1 处调用(install_remote_plugin_bundle)。

overwrite_plugin_manifest_version459–490 ↗
fn overwrite_plugin_manifest_version(
    plugin_root: &Path,
    plugin_version: &str,
) -> Result<(), RemotePluginBundleInstallError>

作用:把插件自己的 plugin.json 里的 version 字段改成后端确认的版本。这样本地安装记录不会被压缩包里可能过旧或不一致的版本误导。

数据流:进去的是插件根目录和目标版本号 → 它找到 plugin.json,读成文本并解析成 JSON,确认 JSON 顶层是对象,然后写入新的 version 字段 → 最后用 write_json_file 保存回文件。

调用关系:它只由 prepare_extracted_remote_plugin_root 调用。它依赖 find_plugin_manifest_path 找清单文件,依赖 write_json_file 做最终格式化写入。

调用图:调用 1 个内部函数(write_json_file);被 1 处调用(prepare_extracted_remote_plugin_root);外部调用 5 个(String, find_plugin_manifest_path, InvalidBundle, read_to_string, from_str)。

overwrite_plugin_app_manifest492–504 ↗
fn overwrite_plugin_app_manifest(
    plugin_root: &Path,
    app_manifest: &JsonValue,
) -> Result<(), RemotePluginBundleInstallError>

作用:把后端给的应用清单写进插件目录。应用清单可以理解成插件暴露哪些应用能力的说明书。

数据流:进去的是插件根目录和要写入的 JSON 值 → 它先尝试从插件清单里找到应用清单路径,找不到就默认用根目录下的 .app.json → 然后把 JSON 写到这个位置。

调用关系:它只由 prepare_extracted_remote_plugin_root 调用。真正创建目录、格式化 JSON 和写文件的细节交给 write_json_file。

调用图:调用 2 个内部函数(load_plugin_manifest, write_json_file);被 1 处调用(prepare_extracted_remote_plugin_root)。

write_json_file506–526 ↗
fn write_json_file(
    path: &Path,
    value: &JsonValue,
    context: &'static str,
) -> Result<(), RemotePluginBundleInstallError>

作用:把一个 JSON 值漂亮地写成文件,并确保父目录存在。它统一处理写清单文件时的格式和错误包装。

数据流:进去的是目标路径、JSON 值和错误背景说明 → 它检查路径有父目录,创建父目录,把 JSON 转成带缩进的文本并补一个换行,最后写入文件 → 出来是成功或带上下文的错误。

调用关系:overwrite_plugin_manifest_version 和 overwrite_plugin_app_manifest 都靠它落盘。它把“怎么安全、整齐地写 JSON 文件”这件小事集中到一处。

调用图:被 2 处调用(overwrite_plugin_app_manifest, overwrite_plugin_manifest_version);外部调用 4 个(parent, create_dir_all, write, to_vec_pretty)。

extract_plugin_bundle_tar_gz528–537 ↗
fn extract_plugin_bundle_tar_gz(
    bytes: &[u8],
    destination: &Path,
) -> Result<(), RemotePluginBundleInstallError>

作用:用默认安全限制解压远程插件的 tar.gz 压缩包。tar.gz 可以理解成一种常见的“打包再压缩”的文件格式。

数据流:进去的是压缩包字节和目标目录 → 它把默认的最大解压总大小传给更底层函数 → 出来是解压成功或错误,目标目录可能新增文件。

调用关系:安装、检出和多个解压测试都会调用它。它只是便捷入口,真正限制大小和转换错误的是 extract_plugin_bundle_tar_gz_with_limits。

调用图:调用 1 个内部函数(extract_plugin_bundle_tar_gz_with_limits);被 5 处调用(extract_remote_plugin_bundle_to_path, install_remote_plugin_bundle, extraction_preserves_executable_permissions, extraction_rejects_tar_path_traversal, extraction_supports_gnu_long_name_entries)。

extract_plugin_bundle_tar_gz_with_limits539–555 ↗
fn extract_plugin_bundle_tar_gz_with_limits(
    bytes: &[u8],
    destination: &Path,
    max_total_bytes: u64,
) -> Result<(), RemotePluginBundleInstallError>

作用:按指定的最大解压总大小解开插件压缩包,并把底层解包错误翻译成远程插件安装错误。

数据流:进去的是压缩包字节、目标目录和最大解压字节数 → 它调用 unpack_plugin_bundle_tar_gz 做实际解包 → 如果底层说太大、文件错误或包不合法,它转换成当前文件的错误类型;成功则目标目录得到解压文件。

调用关系:extract_plugin_bundle_tar_gz 调用它使用默认上限,测试 extraction_rejects_total_size_over_limit 直接调用它验证自定义上限。它连接了通用解包模块和远程插件安装流程。

调用图:调用 1 个内部函数(unpack_plugin_bundle_tar_gz);被 2 处调用(extract_plugin_bundle_tar_gz, extraction_rejects_total_size_over_limit)。

find_extracted_plugin_root557–567 ↗
fn find_extracted_plugin_root(
    extraction_root: &Path,
) -> Result<PathBuf, RemotePluginBundleInstallError>

作用:确认解压目录本身就是标准插件根目录,而不是里面又套了一层目录。标准插件根目录必须能找到 plugin.json。

数据流:进去的是解压根目录路径 → 它检查这个路径下能否发现插件清单 → 能发现就返回这个路径;不能发现就返回“没有标准插件根”的错误。

调用关系:安装和检出都在解压后调用它。它内部交给 is_standard_plugin_root 判断标准性,测试也用它确认嵌套插件根会被拒绝。

调用图:调用 1 个内部函数(is_standard_plugin_root);被 3 处调用(extract_remote_plugin_bundle_to_path, install_remote_plugin_bundle, find_extracted_plugin_root_rejects_nested_plugin_root);外部调用 2 个(to_path_buf, InvalidBundle)。

is_standard_plugin_root569–571 ↗
fn is_standard_plugin_root(path: &Path) -> bool

作用:判断某个目录是不是标准插件根目录。它的标准就是能在这里找到插件清单文件 plugin.json。

数据流:进去的是目录路径 → 它调用 find_plugin_manifest_path 查找清单 → 找到返回 true,找不到返回 false。

调用关系:它只服务于 find_extracted_plugin_root,是一个很小的判断函数。这样以后标准变化时,根目录判断可以集中修改。

调用图:被 1 处调用(find_extracted_plugin_root);外部调用 1 个(find_plugin_manifest_path)。

tests::validate_remote_plugin_bundle_uses_detail_name_for_local_plugin_id585–603 ↗
fn validate_remote_plugin_bundle_uses_detail_name_for_local_plugin_id()

作用:测试远程插件验证时,会用后端详情里的插件名生成本地插件 ID。它防止把远程编号误当成本地插件名。

数据流:进去的是一组合法的测试远程插件信息 → 调用 validate_remote_plugin_bundle → 检查生成的插件名、市场名、版本号和下载地址都符合预期。

调用关系:它直接覆盖 validate_remote_plugin_bundle 的成功路径。这个测试保证上层安装流程拿到的是可用于本地仓库的插件身份。

调用图:调用 1 个内部函数(validate_remote_plugin_bundle);外部调用 1 个(assert_eq!)。

tests::validate_remote_plugin_bundle_rejects_missing_release_version606–621 ↗
fn validate_remote_plugin_bundle_rejects_missing_release_version()

作用:测试缺少发布版本时必须被拒绝。没有版本号就无法可靠记录装的是哪一版。

数据流:进去的是没有 release_version 的测试数据 → 调用 validate_remote_plugin_bundle → 期待返回 MissingReleaseVersion 错误。

调用关系:它验证 validate_remote_plugin_bundle 对必填字段的检查。这样远程响应不完整时不会继续进入下载和安装。

调用图:调用 1 个内部函数(validate_remote_plugin_bundle);外部调用 1 个(assert!)。

tests::validate_remote_plugin_bundle_rejects_invalid_release_version624–639 ↗
fn validate_remote_plugin_bundle_rejects_invalid_release_version()

作用:测试非法版本号会被拒绝。这里用类似 ../1.2.3 的危险字符串,防止版本号被拿去拼路径时出问题。

数据流:进去的是带非法版本字符串的测试数据 → 调用 validate_remote_plugin_bundle → 期待返回 InvalidReleaseVersion 错误。

调用关系:它验证 validate_remote_plugin_bundle 会使用版本段校验函数。这个测试守住了远程输入进入本地文件系统前的一道关。

调用图:调用 1 个内部函数(validate_remote_plugin_bundle);外部调用 1 个(assert!)。

tests::validate_remote_plugin_bundle_rejects_missing_download_url642–657 ↗
fn validate_remote_plugin_bundle_rejects_missing_download_url()

作用:测试缺少下载地址时必须被拒绝。没有地址就无法下载插件包,也不该进入后续流程。

数据流:进去的是没有 bundle_download_url 的测试数据 → 调用 validate_remote_plugin_bundle → 期待返回 MissingBundleDownloadUrl 错误。

调用关系:它覆盖 validate_remote_plugin_bundle 的下载地址必填检查。它保证上层不会拿到一份无法执行的安装计划。

调用图:调用 1 个内部函数(validate_remote_plugin_bundle);外部调用 1 个(assert!)。

tests::validate_remote_plugin_bundle_rejects_unsupported_download_url_scheme660–675 ↗
fn validate_remote_plugin_bundle_rejects_unsupported_download_url_scheme()

作用:测试普通 HTTP 下载地址会被拒绝。这样正式安装不会通过未加密连接下载插件。

数据流:进去的是 http://example.com 这样的测试地址 → 调用 validate_remote_plugin_bundle → 期待返回 UnsupportedBundleDownloadUrlScheme 错误。

调用关系:它验证 validate_remote_plugin_bundle 和 URL 安全判断的配合。这个测试保护远程插件下载必须走安全协议的规则。

调用图:调用 1 个内部函数(validate_remote_plugin_bundle);外部调用 1 个(assert!)。

tests::download_size_limit_rejects_oversized_bundle678–690 ↗
fn download_size_limit_rejects_oversized_bundle()

作用:测试下载大小超过上限时会报错。它确保大小限制不是摆设。

数据流:进去的是当前大小 5、上限 4 的测试值 → 调用 enforce_download_size_limit → 期待返回 DownloadTooLarge 错误。

调用关系:它直接测试 enforce_download_size_limit。这个小函数被网络响应读取流程反复调用,所以它的正确性很关键。

调用图:调用 1 个内部函数(enforce_download_size_limit);外部调用 1 个(assert!)。

tests::install_rejects_invalid_tar_gz_bundle693–705 ↗
fn install_rejects_invalid_tar_gz_bundle()

作用:测试安装流程会拒绝根本不是 tar.gz 的内容。坏压缩包不应该被当成插件继续处理。

数据流:进去的是临时 Codex 主目录、合法插件信息和一段普通文本字节 → 调用 install_remote_plugin_bundle → 期待得到读取压缩包失败相关错误。

调用关系:它直接覆盖 install_remote_plugin_bundle 的解包失败路径。valid_remote_plugin_bundle 提供合法安装计划,tempdir 提供隔离目录。

调用图:调用 1 个内部函数(install_remote_plugin_bundle);外部调用 3 个(assert!, valid_remote_plugin_bundle, tempdir)。

tests::install_rejects_bundle_without_standard_plugin_root708–722 ↗
fn install_rejects_bundle_without_standard_plugin_root()

作用:测试压缩包里没有标准插件根目录时会被拒绝。也就是没有 plugin.json 的包不能安装。

数据流:进去的是只包含 README.md 的测试压缩包 → 调用 install_remote_plugin_bundle → 期待错误信息说明没有标准插件根。

调用关系:它验证 install_remote_plugin_bundle 解压后会调用 find_extracted_plugin_root。tar_gz_bytes 用来现场造一个测试包。

调用图:调用 1 个内部函数(install_remote_plugin_bundle);外部调用 4 个(assert!, tar_gz_bytes, valid_remote_plugin_bundle, tempdir)。

tests::install_preserves_non_global_bundle_manifest_metadata725–794 ↗
fn install_preserves_non_global_bundle_manifest_metadata()

作用:测试非全局远程市场的插件,安装时不会被后端应用清单覆盖包内清单。换句话说,某些来源的包内元数据要原样保留。

数据流:进去的是非全局市场的插件信息、后端版本和后端应用清单,以及包内自己的 plugin.json 和 .app.json → 调用 install_remote_plugin_bundle → 再读回已安装文件,确认包内 version 和 app 内容没有被改写,但安装结果版本仍使用后端版本。

调用关系:它覆盖 prepare_extracted_remote_plugin_root 的“不处理非全局市场”分支。它同时使用 validate_remote_plugin_bundle、tar_gz_bytes 和 JSON 读取比较来确认最终落盘结果。

调用图:调用 2 个内部函数(install_remote_plugin_bundle, validate_remote_plugin_bundle);外部调用 6 个(assert_eq!, tar_gz_bytes, from_str, json!, read_to_string, tempdir)。

tests::find_extracted_plugin_root_uses_local_manifest_discovery797–811 ↗
fn find_extracted_plugin_root_uses_local_manifest_discovery()

作用:测试只要解压根目录下有本地插件清单,就能被识别为插件根目录。它确认清单发现规则能正常工作。

数据流:进去的是一个临时目录,测试先在里面创建 .codex-plugin/plugin.json → 然后检查找到的插件根目录等于这个临时目录 → 输出是断言通过或失败。

调用关系:它间接验证 find_extracted_plugin_root 依赖的清单发现规则。虽然函数清单里没有显示直接调用 find_extracted_plugin_root,但测试意图就是确认根目录识别行为。

调用图:外部调用 4 个(assert_eq!, create_dir_all, write, tempdir)。

tests::find_extracted_plugin_root_rejects_nested_plugin_root814–830 ↗
fn find_extracted_plugin_root_rejects_nested_plugin_root()

作用:测试插件根目录如果藏在解压目录的子目录里,会被拒绝。这个格式要求让远程包结构更简单、更可控。

数据流:进去的是一个临时解压根目录,测试在其子目录 linear 里放 plugin.json → 调用 find_extracted_plugin_root 检查外层目录 → 期待返回错误。

调用关系:它直接验证 find_extracted_plugin_root 不会递归接受嵌套插件根。这样安装流程不会因为包结构多套一层而装错位置。

调用图:调用 1 个内部函数(find_extracted_plugin_root);外部调用 4 个(assert!, create_dir_all, write, tempdir)。

tests::extraction_rejects_tar_path_traversal833–842 ↗
fn extraction_rejects_tar_path_traversal()

作用:测试解压时会拒绝路径穿越。路径穿越就是压缩包里写 ../evil.txt,试图把文件解到目标目录外面。

数据流:进去的是一个带原始危险路径的测试 tar.gz 字节和临时目标目录 → 调用 extract_plugin_bundle_tar_gz → 期待错误里提到逃出解压根目录。

调用关系:它验证 extract_plugin_bundle_tar_gz 背后的安全解包逻辑。tar_gz_bytes_with_raw_path 用来构造普通打包 API 不容易生成的危险路径。

调用图:调用 1 个内部函数(extract_plugin_bundle_tar_gz);外部调用 3 个(assert!, tar_gz_bytes_with_raw_path, tempdir)。

tests::extraction_rejects_total_size_over_limit845–861 ↗
fn extraction_rejects_total_size_over_limit()

作用:测试解压后的总文件大小超过上限时会被拒绝。这样小压缩包也不能膨胀成特别大的目录。

数据流:进去的是两个 4 字节文件组成的压缩包和 6 字节总上限 → 调用 extract_plugin_bundle_tar_gz_with_limits → 期待 ExtractedBundleTooLarge 错误。

调用关系:它直接测试 extract_plugin_bundle_tar_gz_with_limits 的大小限制。这个能力保护安装流程不会被“压缩炸弹”拖垮。

调用图:调用 1 个内部函数(extract_plugin_bundle_tar_gz_with_limits);外部调用 3 个(assert!, tar_gz_bytes, tempdir)。

tests::extraction_supports_gnu_long_name_entries864–878 ↗
fn extraction_supports_gnu_long_name_entries()

作用:测试解压逻辑支持 GNU tar 的长文件名记录。也就是路径很长的文件仍然能正确解出来。

数据流:进去的是一个带很长路径文件的测试压缩包 → 调用 extract_plugin_bundle_tar_gz 解压 → 再读目标文件,确认内容是 long。

调用关系:它验证 extract_plugin_bundle_tar_gz 不只处理最简单的 tar 格式,也能兼容常见的长路径扩展。

调用图:调用 1 个内部函数(extract_plugin_bundle_tar_gz);外部调用 4 个(assert_eq!, tar_gz_bytes, format!, tempdir)。

tests::extraction_preserves_executable_permissions882–905 ↗
fn extraction_preserves_executable_permissions()

作用:在 Unix 系统上,测试解压后可执行文件权限会被保留。比如脚本原本是 755,解出来仍然能执行。

数据流:进去的是包含 plugin.json 和 bin/helper 脚本的测试压缩包,其中脚本权限是 0o755 → 调用 extract_plugin_bundle_tar_gz → 读取解压后文件权限并断言仍是 755。

调用关系:它验证 extract_plugin_bundle_tar_gz 背后的解包过程会保留 Unix 权限。这个测试只在 Unix 平台编译运行。

调用图:调用 1 个内部函数(extract_plugin_bundle_tar_gz);外部调用 4 个(assert_eq!, tar_gz_bytes, metadata, tempdir)。

tests::valid_remote_plugin_bundle907–917 ↗
fn valid_remote_plugin_bundle() -> ValidatedRemotePluginBundle

作用:为测试创建一份合法的已验证远程插件包信息。它避免每个测试都重复写同样的样板数据。

数据流:进去没有参数 → 它调用 validate_remote_plugin_bundle,传入固定的合法远程插件编号、市场、插件名、版本和 HTTPS 下载地址 → 出来是 ValidatedRemotePluginBundle。

调用关系:安装相关测试会调用它来获得一个可信的 bundle。它把测试重点从“构造数据”转回到“验证安装行为”。

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

tests::tar_gz_bytes919–926 ↗
fn tar_gz_bytes(entries: &[(&str, &[u8], u32)]) -> Vec<u8>

作用:为测试现场生成一个 tar.gz 压缩包。调用者只要给出文件路径、内容和权限,就能得到压缩包字节。

数据流:进去的是一组测试文件条目 → 它创建 gzip 编码器和 tar 构建器,把每个条目交给 append_tar_entry 写入,最后调用 finish_tar_gz 收尾 → 出来是完整压缩包字节数组。

调用关系:多个安装和解压测试用它构造正常或特殊的插件包。它把测试数据制作的细节藏起来,让测试更容易读。

调用图:外部调用 6 个(new, new, default, append_tar_entry, finish_tar_gz, new)。

tests::tar_gz_bytes_with_raw_path928–947 ↗
fn tar_gz_bytes_with_raw_path(path: &str, contents: &[u8], mode: u32) -> Vec<u8>

作用:为测试生成一个带原始路径字段的 tar.gz 包,主要用于构造危险路径。普通打包函数可能会自动帮忙规范路径,所以这里手写头部。

数据流:进去的是原始路径、文件内容和权限 → 它手工创建 tar 头,直接把路径字节塞进头部,再写内容、补齐块、写结束标记并完成 gzip → 出来是测试用压缩包字节。

调用关系:路径穿越测试调用它来制造 ../evil.txt 这种恶意条目。它服务于安全测试,不是正常安装流程的一部分。

调用图:外部调用 5 个(new, new, default, new_gnu, vec!)。

tests::append_tar_entry949–964 ↗
fn append_tar_entry(
        tar: &mut tar::Builder<W>,
        entry_type: tar::EntryType,
        path: &str,
        contents: &[u8],
        mode: u32,
    )

作用:往测试用 tar 包里追加一个文件条目。它统一设置文件类型、大小、权限和校验信息。

数据流:进去的是 tar 构建器、条目类型、路径、内容和权限 → 它创建 tar 头并写入条目 → 成功时 tar 构建器多了一个文件;失败时直接 panic,让测试中止。

调用关系:tar_gz_bytes 为每个测试文件调用它。它是测试压缩包生成流水线中的“写一个文件”步骤。

调用图:外部调用 3 个(append_data, panic!, new_gnu)。

tests::finish_tar_gz966–975 ↗
fn finish_tar_gz(tar: tar::Builder<GzEncoder<Vec<u8>>>) -> Vec<u8>

作用:结束测试 tar.gz 包的写入,并取出最终字节。它负责把 tar 和 gzip 两层都收尾。

数据流:进去的是一个 tar 构建器 → 它先取回里面的 gzip 编码器,再完成 gzip 压缩 → 出来是 Vec<u8> 字节数组;如果收尾失败就 panic。

调用关系:tar_gz_bytes 在所有条目写完后调用它。它是测试压缩包生成的最后一步。

调用图:外部调用 2 个(into_inner, panic!)。

core-plugins/src/remote/remote_installed_plugin_sync.rs源码 ↗
orchestration后台同步、插件缓存刷新、清理旧缓存

可以把这个文件想成插件缓存的“同步工”。用户在服务器上装了哪些远端插件,本机不一定马上知道;本机也可能残留以前装过、现在已经不用的插件目录。这个文件会先确认用户有登录凭据,然后同时去远端查询全局、工作区、用户三个范围的已安装插件。拿到列表后,它会为每个插件算出本地缓存里的名字,检查当前缓存版本是否已经一致;不一致就校验下载信息,再下载并安装插件包。最后它会扫一遍本机插件缓存目录,把远端已经不再安装的插件删掉。它还特别防止两个危险情况:同一个缓存目录不要同时跑两次同步;正在安装或改动中的插件缓存不要被清理任务误删。这里的互斥锁就是“一把锁”,用来避免多个任务同时改同一份记录。

函数细节13
RemoteInstalledPluginBundleSyncOutcome::changed_local_cache46–48 ↗
fn changed_local_cache(&self) -> bool

作用:判断这次同步有没有真的改动本机插件缓存。有人会用它来决定是否需要通知系统刷新插件列表。

数据流:输入是一次同步的结果对象,里面有“新安装的插件”和“被删除的缓存插件”两份名单。它只检查这两份名单是不是为空;只要其中一份有内容,就返回 true,否则返回 false。它不改任何数据。

调用关系:maybe_start_remote_installed_plugin_bundle_sync 在后台同步结束后会看这个判断。如果本地缓存变了,并且外面提供了回调函数,它就触发回调,让其他部分知道插件状态需要刷新。

maybe_start_remote_installed_plugin_bundle_sync82–124 ↗
fn maybe_start_remote_installed_plugin_bundle_sync(
    codex_home: PathBuf,
    config: RemotePluginServiceConfig,
    auth: Option<CodexAuth>,
    on_local_cache_changed: Option<Arc<dyn Fn() + Send

作用:尝试启动一次远端插件同步,但不会硬启动:没登录就不做,同一个缓存目录已经在同步就不再开第二个。它适合在应用启动或插件服务准备好时悄悄跑后台刷新。

数据流:输入是 Codex 主目录、远端服务配置、可选登录信息,以及一个“本地缓存变化后要做什么”的回调。它先检查有没有登录信息,再用缓存根目录做一把“正在同步”的标记;标记成功后开一个异步后台任务。任务里真正调用 sync_remote_installed_plugin_bundles_once,结束后记录日志,必要时调用回调,最后清掉“正在同步”的标记。

调用关系:这是外部进入本文件同步流程的入口。它把防重复、后台运行、日志和结束清理串起来,真正的下载与清理工作交给 sync_remote_installed_plugin_bundles_once;开始前用 mark_remote_installed_plugin_bundle_sync_in_flight 占位,结束后用 clear_remote_installed_plugin_bundle_sync_in_flight 释放。

调用图:调用 4 个内部函数(clear_remote_installed_plugin_bundle_sync_in_flight, mark_remote_installed_plugin_bundle_sync_in_flight, remote_plugin_cache_root, sync_remote_installed_plugin_bundles_once);外部调用 3 个(info!, spawn, warn!)。

sync_remote_installed_plugin_bundles_once126–277 ↗
async fn sync_remote_installed_plugin_bundles_once(
    codex_home: PathBuf,
    config: &RemotePluginServiceConfig,
    auth: Option<&CodexAuth>,
) -> Result<RemoteInstalledPluginBundleSyncOutcome, R

作用:完整执行一次“远端已安装插件 → 本地缓存”的同步。它会拉远端列表、下载缺失或版本不同的插件,并删除本地过期缓存。

数据流:输入是 Codex 主目录、远端插件服务配置和登录信息。它先确认登录有效,然后并行查询全局、工作区、用户三个范围的已安装插件;接着打开本地插件仓库,逐个插件计算本地 ID,比较当前缓存版本。如果需要更新,就校验远端插件包信息,再下载安装。所有远端列表处理完后,它把“远端仍然安装的插件名单”交给清理函数,删除不在名单里的缓存。输出是一份结果:安装了哪些、删除了哪些缓存、哪些远端插件失败了。

调用关系:maybe_start_remote_installed_plugin_bundle_sync 会在后台任务里调用它。它自己像总调度员:登录校验交给 ensure_chatgpt_auth,远端列表交给 fetch_installed_plugins_for_scope_with_download_url,插件包检查和安装交给 remote_bundle 里的函数,最后把磁盘清理交给 remove_stale_remote_plugin_caches。

调用图:调用 4 个内部函数(download_and_install_remote_plugin_bundle, validate_remote_plugin_bundle, try_new, new);被 1 处调用(maybe_start_remote_installed_plugin_bundle_sync);外部调用 9 个(from_iter, new, clone, ensure_chatgpt_auth, fetch_installed_plugins_for_scope_with_download_url, remote_plugin_canonical_marketplace_name, spawn_blocking, try_join!, warn!)。

mark_remote_plugin_cache_mutation_in_flight279–297 ↗
fn mark_remote_plugin_cache_mutation_in_flight(
    codex_home: &Path,
    marketplace_name: &str,
    plugin_name: &str,
) -> RemotePluginCacheMutationGuard

作用:标记某个远端插件缓存“正在被改动”。这样清理旧缓存时不会误删一个正在安装或更新的插件目录。

数据流:输入是 Codex 主目录、市场名和插件名。它先算出插件缓存根目录,再把“缓存根目录 + 市场名 + 插件名”组成一个键,放进全局记录表里;如果同一个插件被多处同时标记,它会把计数加一。输出是一个 RemotePluginCacheMutationGuard 守卫对象;这个对象活着,就表示改动还没结束。

调用关系:安装或修改远端插件缓存的代码可以在动手前调用它。remove_stale_remote_plugin_caches 清理时会通过 is_remote_plugin_cache_mutation_in_flight 查这张表;只要守卫还没释放,对应缓存就会被跳过。测试 stale_remote_plugin_cleanup_skips_cache_mutations_in_progress 专门验证了这个保护。

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

RemotePluginCacheMutationGuard::drop300–314 ↗
fn drop(&mut self)

作用:在“缓存改动守卫”离开作用范围时自动撤销标记。它保证不用手动写收尾代码,也能让清理任务知道改动已经结束。

数据流:输入是守卫对象自身保存的那个缓存键。它找到全局“正在改动”记录表,把对应计数减一;如果计数减到零,就把这条记录移除。输出没有返回值,但会改变全局记录表的状态。

调用关系:它是 Rust 的 Drop 自动清理机制,也就是对象销毁时自动运行。mark_remote_plugin_cache_mutation_in_flight 创建它;当调用方完成安装、更新,或者变量被 drop 掉时,它就解除保护。remove_stale_remote_plugin_caches 之后再检查时,就可以删除这个过期缓存了。

remove_stale_remote_plugin_caches317–390 ↗
fn remove_stale_remote_plugin_caches(
    codex_home: &Path,
    installed_plugin_names_by_marketplace: &BTreeMap<String, BTreeSet<String>>,
) -> Result<Vec<String>, String>

作用:删除本机里已经不属于远端已安装列表的插件缓存。它防止磁盘里堆着旧插件,也防止系统继续看到不该存在的远端插件。

数据流:输入是 Codex 主目录,以及一张“每个远端市场现在仍安装哪些插件”的表。它依次检查几个固定的远端市场缓存目录;目录不存在就跳过。对每个缓存项,它看插件名是否还在已安装名单里;在就保留,不在就继续检查是否正在被改动。如果没有正在改动,它会按目录或文件删除,并把被删插件的本地 ID 记入结果。最后排序后返回删除名单;如果读目录、删文件等磁盘操作失败,就返回错误文字。

调用关系:sync_remote_installed_plugin_bundles_once 在下载和安装结束后,把远端最新名单交给它做收尾清理。它清理前会问 is_remote_plugin_cache_mutation_in_flight,避免和正在安装的任务打架。两个测试函数分别验证“正在改动时不删”和“过期市场缓存会被删、正确缓存会保留”。

调用图:调用 2 个内部函数(is_remote_plugin_cache_mutation_in_flight, new);被 2 处调用(stale_remote_plugin_cleanup_removes_stale_marketplace_caches_and_keeps_canonical_cache, stale_remote_plugin_cleanup_skips_cache_mutations_in_progress);外部调用 5 个(join, new, read_dir, remove_dir_all, remove_file)。

remote_plugin_cache_root392–394 ↗
fn remote_plugin_cache_root(codex_home: &Path) -> PathBuf

作用:算出远端插件缓存的根目录。其他函数需要用同一个路径规则,才能判断和清理同一块地方。

数据流:输入是 Codex 主目录路径。它把固定的插件缓存目录名拼到这个主目录后面,输出完整的缓存根路径。它不读写磁盘,只做路径拼接。

调用关系:maybe_start_remote_installed_plugin_bundle_sync 用它生成同步去重用的键;mark_remote_plugin_cache_mutation_in_flight 和 is_remote_plugin_cache_mutation_in_flight 用它确保“正在改动”的记录指向同一缓存根目录;测试也用它检查去重逻辑。

调用图:被 4 处调用(is_remote_plugin_cache_mutation_in_flight, mark_remote_plugin_cache_mutation_in_flight, maybe_start_remote_installed_plugin_bundle_sync, remote_installed_plugin_sync_in_flight_dedupes_by_cache_root);外部调用 1 个(join)。

is_remote_plugin_cache_mutation_in_flight396–413 ↗
fn is_remote_plugin_cache_mutation_in_flight(
    codex_home: &Path,
    marketplace_name: &str,
    plugin_name: &str,
) -> bool

作用:检查某个远端插件缓存现在是不是正在被安装或修改。清理任务靠它决定要不要暂时放过某个目录。

数据流:输入是 Codex 主目录、市场名和插件名。它先看全局“正在改动”记录表是否存在;不存在就说明没人正在改,返回 false。存在时,它用同样的路径规则组成键,到表里查有没有这条记录,有就返回 true,没有就返回 false。

调用关系:remove_stale_remote_plugin_caches 在准备删除某个过期缓存前会调用它。如果它说“正在改”,清理函数就跳过,等下一次再处理;它内部依赖 remote_plugin_cache_root 来保证路径判断一致。

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

mark_remote_installed_plugin_bundle_sync_in_flight415–425 ↗
fn mark_remote_installed_plugin_bundle_sync_in_flight(
    key: RemoteInstalledPluginBundleSyncKey,
) -> bool

作用:给某个插件缓存根目录打上“同步正在进行”的标记。它用来防止同一个地方同时跑两次远端插件同步。

数据流:输入是一个同步键,里面主要是插件缓存根目录。它把这个键放进全局集合;如果之前没有,插入成功并返回 true;如果已经有了,说明同步已经在跑,返回 false。它会修改全局同步状态集合。

调用关系:maybe_start_remote_installed_plugin_bundle_sync 在开后台任务前先调用它。只有它返回 true,后台同步才会启动;测试 remote_installed_plugin_sync_in_flight_dedupes_by_cache_root 验证了同一个缓存根目录会被去重。

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

clear_remote_installed_plugin_bundle_sync_in_flight427–436 ↗
fn clear_remote_installed_plugin_bundle_sync_in_flight(key: &RemoteInstalledPluginBundleSyncKey)

作用:清掉某个缓存根目录的“正在同步”标记。这样下一次同步请求才有机会正常启动。

数据流:输入是之前用于占位的同步键。它找到全局同步集合,把这个键移除;如果集合还没创建,就什么也不做。它没有返回值,只改变全局同步状态。

调用关系:maybe_start_remote_installed_plugin_bundle_sync 的后台任务结束时会调用它,作为收尾动作。测试 remote_installed_plugin_sync_in_flight_dedupes_by_cache_root 也直接调用它,确认清掉标记后可以再次启动同步。

调用图:被 2 处调用(maybe_start_remote_installed_plugin_bundle_sync, remote_installed_plugin_sync_in_flight_dedupes_by_cache_root)。

tests::remote_installed_plugin_sync_in_flight_dedupes_by_cache_root444–462 ↗
fn remote_installed_plugin_sync_in_flight_dedupes_by_cache_root()

作用:测试同一个插件缓存根目录不会同时启动两次同步。这个测试保护的是“后台同步去重”这条安全规则。

数据流:测试先创建一个临时 Codex 主目录,再用 remote_plugin_cache_root 算出同步键。第一次标记应该成功,第二次用同一个键标记应该失败;清掉标记后,再标记又应该成功。它最后再次清理标记,避免影响别的测试。

调用关系:它直接验证 mark_remote_installed_plugin_bundle_sync_in_flight 和 clear_remote_installed_plugin_bundle_sync_in_flight 的配合。这个行为正是 maybe_start_remote_installed_plugin_bundle_sync 防止重复后台任务的基础。

调用图:调用 2 个内部函数(clear_remote_installed_plugin_bundle_sync_in_flight, remote_plugin_cache_root);外部调用 2 个(assert!, tempdir)。

tests::stale_remote_plugin_cleanup_skips_cache_mutations_in_progress465–531 ↗
fn stale_remote_plugin_cleanup_skips_cache_mutations_in_progress()

作用:测试清理旧缓存时,不会删掉正在被改动的插件缓存。它防的是“安装到一半被清理任务删掉”这种很糟糕的情况。

数据流:测试先在临时目录里造出一个看起来已经缓存好的插件文件,但远端已安装名单里不包含它,所以正常来说它该被删。然后它为这个插件创建两个改动守卫。第一次清理时,因为有守卫,文件还在;丢掉第一个守卫后,第二个还在,仍然不删;两个守卫都丢掉后,再清理就会删掉缓存,并返回被删除的插件 ID。

调用关系:它调用 mark_remote_plugin_cache_mutation_in_flight 制造“正在改动”的状态,再调用 remove_stale_remote_plugin_caches 观察清理结果。这个测试覆盖了计数式守卫和自动 drop 收尾的配合。

调用图:调用 2 个内部函数(mark_remote_plugin_cache_mutation_in_flight, remove_stale_remote_plugin_caches);外部调用 7 个(from_iter, new, assert!, assert_eq!, create_dir_all, write, tempdir)。

tests::stale_remote_plugin_cleanup_removes_stale_marketplace_caches_and_keeps_canonical_cache534–620 ↗
fn stale_remote_plugin_cleanup_removes_stale_marketplace_caches_and_keeps_canonical_cache()

作用:测试清理函数会删掉已经过期的远端市场缓存,同时保留仍然属于当前远端安装名单的正确缓存。

数据流:测试在临时目录里造出三个缓存:一个“我创建的”插件、一个私有共享插件、一个当前仍安装的共享插件。它传入的已安装名单只包含那个当前仍安装的共享插件。调用清理函数后,前两个缓存应该被删除并出现在返回名单里,第三个缓存应该保留下来。

调用关系:它直接调用 remove_stale_remote_plugin_caches,验证这个清理函数能正确区分多个远端市场目录。这个结果也保证 sync_remote_installed_plugin_bundles_once 在同步结束后的收尾清理不会误删仍该存在的规范缓存。

调用图:调用 1 个内部函数(remove_stale_remote_plugin_caches);外部调用 8 个(from_iter, from, new, assert!, assert_eq!, create_dir_all, write, tempdir)。

core-plugins/src/remote.rs源码 ↗
io_transportstartup, request handling, background cache refresh

这个文件把“远程插件”这件事从头到尾串起来。它先确认用户是不是用 ChatGPT 账号登录,因为远程插件目录需要这个身份;然后用 HTTP 请求(一种向服务器要数据的网络请求)去后端拿插件列表、已安装插件、技能详情、推荐插件等信息。拿到的数据不是直接丢给界面,而是整理成项目里统一使用的结构,比如插件摘要、插件详情、市场分组。它还会处理分页,就像一本目录分很多页,要一页页翻完;也会用缓存,避免每次都从服务器重新拉全球插件目录。安装和卸载时,它会核对服务器返回的插件编号和启用状态,防止“装错插件”或“服务器说装了但其实没启用”。卸载后还会清掉本地缓存,避免旧文件继续影响后续使用。

函数细节53
is_valid_remote_plugin_id258–263 ↗
fn is_valid_remote_plugin_id(plugin_id: &str) -> bool

作用:检查远程插件编号是不是安全、合法。它只允许英文字母、数字和少数几个符号,避免把奇怪字符带进请求或文件路径里。

数据流:进去一个插件编号字符串 → 它逐个看字符是不是允许的,并且要求不能是空字符串 → 出来一个 true 或 false,表示能不能继续用。

调用关系:它是最基础的门卫函数。validate_remote_plugin_id 会用它生成正式错误;推荐插件筛选、插件分享和卸载响应等流程也会用它挡掉不合格编号。

调用图:被 7 处调用(plugin_share_checkout_response, plugin_share_delete_response, plugin_share_save_response, plugin_share_update_targets_response, plugin_uninstall_response, recommended_plugins_mode, validate_remote_plugin_id)。

validate_remote_plugin_id265–277 ↗
fn validate_remote_plugin_id(plugin_id: &str) -> Result<(), JSONRPCErrorError>

作用:把“检查插件编号”包装成对外可返回的错误格式。有人要读取、安装或卸载远程插件前,会先用它确认编号没问题。

数据流:进去一个插件编号 → 它调用 is_valid_remote_plugin_id 判断 → 合法就返回成功,不合法就返回一个 JSON-RPC 错误,告诉调用方允许哪些字符。

调用关系:它站在接口入口附近。plugin_read_response、plugin_skill_read_response、remote_plugin_install_response、remote_plugin_uninstall_response 会先让它把关,再继续访问远程插件。

调用图:调用 1 个内部函数(is_valid_remote_plugin_id);被 4 处调用(plugin_read_response, plugin_skill_read_response, remote_plugin_install_response, remote_plugin_uninstall_response)。

RemotePluginScope::api_value383–389 ↗
fn api_value(self) -> &'static str

作用:把代码里的插件范围转换成服务器 API 认识的文字,比如 GLOBAL、USER、WORKSPACE。这样请求参数不会写散、写错。

数据流:进去一个 RemotePluginScope 枚举值 → 它按范围选择对应的大写字符串 → 出来一个可直接放进网络请求的参数值。

调用关系:get_remote_plugin_list_page 和 get_remote_plugin_installed_page 在拼请求参数时会用它,把本地概念翻译给后端听。

调用图:被 2 处调用(get_remote_plugin_installed_page, get_remote_plugin_list_page)。

RemotePluginScope::marketplace_name391–397 ↗
fn marketplace_name(self) -> &'static str

作用:把插件范围转换成本项目内部使用的市场名字。比如全局插件属于 OpenAI curated 远程市场,用户自建插件属于 created-by-me。

数据流:进去一个范围值 → 它查表式地选择对应市场的内部名称 → 出来一个稳定的市场标识字符串。

调用关系:这是范围到市场名称的翻译工具。虽然函数清单里没有列出直接调用者,它和同类方法一起保证各处使用同一套市场命名。

RemotePluginScope::marketplace_display_name399–405 ↗
fn marketplace_display_name(self) -> &'static str

作用:给插件范围找一个适合展示给用户看的市场名称。它处理的是“人看得懂的名字”,不是内部编号。

数据流:进去一个范围值 → 它选择对应的展示名,比如 Created by me 或 Workspace Directory → 出来一段界面可显示的文字。

调用关系:build_remote_plugin_detail 会用它给插件详情补上市集展示名,让界面知道这个插件来自哪个分类。

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

RemotePluginScope::from_marketplace_name407–417 ↗
fn from_marketplace_name(name: &str) -> Option<Self>

作用:根据市场名字反推出插件范围。它主要用来确认某个市场名是不是远程插件系统支持的。

数据流:进去一个市场名称字符串 → 它匹配已知的几个远程市场名 → 匹配成功就返回范围,失败就返回空。

调用关系:fetch_remote_plugin_skill_detail 会先用它验证市场名,不认识的市场就不继续请求技能详情。

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

remote_plugin_canonical_marketplace_name539–553 ↗
fn remote_plugin_canonical_marketplace_name(
    plugin: &RemotePluginDirectoryItem,
) -> Result<&'static str, RemotePluginCatalogError>

作用:判断一个远程插件真正应该放在哪个市场桶里。尤其是工作区插件,要根据共享可见性决定是目录插件还是“别人分享给我”的插件。

数据流:进去一个服务器返回的插件目录项 → 它看插件范围;如果是工作区插件,还读取 discoverability(可发现性,也就是公开、私有或未列出)→ 出来规范的市场名称,或发现数据缺失时报错。

调用关系:它是很多转换函数的共同依据。build_remote_plugin_summary、插件详情、已安装缓存、发现列表和卸载流程都靠它统一分类,内部会调用 workspace_plugin_discoverability。

调用图:调用 1 个内部函数(workspace_plugin_discoverability);被 5 处调用(build_remote_plugin_summary, fetch_remote_plugin_detail_with_download_url_option, remote_discoverable_plugin_from_directory_item, remote_installed_plugin_to_cache_entry, uninstall_remote_plugin)。

workspace_plugin_discoverability555–564 ↗
fn workspace_plugin_discoverability(
    plugin: &RemotePluginDirectoryItem,
) -> Result<RemotePluginShareDiscoverability, RemotePluginCatalogError>

作用:读取工作区插件的共享可见性,并在缺失时给出清楚错误。工作区插件没有这个字段,就无法判断该放在哪个列表里。

数据流:进去一个插件目录项 → 它取出 discoverability 字段 → 有值就返回,没值就返回“服务器响应不完整”的错误。

调用关系:remote_plugin_canonical_marketplace_name 和 remote_plugin_share_context 会调用它,因为这两处都需要知道工作区插件是公开、私有还是未列出。

调用图:被 2 处调用(remote_plugin_canonical_marketplace_name, remote_plugin_share_context)。

fetch_remote_marketplaces633–794 ↗
async fn fetch_remote_marketplaces(
    config: &RemotePluginServiceConfig,
    auth: Option<&CodexAuth>,
    sources: &[RemoteMarketplaceSource],
    global_catalog_cache_path: Option<&Path>,
) -> Re

作用:拉取用户要看的远程插件市场列表。它会把服务器目录、已安装状态和共享插件合并成界面可以直接展示的市场分组。

数据流:进去服务配置、登录信息、要看的市场来源、可选缓存路径 → 它先确认登录,再按来源请求目录和安装列表,必要时读取或写入全球目录缓存,最后把插件合成市场对象 → 出来一组 RemoteMarketplace。

调用关系:plugin_list_response 会调用它来生成插件列表。它像总调度员,下面会叫 fetch_directory_plugins_for_scope、fetch_installed_plugins_for_scope、fetch_shared_workspace_plugins 和 build_remote_marketplace 干具体活。

调用图:调用 7 个内部函数(build_remote_marketplace, load_cached_global_directory_plugins, write_cached_global_directory_plugins, ensure_chatgpt_auth, fetch_directory_plugins_for_scope, fetch_installed_plugins_for_scope, fetch_shared_workspace_plugins);被 1 处调用(plugin_list_response);外部调用 3 个(new, iter, try_join!)。

fetch_and_cache_global_remote_plugin_catalog796–806 ↗
async fn fetch_and_cache_global_remote_plugin_catalog(
    codex_home: &Path,
    config: &RemotePluginServiceConfig,
    auth: Option<&CodexAuth>,
) -> Result<(), RemotePluginCatalogError>

作用:专门刷新全球远程插件目录缓存。这样下次启动或推荐工具需要插件目录时,可以先用本地副本,速度更快也更稳。

数据流:进去本地目录、服务配置和登录信息 → 它确认登录后向服务器拉全球插件目录 → 把结果写进缓存,成功时不返回具体数据。

调用关系:启动任务、缓存刷新循环和一些推荐插件流程会调用它。它内部只负责全球目录,具体请求交给 fetch_directory_plugins_for_scope,写文件交给 catalog_cache。

调用图:调用 3 个内部函数(write_cached_global_directory_plugins, ensure_chatgpt_auth, fetch_directory_plugins_for_scope);被 4 处调用(expands_cached_remote_plugins_by_loaded_apps, maybe_start_plugin_startup_tasks_for_config, run_global_remote_catalog_cache_refresh_loop, list_tool_suggest_discoverable_plugins_includes_cached_remote_global_plugins)。

has_cached_global_remote_plugin_catalog888–897 ↗
fn has_cached_global_remote_plugin_catalog(
    codex_home: &Path,
    config: &RemotePluginServiceConfig,
    auth: Option<&CodexAuth>,
) -> bool

作用:快速判断本地有没有可用的全球远程插件目录缓存。它适合在列表接口里决定要不要走缓存相关路径。

数据流:进去本地目录、服务配置和可选登录信息 → 它先确认登录可用,再尝试读取缓存 → 出来 true 或 false。

调用关系:plugin_list_response 会用它判断缓存是否存在。它不发网络请求,只调用 ensure_chatgpt_auth 和 catalog_cache 的读取函数。

调用图:调用 2 个内部函数(load_cached_global_directory_plugins, ensure_chatgpt_auth);被 1 处调用(plugin_list_response)。

cached_global_remote_discoverable_plugins899–915 ↗
fn cached_global_remote_discoverable_plugins(
    codex_home: &Path,
    config: &RemotePluginServiceConfig,
    auth: &CodexAuth,
) -> Vec<RemoteDiscoverablePlugin>

作用:从本地缓存里取出可用于“发现/推荐”的全球远程插件。它让系统不用联网也能拿到一部分可展示的插件信息。

数据流:进去本地目录、服务配置和登录信息 → 它读取缓存,把每个目录项转换成更轻量的可发现插件;转换失败的条目会被跳过并记录警告 → 出来一个插件列表。

调用关系:cached_global_remote_discoverable_plugins_for_config 会调用它。它依赖缓存模块提供原始目录,再用本文件里的转换逻辑整理成推荐可用的形状。

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

fetch_openai_curated_remote_collection_marketplace917–940 ↗
async fn fetch_openai_curated_remote_collection_marketplace(
    config: &RemotePluginServiceConfig,
    auth: Option<&CodexAuth>,
) -> Result<Option<RemoteMarketplace>, RemotePluginCatalogError>

作用:拉取 OpenAI 精选远程插件集合。它不是拿整个全球市场,而是请求特定 collection,用来展示更聚焦的插件列表。

数据流:进去服务配置和登录信息 → 它确认登录后同时拉精选目录和已安装状态 → 交给 build_remote_marketplace 合并,出来一个可能为空的市场。

调用关系:plugin_list_response 会在需要精选远程集合时调用它。它的收尾工作由 build_remote_marketplace 完成。

调用图:调用 2 个内部函数(build_remote_marketplace, ensure_chatgpt_auth);被 1 处调用(plugin_list_response);外部调用 1 个(try_join!)。

build_remote_marketplace942–977 ↗
fn build_remote_marketplace(
    name: &str,
    display_name: &str,
    directory_plugins: Vec<RemotePluginDirectoryItem>,
    installed_plugins: Vec<RemotePluginInstalledItem>,
    include_installed

作用:把“服务器目录插件”和“用户已安装插件”合成一个市场。它负责给每个插件标上是否已安装、是否启用。

数据流:进去市场名、展示名、目录插件列表、已安装插件列表和是否包含仅安装插件的开关 → 它按插件远程 id 对齐两边数据,逐个生成摘要,必要时补上只在安装列表里的插件 → 出来一个市场对象,没插件就返回空。

调用关系:fetch_remote_marketplaces 和 fetch_openai_curated_remote_collection_marketplace 都用它做最后组装。它把单个插件摘要的细节交给 build_remote_plugin_summary。

调用图:被 2 处调用(fetch_openai_curated_remote_collection_marketplace, fetch_remote_marketplaces)。

fetch_remote_installed_plugins979–1012 ↗
async fn fetch_remote_installed_plugins(
    config: &RemotePluginServiceConfig,
    auth: Option<&CodexAuth>,
) -> Result<Vec<RemoteInstalledPlugin>, RemotePluginCatalogError>

作用:一次性拉取所有范围里的已安装远程插件,用于本地缓存或后台刷新。它覆盖全球、工作区和用户自建三个范围。

数据流:进去服务配置和登录信息 → 它确认登录后并行请求三个范围的安装列表,再转换成缓存条目并排序 → 出来统一的 RemoteInstalledPlugin 列表。

调用关系:build_and_cache_remote_installed_plugin_marketplaces 和后台刷新循环会调用它。它下层依赖 fetch_installed_plugins_for_scope,上层通常会把结果写入缓存或分组展示。

调用图:调用 2 个内部函数(ensure_chatgpt_auth, fetch_installed_plugins_for_scope);被 2 处调用(build_and_cache_remote_installed_plugin_marketplaces, run_remote_installed_plugins_cache_refresh_loop);外部调用 1 个(try_join!)。

group_remote_installed_plugins_by_marketplaces1014–1059 ↗
fn group_remote_installed_plugins_by_marketplaces(
    plugins: &[RemoteInstalledPlugin],
    visible_marketplaces: &[&str],
) -> Vec<RemoteMarketplace>

作用:把缓存里的已安装远程插件重新按市场分组,方便界面展示。它还会过滤掉当前不想显示的市场。

数据流:进去已安装插件列表和允许显示的市场名列表 → 它为每个插件生成摘要,按市场收集,再按固定市场顺序输出并在每组内排序 → 出来一组 RemoteMarketplace。

调用关系:缓存构建和从缓存恢复市场列表时会调用它。它把缓存里的扁平列表变回界面喜欢的“市场下面有插件”的结构。

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

fetch_remote_plugin_detail1061–1075 ↗
async fn fetch_remote_plugin_detail(
    config: &RemotePluginServiceConfig,
    auth: Option<&CodexAuth>,
    marketplace_name: &str,
    plugin_id: &str,
) -> Result<RemotePluginDetail, RemotePlugin

作用:获取某个远程插件的详情,但不要求服务器返回下载地址。适合普通详情页读取。

数据流:进去服务配置、登录信息、市场名和插件 id → 它把请求转给 fetch_remote_plugin_detail_with_download_url_option,并把 include_download_urls 设为 false → 出来完整插件详情。

调用关系:plugin_read_response 会调用它。它是更通用详情函数的简化入口。

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

fetch_remote_plugin_share_context1077–1088 ↗
async fn fetch_remote_plugin_share_context(
    config: &RemotePluginServiceConfig,
    auth: Option<&CodexAuth>,
    plugin_id: &str,
) -> Result<Option<RemotePluginShareContext>, RemotePluginCatalog

作用:读取某个远程插件的分享信息,比如分享链接、创建者和可见性。只有工作区插件通常会有这些信息。

数据流:进去服务配置、登录信息和插件 id → 它确认登录,拉取插件详情,再从详情里提取分享上下文 → 出来分享信息或空。

调用关系:plugin_read_response 会在需要分享状态时调用它。它使用 fetch_plugin_detail 拿原始数据,再交给 remote_plugin_share_context 做提取。

调用图:调用 3 个内部函数(ensure_chatgpt_auth, fetch_plugin_detail, remote_plugin_share_context);被 1 处调用(plugin_read_response)。

fetch_remote_plugin_detail_with_download_urls1090–1104 ↗
async fn fetch_remote_plugin_detail_with_download_urls(
    config: &RemotePluginServiceConfig,
    auth: Option<&CodexAuth>,
    marketplace_name: &str,
    plugin_id: &str,
) -> Result<RemotePluginD

作用:获取插件详情,并要求服务器带上下载地址。安装远程插件时需要这些地址来拿插件包。

数据流:进去服务配置、登录信息、市场名和插件 id → 它调用通用详情函数,并把 include_download_urls 设为 true → 出来带下载链接的插件详情。

调用关系:remote_plugin_install_response 会调用它。它和 fetch_remote_plugin_detail 共用同一个底层实现,只是开关不同。

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

fetch_remote_plugin_skill_detail1106–1140 ↗
async fn fetch_remote_plugin_skill_detail(
    config: &RemotePluginServiceConfig,
    auth: Option<&CodexAuth>,
    marketplace_name: &str,
    plugin_id: &str,
    skill_name: &str,
) -> Result<Remo

作用:读取远程插件里某个技能的详细说明内容。技能可以理解成插件提供的一个具体能力或说明文档。

数据流:进去服务配置、登录信息、市场名、插件 id、技能名 → 它确认登录和市场有效,拼技能详情 URL,发送请求并解码;还会核对返回的插件 id 和技能名是否一致 → 出来技能内容。

调用关系:plugin_skill_read_response 会调用它。它用 from_marketplace_name 做入口校验,用 remote_plugin_skill_detail_url 拼安全地址,再用 authenticated_request 和 send_and_decode 完成网络请求。

调用图:调用 6 个内部函数(from_marketplace_name, authenticated_request, ensure_chatgpt_auth, remote_plugin_skill_detail_url, send_and_decode, build_reqwest_client);被 1 处调用(plugin_skill_read_response)。

fetch_remote_plugin_detail_with_download_url_option1142–1158 ↗
async fn fetch_remote_plugin_detail_with_download_url_option(
    config: &RemotePluginServiceConfig,
    auth: Option<&CodexAuth>,
    _marketplace_name: &str,
    plugin_id: &str,
    include_downlo

作用:这是获取插件详情的共用底层函数,区别只在于是否要下载地址。它还会以服务器返回的真实范围为准,而不是盲信调用者传来的市场名。

数据流:进去服务配置、登录信息、市场名、插件 id 和下载地址开关 → 它确认登录,拉取插件详情,计算规范市场名 → 交给 build_remote_plugin_detail 组装成最终详情。

调用关系:fetch_remote_plugin_detail 和 fetch_remote_plugin_detail_with_download_urls 都调用它。它连接原始网络响应和项目内部详情模型。

调用图:调用 4 个内部函数(build_remote_plugin_detail, ensure_chatgpt_auth, fetch_plugin_detail, remote_plugin_canonical_marketplace_name);被 2 处调用(fetch_remote_plugin_detail, fetch_remote_plugin_detail_with_download_urls)。

build_remote_plugin_detail1160–1252 ↗
async fn build_remote_plugin_detail(
    config: &RemotePluginServiceConfig,
    auth: &CodexAuth,
    scope: RemotePluginScope,
    marketplace_name: String,
    plugin_id: &str,
    plugin: RemotePl

作用:把服务器返回的插件原始详情整理成界面和安装流程真正需要的详情对象。它会合并安装状态、技能启用状态、应用连接和 MCP 服务器信息。

数据流:进去配置、登录、插件范围、市场名、插件 id 和原始目录项 → 它查该范围的已安装列表,标出禁用技能,整理 app manifest 或 app id,并应用 MCP 路由策略 → 出来 RemotePluginDetail。

调用关系:fetch_remote_plugin_detail_with_download_url_option 拉到原始数据后会调用它。它会调用 fetch_installed_plugins_for_scope、build_remote_plugin_summary、apply_app_mcp_routing_policy 等函数完成拼装。

调用图:调用 6 个内部函数(apply_app_mcp_routing_policy, marketplace_display_name, build_remote_plugin_summary, fetch_installed_plugins_for_scope, non_empty_string, api_auth_mode);被 1 处调用(fetch_remote_plugin_detail_with_download_url_option);外部调用 1 个(app_connector_ids_from_declarations)。

app_declarations_from_remote_app_ids1254–1263 ↗
fn app_declarations_from_remote_app_ids(app_ids: &[String]) -> Vec<AppDeclaration>

作用:在没有完整应用清单时,把简单的 app id 列表补成项目内部需要的应用声明。它是一个兜底转换器。

数据流:进去一组 app id 字符串 → 它为每个 id 创建一个 AppDeclaration,把名字和连接器 id 都设成这个 id → 出来应用声明列表。

调用关系:插件详情构建时,如果远程响应没有 app_manifest,就会用这种简单方式生成应用声明,保证后续应用和 MCP 路由仍能继续处理。

install_remote_plugin1265–1302 ↗
async fn install_remote_plugin(
    config: &RemotePluginServiceConfig,
    auth: Option<&CodexAuth>,
    _marketplace_name: &str,
    plugin_id: &str,
) -> Result<RemotePluginInstallResult, RemotePlu

作用:请求服务器安装一个远程插件。它还会确认服务器返回的确实是同一个插件,并且已经启用。

数据流:进去服务配置、登录信息、市场名和插件 id → 它确认登录,向 /install 端点发 POST 请求,并要求返回需要授权的 app → 核对 id 和 enabled 后,出来安装结果。

调用关系:remote_plugin_install_response 会调用它。它不先验证市场名,因为远程插件 id 本身能唯一定位插件;网络部分交给 authenticated_request 和 send_and_decode。

调用图:调用 4 个内部函数(authenticated_request, ensure_chatgpt_auth, send_and_decode, build_reqwest_client);被 1 处调用(remote_plugin_install_response);外部调用 1 个(format!)。

uninstall_remote_plugin1304–1350 ↗
async fn uninstall_remote_plugin(
    config: &RemotePluginServiceConfig,
    auth: Option<&CodexAuth>,
    codex_home: PathBuf,
    plugin_id: &str,
) -> Result<(), RemotePluginCatalogError>

作用:请求服务器卸载远程插件,并清理本地缓存。这样用户卸载后,本地旧插件包不会继续残留影响系统。

数据流:进去服务配置、登录信息、本地目录和插件 id → 它先拉详情确定市场和名称,再发卸载请求,核对返回状态为未启用 → 后台删除对应缓存,最后返回成功或错误。

调用关系:remote_plugin_uninstall_response 会调用它。它会用 fetch_plugin_detail 和 remote_plugin_canonical_marketplace_name 找缓存位置,再通过 spawn_blocking 把文件删除交给 remove_remote_plugin_cache。

调用图:调用 6 个内部函数(authenticated_request, ensure_chatgpt_auth, fetch_plugin_detail, remote_plugin_canonical_marketplace_name, send_and_decode, build_reqwest_client);被 1 处调用(remote_plugin_uninstall_response);外部调用 2 个(format!, spawn_blocking)。

remove_remote_plugin_cache1352–1394 ↗
fn remove_remote_plugin_cache(
    codex_home: PathBuf,
    marketplace_name: String,
    plugin_name: String,
    legacy_plugin_id: String,
) -> Result<(), String>

作用:删除某个远程插件在本地留下的缓存目录或旧格式缓存。它专门做磁盘清理,避免阻塞异步网络流程。

数据流:进去本地目录、市场名、插件名和旧插件 id → 它打开插件存储,算出新格式缓存路径并卸载;如果旧格式路径也存在,就删除目录或文件 → 出来成功或错误文字。

调用关系:uninstall_remote_plugin 会在后台线程里调用它。它是卸载流程的收尾清洁工,和服务器请求分开,避免文件操作卡住主异步任务。

调用图:调用 2 个内部函数(try_new, new);外部调用 4 个(clone, join, remove_dir_all, remove_file)。

build_remote_plugin_summary1396–1421 ↗
fn build_remote_plugin_summary(
    plugin: &RemotePluginDirectoryItem,
    installed_plugin: Option<&RemotePluginInstalledItem>,
) -> Result<RemotePluginSummary, RemotePluginCatalogError>

作用:把一个远程插件目录项整理成列表页用的简短信息。它会加上安装状态、启用状态、展示信息和分享信息。

数据流:进去插件目录项和可选的已安装记录 → 它计算市场名和本地配置 id,提取分享上下文与界面信息 → 出来 RemotePluginSummary。

调用关系:build_remote_marketplace 和 build_remote_plugin_detail 都会用它。它内部依赖 remote_plugin_canonical_marketplace_name、remote_plugin_share_context 和 remote_plugin_interface_to_info。

调用图:调用 4 个内部函数(remote_plugin_canonical_marketplace_name, remote_plugin_interface_to_info, remote_plugin_share_context, new);被 1 处调用(build_remote_plugin_detail)。

remote_discoverable_plugin_from_directory_item1423–1449 ↗
fn remote_discoverable_plugin_from_directory_item(
    plugin: &RemotePluginDirectoryItem,
) -> Result<RemoteDiscoverablePlugin, RemotePluginCatalogError>

作用:把目录里的远程插件转换成“可发现插件”格式,用于推荐或发现列表。这个格式比详情更轻,只保留搜索和展示需要的信息。

数据流:进去一个插件目录项 → 它生成配置 id,选择显示名和描述,记录是否有技能、app id、安装策略和可用状态 → 出来 RemoteDiscoverablePlugin。

调用关系:缓存的全球目录被拿来做发现列表时会用到它。它和 build_remote_plugin_summary 类似,但目标是推荐/发现,不是市场列表。

调用图:调用 3 个内部函数(non_empty_string, remote_plugin_canonical_marketplace_name, new)。

remote_plugin_share_context1451–1479 ↗
fn remote_plugin_share_context(
    plugin: &RemotePluginDirectoryItem,
) -> Result<Option<RemotePluginShareContext>, RemotePluginCatalogError>

作用:从插件目录项里提取分享相关信息。非工作区插件没有分享上下文,工作区插件才会带共享可见性、链接和成员信息。

数据流:进去一个插件目录项 → 它看插件范围;全局和用户范围返回空,工作区范围则读取 discoverability、分享 URL、创建者和共享对象 → 出来分享上下文或空。

调用关系:build_remote_plugin_summary 和 fetch_remote_plugin_share_context 会调用它。它需要 workspace_plugin_discoverability 来确认工作区插件的共享状态。

调用图:调用 1 个内部函数(workspace_plugin_discoverability);被 2 处调用(build_remote_plugin_summary, fetch_remote_plugin_share_context)。

remote_installed_plugin_to_cache_entry1481–1499 ↗
fn remote_installed_plugin_to_cache_entry(
    installed_plugin: &RemotePluginInstalledItem,
) -> Result<RemoteInstalledPlugin, RemotePluginCatalogError>

作用:把服务器返回的已安装插件转换成本地缓存条目。缓存只记录列表和启用状态需要的信息,不把每个技能的远程禁用状态写进去。

数据流:进去一个已安装插件项 → 它取出插件基本信息、市场名、启用状态、策略、界面信息和关键词 → 出来 RemoteInstalledPlugin 缓存条目。

调用关系:fetch_remote_installed_plugins 拉到安装列表后会用它做转换。它依赖 remote_plugin_canonical_marketplace_name 和 remote_plugin_interface_to_info。

调用图:调用 2 个内部函数(remote_plugin_canonical_marketplace_name, remote_plugin_interface_to_info)。

remote_plugin_interface_to_info1501–1549 ↗
fn remote_plugin_interface_to_info(plugin: &RemotePluginDirectoryItem) -> Option<PluginInterface>

作用:把服务器里的插件界面信息转换成项目统一的 PluginInterface。界面信息包括显示名、简介、开发者、图标 URL、默认提示词等。

数据流:进去一个插件目录项 → 它清理空字符串,规范默认提示词,复制各种展示字段;如果所有字段都为空,就返回空 → 出来可选的 PluginInterface。

调用关系:build_remote_plugin_summary 和 remote_installed_plugin_to_cache_entry 会调用它。它把后端字段变成前端或列表能统一理解的展示信息。

调用图:调用 1 个内部函数(non_empty_string);被 2 处调用(build_remote_plugin_summary, remote_installed_plugin_to_cache_entry);外部调用 1 个(new)。

remote_skill_interface_to_info1551–1569 ↗
fn remote_skill_interface_to_info(
    interface: Option<RemotePluginSkillInterfaceResponse>,
) -> Option<SkillInterface>

作用:把服务器返回的技能界面信息转换成项目统一的 SkillInterface。技能界面信息比插件少,主要是名字、短描述、颜色和默认提示。

数据流:进去可选的技能界面响应 → 没有响应就返回空;有响应就组装字段,并在全部字段为空时仍返回空 → 出来可选的 SkillInterface。

调用关系:插件详情构建技能列表时会用到它。它让每个技能的展示信息和项目里的通用技能格式对齐。

remote_plugin_display_name1571–1577 ↗
fn remote_plugin_display_name(plugin: &RemotePluginSummary) -> &str

作用:取得插件应该显示给用户看的名字。优先用界面信息里的显示名,没有就退回插件内部名称。

数据流:进去一个插件摘要 → 它查看 interface.display_name 是否存在 → 出来一个字符串引用,作为排序和展示使用。

调用关系:sort_remote_plugin_summaries_by_display_name 会用它比较插件名字。它是排序前的取名规则。

sort_remote_plugin_summaries_by_display_name1579–1589 ↗
fn sort_remote_plugin_summaries_by_display_name(plugins: &mut [RemotePluginSummary])

作用:按插件显示名给插件摘要列表排序。它先忽略大小写比较,再用原始名字和 id 兜底,保证顺序稳定。

数据流:进去一个可修改的插件摘要数组 → 它根据显示名的小写形式、原显示名、插件 id 依次排序 → 原数组被重新排列,不额外返回列表。

调用关系:按已安装插件缓存分组展示时会用到它。它让同一市场里的插件列表看起来更自然。

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

non_empty_string1591–1596 ↗
fn non_empty_string(value: Option<&str>) -> Option<String>

作用:把字符串清理成“真的有内容”的可选值。空白字符串会被当成没有值。

数据流:进去一个可选字符串引用 → 它去掉前后空格,检查是否为空 → 有内容就返回新的 String,没有就返回空。

调用关系:推荐插件、详情构建、可发现插件转换和插件界面信息转换都会用它。它避免界面显示一堆空字符串。

调用图:被 4 处调用(build_remote_plugin_detail, recommended_plugins_mode, remote_discoverable_plugin_from_directory_item, remote_plugin_interface_to_info)。

normalize_remote_default_prompts1598–1605 ↗
fn normalize_remote_default_prompts(prompts: &[String]) -> Option<Vec<String>>

作用:清理一组远程默认提示词,并限制数量。默认提示词就是插件建议用户一键使用的开场文本。

数据流:进去一组提示词字符串 → 它逐条调用 normalize_remote_default_prompt 过滤空的和过长的,再最多保留 3 条 → 出来提示词列表或空。

调用关系:插件界面信息转换时会用它处理 default_prompts。它负责批量规则,单条规则交给 normalize_remote_default_prompt。

normalize_remote_default_prompt1607–1613 ↗
fn normalize_remote_default_prompt(prompt: &str) -> Option<String>

作用:清理一条远程默认提示词。它去掉空格,并拒绝空内容和太长内容,防止界面被异常文本撑坏。

数据流:进去一条提示词 → 它 trim 前后空格,检查是否为空以及字符数是否超过 128 → 合格就返回清理后的字符串,不合格返回空。

调用关系:normalize_remote_default_prompts 会批量调用它;插件界面信息也会在只有单条默认提示时用它。

fetch_directory_plugins_for_scope1615–1624 ↗
async fn fetch_directory_plugins_for_scope(
    config: &RemotePluginServiceConfig,
    auth: &CodexAuth,
    scope: RemotePluginScope,
) -> Result<Vec<RemotePluginDirectoryItem>, RemotePluginCatalogE

作用:拉取某个范围的远程插件目录,比如全球、用户自建或工作区目录。它不指定额外集合。

数据流:进去服务配置、登录和插件范围 → 它把 collection 设为空,转给分页拉取函数 → 出来该范围下的全部目录插件。

调用关系:fetch_remote_marketplaces 和 fetch_and_cache_global_remote_plugin_catalog 会调用它。它是普通目录拉取的便捷入口。

调用图:调用 1 个内部函数(fetch_directory_plugins_for_scope_with_optional_collection);被 2 处调用(fetch_and_cache_global_remote_plugin_catalog, fetch_remote_marketplaces)。

fetch_directory_plugins_for_scope_with_collection1626–1639 ↗
async fn fetch_directory_plugins_for_scope_with_collection(
    config: &RemotePluginServiceConfig,
    auth: &CodexAuth,
    scope: RemotePluginScope,
    collection: &str,
) -> Result<Vec<RemotePlug

作用:拉取某个范围中特定集合的插件目录。集合可以理解成服务器给插件目录打的一个筛选标签。

数据流:进去服务配置、登录、范围和集合名 → 它把集合名传给通用分页函数 → 出来符合该集合的全部目录插件。

调用关系:精选远程市场流程会通过它拿 OpenAI curated collection。它和普通目录函数共用同一个分页实现。

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

fetch_directory_plugins_for_scope_with_optional_collection1641–1660 ↗
async fn fetch_directory_plugins_for_scope_with_optional_collection(
    config: &RemotePluginServiceConfig,
    auth: &CodexAuth,
    scope: RemotePluginScope,
    collection: Option<&str>,
) -> Resu

作用:分页拉完某个范围的插件目录,可选地按集合筛选。它负责一页页请求,直到服务器说没有下一页。

数据流:进去服务配置、登录、范围和可选集合 → 它循环调用 get_remote_plugin_list_page,累积每页 plugins,并更新 page token → 出来完整插件列表。

调用关系:fetch_directory_plugins_for_scope 和 fetch_directory_plugins_for_scope_with_collection 都依赖它。真正单页 HTTP 请求由 get_remote_plugin_list_page 完成。

调用图:调用 1 个内部函数(get_remote_plugin_list_page);被 2 处调用(fetch_directory_plugins_for_scope, fetch_directory_plugins_for_scope_with_collection);外部调用 1 个(new)。

fetch_shared_workspace_plugins1662–1678 ↗
async fn fetch_shared_workspace_plugins(
    config: &RemotePluginServiceConfig,
    auth: &CodexAuth,
) -> Result<Vec<RemotePluginDirectoryItem>, RemotePluginCatalogError>

作用:拉取明确分享给当前用户的工作区插件。它也会处理分页,直到拿完所有分享插件。

数据流:进去服务配置和登录 → 它循环请求共享工作区插件页面,累积插件,并跟随下一页 token → 出来共享插件列表。

调用关系:fetch_remote_marketplaces 在构建“Shared with me”市场时会调用它。单页请求交给 get_remote_shared_workspace_plugins_page。

调用图:调用 1 个内部函数(get_remote_shared_workspace_plugins_page);被 1 处调用(fetch_remote_marketplaces);外部调用 1 个(new)。

fetch_installed_plugins_for_scope1680–1689 ↗
async fn fetch_installed_plugins_for_scope(
    config: &RemotePluginServiceConfig,
    auth: &CodexAuth,
    scope: RemotePluginScope,
) -> Result<Vec<RemotePluginInstalledItem>, RemotePluginCatalogE

作用:拉取某个范围里用户已经安装的远程插件,不要求下载地址。列表和详情状态判断通常只需要这些信息。

数据流:进去服务配置、登录和范围 → 它把 include_download_urls 设为 false,转给分页安装列表函数 → 出来安装插件列表。

调用关系:fetch_remote_marketplaces、fetch_remote_installed_plugins 和 build_remote_plugin_detail 都会调用它。它是已安装列表的常用入口。

调用图:调用 1 个内部函数(fetch_installed_plugins_for_scope_with_download_url);被 3 处调用(build_remote_plugin_detail, fetch_remote_installed_plugins, fetch_remote_marketplaces)。

fetch_installed_plugins_for_scope_with_download_url1691–1715 ↗
async fn fetch_installed_plugins_for_scope_with_download_url(
    config: &RemotePluginServiceConfig,
    auth: &CodexAuth,
    scope: RemotePluginScope,
    include_download_urls: bool,
) -> Result<V

作用:分页拉取某个范围的已安装插件,并可选择是否要求下载地址。它处理服务器分多页返回的情况。

数据流:进去服务配置、登录、范围和下载地址开关 → 它循环调用 get_remote_plugin_installed_page,累积每页安装项 → 出来完整安装列表。

调用关系:fetch_installed_plugins_for_scope 会调用它。它把“多页拉完”的流程和“单页怎么请求”分开。

调用图:调用 1 个内部函数(get_remote_plugin_installed_page);被 1 处调用(fetch_installed_plugins_for_scope);外部调用 1 个(new)。

get_remote_plugin_list_page1717–1737 ↗
async fn get_remote_plugin_list_page(
    config: &RemotePluginServiceConfig,
    auth: &CodexAuth,
    scope: RemotePluginScope,
    page_token: Option<&str>,
    collection: Option<&str>,
) -> Resul

作用:请求远程插件目录的一页数据。它是目录分页循环里的单次网络请求。

数据流:进去服务配置、登录、范围、可选页 token 和可选集合 → 它拼 /ps/plugins/list 地址,加 scope、limit、collection、pageToken 参数并发送 → 出来这一页的插件和下一页信息。

调用关系:fetch_directory_plugins_for_scope_with_optional_collection 会反复调用它。它用 RemotePluginScope::api_value 翻译范围,用 authenticated_request 和 send_and_decode 完成请求。

调用图:调用 4 个内部函数(api_value, authenticated_request, send_and_decode, build_reqwest_client);被 1 处调用(fetch_directory_plugins_for_scope_with_optional_collection);外部调用 1 个(format!)。

get_remote_shared_workspace_plugins_page1739–1753 ↗
async fn get_remote_shared_workspace_plugins_page(
    config: &RemotePluginServiceConfig,
    auth: &CodexAuth,
    page_token: Option<&str>,
) -> Result<RemotePluginListResponse, RemotePluginCatalog

作用:请求“分享给我”的工作区插件的一页数据。它只负责一页,不负责循环。

数据流:进去服务配置、登录和可选页 token → 它拼 /ps/plugins/workspace/shared 地址,加 limit 和 pageToken,发送并解析 → 出来这一页共享插件响应。

调用关系:fetch_shared_workspace_plugins 会循环调用它。它使用 authenticated_request 加认证,send_and_decode 处理发送和 JSON 解析。

调用图:调用 3 个内部函数(authenticated_request, send_and_decode, build_reqwest_client);被 1 处调用(fetch_shared_workspace_plugins);外部调用 1 个(format!)。

get_remote_plugin_installed_page1755–1774 ↗
async fn get_remote_plugin_installed_page(
    config: &RemotePluginServiceConfig,
    auth: &CodexAuth,
    scope: RemotePluginScope,
    page_token: Option<&str>,
    include_download_urls: bool,
)

作用:请求某个范围的已安装插件的一页数据。它支持可选地让服务器带上下载链接。

数据流:进去服务配置、登录、范围、可选页 token 和下载地址开关 → 它拼 /ps/plugins/installed 地址,加 scope、includeDownloadUrls、pageToken 等参数 → 出来这一页安装列表响应。

调用关系:fetch_installed_plugins_for_scope_with_download_url 会反复调用它。它用 api_value 生成 scope 参数,并走统一的认证和解码流程。

调用图:调用 4 个内部函数(api_value, authenticated_request, send_and_decode, build_reqwest_client);被 1 处调用(fetch_installed_plugins_for_scope_with_download_url);外部调用 1 个(format!)。

fetch_plugin_detail1776–1790 ↗
async fn fetch_plugin_detail(
    config: &RemotePluginServiceConfig,
    auth: &CodexAuth,
    plugin_id: &str,
    include_download_urls: bool,
) -> Result<RemotePluginDirectoryItem, RemotePluginCat

作用:请求某个远程插件的原始详情。它是详情页、分享信息和卸载流程的基础网络读取函数。

数据流:进去服务配置、登录、插件 id 和下载地址开关 → 它拼 /ps/plugins/{plugin_id} 地址,按需加 includeDownloadUrls 参数,发送请求并解码 → 出来 RemotePluginDirectoryItem。

调用关系:fetch_remote_plugin_detail_with_download_url_option、fetch_remote_plugin_share_context 和 uninstall_remote_plugin 都会调用它。它只拿原始数据,不负责最终展示整理。

调用图:调用 3 个内部函数(authenticated_request, send_and_decode, build_reqwest_client);被 3 处调用(fetch_remote_plugin_detail_with_download_url_option, fetch_remote_plugin_share_context, uninstall_remote_plugin);外部调用 1 个(format!)。

remote_plugin_skill_detail_url1792–1811 ↗
fn remote_plugin_skill_detail_url(
    config: &RemotePluginServiceConfig,
    plugin_id: &str,
    skill_name: &str,
) -> Result<String, RemotePluginCatalogError>

作用:安全地拼出某个插件技能详情的 URL。它用 URL 解析器逐段添加路径,避免手写字符串时路径出错。

数据流:进去服务配置、插件 id 和技能名 → 它解析基础 URL,依次追加 ps、plugins、插件 id、skills、技能名 → 出来完整 URL,基础 URL 不合法就报错。

调用关系:fetch_remote_plugin_skill_detail 会调用它来生成请求地址。这样技能名即使包含特殊路径字符,也会按 URL 规则处理。

调用图:被 1 处调用(fetch_remote_plugin_skill_detail);外部调用 1 个(parse)。

ensure_chatgpt_auth1813–1821 ↗
fn ensure_chatgpt_auth(auth: Option<&CodexAuth>) -> Result<&CodexAuth, RemotePluginCatalogError>

作用:确认调用者提供了 ChatGPT 后端可用的登录身份。远程插件目录不支持普通 API key 模式。

数据流:进去一个可选认证对象 → 没有认证就返回 AuthRequired;认证方式不支持就返回 UnsupportedAuthMode;合格就返回认证引用。

调用关系:几乎所有远程插件网络入口都会先调用它,包括市场列表、详情、技能详情、安装、卸载、推荐和缓存刷新。它是远程插件访问的身份门禁。

调用图:被 11 处调用(fetch_and_cache_global_remote_plugin_catalog, fetch_openai_curated_remote_collection_marketplace, fetch_recommended_plugins, fetch_remote_installed_plugins, fetch_remote_marketplaces, fetch_remote_plugin_detail_with_download_url_option, fetch_remote_plugin_share_context, fetch_remote_plugin_skill_detail, has_cached_global_remote_plugin_catalog, install_remote_plugin (+1 more))。

authenticated_request1823–1831 ↗
fn authenticated_request(
    request: RequestBuilder,
    auth: &CodexAuth,
) -> Result<RequestBuilder, RemotePluginCatalogError>

作用:给网络请求统一加上超时、认证头和产品标识。这样所有远程插件请求都带着正确身份和 Codex 产品 SKU。

数据流:进去一个 reqwest 请求构造器和认证信息 → 它设置 30 秒超时,加入认证头,再加 OAI-Product-Sku: codex → 出来可发送的请求构造器。

调用关系:fetch_plugin_detail、列表分页、推荐、技能详情、安装和卸载都会调用它。它把重复的请求头设置集中在一个地方。

调用图:被 8 处调用(fetch_plugin_detail, fetch_recommended_plugins, fetch_remote_plugin_skill_detail, get_remote_plugin_installed_page, get_remote_plugin_list_page, get_remote_shared_workspace_plugins_page, install_remote_plugin, uninstall_remote_plugin);外部调用 2 个(timeout, auth_provider_from_auth)。

send_and_decode1833–1858 ↗
async fn send_and_decode(
    request: RequestBuilder,
    url: &str,
) -> Result<T, RemotePluginCatalogError>

作用:发送 HTTP 请求并把服务器返回的 JSON 解码成指定类型。它还会把网络失败、状态码失败和 JSON 解析失败区分成不同错误。

数据流:进去一个已经配置好的请求和 URL 字符串 → 它发送请求,读取状态码和响应正文;状态码不成功就报错,成功就按目标类型解析 JSON → 出来解析后的结构体。

调用关系:所有真正访问远程插件 API 的函数最终都会用它收尾。它是网络层的统一出口,让上层不用重复写发送、读正文和解析错误处理。

调用图:被 8 处调用(fetch_plugin_detail, fetch_recommended_plugins, fetch_remote_plugin_skill_detail, get_remote_plugin_installed_page, get_remote_plugin_list_page, get_remote_shared_workspace_plugins_page, install_remote_plugin, uninstall_remote_plugin);外部调用 2 个(send, from_str)。

core-plugins/src/remote_legacy.rs源码 ↗
io_transportrequest handling;在获取远程推荐插件、安装同步、卸载同步时活跃

这个文件像一个“插件服务的电话员”。本地代码想知道服务器推荐哪些插件,或者想把某个插件在远端启用、卸载时,不直接拼请求,而是交给这里。它会根据配置里的 ChatGPT 地址拼出正确的网址,用 reqwest(一套发 HTTP 网络请求的工具)发送请求,并按情况带上 CodexAuth(用户登录凭证)。拉推荐插件比较宽松:如果有合适的登录信息就带上,没有也能试着请求。修改插件状态则更严格:必须是 ChatGPT/Codex 后端登录,API key 这种方式不支持。服务器返回后,它不只看“请求成功”,还会解析 JSON(一种常见的数据文本格式),确认返回的插件 id 和启用状态都符合预期,防止服务器改错对象或状态不一致。文件里还定义了清楚的错误类型,让上层能知道是没登录、网址不对、网络失败、服务器拒绝,还是返回内容看不懂。

函数细节6
enable_remote_plugin138–145 ↗
async fn enable_remote_plugin(
    config: &RemotePluginServiceConfig,
    auth: Option<&CodexAuth>,
    plugin_id: &str,
) -> Result<(), RemotePluginMutationError>

作用:这个函数告诉远程服务器:请启用某个插件。它给上层提供了一个简单入口,让调用者不用关心具体请求地址和返回校验。

数据流:进去的是远程服务配置、可选登录信息和插件 id。它把这些交给更底层的 post_remote_plugin_mutation,并指定动作是 enable。如果远端确认插件已启用,它返回空结果表示成功;如果登录不合适、请求失败或服务器返回内容不对,就把错误传回去。

调用关系:它由 install_plugin_with_remote_sync 调用,通常发生在本地安装插件并需要同步到远端时。它自己不直接发网络请求,而是把实际工作交给 post_remote_plugin_mutation,相当于给“启用插件”这个动作起了一个清楚的名字。

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

uninstall_remote_plugin147–154 ↗
async fn uninstall_remote_plugin(
    config: &RemotePluginServiceConfig,
    auth: Option<&CodexAuth>,
    plugin_id: &str,
) -> Result<(), RemotePluginMutationError>

作用:这个函数告诉远程服务器:请卸载或关闭某个插件。它让上层用一句话完成远端卸载同步,而不用知道旧版接口怎么调用。

数据流:进去的是远程服务配置、可选登录信息和插件 id。它调用 post_remote_plugin_mutation,动作写成 uninstall。如果服务器返回的插件 id 和状态都符合卸载后的预期,它返回成功;否则把具体失败原因交还给调用者。

调用关系:它由 uninstall_plugin_with_remote_sync 调用,通常发生在本地卸载插件后要通知远端时。它和 enable_remote_plugin 用同一套底层发送逻辑,只是传入的动作不同。

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

ensure_codex_backend_auth156–166 ↗
fn ensure_codex_backend_auth(
    auth: Option<&CodexAuth>,
) -> Result<&CodexAuth, RemotePluginMutationError>

作用:这个函数检查有没有可用的 ChatGPT/Codex 后端登录凭证。远程修改插件状态是敏感操作,所以不能匿名做,也不能用不支持的 API key 方式做。

数据流:进去的是一个可能为空的登录信息。它先看有没有登录;没有就返回“需要登录”的错误。再看这个登录是否使用 Codex 后端;不是的话返回“不支持这种认证方式”的错误。检查通过后,出来的是确认可用的登录信息引用。

调用关系:它被 post_remote_plugin_mutation 在真正发修改请求前调用。它像门口保安,先确认调用者有合适的身份,再允许后面的联网修改继续进行。

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

post_remote_plugin_mutation168–216 ↗
async fn post_remote_plugin_mutation(
    config: &RemotePluginServiceConfig,
    auth: Option<&CodexAuth>,
    plugin_id: &str,
    action: &str,
) -> Result<RemotePluginMutationResponse, RemotePlugi

作用:这个函数是真正向远程服务器提交“启用”或“卸载”插件请求的核心步骤。它不仅发请求,还会核对服务器返回的结果是不是改了正确的插件、改成了正确的状态。

数据流:进去的是远程服务配置、登录信息、插件 id 和动作名,比如 enableuninstall。它先用 ensure_codex_backend_auth 确认登录合格,再用 remote_plugin_mutation_url 拼出接口地址。之后创建 HTTP 客户端,带上认证头,发 POST 请求,最多等 30 秒。收到响应后,它检查状态码,读取正文,按 JSON 解析成包含插件 id 和 enabled 状态的结构。最后它核对返回的 id 是否等于请求的插件 id,并根据动作判断 enabled 是否符合预期。出来的是服务器返回的插件变更结果;任何一步不对都会变成明确错误。

调用关系:它是 enable_remote_pluginuninstall_remote_plugin 共用的底层执行器。上层函数只决定动作是什么,它负责认证、拼 URL、发请求、解析和校验,必要时还会调用认证头生成工具和 JSON 解析工具。

调用图:调用 3 个内部函数(ensure_codex_backend_auth, remote_plugin_mutation_url, build_reqwest_client);被 2 处调用(enable_remote_plugin, uninstall_remote_plugin);外部调用 2 个(auth_provider_from_auth, from_str)。

remote_plugin_mutation_url218–235 ↗
fn remote_plugin_mutation_url(
    config: &RemotePluginServiceConfig,
    plugin_id: &str,
    action: &str,
) -> Result<String, RemotePluginMutationError>

作用:这个函数负责把基础服务器地址、插件 id 和动作拼成一个安全、规范的远程接口网址。没有它,代码很容易因为多一个斜杠、少一段路径,或者基础地址非法而请求到错误位置。

数据流:进去的是远程服务配置、插件 id 和动作名。它先解析配置里的 ChatGPT 基础地址;如果地址本身不合法,就返回地址错误。然后它在路径后面依次加上 plugins、插件 id、动作名,并处理末尾空路径这种小细节。出来的是完整的网址字符串,例如指向某个插件的启用或卸载接口。

调用关系:它由 post_remote_plugin_mutation 在发请求前调用。它专心做“拼地址”这件事,让发送请求的函数不用手写字符串拼接,也能更早发现配置里的坏地址。

调用图:被 1 处调用(post_remote_plugin_mutation);外部调用 1 个(parse)。

core-plugins/src/remote/share.rs源码 ↗
io_transport用户执行插件分享、列出分享、删除分享或修改分享权限时活跃

这个文件解决的是“本地插件怎么安全地放到远端并分享给别人”的问题。插件不能直接乱传:要先确认用户已经用 ChatGPT 账号登录,再把插件目录压成 tar.gz 包(一种常见压缩包),还要限制最大 50MB,避免传太大的东西。上传分三步:先向服务端申请一个临时上传地址;再把压缩包传到这个地址;最后告诉服务端这个文件可以变成一个可分享的远端插件。它还会处理分享范围,比如公开、隐藏链接、私有,以及哪些用户、组或工作区能看。一个重要细节是:如果分享设成“隐藏链接”,它会自动把当前工作区加成可读对象,避免插件上传后自己所在工作区反而看不到。文件还会在本地记录“远端插件 ID 对应哪个本地路径”,方便以后列表或删除时对得上。

函数细节15
save_remote_plugin_share138–203 ↗
async fn save_remote_plugin_share(
    config: &RemotePluginServiceConfig,
    auth: Option<&CodexAuth>,
    codex_home: &Path,
    plugin_path: &AbsolutePathBuf,
    remote_plugin_id: Option<&str>,

作用:把一个本地插件保存到远端,并生成或更新一个可分享的远端插件。别人会在点击“分享插件”或“更新已分享插件”时用到它。

数据流:进去的是服务地址配置、登录信息、本地 Codex 目录、插件路径、可选的远端插件 ID,以及分享权限设置。它先检查登录,再在后台线程里把插件目录打成压缩包并取好文件名;然后向服务器申请上传地址,把压缩包传上去,再提交文件 ID、校验用的 etag 和分享设置;最后返回远端插件 ID 和可能存在的分享链接,并尽量把“远端 ID → 本地路径”的关系写到本地。

调用关系:这是保存分享的主流程。它把具体小活交给 create_workspace_plugin_upload 申请上传位置,交给 put_workspace_plugin_upload 真正传文件,交给 ensure_unlisted_workspace_target 修正隐藏分享的目标列表,交给 finalize_workspace_plugin_upload 完成服务器端创建或更新,最后调用本地路径记录函数保存映射。

调用图:调用 6 个内部函数(create_workspace_plugin_upload, ensure_unlisted_workspace_target, finalize_workspace_plugin_upload, record_plugin_share_local_path, put_workspace_plugin_upload, as_path);外部调用 4 个(UnexpectedResponse, spawn_blocking, clone, warn!)。

list_remote_plugin_shares205–251 ↗
async fn list_remote_plugin_shares(
    config: &RemotePluginServiceConfig,
    auth: Option<&CodexAuth>,
    codex_home: &Path,
) -> Result<Vec<RemotePluginShareSummary>, RemotePluginCatalogError>

作用:列出当前账号创建过的远端插件分享,并尽量附上它们对应的本地插件路径。用户想看“我分享过哪些插件”时会走这里。

数据流:进去的是服务配置、登录信息和本地 Codex 目录。它先确认登录,再从服务器取出自己创建的工作区插件;如果没有,就直接返回空列表。否则它还会取已安装插件信息、读取本地保存的路径映射,然后把这些信息合成一条条分享摘要;如果服务器返回的分享对象缺少必要的分享人员信息,就返回错误。

调用关系:这是列表页或列表命令的主入口。它先让 fetch_created_workspace_plugins 翻页拿完远端数据,再读 local_paths 里的本地映射,并依赖上层已有的摘要构建逻辑把远端插件和安装状态拼成可展示的信息。

调用图:调用 2 个内部函数(fetch_created_workspace_plugins, load_plugin_share_local_paths);外部调用 1 个(new)。

load_plugin_share_remote_ids_by_local_path253–271 ↗
fn load_plugin_share_remote_ids_by_local_path(
    codex_home: &Path,
) -> io::Result<BTreeMap<AbsolutePathBuf, String>>

作用:读取本地记录,并反过来得到“本地路径对应哪个远端插件 ID”。这让程序能从一个本地插件目录找到它已经分享到远端的身份。

数据流:进去的是 Codex 的本地目录。它读取保存过的“远端插件 ID → 本地路径”表,逐条检查远端 ID 是否像合法 ID;然后把方向反过来,变成“本地路径 → 远端插件 ID”的表返回。如果记录里有坏 ID,就返回本地文件数据损坏类错误。

调用关系:它依赖 local_paths::load_plugin_share_local_paths 读取磁盘上的映射文件。它本身不联网,通常给需要根据本地路径判断是否已有远端分享的流程使用。

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

delete_remote_plugin_share273–292 ↗
async fn delete_remote_plugin_share(
    config: &RemotePluginServiceConfig,
    auth: Option<&CodexAuth>,
    codex_home: &Path,
    remote_plugin_id: &str,
) -> Result<(), RemotePluginCatalogError>

作用:删除一个远端插件分享,并顺手清掉本地保存的路径映射。用户选择“取消分享”或删除远端插件时会用它。

数据流:进去的是服务配置、登录信息、本地 Codex 目录和远端插件 ID。它确认登录后拼出删除接口地址,带上认证信息发送 HTTP DELETE 请求;服务器返回 204 No Content 才算成功。成功后它尝试删除本地的 ID 到路径记录;如果本地清理失败,只记警告,不让远端删除结果变失败。

调用关系:这是删除分享的主流程。它用 build_reqwest_client 创建网络客户端,用 send_and_expect_status 统一发送请求并检查状态码,再把本地清理交给 local_paths::remove_plugin_share_local_path。

调用图:调用 3 个内部函数(remove_plugin_share_local_path, send_and_expect_status, build_reqwest_client);外部调用 2 个(format!, warn!)。

update_remote_plugin_share_targets294–327 ↗
async fn update_remote_plugin_share_targets(
    config: &RemotePluginServiceConfig,
    auth: Option<&CodexAuth>,
    remote_plugin_id: &str,
    targets: Vec<RemotePluginShareTarget>,
    discoverab

作用:修改一个已分享插件的可见范围和可访问对象。比如把插件改成私有,或者指定某些用户、组、工作区可以看。

数据流:进去的是服务配置、登录信息、远端插件 ID、新的目标列表,以及新的可见性。它先确认登录,把“可更新的可见性”转换成普通可见性;如果是隐藏分享,就自动确保当前工作区在目标列表里。然后它向服务器的分享接口发送 PUT 请求,最后返回服务器确认后的人员列表和实际可见性。

调用关系:这是权限编辑流程的主入口。它复用 ensure_unlisted_workspace_target 来避免隐藏分享漏掉工作区,再通过 HTTP 客户端把 RemotePluginShareUpdateTargetsRequest 发给远端服务。

调用图:调用 2 个内部函数(ensure_unlisted_workspace_target, build_reqwest_client);外部调用 1 个(format!)。

ensure_unlisted_workspace_target329–354 ↗
fn ensure_unlisted_workspace_target(
    auth: &CodexAuth,
    discoverability: Option<RemotePluginShareDiscoverability>,
    targets: Option<Vec<RemotePluginShareTarget>>,
) -> Result<Option<Vec<Remo

作用:保证“隐藏链接”分享至少包含当前工作区这个可读目标。这样插件不会因为权限列表不完整而让当前工作区的人看不到。

数据流:进去的是登录信息、可见性和可选的分享目标列表。它先看可见性是不是 Unlisted,也就是不公开列出但可通过指定范围访问;如果不是,就原样返回目标列表。如果是,它从登录信息里取当前账号或工作区 ID,把目标列表补齐:如果里面还没有这个工作区,就加一个 reader 只读权限。最后返回补过的目标列表。

调用关系:save_remote_plugin_share 和 update_remote_plugin_share_targets 都会在提交权限前调用它。它只做权限列表的安全修正,不负责联网;需要账号 ID 时会向 CodexAuth 询问 get_account_id。

调用图:调用 1 个内部函数(get_account_id);被 2 处调用(save_remote_plugin_share, update_remote_plugin_share_targets)。

fetch_created_workspace_plugins356–372 ↗
async fn fetch_created_workspace_plugins(
    config: &RemotePluginServiceConfig,
    auth: &CodexAuth,
) -> Result<Vec<RemotePluginDirectoryItem>, RemotePluginCatalogError>

作用:把服务器上“我创建的工作区插件”全部取回来。因为服务器可能分页返回,它负责一页一页拿完整。

数据流:进去的是服务配置和登录信息。它从第一页开始请求,把每页里的插件追加到列表;如果响应里还有下一页标记,就继续用这个标记请求下一页;直到没有下一页为止,最后返回完整插件列表。

调用关系:list_remote_plugin_shares 调用它来拿远端分享的原始数据。它把每一次具体取一页的工作交给 get_created_workspace_plugins_page,自己只负责循环翻页和汇总。

调用图:调用 1 个内部函数(get_created_workspace_plugins_page);被 1 处调用(list_remote_plugin_shares);外部调用 1 个(new)。

get_created_workspace_plugins_page374–388 ↗
async fn get_created_workspace_plugins_page(
    config: &RemotePluginServiceConfig,
    auth: &CodexAuth,
    page_token: Option<&str>,
) -> Result<RemotePluginListResponse, RemotePluginCatalogError>

作用:从服务器取一页“我创建的工作区插件”。它是分页列表里的单次网络请求。

数据流:进去的是服务配置、登录信息,以及可选的下一页标记。它拼出列表接口地址,创建带认证的 GET 请求,加上每页数量限制;如果有页标记,就把它也放进查询参数。最后发送请求并把服务器返回的 JSON 解成列表响应对象。

调用关系:fetch_created_workspace_plugins 在翻页循环里反复调用它。它负责和远端接口打交道,翻页判断和结果汇总则由上层函数完成。

调用图:调用 1 个内部函数(build_reqwest_client);被 1 处调用(fetch_created_workspace_plugins);外部调用 1 个(format!)。

create_workspace_plugin_upload390–409 ↗
async fn create_workspace_plugin_upload(
    config: &RemotePluginServiceConfig,
    auth: &CodexAuth,
    filename: &str,
    size_bytes: usize,
    remote_plugin_id: Option<&str>,
) -> Result<Remote

作用:向服务器申请一个临时上传地址。真正上传大文件前,程序需要先拿到这个地址和文件 ID。

数据流:进去的是服务配置、登录信息、压缩包文件名、文件大小,以及可选的已有远端插件 ID。它把这些信息组成请求,告诉服务器“我要上传一个 gzip 文件”;服务器返回 file_id、upload_url 和可能的 etag。函数把这些解码后交回调用者。

调用关系:save_remote_plugin_share 在上传压缩包前先调用它。它只负责申请上传位置,不传文件;后续文件传输由 put_workspace_plugin_upload 完成。

调用图:调用 1 个内部函数(build_reqwest_client);被 1 处调用(save_remote_plugin_share);外部调用 1 个(format!)。

put_workspace_plugin_upload411–439 ↗
async fn put_workspace_plugin_upload(
    upload_url: &str,
    archive_bytes: Vec<u8>,
) -> Result<(), RemotePluginCatalogError>

作用:把插件压缩包真正上传到服务器给的临时地址。可以把它理解成把包裹送到已经预约好的仓库门口。

数据流:进去的是上传地址和压缩包字节内容。它创建 PUT 请求,设置这是 gzip 文件,并加上云存储需要的 x-ms-blob-type 头;然后发送文件内容。服务器返回 200 OK 或 201 Created 才算成功,否则把状态码和响应正文包装成错误。

调用关系:save_remote_plugin_share 在拿到上传地址后调用它。它不需要 Codex 的认证请求包装,因为这个 upload_url 本身就是服务器发给它的临时可用地址。

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

finalize_workspace_plugin_upload441–456 ↗
async fn finalize_workspace_plugin_upload(
    config: &RemotePluginServiceConfig,
    auth: &CodexAuth,
    remote_plugin_id: Option<&str>,
    body: RemoteWorkspacePluginCreateRequest,
) -> Result<R

作用:通知服务器:刚才上传的文件可以正式变成远端插件了。没有这一步,文件只是上传了,还没有变成可分享插件。

数据流:进去的是服务配置、登录信息、可选的远端插件 ID,以及包含 file_id、etag 和分享设置的请求体。如果有远端插件 ID,就拼成更新已有插件的地址;没有就拼成创建新插件的地址。它发送认证 POST 请求,最后返回服务器生成的插件 ID 和分享链接。

调用关系:save_remote_plugin_share 在文件上传成功后调用它,完成最后的“登记发布”。它依赖之前 create_workspace_plugin_upload 给出的 file_id 和 etag,也依赖 put_workspace_plugin_upload 已经把文件传好。

调用图:调用 1 个内部函数(build_reqwest_client);被 1 处调用(save_remote_plugin_share);外部调用 1 个(format!)。

archive_filename458–467 ↗
fn archive_filename(plugin_path: &Path) -> Result<String, RemotePluginCatalogError>

作用:根据插件目录名生成上传用的压缩包文件名。比如目录叫 my-plugin,就生成 my-plugin.tar.gz。

数据流:进去的是插件路径。它取路径最后一段作为插件名,并要求这个名字是合法的 UTF-8 文本;如果路径没有正常目录名,就返回“插件路径无效”的错误。成功时输出一个以 .tar.gz 结尾的文件名。

调用关系:保存分享时的打包阶段会用它来给压缩包起名。它只处理名字,不读取插件内容,也不联网。

调用图:外部调用 2 个(file_name, format!)。

archive_plugin_for_upload469–471 ↗
fn archive_plugin_for_upload(plugin_path: &Path) -> Result<Vec<u8>, RemotePluginCatalogError>

作用:把插件目录压成上传用的字节包,并使用本文件规定的 50MB 最大限制。

数据流:进去的是插件路径。它不自己做压缩细节,而是把路径和默认大小上限交给 archive_plugin_for_upload_with_limit;成功时出来的是压缩包的二进制内容,失败时出来的是适合远端插件目录使用的错误。

调用关系:保存分享时会在后台打包阶段用它。它是一个带默认限制的薄包装,真正的压缩和错误转换由 archive_plugin_for_upload_with_limit 完成。

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

archive_plugin_for_upload_with_limit473–489 ↗
fn archive_plugin_for_upload_with_limit(
    plugin_path: &Path,
    max_bytes: usize,
) -> Result<Vec<u8>, RemotePluginCatalogError>

作用:按指定大小上限把插件目录打成 tar.gz 压缩包,并把底层打包错误翻译成这个模块统一使用的错误。

数据流:进去的是插件路径和最大允许字节数。它调用 pack_plugin_bundle_tar_gz 读取目录并生成压缩包;如果路径无效、压缩包太大或读文件出错,它分别转换成 InvalidPluginPath、ArchiveTooLarge 或 Archive 错误。成功时返回压缩包字节。

调用关系:archive_plugin_for_upload 调用它来使用默认 50MB 限制。测试也容易直接用它传入较小限制,检查超限行为;底层实际打包工作交给 plugin_bundle_archive 模块。

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

send_and_expect_status491–513 ↗
async fn send_and_expect_status(
    request: RequestBuilder,
    url_for_error: &str,
    expected_statuses: &[StatusCode],
) -> Result<(), RemotePluginCatalogError>

作用:发送一个 HTTP 请求,并检查返回状态码是不是预期的。它适合删除这类“不需要解析返回内容,只看成功码”的接口。

数据流:进去的是已经组好的请求、用于报错显示的地址,以及允许的状态码列表。它发送请求;如果网络失败,就返回请求错误。收到响应后读取状态码和正文;如果状态码不在允许列表里,就返回包含状态码和正文的错误;否则返回成功空结果。

调用关系:delete_remote_plugin_share 用它来确认删除接口返回 204 No Content。它把“发送请求、读响应、判断状态码、生成统一错误”这些重复动作集中起来,避免每个调用点自己写一遍。

调用图:被 1 处调用(delete_remote_plugin_share);外部调用 2 个(send, contains)。

core-plugins/src/remote/share/checkout.rs源码 ↗
domain_logicrequest handling

可以把这个文件理解成“把共享文件复制到自己文件夹,并登记到目录册”的流程。它先向远端服务询问插件详情,确认这个插件确实是可签出的共享插件,并检查插件名是否合法。然后它决定插件应该放在本机哪个目录:如果以前签出过,就复用旧位置;如果没有,就放到用户主目录下的插件目录,并确保路径不会跑到主目录外面。接着它下载并解压插件包,再更新个人 marketplace 文件。这里的 marketplace 就像一个插件目录册,告诉系统“这个本地目录里有一个插件”。最后它记录远端插件和本地路径的对应关系,方便下次再找到。重要的是,若中途失败,它会尽量删除刚下载出来的目录,避免留下半成品。

函数细节15
checkout_remote_plugin_share37–162 ↗
async fn checkout_remote_plugin_share(
    config: &RemotePluginServiceConfig,
    auth: Option<&CodexAuth>,
    codex_home: &Path,
    remote_plugin_id: &str,
) -> Result<RemotePluginShareCheckoutRes

作用:这是整个“签出共享插件”流程的主入口。别人分享来的远程插件要变成本机可编辑插件时,就靠它把查询、下载、写目录册、记录映射这些步骤串起来。

数据流:输入远端服务配置、可选登录信息、本机 Codex 目录和远端插件 ID。它先读取远端插件详情,检查插件名、共享来源和本机路径;如果本地还没有,就下载并解压插件;随后更新个人插件市场文件,再记录远端 ID 到本地路径的对应关系。成功时输出一个结果,里面有远端 ID、本地插件 ID、插件名、本地路径、marketplace 路径和远端版本;失败时返回明确错误,并在必要时清理刚创建的目录。

调用关系:它是本文件的总调度员。它会调用路径读取和选择函数来决定放哪儿,调用远端 bundle 下载函数把插件拿下来,调用 update_personal_marketplace 把插件写进个人目录册,最后调用 record_plugin_share_local_path 记住映射;如果后面步骤失败,它会把清理工作交给 clean_up_created_checkout_path。

调用图:调用 11 个内部函数(home_dir, clean_up_created_checkout_path, editable_plugin_path_for_checkout, is_checkout_supported_share_marketplace, load_share_local_paths_for_checkout, update_personal_marketplace, record_plugin_share_local_path, download_and_extract_remote_plugin_bundle_to_path, validate_remote_plugin_bundle, new (+1 more));外部调用 4 个(validate_plugin_segment, UnexpectedResponse, format!, fetch_remote_plugin_detail_with_download_urls)。

is_checkout_supported_share_marketplace164–171 ↗
fn is_checkout_supported_share_marketplace(marketplace_name: &str) -> bool

作用:这个函数判断一个远端插件市场是不是允许“签出共享插件”。它像门卫,只放行几种专门表示“别人分享给我”的 marketplace。

数据流:输入一个 marketplace 名字。它把这个名字和三种受支持的共享 marketplace 常量做比较。输出 true 表示可以继续签出,输出 false 表示这个来源不支持签出。

调用关系:checkout_remote_plugin_share 在拿到远端详情后会用它做资格检查。它不做下载,也不改文件,只负责回答“这个来源能不能走签出流程”。

调用图:被 1 处调用(checkout_remote_plugin_share);外部调用 1 个(matches!)。

load_share_local_paths_for_checkout173–183 ↗
fn load_share_local_paths_for_checkout(
    codex_home: &Path,
) -> Result<BTreeMap<String, AbsolutePathBuf>, RemotePluginCatalogError>

作用:这个函数读取“远端共享插件 ID 对应本地路径”的记录。它的作用是让同一个共享插件下次签出时能找到以前的位置,而不是盲目新建一个目录。

数据流:输入 Codex 的本机目录。它调用本地路径模块读取映射表;如果文件内容坏到无法解析,它把映射当作空表处理;如果是其他读取错误,就包装成远端插件目录错误。输出一个按远端插件 ID 排列的路径表。

调用关系:checkout_remote_plugin_share 在决定本地插件位置前会调用它。它把底层 local_paths::load_plugin_share_local_paths 的磁盘读取结果,整理成签出流程能直接使用的形式。

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

editable_plugin_path_for_checkout185–214 ↗
fn editable_plugin_path_for_checkout(
    home: &AbsolutePathBuf,
    plugin_name: &str,
    remote_plugin_id: &str,
    local_paths: &BTreeMap<String, AbsolutePathBuf>,
) -> Result<(AbsolutePathBuf,

作用:这个函数决定共享插件应该放在本机哪个目录,并判断它是不是已经签出过。它防止新下载的插件覆盖用户已有文件。

数据流:输入用户主目录、插件名、远端插件 ID 和已记录的本地路径表。它先看这个远端 ID 是否已有记录且目录还存在;有的话检查这个路径能不能写进个人 marketplace,然后返回旧路径和“已签出”。没有可用旧路径时,它选择记录里的路径或默认的 home/plugins/插件名,再检查路径是否合法;如果目标已经存在,就报错,避免覆盖。输出本地路径和一个是否已存在的标志。

调用关系:checkout_remote_plugin_share 用它来决定后续是直接复用本地目录,还是下载远端插件包。它会把路径合法性检查交给 ensure_path_can_be_listed_in_personal_marketplace。

调用图:调用 1 个内部函数(ensure_path_can_be_listed_in_personal_marketplace);被 1 处调用(checkout_remote_plugin_share);外部调用 1 个(format!)。

clean_up_created_checkout_path216–232 ↗
fn clean_up_created_checkout_path(
    created_checkout_path: bool,
    local_plugin_path: &AbsolutePathBuf,
    original_err: RemotePluginCatalogError,
) -> RemotePluginCatalogError

作用:这个函数在签出中途失败时做善后。它只在本次流程真的新建了插件目录时才尝试删除,避免误删用户原本的东西。

数据流:输入一个标志,说明是否刚创建过签出路径;再输入本地插件路径和原始错误。如果没有新建路径,它原样返回原始错误;如果新建过,它调用删除函数清掉目录或文件。清理成功时仍返回原始错误;清理失败时返回一个包含“原始错误”和“清理也失败”的新错误。

调用关系:checkout_remote_plugin_share 在下载完成后,如果更新 marketplace 或记录路径失败,会调用它。真正删除文件的动作由 remove_created_checkout_path 完成。

调用图:调用 1 个内部函数(remove_created_checkout_path);被 1 处调用(checkout_remote_plugin_share);外部调用 2 个(UnexpectedResponse, format!)。

remove_created_checkout_path234–240 ↗
fn remove_created_checkout_path(local_plugin_path: &AbsolutePathBuf) -> io::Result<()>

作用:这个函数负责实际删除刚签出的本地路径。它会根据路径是目录还是文件,选择合适的删除方式。

数据流:输入一个绝对路径。它先查看这个路径是不是目录;如果是目录,就递归删除整个目录;否则按单个文件删除。输出成功或系统层面的输入输出错误。

调用关系:它只被 clean_up_created_checkout_path 调用,是清理流程里的“真正动手删除”的小工具。

调用图:调用 1 个内部函数(as_path);被 1 处调用(clean_up_created_checkout_path);外部调用 2 个(remove_dir_all, remove_file)。

ensure_path_can_be_listed_in_personal_marketplace242–247 ↗
fn ensure_path_can_be_listed_in_personal_marketplace(
    home: &AbsolutePathBuf,
    path: &AbsolutePathBuf,
) -> Result<(), RemotePluginCatalogError>

作用:这个函数检查某个本地插件路径能不能写进个人 marketplace 文件。简单说,它确认这个路径能被表示成相对于用户主目录的安全路径。

数据流:输入用户主目录和候选插件路径。它尝试把候选路径转换成个人 marketplace 使用的相对路径;转换成功就表示可登记,返回成功;转换失败就返回路径不合法的错误。

调用关系:editable_plugin_path_for_checkout 在接受一个本地路径前会调用它。它本身把细节交给 personal_marketplace_relative_plugin_path。

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

update_personal_marketplace254–341 ↗
fn update_personal_marketplace(
    home: &AbsolutePathBuf,
    plugin_name: &str,
    local_plugin_path: &AbsolutePathBuf,
    install_policy: PluginInstallPolicy,
    auth_policy: PluginAuthPolicy,

作用:这个函数把签出的插件写进用户的个人插件市场文件。这个文件像一本本地插件目录册,系统靠它知道哪里有插件、插件叫什么、安装和登录策略是什么。

数据流:输入用户主目录、插件名、本地插件路径、安装策略、认证策略和可选分类。它算出 marketplace 文件路径和插件相对路径,读取或新建 JSON 文件,检查里面的 name 和 plugins 字段是否合理,然后生成插件条目;如果同名插件已存在且路径相同,就更新它,否则新增。最后把 JSON 美化后原子写入磁盘。输出 marketplace 的名字和文件路径。

调用关系:checkout_remote_plugin_share 下载或复用插件目录后会调用它,把本地目录登记进个人 marketplace。它会调用 read_or_create_personal_marketplace 读文件,personal_marketplace_plugin_entry 造条目,personal_marketplace_relative_plugin_path 算路径,write_json_atomically 安全写回。

调用图:调用 6 个内部函数(invalid_marketplace_file, personal_marketplace_plugin_entry, personal_marketplace_relative_plugin_path, read_or_create_personal_marketplace, write_json_atomically, join);被 1 处调用(checkout_remote_plugin_share);外部调用 3 个(validate_plugin_segment, format!, to_string_pretty)。

read_or_create_personal_marketplace343–364 ↗
fn read_or_create_personal_marketplace(
    marketplace_path: &Path,
) -> Result<JsonValue, RemotePluginCatalogError>

作用:这个函数读取个人 marketplace JSON 文件;如果文件还不存在,就造一个默认的空目录册。这样新用户不需要手动创建配置文件。

数据流:输入 marketplace 文件路径。它尝试读取文本并解析成 JSON;如果文件不存在,就返回一个带默认 name、显示名和空 plugins 列表的 JSON 对象;如果文件存在但 JSON 写坏了,就返回“marketplace 文件无效”的错误;如果读取失败,则返回读取错误。

调用关系:update_personal_marketplace 在修改目录册之前会调用它,拿到当前内容或一份默认内容。它不负责写回,只负责准备可修改的 JSON。

调用图:被 1 处调用(update_personal_marketplace);外部调用 5 个(UnexpectedResponse, format!, json!, from_str, read_to_string)。

personal_marketplace_plugin_entry366–391 ↗
fn personal_marketplace_plugin_entry(
    plugin_name: &str,
    relative_plugin_path: &str,
    install_policy: PluginInstallPolicy,
    auth_policy: PluginAuthPolicy,
    category: Option<String>,
)

作用:这个函数生成一个插件在个人 marketplace 里的 JSON 条目。它把插件名、本地路径、安装规则和登录规则整理成系统能识别的格式。

数据流:输入插件名、相对路径、安装策略、认证策略和可选分类。它创建一个 JSON 对象,说明插件来源是本地路径,并填入 policy 字段;如果分类存在且不是空白,还会加上 category。输出这一个插件条目。

调用关系:update_personal_marketplace 需要新增或替换插件记录时会调用它。它会使用安装策略和认证策略的转换函数,把程序里的枚举值变成 JSON 里固定的大写字符串。

调用图:被 1 处调用(update_personal_marketplace);外部调用 1 个(json!)。

plugin_install_policy_value393–399 ↗
fn plugin_install_policy_value(policy: PluginInstallPolicy) -> &'static str

作用:这个函数把安装策略从程序内部的枚举值翻译成 JSON 文件里的字符串。枚举值可以理解成几种固定选项,字符串则是写进配置文件的文字版本。

数据流:输入一个安装策略:不可安装、可安装、或默认安装。它按固定对应关系返回 NOT_AVAILABLE、AVAILABLE 或 INSTALLED_BY_DEFAULT。它不读文件,也不修改任何状态。

调用关系:personal_marketplace_plugin_entry 在拼出 policy.installation 字段时会用到这个翻译结果,让 marketplace 文件保持远端协议要求的写法。

plugin_auth_policy_value401–406 ↗
fn plugin_auth_policy_value(policy: PluginAuthPolicy) -> &'static str

作用:这个函数把认证策略翻译成 JSON 里使用的字符串。认证策略说的是插件什么时候需要登录授权:安装时,或使用时。

数据流:输入一个认证策略。它把 OnInstall 转成 ON_INSTALL,把 OnUse 转成 ON_USE。输出固定字符串,不产生副作用。

调用关系:personal_marketplace_plugin_entry 在拼出 policy.authentication 字段时会用到它,保证写入 marketplace 的认证规则格式统一。

personal_marketplace_relative_plugin_path408–449 ↗
fn personal_marketplace_relative_plugin_path(
    home: &AbsolutePathBuf,
    local_plugin_path: &AbsolutePathBuf,
) -> Result<String, RemotePluginCatalogError>

作用:这个函数把本地插件的绝对路径转换成个人 marketplace 能保存的相对路径。它也顺手做安全检查,防止把主目录外的路径登记进去。

数据流:输入用户主目录和插件绝对路径。它先确认插件路径在用户主目录里面,再逐段检查路径:普通目录名可以保留,当前目录符号会忽略,父目录、根目录、磁盘前缀或非 UTF-8 名字都会报错。最后输出形如 ./plugins/foo 的相对路径;如果路径就是主目录本身,也会报错。

调用关系:ensure_path_can_be_listed_in_personal_marketplace 用它做纯检查,update_personal_marketplace 用它生成真正写进 JSON 的路径。它是防止 marketplace 记录危险路径的关键小闸门。

调用图:调用 2 个内部函数(as_path, to_path_buf);被 2 处调用(ensure_path_can_be_listed_in_personal_marketplace, update_personal_marketplace);外部调用 2 个(new, format!)。

invalid_marketplace_file451–456 ↗
fn invalid_marketplace_file(path: &Path, message: &str) -> RemotePluginCatalogError

作用:这个函数统一生成“个人 marketplace 文件有问题”的错误。这样上层看到的错误都带着出问题的文件路径和清楚原因。

数据流:输入文件路径和错误说明文字。它把路径复制到错误对象里,把说明文字作为原因。输出一个 RemotePluginCatalogError::InvalidPluginPath 错误值。

调用关系:update_personal_marketplace 和 read_or_create_personal_marketplace 在发现 JSON 结构不对、字段类型不对或内容非法时会调用它,避免每个地方都手写同样的错误包装。

调用图:被 1 处调用(update_personal_marketplace);外部调用 1 个(to_path_buf)。

write_json_atomically458–470 ↗
fn write_json_atomically(write_path: &Path, contents: &str) -> io::Result<()>

作用:这个函数用“先写临时文件,再替换目标文件”的方式保存 JSON。这样即使写到一半出问题,也更不容易留下半截损坏的配置文件。

数据流:输入目标文件路径和要写入的文本。它先找到父目录并确保目录存在,然后在同一目录创建临时文件,写入全部内容,最后把临时文件持久化为目标文件。输出成功或输入输出错误。

调用关系:update_personal_marketplace 在准备好新的 marketplace JSON 后调用它。它是最后落盘的一步,负责让文件更新尽量安全。

调用图:被 1 处调用(update_personal_marketplace);外部调用 3 个(parent, create_dir_all, new_in)。

发现与连接器策略

这些文件计算连接器可用性、工具策略、可发现的插件建议、mention 解析,以及 install-suggestion 和选择流程使用的相关工具归一化。

connectors/src/app_tool_policy.rs源码 ↗
domain_logic工具暴露前和工具调用前,根据配置计算可用性与审批策略

这段代码像一个工具门卫。每个连接器可能提供很多工具,但不是所有工具都应该随便开放:有的可能会删数据,有的可能会访问外部世界。这里先从配置层里读出 apps 配置,再叠加强制要求配置,比如某个应用被管理员关掉。之后,对每个工具算出一份 AppToolPolicy:enabled 表示能不能用,approval 表示是自动允许还是需要审批。它还支持按应用、按工具名、按工具标题设置规则,并且会用“危险操作提示”和“外部访问提示”来兜底判断。重要的是,AppToolPolicyEvaluator 可以复用同一份配置快照,避免每个工具都重新解析配置。

函数细节10
AppToolPolicy::default15–20 ↗
fn default() -> Self

作用:给工具策略提供一个最宽松的默认值:工具默认可用,并且审批模式是 Auto,也就是系统认为可以自动通过。

数据流:不需要输入任何东西 → 直接构造一份默认策略 → 输出 enabled 为 true、approval 为 Auto 的 AppToolPolicy,不改动外部数据。

调用关系:当调用工具时如果没有找到更具体的配置,handle_mcp_tool_call 会用这份默认策略当兜底,保证系统不会因为缺少配置就崩掉。

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

AppToolPolicyEvaluator::new43–47 ↗
fn new(config_layer_stack: &'a ConfigLayerStack) -> Self

作用:用一整套配置层创建一个策略评估器。调用方可以创建一次,然后给同一批工具反复算策略,省得重复解析配置。

数据流:输入 ConfigLayerStack,也就是多层配置叠在一起后的对象 → 读取普通 apps 配置,也读取 requirements.toml 里的强制 apps 要求 → 交给内部构造步骤,输出一个 AppToolPolicyEvaluator。

调用关系:filter_codex_apps_mcp_tools 在筛选要暴露的工具时会用它,handle_mcp_tool_call 在真正调用工具前也会用它;它会调用 apps_config_from_layer_stack 读普通配置,并通过 requirements_toml 拿到强制配置。

调用图:调用 2 个内部函数(requirements_toml, apps_config_from_layer_stack);被 3 处调用(policy_from_config_parts, handle_mcp_tool_call, filter_codex_apps_mcp_tools);外部调用 1 个(from_parts)。

AppToolPolicyEvaluator::policy49–56 ↗
fn policy(&self, input: AppToolPolicyInput<'_>) -> AppToolPolicy

作用:给某一个具体工具算最终政策:能不能用,以及要不要审批。调用方把工具的信息交进来,就能得到明确答案。

数据流:输入 AppToolPolicyInput,里面有连接器编号、工具名、标题、是否危险、是否会访问外部世界等线索 → 先查强制配置里有没有这个工具的审批要求,再结合普通 apps 配置计算 → 输出 AppToolPolicy。

调用关系:这是评估器对外最核心的动作。它先调用 managed_app_tool_approval 找强制审批规则,再把结果交给 app_tool_policy_from_apps_config 做完整判断。

调用图:调用 2 个内部函数(app_tool_policy_from_apps_config, managed_app_tool_approval)。

AppToolPolicyEvaluator::from_parts58–66 ↗
fn from_parts(
        apps_config: Option<AppsConfigToml>,
        requirements_apps_config: Option<&'a AppsRequirementsToml>,
    ) -> Self

作用:用已经准备好的普通配置和强制配置拼出一个评估器。它主要给内部和测试使用,方便直接喂入配置片段。

数据流:输入可选的 AppsConfigToml 和可选的 AppsRequirementsToml 引用 → 先把强制要求应用到普通配置上,得到真正生效的 apps 配置 → 输出 AppToolPolicyEvaluator,保存这份处理后的配置快照。

调用关系:测试 evaluator_reuses_one_snapshot_across_tools 会直接用它验证评估器是否复用同一份配置。它把合并配置这件事交给 effective_apps_config。

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

apps_config_from_layer_stack70–79 ↗
fn apps_config_from_layer_stack(
    config_layer_stack: &ConfigLayerStack,
) -> Option<AppsConfigToml>

作用:从多层配置里把 apps 这一块读出来,并转成程序能理解的结构。没有合法 apps 配置时,就返回空。

数据流:输入 ConfigLayerStack → 先取 effective_config,也就是合并后的最终配置,再找到里面的 apps 表,并尝试反序列化成 AppsConfigToml;反序列化就是把配置文本变成 Rust 结构 → 输出 Some 配置或 None。

调用关系:AppToolPolicyEvaluator::new 会调用它,作为创建评估器的第一步。它只负责读取普通、非强制的 apps 配置,不处理 requirements.toml。

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

app_is_enabled81–92 ↗
fn app_is_enabled(apps_config: &AppsConfigToml, connector_id: Option<&str>) -> bool

作用:判断某个应用整体是不是开启的。工具再细的规则也要先服从应用总开关。

数据流:输入 apps 配置和可选的连接器编号 → 先看全局默认 enabled,默认没写就是 true;再看这个具体应用有没有覆盖这个开关 → 输出 true 或 false。

调用关系:app_tool_policy_from_apps_config 会调用它。它像总电闸:如果应用被关掉,后面的单个工具配置再怎么允许,也不能启用。

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

effective_apps_config94–106 ↗
fn effective_apps_config(
    apps_config: Option<AppsConfigToml>,
    requirements_apps_config: Option<&AppsRequirementsToml>,
) -> Option<AppsConfigToml>

作用:算出真正生效的 apps 配置。它把用户配置和强制要求合并,得到后续评估工具时统一使用的一份结果。

数据流:输入可选普通 apps 配置和可选强制 apps 配置 → 如果普通配置没有,就从默认空配置开始;再把强制限制写进去 → 如果最终确实有内容就输出 Some,否则输出 None。

调用关系:AppToolPolicyEvaluator::from_parts 会调用它。它内部把具体的强制限制应用工作交给 apply_requirements_apps_constraints。

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

apply_requirements_apps_constraints108–122 ↗
fn apply_requirements_apps_constraints(
    apps_config: &mut AppsConfigToml,
    requirements_apps_config: Option<&AppsRequirementsToml>,
)

作用:把 requirements 配置里的硬性限制写进 apps 配置。现在它主要处理一种情况:强制关闭某个应用。

数据流:输入一份可修改的 AppsConfigToml 和可选的 AppsRequirementsToml → 如果强制配置里某个 app 写了 enabled = false,就在 apps 配置里把这个 app 设为关闭 → 不返回新值,而是直接改传入的 apps_config。

调用关系:effective_apps_config 会调用它。它保证管理员或系统要求的禁用规则优先落地,避免普通配置把被禁止的应用重新打开。

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

managed_app_tool_approval124–138 ↗
fn managed_app_tool_approval(
    requirements_apps_config: Option<&AppsRequirementsToml>,
    connector_id: Option<&str>,
    tool_name: &str,
) -> Option<AppToolApproval>

作用:查找强制配置里有没有指定某个工具的审批模式。这里的“强制”意思是优先级最高,普通配置不能覆盖。

数据流:输入可选强制 apps 配置、可选连接器编号和工具名 → 如果缺连接器编号、缺强制配置、找不到应用、找不到工具,就返回 None;如果找到 approval_mode,就返回它 → 不修改任何数据。

调用关系:AppToolPolicyEvaluator::policy 会先调用它。得到的结果会传给 app_tool_policy_from_apps_config,并在最终审批判断里排在最高优先级。

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

app_tool_policy_from_apps_config140–203 ↗
fn app_tool_policy_from_apps_config(
    apps_config: Option<&AppsConfigToml>,
    input: AppToolPolicyInput<'_>,
    managed_approval: Option<AppToolApproval>,
) -> AppToolPolicy

作用:这是最终算工具策略的核心函数。它把应用开关、工具单独配置、默认规则、危险提示和强制审批要求全部放在一起,算出最后答案。

数据流:输入可选 apps 配置、一个工具输入信息、可选强制审批模式 → 如果没有 apps 配置,就返回默认启用,并使用强制审批或 Auto;如果有配置,就先找对应应用和工具配置,工具可以按 tool_name 找,也可以按 tool_title 找 → 审批模式按“强制配置、工具配置、应用默认、Auto”的顺序选择;启用状态先看应用总开关,再看工具开关,再看应用工具默认开关,最后用 destructive 和 open_world 这些风险开关兜底 → 输出 AppToolPolicy。

调用关系:AppToolPolicyEvaluator::policy 会调用它来得到最终结果。它会调用 app_is_enabled 先检查应用总开关,也会在没有更具体规则时借用默认策略思想作为兜底。

调用图:调用 1 个内部函数(app_is_enabled);被 1 处调用(policy);外部调用 1 个(default)。

utils/plugins/src/mcp_connector.rs源码 ↗
utilcross-cutting

这里的 MCP 连接器可以理解成外部工具或服务的接入口。不是所有接入口都允许使用,所以文件里先放了两份“黑名单”:普通场景一份,第一方聊天场景一份。第一方聊天就是项目自己官方聊天入口,它有自己的额外限制。检查时,代码会先看当前请求来自哪里,再选对应黑名单,只要连接器编号不在名单里,就算允许。另一个功能是清洗名字:把名字里的英文字母和数字保留下来并转成小写,其他符号都变成分隔符,最后再把短横线换成下划线。这样做是为了让名字更像程序里安全可用的标识,避免空格、中文标点或特殊符号造成麻烦。如果名字清洗完什么都不剩,就默认叫“app”。

函数细节4
is_connector_id_allowed15–17 ↗
fn is_connector_id_allowed(connector_id: &str) -> bool

作用:这是给外部使用的入口,用来判断一个连接器编号当前能不能用。调用者不用自己关心“当前是谁发起的”,它会自动去查。

数据流:输入是一个连接器编号。它先读取当前的 originator,也就是“请求来源”信息,再把连接器编号和来源一起交给更底层的判断函数。输出是 true 或 false:true 表示允许,false 表示这个编号在当前来源下被禁止。

调用关系:它位于连接器门禁检查的最外层。需要判断连接器是否可用的代码会调用它;它自己会先调用 originator 取得当前来源,再把真正的判断交给 is_connector_id_allowed_for_originator。

调用图:调用 2 个内部函数(originator, is_connector_id_allowed_for_originator)。

is_connector_id_allowed_for_originator19–27 ↗
fn is_connector_id_allowed_for_originator(connector_id: &str, originator_value: &str) -> bool

作用:这个函数做真正的黑名单判断:同一个连接器,在不同来源下可能规则不同。它让代码可以按来源选择不同的禁用名单。

数据流:输入是连接器编号和来源字符串。它先用 is_first_party_chat_originator 判断这个来源是不是官方聊天入口;如果是,就使用第一方聊天专用黑名单,否则使用普通黑名单。最后检查连接器编号是否在名单里,不在就返回 true,在就返回 false。

调用关系:它是 is_connector_id_allowed 背后的核心判断步骤。is_connector_id_allowed 负责拿到当前来源,它负责根据来源挑名单并做最终决定;判断来源类型时,它会调用 is_first_party_chat_originator。

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

sanitize_name29–31 ↗
fn sanitize_name(name: &str) -> String

作用:这个函数把用户或外部系统给的名字整理成更适合程序使用的名字。它主要把不稳定、不规整的名称变成小写、下划线风格的安全名称。

数据流:输入是原始名字字符串。它先调用 sanitize_slug,把名字变成只含小写字母、数字和短横线的形式;然后把短横线全部换成下划线。输出是清洗后的新字符串,不会改动原始输入。

调用关系:它是名字清洗功能对外常用的入口,会被 normalize_codex_apps_callable_name 调用,用来生成可调用、较规范的应用名称。它把第一步清洗交给 sanitize_slug,自己只负责最后把短横线风格改成下划线风格。

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

sanitize_slug33–48 ↗
fn sanitize_slug(name: &str) -> String

作用:这个函数把任意名字先整理成网页短名那种格式:小写字母、数字和短横线。它是更基础的一层清洗工具。

数据流:输入是原始名字。它逐个看字符:如果是英文字母或数字,就转成小写后保留;如果是空格、标点、其他符号,就替换成短横线。处理完后,它会去掉开头和结尾多余的短横线;如果结果为空,就返回默认名字“app”。输出是整理后的字符串。

调用关系:它只被 sanitize_name 调用,是名字清洗流水线的第一步。为了少做一些重复分配,它会按原名字长度预留字符串空间;之后 sanitize_name 再把它生成的短横线形式转换成下划线形式。

调用图:被 1 处调用(sanitize_name);外部调用 1 个(with_capacity)。

utils/plugins/src/plugin_namespace.rs源码 ↗
domain_logiccross-cutting

插件就像一个个独立的小工具包,技能文件只是工具包里的某个说明页。系统拿到一个技能文件路径时,需要知道它属于哪个工具包,也就是插件命名空间。这个文件做的事就是:从技能文件所在位置开始,沿着父目录一层层往上找,看看哪里有可识别的插件说明文件。目前它认两种位置:.codex-plugin/plugin.json 和 .claude-plugin/plugin.json,而且有先后顺序。找到后,它会读取 JSON 文件里的 name 字段;如果 name 是空的,就退而求其次用插件根目录的文件夹名。这里既有直接查本地磁盘的函数,也有通过 ExecutorFileSystem(一层文件系统接口,方便真实磁盘或受控环境都能用)异步读取的函数。下面的测试会建临时目录,验证标准路径和备用路径都能被识别。

函数细节5
find_plugin_manifest_path13–18 ↗
fn find_plugin_manifest_path(plugin_root: &Path) -> Option<PathBuf>

作用:这个函数在一个给定的插件根目录下面,按固定顺序寻找插件说明文件。有人只想快速知道“这个目录是不是插件、说明文件在哪儿”时会用它。

数据流:进去的是一个目录路径 plugin_root → 它把两个候选相对路径拼到这个目录后面,并检查哪个真实存在且是文件 → 出来的是第一个找到的 plugin.json 路径;如果两个都没有,就返回空。

调用关系:它是一个简单的本地磁盘查找工具。测试 tests::uses_name_from_alternate_discoverable_manifest_path 会用它确认备用说明文件路径也能被发现;主异步流程则使用更通用的 plugin_manifest_name 来适配 ExecutorFileSystem。

plugin_manifest_name27–58 ↗
async fn plugin_manifest_name(
    fs: &dyn ExecutorFileSystem,
    plugin_root: &AbsolutePathBuf,
) -> Option<String>

作用:这个函数负责在某个可能的插件根目录里找到说明文件,并读出插件名字。它是把“目录”变成“插件命名空间名字”的关键一步。

数据流:进去的是文件系统接口 fs 和一个绝对目录路径 plugin_root → 它按顺序拼出候选 plugin.json 路径,把路径转成 URI(一种统一表示文件位置的格式),先查元数据确认是不是文件,再读取文本内容,然后把 JSON 解析出 name 字段 → 出来的是插件名字;如果找不到文件、读不了、JSON 解析失败,就返回空。如果 name 为空白,它会用插件根目录的文件夹名当名字。

调用关系:它被 plugin_namespace_for_skill_path 反复调用。plugin_namespace_for_skill_path 每往上走到一个祖先目录,就请它判断“这里是不是插件根目录、能不能拿到插件名”。它自己把读取文件、检查文件信息、解析 JSON 这些细活交给文件系统接口和 serde_json 完成。

调用图:调用 3 个内部函数(read_file_text, join, from_abs_path);被 1 处调用(plugin_namespace_for_skill_path);外部调用 3 个(get_metadata, from_str, file_name)。

plugin_namespace_for_skill_path62–72 ↗
async fn plugin_namespace_for_skill_path(
    fs: &dyn ExecutorFileSystem,
    path: &AbsolutePathBuf,
) -> Option<String>

作用:这个函数从一个技能文件路径出发,找出离它最近的插件名字。这样系统就能知道这个技能属于哪个插件,而不是只看到一个孤零零的文件。

数据流:进去的是文件系统接口 fs 和技能文件的绝对路径 path → 它从这个路径开始,依次查看自己和每一级父目录,请 plugin_manifest_name 判断那里有没有有效插件说明文件 → 一旦找到名字就立刻返回;一路找到顶层都没有,就返回空。

调用关系:它是这个文件对外最重要的异步入口。上层代码在加载或识别技能文件时会调用它;它不自己解析说明文件,而是把每个候选目录交给 plugin_manifest_name,由后者完成查文件和读名字。

调用图:调用 2 个内部函数(ancestors, plugin_manifest_name)。

tests::uses_manifest_name86–104 ↗
async fn uses_manifest_name()

作用:这个测试验证标准位置 .codex-plugin/plugin.json 里的 name 会被正确当作插件名字。它防止以后改代码时,不小心把最常用的插件识别方式弄坏。

数据流:进去的是测试现场临时创建的一组目录和文件 → 测试创建一个插件目录、一个技能文件、一个标准 plugin.json,并在 JSON 里写入 name 为 sample → 调用 plugin_namespace_for_skill_path 后,期望出来的结果正好是 sample。

调用关系:它模拟真实使用场景:先准备文件,再调用 plugin_namespace_for_skill_path。这个测试间接覆盖 plugin_manifest_name 的查找、读取和 JSON 解析行为。

调用图:外部调用 4 个(assert_eq!, create_dir_all, write, tempdir)。

tests::uses_name_from_alternate_discoverable_manifest_path107–124 ↗
async fn uses_name_from_alternate_discoverable_manifest_path()

作用:这个测试验证备用位置 .claude-plugin/plugin.json 也能被识别。它保证系统兼容另一种插件目录布局。

数据流:进去的是测试临时生成的插件目录、技能文件和备用 plugin.json → 测试把 name 写成 sample,然后调用 plugin_namespace_for_skill_path 看能否得到 sample,同时调用 find_plugin_manifest_path 看能否找到这个备用说明文件 → 出来的是两个断言都通过,说明异步命名空间查找和本地路径查找都支持备用位置。

调用关系:它同时检查两条路线:plugin_namespace_for_skill_path 负责从技能文件找到插件名,find_plugin_manifest_path 负责从插件根目录找到说明文件路径。这样可以确认同一组可发现路径在不同函数里保持一致。

调用图:外部调用 4 个(assert_eq!, create_dir_all, write, tempdir)。

core/src/connectors.rs源码 ↗
orchestrationcross-cutting:列应用、查工具元数据、构建初始上下文、刷新连接器和工具推荐时都会用到

这个文件像一个“应用连接器前台接待员”。它先看用户是否登录、功能开关是否允许使用 Apps;再通过 MCP(Model Context Protocol,一种让程序列出和调用外部工具的协议)去问 Codex Apps 服务器有哪些工具;然后把工具归并成一个个应用连接器。为了不每次都重新问服务器,它还做了一个短期内存缓存,并用账号、用户、服务地址这些信息当作缓存钥匙,避免拿错人的结果。它还会按配置把连接器标成启用或禁用,过滤掉当前来源不允许显示的连接器,并补上这些连接器来自哪些插件。工具推荐也靠它把“目录里的连接器”和“插件带来的连接器”合成一份可推荐清单。整体上,它连接了认证、配置、MCP 工具列表、缓存和插件来源,是应用连接器能被正确展示和使用的关键中转站。

函数细节18
list_accessible_connectors_from_mcp_tools72–82 ↗
async fn list_accessible_connectors_from_mcp_tools(
    config: &Config,
) -> anyhow::Result<Vec<AppInfo>>

作用:这是最简单的入口:返回当前用户可以访问的连接器列表。调用者不关心准备状态时,用它就够了。

数据流:进去的是全局配置 config → 它用默认选项去调用更完整的查询函数,不强制重新拉取 → 出来的是连接器数组;如果底层查询失败,就把错误原样交回去。

调用关系:lookup_mcp_tool_metadata 需要查工具属于哪些连接器时会用它。它自己不做重活,而是把任务交给 list_accessible_connectors_from_mcp_tools_with_options_and_status,再只取其中的 connectors 部分。

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

list_accessible_and_enabled_connectors_from_manager84–95 ↗
async fn list_accessible_and_enabled_connectors_from_manager(
    mcp_connection_manager: &McpConnectionManager,
    config: &Config,
) -> Vec<AppInfo>

作用:这个函数从一个已经存在的 MCP 连接管理器里拿工具清单,然后只留下“用户能访问并且配置上启用”的连接器。

数据流:进去的是 MCP 连接管理器和配置 → 它先读取所有 MCP 工具,把工具整理成连接器,再按配置补上启用状态 → 出来的是既可访问又启用的连接器列表。

调用关系:build_initial_context 在准备初始运行环境时会用它。它依赖连接管理器的 list_all_tools 拿原始工具,再交给 accessible_connectors_from_mcp_tools 和 with_app_enabled_state 做整理和开关判断。

调用图:调用 3 个内部函数(list_all_tools, accessible_connectors_from_mcp_tools, with_app_enabled_state);被 1 处调用(build_initial_context)。

list_tool_suggest_discoverable_tools_with_auth97–130 ↗
async fn list_tool_suggest_discoverable_tools_with_auth(
    config: &Config,
    plugins_manager: &PluginsManager,
    auth: Option<&CodexAuth>,
    accessible_connectors: &[AppInfo],
    loaded_plug

作用:这个函数给“工具建议”功能准备可发现的工具,既包括连接器,也包括插件。简单说,就是决定哪些外部能力可以被推荐给用户安装或使用。

数据流:进去的是配置、插件管理器、可选登录信息、已可访问连接器和已加载插件的连接器 ID → 它先算出应该参与推荐的连接器 ID,再读取缓存里的连接器目录,合并插件连接器,过滤出真正适合推荐的连接器,同时再查可推荐插件 → 出来的是一份统一的 DiscoverableTool 列表。

调用关系:built_tools 构建工具列表时会调用它。它把连接器侧的筛选工作交给 tool_suggest_connector_ids、cached_directory_connectors_for_tool_suggest_with_auth、merge_plugin_connectors 和 filter_tool_suggest_discoverable_connectors;插件侧则交给 list_tool_suggest_discoverable_plugins,最后把两边结果合并。

调用图:调用 5 个内部函数(filter_tool_suggest_discoverable_connectors, merge_plugin_connectors, cached_directory_connectors_for_tool_suggest_with_auth, tool_suggest_connector_ids, originator);被 1 处调用(built_tools);外部调用 1 个(list_tool_suggest_discoverable_plugins)。

list_cached_accessible_connectors_from_mcp_tools132–151 ↗
async fn list_cached_accessible_connectors_from_mcp_tools(
    config: &Config,
) -> Option<Vec<AppInfo>>

作用:这个函数只尝试读缓存里的可访问连接器,不主动连接 MCP 服务器。适合想快速显示旧结果、避免等待网络或服务器启动的地方。

数据流:进去的是配置 → 它读取当前认证信息,先确认 Apps 功能对这个账号是否开启;如果没开启,直接返回空列表;如果开启,就按账号和服务地址生成缓存钥匙并读缓存 → 出来的是可选的连接器列表,读不到缓存就返回 None。

调用关系:plugin_apps_needing_auth_for_install 和 lookup_mcp_tool_metadata 会用它做快速查询。它会调用 accessible_connectors_cache_key 生成钥匙,再调用 read_cached_accessible_connectors 取数据,并在返回前过滤掉不允许当前来源看到的连接器。

调用图:调用 3 个内部函数(accessible_connectors_cache_key, read_cached_accessible_connectors, shared_from_config);被 2 处调用(plugin_apps_needing_auth_for_install, lookup_mcp_tool_metadata);外部调用 1 个(new)。

refresh_accessible_connectors_cache_from_mcp_tools153–168 ↗
fn refresh_accessible_connectors_cache_from_mcp_tools(
    config: &Config,
    auth: Option<&CodexAuth>,
    mcp_tools: &[ToolInfo],
)

作用:这个函数用一批刚拿到的 MCP 工具刷新连接器缓存。它的作用是让后续查询能更快拿到最新结果。

数据流:进去的是配置、可选认证信息和 MCP 工具列表 → 如果 Apps 功能没开,它什么也不做;如果开了,它把工具整理成连接器,过滤掉不允许显示的项,然后写进缓存 → 结果没有返回值,但全局缓存会被更新。

调用关系:refresh_codex_apps_after_connector_auth 和 refresh_missing_requested_connectors 在刷新应用连接器后会调用它。它把工具转连接器的活交给 accessible_connectors_from_mcp_tools,把保存缓存的活交给 write_cached_accessible_connectors。

调用图:调用 5 个内部函数(filter_disallowed_connectors, accessible_connectors_cache_key, accessible_connectors_from_mcp_tools, write_cached_accessible_connectors, originator);被 2 处调用(refresh_codex_apps_after_connector_auth, refresh_missing_requested_connectors)。

list_accessible_connectors_from_mcp_tools_with_options170–179 ↗
async fn list_accessible_connectors_from_mcp_tools_with_options(
    config: &Config,
    force_refetch: bool,
) -> anyhow::Result<Vec<AppInfo>>

作用:这个函数和普通查询类似,但多了一个 force_refetch 参数,可以要求强制重新拉取而不是优先用缓存。

数据流:进去的是配置和是否强制刷新 → 它调用带状态的完整版本 → 出来的是连接器列表,准备状态被丢弃不用。

调用关系:它是 list_accessible_connectors_from_mcp_tools_with_options_and_status 的轻量包装。调用者想控制刷新但不关心 codex_apps_ready 状态时,可以用它。

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

list_accessible_connectors_from_mcp_tools_with_options_and_status181–201 ↗
async fn list_accessible_connectors_from_mcp_tools_with_options_and_status(
    config: &Config,
    force_refetch: bool,
) -> anyhow::Result<AccessibleConnectorsStatus>

作用:这个函数会返回连接器列表,也会告诉调用者 Codex Apps 服务器是否已经准备好。它还会自己临时创建运行环境管理器,方便没有现成环境的调用者使用。

数据流:进去的是配置和是否强制刷新 → 它先根据配置里的可执行文件路径创建运行路径,再从 codex_home 创建 EnvironmentManager(环境管理器,用来准备运行外部工具所需的环境)→ 然后把这些交给下一层 → 出来的是 AccessibleConnectorsStatus,里面有连接器和 ready 状态。

调用关系:list_accessible_connectors_from_mcp_tools 和 list_accessible_connectors_from_mcp_tools_with_options 都会调用它。它自己主要负责补齐环境管理器,然后把真正查询交给 list_accessible_connectors_from_mcp_tools_with_environment_manager。

调用图:调用 3 个内部函数(list_accessible_connectors_from_mcp_tools_with_environment_manager, from_codex_home, from_optional_paths);被 2 处调用(list_accessible_connectors_from_mcp_tools, list_accessible_connectors_from_mcp_tools_with_options);外部调用 1 个(new)。

list_accessible_connectors_from_mcp_tools_with_environment_manager203–217 ↗
async fn list_accessible_connectors_from_mcp_tools_with_environment_manager(
    config: &Config,
    force_refetch: bool,
    environment_manager: Arc<EnvironmentManager>,
) -> anyhow::Result<Accessi

作用:这个函数适合调用者已经有 EnvironmentManager 的情况,避免重复创建环境。它会补上插件管理器和 MCP 管理器,然后继续查询连接器。

数据流:进去的是配置、是否强制刷新和环境管理器 → 它创建 PluginsManager(插件管理器)和 McpManager(MCP 配置与连接相关的管理器)→ 出来的是下一层查询返回的连接器状态。

调用关系:list_accessible_connectors_from_mcp_tools_with_options_and_status 会调用它。它处在“准备依赖对象”的中间层,最后把工作交给 list_accessible_connectors_from_mcp_tools_with_mcp_manager。

调用图:调用 3 个内部函数(new, list_accessible_connectors_from_mcp_tools_with_mcp_manager, new);被 1 处调用(list_accessible_connectors_from_mcp_tools_with_options_and_status);外部调用 1 个(new)。

list_accessible_connectors_from_mcp_tools_with_mcp_manager219–368 ↗
async fn list_accessible_connectors_from_mcp_tools_with_mcp_manager(
    config: &Config,
    force_refetch: bool,
    environment_manager: Arc<EnvironmentManager>,
    mcp_manager: Arc<McpManager>,
)

作用:这是本文件里最核心的查询流程:它决定是否用缓存、是否启动 MCP 连接、是否等待 Codex Apps 服务器准备好,以及最后该返回哪些连接器。

数据流:进去的是配置、是否强制刷新、环境管理器和 MCP 管理器 → 它读取认证和功能开关;能用缓存且不强制刷新时,直接返回缓存;否则只保留 Codex Apps 这个 MCP 服务器,计算认证状态,创建 MCP 连接管理器,必要时强制刷新工具缓存或等待服务器启动;接着把工具整理成连接器、过滤不允许显示的项、写入缓存、补上插件来源,最后关闭 MCP 连接 → 出来的是连接器列表和 codex_apps_ready 状态。

调用关系:apps_list_response 和上一层 list_accessible_connectors_from_mcp_tools_with_environment_manager 会调用它。它串起认证、MCP 配置、工具拉取、缓存读写、连接器提取和插件来源补全,是整个连接器发现流程的主干。

调用图:调用 11 个内部函数(new, new, filter_disallowed_connectors, accessible_connectors_cache_key, accessible_connectors_from_mcp_tools, read_cached_accessible_connectors, with_app_plugin_sources, write_cached_accessible_connectors, originator, shared_from_config (+1 more));被 2 处调用(apps_list_response, list_accessible_connectors_from_mcp_tools_with_environment_manager);外部调用 11 个(new, new, auth_keyring_backend_kind, unbounded, default, codex_apps_tools_cache_key, compute_auth_statuses, effective_mcp_servers, host_owned_codex_apps_enabled, tool_plugin_provenance (+1 more))。

accessible_connectors_cache_key370–383 ↗
fn accessible_connectors_cache_key(
    config: &Config,
    auth: Option<&CodexAuth>,
) -> AccessibleConnectorsCacheKey

作用:这个函数生成缓存钥匙,确保不同账号、不同用户或不同 ChatGPT 服务地址不会混用同一份连接器缓存。

数据流:进去的是配置和可选认证信息 → 它取出 chatgpt_base_url、账号 ID、ChatGPT 用户 ID、是否工作区账号 → 出来的是 AccessibleConnectorsCacheKey。

调用关系:list_accessible_connectors_from_mcp_tools_with_mcp_manager、list_cached_accessible_connectors_from_mcp_tools 和 refresh_accessible_connectors_cache_from_mcp_tools 都会先用它生成钥匙,再读或写缓存。

调用图:被 3 处调用(list_accessible_connectors_from_mcp_tools_with_mcp_manager, list_cached_accessible_connectors_from_mcp_tools, refresh_accessible_connectors_cache_from_mcp_tools)。

read_cached_accessible_connectors385–403 ↗
fn read_cached_accessible_connectors(
    cache_key: &AccessibleConnectorsCacheKey,
) -> Option<Vec<AppInfo>>

作用:这个函数从内存缓存里取连接器列表,并负责判断缓存有没有过期、钥匙对不对。

数据流:进去的是缓存钥匙 → 它锁住全局缓存这个共享盒子,查看当前时间;如果缓存没过期且钥匙相同,就复制一份连接器返回;如果过期,就清空缓存 → 出来的是 Some 连接器列表或 None。

调用关系:list_accessible_connectors_from_mcp_tools_with_mcp_manager 和 list_cached_accessible_connectors_from_mcp_tools 会用它。它只读缓存,不会主动联系 MCP 服务器。

调用图:被 2 处调用(list_accessible_connectors_from_mcp_tools_with_mcp_manager, list_cached_accessible_connectors_from_mcp_tools);外部调用 1 个(now)。

write_cached_accessible_connectors405–417 ↗
fn write_cached_accessible_connectors(
    cache_key: AccessibleConnectorsCacheKey,
    connectors: &[AppInfo],
)

作用:这个函数把最新连接器列表写进内存缓存,并设置过期时间。这样短时间内重复查询就不用重新启动或询问 MCP 服务器。

数据流:进去的是缓存钥匙和连接器切片 → 它锁住全局缓存,把连接器复制成自己的列表,并把过期时间设为当前时间加上固定缓存时长 → 没有返回值,但缓存内容被替换。

调用关系:list_accessible_connectors_from_mcp_tools_with_mcp_manager 在成功查到结果后会调用它;refresh_accessible_connectors_cache_from_mcp_tools 在外部刷新后也会调用它。

调用图:被 2 处调用(list_accessible_connectors_from_mcp_tools_with_mcp_manager, refresh_accessible_connectors_cache_from_mcp_tools);外部调用 2 个(now, to_vec)。

tool_suggest_connector_ids419–444 ↗
fn tool_suggest_connector_ids(
    config: &Config,
    loaded_plugin_app_connector_ids: &[String],
) -> HashSet<String>

作用:这个函数算出工具建议功能应该考虑哪些连接器 ID,同时剔除用户明确禁用的连接器。

数据流:进去的是配置和已加载插件带来的连接器 ID → 它先把插件连接器 ID 放进集合,再加入配置里声明可发现的连接器;然后读取配置里禁用的连接器并从集合中删除 → 出来的是最终允许参与推荐的连接器 ID 集合。

调用关系:list_tool_suggest_discoverable_tools_with_auth 会调用它,用它的结果去合并目录连接器和插件连接器,并决定哪些连接器可以进入工具建议。

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

cached_directory_connectors_for_tool_suggest_with_auth447–484 ↗
async fn cached_directory_connectors_for_tool_suggest_with_auth(
    config: &Config,
    auth: Option<&CodexAuth>,
) -> Vec<AppInfo>

作用:这个函数为工具建议读取“连接器目录”的缓存。连接器目录可以理解为一份官方或远端同步过来的应用清单。

数据流:进去的是配置和可选认证信息 → 它先检查 Apps 功能是否开启;如果没有传认证,就自己从配置加载;如果没有使用 Codex 后端或没有账号 ID,就返回空列表;否则用 codex_home、服务地址、账号和用户信息创建目录缓存上下文 → 出来的是缓存目录里的连接器列表,读取失败时返回空列表。

调用关系:list_tool_suggest_discoverable_tools_with_auth 会调用它,拿到目录连接器后再和插件连接器合并。它在认证不足或功能没开时会尽早停止,避免给工具建议提供不该出现的内容。

调用图:调用 3 个内部函数(new, new, shared_from_config);被 1 处调用(list_tool_suggest_discoverable_tools_with_auth);外部调用 2 个(new, cached_directory_connectors)。

accessible_connectors_from_mcp_tools486–502 ↗
fn accessible_connectors_from_mcp_tools(mcp_tools: &[ToolInfo]) -> Vec<AppInfo>

作用:这个函数把一堆 MCP 工具整理成“可访问连接器”。也就是说,它从工具清单里反推出用户能看到哪些应用连接器。

数据流:进去的是 ToolInfo 列表 → 它只看来自 Codex Apps MCP 服务器的工具,并要求工具带有 connector_id;然后取出连接器 ID、名称、描述和插件显示名 → 出来的是去重和汇总后的 AppInfo 列表。

调用关系:list_accessible_and_enabled_connectors_from_manager、list_accessible_connectors_from_mcp_tools_with_mcp_manager、refresh_accessible_connectors_cache_from_mcp_tools、build_skills_and_plugins 和 refresh_missing_requested_connectors 都会用它。它是“工具列表”变成“应用列表”的关键转换器。

调用图:调用 1 个内部函数(collect_accessible_connectors);被 5 处调用(list_accessible_and_enabled_connectors_from_manager, list_accessible_connectors_from_mcp_tools_with_mcp_manager, refresh_accessible_connectors_cache_from_mcp_tools, build_skills_and_plugins, refresh_missing_requested_connectors);外部调用 1 个(iter)。

with_app_enabled_state504–528 ↗
fn with_app_enabled_state(mut connectors: Vec<AppInfo>, config: &Config) -> Vec<AppInfo>

作用:这个函数根据用户配置和项目要求,把连接器标成启用或禁用。它解决的是“能访问”和“允许使用”不是一回事的问题。

数据流:进去的是连接器列表和配置 → 它读取配置层里的 apps 设置;如果用户配置里有默认值或某个应用的单独设置,就更新连接器的 is_enabled;如果 requirements 配置明确禁止某个应用,就强制设为禁用 → 出来的是更新过启用状态的连接器列表。

调用关系:apps_list_response、list_connectors、list_accessible_and_enabled_connectors_from_manager、build_skills_and_plugins、built_tools 和 refresh_missing_requested_connectors 都会用它。它通常接在“找到连接器”之后,用来套上本地规则。

调用图:被 6 处调用(apps_list_response, list_connectors, list_accessible_and_enabled_connectors_from_manager, build_skills_and_plugins, built_tools, refresh_missing_requested_connectors);外部调用 2 个(app_is_enabled, apps_config_from_layer_stack)。

with_app_plugin_sources530–540 ↗
fn with_app_plugin_sources(
    mut connectors: Vec<AppInfo>,
    tool_plugin_provenance: &ToolPluginProvenance,
) -> Vec<AppInfo>

作用:这个函数给每个连接器补上它来自哪些插件的显示名称。这样界面或日志不只知道有这个连接器,还知道它是哪个插件带来的。

数据流:进去的是连接器列表和 ToolPluginProvenance(工具插件来源记录,记着工具或连接器来自哪些插件)→ 它逐个按连接器 ID 查询插件显示名,并写回 connector.plugin_display_names → 出来的是补充好插件来源的连接器列表。

调用关系:list_accessible_connectors_from_mcp_tools_with_mcp_manager 在返回连接器前会调用它。它让缓存或 MCP 查询得到的连接器信息更适合展示。

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

mcp_approvals_reviewer542–574 ↗
fn mcp_approvals_reviewer(
    config: &Config,
    server_name: &str,
    connector_id: Option<&str>,
) -> ApprovalsReviewer

作用:这个函数决定某个 MCP 调用该由谁来审批。审批者可以理解为“遇到需要确认的操作时,谁有权点同意”。

数据流:进去的是配置、MCP 服务器名和可选连接器 ID → 如果服务器是 Codex Apps,它会先看这个连接器或默认 apps 配置里有没有单独设置审批者;再检查 requirements 规则是否允许使用这个设置;如果允许,就返回应用级审批者;否则回退到全局 config.approvals_reviewer → 出来的是最终审批者设置。

调用关系:review_guardian_mcp_elicitation 在处理 MCP 请求需要审批时会用它。它读取 apps 配置来给具体连接器更细的审批规则,但始终受 requirements 这类更高约束保护。

调用图:被 2 处调用(mcp_approvals_reviewer, review_guardian_mcp_elicitation);外部调用 1 个(apps_config_from_layer_stack)。

core-plugins/src/discoverable.rs源码 ↗
domain_logicrequest handling / tool suggestion

当系统想提示用户“你可能可以装这个插件来完成任务”时,不能随便把所有插件都列出来。这个文件就像一个筛选员:先看总开关有没有打开;再看本地插件市场里有哪些插件;跳过已经安装、管理员不允许安装、用户禁用的插件;只保留用户配置过的插件,或者 OpenAI 预先允许兜底推荐的一小批插件。它还会读取插件详情,整理成统一的推荐信息,比如名字、描述、是否有技能、能连哪些应用。远程插件也会单独检查:如果远程插件功能打开,就从缓存里找全球远程插件,并排除已经安装、不可用、被管理员禁用的项目。最后它按名字和编号排序,让展示结果稳定、好读。这里的“allowlist(白名单)”就是一份明确允许推荐的插件名单,防止系统推荐太多不合适的东西。

函数细节2
PluginsManager::list_tool_suggest_discoverable_plugins70–200 ↗
async fn list_tool_suggest_discoverable_plugins(
        &self,
        input: &ToolSuggestPluginDiscoveryInput,
        auth: Option<&CodexAuth>,
    ) -> anyhow::Result<Vec<ToolSuggestDiscoverablePl

作用:找出当前可以在“工具建议”里露出的插件。它的目标不是列出所有插件,而是只给出“用户可能会用、还没装、也允许推荐”的那一小部分。

数据流:进去的是插件配置、已配置插件编号、已禁用插件编号、已加载的应用连接器编号,以及可选的登录身份信息。它先检查插件总开关;然后读取本地市场和远程缓存里的插件列表;逐个排除已安装、不可安装、被禁用、不该兜底推荐的插件;能读取到详情的就转换成 ToolSuggestDiscoverablePlugin。最后输出一个按名称和编号排好序的推荐插件列表;如果某个插件详情读失败,只记录警告,不让整个列表失败。

调用关系:这是外部想生成工具推荐时会调用的主流程。它会用 is_tool_suggest_fallback_plugin 判断某个插件是否属于允许兜底推荐的白名单;本地插件详情读取、远程插件缓存读取和当前已装插件汇总则交给 PluginsManager 的其他能力完成。遇到单个插件坏了,它用 warn! 记一条日志,然后继续处理后面的插件。

调用图:调用 1 个内部函数(is_tool_suggest_fallback_plugin);外部调用 3 个(new, new, warn!)。

is_tool_suggest_fallback_plugin203–220 ↗
fn is_tool_suggest_fallback_plugin(plugin_id: &str) -> bool

作用:判断一个插件编号是不是“即使用户没有明确配置,也允许出现在工具建议里”的兜底插件。它保护推荐入口,避免系统乱推荐不在白名单里的插件。

数据流:进去的是一个插件编号字符串。它先直接查白名单;如果没命中,就尝试把编号拆成“插件名”和“市场名”。如果这个编号来自 OpenAI 的 API curated 市场,它会把它换算成普通 curated 市场里的等价编号,再查一次白名单。出来的是 true 或 false,表示能不能作为兜底推荐。

调用关系:它是 PluginsManager::list_tool_suggest_discoverable_plugins 里的小门卫。主流程在筛选本地和远程插件时都会靠它判断:这个插件没有被用户配置过时,是否仍然可以被推荐。它内部会调用 PluginId::parse 来理解插件编号格式,并用 format! 拼出等价的白名单编号。

调用图:调用 1 个内部函数(parse);被 1 处调用(list_tool_suggest_discoverable_plugins);外部调用 1 个(format!)。

core/src/plugins/discoverable.rs源码 ↗
orchestrationtool suggestion discovery

当系统想给用户推荐可用插件时,不能只把所有插件一股脑列出来。它要先看用户配置里允许发现哪些插件,也要排除已经禁用的插件,还要知道当前已经加载了哪些应用连接器。这个文件就像一个“前台接待员”:先把这些零散信息整理成插件系统能看懂的一份表单,然后请真正的插件管理器去查询。查询回来后,它再把结果转换成工具推荐需要的统一格式,比如插件的名字、说明、是否带技能、关联的 MCP 服务器名等。这里的 MCP 可以理解成一种让外部工具和系统对接的服务通道。重要的是,这个文件本身不决定插件内容,它负责把“该查什么、该排除什么、当前有什么条件”说清楚,保证后面的推荐结果更准确。

函数细节1
list_tool_suggest_discoverable_plugins11–55 ↗
async fn list_tool_suggest_discoverable_plugins(
    config: &Config,
    plugins_manager: &PluginsManager,
    auth: Option<&CodexAuth>,
    loaded_plugin_app_connector_ids: &[String],
) -> anyhow::R

作用:这个函数列出当前可以作为“工具建议”展示给用户的插件。它会综合用户配置、禁用名单、已加载的连接器和可选的登录身份,避免把不该显示的插件推荐出来。

数据流:进去的是配置对象、插件管理器、可选的登录信息,以及已经加载的应用连接器 ID 列表。函数先从配置里取出插件配置,再把“允许发现的插件 ID”“已禁用的插件 ID”“已加载连接器 ID”分别整理成集合,方便后续快速判断。然后它把这些信息打包成查询输入,交给插件管理器异步查询。出来的是一组 DiscoverablePluginInfo,也就是外部可以直接拿去展示的插件简介;函数本身不改配置,只是读取、整理和转换数据。

调用关系:它通常在系统需要生成工具推荐列表时被调用。它先调用 config.plugins_config_input 取得插件配置输入,再把整理好的条件交给 plugins_manager.list_tool_suggest_discoverable_plugins 去做真正的插件发现。插件管理器返回内部结果后,这个函数负责把它们换成 codex_tools 使用的 DiscoverablePluginInfo 格式,作为上层推荐流程的输出。

调用图:外部调用 2 个(plugins_config_input, list_tool_suggest_discoverable_plugins)。

core/src/plugins/mentions.rs源码 ↗
domain_logicrequest handling / skill and plugin selection

用户可能会在文字里写一个工具名,也可能用结构化的“提及”形式点某个应用或插件。这个文件就像一个前台登记员:把这些提及统一收集起来,去重,再分清它们到底是应用、插件,还是普通名字。它先从普通文本里按特定符号找提及,比如工具默认用一个符号,插件文本链接用另一个符号;也会读取已经被解析好的 UserInput::Mention。然后它会检查路径代表的类型,只留下真正的 App 或 Plugin,再提取出系统内部能识别的 app id 或插件配置名。最后,它还能给连接器生成“短名字”计数,用来发现重名或做后续匹配。没有它,用户明明点名了某个插件,系统可能不知道;或者同名工具混在一起,后面的选择就容易出错。

函数细节5
collect_tool_mentions_from_messages23–25 ↗
fn collect_tool_mentions_from_messages(messages: &[String]) -> CollectedToolMentions

作用:从一批普通文字消息里收集工具提及。它使用系统默认的工具提及符号,所以调用者不用关心具体符号是什么。

数据流:输入是一组字符串消息。它把这些消息交给更底层的 collect_tool_mentions_from_messages_with_sigil,并指定默认工具符号;底层会找出普通名字和路径。输出是一个 CollectedToolMentions,里面有去重后的 plain_names 和 paths。

调用关系:它是常用入口,隐藏了“用哪个提及符号”的细节。collect_explicit_app_ids 会用它从用户文本里找应用路径,collect_explicit_app_ids_from_skill_items 也会借它从技能条目里找工具提及;真正扫描文字的工作则交给 collect_tool_mentions_from_messages_with_sigil。

调用图:调用 1 个内部函数(collect_tool_mentions_from_messages_with_sigil);被 2 处调用(collect_explicit_app_ids, collect_explicit_app_ids_from_skill_items)。

collect_tool_mentions_from_messages_with_sigil27–39 ↗
fn collect_tool_mentions_from_messages_with_sigil(
    messages: &[String],
    sigil: char,
) -> CollectedToolMentions

作用:按指定符号从多条消息里找工具或插件提及。这里的 sigil 指“提及开头的特殊符号”,比如像日常聊天里用 @ 提人一样。

数据流:输入是一组消息和一个提及符号。它逐条读取消息,调用 extract_tool_mentions_with_sigil 解析出提及结果;再把其中的普通名字和路径分别放进 HashSet,也就是自动去重的集合。输出是 CollectedToolMentions,包含所有找到的名字和路径。

调用关系:它是这个文件里真正做文本扫描汇总的核心小工具。collect_tool_mentions_from_messages 用它处理默认工具符号;collect_explicit_plugin_mentions 也用它,但会换成插件文本链接使用的符号,因为插件在普通文字里不是按默认工具符号来写的。

调用图:调用 1 个内部函数(extract_tool_mentions_with_sigil);被 2 处调用(collect_explicit_plugin_mentions, collect_tool_mentions_from_messages);外部调用 1 个(new)。

collect_explicit_app_ids41–60 ↗
fn collect_explicit_app_ids(input: &[UserInput]) -> HashSet<String>

作用:从用户输入里找出用户明确点名的应用 id。这样系统后面构建能力列表时,可以优先或只考虑这些被用户直接叫到的应用。

数据流:输入是一组 UserInput。它先把其中的文本内容取出来,从文字里扫描工具提及;同时也读取结构化的 Mention 路径。接着它只保留类型是 App 的路径,再从路径里拆出 app id。输出是一个去重后的字符串集合,也就是所有明确提到的应用 id。

调用关系:它在 build_skills_and_plugins 阶段被调用,也就是系统准备技能和插件能力时使用。它自己不解析每种文本细节,而是把普通文本扫描交给 collect_tool_mentions_from_messages,再用 tool_kind_for_path 和 app_id_from_path 判断并提取真正的应用身份。

调用图:调用 1 个内部函数(collect_tool_mentions_from_messages);被 1 处调用(build_skills_and_plugins);外部调用 1 个(iter)。

collect_explicit_plugin_mentions63–103 ↗
fn collect_explicit_plugin_mentions(
    input: &[UserInput],
    plugins: &[PluginCapabilitySummary],
) -> Vec<PluginCapabilitySummary>

作用:从用户输入里找出用户明确点名的插件,并返回这些插件的能力摘要。简单说,它把“用户提到了哪个插件”变成“系统应该拿出哪个插件资料”。

数据流:输入是一组 UserInput 和当前可用的插件列表。它先处理空插件列表,没插件就直接返回空结果;然后从文本输入里收集插件链接,也从结构化 Mention 里读取路径。它只保留类型是 Plugin 的路径,提取插件配置名,再用这些配置名去筛选传入的插件列表。输出是被点名的 PluginCapabilitySummary 列表,内容来自原插件列表的克隆副本。

调用关系:它在 build_skills_and_plugins 阶段被调用,帮助决定哪些插件应进入本轮上下文。它会调用 collect_tool_mentions_from_messages_with_sigil,但特意使用 PLUGIN_TEXT_MENTION_SIGIL,因为插件的纯文本提及使用的符号和普通工具不同;筛选完成后,它不再做加载插件的事,只把匹配到的插件摘要交回上层。

调用图:调用 1 个内部函数(collect_tool_mentions_from_messages_with_sigil);被 1 处调用(build_skills_and_plugins);外部调用 4 个(new, iter, is_empty, iter)。

build_connector_slug_counts107–116 ↗
fn build_connector_slug_counts(
    connectors: &[connectors::AppInfo],
) -> HashMap<String, usize>

作用:统计每个连接器提及短名出现了多少次。这个统计能帮助系统知道哪些短名是唯一的,哪些可能重名,避免用户提到一个名字时匹配错对象。

数据流:输入是一组连接器 AppInfo。它逐个调用 connector_mention_slug,把连接器变成适合用户提及的短名;然后在 HashMap 里给这个短名计数,第一次出现记为 1,之后每出现一次就加 1。输出是“短名到出现次数”的映射表。

调用关系:它会在 build_skills_and_plugins 准备技能和插件时使用,也会被 collect_explicit_app_ids_from_skill_items 用来辅助识别技能条目里的应用提及。它本身只负责计数,短名怎么生成交给 connector_mention_slug,计数结果再交给上层做后续判断。

调用图:调用 1 个内部函数(connector_mention_slug);被 2 处调用(build_skills_and_plugins, collect_explicit_app_ids_from_skill_items);外部调用 1 个(new)。

core/src/plugins/mod.rs源码 ↗
othercross-cutting

可以把这个文件理解成插件区域的前台目录。真正的工作分散在 discoverable、injection、mentions、render 这些文件里:有的负责找出哪些插件可以推荐给用户,有的负责把插件说明塞进提示词里,有的负责识别用户消息里提到的插件或工具,有的负责渲染明确指定的插件说明。这个 mod.rs 做的事很简单但很重要:告诉 Rust(项目使用的编程语言)这些子文件属于同一个 plugins 模块,并把常用函数“摆到门口”。这样其他地方不需要知道每个功能藏在哪个子文件里,只要从 plugins 这个统一入口拿就行。测试专用的 test_support 只在测试时编译,避免正式运行时带上测试工具。

安装建议工具

这些文件定义面向模型的工具规范和请求载荷,用于列出可安装的插件/连接器并提示用户安装它们,然后编排运行时 install-suggestion 流程。

core/src/tools/handlers/list_available_plugins_to_install_spec.rs源码 ↗
domain_logic工具规格注册或构建阶段

这个文件解决的是“用户想用某个插件,但当前还没有这个插件可用”时,系统该怎么继续的问题。它创建了一个工具规格,也就是一张给系统看的说明卡:工具名是 list_available_plugins_to_install,意思是列出可以安装的插件或连接器;说明文字里明确写了只有在用户点名要某个尚不可用的插件,并且 tool_search 这个搜索工具不可用或没搜到时,才该使用它。它不需要输入参数,所以参数表是一个空对象。返回结果的具体格式这里不规定。可以把它理解成商店门口的一张“可购买商品清单入口”的挂牌:真正安装不是它做的,它只是告诉系统有哪些候选项可以交给 request_plugin_install 去申请安装。文件里还带了一个测试,确保这张“挂牌”的名字、说明、参数形状都没有被意外改坏。

函数细节2
create_list_available_plugins_to_install_tool7–20 ↗
fn create_list_available_plugins_to_install_tool() -> ToolSpec

作用:创建“列出可安装插件/连接器”这个工具的完整说明。系统需要把工具交给模型或外部接口时,会用它生成统一格式的工具定义。

数据流:进去时没有外部输入,只读取几个固定的工具名称常量 → 它拼出一段给系统和模型看的说明文字,并声明这个工具不需要任何参数 → 出来的是一个 ToolSpec,也就是一份可注册、可传输的工具说明;它本身不去查插件,也不安装插件。

调用关系:它是这个文件的核心函数,会被 spec 流程调用来拿到工具规格。函数内部把 JsonSchema::object 生成的空参数结构塞进 ResponsesApiTool,再包装成 ToolSpec::Function,交给上层工具注册或暴露流程使用。

调用图:调用 1 个内部函数(object);被 1 处调用(spec);外部调用 4 个(default, new, format!, Function)。

tests::create_list_available_plugins_to_install_tool_uses_expected_wire_shape28–44 ↗
fn create_list_available_plugins_to_install_tool_uses_expected_wire_shape()

作用:检查这个工具生成出来的“对外形状”是不是完全符合预期。这里的“对外形状”指工具名、说明文字、参数格式等会被传给接口或模型看的内容。

数据流:进去时没有输入 → 它调用 create_list_available_plugins_to_install_tool 得到实际生成的工具说明,并拿它和手写的标准答案逐项比较 → 如果完全一样,测试通过;如果名字、说明、参数等任何地方变了,测试就会失败。

调用关系:它只在测试时运行,用来保护 create_list_available_plugins_to_install_tool 不被无意改坏。它通过 assert_eq! 做比较,不参与正常运行时的插件查找或安装流程。

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

core/src/tools/handlers/list_available_plugins_to_install.rs源码 ↗
domain_logicrequest handling

这个文件解决的是“用户想知道还能安装哪些插件”这个问题。它像一个整理货架的店员:启动时拿到一堆插件资料,先按名字和编号排好顺序;真正有人来问时,再把这些资料包装成固定格式返回。它还会把插件描述限制在最多 240 个字符,避免一段说明太长,把工具返回内容撑得过大。这里的核心结构是 ListAvailablePluginsToInstallHandler,它保存插件列表,并实现 ToolExecutor,也就是“工具执行器”:系统调用这个工具时,会问它工具叫什么、规格是什么、能不能并行跑、以及真正怎么处理请求。这个工具明确不支持并行调用,因为它只是一个清单查询工具,没有必要同时跑多份。文件底部还有测试,确认它不会并行执行,也确认插件描述会被安全截断。

函数细节10
ListAvailablePluginsToInstallHandler::new23–30 ↗
fn new(mut tools: Vec<RequestPluginInstallEntry>) -> Self

作用:创建一个“可安装插件列表”处理器。它会先把传进来的插件按名称排序,名称相同再按编号排序,这样每次返回的顺序都稳定、好读。

数据流:进去的是一组插件资料 → 函数把这组资料原地排序,先比插件名字,再比插件 id → 出来的是一个保存好排序后插件列表的 ListAvailablePluginsToInstallHandler。

调用关系:系统在注册核心工具时会用它创建处理器,测试里也会用它准备样例数据。后面真正处理工具请求时,会从这个处理器保存的插件列表里生成返回结果。

调用图:被 2 处调用(result_truncates_candidate_descriptions, add_core_utility_tools)。

ListAvailablePluginsToInstallHandler::result32–54 ↗
fn result(&self) -> ListAvailablePluginsToInstallResult

作用:把处理器里保存的插件列表整理成最终要返回的结果对象。它会复制必要字段,并把过长的描述剪短。

数据流:进去的是处理器内部已经排好序的插件列表 → 函数逐个读取插件的 id、名字、描述、类型等信息;如果描述存在,就调用安全截断逻辑,把它限制到指定字符数以内 → 出来的是 ListAvailablePluginsToInstallResult,也就是可以被序列化返回的插件清单。

调用关系:它是实际生成回答内容的地方,由 ListAvailablePluginsToInstallHandler::handle_call 在收到工具调用后使用。它不直接处理网络或请求,只负责把内部数据变成对外结果。

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

ListAvailablePluginsToInstallHandler::tool_name58–60 ↗
fn tool_name(&self) -> ToolName

作用:告诉工具系统:这个处理器对应的工具名字是什么。名字就像柜台编号,系统靠它把一次工具调用送到正确的处理器。

数据流:进去的是处理器自身 → 函数读取固定的工具名常量,并用 ToolName::plain 包装成系统认识的工具名格式 → 出来的是 ToolName。

调用关系:工具注册和调度时会询问每个处理器的名字。这里把活儿交给 plain 来生成普通工具名,方便系统按名称匹配调用。

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

ListAvailablePluginsToInstallHandler::spec62–64 ↗
fn spec(&self) -> ToolSpec

作用:返回这个工具的说明书,也就是系统和模型需要知道的“这个工具怎么用”。

数据流:进去的是处理器自身 → 函数调用 create_list_available_plugins_to_install_tool 生成工具规格 → 出来的是 ToolSpec,里面描述这个工具的能力和调用形式。

调用关系:工具注册或向模型公开工具时会用到它。它不自己拼说明书,而是交给专门的规格创建函数,避免处理逻辑和工具说明混在一起。

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

ListAvailablePluginsToInstallHandler::supports_parallel_tool_calls66–68 ↗
fn supports_parallel_tool_calls(&self) -> bool

作用:明确告诉系统:这个工具不支持并行调用。也就是说,不要同时发起多个同类请求让它一起跑。

数据流:进去的是处理器自身 → 函数不读取额外数据,直接返回 false → 出来的是“不能并行”的布尔值。

调用关系:调度工具调用的系统会看这个结果来决定能不能并发执行。测试 tests::list_tool_does_not_support_parallel_calls 专门确认了这个行为。

ListAvailablePluginsToInstallHandler::handle70–72 ↗
fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_>

作用:这是工具系统真正调用处理器时进入的统一入口。它把一次工具调用转交给异步处理函数去做。

数据流:进去的是一次 ToolInvocation,也就是一次工具调用请求 → 函数把 handle_call 这个异步任务用 pin 包起来,pin 可以理解成把任务固定好,方便异步运行器安全地执行 → 出来的是一个将来会完成的执行任务。

调用关系:工具框架会调用它,而它马上把具体工作交给 ListAvailablePluginsToInstallHandler::handle_call。它像前台接线员,只负责把电话转到真正办事的人那里。

调用图:调用 1 个内部函数(handle_call);外部调用 1 个(pin)。

ListAvailablePluginsToInstallHandler::handle_call76–100 ↗
async fn handle_call(
        &self,
        invocation: ToolInvocation,
    ) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError>

作用:真正处理“列出可安装插件”这次工具调用。它检查请求类型是否正确,生成插件清单,把结果变成文本形式返回。

数据流:进去的是一次 ToolInvocation → 函数先拆出 payload,也就是调用携带的内容;如果不是函数调用类型,就返回致命错误;如果类型正确,就调用 result 生成插件清单,再用 JSON 序列化把它变成字符串;最后用 FunctionToolOutput::from_text 和 boxed_tool_output 包成工具系统能接收的输出 → 出来的是成功的工具输出,或者失败时的 FunctionCallError。

调用关系:它由 ListAvailablePluginsToInstallHandler::handle 调用,是这条工具请求的核心执行步骤。它会向下调用 result 生成内容,也会调用序列化和输出包装工具,把内部数据变成外部能读的响应。

调用图:调用 3 个内部函数(from_text, boxed_tool_output, result);被 1 处调用(handle);外部调用 3 个(format!, to_string, Fatal)。

truncate_to_char_boundary105–110 ↗
fn truncate_to_char_boundary(value: &str, max_chars: usize) -> &str

作用:安全地把一段文字截到指定字符数以内。它特别注意不要把一个字符从中间切开,避免中文、表情符号等多字节字符被截坏。

数据流:进去的是一段字符串和最大字符数 → 函数按“字符”而不是按“字节”找截断位置;如果文字超过限制,就返回从开头到安全边界的那一段;如果没超过,就原样返回 → 出来的是原字符串的一段引用,不会新建多余内容。

调用关系:ListAvailablePluginsToInstallHandler::result 在整理插件描述时使用它。它是一个小工具函数,专门保证描述变短时仍然是合法、可显示的文字。

tests::list_tool_does_not_support_parallel_calls119–123 ↗
fn list_tool_does_not_support_parallel_calls()

作用:这个测试确认“列出可安装插件”工具确实声明为不支持并行调用。这样以后有人改代码时,不容易无意中改变这个约定。

数据流:进去的是一个空插件列表 → 测试创建处理器,调用 supports_parallel_tool_calls → 期望得到 false;如果不是 false,测试就失败。

调用关系:它验证 ListAvailablePluginsToInstallHandler::supports_parallel_tool_calls 的行为。它只在测试运行时活跃,不参与正式功能执行。

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

tests::result_truncates_candidate_descriptions126–177 ↗
fn result_truncates_candidate_descriptions()

作用:这个测试确认返回结果会按名字排序,并且过长的插件描述会被截到规定长度。它保护的是对外返回内容的稳定性和大小控制。

数据流:进去的是两个样例插件:一个描述很长,一个描述较短 → 测试用 new 创建处理器,再调用 result 隐式生成结果并与预期值比较 → 如果排序、字段复制或描述截断有任何不一致,测试就失败。

调用关系:它会调用 ListAvailablePluginsToInstallHandler::new,并间接检验 ListAvailablePluginsToInstallHandler::result 的整理规则。它是防回归测试,确保未来改动不会破坏插件清单的格式。

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

core/src/tools/handlers/request_plugin_install_spec.rs源码 ↗
configstartup / tool registration

这个文件像是在给一个按钮写“使用说明书”。系统里有一个工具叫 request_plugin_install,用来建议用户安装某个插件或连接器。但这个建议不能乱提:必须先通过另一个工具查到可安装列表,并且结果要和用户明确要求完全匹配。文件里的主要函数会组装一份 ToolSpec,也就是“工具规格说明”,里面包括工具名称、给模型看的说明文字、必须填写的参数,以及每个参数是什么意思。参数用 JSON Schema(可以理解成一张表格,规定输入必须长什么样)来描述,比如工具类型、动作类型、工具 id、推荐理由。下面的测试则像验收清单,确认生成出来的工具说明没有走样,因为这种“接口形状”一旦变了,外部调用方可能就无法正确理解或使用这个工具。

函数细节2
create_request_plugin_install_tool8–55 ↗
fn create_request_plugin_install_tool() -> ToolSpec

作用:这个函数生成 request_plugin_install 这个工具的完整说明。别人调用它,是为了把这个工具注册到系统里,让模型知道什么时候能用、要传哪些信息、不能怎么乱用。

数据流:进去时不需要外部参数;它读取预先定义好的工具名常量,并手工写出四个必填字段的说明:tool_type、action_type、tool_id、suggest_reason。然后它把这些字段打包成 JSON Schema,再连同一段严格的使用说明一起放进 ResponsesApiTool,最后返回一个 ToolSpec::Function,表示“这是一个可调用的函数型工具”。它不会安装插件本身,只是产出这份工具说明。

调用关系:它会被 spec 调用,通常发生在系统整理可用工具清单的时候。它内部把创建字符串字段、对象参数结构、函数型工具包装这些小步骤串起来,最后把成品交给外层工具注册流程使用。

调用图:调用 2 个内部函数(object, string);被 1 处调用(spec);外部调用 4 个(from, format!, Function, vec!)。

tests::create_request_plugin_install_tool_uses_expected_wire_shape65–118 ↗
fn create_request_plugin_install_tool_uses_expected_wire_shape()

作用:这个测试确认 create_request_plugin_install_tool 生成的工具说明和预期完全一致。这样可以防止有人不小心改了工具名、参数名、说明文字或必填字段,导致外部系统调用失败。

数据流:进去时没有输入;它先拼出一份期望中的说明文字,再手动构造一份期望中的 ToolSpec。然后它调用 create_request_plugin_install_tool,把实际生成结果和期望结果逐项比较。出来的结果是测试通过或失败;它不改运行时数据,只是在开发和测试阶段帮忙守住接口格式。

调用关系:它只在测试运行时被测试框架调用。它的重点不是参与真实安装流程,而是盯住 create_request_plugin_install_tool 的输出,确保这份工具规格对外呈现的“线缆形状”稳定不变。

调用图:外部调用 2 个(assert_eq!, concat!)。

tools/src/request_plugin_install.rs源码 ↗
domain_logicrequest handling

当系统发现某个工具能帮上忙,但用户还没安装时,不能直接偷偷安装,必须先问用户。这个文件就是做这件事的“申请单生成器”。它把工具类型、工具编号、工具名字、推荐原因、安装地址等信息整理成一份表单请求,交给上层界面去展示给用户。这里的“elicitation request”可以理解成“向用户要一个明确答复的请求”。文件里还定义了请求输入和结果输出的结构,方便别的代码用统一格式传递信息。另一个重要作用是安装后的核对:如果一个插件依赖若干连接器,它会逐个检查这些连接器是否出现在可访问列表里,并且状态是真的可用。这样可以避免界面上看起来安装了,但实际用不了的尴尬情况。

函数细节4
build_request_plugin_install_elicitation_request56–86 ↗
fn build_request_plugin_install_elicitation_request(
    server_name: &str,
    thread_id: String,
    turn_id: String,
    args: &RequestPluginInstallArgs,
    suggest_reason: &str,
    tool: &Discov

作用:这个函数把“建议安装某个工具”的信息包装成一份可以发给用户确认的请求。别人会用它来生成弹窗、表单或类似的确认内容,让用户知道为什么建议安装、安装的是哪个工具。

数据流:进去的是服务器名、会话线索编号、当前轮次编号、安装建议参数、推荐理由,以及具体工具信息。它先把推荐理由作为用户能看到的消息,再调用内部的元信息构建函数,把工具编号、名字、安装地址等藏在请求的附加信息里,最后产出一个 McpServerElicitationRequestParams,也就是一份完整的“请用户确认安装”的请求对象;它本身不发送网络请求,只负责把材料准备好。

调用关系:它处在“要询问用户”这个流程的前半段:上层逻辑决定需要建议安装工具后,会调用它来做请求包。它内部会借助 build_request_plugin_install_meta 生成更详细的元数据,并用 json! 把这些元数据变成通用的 JSON 数据,方便后续协议层或界面层读取。

调用图:外部调用 2 个(new, json!)。

all_requested_connectors_picked_up88–95 ↗
fn all_requested_connectors_picked_up(
    expected_connector_ids: &[String],
    accessible_connectors: &[AppInfo],
) -> bool

作用:这个函数检查一批用户需要的连接器是不是都已经安装并可用了。它适合用在安装之后的确认步骤,避免只装了一部分就继续往下走。

数据流:进去的是期望出现的连接器编号列表,以及当前系统能访问到的应用或连接器列表。它会逐个拿期望编号去核对,看对应连接器是否存在并且可访问;只有全部通过时才返回 true,只要有一个没找到或不可用就返回 false。它不改动任何数据,只给出“是否全部就绪”的判断。

调用关系:它像一个总检查员,用在需要确认多个连接器都到位的时候。具体每个连接器是否合格,会交给 verified_connector_install_completed 去判断;它自己负责把所有单项结果合并成一个最终结论。

verified_connector_install_completed97–105 ↗
fn verified_connector_install_completed(
    tool_id: &str,
    accessible_connectors: &[AppInfo],
) -> bool

作用:这个函数检查某一个连接器是否已经安装完成,并且当前真的可以访问。它用来做单个连接器的验收。

数据流:进去的是一个工具或连接器编号,以及当前可访问的应用列表。它会在列表里找编号相同的那一项;如果找到了,再看它的 is_accessible 标记是不是 true。找到且可访问就返回 true,否则返回 false。它不会修改列表,只做查询和判断。

调用关系:它是安装核对流程里的“小检查项”。all_requested_connectors_picked_up 会反复使用它来检查多个连接器;它自己只依赖传进来的列表,通过遍历列表找到目标。

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

build_request_plugin_install_meta107–132 ↗
fn build_request_plugin_install_meta(
    tool_type: DiscoverableToolType,
    action_type: DiscoverableToolAction,
    suggest_reason: &'a str,
    tool: &'a DiscoverableTool,
) -> RequestPluginInsta

作用:这个函数生成安装建议请求里的隐藏说明书,也就是给系统和界面看的附加信息。它会说明这是一个工具安装建议、是否需要长期记住、工具叫什么、怎么安装,以及插件可能关联哪些连接器。

数据流:进去的是工具类型、建议动作类型、推荐理由和工具本身。它会根据工具是连接器还是插件,决定要不要附上远程插件编号和关联的连接器编号;然后从工具里取出编号、名称、安装地址等信息,组装成 RequestPluginInstallMeta 返回。它不发送请求,也不改变工具,只是把工具信息重新整理成适合放进请求里的格式。

调用关系:它服务于 build_request_plugin_install_elicitation_request,是生成用户确认请求前的“资料整理员”。它会调用工具对象的 id、name、install_url 等方法来取基础信息,然后把这些信息交回给外层函数,外层函数再把它放进 JSON 元数据里。

调用图:调用 3 个内部函数(id, install_url, name)。

core/src/tools/handlers/request_plugin_install.rs源码 ↗
orchestrationrequest handling

这个文件像一个“安装建议的前台接待员”。模型觉得某个插件或连接器有用时,会调用这里的工具,但这里不会直接盲目安装,而是先检查请求是不是合法:理由不能为空,只能是安装动作,目标必须在可发现的工具列表里。然后它通过 MCP(模型上下文协议,可以理解成应用和外部工具之间的通信通道)向客户端发起一次用户确认,也就是问用户“要不要装”。如果用户拒绝并选择“以后别再提醒”,它会把这个偏好写进配置文件。若用户同意,它会重新读取配置或刷新连接器列表,确认插件/连接器是否真的可用了。最后它把“用户是否确认、是否完成、装的是谁”等结果打包成 JSON 返回给模型。这个文件重要的地方在于:它把模型建议、用户授权、配置持久化、安装结果验证这几步串起来,避免误装,也避免重复打扰用户。

函数细节14
RequestPluginInstallHandler::new46–48 ↗
fn new(discoverable_tools: Vec<DiscoverableTool>) -> Self

作用:创建一个安装请求处理器,并把当前能被推荐安装的工具列表保存进去。后面模型请求安装时,就用这份列表来判断目标是否合法。

数据流:输入是一组可发现的工具 → 函数把它们放进 RequestPluginInstallHandler 结构里 → 输出一个新的处理器对象,供工具注册系统使用。

调用关系:它由 add_core_utility_tools 在注册核心工具时调用,是这个处理器进入系统的起点。之后真正处理请求的工作会由同一个对象的 handle 和 handle_call 接着完成。

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

RequestPluginInstallHandler::tool_name52–54 ↗
fn tool_name(&self) -> ToolName

作用:告诉工具系统:这个处理器对应的工具名字是什么。工具系统靠这个名字把模型发来的调用分派到正确处理器。

数据流:不需要外部输入 → 使用 REQUEST_PLUGIN_INSTALL_TOOL_NAME 这个固定名字生成 ToolName → 返回给注册表或调用分发逻辑。

调用关系:工具注册和查找时会用到它。它内部调用 plain,把普通字符串包装成系统认识的工具名格式。

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

RequestPluginInstallHandler::spec56–58 ↗
fn spec(&self) -> ToolSpec

作用:提供这个工具的说明书,也就是告诉模型这个工具需要哪些参数、能做什么。没有这份说明,模型就不知道该怎么正确调用它。

数据流:不需要输入 → 调用 create_request_plugin_install_tool 生成工具规格 → 返回 ToolSpec 给工具系统。

调用关系:注册工具或向模型暴露工具能力时会调用它。实际规格细节交给 create_request_plugin_install_tool 生成,这里只负责把说明书拿出来。

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

RequestPluginInstallHandler::supports_parallel_tool_calls60–62 ↗
fn supports_parallel_tool_calls(&self) -> bool

作用:声明这个工具允许并行调用。也就是说,系统可以同时处理多个安装建议请求,而不必强制一个等一个。

数据流:不读取额外数据 → 直接返回 true → 工具调度器据此知道它可以并发安排调用。

调用关系:工具运行框架会在调度时看这个结果。它不调用别的函数,只给调度器一个明确的能力声明。

RequestPluginInstallHandler::handle64–66 ↗
fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_>

作用:这是工具被调用时的入口包装层。它把真实处理逻辑包装成异步任务,让系统可以等待用户确认、刷新配置这些耗时操作。

数据流:输入一次 ToolInvocation,也就是模型发来的工具调用上下文 → 把它交给 handle_call,并用 pin 固定成可等待的异步任务 → 返回这个任务给工具执行框架。

调用关系:工具执行框架调用 handle,handle 再把所有实际工作交给 handle_call。它本身不做判断,只是把同步入口接到异步流程上。

调用图:调用 1 个内部函数(handle_call);外部调用 1 个(pin)。

RequestPluginInstallHandler::handle_call70–199 ↗
async fn handle_call(
        &self,
        invocation: ToolInvocation,
    ) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError>

作用:这是整个文件的主流程:检查模型的安装请求,询问用户,按用户选择写配置或验证安装,最后把结果返回给模型。

数据流:输入包含参数、会话、当前轮次和调用编号的 ToolInvocation → 先确认载荷是函数参数,再解析成 RequestPluginInstallArgs,检查推荐理由、动作类型、客户端限制和工具 ID;接着构造一次用户确认请求并发给客户端;如果用户选择永久不提醒,就写配置;如果用户同意,就验证插件或连接器是否真的安装成功;最后把完成状态、用户选择、工具信息等序列化成 JSON 文本输出。

调用关系:它由 handle 调用,是安装请求工具的核心。它会把参数解析交给 parse_arguments,把用户确认请求交给 build_request_plugin_install_elicitation_request 和会话对象,把“别再提醒”交给 maybe_persist_disabled_install_request,把安装验证交给 verify_request_plugin_install_completed,最后用 from_text 和 boxed_tool_output 生成工具返回值。

调用图:调用 5 个内部函数(from_text, boxed_tool_output, parse_arguments, maybe_persist_disabled_install_request, verify_request_plugin_install_completed);被 1 处调用(handle);外部调用 8 个(from, String, build_request_plugin_install_elicitation_request, filter_request_plugin_install_discoverable_tools_for_client, format!, to_string, Fatal, RespondToModel)。

maybe_persist_disabled_install_request204–224 ↗
async fn maybe_persist_disabled_install_request(
    session: &crate::session::session::Session,
    turn: &crate::session::turn_context::TurnContext,
    tool: &DiscoverableTool,
    response: &Elici

作用:在用户拒绝安装并明确选择“以后一直别提醒我”时,把这个决定保存下来。这样系统以后不会继续建议同一个工具,减少打扰。

数据流:输入当前会话、轮次配置、被建议的工具和用户确认响应 → 先判断响应里是否包含永久禁用的意思;如果没有就什么也不做;如果有,就调用 persist_disabled_install_request 写入配置,成功后让会话重新加载用户配置;失败时只记录警告,不打断主流程。

调用关系:它由 handle_call 在收到用户响应后调用。它先请 request_plugin_install_response_requests_persistent_disable 判断用户是否真的要求永久禁用,再把写文件工作交给 persist_disabled_install_request,最后通过 reload_user_config_layer 让新配置立刻生效。

调用图:调用 2 个内部函数(persist_disabled_install_request, request_plugin_install_response_requests_persistent_disable);被 1 处调用(handle_call);外部调用 2 个(reload_user_config_layer, warn!)。

request_plugin_install_response_requests_persistent_disable226–240 ↗
fn request_plugin_install_response_requests_persistent_disable(
    response: &ElicitationResponse,
) -> bool

作用:判断用户这次拒绝安装,是不是还带了“以后总是不要再推荐”的选择。它只做判断,不改任何东西。

数据流:输入用户确认响应 → 先看动作是不是 Decline,也就是拒绝;再检查响应附带的 meta 数据里是否有指定键和值 → 输出 true 或 false。

调用关系:它被 maybe_persist_disabled_install_request 调用,像一个门卫:只有它判断为 true,后面才会真的写入禁用建议的配置。

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

persist_disabled_install_request242–252 ↗
async fn persist_disabled_install_request(
    codex_home: &codex_utils_absolute_path::AbsolutePathBuf,
    tool: &DiscoverableTool,
) -> anyhow::Result<()>

作用:把“不要再建议安装这个工具”的决定写进用户配置。这样这个偏好不是只在当前会话有效,而是下次启动也还记得。

数据流:输入 Codex 的主目录路径和目标工具 → 把工具转换成配置里能保存的禁用项 → 通过 ConfigEditsBuilder 构造一次配置修改并应用 → 成功返回空结果,失败返回错误。

调用关系:它由 maybe_persist_disabled_install_request 调用。它自己不判断用户意图,只负责落盘;具体该保存成连接器还是插件,由 disabled_install_request 帮它转换。

调用图:调用 2 个内部函数(new, disabled_install_request);被 1 处调用(maybe_persist_disabled_install_request);外部调用 1 个(AddToolSuggestDisabledTool)。

disabled_install_request254–261 ↗
fn disabled_install_request(tool: &DiscoverableTool) -> ToolSuggestDisabledTool

作用:把一个可发现工具转换成配置文件里的“禁用推荐项”。连接器和插件在配置里格式不同,所以这里负责分清楚。

数据流:输入一个 DiscoverableTool → 如果它是连接器,就取连接器 ID 生成连接器禁用项;如果它是插件,就取插件 ID 生成插件禁用项 → 输出 ToolSuggestDisabledTool。

调用关系:它被 persist_disabled_install_request 调用,是写配置前的小翻译员。它内部使用 connector 或 plugin 构造对应类型的配置值。

调用图:调用 2 个内部函数(connector, plugin);被 1 处调用(persist_disabled_install_request)。

verify_request_plugin_install_completed263–304 ↗
async fn verify_request_plugin_install_completed(
    session: &crate::session::session::Session,
    turn: &crate::session::turn_context::TurnContext,
    tool: &DiscoverableTool,
    auth: Option<&c

作用:在用户同意安装后,检查安装是不是真的成功了。它避免只因为用户点了同意,就误以为插件或连接器已经可用。

数据流:输入会话、当前轮次、目标工具和可选登录认证信息 → 如果目标是连接器,就刷新并检查指定连接器是否已经出现在可访问列表里;如果目标是插件,远程市场插件会直接视为已完成,普通插件则重新加载配置、查看插件市场列表里它是否已安装,并顺便刷新它依赖的连接器 → 输出一个布尔值表示是否完成。

调用关系:它由 handle_call 在用户确认后调用。它会根据工具类型分路:连接器路径主要交给 refresh_missing_requested_connectors 和 verified_connector_install_completed;插件路径会用 is_remote_plugin_install_suggestion 判断特殊情况,再用 verified_plugin_install_completed 检查配置和插件管理器。

调用图:调用 3 个内部函数(is_remote_plugin_install_suggestion, refresh_missing_requested_connectors, verified_plugin_install_completed);被 1 处调用(handle_call);外部调用 3 个(get_config, reload_user_config_layer, from_ref)。

is_remote_plugin_install_suggestion306–310 ↗
fn is_remote_plugin_install_suggestion(plugin_id: &str) -> bool

作用:判断一个插件 ID 是否来自远程全局市场。这样的插件安装建议在这里被当作一种特殊情况处理。

数据流:输入插件 ID 字符串 → 从最后一个 @ 后面取市场名 → 如果市场名等于远程全局市场名就返回 true,否则返回 false。

调用关系:它被 verify_request_plugin_install_completed 用在插件验证流程里。若它判断为 true,验证函数会直接认为这类远程插件建议已完成,不再走本地插件安装检查。

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

refresh_missing_requested_connectors312–353 ↗
async fn refresh_missing_requested_connectors(
    session: &crate::session::session::Session,
    turn: &crate::session::turn_context::TurnContext,
    auth: Option<&codex_login::CodexAuth>,
    expe

作用:刷新连接器列表,并检查需要的连接器是否已经能用了。连接器可以理解成 Codex 连接外部应用的入口,比如某个应用服务的桥。

数据流:输入会话、当前轮次、登录认证、期望出现的连接器 ID 列表和工具 ID → 如果没有期望连接器,直接返回空列表;否则先从 MCP 连接管理器读取当前所有工具,换算成可访问连接器并带上启用状态;如果已经齐了就返回;如果没齐,就强制刷新工具缓存,再重新计算连接器列表并更新缓存;刷新失败则记录警告并返回 None。

调用关系:它由 verify_request_plugin_install_completed 调用。连接器安装验证会直接依赖它;插件验证也会用它顺手刷新插件需要的应用连接器。它把底层数据提取交给 connectors::accessible_connectors_from_mcp_tools,把启用状态合并交给 connectors::with_app_enabled_state,把缓存更新交给 connectors::refresh_accessible_connectors_cache_from_mcp_tools。

调用图:调用 3 个内部函数(accessible_connectors_from_mcp_tools, refresh_accessible_connectors_cache_from_mcp_tools, with_app_enabled_state);被 1 处调用(verify_request_plugin_install_completed);外部调用 3 个(new, all_requested_connectors_picked_up, warn!)。

verified_plugin_install_completed355–368 ↗
fn verified_plugin_install_completed(
    tool_id: &str,
    config: &crate::config::Config,
    plugins_manager: &codex_core_plugins::PluginsManager,
) -> bool

作用:检查某个插件在当前配置和插件管理器看到的市场列表里,是否已经标记为已安装。它是普通插件安装完成判断的核心。

数据流:输入插件 ID、当前配置和插件管理器 → 从配置里取插件相关输入,列出可用市场及插件 → 在所有插件中查找 ID 相同且 installed 为真的那一个 → 找到就返回 true,找不到或读取失败就返回 false。

调用关系:它被 verify_request_plugin_install_completed 调用,用在非远程全局市场插件的验证步骤里。它不负责重新加载配置,配置刷新由上层验证函数先完成;它只负责看插件管理器给出的结果。

调用图:调用 1 个内部函数(list_marketplaces_for_config);被 1 处调用(verify_request_plugin_install_completed);外部调用 1 个(plugins_config_input)。

面向用户的插件管理界面

这些文件通过 CLI 和 TUI 暴露插件生态管理,将用户操作转换为 marketplace 和 manager 操作。

cli/src/plugin_cmd.rs源码 ↗
orchestrationcommand handling

插件就像给 Codex 加功能的小工具,而插件市场像一个工具货架。这个文件负责命令行这一层:用户输入 codex plugin add/list/remove 后,它先读 Codex 的家目录和配置,再准备一个插件管理器,然后根据用户给的是普通文字输出还是 JSON 输出,走不同的展示方式。它还会检查用户写的插件名和市场名是否合理,比如 插件@市场--marketplace 不能互相打架。安装前,它会确认指定市场真的存在、市场快照能读、里面确实有这个插件;列出时,它会把已安装、可安装、来源、权限策略等整理成表格或 JSON;删除时,它会把本地配置和缓存里的插件移走。一个重要细节是:它会加载登录认证模式,确保插件需要认证时后续有凭据可用;同时它会把坏掉的市场配置整理成清楚的报错,而不是让用户只看到模糊失败。

函数细节20
run_plugin_add124–173 ↗
async fn run_plugin_add(
    overrides: Vec<(String, toml::Value)>,
    args: AddPluginArgs,
) -> Result<()>

作用:执行 codex plugin add,也就是安装一个插件。它把用户输入的插件选择、配置、插件市场检查和最终安装串起来。

数据流:进去的是命令行配置覆盖项和安装参数,例如插件名、市场名、是否输出 JSON。它先加载运行所需的上下文,再解析插件属于哪个市场,然后在市场里找到这个插件,交给插件管理器安装。出来的是一条成功提示或一段 JSON;同时本地插件缓存和配置会被更新。

调用关系:它由顶层命令入口 cli_main 调用。它自己不直接干底层安装,而是先调用 load_plugin_command_context 准备环境,调用 parse_plugin_selection 理清用户要哪个插件,再调用 find_marketplace_for_plugin 找到正确市场,最后把安装结果交给 JsonPluginAddOutput::from_outcome 或直接打印给用户。

调用图:调用 4 个内部函数(from_outcome, find_marketplace_for_plugin, load_plugin_command_context, parse_plugin_selection);被 1 处调用(cli_main);外部调用 1 个(println!)。

JsonPluginAddOutput::from_outcome187–196 ↗
fn from_outcome(outcome: PluginInstallOutcome) -> Self

作用:把安装插件后的内部结果,整理成适合 JSON 输出的简单对象。这样脚本或其他程序可以稳定读取安装结果。

数据流:进去的是插件管理器返回的安装结果,里面有插件编号、版本、安装路径和认证要求。它把这些信息改成字符串字段,并用 auth_policy_label 把认证策略转成固定标签。出来的是 JsonPluginAddOutput,随后会被序列化成 JSON 打印。

调用关系:它只在 run_plugin_add 需要 --json 输出时使用。它属于展示前的格式转换小步骤,不参与实际安装。

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

run_plugin_list199–312 ↗
async fn run_plugin_list(
    overrides: Vec<(String, toml::Value)>,
    args: ListPluginsArgs,
) -> Result<()>

作用:执行 codex plugin list,列出配置里的插件市场中有哪些插件,以及哪些已经安装。它支持普通表格输出,也支持给程序读取的 JSON 输出。

数据流:进去的是命令行配置覆盖项和列表参数,例如是否限定某个市场、是否输出 JSON、是否显示未安装插件。它加载上下文,请插件管理器列出市场和插件,检查市场快照有没有加载失败,再按市场名过滤。出来的是终端表格或 JSON;它不安装也不删除插件,只读取并展示状态。

调用关系:它由 cli_main 调用。它会调用 load_plugin_command_context 准备配置和管理器,调用 ensure_configured_marketplace_snapshots_loaded 把坏市场配置提前报清楚,调用 configured_marketplace_sources 补充市场来源信息;如果要 JSON,则交给 JsonPluginListOutput::from_marketplaces 组装结果。

调用图:调用 4 个内部函数(from_marketplaces, configured_marketplace_sources, ensure_configured_marketplace_snapshots_loaded, load_plugin_command_context);被 1 处调用(cli_main);外部调用 4 个(new, format!, println!, vec!)。

JsonPluginListOutput::from_marketplaces322–350 ↗
fn from_marketplaces(
        marketplaces: Vec<codex_core_plugins::ConfiguredMarketplace>,
        include_available: bool,
        marketplace_sources: &HashMap<String, JsonMarketplaceSource>,
    )

作用:把多个插件市场里的插件,整理成 JSON 输出里的“已安装”和“可用”两组。这样用户或脚本能一眼区分已经在本机的插件和只是市场里有的插件。

数据流:进去的是市场列表、是否包含未安装插件的开关、以及每个市场的来源信息。它逐个市场、逐个插件创建 JSON 条目;已安装的放进 installed,未安装但允许显示的放进 available。出来的是完整的 JsonPluginListOutput

调用关系:它在 run_plugin_list 选择 JSON 输出时被调用。它把单个插件条目的转换工作交给 JsonPluginListEntry::from_configured_plugin

调用图:调用 1 个内部函数(from_configured_plugin);被 1 处调用(run_plugin_list);外部调用 1 个(new)。

JsonPluginListEntry::from_configured_plugin370–388 ↗
fn from_configured_plugin(
        marketplace_name: &str,
        marketplace_source: Option<JsonMarketplaceSource>,
        plugin: codex_core_plugins::ConfiguredMarketplacePlugin,
    ) -> Self

作用:把一个插件的内部描述,变成 JSON 里的一条插件记录。它会把名字、版本、是否安装、来源和权限策略都压成好读的字段。

数据流:进去的是市场名、可选的市场来源、以及插件对象。它选出要展示的版本号,把插件来源转换成 JSON 形状,并把安装策略、认证策略转换成固定文字标签。出来的是一条 JsonPluginListEntry

调用关系:它由 JsonPluginListOutput::from_marketplaces 在遍历插件时调用。它把来源转换交给 JsonPluginSource::from_marketplace_source,把策略标签转换交给 install_policy_labelauth_policy_label

调用图:调用 3 个内部函数(from_marketplace_source, auth_policy_label, install_policy_label);被 1 处调用(from_marketplaces)。

JsonPluginSource::from_marketplace_source415–438 ↗
fn from_marketplace_source(source: MarketplacePluginSource) -> Self

作用:把插件来源转换成 JSON 能清楚表达的形式。来源可能是本地路径,也可能是 Git 仓库,还可能是 Git 仓库里的某个子目录。

数据流:进去的是市场提供的插件来源信息。它判断来源类型:本地就输出路径;Git 且有子目录就输出仓库地址加子目录;Git 无子目录就只输出仓库地址和可选版本引用。出来的是 JsonPluginSource 枚举值,随后放进 JSON 条目。

调用关系:它由 JsonPluginListEntry::from_configured_plugin 调用。它只负责把来源说清楚,不读取文件、不访问网络。

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

configured_marketplace_sources448–477 ↗
fn configured_marketplace_sources(
    plugins_input: &PluginsConfigInput,
) -> HashMap<String, JsonMarketplaceSource>

作用:从用户配置里提取每个插件市场的来源信息,比如这个市场来自本地目录还是其他来源。这样列表 JSON 可以告诉用户“这个市场是从哪来的”。

数据流:进去的是插件配置输入。它查看当前生效的用户配置,找到 marketplaces 表,再逐个读取 source_typesource。出来的是一个按市场名索引的映射;如果没有相关配置或格式不完整,就跳过或返回空映射。

调用关系:它在 run_plugin_list 生成 JSON 列表时使用,也会被其他市场来源查询流程调用。它只是读取配置并整理数据,不验证市场是否真的能用。

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

install_policy_label479–485 ↗
fn install_policy_label(policy: MarketplacePluginInstallPolicy) -> &'static str

作用:把插件安装策略转换成固定的文字标签,方便 JSON 输出保持稳定。比如“不可安装”“可安装”“默认安装”。

数据流:进去的是内部的安装策略枚举。它按不同策略匹配到对应的大写字符串。出来的是一个静态字符串,不改动任何状态。

调用关系:它由 JsonPluginListEntry::from_configured_plugin 调用,是列表 JSON 格式化的一小步。

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

auth_policy_label487–492 ↗
fn auth_policy_label(policy: MarketplacePluginAuthPolicy) -> &'static str

作用:把插件认证策略转换成固定的文字标签。认证策略说的是插件什么时候需要用户授权:安装时,还是使用时。

数据流:进去的是内部认证策略枚举。它把 OnInstall 变成 ON_INSTALL,把 OnUse 变成 ON_USE。出来的是一个静态字符串,不改动任何状态。

调用关系:它在安装 JSON 输出和列表 JSON 输出里都会用到,分别由 JsonPluginAddOutput::from_outcomeJsonPluginListEntry::from_configured_plugin 调用。

调用图:被 2 处调用(from_outcome, from_configured_plugin)。

run_plugin_remove494–521 ↗
async fn run_plugin_remove(
    overrides: Vec<(String, toml::Value)>,
    args: RemovePluginArgs,
) -> Result<()>

作用:执行 codex plugin remove,也就是从本机移除一个已安装插件。它负责理解用户指定的是哪个插件,并让插件管理器完成卸载。

数据流:进去的是命令行配置覆盖项和删除参数,例如插件名、市场名、是否输出 JSON。它加载上下文,解析插件选择,然后把插件唯一编号交给管理器卸载。出来的是成功提示或 JSON;同时本地插件配置和缓存会被改动。

调用关系:它由 cli_main 调用。它先用 load_plugin_command_context 准备管理器,再用 parse_plugin_selection 得到插件键;卸载完成后,如果需要 JSON,就调用 JsonPluginRemoveOutput::from_selection 整理输出。

调用图:调用 3 个内部函数(from_selection, load_plugin_command_context, parse_plugin_selection);被 1 处调用(cli_main);外部调用 1 个(println!)。

JsonPluginRemoveOutput::from_selection532–538 ↗
fn from_selection(selection: PluginSelection) -> Self

作用:把用户要删除的插件选择,转换成删除成功后的 JSON 输出。它让脚本能知道刚刚删的是哪个插件和哪个市场里的插件。

数据流:进去的是已经解析好的插件选择,包含插件名、市场名和完整插件编号。它直接把这些字段放进输出对象。出来的是 JsonPluginRemoveOutput,随后会被打印成 JSON。

调用关系:它只在 run_plugin_remove 使用 --json 时被调用。它不执行删除,只负责把删除结果包装成稳定格式。

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

load_plugin_command_context547–562 ↗
async fn load_plugin_command_context(
    overrides: Vec<(String, toml::Value)>,
) -> Result<PluginCommandContext>

作用:为插件命令准备共同的运行环境。安装、列表、删除都需要它先找到 Codex 家目录、读取配置、创建插件管理器。

数据流:进去的是命令行传来的配置覆盖项。它找到 CODEX_HOME,加载配置,把配置里和插件有关的部分取出来,创建 PluginsManager,并设置当前认证模式。出来的是 PluginCommandContext,里面装着家目录、插件配置输入和管理器。

调用关系:它被 run_plugin_addrun_plugin_listrun_plugin_remove 共用,避免每个命令重复准备环境。它会调用 load_cli_auth_mode 获取认证方式,并调用配置和路径相关函数做底层读取。

调用图:调用 3 个内部函数(load_cli_auth_mode, new, find_codex_home);被 3 处调用(run_plugin_add, run_plugin_list, run_plugin_remove);外部调用 1 个(load_with_cli_overrides)。

load_cli_auth_mode564–579 ↗
async fn load_cli_auth_mode(config: &Config) -> Option<AuthMode>

作用:找出命令行当前能用的认证方式。插件可能需要访问需要授权的服务,所以这里提前把登录信息准备好。

数据流:进去的是已加载的配置。它先看环境变量里有没有 API key;如果有,就用它生成认证模式。没有的话,再从本机保存的登录信息里读取。出来的是可选的 AuthMode;如果读不到或失败,就返回空值。

调用关系:它由 load_plugin_command_context 调用,也会被其他列表流程使用。它把具体读取工作交给环境变量读取函数和认证存储读取函数。

调用图:调用 2 个内部函数(from_api_key, from_auth_storage);被 2 处调用(run_list, load_plugin_command_context);外部调用 2 个(auth_keyring_backend_kind, read_codex_api_key_from_env)。

PluginSelection::from_plugin_id588–595 ↗
fn from_plugin_id(plugin_id: PluginId) -> Self

作用:把标准插件编号拆成命令后续容易使用的三份:插件名、市场名、完整键。标准编号可以理解成 插件@市场 这种完整地址。

数据流:进去的是一个已经合法的 PluginId。它取出插件名和市场名,并调用 as_key 生成完整字符串键。出来的是 PluginSelection

调用关系:它由 parse_plugin_selection 调用。它是解析完成后的包装步骤,方便安装和删除命令共用同一种结果。

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

parse_plugin_selection598–623 ↗
fn parse_plugin_selection(
    plugin: String,
    marketplace_name: Option<String>,
) -> Result<PluginSelection>

作用:理解用户到底选了哪个插件。用户可以写 插件@市场,也可以写 插件 --marketplace 市场,这个函数负责把两种写法变成同一种结果。

数据流:进去的是插件字符串和可选市场名。它先尝试把插件字符串当成完整插件编号解析;如果同时又给了 --marketplace,还会检查两边市场名是否一致。如果只有普通插件名,就必须有市场名。出来的是 PluginSelection;如果信息缺失或冲突,就返回清楚的错误。

调用关系:它被 run_plugin_addrun_plugin_remove 调用,是安装和删除前的“地址校验员”。它内部会调用 PluginSelection::from_plugin_id,必要时创建新的插件编号。

调用图:调用 3 个内部函数(from_plugin_id, new, parse);被 2 处调用(run_plugin_add, run_plugin_remove);外部调用 1 个(bail!)。

find_marketplace_for_plugin625–660 ↗
fn find_marketplace_for_plugin(
    manager: &PluginsManager,
    codex_home: &std::path::Path,
    plugins_input: &PluginsConfigInput,
    marketplace_name: &str,
    plugin_name: &str,
) -> Result<C

作用:在配置好的插件市场里,找到包含指定插件的那个市场。安装前必须确认插件真的在那个市场里,否则不能盲目安装。

数据流:进去的是插件管理器、Codex 家目录、插件配置、市场名和插件名。它先列出所有可用市场,再检查配置的市场快照是否正常加载,然后筛选出名字匹配且包含该插件的市场。出来的是唯一匹配的市场;如果找不到或匹配到多个,就返回错误。

调用关系:它由 run_plugin_add 在真正安装前调用。它依赖插件管理器列市场,并调用 ensure_configured_marketplace_snapshots_loaded 把市场读取问题变成用户能看懂的错误。

调用图:调用 2 个内部函数(ensure_configured_marketplace_snapshots_loaded, list_marketplaces_for_config);被 1 处调用(run_plugin_add);外部调用 1 个(bail!)。

ensure_configured_marketplace_snapshots_loaded668–697 ↗
fn ensure_configured_marketplace_snapshots_loaded(
    codex_home: &std::path::Path,
    plugins_input: &PluginsConfigInput,
    load_errors: &[MarketplaceListError],
    marketplace_name: Option<&str

作用:确认用户配置的插件市场快照没有明显问题。快照可以理解成市场清单的本地副本;如果坏了,后面列插件或安装插件都会出错。

数据流:进去的是 Codex 家目录、插件配置、加载市场时收集到的错误,以及可选的指定市场名。它调用更细的检查函数拿到问题列表;如果没有问题就正常返回,如果有问题就把每个市场的问题整理成多行错误信息。出来的是成功或一个带详细说明的失败。

调用关系:它被 run_plugin_listfind_marketplace_for_plugin 调用。它把具体检查交给 configured_marketplace_snapshot_issues,自己负责决定是否中断命令并展示错误。

调用图:调用 1 个内部函数(configured_marketplace_snapshot_issues);被 2 处调用(find_marketplace_for_plugin, run_plugin_list);外部调用 1 个(bail!)。

configured_marketplace_snapshot_issues699–786 ↗
fn configured_marketplace_snapshot_issues(
    codex_home: &std::path::Path,
    plugins_input: &PluginsConfigInput,
    load_errors: &[MarketplaceListError],
    marketplace_name: Option<&str>,
) ->

作用:逐项检查用户配置的插件市场,找出为什么市场快照不能正常使用。它负责把“配置写错、路径不对、清单坏了”等问题整理成结构化列表。

数据流:进去的是 Codex 家目录、插件配置、市场加载错误列表,以及可选的市场名过滤条件。它读取用户配置里的 marketplaces,检查每个条目是不是表格、名字是否合法、本地来源是否为空,然后解析市场根目录并寻找支持的清单文件;最后把加载错误也对应回具体市场。出来的是一组 ConfiguredMarketplaceSnapshotIssue,每条包含市场名、路径和错误信息。

调用关系:它由 ensure_configured_marketplace_snapshots_loaded 调用,也会被其他列表检查流程使用。它会调用路径解析、清单查找、名字校验等底层工具,并用 is_implicit_system_marketplace_root 避免把系统内置市场的临时路径误报成错误。

调用图:调用 4 个内部函数(is_implicit_system_marketplace_root, marketplace_install_root, resolve_configured_marketplace_root, find_marketplace_manifest_path);被 2 处调用(run_list, ensure_configured_marketplace_snapshots_loaded);外部调用 3 个(from, new, validate_plugin_segment)。

is_implicit_system_marketplace_root788–811 ↗
fn is_implicit_system_marketplace_root(
    marketplace_name: &str,
    _codex_home: &Path,
    root: &Path,
) -> bool

作用:判断一个看起来没有清单的市场目录,是否其实是系统自动准备的内置市场路径。这样可以避免把某些内部临时目录当成用户配置错误。

数据流:进去的是市场名、Codex 家目录和市场根目录。它检查市场名是否属于几个系统内置名字,并判断路径结尾是否符合这些内置市场的固定目录形状。出来的是布尔值:是内置系统路径就返回 true,否则 false。

调用关系:它由 configured_marketplace_snapshot_issues 在找不到市场清单时调用。它把具体“路径结尾是否匹配”的判断交给 path_ends_with

调用图:调用 1 个内部函数(path_ends_with);被 1 处调用(configured_marketplace_snapshot_issues);外部调用 1 个(matches!)。

path_ends_with813–824 ↗
fn path_ends_with(path: &Path, suffix: &[&str]) -> bool

作用:检查一个文件路径的最后几段是否等于指定后缀。比如判断路径是不是以 plugins/openai-primary-runtime 这样的目录串结尾。

数据流:进去的是一个路径和一组期望的结尾目录名。它把路径拆成一段一段的目录名,再比较最后几段是否和期望列表一样。出来的是 true 或 false,不改动任何文件。

调用关系:它由 is_implicit_system_marketplace_root 调用,是一个很小的路径判断工具,用来支持内置市场路径识别。

调用图:被 1 处调用(is_implicit_system_marketplace_root);外部调用 1 个(components)。

tui/src/chatwidget/plugins.rs源码 ↗
orchestration用户打开 /plugins、插件弹窗交互、后台插件操作结果返回时

可以把这个文件理解成“插件商店窗口”的前台指挥员。用户输入 /plugins 后,它先检查插件功能是否开启,再请求后台拿插件列表;如果还没拿到,就显示加载窗口;拿到了,就把插件按“全部、已安装、OpenAI 精选、各个市场、添加市场”做成标签页。用户按回车、空格、快捷键时,它把动作变成 AppEvent(一种发给后台或主程序的事件消息),比如安装插件、读取详情、启用插件、升级市场。后台完成后再把结果送回来,这个文件负责更新缓存、刷新弹窗、显示成功或错误信息。它还处理安装后需要去 ChatGPT 授权 App 的后续步骤,像一个安装向导一样一步步带用户继续。

函数细节82
DelayedLoadingHeader::new91–104 ↗
fn new(
        frame_requester: FrameRequester,
        animations_enabled: bool,
        loading_text: String,
        note: Option<String>,
    ) -> Self

作用:创建一个“稍等一下”的加载标题,用在插件列表、详情、市场操作还没完成时。它记住开始时间,这样界面可以先静态显示,等久了再播放动画。

数据流:输入帧刷新器、是否开启动画、加载文字和可选说明 → 记录当前时间并保存这些显示材料 → 输出一个可渲染的加载标题对象。

调用关系:各类 loading popup 参数函数会调用它,把加载状态包装成底部弹窗的头部。

调用图:被 4 处调用(marketplace_add_loading_popup_params, marketplace_upgrade_loading_popup_params, plugin_detail_loading_popup_params, plugins_loading_popup_params);外部调用 1 个(now)。

DelayedLoadingHeader::render108–138 ↗
fn render(&self, area: Rect, buf: &mut Buffer)

作用:把加载标题画到终端界面上。它会根据等待时间决定是普通灰字,还是闪烁的“正在加载”效果。

数据流:输入要画的区域和屏幕缓冲区 → 检查区域是否为空、计算已经等待多久、安排下一帧刷新 → 在缓冲区里写入标题、加载文字和说明。

调用关系:底部面板需要显示加载头部时会调用它;它会调用 shimmer_text 做动画文字,并让 FrameRequester 安排下一次重画。

调用图:调用 2 个内部函数(shimmer_text, schedule_frame_in);外部调用 5 个(now, from, new, is_empty, with_capacity)。

DelayedLoadingHeader::desired_height140–142 ↗
fn desired_height(&self, _width: u16) -> u16

作用:告诉界面这个加载标题大概要占几行。这样底部面板能提前排版,不会挤在一起。

数据流:输入可用宽度但这里不依赖它 → 看有没有额外说明文字 → 输出 2 行或 3 行高度。

调用关系:作为 Renderable 的一部分,被界面布局系统在计算高度时使用。

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

PluginDisclosureLine::render152–157 ↗
fn render(&self, area: Rect, buf: &mut Buffer)

作用:显示安装未安装插件前的提示文字,提醒用户数据会受 App 的服务条款和隐私政策约束。它还把“Learn more”链接标成可点击链接。

数据流:输入显示区域和屏幕缓冲区 → 把提示行按宽度换行画出来 → 在同一区域标记帮助文章 URL 的超链接。

调用关系:插件详情页在未安装插件时会把它放进页头,用来展示安全和隐私提示。

调用图:外部调用 3 个(clone, new, mark_url_hyperlink)。

PluginDisclosureLine::desired_height159–165 ↗
fn desired_height(&self, width: u16) -> u16

作用:计算这段提示文字在当前宽度下会占多少行。这样详情页能正确留出空间。

数据流:输入宽度 → 用同样的换行规则估算行数 → 输出需要的高度。

调用关系:界面布局系统在绘制 PluginDisclosureLine 之前会用它决定高度。

调用图:外部调用 2 个(clone, new)。

ChatWidget::add_plugins_output178–202 ↗
fn add_plugins_output(&mut self)

作用:这是用户打开 /plugins 时的入口。它负责检查功能开关、启动插件列表预取,并决定马上显示列表、错误,还是加载窗口。

数据流:读取当前配置、插件缓存和功能开关 → 如果插件功能关闭就写提示;否则发起列表请求并查看缓存状态 → 打开对应弹窗并要求界面重画。

调用关系:它会调用 prefetch_plugins 发请求,再根据 plugins_cache_for_current_cwd 的结果调用 open_plugins_popup 或 open_plugins_loading_popup。

调用图:调用 4 个内部函数(open_plugins_loading_popup, open_plugins_popup, plugins_cache_for_current_cwd, prefetch_plugins);外部调用 1 个(new_error_event)。

ChatWidget::on_plugins_loaded204–275 ↗
fn on_plugins_loaded(
        &mut self,
        cwd: PathBuf,
        result: Result<PluginListResponse, String>,
    )

作用:后台把插件列表拿回来后,这个函数接收结果。它要防止旧目录的结果误刷新当前界面,并把成功或失败状态反映到弹窗里。

数据流:输入请求目录和插件列表结果 → 校验目录、更新正在请求和缓存状态、修正当前标签页 → 成功时刷新插件弹窗,失败时显示错误弹窗。

调用关系:插件列表请求完成后由事件流程调用;它会用 refresh_plugins_popup_if_open 或 plugins_error_popup_params 更新底部面板。

调用图:调用 2 个内部函数(plugins_error_popup_params, refresh_plugins_popup_if_open);外部调用 4 个(as_path, matches!, Failed, Ready)。

ChatWidget::on_plugin_remote_sections_loaded277–312 ↗
fn on_plugin_remote_sections_loaded(
        &mut self,
        cwd: PathBuf,
        marketplaces: Vec<PluginMarketplaceEntry>,
        section_errors: Vec<PluginRemoteSectionError>,
    )

作用:远程插件市场的分区晚一步加载完成时,用它把新内容合进现有插件列表。这样用户先看到基础列表,后续远程部分再自动补上。

数据流:输入目录、远程市场列表和分区错误 → 确认还是当前目录,更新远程加载标记,把远程市场合并进缓存 → 如果插件窗口开着就刷新显示。

调用关系:后台加载远程市场分区后调用它;它把合并工作交给 merge_remote_marketplaces,再调用 refresh_plugins_popup_if_open。

调用图:调用 2 个内部函数(refresh_plugins_popup_if_open, merge_remote_marketplaces);外部调用 1 个(as_path)。

ChatWidget::prefetch_plugins314–322 ↗
fn prefetch_plugins(&mut self)

作用:发起一次插件列表预加载。它会避免同一个目录重复发请求,减少无意义的后台工作。

数据流:读取当前工作目录和正在请求的目录 → 如果同目录已有请求就退出,否则标记请求开始 → 发出 FetchPluginsList 事件。

调用关系:add_plugins_output 会先调用它;它内部调用 on_plugins_list_fetch_started 做本地状态标记。

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

ChatWidget::on_plugins_list_fetch_started324–333 ↗
fn on_plugins_list_fetch_started(&mut self, cwd: PathBuf)

作用:记录“插件列表请求已经开始”。如果当前目录还没有可用缓存,就把界面状态改成加载中。

数据流:输入请求目录 → 确认目录匹配当前目录 → 保存正在请求的目录,并在缓存不属于该目录时设为 Loading。

调用关系:prefetch_plugins 发请求前调用它,方便后续 on_plugins_loaded 判断哪个请求还在路上。

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

ChatWidget::plugins_cache_for_current_cwd335–341 ↗
fn plugins_cache_for_current_cwd(&self) -> PluginsCacheState

作用:安全读取当前目录对应的插件缓存。它避免把别的项目目录的插件列表误拿来用。

数据流:读取缓存绑定的目录和当前目录 → 如果一致就返回缓存副本,否则返回未初始化状态 → 不改动界面。

调用关系:很多入口都会先调用它,比如打开插件页、看详情、安装失败回退、启停插件刷新。

调用图:被 10 处调用(add_plugins_output, finish_plugin_install_auth_flow, handle_plugins_popup_key_event, marketplace_add_error_popup_params, marketplace_remove_error_popup_params, on_plugin_detail_loaded, on_plugin_enabled_set, on_plugin_install_loaded, on_plugin_uninstall_loaded, open_marketplace_remove_confirmation)。

ChatWidget::open_plugins_loading_popup343–351 ↗
fn open_plugins_loading_popup(&mut self)

作用:打开或替换插件列表的加载弹窗。用户会看到“正在加载插件”的占位内容,而不是空白界面。

数据流:读取当前界面状态 → 生成加载弹窗参数 → 如果插件弹窗已打开就替换,否则新开一个选择视图。

调用关系:add_plugins_output 在缓存还没准备好时调用它;具体窗口内容由 plugins_loading_popup_params 生成。

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

ChatWidget::open_plugins_popup353–361 ↗
fn open_plugins_popup(&mut self, response: &PluginListResponse)

作用:打开真正的插件列表弹窗。它默认切到“全部插件”标签页。

数据流:输入插件列表响应 → 记录当前标签为 All Plugins → 用响应生成选择视图并显示在底部面板。

调用关系:add_plugins_output 在已有可用缓存时调用它;弹窗结构由 plugins_popup_params 生成。

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

ChatWidget::open_marketplace_add_prompt363–387 ↗
fn open_marketplace_add_prompt(&mut self)

作用:打开“添加插件市场”的输入框。用户可以填 Git 仓库、Git URL 或本地路径。

数据流:读取当前目录和事件发送器 → 创建一个自定义输入视图 → 用户提交后发送打开加载页和真正添加市场的事件。

调用关系:添加市场标签页或错误页的“重试”按钮会触发它;提交后交给后台 FetchMarketplaceAdd。

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

ChatWidget::open_marketplace_add_loading_popup389–399 ↗
fn open_marketplace_add_loading_popup(&mut self, _source: &str)

作用:显示正在添加插件市场的加载窗口。它让用户知道输入已经收到,后台正在安装或读取市场。

数据流:输入市场来源但这里只用于流程语义 → 设置当前标签为添加市场 → 生成并显示添加市场的加载弹窗。

调用关系:OpenMarketplaceAddLoading 事件到达前台时调用;内容由 marketplace_add_loading_popup_params 生成。

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

ChatWidget::open_marketplace_upgrade_loading_popup401–419 ↗
fn open_marketplace_upgrade_loading_popup(
        &mut self,
        marketplace_name: Option<&str>,
    )

作用:显示升级插件市场时的加载窗口。可以显示升级某一个市场,也可以显示升级多个市场。

数据流:输入可选市场名 → 保存当前插件标签页作为回到的位置 → 生成升级加载弹窗并替换或打开。

调用关系:handle_plugins_popup_key_event 检测到升级快捷键后调用它;后台完成后会走 on_marketplace_upgrade_loaded。

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

ChatWidget::open_marketplace_remove_confirmation421–454 ↗
fn open_marketplace_remove_confirmation(
        &mut self,
        marketplace_name: String,
        marketplace_display_name: String,
    )

作用:打开删除插件市场前的确认窗口。它防止用户误按快捷键就直接删掉配置。

数据流:输入市场内部名和显示名 → 从当前目录缓存取插件列表 → 生成包含“删除”和“返回”的确认视图并显示。

调用关系:handle_plugins_popup_key_event 在用户按删除快捷键时调用;确认视图由 marketplace_remove_confirmation_popup_params 生成。

调用图:调用 2 个内部函数(marketplace_remove_confirmation_popup_params, plugins_cache_for_current_cwd);被 1 处调用(handle_plugins_popup_key_event)。

ChatWidget::open_marketplace_remove_loading_popup456–466 ↗
fn open_marketplace_remove_loading_popup(&mut self, marketplace_display_name: &str)

作用:显示正在删除插件市场的加载窗口。用户可以看到删除操作已经开始。

数据流:输入市场显示名 → 生成删除加载弹窗 → 替换当前插件选择视图或新开视图。

调用关系:删除确认里的动作会触发对应事件,随后调用它;窗口内容由 marketplace_remove_loading_popup_params 生成。

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

ChatWidget::open_plugin_detail_loading_popup468–478 ↗
fn open_plugin_detail_loading_popup(&mut self, plugin_display_name: &str)

作用:在打开插件详情前显示加载窗口。这样网络或磁盘读取较慢时,用户不会以为没反应。

数据流:输入插件显示名 → 保留当前标签位置 → 生成详情加载弹窗并尝试替换当前插件窗口。

调用关系:插件列表项的回车动作会先触发它,再请求后台 FetchPluginDetail。

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

ChatWidget::open_plugin_install_loading_popup480–485 ↗
fn open_plugin_install_loading_popup(&mut self, plugin_display_name: &str)

作用:显示正在安装某个插件的加载窗口。它给安装过程一个明确的前台反馈。

数据流:输入插件显示名 → 生成安装加载弹窗 → 替换当前插件选择视图。

调用关系:插件详情页里的“Install plugin”动作会触发它,随后后台安装完成走 on_plugin_install_loaded。

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

ChatWidget::open_plugin_uninstall_loading_popup487–492 ↗
fn open_plugin_uninstall_loading_popup(&mut self, plugin_display_name: &str)

作用:显示正在卸载插件的加载窗口。它告诉用户卸载请求已经发出。

数据流:输入插件显示名 → 生成卸载加载弹窗 → 替换当前插件选择视图。

调用关系:插件详情页里的“Uninstall plugin”动作会触发它,随后后台结果走 on_plugin_uninstall_loaded。

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

ChatWidget::on_plugin_detail_loaded494–524 ↗
fn on_plugin_detail_loaded(
        &mut self,
        cwd: PathBuf,
        result: Result<PluginReadResponse, String>,
    )

作用:后台返回插件详情后,用它切换到详情页或错误页。它同样会忽略旧目录的过期结果。

数据流:输入目录和详情读取结果 → 校验目录并尝试拿当前插件列表缓存 → 成功就显示详情页,失败就显示详情错误页。

调用关系:FetchPluginDetail 完成后调用;成功交给 plugin_detail_popup_params,失败交给 plugin_detail_error_popup_params。

调用图:调用 3 个内部函数(plugin_detail_error_popup_params, plugin_detail_popup_params, plugins_cache_for_current_cwd);外部调用 1 个(as_path)。

ChatWidget::on_plugin_install_loaded526–584 ↗
fn on_plugin_install_loaded(
        &mut self,
        cwd: PathBuf,
        _location: PluginLocation,
        _plugin_name: String,
        plugin_display_name: String,
        result: Result<Plugi

作用:处理插件安装完成后的结果。除了显示安装成功或失败,它还会启动“需要安装 ChatGPT App”的后续授权流程。

数据流:输入目录、插件位置、名称、显示名和安装结果 → 校验目录;成功时保存需要授权的 App 列表,失败时清空状态并显示错误 → 返回布尔值表示流程是否已经结束。

调用关系:后台安装完成后调用;如果还要授权,它会调用 open_plugin_install_auth_popup,否则只写消息。

调用图:调用 3 个内部函数(open_plugin_install_auth_popup, plugin_detail_error_popup_params, plugins_cache_for_current_cwd);外部调用 2 个(as_path, format!)。

ChatWidget::on_marketplace_add_loaded586–630 ↗
fn on_marketplace_add_loaded(
        &mut self,
        cwd: PathBuf,
        _source: String,
        result: Result<MarketplaceAddResponse, String>,
    )

作用:处理添加插件市场的结果。成功时告诉用户市场已加入,失败时给出重试入口。

数据流:输入目录、来源和添加结果 → 校验目录;成功时记录新市场标签并写提示消息,失败时打开添加失败弹窗 → 可能改变当前插件标签页。

调用关系:FetchMarketplaceAdd 完成后调用;成功后下次列表刷新会跳到新市场,失败页由 marketplace_add_error_popup_params 生成。

调用图:调用 2 个内部函数(marketplace_add_error_popup_params, marketplace_tab_id_from_path);外部调用 2 个(as_path, format!)。

ChatWidget::on_marketplace_remove_loaded632–677 ↗
fn on_marketplace_remove_loaded(
        &mut self,
        cwd: PathBuf,
        marketplace_name: String,
        marketplace_display_name: String,
        result: Result<MarketplaceRemoveResponse,

作用:处理删除插件市场的结果。成功时回到全部插件,失败时给出再次确认删除的入口。

数据流:输入目录、市场名、显示名和删除结果 → 校验目录;成功时写删除成功消息,失败时显示删除失败弹窗 → 更新当前活动标签。

调用关系:FetchMarketplaceRemove 完成后调用;失败页由 marketplace_remove_error_popup_params 生成。

调用图:调用 1 个内部函数(marketplace_remove_error_popup_params);外部调用 2 个(as_path, format!)。

ChatWidget::on_marketplace_upgrade_loaded679–768 ↗
fn on_marketplace_upgrade_loaded(
        &mut self,
        cwd: PathBuf,
        result: Result<MarketplaceUpgradeResponse, String>,
    )

作用:处理插件市场升级结果。它会把“没有可升级、已是最新、升级成功、部分失败”这些情况分别说清楚。

数据流:输入目录和升级结果 → 校验目录;成功时统计选中、升级和错误数量并显示不同消息,失败时显示错误消息 → 可能把当前标签切到升级过的市场。

调用关系:FetchMarketplaceUpgrade 完成后调用;快捷键升级流程由 handle_plugins_popup_key_event 发起。

调用图:调用 1 个内部函数(marketplace_tab_id_from_path);外部调用 2 个(as_path, format!)。

ChatWidget::handle_plugins_popup_key_event770–822 ↗
fn handle_plugins_popup_key_event(&mut self, key_event: KeyEvent) -> bool

作用:处理插件弹窗里的特殊快捷键:Ctrl+R 删除市场,Ctrl+U 升级市场。它会先确认当前标签确实是用户配置的市场。

数据流:输入键盘事件 → 判断是否是删除或升级快捷键,读取当前标签和插件缓存 → 符合条件就打开确认页或发起升级请求,并返回是否已处理。

调用关系:底部插件弹窗收到键盘事件时调用;删除交给 open_marketplace_remove_confirmation,升级交给 open_marketplace_upgrade_loading_popup 和后台事件。

调用图:调用 6 个内部函数(open_marketplace_remove_confirmation, open_marketplace_upgrade_loading_popup, plugins_cache_for_current_cwd, marketplace_display_name, marketplace_is_user_configured_git, ctrl);外部调用 1 个(Char)。

ChatWidget::on_plugin_enabled_set824–865 ↗
fn on_plugin_enabled_set(
        &mut self,
        cwd: PathBuf,
        plugin_id: String,
        enabled: bool,
        result: Result<(), String>,
    )

作用:处理用户启用或停用插件后的结果。成功时更新缓存里的开关状态,失败时回滚刷新并提示错误。

数据流:输入目录、插件 ID、目标开关值和执行结果 → 校验目录;失败则显示错误并刷新,成功则改缓存中对应插件的 enabled 字段 → 刷新插件窗口。

调用关系:插件列表项的空格开关会发 SetPluginEnabled,结果回来后调用它;刷新交给 refresh_plugins_popup_if_open。

调用图:调用 2 个内部函数(plugins_cache_for_current_cwd, refresh_plugins_popup_if_open);外部调用 2 个(as_path, format!)。

ChatWidget::on_plugin_uninstall_loaded867–897 ↗
fn on_plugin_uninstall_loaded(
        &mut self,
        cwd: PathBuf,
        plugin_display_name: String,
        result: Result<PluginUninstallResponse, String>,
    )

作用:处理插件卸载完成后的结果。成功会清理安装授权流程状态,失败会把错误显示在插件详情区域。

数据流:输入目录、插件显示名和卸载结果 → 校验目录;成功时清空相关状态并写成功消息,失败时拿缓存并显示详情错误页。

调用关系:FetchPluginUninstall 完成后调用;失败显示由 plugin_detail_error_popup_params 生成。

调用图:调用 2 个内部函数(plugin_detail_error_popup_params, plugins_cache_for_current_cwd);外部调用 2 个(as_path, format!)。

ChatWidget::advance_plugin_install_auth_flow899–914 ↗
fn advance_plugin_install_auth_flow(&mut self)

作用:安装插件后,如果还有多个 ChatGPT App 需要用户安装/授权,这个函数推进到下一个。到最后一个后会结束流程。

数据流:读取当前授权流程状态 → 把下一个 App 的索引加一 → 如果还有 App 就打开下一步弹窗,否则完成流程。

调用关系:用户点“我已安装”或“继续”后触发;它会调用 open_plugin_install_auth_popup 或 finish_plugin_install_auth_flow。

调用图:调用 2 个内部函数(finish_plugin_install_auth_flow, open_plugin_install_auth_popup)。

ChatWidget::abandon_plugin_install_auth_flow916–918 ↗
fn abandon_plugin_install_auth_flow(&mut self)

作用:用户不想继续设置剩余 App 时,用它放弃后续安装向导。它不会卸载插件,只是停止引导。

数据流:不需要额外输入 → 调用完成流程并标记为 abandoned → 清理授权状态并显示跳过提示。

调用关系:授权弹窗里的“Skip remaining app setup”动作会触发它;实际收尾由 finish_plugin_install_auth_flow 完成。

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

ChatWidget::open_plugin_install_auth_popup920–932 ↗
fn open_plugin_install_auth_popup(&mut self)

作用:打开安装插件后的 App 授权向导弹窗。它会在当前插件窗口里一步步提示用户去 ChatGPT 安装所需 App。

数据流:读取授权流程状态 → 生成当前 App 的向导参数;如果没有可显示内容就结束流程 → 替换或打开选择视图。

调用关系:on_plugin_install_loaded 和 advance_plugin_install_auth_flow 会调用它;内容由 plugin_install_auth_popup_params 生成。

调用图:调用 2 个内部函数(finish_plugin_install_auth_flow, plugin_install_auth_popup_params);被 2 处调用(advance_plugin_install_auth_flow, on_plugin_install_loaded)。

ChatWidget::plugin_install_auth_popup_params934–1033 ↗
fn plugin_install_auth_popup_params(&self) -> Option<SelectionViewParams>

作用:生成 App 授权向导这一页的按钮和文字。页面会告诉用户当前是第几个 App,并提供打开浏览器、继续或跳过的选项。

数据流:读取当前授权流程、待授权 App 列表和连接器状态 → 判断这个 App 是否已在本会话可用 → 输出 SelectionViewParams,里面包含按钮和对应事件。

调用关系:open_plugin_install_auth_popup 调用它;它会用 plugin_install_auth_app_is_installed 检查状态,并用 plugin_detail_hint_line 做页脚。

调用图:调用 3 个内部函数(plugin_install_auth_app_is_installed, plugin_detail_hint_line, new);被 1 处调用(open_plugin_install_auth_popup);外部调用 6 个(new, default, from, new, format!, vec!)。

ChatWidget::plugin_install_auth_app_is_installed1035–1041 ↗
fn plugin_install_auth_app_is_installed(&self, app_id: &str) -> bool

作用:检查某个需要授权的 App 当前是否已经可访问。这样向导可以显示“已安装”而不是重复催用户安装。

数据流:输入 App ID → 读取当前可用于提及的连接器列表 → 如果找到同 ID 且可访问的连接器就返回 true。

调用关系:plugin_install_auth_popup_params 用它决定按钮文案是“Install on ChatGPT”还是“Manage on ChatGPT/Continue”。

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

ChatWidget::finish_plugin_install_auth_flow1043–1081 ↗
fn finish_plugin_install_auth_flow(&mut self, abandoned: bool)

作用:结束插件安装后的 App 设置流程。它负责清理状态、提示用户完成或跳过,并尽量回到插件列表。

数据流:输入是否为用户主动放弃 → 取出并清空流程状态和待授权 App 列表 → 写提示消息,并用当前缓存重建插件列表弹窗。

调用关系:advance_plugin_install_auth_flow、abandon_plugin_install_auth_flow 和 open_plugin_install_auth_popup 都可能调用它。

调用图:调用 2 个内部函数(plugins_cache_for_current_cwd, plugins_popup_params);被 3 处调用(abandon_plugin_install_auth_flow, advance_plugin_install_auth_flow, open_plugin_install_auth_popup);外部调用 1 个(format!)。

ChatWidget::refresh_plugins_popup_if_open1083–1097 ↗
fn refresh_plugins_popup_if_open(&mut self, response: &PluginListResponse)

作用:在插件数据变化后刷新已经打开的插件窗口,同时尽量保留用户当前标签页和选中行。

数据流:输入新的插件列表响应 → 读取当前活动标签和选中位置 → 重新生成插件弹窗参数并替换当前视图。

调用关系:列表加载、远程分区加载、插件启停结果都会调用它;核心重建由 plugins_popup_params 完成。

调用图:调用 1 个内部函数(plugins_popup_params);被 3 处调用(on_plugin_enabled_set, on_plugin_remote_sections_loaded, on_plugins_loaded)。

ChatWidget::plugins_loading_popup_params1099–1116 ↗
fn plugins_loading_popup_params(&self) -> SelectionViewParams

作用:生成“正在加载插件列表”的弹窗配置。它定义了标题、说明和一个不可点击的加载行。

数据流:读取动画配置和帧刷新器 → 创建 DelayedLoadingHeader 和加载占位项 → 输出 SelectionViewParams。

调用关系:open_plugins_loading_popup 调用它来获得可显示的加载视图。

调用图:调用 1 个内部函数(new);被 1 处调用(open_plugins_loading_popup);外部调用 3 个(new, default, vec!)。

ChatWidget::marketplace_add_loading_popup_params1118–1137 ↗
fn marketplace_add_loading_popup_params(&self) -> SelectionViewParams

作用:生成“正在添加插件市场”的弹窗配置。用户提交市场来源后会看到这个页面。

数据流:读取动画配置和帧刷新器 → 创建加载头部和禁用的加载项 → 输出选择视图参数。

调用关系:open_marketplace_add_loading_popup 调用它。

调用图:调用 1 个内部函数(new);被 1 处调用(open_marketplace_add_loading_popup);外部调用 3 个(new, default, vec!)。

ChatWidget::marketplace_remove_confirmation_popup_params1139–1211 ↗
fn marketplace_remove_confirmation_popup_params(
        &self,
        plugins_response: &PluginListResponse,
        marketplace_name: String,
        marketplace_display_name: String,
    ) -> Sele

作用:生成删除市场前的确认弹窗。它包含真正删除和返回插件列表两个选项。

数据流:输入插件列表响应、市场名和显示名 → 组装说明文字、按钮动作和取消动作 → 输出确认用的 SelectionViewParams。

调用关系:open_marketplace_remove_confirmation 调用它;删除按钮会发 OpenMarketplaceRemoveLoading 和 FetchMarketplaceRemove。

调用图:调用 1 个内部函数(new);被 1 处调用(open_marketplace_remove_confirmation);外部调用 6 个(new, default, from, clone, format!, vec!)。

ChatWidget::marketplace_remove_loading_popup_params1213–1234 ↗
fn marketplace_remove_loading_popup_params(
        &self,
        marketplace_display_name: &str,
    ) -> SelectionViewParams

作用:生成“正在删除市场”的加载弹窗配置。它把具体市场名显示在标题里。

数据流:输入市场显示名 → 组装标题和禁用加载项 → 输出 SelectionViewParams。

调用关系:open_marketplace_remove_loading_popup 调用它。

调用图:调用 1 个内部函数(new);被 1 处调用(open_marketplace_remove_loading_popup);外部调用 5 个(new, default, from, format!, vec!)。

ChatWidget::marketplace_upgrade_loading_popup_params1236–1259 ↗
fn marketplace_upgrade_loading_popup_params(
        &self,
        marketplace_name: Option<&str>,
    ) -> SelectionViewParams

作用:生成“正在升级市场”的加载弹窗配置。可以针对一个市场,也可以是所有可升级市场。

数据流:输入可选市场名 → 生成合适的加载文字 → 输出带延迟动画头部和禁用加载项的视图参数。

调用关系:open_marketplace_upgrade_loading_popup 调用它。

调用图:调用 1 个内部函数(new);被 1 处调用(open_marketplace_upgrade_loading_popup);外部调用 3 个(new, default, vec!)。

ChatWidget::plugin_detail_loading_popup_params1261–1278 ↗
fn plugin_detail_loading_popup_params(&self, plugin_display_name: &str) -> SelectionViewParams

作用:生成“正在读取插件详情”的弹窗配置。标题里会带上插件显示名。

数据流:输入插件显示名 → 创建延迟加载头部和占位项 → 输出 SelectionViewParams。

调用关系:open_plugin_detail_loading_popup 调用它。

调用图:调用 1 个内部函数(new);被 1 处调用(open_plugin_detail_loading_popup);外部调用 4 个(new, default, format!, vec!)。

ChatWidget::plugin_install_loading_popup_params1280–1301 ↗
fn plugin_install_loading_popup_params(
        &self,
        plugin_display_name: &str,
    ) -> SelectionViewParams

作用:生成“正在安装插件”的弹窗配置。它让用户知道安装动作已经开始。

数据流:输入插件显示名 → 组装标题和禁用加载项 → 输出 SelectionViewParams。

调用关系:open_plugin_install_loading_popup 调用它。

调用图:调用 1 个内部函数(new);被 1 处调用(open_plugin_install_loading_popup);外部调用 5 个(new, default, from, format!, vec!)。

ChatWidget::plugin_uninstall_loading_popup_params1303–1324 ↗
fn plugin_uninstall_loading_popup_params(
        &self,
        plugin_display_name: &str,
    ) -> SelectionViewParams

作用:生成“正在卸载插件”的弹窗配置。它用于卸载请求发出后的等待阶段。

数据流:输入插件显示名 → 组装标题和禁用加载项 → 输出 SelectionViewParams。

调用关系:open_plugin_uninstall_loading_popup 调用它。

调用图:调用 1 个内部函数(new);被 1 处调用(open_plugin_uninstall_loading_popup);外部调用 5 个(new, default, from, format!, vec!)。

ChatWidget::plugins_error_popup_params1326–1342 ↗
fn plugins_error_popup_params(&self, err: &str) -> SelectionViewParams

作用:生成插件列表加载失败的错误弹窗。它把后台返回的错误文字展示给用户。

数据流:输入错误字符串 → 组装失败标题和一个不可点击的错误行 → 输出 SelectionViewParams。

调用关系:on_plugins_loaded 在列表加载失败时调用它。

调用图:调用 1 个内部函数(new);被 1 处调用(on_plugins_loaded);外部调用 4 个(new, default, from, vec!)。

ChatWidget::marketplace_add_error_popup_params1344–1392 ↗
fn marketplace_add_error_popup_params(&self) -> SelectionViewParams

作用:生成添加插件市场失败后的弹窗。它提供“重试”,如果已有插件缓存还提供“返回插件列表”。

数据流:读取当前缓存并构造错误项 → 添加重试动作;如果有缓存,再添加回到插件列表的动作 → 输出错误视图参数。

调用关系:on_marketplace_add_loaded 在失败时调用它;页脚提示来自 plugin_detail_hint_line。

调用图:调用 3 个内部函数(plugins_cache_for_current_cwd, plugin_detail_hint_line, new);被 1 处调用(on_marketplace_add_loaded);外部调用 4 个(new, default, from, vec!)。

ChatWidget::marketplace_remove_error_popup_params1394–1449 ↗
fn marketplace_remove_error_popup_params(
        &self,
        marketplace_name: &str,
        marketplace_display_name: &str,
    ) -> SelectionViewParams

作用:生成删除插件市场失败后的弹窗。用户可以重新打开确认页,或者回到插件列表。

数据流:输入市场名和显示名,读取当前缓存 → 组装失败项、重试动作和可选返回动作 → 输出 SelectionViewParams。

调用关系:on_marketplace_remove_loaded 在失败时调用它;重试动作会发 OpenMarketplaceRemoveConfirm。

调用图:调用 3 个内部函数(plugins_cache_for_current_cwd, plugin_detail_hint_line, new);被 1 处调用(on_marketplace_remove_loaded);外部调用 4 个(new, default, from, vec!)。

ChatWidget::plugin_detail_error_popup_params1451–1489 ↗
fn plugin_detail_error_popup_params(
        &self,
        err: &str,
        plugins_response: Option<&PluginListResponse>,
    ) -> SelectionViewParams

作用:生成插件详情读取、安装或卸载失败时的错误页。它尽量给用户一个返回插件列表的按钮。

数据流:输入错误文字和可选插件列表响应 → 生成错误项;如果有列表缓存,就加入返回动作 → 输出 SelectionViewParams。

调用关系:on_plugin_detail_loaded、on_plugin_install_loaded、on_plugin_uninstall_loaded 遇错时都会调用它。

调用图:调用 2 个内部函数(plugin_detail_hint_line, new);被 3 处调用(on_plugin_detail_loaded, on_plugin_install_loaded, on_plugin_uninstall_loaded);外部调用 4 个(new, default, from, vec!)。

ChatWidget::plugins_popup_params1491–1668 ↗
fn plugins_popup_params(
        &self,
        response: &PluginListResponse,
        active_tab_id: Option<String>,
        initial_selected_idx: Option<usize>,
    ) -> SelectionViewParams

作用:生成完整的插件管理弹窗。这是整个文件最核心的“组装台”,负责把后台插件列表变成用户能浏览和操作的标签页。

数据流:输入插件列表、目标标签和初始选中行 → 统计安装数量、整理插件条目、建立全部/已安装/精选/各市场/添加市场标签 → 输出可搜索、可切换、可执行动作的 SelectionViewParams。

调用关系:open_plugins_popup、refresh_plugins_popup_if_open 和 finish_plugin_install_auth_flow 调用它;它会调用大量小工具函数生成条目、标题、提示和标签 ID。

调用图:调用 9 个内部函数(marketplace_add_tab, plugin_selection_items, disambiguate_duplicate_tab_labels, marketplace_is_user_configured, marketplace_is_user_configured_git, marketplace_tab_id, plugin_entries_for_marketplaces, plugins_header, plugins_popup_hint_line);被 3 处调用(finish_plugin_install_auth_flow, open_plugins_popup, refresh_plugins_popup_if_open);外部调用 5 个(new, default, width, new, format!)。

ChatWidget::marketplace_add_tab1670–1692 ↗
fn marketplace_add_tab(&self) -> SelectionTab

作用:生成“添加市场”这个标签页。里面只有一个入口,让用户打开输入框。

数据流:不需要外部输入 → 组装标签页标题和一个“Add marketplace”选项 → 输出 SelectionTab。

调用关系:plugins_popup_params 在构造所有插件标签页的最后调用它。

调用图:调用 1 个内部函数(plugins_header);被 1 处调用(plugins_popup_params);外部调用 1 个(vec!)。

ChatWidget::plugin_detail_popup_params1694–1863 ↗
fn plugin_detail_popup_params(
        &self,
        plugins_response: &PluginListResponse,
        plugin: &PluginDetail,
    ) -> SelectionViewParams

作用:生成插件详情页。它展示插件状态、描述、技能、Hooks、Apps、MCP Servers,并提供安装、卸载或返回按钮。

数据流:输入插件列表响应和插件详情 → 判断插件是否已安装、是否可安装、是否被管理员禁用 → 输出带操作按钮和信息行的详情视图参数。

调用关系:on_plugin_detail_loaded 成功时调用它;它会用多个摘要函数把插件详情转成人能读懂的短文字。

调用图:调用 11 个内部函数(plugin_app_summary, plugin_detail_description, plugin_detail_hint_line, plugin_detail_location, plugin_display_name, plugin_hook_summary, plugin_mcp_summary, plugin_request_name, plugin_skill_summary, plugin_uninstall_id (+1 more));被 1 处调用(on_plugin_detail_loaded);外部调用 6 个(new, default, from, clone, format!, vec!)。

ChatWidget::plugin_selection_items1865–1977 ↗
fn plugin_selection_items(
        &self,
        mut plugin_entries: Vec<(&'a PluginMarketplaceEntry, &'a PluginSummary, String)>,
        include_marketplace_names: bool,
        empty_name: &str,

作用:把插件条目变成列表里的一行一行。每一行都包含名称、状态说明、可搜索文字、开关和打开详情的动作。

数据流:输入插件条目、是否显示市场名、空列表文案 → 排序、计算状态列宽、为每个插件生成 SelectionItem → 如果没有插件则输出一个禁用的空提示项。

调用关系:plugins_popup_params 为每个标签页调用它;它会调用排序、描述、详情请求和市场显示名等辅助函数。

调用图:调用 6 个内部函数(marketplace_display_name, plugin_brief_description, plugin_brief_description_without_marketplace, plugin_detail_request_for_entry, plugin_status_label, sort_plugin_entries);被 1 处调用(plugins_popup_params);外部调用 4 个(default, new, format!, vec!)。

plugins_popup_hint_line1980–1998 ↗
fn plugins_popup_hint_line(
    can_remove_marketplace: bool,
    can_upgrade_marketplace: bool,
) -> Line<'static>

作用:生成插件弹窗底部的快捷键提示。不同市场是否能删除或升级,提示文字会不一样。

数据流:输入是否可删除、是否可升级 → 选择对应的一行快捷键说明 → 输出 Line 文本。

调用关系:plugins_popup_params 用它作为全局页脚,也给某些市场标签页生成专属页脚。

调用图:被 1 处调用(plugins_popup_params);外部调用 1 个(from)。

plugin_detail_hint_line2000–2002 ↗
fn plugin_detail_hint_line() -> Line<'static>

作用:生成详情类页面的简单页脚提示。当前只有“按 Esc 关闭”。

数据流:不需要输入 → 生成固定文字行 → 输出 Line。

调用关系:插件详情、错误页、安装授权页等都会用它保持提示风格一致。

调用图:被 5 处调用(marketplace_add_error_popup_params, marketplace_remove_error_popup_params, plugin_detail_error_popup_params, plugin_detail_popup_params, plugin_install_auth_popup_params);外部调用 1 个(from)。

plugins_header2004–2010 ↗
fn plugins_header(subtitle: String, count_line: String) -> Box<dyn Renderable>

作用:生成插件标签页的统一头部。它固定第一行是 Plugins,后面放副标题和统计说明。

数据流:输入副标题和数量说明 → 放进 ColumnRenderable → 输出可渲染头部对象。

调用关系:plugins_popup_params 和 marketplace_add_tab 用它给各个标签页做标题。

调用图:调用 1 个内部函数(new);被 2 处调用(marketplace_add_tab, plugins_popup_params);外部调用 2 个(new, from)。

plugin_entries_for_marketplaces2012–2024 ↗
fn plugin_entries_for_marketplaces(
    marketplaces: impl IntoIterator<Item = &'a PluginMarketplaceEntry>,
) -> Vec<(&'a PluginMarketplaceEntry, &'a PluginSummary, String)>

作用:把多个市场里的插件摊平成一个列表。每个结果都记着它来自哪个市场、插件本身和显示名。

数据流:输入市场集合 → 遍历每个市场的插件 → 输出扁平的插件条目列表。

调用关系:plugins_popup_params 用它生成“全部插件”、精选市场和单个市场的条目。

调用图:被 1 处调用(plugins_popup_params);外部调用 1 个(into_iter)。

sort_plugin_entries2026–2041 ↗
fn sort_plugin_entries(entries: &mut [(&PluginMarketplaceEntry, &PluginSummary, String)])

作用:给插件列表排序。已安装的排前面,然后按显示名、内部名和 ID 稳定排序。

数据流:输入可修改的插件条目切片 → 原地排序 → 不返回新列表,但改变输入顺序。

调用关系:plugin_selection_items 在生成列表项之前调用它,让用户看到更合理的顺序。

调用图:被 1 处调用(plugin_selection_items);外部调用 1 个(sort_by)。

marketplace_tab_id2043–2048 ↗
fn marketplace_tab_id(marketplace: &PluginMarketplaceEntry) -> String

作用:为一个插件市场生成标签页 ID。这个 ID 用来记住当前选中的是哪个市场标签。

数据流:输入市场对象 → 如果有本地路径就按路径生成 ID,否则用市场名生成 ID → 输出字符串。

调用关系:plugins_popup_params 用它创建和匹配市场标签;路径形式会交给 marketplace_tab_id_from_path。

调用图:调用 1 个内部函数(marketplace_tab_id_from_path);被 1 处调用(plugins_popup_params);外部调用 1 个(format!)。

marketplace_tab_id_from_path2050–2052 ↗
fn marketplace_tab_id_from_path(path: &Path) -> String

作用:根据市场的本地路径生成标签页 ID。这样即使市场名重复,也能靠路径区分。

数据流:输入路径 → 加上 marketplace: 前缀并转成字符串 → 输出标签页 ID。

调用关系:marketplace_tab_id、on_marketplace_add_loaded 和 on_marketplace_upgrade_loaded 都会用它定位市场标签。

调用图:被 3 处调用(on_marketplace_add_loaded, on_marketplace_upgrade_loaded, marketplace_tab_id);外部调用 1 个(format!)。

marketplace_tab_id_matching_saved_id2054–2077 ↗
fn marketplace_tab_id_matching_saved_id(
    saved_tab_id: &str,
    marketplaces: &[PluginMarketplaceEntry],
) -> Option<String>

作用:把以前保存的市场标签 ID 匹配到当前插件列表里的真实标签 ID。它能处理市场路径变化后仍在旧根目录下面的情况。

数据流:输入旧标签 ID 和当前市场列表 → 先精确匹配;失败后尝试把旧 ID 当路径前缀匹配 → 输出新的标签 ID 或 None。

调用关系:on_plugins_loaded 用它在刷新插件列表后尽量保留用户原来的市场标签。

调用图:外部调用 2 个(new, iter)。

merge_remote_marketplaces2079–2093 ↗
fn merge_remote_marketplaces(
    response: &mut PluginListResponse,
    remote_marketplaces: Vec<PluginMarketplaceEntry>,
)

作用:把晚到的远程市场列表合并进已有插件列表。它会先移除旧的远程分区,避免重复显示。

数据流:输入可修改的插件列表响应和新的远程市场 → 收集远程市场名,过滤掉旧远程分区或同名远程项 → 把新远程市场追加进去。

调用关系:on_plugin_remote_sections_loaded 调用它完成远程分区刷新。

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

remote_marketplace_is_remote_section2095–2103 ↗
fn remote_marketplace_is_remote_section(marketplace: &PluginMarketplaceEntry) -> bool

作用:判断一个市场名是不是系统预定义的远程分区。比如工作区共享给我的插件分区。

数据流:输入市场对象 → 把市场名和几个固定远程分区名比较 → 输出 true 或 false。

调用关系:merge_remote_marketplaces 用它决定哪些旧市场需要在合并前删掉。

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

disambiguate_duplicate_tab_labels2105–2140 ↗
fn disambiguate_duplicate_tab_labels(labels: Vec<String>) -> Vec<String>

作用:给重复的市场标签名加编号。比如两个市场都叫 Tools,就变成 Tools (1/2)、Tools (2/2)。

数据流:输入标签名列表 → 先统计每个名字出现次数,再按出现顺序补编号 → 输出不容易混淆的新标签名列表。

调用关系:plugins_popup_params 在创建额外市场标签前调用它。

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

marketplace_display_name2142–2151 ↗
fn marketplace_display_name(marketplace: &PluginMarketplaceEntry) -> String

作用:拿到插件市场给用户看的名字。优先用市场接口里配置的显示名,没有就用内部名。

数据流:输入市场对象 → 查找 interface.display_name、去掉空白并排除空字符串 → 输出显示名字符串。

调用关系:插件列表、市场排序和删除确认都会用它显示更友好的市场名。

调用图:被 2 处调用(handle_plugins_popup_key_event, plugin_selection_items)。

marketplace_is_user_configured2153–2161 ↗
fn marketplace_is_user_configured(config: &Config, marketplace_name: &str) -> bool

作用:判断某个市场是不是用户配置文件里明确配置的。只有这种市场才允许用户从界面删除。

数据流:输入配置和市场名 → 读取有效用户配置里的 marketplaces 表 → 检查是否包含该市场名。

调用关系:plugins_popup_params 和 handle_plugins_popup_key_event 用它决定是否显示/执行删除操作。

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

marketplace_is_user_configured_git2163–2174 ↗
fn marketplace_is_user_configured_git(config: &Config, marketplace_name: &str) -> bool

作用:判断某个用户配置的市场是不是 Git 来源。只有 Git 市场才支持从界面升级。

数据流:输入配置和市场名 → 读取当前用户层配置中的 marketplaces.source_type → 如果等于 git 就返回 true。

调用关系:plugins_popup_params 用它决定是否显示升级提示,handle_plugins_popup_key_event 用它决定快捷键是否生效。

调用图:被 2 处调用(handle_plugins_popup_key_event, plugins_popup_params)。

plugin_display_name2176–2185 ↗
fn plugin_display_name(plugin: &PluginSummary) -> String

作用:拿到插件给用户看的名字。优先用插件自己提供的显示名,没有就用内部名称。

数据流:输入插件摘要 → 查找 interface.display_name、修剪空白并排除空值 → 输出显示名字符串。

调用关系:plugin_entries_for_marketplaces 和 plugin_detail_popup_params 用它统一显示插件名称。

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

plugin_brief_description2187–2198 ↗
fn plugin_brief_description(
    plugin: &PluginSummary,
    marketplace_label: &str,
    status_label_width: usize,
) -> String

作用:生成插件列表里带市场名的简短说明。它把状态、市场和描述拼成一行。

数据流:输入插件、市场显示名和状态列宽 → 取状态标签和插件描述 → 输出格式化后的一行说明。

调用关系:plugin_selection_items 在“全部插件”和“已安装”这类跨市场列表里调用它。

调用图:调用 2 个内部函数(plugin_description, plugin_status_label);被 1 处调用(plugin_selection_items);外部调用 1 个(format!)。

plugin_brief_description_without_marketplace2200–2210 ↗
fn plugin_brief_description_without_marketplace(
    plugin: &PluginSummary,
    status_label_width: usize,
) -> String

作用:生成不带市场名的插件简短说明。用于已经处在某个市场标签页里,不必重复显示市场名的情况。

数据流:输入插件和状态列宽 → 取状态标签和插件描述 → 输出状态加描述,或只有状态。

调用关系:plugin_selection_items 在单个市场标签页里调用它。

调用图:调用 2 个内部函数(plugin_description, plugin_status_label);被 1 处调用(plugin_selection_items);外部调用 1 个(format!)。

plugin_status_label2212–2229 ↗
fn plugin_status_label(plugin: &PluginSummary) -> &'static str

作用:把插件状态转成人能读懂的短标签,比如 Installed、Disabled、Available、Disabled by admin。

数据流:输入插件摘要 → 按管理员禁用、已安装、启用状态、安装策略依次判断 → 输出固定状态文字。

调用关系:列表描述和选中说明都靠它保持状态文字一致。

调用图:被 3 处调用(plugin_selection_items, plugin_brief_description, plugin_brief_description_without_marketplace)。

plugin_location_for_marketplace2231–2241 ↗
fn plugin_location_for_marketplace(
    marketplace: &PluginMarketplaceEntry,
    plugin: &PluginSummary,
) -> Option<PluginLocation>

作用:判断从列表里的某个市场访问插件时,该用本地路径还是远程市场名。后台读取详情或安装时需要这个位置信息。

数据流:输入市场和插件摘要 → 如果市场有本地路径就返回 Local;否则检查远程插件身份并返回 Remote → 没有身份则返回 None。

调用关系:plugin_detail_request_for_entry 调用它来构造读取插件详情的请求。

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

plugin_detail_location2243–2250 ↗
fn plugin_detail_location(plugin: &PluginDetail) -> Option<PluginLocation>

作用:根据插件详情判断安装时要用的位置。它和列表版本类似,但数据来自详情对象。

数据流:输入插件详情 → 优先使用 marketplace_path;否则检查远程身份并使用 marketplace_name → 输出 PluginLocation 或 None。

调用关系:plugin_detail_popup_params 用它决定“Install plugin”按钮能不能真正发起安装。

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

plugin_detail_request_for_entry2252–2258 ↗
fn plugin_detail_request_for_entry(
    marketplace: &PluginMarketplaceEntry,
    plugin: &PluginSummary,
) -> Option<(PluginLocation, String)>

作用:为列表中的插件准备“读取详情”的请求材料。它把位置和请求名打包在一起。

数据流:输入市场和插件摘要 → 先求插件位置,再求请求用的插件名 → 输出二元组或 None。

调用关系:plugin_selection_items 用它决定某一行能不能按回车打开详情。

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

plugin_request_name2260–2267 ↗
fn plugin_request_name(plugin: &PluginSummary) -> String

作用:选择发给后台的插件名称。远程插件优先用远程插件 ID,本地插件用普通名称。

数据流:输入插件摘要 → 如果来源是远程且有远程身份,就返回远程 ID → 否则返回 plugin.name。

调用关系:plugin_detail_popup_params 和 plugin_detail_request_for_entry 用它构造安装或详情请求。

调用图:调用 1 个内部函数(plugin_remote_identity);被 1 处调用(plugin_detail_popup_params);外部调用 1 个(matches!)。

plugin_remote_identity2269–2275 ↗
fn plugin_remote_identity(plugin: &PluginSummary) -> Option<String>

作用:取出远程插件的真实身份 ID。它优先看共享上下文里的 remote_plugin_id,再看插件自身字段。

数据流:输入插件摘要 → 检查 share_context.remote_plugin_id,再检查 remote_plugin_id → 输出 ID 或 None。

调用关系:远程位置、请求名和卸载 ID 的判断都依赖它。

调用图:被 4 处调用(plugin_detail_location, plugin_location_for_marketplace, plugin_request_name, plugin_uninstall_id)。

plugin_uninstall_id2277–2282 ↗
fn plugin_uninstall_id(plugin: &PluginSummary) -> Option<String>

作用:决定卸载插件时发给后台的 ID。远程插件必须用远程身份,本地插件用普通插件 ID。

数据流:输入插件摘要 → 如果是远程来源就取远程身份,否则返回 plugin.id → 输出可卸载 ID 或 None。

调用关系:plugin_detail_popup_params 用它决定是否启用“Uninstall plugin”按钮。

调用图:调用 1 个内部函数(plugin_remote_identity);被 1 处调用(plugin_detail_popup_params);外部调用 1 个(matches!)。

plugin_description2284–2297 ↗
fn plugin_description(plugin: &PluginSummary) -> Option<String>

作用:从插件摘要里取一段适合列表显示的描述。优先短描述,没有再用长描述。

数据流:输入插件摘要 → 查找 short_description 或 long_description,去空白并过滤空字符串 → 输出描述或 None。

调用关系:plugin_brief_description 和 plugin_brief_description_without_marketplace 调用它。

调用图:被 2 处调用(plugin_brief_description, plugin_brief_description_without_marketplace)。

plugin_detail_description2299–2320 ↗
fn plugin_detail_description(plugin: &PluginDetail) -> Option<String>

作用:从插件详情里取一段适合详情页显示的描述。优先详情自身描述,再退回长描述和短描述。

数据流:输入插件详情 → 按优先级查找描述文字、修剪空白、过滤空值 → 输出描述或 None。

调用关系:plugin_detail_popup_params 用它填充详情页头部说明。

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

plugin_skill_summary2322–2333 ↗
fn plugin_skill_summary(plugin: &PluginDetail) -> String

作用:把插件提供的技能列表压缩成一行文字。没有技能时也会明确写“没有”。

数据流:输入插件详情 → 如果 skills 为空就返回 No plugin skills,否则提取每个技能名并用逗号连接 → 输出字符串。

调用关系:plugin_detail_popup_params 用它生成详情页里的 Skills 行。

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

plugin_app_summary2335–2346 ↗
fn plugin_app_summary(plugin: &PluginDetail) -> String

作用:把插件关联的 Apps 列成一行。没有 App 时返回明确的空提示。

数据流:输入插件详情 → 如果 apps 为空就返回 No plugin apps,否则提取 App 名并拼接 → 输出字符串。

调用关系:plugin_detail_popup_params 用它生成详情页里的 Apps 行。

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

plugin_hook_summary2348–2369 ↗
fn plugin_hook_summary(plugin: &PluginDetail) -> String

作用:汇总插件的 Hook 信息。Hook 可以理解成“某个事件发生时自动运行的处理器”,这里按事件类型统计数量。

数据流:输入插件详情 → 如果 hooks 为空就返回 No plugin hooks,否则按事件名累计处理器数量 → 输出类似 EventName (2) 的列表字符串。

调用关系:plugin_detail_popup_params 用它生成详情页里的 Hooks 行。

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

plugin_mcp_summary2371–2377 ↗
fn plugin_mcp_summary(plugin: &PluginDetail) -> String

作用:把插件包含的 MCP Servers 列成一行。MCP Server 是插件可连接的外部工具服务入口。

数据流:输入插件详情 → 如果 mcp_servers 为空就返回 No plugin MCP servers,否则用逗号拼接服务器名 → 输出字符串。

调用关系:plugin_detail_popup_params 用它生成详情页里的 MCP Servers 行。

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