Codex 系统手册

App-server 集成发现与搜索适配器

stage-14.3.45 个文件

这一阶段像应用服务器里的“前台导购和搜索台”,客户端来问有哪些应用、想快速找文件,都由这里接住。connectors 里的几个文件先把各路连接器名单整理干净:过滤掉不该露出的,合并重复的,补齐名字、图标、说明和插件信息。apps_processor 再把登录状态、工作区配置和插件列表串起来,返回用户真正能看到的应用。fuzzy_file_search 则负责文件模糊搜索,既能查一次,也能在用户边输入时边推送新结果。

本阶段的文件5

连接器目录整形

这些文件将连接器和插件数据构建、过滤并合并为面向应用的目录,供发现流程使用。

connectors/src/accessible.rs源码 ↗
domain_logicrequest handling

一个连接器可以提供多个工具,就像一个应用里有多个小功能。如果直接把工具原样展示给用户,就可能看到同一个连接器重复出现很多次,名字还可能为空或不规范。这个文件先定义了 AccessibleConnectorTool,用来装每个工具带来的连接器信息:连接器编号、名字、描述,以及插件显示名。核心函数 collect_accessible_connectors 会按 connector_id 把工具归成一组;名字和描述会先做清洗,空白值会被当成没有;插件显示名会用 BTreeSet 去重并排序。最后它生成 AppInfo,也就是前端或协议层更容易展示的应用信息,并补上安装链接,再按“可访问优先、名字、编号”的顺序排好。

函数细节1
collect_accessible_connectors15–76 ↗
fn collect_accessible_connectors(tools: I) -> Vec<AppInfo>

作用:这个函数把多个工具声明合并成一份干净的连接器列表。别人会用它来生成“当前用户可访问哪些连接器”的展示数据,避免同一个连接器因为有多个工具而重复出现。

数据流:输入是一批 AccessibleConnectorTool,每个里面有连接器编号、可选名字、可选描述和插件显示名。函数先按连接器编号分组;对名字和描述做规范化清洗;如果同一个连接器出现多次,就保留更像样的名字、补上缺失的描述,并合并插件名。之后它把每组变成 AppInfo,补上安装地址,把插件名去重后放回去,最后排序并返回一个 Vec<AppInfo> 列表。它不会改外部数据,只返回整理后的新列表。

调用关系:它被 accessible_connectors_from_mcp_tools 调用,通常是在系统已经从 MCP 工具里拿到原始信息之后,用来把这些零散信息变成适合展示或返回给客户端的连接器清单。函数内部会把清洗文字的工作交给 normalize_connector_value,把生成安装链接的工作交给 connector_install_url,自己主要负责归并、去重和排序。

调用图:被 1 处调用(accessible_connectors_from_mcp_tools);外部调用 3 个(new, new, normalize_connector_value)。

connectors/src/filter.rs源码 ↗
domain_logicrequest handling / cross-cutting

连接器可以理解成让系统接入外部服务的小插件,比如日历、邮箱之类。这个文件解决的问题是:同一批连接器里,有些用户已经能用了,有些不该推荐,有些因为来源不同还要特殊隐藏;如果不筛选,用户可能会看到重复、不可安装、或产品上不希望出现的选项。核心流程是先拿到目录里的全部连接器,再去掉黑名单里的连接器,然后排除用户已经可访问的连接器,只留下允许被发现的连接器,最后按名字和编号排序,让列表稳定、好看。这里还有一组测试,用假的连接器检查各种边界情况,比如普通来源和第一方聊天来源看到的隐藏规则不同,以及“已经可访问但被禁用”的连接器也不会再被推荐。

函数细节12
filter_tool_suggest_discoverable_connectors5–28 ↗
fn filter_tool_suggest_discoverable_connectors(
    directory_connectors: Vec<AppInfo>,
    accessible_connectors: &[AppInfo],
    discoverable_connector_ids: &HashSet<String>,
    originator_value: &

作用:这个函数用来生成“可以建议用户去发现或安装”的连接器列表。它会避开用户已经能用的连接器,也会避开不允许展示或不在可发现名单里的连接器。

数据流:进去的是目录里的连接器列表、用户当前可访问的连接器列表、一组允许被发现的连接器编号,以及请求来源标识。它先从可访问列表里记下已经能用的编号,再调用过滤黑名单的函数去掉不该出现的连接器,接着只保留“用户还不能用”并且“在可发现名单里”的项,最后按连接器名字排序,名字一样再按编号排序。出来的是一份干净、稳定排序的推荐连接器列表,不会改动原来的外部数据。

调用关系:它通常在需要列出可推荐工具时被使用,比如 list_tool_suggest_discoverable_tools_with_auth 会走到这里。它先把黑名单过滤这一步交给 filter_disallowed_connectors,再用迭代器逐个筛选;相关测试会直接调用它,确认已安装或已可访问的连接器不会被重复推荐。

调用图:调用 1 个内部函数(filter_disallowed_connectors);被 3 处调用(filter_tool_suggest_discoverable_connectors_excludes_accessible_apps_even_when_disabled, filter_tool_suggest_discoverable_connectors_keeps_only_plugin_backed_uninstalled_apps, list_tool_suggest_discoverable_tools_with_auth);外部调用 1 个(iter)。

filter_disallowed_connectors41–52 ↗
fn filter_disallowed_connectors(
    connectors: Vec<AppInfo>,
    originator_value: &str,
) -> Vec<AppInfo>

作用:这个函数负责从一批连接器里删掉明确不允许展示的编号。它让上层代码不用到处记黑名单,只要把列表交给它即可。

数据流:进去的是一个连接器列表和一个来源标识。它先判断这个来源是不是第一方聊天产品,然后根据来源选择对应的禁止编号名单,再逐个检查连接器编号是否允许保留。出来的是过滤后的新列表,原列表会被消费掉并变成结果列表。

调用关系:它是这个文件里的基础过滤关卡,被多个流程复用,比如 connectors_for_plugin_apps、merge_and_filter_plugin_connectors、merge_connectors_with_accessible,以及 filter_tool_suggest_discoverable_connectors。它会先调用 is_first_party_chat_originator 判断该用哪套隐藏规则,然后用编号允许规则完成筛选;多条测试专门验证它在不同来源下的表现。

调用图:调用 1 个内部函数(is_first_party_chat_originator);被 10 处调用(connectors_for_plugin_apps, merge_and_filter_plugin_connectors, merge_connectors_with_accessible, filter_tool_suggest_discoverable_connectors, filter_disallowed_connectors_allows_non_disallowed_connectors, filter_disallowed_connectors_allows_openai_prefix, filter_disallowed_connectors_filters_disallowed_connector_ids, first_party_chat_originator_filters_target_connector_ids, list_accessible_connectors_from_mcp_tools_with_mcp_manager, refresh_accessible_connectors_cache_from_mcp_tools)。

is_first_party_chat_originator54–56 ↗
fn is_first_party_chat_originator(originator_value: &str) -> bool

作用:这个小函数判断请求是不是来自指定的第一方聊天产品。这里的“第一方”可以理解成项目自己家的客户端,而不是外部来源。

数据流:进去的是一个来源字符串。它把这个字符串和两个固定值比较:codex_atlas、codex_chatgpt_desktop;如果匹配其中一个,就返回 true,否则返回 false。它不改动任何数据。

调用关系:它只服务于 filter_disallowed_connectors。过滤连接器前,filter_disallowed_connectors 会用它决定该套用普通黑名单,还是第一方聊天产品专用的黑名单。

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

is_connector_id_allowed58–66 ↗
fn is_connector_id_allowed(connector_id: &str, first_party_chat_originator: bool) -> bool

作用:这个函数判断某个连接器编号能不能展示。它把“查黑名单”这件事集中放在一个地方,避免筛选逻辑散落各处。

数据流:进去的是一个连接器编号,以及一个布尔值,表示当前来源是否是第一方聊天产品。它根据这个布尔值选择一份禁止名单,然后检查编号是否在名单里;不在名单里就返回 true,表示允许,反之返回 false。它只做判断,不修改列表。

调用关系:它是 filter_disallowed_connectors 进行逐项筛选时用到的判断零件。filter_disallowed_connectors 负责遍历整批连接器,而它负责回答单个编号“能不能留下”。

tests::app74–90 ↗
fn app(id: &str) -> AppInfo

作用:这是测试里用的造假数据工具,用来快速做出一个最简单的连接器。它让测试不用每次手写一大堆字段。

数据流:进去的是一个连接器编号。它把这个编号同时填到 id 和 name 里,其他可选信息大多留空,并设置成默认的“不可访问但已启用”状态。出来的是一个 AppInfo 测试对象。

调用关系:它只在测试模块里使用,是各个测试准备输入和期望结果的基础小工具。tests::named_app 会在它的基础上再补充名字和安装链接。

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

tests::named_app92–99 ↗
fn named_app(id: &str, name: &str) -> AppInfo

作用:这是测试里用来创建“有真实名字和安装链接”的假连接器。它比 tests::app 更接近实际推荐列表里的连接器样子。

数据流:进去的是连接器编号和显示名称。它先通过 connector_install_url 生成安装地址,再借用 tests::app 的默认字段,覆盖名称和安装链接。出来的是一个带名字、带安装地址的 AppInfo 测试对象。

调用关系:它服务于推荐连接器相关的测试,因为这些测试需要比较排序和安装信息。它把基础造数工作交给 tests::app,把安装链接生成交给 connector_install_url。

调用图:调用 1 个内部函数(connector_install_url);外部调用 1 个(app)。

tests::filter_disallowed_connectors_allows_non_disallowed_connectors102–106 ↗
fn filter_disallowed_connectors_allows_non_disallowed_connectors()

作用:这个测试确认:不在黑名单里的普通连接器不会被误删。它防止过滤规则过于激进。

数据流:进去的是两个假连接器和普通来源 codex_cli。测试调用 filter_disallowed_connectors 后,检查出来的列表仍然包含这两个连接器,顺序也不变。它不产出业务数据,只产出测试通过或失败的结果。

调用关系:它直接验证 filter_disallowed_connectors 的基本行为。测试用 tests::app 准备连接器,用断言比较实际结果和期望结果。

调用图:调用 1 个内部函数(filter_disallowed_connectors);外部调用 2 个(assert_eq!, vec!)。

tests::filter_disallowed_connectors_allows_openai_prefix109–126 ↗
fn filter_disallowed_connectors_allows_openai_prefix()

作用:这个测试确认:带 connector_openai_ 前缀的连接器不会因为前缀看起来特殊就被自动隐藏。真正决定隐藏的是完整编号是否在黑名单里。

数据流:进去的是几个带不同编号的假连接器和普通来源。它调用 filter_disallowed_connectors,然后检查这些连接器都被保留下来。结果是测试通过或失败。

调用关系:它直接保护 filter_disallowed_connectors 的规则边界,说明代码不是按前缀粗暴过滤,而是按明确列出的禁止编号过滤。

调用图:调用 1 个内部函数(filter_disallowed_connectors);外部调用 2 个(assert_eq!, vec!)。

tests::filter_disallowed_connectors_filters_disallowed_connector_ids129–139 ↗
fn filter_disallowed_connectors_filters_disallowed_connector_ids()

作用:这个测试确认:普通来源下,黑名单里的连接器编号确实会被删掉。它防止被禁止展示的连接器意外露出来。

数据流:进去的是两个黑名单编号和一个普通编号。测试调用 filter_disallowed_connectors 后,检查结果只剩普通编号 delta。出来的是测试是否符合预期。

调用关系:它直接覆盖 filter_disallowed_connectors 的核心用途:按 DISALLOWED_CONNECTOR_IDS 过滤。若黑名单改错或判断失效,这个测试会失败。

调用图:调用 1 个内部函数(filter_disallowed_connectors);外部调用 2 个(assert_eq!, vec!)。

tests::first_party_chat_originator_filters_target_connector_ids142–158 ↗
fn first_party_chat_originator_filters_target_connector_ids()

作用:这个测试确认:当来源是第一方聊天产品时,会使用另一套隐藏规则。也就是说,不同入口看到的连接器隐藏策略可以不同。

数据流:进去的是三个假连接器,其中一个属于第一方聊天来源专用的禁止编号,另一个属于普通黑名单。测试用 codex_atlas 作为来源调用 filter_disallowed_connectors,最后检查只有第一方聊天专用禁止编号被删掉,普通黑名单里的那个反而被保留。结果是测试通过或失败。

调用关系:它验证 filter_disallowed_connectors 会通过 is_first_party_chat_originator 切换规则。这个测试很重要,因为它说明黑名单不是全局一刀切,而是和请求来源有关。

调用图:调用 1 个内部函数(filter_disallowed_connectors);外部调用 2 个(assert_eq!, vec!)。

tests::filter_tool_suggest_discoverable_connectors_keeps_only_plugin_backed_uninstalled_apps161–192 ↗
fn filter_tool_suggest_discoverable_connectors_keeps_only_plugin_backed_uninstalled_apps()

作用:这个测试确认推荐列表只留下“可发现、但用户还不能访问”的连接器。它防止推荐里混入无关项或用户已经有的项。

数据流:进去的是目录里的三个假连接器、一个已经可访问的连接器,以及两个允许被发现的编号。测试调用 filter_tool_suggest_discoverable_connectors 后,检查结果只剩 Gmail:因为 Google Calendar 已经可访问,Other 不在可发现名单里。出来的是测试是否通过。

调用关系:它直接验证 filter_tool_suggest_discoverable_connectors 的组合筛选流程。测试用 tests::named_app 构造更像真实目录的连接器,并用集合表示“允许被发现”的编号清单。

调用图:调用 1 个内部函数(filter_tool_suggest_discoverable_connectors);外部调用 4 个(from, assert_eq!, named_app, vec!)。

tests::filter_tool_suggest_discoverable_connectors_excludes_accessible_apps_even_when_disabled195–226 ↗
fn filter_tool_suggest_discoverable_connectors_excludes_accessible_apps_even_when_disabled()

作用:这个测试确认:只要连接器对用户来说是可访问的,就算它当前是禁用状态,也不应该再出现在推荐发现列表里。这样可以避免用户看到重复或误导性的安装建议。

数据流:进去的是两个目录连接器、两个都标为可访问的连接器,其中一个还被标为禁用,以及对应的可发现编号集合。测试调用 filter_tool_suggest_discoverable_connectors 后,期望得到空列表。出来的是测试通过或失败。

调用关系:它补充验证 filter_tool_suggest_discoverable_connectors 对“可访问”的判断优先级。这个测试说明函数看的是 is_accessible,而不是只看 is_enabled;因此已可访问但暂时禁用的连接器也会被排除。

调用图:调用 1 个内部函数(filter_tool_suggest_discoverable_connectors);外部调用 4 个(from, assert_eq!, named_app, vec!)。

connectors/src/merge.rs源码 ↗
domain_logicconnector discovery and tool listing

连接器可以理解成系统能接入的外部应用,比如日历、邮件或别的工具。这个文件解决的问题是:同一个连接器可能从“插件发现”和“用户可访问的应用目录”两边都出现,而且两边信息不一样。这里会按连接器的 id,也就是唯一编号,把它们合并成一条记录。插件那边可能只有一个占位名字,目录那边可能有正式名称、说明、图标等更完整的信息,所以合并时会优先补上这些更好的资料。同时,它会标记这个连接器当前是否可访问,补默认安装链接,合并并去重插件展示名,最后再排序,让可访问、名字合适的连接器更容易被展示。文件底部的测试用 Google Calendar 这种例子,确认占位名会被替换,重复的展示名会被清理。

函数细节8
merge_connectors8–58 ↗
fn merge_connectors(
    connectors: Vec<AppInfo>,
    accessible_connectors: Vec<AppInfo>,
) -> Vec<AppInfo>

作用:把普通连接器列表和“当前可访问”的连接器列表合成一份。有人会用它来得到最终展示给用户的连接器清单,既不重复,又尽量信息完整。

数据流:输入是两份 AppInfo 列表:一份基础连接器,一份可访问连接器。它先用连接器 id 做索引,把基础列表都先标成不可访问;然后逐个加入可访问列表,遇到同 id 就把可访问标记改成 true,并补上更正式的名字、说明、图标、发布渠道和插件展示名;遇到新 id 就直接加入。最后它给没有安装地址的连接器补上 connector_install_url 生成的链接,给插件展示名排序并去重,再用 sort_connectors_by_accessibility_and_name 排好顺序,输出合并后的列表。

调用关系:这是本文件最核心的合并步骤。merge_connectors_with_accessible 和 merge_plugin_connectors_with_accessible 会把准备好的连接器交给它做最终合并;两个测试也直接调用它,检查名字替换和展示名去重是否正确。它自己把安装链接生成交给 connector_install_url,把最终排序交给 sort_connectors_by_accessibility_and_name。

调用图:调用 2 个内部函数(connector_install_url, sort_connectors_by_accessibility_and_name);被 4 处调用(merge_connectors_with_accessible, merge_plugin_connectors_with_accessible, merge_connectors_replaces_plugin_placeholder_name_with_accessible_name, merge_connectors_unions_and_dedupes_plugin_display_names)。

merge_plugin_connectors60–78 ↗
fn merge_plugin_connectors(connectors: Vec<AppInfo>, plugin_app_ids: I) -> Vec<AppInfo>

作用:把插件提供的应用 id 补进已有连接器列表。它适合用在只知道插件 id、但还没有完整应用资料的时候。

数据流:输入是一份已有 AppInfo 列表,以及一批插件应用 id。它先记住已有连接器的 id,之后遍历插件 id:如果某个 id 已经存在,就跳过;如果是新的,就用 plugin_connector_to_app_info 造一个占位连接器加进去。最后调用 sort_connectors_by_accessibility_and_name 排序,输出补完后的列表。

调用关系:它是插件发现流程里的补名单工具。connectors_for_plugin_apps、merge_and_filter_plugin_connectors、list_tool_suggest_discoverable_tools_with_auth 会在整理插件相关连接器时调用它。它把“从 id 造占位连接器”的小活交给 plugin_connector_to_app_info,把排序交给 sort_connectors_by_accessibility_and_name。

调用图:调用 2 个内部函数(plugin_connector_to_app_info, sort_connectors_by_accessibility_and_name);被 3 处调用(connectors_for_plugin_apps, merge_and_filter_plugin_connectors, list_tool_suggest_discoverable_tools_with_auth)。

merge_plugin_connectors_with_accessible80–97 ↗
fn merge_plugin_connectors_with_accessible(
    plugin_app_ids: I,
    accessible_connectors: Vec<AppInfo>,
) -> Vec<AppInfo>

作用:只保留那些既是插件应用、又确实可访问的连接器,然后合并成最终列表。它用于避免把用户不能用的插件连接器拿出来展示。

数据流:输入是一批插件应用 id 和一份可访问连接器列表。它先从可访问列表里取出所有 id,做成一个快速查找集合;再遍历插件 id,只挑出也在可访问集合里的 id,并用 plugin_connector_to_app_info 变成占位 AppInfo;最后把这些占位连接器和完整的可访问连接器交给 merge_connectors,输出补全后的列表。

调用关系:它位于更高层的构建流程中,build_skills_and_plugins 和 built_tools 会调用它来准备可用的插件工具。它本身不做最终细节合并,而是先筛选,再把真正的合并工作交给 merge_connectors。

调用图:调用 1 个内部函数(merge_connectors);被 2 处调用(build_skills_and_plugins, built_tools);外部调用 1 个(into_iter)。

plugin_connector_to_app_info99–119 ↗
fn plugin_connector_to_app_info(connector_id: String) -> AppInfo

作用:把一个插件应用 id 包装成一个最基础的 AppInfo 连接器记录。它像是先给连接器建一张临时身份证,后面再用正式资料补全。

数据流:输入是一个 connector_id 字符串。它把这个 id 同时当作 id 和临时 name,其他说明、图标、品牌等资料先留空;安装地址用 connector_install_url 生成;可访问状态设为 false,启用状态设为 true,插件展示名为空。输出是一条可被后续合并流程使用的 AppInfo。

调用关系:merge_plugin_connectors 会用它为新增插件 id 创建占位连接器,merge_plugin_connectors_with_accessible 也通过它准备待合并的插件连接器。测试里也直接调用它,专门验证这种占位记录在 merge_connectors 里能被更完整的可访问连接器资料替换。

调用图:调用 1 个内部函数(connector_install_url);被 3 处调用(merge_plugin_connectors, merge_connectors_replaces_plugin_placeholder_name_with_accessible_name, merge_connectors_unions_and_dedupes_plugin_display_names);外部调用 1 个(new)。

tests::plugin_names128–130 ↗
fn plugin_names(names: &[&str]) -> Vec<String>

作用:这是测试里的小帮手,把一组简短的文本名字变成代码需要的字符串列表。它让测试用例写起来更短、更清楚。

数据流:输入是一组字符串切片,比如 alpha、beta。它逐个复制成 String,收集成 Vec<String>,输出给测试里的 AppInfo.plugin_display_names 使用。它不改动外部状态。

调用关系:它只在测试代码里服务。tests::google_calendar_accessible_connector 会用它生成可访问连接器的插件展示名,tests::merge_connectors_unions_and_dedupes_plugin_display_names 也会用它构造预期结果。

tests::google_calendar_accessible_connector132–148 ↗
fn google_calendar_accessible_connector(plugin_display_names: &[&str]) -> AppInfo

作用:这是测试用的样板数据生成器,专门造一条“Google Calendar 可访问连接器”。它让测试不用反复手写一大段 AppInfo。

数据流:输入是一组插件展示名。它调用 tests::plugin_names 把这些名字变成字符串列表,然后填入一条固定的 AppInfo:id 是 calendar,正式名字是 Google Calendar,带说明、图标、发布渠道,并标记为可访问。输出这条测试用连接器。

调用关系:它只被测试使用。两个 merge_connectors 相关测试都用它来模拟来自应用目录或工具发现的完整连接器资料,从而检查 merge_connectors 是否会正确补全插件占位记录。

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

tests::merge_connectors_replaces_plugin_placeholder_name_with_accessible_name151–176 ↗
fn merge_connectors_replaces_plugin_placeholder_name_with_accessible_name()

作用:这个测试确认:插件占位连接器的名字如果只是 id,合并后会被正式名称替换。没有这个保障,用户可能看到 calendar 这种机器味很重的名字,而不是 Google Calendar。

数据流:测试先用 plugin_connector_to_app_info 创建一个 id 为 calendar 的占位连接器,再用 tests::google_calendar_accessible_connector 创建一条完整的可访问连接器。它把两者交给 merge_connectors,然后用断言检查结果:名字、说明、图标、渠道和可访问标记都正确,同时安装链接保留为生成出来的链接,并额外确认提及用的 slug 会基于正式名字变成 google-calendar。

调用关系:它由测试运行器执行,用来守住 merge_connectors 的一个重要行为:占位名可以被正式资料替换。它调用 plugin_connector_to_app_info 准备插件侧数据,调用 tests::google_calendar_accessible_connector 准备可访问侧数据,再调用 merge_connectors 验证最终效果。

调用图:调用 2 个内部函数(merge_connectors, plugin_connector_to_app_info);外部调用 3 个(assert_eq!, google_calendar_accessible_connector, vec!)。

tests::merge_connectors_unions_and_dedupes_plugin_display_names179–205 ↗
fn merge_connectors_unions_and_dedupes_plugin_display_names()

作用:这个测试确认:来自不同来源的插件展示名会合在一起,并且重复项会被去掉。这样最终列表不会漏掉别名,也不会出现重复名字。

数据流:测试先创建一个 calendar 占位连接器,并给它放入 sample、alpha、sample 这些展示名,其中 sample 重复。然后创建一条可访问的 Google Calendar 连接器,带 beta、alpha。它把两者交给 merge_connectors,最后断言输出里的展示名变成排序且去重后的 alpha、beta、sample,同时其他连接器资料也被正确补全。

调用关系:它由测试运行器执行,用来保护 merge_connectors 对 plugin_display_names 的合并规则。它调用 plugin_connector_to_app_info 和 tests::google_calendar_accessible_connector 准备两边数据,调用 tests::plugin_names 构造输入和预期值,最后通过 merge_connectors 检查合并、排序、去重是否符合预期。

调用图:调用 2 个内部函数(merge_connectors, plugin_connector_to_app_info);外部调用 4 个(assert_eq!, google_calendar_accessible_connector, plugin_names, vec!)。

应用发现请求处理

此请求处理器通过协调缓存和刷新的连接器目录数据,并将更新流式传回客户端,来服务 apps/list

app-server/src/request_processors/apps_processor.rs源码 ↗
orchestrationrequest handling / background refresh / shutdown

客户端想看“有哪些应用可以用”时,不能只读一个固定清单。因为应用可能来自插件目录,也可能来自当前工作区能访问的工具,还要看用户是否登录、功能开关是否打开、线程自己的设置是否允许。这个文件就像前台服务员:先确认这桌客人有没有权限点菜,再去厨房和仓库同时查菜单。它会先用缓存结果尽快通知客户端,让界面别干等;然后后台继续刷新,等完整结果回来后再发最终列表。它还支持分页,避免一次塞太多数据。一个重要行为是:如果 Codex 应用还没准备好,它会先返回当前结果,然后偷偷强制刷新一次,争取让下一轮数据变完整。关闭服务时,它也能取消还没跑完的后台任务,避免旧请求继续乱发消息。

函数细节13
AppsRequestProcessor::new14–32 ↗
fn new(
        auth_manager: Arc<AuthManager>,
        thread_manager: Arc<ThreadManager>,
        outgoing: Arc<OutgoingMessageSender>,
        config_manager: ConfigManager,
        workspace_setti

作用:创建一个处理“应用列表请求”的对象,把它之后要用到的认证、线程、发送消息、配置和关闭信号都装进去。有人启动请求处理器时会用它。

数据流:进去的是各种已经建好的共享组件,比如认证管理器、线程管理器、消息发送器和配置管理器。它把这些组件保存到结构体里,并从关闭信号上做一个“守门员”,保证对象被丢掉时相关取消机制也能生效。出来的是一个可以接收应用列表请求的 AppsRequestProcessor。

调用关系:它在更外层的 new 流程里被调用,属于搭机器的步骤。后面的 apps_list、shutdown 等方法都依赖这里保存好的那些零件。

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

AppsRequestProcessor::apps_list34–42 ↗
async fn apps_list(
        &self,
        request_id: &ConnectionRequestId,
        params: AppsListParams,
    ) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError>

作用:这是客户端请求“列出应用”时进入这个文件的公开入口。它负责把内部结果包装成客户端能懂的响应格式。

数据流:进去的是请求编号和查询参数,比如分页游标、每页数量、线程编号、是否强制刷新。它把活交给 apps_list_inner,拿到内部格式的结果后转换成客户端响应;如果内部决定后台稍后回复,它也会保持这个“稍后回复”的状态。出来的是一个可能立刻返回、也可能表示异步等待的结果。

调用关系:它由 handle_initialized_client_request 在收到已初始化客户端的请求时调用。它自己不做复杂判断,只把请求转交给 apps_list_inner,并负责最后的格式转换。

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

AppsRequestProcessor::apps_list_inner44–109 ↗
async fn apps_list_inner(
        &self,
        request_id: &ConnectionRequestId,
        params: AppsListParams,
    ) -> Result<Option<AppsListResponse>, JSONRPCErrorError>

作用:这是处理应用列表请求的主调度函数。它先判断这个请求到底有没有资格看应用列表,然后决定是立刻返回空列表,还是启动后台任务去慢慢加载。

数据流:进去的是请求编号和应用列表参数。它可能先按 thread_id 找到对应线程,再按线程的当前目录加载最新配置;接着检查功能开关、登录状态和工作区是否允许 Codex 插件。如果不允许,就直接产出空列表;如果允许,就拿到环境、MCP、插件等管理器,启动一个后台任务。出来的结果通常是 None,意思是“请求已接收,稍后用消息发结果”;也可能是一个空列表响应或错误。

调用关系:它由 apps_list 调用,是这条请求的门卫和调度员。它会调用 load_thread、load_latest_config、workspace_codex_plugins_enabled 做前置检查,通过 tokio 的后台任务把真正耗时的加载交给 apps_list_task。

调用图:调用 3 个内部函数(load_latest_config, load_thread, workspace_codex_plugins_enabled);被 1 处调用(apps_list);外部调用 6 个(clone, child_token, new, clone, select!, spawn)。

AppsRequestProcessor::shutdown111–113 ↗
fn shutdown(&self)

作用:通知这个处理器停止工作,取消还没完成的后台应用列表任务。服务清理资源时会用它。

数据流:进去没有业务数据,只读取自己保存的关闭信号。它把这个信号设置为“已取消”,之后监听这个信号的后台任务就会停下来。出来没有返回值,但会改变运行中的取消状态。

调用关系:它由 clear_runtime_references 在清理运行时引用时调用。它和 apps_list_inner 里创建的子关闭信号配合,保证关闭时不会有旧任务继续发送响应。

调用图:被 1 处调用(clear_runtime_references);外部调用 1 个(cancel)。

AppsRequestProcessor::apps_list_task115–161 ↗
async fn apps_list_task(
        outgoing: Arc<OutgoingMessageSender>,
        request_id: ConnectionRequestId,
        params: AppsListParams,
        config: Config,
        environment_manager: Arc

作用:这是后台真正跑应用列表请求的任务。它调用加载函数拿结果,发送给客户端,并在发现 Codex 应用还没准备好时尝试再刷新一次。

数据流:进去的是消息发送器、请求编号、查询参数、配置,以及环境、MCP、插件管理器。它先保留一份参数和配置备用,然后调用 apps_list_response 生成列表;生成后通过 outgoing 按请求编号发回结果。如果结果显示 Codex 应用还没就绪,并且这次不是强制刷新,它会把 force_refetch 改成 true 再跑一遍,用来刷新缓存和发更新通知。出来没有直接返回给调用者,主要效果是向客户端发结果或写警告日志。

调用关系:它由 apps_list_inner 放进后台执行。它把核心加载工作交给 apps_list_response,再把结果交给 OutgoingMessageSender 发送;如果补刷失败,只记录 warn 日志,不再打断已经发出的主结果。

调用图:外部调用 5 个(clone, apps_list_response, clone, clone, warn!)。

AppsRequestProcessor::apps_list_response163–309 ↗
async fn apps_list_response(
        outgoing: &Arc<OutgoingMessageSender>,
        params: AppsListParams,
        config: Config,
        environment_manager: Arc<EnvironmentManager>,
        mcp_ma

作用:这是生成应用列表的核心流程:同时查“所有应用”和“当前可访问应用”,不断合并结果,必要时给客户端发列表更新,最后返回分页后的正式响应。

数据流:进去的是参数、配置、消息发送器,以及环境、MCP、插件管理器。它先解析分页游标,再从插件配置里拿插件应用;接着读取缓存的可访问应用和全部应用,先尽量发一次快速更新。随后它同时启动两个异步查询:一个查当前真正能访问哪些应用,一个查目录里所有应用。每收到一边结果,就合并应用、标上启用状态、决定是否通知客户端。两边都加载完后,它按 start 和 limit 切出一页,返回应用列表和 Codex 应用是否已准备好的标志。它也会处理游标错误、加载失败和 90 秒超时。

调用关系:它由 apps_list_task 调用,是这个文件里最核心的装配线。它会用 merge_loaded_apps 合并来源,用 should_send_app_list_updated_notification 判断要不要推送,用 send_app_list_updated_notification 发通知,最后用 paginate_apps 做正式分页。

调用图:调用 7 个内部函数(merge_loaded_apps, paginate_apps, send_app_list_updated_notification, should_send_app_list_updated_notification, list_all_connectors_with_options, list_accessible_connectors_from_mcp_tools_with_mcp_manager, with_app_enabled_state);外部调用 11 个(clone, clone, plugins_config_input, Accessible, Directory, format!, join!, spawn, unbounded_channel, now (+1 more))。

AppsRequestProcessor::load_thread311–325 ↗
async fn load_thread(
        &self,
        thread_id: &str,
    ) -> Result<(ThreadId, Arc<CodexThread>), JSONRPCErrorError>

作用:根据客户端给的线程编号找到正在管理的线程。这样列应用时可以继承这个线程的目录和功能开关。

数据流:进去的是一个字符串形式的 thread_id。它先把字符串转成系统内部的 ThreadId;如果格式不对,就返回“请求无效”。然后去线程管理器里找这个线程;找不到也返回“请求无效”。成功时出来的是解析后的线程编号和线程对象。

调用关系:它只被 apps_list_inner 在参数带 thread_id 时调用。它提供线程上下文,后面 apps_list_inner 会用这个线程决定配置目录和 Apps 功能是否打开。

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

AppsRequestProcessor::load_latest_config327–335 ↗
async fn load_latest_config(
        &self,
        fallback_cwd: Option<PathBuf>,
    ) -> Result<Config, JSONRPCErrorError>

作用:重新加载最新配置,保证列应用时用的是当前设置,而不是旧缓存。配置加载失败时会变成客户端能收到的内部错误。

数据流:进去的是一个可选的备用工作目录。如果有线程上下文,这个目录通常来自线程;否则可能为空。它把这个目录交给配置管理器加载最新配置;成功出来 Config,失败出来 JSON-RPC 风格的错误。

调用关系:它由 apps_list_inner 在前置检查阶段调用。后续功能开关、插件配置、应用来源判断,都依赖它拿到的 Config。

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

AppsRequestProcessor::workspace_codex_plugins_enabled337–357 ↗
async fn workspace_codex_plugins_enabled(
        &self,
        config: &Config,
        auth: Option<&CodexAuth>,
    ) -> bool

作用:检查当前工作区是否允许使用 Codex 插件。它是一个安全阀,避免在工作区明确禁用时还把相关应用展示出来。

数据流:进去的是当前配置和可选的登录信息。它询问 workspace_settings,并带上工作区设置缓存,判断这个工作区是否启用 Codex 插件。如果查询成功,就返回设置值;如果查询失败,它会记一条警告,并选择放行,避免因为设置服务临时出错就把功能全部关掉。

调用关系:它由 apps_list_inner 在决定是否继续加载应用前调用。它不直接列应用,只决定后面的 apps_list_task 有没有必要启动。

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

merge_loaded_apps367–375 ↗
fn merge_loaded_apps(
    all_connectors: Option<&[AppInfo]>,
    accessible_connectors: Option<&[AppInfo]>,
) -> Vec<AppInfo>

作用:把“全部应用列表”和“可访问应用列表”合成一份给界面看的列表。这样客户端既知道有哪些应用,也知道哪些当前能用。

数据流:进去的是两个可选列表:一个是全部应用,一个是当前可访问应用。它把缺失的列表当空列表处理,同时记住“全部应用”是否真的加载过。然后交给 connectors::merge_connectors_with_accessible 做合并,出来是一份 AppInfo 列表。

调用关系:它由 apps_list_response 在收到缓存结果或新加载结果后反复调用。它是两路数据汇合的地方,后面还会继续给这些应用标上启用状态并决定是否通知客户端。

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

should_send_app_list_updated_notification377–383 ↗
fn should_send_app_list_updated_notification(
    connectors: &[AppInfo],
    accessible_loaded: bool,
    all_loaded: bool,
) -> bool

作用:判断现在这份应用列表值不值得立刻通知客户端。它避免界面收到没意义的空更新。

数据流:进去的是当前合并后的应用列表,以及“可访问列表是否已加载”“全部列表是否已加载”两个状态。它检查列表里是否已经有可访问的应用;如果有,就应该通知。或者两边都加载完了,即使没有可访问应用,也应该通知客户端最终状态。出来是 true 或 false。

调用关系:它由 apps_list_response 在每次合并出新列表后调用。返回 true 时,apps_list_response 才会继续调用 send_app_list_updated_notification。

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

paginate_apps385–407 ↗
fn paginate_apps(
    connectors: &[AppInfo],
    start: usize,
    limit: Option<u32>,
) -> Result<AppsListResponse, JSONRPCErrorError>

作用:把完整应用列表切成客户端请求的那一页。这样应用很多时,客户端可以一页一页拿。

数据流:进去的是完整应用数组、起始位置 start 和可选的 limit。它先检查 start 有没有超过总数,超过就返回“请求无效”。然后算出本页结束位置,复制这一段应用作为 data;如果后面还有更多应用,就把下一个起点做成 next_cursor。出来的是 AppsListResponse。

调用关系:它由 apps_list_response 在两类应用数据都加载完成后调用。它是正式响应发回客户端前的最后整理步骤。

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

send_app_list_updated_notification409–418 ↗
async fn send_app_list_updated_notification(
    outgoing: &Arc<OutgoingMessageSender>,
    data: Vec<AppInfo>,
)

作用:主动给客户端推送“应用列表更新了”的通知。它用于在正式响应前或刷新过程中,让界面尽快显示新信息。

数据流:进去的是消息发送器和一份应用列表。它把列表包成 AppListUpdatedNotification,再包成服务器通知,通过 outgoing 发出去。出来没有业务返回值,主要效果是客户端收到一条更新消息。

调用关系:它由 apps_list_response 在判断需要通知时调用。它不决定发什么、不合并列表,只负责把已经准备好的列表送到客户端。

调用图:被 1 处调用(apps_list_response);外部调用 1 个(AppListUpdated)。

交互式文件搜索

此适配器提供一次性和基于会话的模糊文件搜索,负责结果转换、限制、取消以及抑制过期更新。

app-server/src/fuzzy_file_search.rs源码 ↗
domain_logicrequest handling / live search session

这个文件解决的是编辑器里常见的“模糊找文件”问题:用户不需要输入完整路径,只输入几个字符,系统就从指定目录里找最像的文件或文件夹。它的重要性在于,文件搜索可能很慢,不能卡住服务器主流程,所以这里会把真正的搜索丢到后台线程里做。它还限制最多返回 50 条结果,最多用 12 个线程,避免一次搜索把机器资源吃光。文件里有两种模式:run_fuzzy_file_search 是一次性搜索,拿到结果后直接返回;start_fuzzy_file_search_session 会创建一个长期搜索会话,适合用户不断修改搜索词。SessionReporterImpl 像一个“传话员”,底层搜索有新结果或结束时,它把结果转换成服务器协议里的通知,再通过 outgoing 发给客户端。这里还用取消标记 AtomicBool(一个线程安全的开关)防止会话结束后继续发送过期消息。

函数细节9
FuzzyFileSearchSession::update_query99–109 ↗
fn update_query(&self, query: String)

作用:更新一个正在运行的搜索会话里的搜索词。用户在输入框里继续打字时,就靠它把新词送进底层搜索器。

数据流:进去的是新的 query 字符串。它先看会话是否已经取消;如果没取消,就把 latest_query 这份共享记录改成新词,再调用底层 session.update_query 让搜索器按新词重新搜索。出来没有返回值,但会改变会话内部的当前搜索词,并触发后续更新结果。

调用关系:它属于 FuzzyFileSearchSession,通常由保存这个会话的上层代码在收到“搜索词变化”时调用。它把新搜索词交给底层 file_search 会话,之后结果会通过 SessionReporterImpl::on_update 再送回客户端。

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

FuzzyFileSearchSession::drop113–115 ↗
fn drop(&mut self)

作用:在搜索会话被销毁时,把会话标成已取消。这样后台搜索或回调就知道不要再给已经结束的客户端发消息。

数据流:进去的是即将被释放的会话对象本身。它把 shared.canceled 这个线程安全布尔值设为 true。出来没有返回值,但之后其他地方读到这个开关,就会停止更新和完成通知。

调用关系:它是 Rust 的 Drop 清理动作,会在 FuzzyFileSearchSession 生命周期结束时自动执行。SessionReporterImpl::send_snapshot 和 SessionReporterImpl::send_complete 都会读取这个取消标记,避免发送过期通知。

start_fuzzy_file_search_session118–158 ↗
fn start_fuzzy_file_search_session(
    session_id: String,
    roots: Vec<String>,
    outgoing: Arc<OutgoingMessageSender>,
) -> anyhow::Result<FuzzyFileSearchSession>

作用:启动一个可持续更新的模糊文件搜索会话。它适合前端打开搜索框后,随着用户不断输入,服务器不断推送新结果。

数据流:进去的是 session_id 会话编号、roots 搜索目录、outgoing 发消息通道。它设置结果上限和线程数,把目录变成路径,创建取消开关和共享状态,再创建一个 SessionReporterImpl 作为回调报告员,最后调用底层 create_session 建立真正的文件搜索会话。出来的是 FuzzyFileSearchSession;如果底层创建失败,就返回错误。

调用关系:它被 fuzzy_file_search_session_start_response 这个上层响应流程调用,用来开一场新的搜索会话。它把结果汇报工作交给 SessionReporterImpl,底层搜索器之后会通过 on_update 和 on_complete 通知它。

调用图:被 1 处调用(fuzzy_file_search_session_start_response);外部调用 9 个(new, new, default, new, new, new, create_session, available_parallelism, current)。

SessionReporterImpl::send_snapshot173–203 ↗
fn send_snapshot(&self, snapshot: &file_search::FileSearchSnapshot)

作用:把底层搜索器给出的“当前结果快照”发给客户端。它会过滤掉已经取消或已经过时的结果,避免客户端看到旧搜索词的结果。

数据流:进去的是 FileSearchSnapshot,也就是底层搜索器某一刻的搜索状态。它先检查会话是否取消,再读取最新搜索词;如果快照里的 query 不是最新词,就直接丢弃。若搜索词为空,它发送空结果;否则调用 collect_files 把匹配项转换并排序。最后包装成 FuzzyFileSearchSessionUpdated 通知,并在 Tokio 运行时里异步发出去。

调用关系:它由 SessionReporterImpl::on_update 调用,是搜索结果从底层搜索库流向客户端的关键一步。它会调用 collect_files 做格式转换,然后通过 OutgoingMessageSender 把通知交给网络/消息发送层。

调用图:调用 1 个内部函数(collect_files);被 1 处调用(on_update);外部调用 2 个(FuzzyFileSearchSessionUpdated, new)。

SessionReporterImpl::send_complete205–217 ↗
fn send_complete(&self)

作用:通知客户端:这个搜索会话已经完成。它只负责发“结束了”这个消息,不带文件列表。

数据流:进去的是 reporter 自己持有的共享状态。它先看取消开关;如果会话还有效,就取出 session_id 和 outgoing 发送器,创建 FuzzyFileSearchSessionCompleted 通知,并异步发送。出来没有返回值,但客户端会收到会话完成的消息。

调用关系:它由 SessionReporterImpl::on_complete 调用。底层搜索库宣布完成时,on_complete 把事件转给它,它再通过 outgoing 把完成通知发到客户端。

调用图:被 1 处调用(on_complete);外部调用 1 个(FuzzyFileSearchSessionCompleted)。

SessionReporterImpl::on_update221–223 ↗
fn on_update(&self, snapshot: &file_search::FileSearchSnapshot)

作用:这是底层搜索库要求实现的“有新结果时叫我”的接口。它本身很薄,只是把新快照交给 send_snapshot 处理。

数据流:进去的是底层搜索器传来的 FileSearchSnapshot。它不直接转换或发送,而是调用 send_snapshot,让那里完成取消检查、过期检查、结果转换和通知发送。出来没有返回值。

调用关系:它是 SessionReporterImpl 作为 file_search::SessionReporter 的一部分。底层搜索会话在结果变化时调用它,它再把工作交给 SessionReporterImpl::send_snapshot。

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

SessionReporterImpl::on_complete225–227 ↗
fn on_complete(&self)

作用:这是底层搜索库要求实现的“搜索结束时叫我”的接口。它把结束事件转成客户端能理解的完成通知。

数据流:进去没有额外数据,只有 reporter 自己保存的会话信息。它调用 send_complete,由后者检查是否取消、生成完成通知并发送。出来没有返回值。

调用关系:它是 SessionReporterImpl 实现 file_search::SessionReporter 的另一个回调。底层搜索会话完成时调用它,它再把后续发送动作交给 SessionReporterImpl::send_complete。

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

collect_files230–256 ↗
fn collect_files(snapshot: &file_search::FileSearchSnapshot) -> Vec<FuzzyFileSearchResult>

作用:把底层搜索结果整理成服务器要发给客户端的格式。它还会重新排序,保证最好、最相关的结果排在前面。

数据流:进去的是 FileSearchSnapshot,里面有一批匹配到的文件或文件夹。它逐条取出根目录、完整路径、文件名、匹配类型、分数和命中的字符位置,把这些变成 FuzzyFileSearchResult。然后按分数从高到低、路径从小到大排序。出来的是整理好的结果列表。

调用关系:它被 SessionReporterImpl::send_snapshot 调用,只服务于会话更新通知。send_snapshot 负责判断该不该发,collect_files 负责把能发的搜索快照变成客户端协议里的文件列表。

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