Codex 系统手册

异步原语、图像处理与其他小型支持库

stage-22.525 个文件

这一阶段是后台工具箱,不是主流程本身,却让系统跑得稳。异步工具负责“等到就绪”和“随时取消”;倒计时、刷新限速、缓存、防休眠让任务别卡住、别太耗电、别中途睡着。图片工具负责读图、缩图、缓存、转成模型或终端能接受的格式,还能把宠物动画切帧显示。另有运行时值转换、聊天事件重放筛选、沙箱和配置摘要,以及一个 V8 连接试验。它们像一盒小零件,被各处按需拿来用。

本阶段的文件25

终端宠物图像渲染

这些文件准备缓存的宠物动画帧,并选择或编码用于显示它们的终端图像协议载荷。

tui/src/pets/frames.rs源码 ↗
domain_logicstartup / pet asset loading

宠物动画通常不是一堆单独图片,而是一张 spritesheet(精灵图,也就是把很多小画面按行列排在一张大图里)。这个文件做的事,就像把一整版邮票按格子裁开:先算出应该有多少张 frame_000.png、frame_001.png 这样的帧图;如果这些文件已经齐了,就直接复用,省时间;如果不齐,就清掉旧的帧文件,重新打开宠物的精灵图,按宠物配置里的行数、列数、每帧宽高,把每一格裁出来并保存成 PNG。它还会检查数字计算有没有溢出,避免因为尺寸或数量异常而切错位置。里面的测试用一张极小的假图片验证:不靠外部命令,也能正确切出帧文件。

函数细节3
prepare_png_frames11–52 ↗
fn prepare_png_frames(pet: &Pet, frame_dir: &Path) -> Result<Vec<PathBuf>>

作用:准备某个宠物动画需要用到的所有 PNG 帧图。调用者给它宠物信息和一个输出目录,它会保证这个目录里有完整的一组 frame_XXX.png 文件。

数据流:进去的是一个 Pet(里面有精灵图路径、每帧宽高、行列数、总帧数等信息)和一个帧图目录。它先创建目录,再按总帧数算出所有应该存在的文件名;如果文件已经齐全,就直接返回这些路径;如果不齐,就找出旧的 frame_*.png 删除,打开精灵图,按行列一格格裁剪,保存成 PNG。出来的是按顺序排列的帧图片路径;同时磁盘上的帧目录可能被新建、清理和写入新图片。

调用关系:它通常由加载宠物资源的流程 load 调用,相当于在宠物真正显示前把动画素材准备好。它自己会请 glob_frame_files 帮忙找旧帧文件,也会使用图片库打开和裁剪精灵图;测试函数 tests::prepare_png_frames_slices_spritesheet_without_external_command 会专门调用它,确认这套切图流程能正常工作。

调用图:调用 2 个内部函数(glob_frame_files, frame_count);被 2 处调用(load, prepare_png_frames_slices_spritesheet_without_external_command);外部调用 4 个(create_dir_all, remove_file, open, try_from)。

glob_frame_files54–71 ↗
fn glob_frame_files(frame_dir: &Path) -> Result<Vec<PathBuf>>

作用:找出某个目录里看起来像宠物帧图的文件,也就是名字以 frame_ 开头、以 .png 结尾的文件。它主要用来在重新切图前清理旧文件。

数据流:进去的是一个目录路径。它先看目录是否存在;如果不存在,就返回空列表。目录存在时,它逐个读取目录里的文件,只挑出文件名符合 frame_*.png 这种规则的路径。出来的是这些旧帧文件的路径列表;它本身不删除文件,只负责找出来。

调用关系:它是 prepare_png_frames 的小帮手。prepare_png_frames 发现帧图不完整时,会先调用它把旧帧图名单拿到手,然后再逐个删除,避免新旧帧混在一起造成动画显示错乱。

调用图:被 1 处调用(prepare_png_frames);外部调用 3 个(exists, new, read_dir)。

tests::prepare_png_frames_slices_spritesheet_without_external_command83–115 ↗
fn prepare_png_frames_slices_spritesheet_without_external_command()

作用:这是一个自动测试,用来证明切帧功能真的能把一张精灵图切成多个 PNG 文件,而且不需要依赖系统里的外部图片处理命令。

数据流:进去没有外部输入,测试自己创建一个临时目录和一张 2 像素宽、1 像素高的小图片:左边红色、右边绿色。它构造一个 Pet,说明每帧是 1×1、一共两帧,然后调用 prepare_png_frames。最后它检查返回了两个路径,并且这两个文件确实已经写到磁盘上。

调用关系:它只在测试时运行,不参与正常程序。它站在使用者角度调用 prepare_png_frames,模拟资源加载时的真实场景,用断言检查结果,防止以后改代码时不小心把切图功能弄坏。

调用图:调用 1 个内部函数(prepare_png_frames);外部调用 6 个(new, from_fn, new, assert!, assert_eq!, tempdir)。

tui/src/pets/image_protocol.rs源码 ↗
io_transportcross-cutting:启动或配置解析时检测支持情况,渲染宠物图片时生成终端图片指令,Sixel 图片会按需写入缓存

终端本来主要显示文字,不同终端显示图片的方法也不一样。这个文件就像一个“图片播放适配器”:先检查是不是在 tmux、Zellij 这类终端分屏工具里,因为图片可能串到别的窗格或弄乱滚动记录,所以默认禁用;再识别 Kitty、WezTerm、Ghostty、iTerm2、Windows Terminal 等环境,选择 Kitty graphics 或 Sixel 协议。Kitty graphics 和 Sixel 都是“用特殊控制字符告诉终端画图”的协议。选好协议后,它还能把 PNG 文件读出来、转成 base64 文本,拼成终端控制序列;或者为 iTerm2 这类场景只传本地文件路径。Sixel 路线会把图片缩放到合适高度,编码后缓存到磁盘,避免每一帧都重新生成。

函数细节40
PetImageSupport::protocol40–45 ↗
fn protocol(self) -> Option<ImageProtocol>

作用:把“是否支持宠物图片”的结果,拆成一个更简单的“可用协议”。如果不支持,就返回空,方便别的代码快速判断能不能画图。

数据流:输入是一份支持状态:可能是 Supported,里面带着 Kitty 或 Sixel;也可能是 Unsupported,里面带着原因。它只看这个状态,支持就取出协议,不支持就给出 None,不改任何外部东西。

调用关系:宠物绘制相关代码会调用它,比如 draw_request、preview_draw_request、image_enabled、next_frame_delay,用它决定后面要不要继续准备图片帧。

调用图:被 4 处调用(draw_request, image_enabled, next_frame_delay, preview_draw_request)。

PetImageSupport::unsupported_message47–52 ↗
fn unsupported_message(self) -> Option<&'static str>

作用:把“不支持宠物图片”的结果变成一段能给用户看的说明。支持时就不返回说明。

数据流:输入是一份支持状态。它如果看到 Unsupported,就把里面的原因交给 PetImageUnsupportedReason::message 变成人话;如果是 Supported,就返回 None。

调用关系:它是用户提示文字的出口。上层界面可以用它解释为什么宠物没有出现,而不需要自己理解 tmux、Zellij 或终端协议细节。

PetImageUnsupportedReason::message64–79 ↗
fn message(self) -> &'static str

作用:为每一种禁用原因准备固定的用户提示。它告诉用户是因为 tmux、Zellij、iTerm2 太旧,还是当前终端本身不支持图片。

数据流:输入是一个具体原因枚举。它按原因选择一段静态文本输出,不读取环境,也不修改状态。

调用关系:PetImageSupport::unsupported_message 会调用它,把内部原因翻译成界面上可以直接显示的说明。

ProtocolSelection::resolve90–96 ↗
fn resolve(self) -> PetImageSupport

作用:把用户选择的图片协议变成最终结果。用户如果明确选 Kitty 或 Sixel,就直接照办;如果选 auto,就自动检测当前终端。

数据流:输入是 Auto、Kitty、Sixel 三选一。Kitty 和 Sixel 会直接输出 Supported;Auto 会调用 detect_pet_image_support 读取环境和终端信息后输出支持或不支持。

调用关系:这是配置值通向实际渲染能力的入口。它把“用户想怎么选”和“机器实际能不能用”接起来。

调用图:调用 1 个内部函数(detect_pet_image_support);外部调用 1 个(Supported)。

ProtocolSelection::from_str102–109 ↗
fn from_str(value: &str) -> Result<Self>

作用:把配置文件或命令行里的文字变成协议选择。只接受 auto、kitty、sixel,写错会报清楚的错误。

数据流:输入是一段字符串。它匹配已知值并输出对应的 ProtocolSelection;如果是别的词,就用 bail! 生成错误,告诉调用者可选项是什么。

调用关系:它通常在读取配置或解析参数时使用,确保后面的 ProtocolSelection::resolve 收到的是合法选择。

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

detect_pet_image_support112–133 ↗
fn detect_pet_image_support() -> PetImageSupport

作用:自动判断当前运行环境能不能显示终端宠物图片。它先避开已知危险环境,再根据终端能力选择合适协议。

数据流:它读取环境变量,比如 TMUX、ZELLIJ、KITTY_WINDOW_ID、WEZTERM_VERSION。发现 tmux 或 Zellij 就输出不支持;发现 Kitty 或 WezTerm 环境就输出 Kitty;否则读取 terminal_info,再交给 pet_image_support_for_terminal 判断。

调用关系:ProtocolSelection::resolve 在 auto 模式下会调用它。它是自动检测的第一关,先处理最直接、最可靠的环境变量,再把复杂终端信息交给下一层。

调用图:调用 1 个内部函数(pet_image_support_for_terminal);被 1 处调用(resolve);外部调用 4 个(terminal_info, var_os, Supported, Unsupported)。

pet_image_support_for_terminal135–163 ↗
fn pet_image_support_for_terminal(info: &TerminalInfo) -> PetImageSupport

作用:根据整理好的终端信息,决定宠物图片该用哪种协议,或者为什么不能用。

数据流:输入是 TerminalInfo,里面有终端名字、版本、TERM 字段、TERM_PROGRAM 字段、是否在分屏工具里等。它先检查 tmux/Zellij 安全问题,再判断 iTerm2 新版本、普通 Kitty 图形、Sixel,最后都不符合就输出 Terminal 不支持。

调用关系:detect_pet_image_support 会把 terminal_info 的结果交给它。它内部再调用多个小判断函数,像过安检一样一项项排除和确认。

调用图:调用 4 个内部函数(is_iterm2_terminal, supports_iterm2_kitty_graphics, supports_kitty_graphics, supports_sixel);被 1 处调用(detect_pet_image_support);外部调用 2 个(Supported, Unsupported)。

supports_iterm2_kitty_graphics165–171 ↗
fn supports_iterm2_kitty_graphics(info: &TerminalInfo) -> bool

作用:判断 iTerm2 是否新到足以使用 Kitty 风格的本地文件图片显示方式。这里要求 iTerm2 版本至少是 3.6.0。

数据流:输入是 TerminalInfo。它先确认是不是 iTerm2,再把版本字符串交给 version_is_at_least 比较;两个条件都满足才输出 true。

调用关系:pet_image_support_for_terminal 会优先问它,因为新 iTerm2 走的是 KittyLocalFile 这种特殊路线。

调用图:调用 2 个内部函数(is_iterm2_terminal, version_is_at_least);被 1 处调用(pet_image_support_for_terminal)。

is_iterm2_terminal173–176 ↗
fn is_iterm2_terminal(info: &TerminalInfo) -> bool

作用:判断当前终端是不是 iTerm2。它不只看标准名字,也会看 TERM_PROGRAM 这类环境字段里有没有 iterm。

数据流:输入是 TerminalInfo。它检查 name 是否是 Iterm2,或把 term_program 转小写后搜索 iterm;匹配到就输出 true。

调用关系:pet_image_support_for_terminal 用它识别旧 iTerm2,supports_iterm2_kitty_graphics 也用它确认新 iTerm2。

调用图:调用 1 个内部函数(terminal_field_contains);被 2 处调用(pet_image_support_for_terminal, supports_iterm2_kitty_graphics);外部调用 1 个(matches!)。

supports_kitty_graphics178–188 ↗
fn supports_kitty_graphics(info: &TerminalInfo) -> bool

作用:判断当前终端是否支持 Kitty graphics。Kitty graphics 是一种终端图片协议,很多现代终端也兼容它。

数据流:输入是 TerminalInfo。它检查终端名是否是 Ghostty、Kitty、WezTerm,也检查 term 和 term_program 字段里是否包含 kitty、ghostty、wezterm。符合任一条件就输出 true。

调用关系:pet_image_support_for_terminal 在排除 iTerm2 特殊情况后调用它,用来选择普通 Kitty 图片传输方式。

调用图:调用 1 个内部函数(terminal_field_contains);被 1 处调用(pet_image_support_for_terminal);外部调用 1 个(matches!)。

supports_sixel190–195 ↗
fn supports_sixel(info: &TerminalInfo) -> bool

作用:判断当前终端是否支持 Sixel 图片。Sixel 是一种较老但仍有不少终端支持的图片绘制协议。

数据流:输入是 TerminalInfo。它检查是否是 Windows Terminal,或者 TERM 字段里是否出现 sixel、mlterm、foot。匹配就输出 true。

调用关系:pet_image_support_for_terminal 在 Kitty 路线不合适时调用它,作为另一种图片显示方案。

调用图:调用 1 个内部函数(terminal_field_contains);被 1 处调用(pet_image_support_for_terminal);外部调用 1 个(matches!)。

terminal_field_contains197–199 ↗
fn terminal_field_contains(value: Option<&str>, needle: &str) -> bool

作用:安全地在一个可选的终端字段里搜索关键词,不区分大小写。它避免每个判断函数都重复写同样的空值检查。

数据流:输入是一个可能为空的字符串和要找的关键词。字符串存在时先转小写再搜索;不存在就直接输出 false。

调用关系:is_iterm2_terminal、supports_kitty_graphics、supports_sixel 都用它来检查 TERM 或 TERM_PROGRAM 这类字段。

调用图:被 3 处调用(is_iterm2_terminal, supports_kitty_graphics, supports_sixel)。

version_is_at_least201–203 ↗
fn version_is_at_least(version: Option<&str>, minimum: (u64, u64, u64)) -> bool

作用:判断一个版本号是否不低于指定最低版本。比如确认 iTerm2 是否至少是 3.6.0。

数据流:输入是可能为空的版本字符串,以及最低版本三元组。它先调用 parse_dotted_version 解析版本,解析成功后做大小比较;解析失败就输出 false。

调用关系:supports_iterm2_kitty_graphics 用它来做版本门槛判断。

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

parse_dotted_version205–217 ↗
fn parse_dotted_version(version: Option<&str>) -> Option<(u64, u64, u64)>

作用:把“3.6.10”这种点分版本号拆成三个数字,方便比较大小。

数据流:输入是可能为空的版本字符串。它按点分割,主版本必须有,次版本和补丁号没有就当 0;如果有多余段或不是纯数字,就返回 None。

调用关系:version_is_at_least 调用它,把原始文本版本变成可比较的数字版本。

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

kitty_delete_image219–221 ↗
fn kitty_delete_image(image_id: u32) -> String

作用:生成一条 Kitty graphics 删除图片的终端指令。调用者给图片编号,它返回能让终端清掉这张图的字符串。

数据流:输入是 image_id。它把编号拼进 Kitty 删除命令,再交给 wrap_for_tmux_if_needed;输出是最终要写到终端的控制字符串。

调用关系:它和图片传输函数是一组:传图函数负责显示,这个函数负责按编号清理终端里的旧图。

调用图:调用 1 个内部函数(wrap_for_tmux_if_needed);外部调用 1 个(format!)。

kitty_transmit_png_with_id223–252 ↗
fn kitty_transmit_png_with_id(
    path: &Path,
    columns: u16,
    rows: u16,
    image_id: Option<u32>,
) -> Result<String>

作用:把 PNG 文件内容直接塞进 Kitty graphics 指令里发送给终端。适合终端需要收到完整图片数据的情况。

数据流:输入是 PNG 路径、要占用的列数和行数、可选图片编号。它读取文件,把二进制 PNG 转成 base64 文本,按 4096 字节分块,拼成一段或多段 Kitty 控制序列,最后按需加 tmux 包装;输出这整串终端命令。

调用关系:render_pet_image 会在 Kitty 普通传输路线中调用它;测试 kitty_png_transmission_encodes_inline_data 会验证它确实把文件内容编码进命令里。

调用图:调用 2 个内部函数(kitty_image_id_arg, wrap_for_tmux_if_needed);被 2 处调用(kitty_png_transmission_encodes_inline_data, render_pet_image);外部调用 5 个(new, format!, read, from_utf8, from)。

kitty_transmit_png_file_with_id254–268 ↗
fn kitty_transmit_png_file_with_id(
    path: &Path,
    columns: u16,
    rows: u16,
    image_id: Option<u32>,
) -> Result<String>

作用:生成一条“让终端自己读取本地 PNG 文件”的 Kitty 指令。它不传图片内容,而是传文件路径。

数据流:输入是图片路径、列数、行数、可选图片编号。它先把路径规范成绝对路径,再把路径文字转成 base64,拼进 Kitty 本地文件命令,最后按需加 tmux 包装;输出终端命令字符串。

调用关系:render_pet_image 会在 KittyLocalFile 路线中调用它,尤其适合新 iTerm2 这类支持通过本地文件引用显示图片的终端。

调用图:调用 2 个内部函数(kitty_image_id_arg, wrap_for_tmux_if_needed);被 2 处调用(kitty_file_png_transmission_encodes_local_file_reference, render_pet_image);外部调用 3 个(canonicalize, to_string_lossy, format!)。

kitty_image_id_arg270–274 ↗
fn kitty_image_id_arg(image_id: Option<u32>) -> String

作用:把可选图片编号变成 Kitty 命令里需要的参数片段。没有编号时就什么都不加。

数据流:输入是 Option<u32>。有编号就输出类似“,i=7”的字符串;没有就输出空字符串。

调用关系:kitty_transmit_png_with_id 和 kitty_transmit_png_file_with_id 都调用它,避免两处重复拼图片编号。

调用图:被 2 处调用(kitty_transmit_png_file_with_id, kitty_transmit_png_with_id)。

wrap_for_tmux_if_needed276–283 ↗
fn wrap_for_tmux_if_needed(command: &str) -> String

作用:如果当前在 tmux 里,就把终端图片控制序列包成 tmux 能透传的格式。tmux 是终端复用器,直接发控制序列可能被它拦住或误解。

数据流:输入是一段终端控制命令。它读取 TMUX 环境变量;不在 tmux 就原样返回;在 tmux 就把 ESC 控制字符转义,并包进 tmux passthrough 外壳里。

调用关系:kitty_delete_image、kitty_transmit_png_with_id、kitty_transmit_png_file_with_id 都在输出前调用它,保证命令在 tmux 环境里格式正确。

调用图:被 3 处调用(kitty_delete_image, kitty_transmit_png_file_with_id, kitty_transmit_png_with_id);外部调用 2 个(var_os, format!)。

sixel_frame285–310 ↗
fn sixel_frame(frame_path: &Path, cache_dir: &Path, height_px: u16) -> Result<PathBuf>

作用:把一帧图片转换成 Sixel 文件,并缓存起来。这样同一张图、同一高度下次不用重新缩放和编码。

数据流:输入是原始帧路径、缓存目录、目标高度像素。它创建缓存目录,根据文件名和高度算出缓存文件名;如果缓存已存在就直接返回路径。否则打开图片,按比例缩放,转成 RGBA 像素,再调用 sixel::encode_rgba 编码,写入 .six 文件,最后返回缓存路径。

调用关系:render_pet_image 会在 Sixel 协议路线中调用它;测试 sixel_frame_encodes_without_external_crate 会确认它能生成有效 Sixel 内容。

调用图:调用 1 个内部函数(encode_rgba);被 2 处调用(sixel_frame_encodes_without_external_crate, render_pet_image);外部调用 8 个(file_stem, join, format!, create_dir_all, write, open, from, from)。

tests::EnvVarGuard::new324–331 ↗
fn new(name: &'static str, value: Option<&str>) -> Self

作用:测试用的小工具,用来临时设置或删除环境变量,并记住原来的值。这样测试不会把全局环境弄乱。

数据流:输入是环境变量名和新的可选值。它先读取旧值,再按要求 set_var 或 remove_var,最后返回一个守卫对象,里面保存旧值。

调用关系:多个测试在模拟 tmux、WezTerm 等环境时使用它;测试结束时会自动触发 tests::EnvVarGuard::drop 恢复现场。

调用图:外部调用 3 个(remove_var, set_var, var_os)。

tests::EnvVarGuard::drop335–340 ↗
fn drop(&mut self)

作用:在测试守卫对象销毁时恢复环境变量。它保证一个测试改过的环境不会影响下一个测试。

数据流:输入是守卫对象自己保存的变量名和旧值。旧值存在就设回去;旧值原本不存在就删除变量;它会消耗保存的旧值。

调用关系:它由 Rust 的 Drop 机制自动调用。凡是 tests::EnvVarGuard::new 创建的守卫离开作用域,都会走到这里。

调用图:外部调用 2 个(remove_var, set_var)。

tests::kitty_png_transmission_encodes_inline_data345–359 ↗
fn kitty_png_transmission_encodes_inline_data()

作用:验证 Kitty 内联传图会把 PNG 文件内容编码进控制命令里。

数据流:它临时确保不在 tmux,创建临时 PNG 文件,调用 kitty_transmit_png_with_id,然后检查命令开头、base64 内容和结尾是否符合预期。

调用关系:这是 kitty_transmit_png_with_id 的直接测试,保证渲染时发给终端的字符串不是空壳。

调用图:调用 1 个内部函数(kitty_transmit_png_with_id);外部调用 4 个(new, assert!, write, tempdir)。

tests::tmux_passthrough_wraps_and_escapes_control_sequence363–369 ↗
fn tmux_passthrough_wraps_and_escapes_control_sequence()

作用:验证在 tmux 环境里,控制序列会被正确包装和转义。

数据流:它临时设置 TMUX 环境变量,把一段带 ESC 的命令交给 wrap_for_tmux_if_needed,然后比较返回值是否是 tmux passthrough 格式。

调用关系:这是 wrap_for_tmux_if_needed 的核心安全测试,防止图片命令在 tmux 中被错误解释。

调用图:外部调用 2 个(new, assert_eq!)。

tests::parses_protocol_selection372–385 ↗
fn parses_protocol_selection()

作用:验证 auto、kitty、sixel 三个配置文字能正确解析成协议选择。

数据流:它把三个字符串分别 parse 成 ProtocolSelection,然后和期望枚举值比较。

调用关系:这是 ProtocolSelection::from_str 的基础测试,确保配置入口接受正确写法。

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

tests::auto_protocol_is_disabled_inside_tmux389–396 ↗
fn auto_protocol_is_disabled_inside_tmux()

作用:验证自动检测模式在 tmux 里会禁用宠物图片。

数据流:它临时设置 TMUX 环境变量,调用 ProtocolSelection::Auto.resolve,然后确认结果是不支持,原因是 Tmux。

调用关系:它覆盖 ProtocolSelection::resolve 到 detect_pet_image_support 的路径,确认安全优先于显示效果。

调用图:外部调用 2 个(new, assert_eq!)。

tests::explicit_protocol_still_resolves_inside_tmux400–411 ↗
fn explicit_protocol_still_resolves_inside_tmux()

作用:验证用户明确指定协议时,不会被自动检测的 tmux 禁用规则拦住。

数据流:它临时设置 TMUX,然后分别解析 Kitty 和 Sixel 的 resolve 结果,确认都返回 Supported。

调用关系:它测试 ProtocolSelection::resolve 的“显式选择直接照办”规则,和 auto 模式形成对照。

调用图:外部调用 2 个(new, assert_eq!)。

tests::pet_image_support_prefers_multiplexer_safety414–433 ↗
fn pet_image_support_prefers_multiplexer_safety()

作用:验证只要终端信息显示在 tmux 或 Zellij 里,就优先禁用图片,即使底层终端本身支持图片。

数据流:它构造带 tmux 的 Ghostty 信息、带 Zellij 的 Kitty 信息,交给 pet_image_support_for_terminal,然后检查返回的禁用原因。

调用关系:它直接测试 pet_image_support_for_terminal 的第一层安全判断,防止后面的 Kitty 支持判断抢先通过。

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

tests::pet_image_support_detects_iterm2_kitty_file_graphics436–458 ↗
fn pet_image_support_detects_iterm2_kitty_file_graphics()

作用:验证新版本 iTerm2 会被识别为支持 KittyLocalFile。

数据流:它构造 iTerm2 3.6.10 的终端信息,包括标准名字识别和 TERM_PROGRAM 识别两种情况,再检查 pet_image_support_for_terminal 的结果。

调用关系:它覆盖 supports_iterm2_kitty_graphics、is_iterm2_terminal、version_is_at_least 这条判断链。

调用图:外部调用 2 个(assert_eq!, terminal_info_with_version_for_test)。

tests::pet_image_support_rejects_old_iterm2_versions461–490 ↗
fn pet_image_support_rejects_old_iterm2_versions()

作用:验证旧版或无法确认版本的 iTerm2 会被拒绝,并标记为版本太旧。

数据流:它构造 3.5.14、无版本、3.5 等 iTerm2 信息,交给 pet_image_support_for_terminal,检查结果都是 Iterm2TooOld。

调用关系:它保证 iTerm2 不会因为名字匹配就盲目启用图片,必须过版本门槛。

调用图:外部调用 2 个(assert_eq!, terminal_info_with_version_for_test)。

tests::pet_image_support_old_iterm2_message_mentions_upgrade493–501 ↗
fn pet_image_support_old_iterm2_message_mentions_upgrade()

作用:验证旧 iTerm2 的提示文字明确告诉用户需要升级。

数据流:它构造一个 Unsupported(Iterm2TooOld),调用 unsupported_message,然后比较返回的完整提示。

调用关系:它测试 PetImageSupport::unsupported_message 和 PetImageUnsupportedReason::message 的用户提示链路。

调用图:外部调用 2 个(assert_eq!, Unsupported)。

tests::pet_image_support_detects_kitty_graphics_terminals504–548 ↗
fn pet_image_support_detects_kitty_graphics_terminals()

作用:验证 Kitty、Ghostty、WezTerm 等支持 Kitty graphics 的终端能被识别出来。

数据流:它构造多种终端信息,有的靠终端名,有的靠 TERM 或 TERM_PROGRAM 字段,然后检查 pet_image_support_for_terminal 都返回 Kitty。

调用关系:它主要覆盖 supports_kitty_graphics,确保常见兼容终端不会被误判成不支持。

调用图:外部调用 2 个(assert_eq!, terminal_info_for_test)。

tests::pet_image_support_detects_sixel_terminals551–583 ↗
fn pet_image_support_detects_sixel_terminals()

作用:验证支持 Sixel 的终端能被识别出来。

数据流:它构造 TERM 里带 xterm-sixel、foot、mlterm 的信息,以及 Windows Terminal 信息,然后检查结果都是 Sixel。

调用关系:它覆盖 supports_sixel,保证 Kitty 不适用时还有 Sixel 备选路线。

调用图:外部调用 2 个(assert_eq!, terminal_info_for_test)。

tests::wezterm_env_uses_kitty_graphics_for_ambient_pets587–601 ↗
fn wezterm_env_uses_kitty_graphics_for_ambient_pets()

作用:验证只靠 WezTerm 环境变量也能自动启用 Kitty graphics。

数据流:它先清掉 tmux、Zellij、Kitty 等干扰变量,再设置 WEZTERM_VERSION,调用 detect_pet_image_support,确认返回 Kitty。

调用关系:它测试 detect_pet_image_support 的环境变量快速路径,不依赖 terminal_info。

调用图:外部调用 2 个(new, assert_eq!)。

tests::pet_image_support_rejects_unknown_terminals604–614 ↗
fn pet_image_support_rejects_unknown_terminals()

作用:验证普通未知终端不会被误判为支持图片。

数据流:它构造一个 Unknown 且 TERM 为 xterm-256color 的终端信息,调用 pet_image_support_for_terminal,确认返回 Terminal 不支持。

调用关系:它覆盖检测链的最后兜底分支,防止没有证据时贸然启用终端图片。

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

tests::terminal_info_for_test616–629 ↗
fn terminal_info_for_test(
        name: TerminalName,
        multiplexer: Option<Multiplexer>,
        term_program: Option<&str>,
        term: Option<&str>,
    ) -> TerminalInfo

作用:测试辅助函数,用较少参数快速构造 TerminalInfo。

数据流:输入是终端名、可选分屏工具、可选 TERM_PROGRAM、可选 TERM。它把版本固定为 None,再交给 terminal_info_with_version_for_test 创建完整结构。

调用关系:多个终端检测测试用它减少重复代码;需要版本号的测试则直接用 terminal_info_with_version_for_test。

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

tests::terminal_info_with_version_for_test631–645 ↗
fn terminal_info_with_version_for_test(
        name: TerminalName,
        multiplexer: Option<Multiplexer>,
        term_program: Option<&str>,
        version: Option<&str>,
        term: Option<&s

作用:测试辅助函数,用来构造带版本号的 TerminalInfo。

数据流:输入是终端名、分屏工具、TERM_PROGRAM、版本、TERM。它把可选字符串转成 owned String,填进 TerminalInfo 后返回。

调用关系:iTerm2 版本测试和 terminal_info_for_test 都依赖它,方便模拟各种终端环境。

tests::parse_dotted_version_requires_simple_numeric_components648–655 ↗
fn parse_dotted_version_requires_simple_numeric_components()

作用:验证版本号解析只接受简单数字点分格式。

数据流:它分别检查 3.6.10、3.6、3 能解析成三段数字,也检查 3.6.10.1、3.6beta、None 会解析失败。

调用关系:它直接保护 parse_dotted_version 的规则,避免版本比较接受含糊格式。

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

tests::sixel_frame_encodes_without_external_crate658–672 ↗
fn sixel_frame_encodes_without_external_crate()

作用:验证 Sixel 帧生成能正常工作,并且生成的内容看起来像有效 Sixel 控制序列。

数据流:它创建一张 1×1 红色 PNG,调用 sixel_frame 生成缓存文件,再读回文本,检查开头、颜色定义、像素数据和结尾。

调用关系:这是 sixel_frame 的直接测试,也间接验证 sixel::encode_rgba 能被正确接入。

调用图:调用 1 个内部函数(sixel_frame);外部调用 5 个(assert!, read_to_string, Rgba, from_pixel, tempdir)。

tests::kitty_file_png_transmission_encodes_local_file_reference676–696 ↗
fn kitty_file_png_transmission_encodes_local_file_reference()

作用:验证 Kitty 本地文件传图会把规范化后的文件路径编码进命令,并带上图片编号。

数据流:它创建临时 PNG,调用 kitty_transmit_png_file_with_id,自己算出规范路径的 base64,再和完整命令逐字比较。

调用关系:这是 kitty_transmit_png_file_with_id 的直接测试,确保 KittyLocalFile 路线传的是路径而不是文件内容。

调用图:调用 1 个内部函数(kitty_transmit_png_file_with_id);外部调用 4 个(new, assert_eq!, write, tempdir)。

tui/src/pets/sixel.rs源码 ↗
io_transport渲染宠物帧、准备写到终端时

宠物动画到这里时,已经是一块块 RGBA 图片数据了:每个像素有红、绿、蓝和透明度。终端不能直接懂这些像素,所以这个文件把它翻译成 Sixel(一种老牌终端图片指令格式)。它先检查宽高和数据长度,避免把坏图片写成乱码;再把颜色压缩成 RGB332,也就是红绿各 3 位、蓝色 2 位,最多 256 种颜色;透明像素直接跳过,不画。Sixel 一次按 6 行一组来写,所以代码会一组一组扫图,找出这一组里用到的颜色,再按颜色分别写像素。连续重复的图案还会用短写法压缩,像把“AAAA”写成“4个A”,让输出更短。

函数细节21
encode_rgba18–41 ↗
fn encode_rgba(rgba: &[u8], width: u32, height: u32) -> Result<Vec<u8>>

作用:这是外部最主要会调用的入口:给它一张 RGBA 小图,它返回一段可以发给终端的 Sixel 数据。它负责先把输入检查干净,避免尺寸不对或数据缺失导致终端显示乱掉。

数据流:进去的是 RGBA 字节数组、图片宽度和高度。它先确认宽高不是 0,再算出理论上应该有多少字节,并和实际长度对比;然后建立调色板,写入 Sixel 的开头、尺寸、颜色定义和像素内容;最后补上结束标记。出来的是一段 Vec<u8> 字节串,失败时返回带说明的错误。

调用关系:它是这个文件的总开关。宠物渲染流程里的 sixel_frame 会调用它,测试也围绕它验证各种情况。它自己不逐像素写细节,而是把颜色收集交给 Palette::from_rgba,把像素编码交给 write_pixels。

调用图:调用 3 个内部函数(from_rgba, pixel_count, write_pixels);被 6 处调用(sixel_frame, encodes_red_pixel_with_palette_and_pixel_data, multi_band_images_advance_to_next_sixel_band, rejects_mismatched_rgba_buffer_length, repeated_cells_use_sixel_run_length_encoding, transparent_pixels_do_not_emit_palette_or_pixel_data);外部调用 3 个(new, bail!, format!)。

write_pixels43–78 ↗
fn write_pixels(
    output: &mut Vec<u8>,
    rgba: &[u8],
    width: u32,
    height: u32,
    palette: &Palette,
) -> Result<()>

作用:这个函数真正把图片的像素内容写成 Sixel 的主体。它按 Sixel 的规则,每 6 行作为一条横带来处理。

数据流:进去的是输出缓冲区、RGBA 数据、宽高和调色板。它一条 6 行横带一条横带地扫描,先找这一带出现了哪些颜色,再对每种颜色逐列生成 Sixel 字符,并把连续重复的字符压缩写入。它会直接往 output 里追加像素指令,完成后不单独返回新数据,只返回成功或错误。

调用关系:encode_rgba 在写完头部和调色板后把活交给它。它会调用 active_colors_for_band 找颜色,调用 sixel_data_for_column 算每列的 6 个点,再用 push_run 和 flush_run 做重复压缩。

调用图:调用 4 个内部函数(active_colors_for_band, flush_run, push_run, sixel_data_for_column);被 1 处调用(encode_rgba);外部调用 1 个(format!)。

active_colors_for_band80–100 ↗
fn active_colors_for_band(
    rgba: &[u8],
    width: u32,
    height: u32,
    band_top: u32,
    palette: &Palette,
) -> Result<Vec<u8>>

作用:这个函数找出某个 6 行横带里实际出现过的颜色。这样后面只为用到的颜色写数据,不浪费输出。

数据流:进去的是整张 RGBA 图、宽高、当前横带的起始行,以及调色板。它扫描这 6 行范围内的每个像素,透明像素忽略,不透明像素转成颜色编号并做标记;最后按调色板顺序吐出这一带真正用到的颜色编号列表。

调用关系:write_pixels 每处理一条横带都会先叫它来点名:这一带有哪些颜色需要画。它内部通过 color_index_at 读取单个像素的颜色,并通过 Palette::indices 保持颜色输出顺序稳定。

调用图:调用 2 个内部函数(indices, color_index_at);被 1 处调用(write_pixels);外部调用 1 个(from)。

sixel_data_for_column102–123 ↗
fn sixel_data_for_column(
    rgba: &[u8],
    width: u32,
    height: u32,
    band_top: u32,
    x: u32,
    color_index: u8,
) -> Result<u8>

作用:这个函数把某一列、某一种颜色,在最多 6 个竖向像素里的出现情况,变成一个 Sixel 字符。可以把它想成把 6 个小灯泡的亮灭状态压成一个字符。

数据流:进去的是 RGBA 数据、宽高、横带起始行、列号 x,以及目标颜色编号。它从这列的 6 个位置往下看,哪个位置是这个颜色,就在对应的二进制位上打勾;最后把这个位图加上 Sixel 的基础字符偏移,输出一个字节。

调用关系:write_pixels 在逐列写某个颜色时反复调用它。它自己不关心整条横带有哪些颜色,只负责回答“这一列对这个颜色该写哪个字符”,读取像素颜色时交给 color_index_at。

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

color_index_at125–137 ↗
fn color_index_at(rgba: &[u8], width: u32, x: u32, y: u32) -> Result<Option<u8>>

作用:这个函数读取某个像素,并判断它应该算作哪种 Sixel 调色板颜色。透明像素会被当成“不存在”,后续就不会画出来。

数据流:进去的是 RGBA 数据、图片宽度和像素坐标 x、y。它先算出这个像素在字节数组里的位置,读取透明度;透明度低于阈值就返回 None,表示不画;否则把红绿蓝压缩成 RGB332 颜色编号并返回。

调用关系:它是扫描像素时的统一入口。active_colors_for_band 用它找哪些颜色出现过,sixel_data_for_column 用它判断某个位置是不是目标颜色。它把位置计算交给 pixel_offset,把颜色压缩交给 rgb332_index。

调用图:调用 2 个内部函数(pixel_offset, rgb332_index);被 2 处调用(active_colors_for_band, sixel_data_for_column)。

push_run139–150 ↗
fn push_run(run_char: &mut Option<u8>, run_len: &mut usize, output: &mut Vec<u8>, byte: u8)

作用:这个函数用来记录一串连续相同的 Sixel 字符,为后面的压缩做准备。它避免每看到一个字符就立刻写出去。

数据流:进去的是当前正在累计的字符、累计长度、输出缓冲区,以及新来的字节。若新字节和当前累计字符一样,就只把长度加一;若不一样,就先把旧的一串写出去,再开始累计新的字符。它会修改 run_char、run_len 和 output。

调用关系:write_pixels 每算出一列的 Sixel 字符后都会交给它。它遇到字符变化时会调用 flush_run,把上一段连续字符真正写进输出。

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

flush_run152–164 ↗
fn flush_run(run_char: &mut Option<u8>, run_len: &mut usize, output: &mut Vec<u8>)

作用:这个函数把已经累计好的连续字符写到输出里。如果连续次数够多,它会使用 Sixel 的重复写法,让结果更短。

数据流:进去的是当前累计字符、累计长度和输出缓冲区。如果没有累计字符,它什么也不做;如果长度大于 3,就写成类似“重复 N 次某字符”的形式;否则就老老实实重复写几个字符。最后清空累计状态。

调用关系:push_run 在切换字符时会叫它收尾,write_pixels 在一行颜色数据结束时也会叫它把最后一段写完。它是压缩输出的最后一步。

调用图:被 2 处调用(push_run, write_pixels);外部调用 2 个(format!, repeat_n)。

pixel_offset166–175 ↗
fn pixel_offset(width: u32, x: u32, y: u32) -> Result<usize>

作用:这个函数把二维坐标转换成 RGBA 字节数组里的位置。没有它,代码就不知道第 x 列第 y 行对应数组中的哪 4 个字节。

数据流:进去的是图片宽度和坐标 x、y。它先算像素序号,再乘以 4 得到字节位置;每一步都检查数字是否溢出,最后转成当前机器能用的 usize。出来的是字节下标,失败时返回错误。

调用关系:color_index_at 每次读取像素前都会调用它。它是安全保险丝,防止超大尺寸图片让乘法溢出后读错内存位置。

调用图:被 1 处调用(color_index_at);外部调用 2 个(from, try_from)。

pixel_count177–182 ↗
fn pixel_count(width: u32, height: u32) -> Result<usize>

作用:这个函数计算整张图应该有多少个像素。它主要用来确认传进来的 RGBA 数据长度是不是正确。

数据流:进去的是宽度和高度。它把两者相乘得到像素总数,并检查乘法和类型转换是否安全;出来的是像素数量,或一个说明溢出的错误。

调用关系:encode_rgba 一开始就调用它,然后再乘以 4 算出应该有多少 RGBA 字节。它把错误尽早拦住,避免后续编码时才发现数据对不上。

调用图:被 1 处调用(encode_rgba);外部调用 2 个(from, try_from)。

rgb332_index184–189 ↗
fn rgb332_index(red: u8, green: u8, blue: u8) -> u8

作用:这个函数把 24 位颜色压成 8 位颜色编号。简单说,就是把很多相近颜色归到同一个小格子里,换来固定的 256 色调色板。

数据流:进去的是红、绿、蓝三个 0 到 255 的值。它取红色最高 3 位、绿色最高 3 位、蓝色最高 2 位,再拼成一个 0 到 255 的编号。出来的是这个压缩后的颜色编号。

调用关系:Palette::from_rgba 用它统计整张图用了哪些颜色,color_index_at 用它给单个像素编号。它保证同一颜色在收集调色板和写像素时得到同一个编号。

调用图:被 2 处调用(from_rgba, color_index_at)。

rgb332_color191–200 ↗
fn rgb332_color(index: u8) -> (u8, u8, u8)

作用:这个函数把 RGB332 颜色编号还原成大致的红绿蓝值,用来写 Sixel 调色板定义。它不是恢复原图精确颜色,而是给终端一个代表色。

数据流:进去的是 0 到 255 的颜色编号。它拆出红、绿、蓝三个小桶编号,再把这些桶放大回 0 到 255 的普通颜色范围;出来的是一组三个字节的红绿蓝值。

调用关系:Palette::write_definitions 在告诉终端“某个编号代表什么颜色”时会用它。它内部把每个颜色通道的放大计算交给 scale_bucket_to_byte。

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

scale_bucket_to_byte202–205 ↗
fn scale_bucket_to_byte(bucket: u8, max: u8) -> u8

作用:这个函数把压缩后的小档位换算回 0 到 255 的颜色强度。比如 7 档里的最高档会变回 255。

数据流:进去的是当前档位 bucket 和最大档位 max。它按比例算出对应的 0 到 255 数值,再安全转回 u8;出来的是一个普通颜色字节。

调用关系:rgb332_color 用它分别处理红、绿、蓝三个通道。它是颜色还原时的小计算器。

调用图:被 1 处调用(rgb332_color);外部调用 2 个(from, try_from)。

byte_to_sixel_percent207–210 ↗
fn byte_to_sixel_percent(value: u8) -> u8

作用:这个函数把普通颜色值从 0 到 255 换成 Sixel 需要的 0 到 100 百分比。因为 Sixel 调色板里颜色强度用百分比表达。

数据流:进去的是一个颜色字节。它按比例换算成 0 到 100,并安全转成 u8;出来的是 Sixel 可写入的百分比数值。

调用关系:写调色板颜色定义时需要这种换算。它让内部常见的 RGB 字节值能变成终端协议看得懂的格式。

调用图:外部调用 2 个(from, try_from)。

Palette::from_rgba217–228 ↗
fn from_rgba(rgba: &[u8]) -> Self

作用:这个函数从一张 RGBA 图里生成调色板,记录哪些压缩颜色真的出现过。透明像素不会进入调色板。

数据流:进去的是整张 RGBA 字节数组。它按每 4 个字节一个像素来扫,跳过透明度低的像素;对不透明像素计算 RGB332 颜色编号,并在 used 表里打勾。出来的是一个 Palette。

调用关系:encode_rgba 在正式写 Sixel 前先调用它。后面的 Palette::write_definitions 和 active_colors_for_band 都依赖这个调色板,确保只写实际用到的颜色。

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

Palette::indices230–232 ↗
fn indices(&self) -> impl Iterator<Item = u8> + '_

作用:这个函数按固定顺序列出调色板里已经使用的颜色编号。固定顺序很重要,这样同一张图每次编码结果都稳定。

数据流:进去的是 Palette 自己保存的 used 布尔表。它从 0 到 255 检查哪些位置被标记为 true,并把这些颜色编号作为迭代结果一个个交出去。

调用关系:Palette::write_definitions 用它写所有颜色定义,active_colors_for_band 用它按调色板顺序筛出某条横带的颜色。它像调色板的点名册。

调用图:被 2 处调用(write_definitions, active_colors_for_band)。

Palette::write_definitions234–247 ↗
fn write_definitions(&self, output: &mut Vec<u8>)

作用:这个函数把调色板写成 Sixel 能理解的颜色定义。没有这些定义,后面的像素编号就不知道对应什么颜色。

数据流:进去的是 Palette 和输出缓冲区。它遍历每个已使用的颜色编号,把编号还原成红绿蓝,再转成 Sixel 需要的百分比格式,最后把类似“这个编号是这种颜色”的指令追加到 output。

调用关系:encode_rgba 在写像素前调用它。它使用 Palette::indices 知道要写哪些颜色,用 rgb332_color 得到每个编号的代表色。

调用图:调用 2 个内部函数(indices, rgb332_color);外部调用 1 个(format!)。

tests::encodes_red_pixel_with_palette_and_pixel_data257–265 ↗
fn encodes_red_pixel_with_palette_and_pixel_data()

作用:这个测试确认一个不透明红色像素会被正确编码:既有红色调色板定义,也有像素数据。

数据流:进去的是手写的 1×1 红色 RGBA 数据。测试调用 encode_rgba,把返回字节转成字符串,然后和预期的 Sixel 字符串逐字比较。结果是测试通过或失败。

调用关系:它直接验证 encode_rgba 的最基本成功路径。这个测试能防止以后改代码时把颜色编号、调色板或像素字符写错。

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

tests::transparent_pixels_do_not_emit_palette_or_pixel_data268–276 ↗
fn transparent_pixels_do_not_emit_palette_or_pixel_data()

作用:这个测试确认透明像素不会被画出来,也不会占用调色板颜色。这样宠物周围透明背景才不会变成色块。

数据流:进去的是一个透明的 1×1 红色像素。测试调用 encode_rgba,把结果转成字符串,并检查输出里只有 Sixel 头、尺寸和结束符,没有颜色定义和像素内容。

调用关系:它验证 encode_rgba 通过 Palette::from_rgba、color_index_at 等路径正确跳过透明像素。这个行为对宠物贴图边缘是否干净很重要。

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

tests::multi_band_images_advance_to_next_sixel_band279–294 ↗
fn multi_band_images_advance_to_next_sixel_band()

作用:这个测试确认高度超过 6 行时,编码器会正确换到下一条 Sixel 横带。Sixel 的核心规则就是每 6 行一组。

数据流:进去的是 1 列 7 行的红色图片数据。测试调用 encode_rgba,把结果转成字符串,检查第 6 行之后是否包含换带用的 Sixel 控制字符,并且第 7 行也被画出来。

调用关系:它重点保护 write_pixels 的分带逻辑。若换带符写错,多行宠物图就可能上下错位或丢行。

调用图:调用 1 个内部函数(encode_rgba);外部调用 3 个(from_utf8, new, assert_eq!)。

tests::repeated_cells_use_sixel_run_length_encoding297–307 ↗
fn repeated_cells_use_sixel_run_length_encoding()

作用:这个测试确认连续重复的 Sixel 单元会使用压缩写法。这样输出更短,写到终端也更省。

数据流:进去的是 4 个连续红色像素组成的 4×1 图片。测试调用 encode_rgba,把结果转成字符串,然后检查里面包含表示“重复 4 次”的片段。

调用关系:它验证 push_run 和 flush_run 这套重复压缩机制确实被 write_pixels 用上了。若压缩坏掉,图片可能还能显示,但输出会变长。

调用图:调用 1 个内部函数(encode_rgba);外部调用 3 个(from_utf8, new, assert!)。

tests::rejects_mismatched_rgba_buffer_length310–314 ↗
fn rejects_mismatched_rgba_buffer_length()

作用:这个测试确认输入数据长度不对时,编码器会明确报错,而不是继续乱读。它保护的是安全性和错误提示质量。

数据流:进去的是只有 3 个字节的数据,但 1×1 RGBA 图片应该有 4 个字节。测试调用 encode_rgba 并期待它失败,再检查错误文字是否准确说明实际长度和期望长度。

调用关系:它验证 encode_rgba 开头的输入检查,也间接保护 pixel_count 的使用场景。没有这个检查,坏数据可能在后面造成难懂的错误。

调用图:调用 1 个内部函数(encode_rgba);外部调用 1 个(assert_eq!)。

图像摄取和细节策略

这些文件定义图像处理错误,摄取并规范化提示图像,并应用共享策略辅助工具来选择图像细节和进行预处理。

utils/image/src/error.rs源码 ↗
data_modelcross-cutting

图片处理很容易失败:文件可能读不到,图片格式可能不支持,图片内容可能损坏,或者图片太大。这个文件把这些情况统一成一个错误类型 ImageProcessingError。你可以把它想成一张“故障分类表”:不同故障放进不同格子里,还保留原始错误,方便后面排查。它还提供两个小工具:decode_error 会根据底层图片库给出的错误,判断这是“图片内容解不开”,还是“格式根本不支持”;is_invalid_image 则用来快速判断一个错误是不是“图片本身无效”。这样上层代码就不用到处猜错误含义,可以按统一规则给用户提示、记录日志,或者决定下一步怎么处理。

函数细节2
ImageProcessingError::decode_error39–52 ↗
fn decode_error(path: &std::path::Path, source: image::ImageError) -> Self

作用:这个函数把图片库返回的解码错误,翻译成本项目自己的错误说法。有人会在尝试打开或解析图片失败后调用它,用来判断是“图片坏了”,还是“这种图片格式不支持”。

数据流:进去的是一个图片路径和一个底层图片库给出的错误。它先看这个错误是不是明确的“解码失败”;如果是,就把路径复制成可保存的 PathBuf,并返回 Decode 错误。否则,它会根据文件路径猜测图片的 MIME 类型(可以理解成“文件格式标签”,比如 image/png),猜不到就写 unknown,最后返回 UnsupportedImageFormat。出来的是一个更适合项目内部使用的 ImageProcessingError。

调用关系:它通常处在图片读取或解码失败之后,负责把外部图片库的错误改写成项目自己的错误分类。它内部会用 matches! 判断错误种类,用 to_path_buf 保存路径,再用 mime_guess::from_path 根据文件名推测格式;这样后面的调用者拿到的不是一团底层错误,而是可以直接展示或分支处理的明确结果。

调用图:外部调用 3 个(to_path_buf, matches!, from_path)。

ImageProcessingError::is_invalid_image54–62 ↗
fn is_invalid_image(&self) -> bool

作用:这个函数用来回答一个简单问题:这个错误是不是因为图片内容本身无效、解码不了。上层代码可以用它来决定是否给用户显示“图片损坏或不是有效图片”之类的提示。

数据流:进去的是一个已经生成的 ImageProcessingError。它只检查这个错误是不是 Decode,并且里面的底层错误确实是图片库报告的 Decoding 错误。符合就出来 true,不符合就出来 false;它不会改动任何数据。

调用关系:它是在错误已经发生、并被包装成 ImageProcessingError 之后使用的小判断器。它通过 matches! 做模式检查,不再调用其他复杂流程;调用者可以用它把“无效图片”和“读文件失败、格式不支持、图片太大”等其他问题区分开。

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

utils/image/src/lib.rs源码 ↗
domain_logic处理用户提示图片时

这份代码解决的是“提示里带图片”这件事的前处理问题。用户可能给一张本地图片的字节,也可能给一个 data URL(把图片内容直接塞进字符串里的格式)。文件会先检查图片是不是太大,再解码确认它真的是图片。之后按模式决定保留原图,还是缩小到合适尺寸,避免图片太大导致上传慢、占内存多,或者超过模型能看的“图片块”预算。它还尽量保留 ICC 色彩配置和 EXIF 信息(比如照片方向),这样图片不会莫名偏色或横竖颠倒。处理后的图片会带上 MIME 类型(例如 image/png,告诉别人这是什么格式)、宽高和字节内容。为了省时间,它用一个小缓存记住最近处理过的图片;同一张图、同一种处理模式,下次可以直接拿结果。

函数细节11
EncodedImage::into_data_url47–49 ↗
fn into_data_url(self) -> String

作用:把已经处理好的图片变成 data URL 字符串。data URL 可以理解成“把图片文件直接写进一段文本里”,方便放进请求或消息中传递。

数据流:进去的是一个 EncodedImage,里面有图片字节和 MIME 类型 → 它把 MIME 类型和图片字节交给 data_url_from_bytes → 出来是一段以 data: 开头、包含 base64 图片内容的字符串;原来的 EncodedImage 会被消耗掉。

调用关系:这是 EncodedImage 这个结果对象上的便捷出口。外部代码拿到处理好的图片后,如果需要文本形式,就调用它;它自己不做编码细节,而是把活交给 data_url_from_bytes。

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

data_url_from_bytes53–56 ↗
fn data_url_from_bytes(mime: &str, bytes: &[u8]) -> String

作用:把一段原始图片字节包成 data URL,但不检查这些字节是不是真的图片。它适合在已经信任或已经处理过图片内容时使用。

数据流:进去的是 MIME 类型和一串字节 → 它先用 base64(一种把二进制内容变成普通文本的编码方式)把字节转成文本 → 出来是形如 data:image/png;base64,... 的字符串。

调用关系:它是最底层的小工具,被 EncodedImage::into_data_url 调用。上层负责保证图片内容合理,它只负责拼出标准格式字符串。

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

load_for_prompt_bytes87–193 ↗
fn load_for_prompt_bytes(
    path: &Path,
    file_bytes: Vec<u8>,
    mode: PromptImageMode,
) -> Result<EncodedImage, ImageProcessingError>

作用:这是处理图片字节的主入口:识别格式、读取尺寸、按需要缩小或重新编码,最后返回适合放进提示里的图片。没有它,系统可能把过大的、格式不合适的、方向或颜色信息丢失的图片直接送出去。

数据流:进去的是图片路径、图片原始字节和处理模式 → 它先用图片内容算一个 SHA-1 摘要(像给文件内容取指纹),用来查缓存;没命中缓存时,它猜图片格式、解码图片、读取 ICC 色彩配置和 EXIF 照片信息,再按模式决定是否缩小;如果原格式能安全保留,就尽量保留原字节,否则重新编码成 PNG、JPEG 或 WebP → 出来是 EncodedImage,里面有新字节、MIME 类型和宽高;同时可能把结果写进缓存。

调用关系:load_data_url_for_prompt 把 data URL 解成字节后会调用它。它是本文件的核心流水线:前面查缓存,中间做图片处理,后面通过 cache_image 保存结果;在需要重新编码时会用 encode_image,在计算受限尺寸时会用尺寸限制相关函数。

调用图:调用 1 个内部函数(cache_image);被 1 处调用(load_data_url_for_prompt);外部调用 2 个(to_path_buf, sha1_digest)。

cache_image195–213 ↗
fn cache_image(cache: &ImageCache, key: ImageCacheKey, image: EncodedImage, byte_capacity: usize)

作用:把处理好的图片放进内存缓存里,并控制缓存总大小。它像一个小抽屉:常用图片放进去,但抽屉满了就丢掉最久没用的。

数据流:进去的是缓存对象、缓存键、图片结果和最多允许占用的字节数 → 如果单张图片已经超过容量,就直接不缓存;否则把图片放进去,统计缓存里所有图片的字节数,超过上限时不断移除最久没用的图片 → 出来没有返回值,但缓存内容会被更新。

调用关系:它由 load_for_prompt_bytes 在成功处理图片后调用。load_for_prompt_bytes 负责产出图片,它负责让这些结果可复用,同时避免缓存无限变大。

调用图:被 1 处调用(load_for_prompt_bytes);外部调用 1 个(with_mut)。

load_data_url_for_prompt215–262 ↗
fn load_data_url_for_prompt(
    image_url: &str,
    mode: PromptImageMode,
) -> Result<EncodedImage, ImageProcessingError>

作用:处理用户直接传来的 data URL 图片。它负责把字符串拆开、检查格式、解出图片字节,再交给真正的图片处理流程。

数据流:进去的是一段 data URL 字符串和处理模式 → 它检查是否以 data: 开头,是否有逗号分隔元信息和内容,是否声明 base64;再检查 base64 文本和解码后的字节是否超过大小上限;然后把 base64 解成原始图片字节 → 出来是 load_for_prompt_bytes 处理后的 EncodedImage,或者返回清楚的错误原因。

调用关系:这是 data URL 入口。它自己只负责把“文本里的图片”还原成字节并做安全检查,随后调用 load_for_prompt_bytes,让统一的图片处理流水线继续完成识别、缩放、编码和缓存。

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

prompt_image_output_dimensions_for_limits264–299 ↗
fn prompt_image_output_dimensions_for_limits(
    width: u32,
    height: u32,
    limits: PromptImageResizeLimits,
) -> (u32, u32)

作用:根据最大边长和最大“图片块”数量,算出图片应该缩到多大。这里的图片块可以理解成模型看图时用的小格子,格子太多就太贵或超限。

数据流:进去的是原始宽、高和限制条件 → 它先保证宽高至少是 1;如果已经符合限制,就原样返回;否则先按最大边长缩一轮,再检查是否合格;如果格子数量仍然太多,就按面积继续缩,并向下取整,确保最终不会超过预算 → 出来是一组新的宽高。

调用关系:它在需要按 PromptImageMode::ResizeWithLimits 处理图片时被 load_for_prompt_bytes 使用。它会反复借助 prompt_image_dimensions_fit 判断当前尺寸是否已经安全。

调用图:调用 1 个内部函数(prompt_image_dimensions_fit);外部调用 1 个(from)。

prompt_image_dimensions_fit301–308 ↗
fn prompt_image_dimensions_fit(width: u32, height: u32, limits: PromptImageResizeLimits) -> bool

作用:判断某个宽高是否满足提示图片的限制。它同时看两个条件:边长不能太大,按 32 像素一格切出来的格子数量也不能太多。

数据流:进去的是宽、高和限制条件 → 它把宽高按 PROMPT_IMAGE_PATCH_SIZE 计算成横向多少格、纵向多少格,再乘出总格数 → 出来是 true 或 false,表示这张图在当前尺寸下能不能直接用。

调用关系:它是 prompt_image_output_dimensions_for_limits 的尺子。后者负责一步步缩图,它负责每一步判断“现在够不够安全”。

调用图:被 1 处调用(prompt_image_output_dimensions_for_limits);外部调用 1 个(from)。

can_preserve_source_bytes310–317 ↗
fn can_preserve_source_bytes(format: ImageFormat) -> bool

作用:判断某种图片格式能不能原封不动地保留原始字节。这样可以避免不必要的重新编码,减少画质变化和处理时间。

数据流:进去的是图片格式 → 它只认可 PNG、JPEG 和 WebP 这几种可以安全直传的格式 → 出来是布尔值,true 表示可以保留原文件字节,false 表示应该重新编码成更稳妥的格式。

调用关系:load_for_prompt_bytes 在决定“直接用原图字节”还是“重新编码”时会依赖这个判断。它把格式安全规则集中在一个地方,避免主流程里到处写判断。

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

encode_image319–380 ↗
fn encode_image(
    image: &DynamicImage,
    preferred_format: ImageFormat,
    metadata: ImageMetadata,
) -> Result<(Vec<u8>, ImageFormat), ImageProcessingError>

作用:把已经解码出来的图片重新写成 PNG、JPEG 或 WebP 字节。它用于缩图后,或者原始格式不适合直接保留时。

数据流:进去的是内存里的图片、偏好的输出格式,以及要保留的元信息 → 它先把目标格式限定在 JPEG、WebP 或 PNG;PNG 和 WebP 会转成 RGBA 像素再写出,JPEG 用质量 85 写出;写之前会调用 apply_image_metadata 尝试附上 ICC 色彩配置和 EXIF 信息 → 出来是一段新的图片字节,以及实际使用的图片格式;如果编码失败,会返回图片处理错误。

调用关系:它是 load_for_prompt_bytes 的“重新打包机器”。主流程决定什么时候需要重打包;encode_image 负责具体写成哪种图片文件,并把设置元信息的步骤交给 apply_image_metadata。

调用图:调用 1 个内部函数(apply_image_metadata);外部调用 8 个(height, to_rgba8, width, new_with_quality, new, new, new_lossless, unreachable!)。

apply_image_metadata382–405 ↗
fn apply_image_metadata(
    encoder: &mut impl ImageEncoder,
    icc_profile: Option<Vec<u8>>,
    exif: Option<Vec<u8>>,
    format: ImageFormat,
) -> Result<(), ImageProcessingError>

作用:把颜色配置和照片元信息写进新的图片文件里。这样重新编码后,图片更可能保持原来的颜色和方向。

数据流:进去的是图片编码器、可选的 ICC 色彩配置、可选的 EXIF 信息和目标格式 → 如果有 ICC,就调用编码器写入;如果有 EXIF,也写入;任一步不支持或失败,就包装成统一的编码错误 → 出来是成功标记,或一个说明失败原因的错误。

调用关系:它只被 encode_image 使用。encode_image 负责写图片主体,它负责在写之前把关键“说明书”贴回图片上。

调用图:被 1 处调用(encode_image);外部调用 2 个(set_exif_metadata, set_icc_profile)。

format_to_mime407–414 ↗
fn format_to_mime(format: ImageFormat) -> String

作用:把程序内部的图片格式名称转换成网络和 data URL 常用的 MIME 类型。MIME 类型就是告诉接收方“这串字节是什么文件格式”的标签。

数据流:进去的是 ImageFormat,比如 JPEG、GIF、WebP 或其他 → 它映射成 image/jpeg、image/gif、image/webp;没有特别列出的格式默认当作 image/png → 出来是一段 MIME 类型字符串。

调用关系:load_for_prompt_bytes 在生成 EncodedImage 时需要这个标签。后续 EncodedImage::into_data_url 也会用这个 MIME 字符串拼出正确的 data URL。

tools/src/image_detail.rs源码 ↗
domain_logicrequest handling

有些模型支持读取图片的“原始细节”,有些不支持。如果代码不管模型能力,硬把“Original(原始细节)”塞进请求或工具输出里,就可能让后面的模型调用失败。这个文件做的事很直接:先看模型资料里有没有写“支持原始图片细节”;然后在整理图片细节参数时,只在支持的情况下保留 Original;如果不支持,就把它当成没有指定,或者在一批输出内容里把 Original 换成默认细节。可以把它理解成点菜前看厨房能不能做这道菜:能做就下单,不能做就换成默认菜式,别让服务员把做不了的单子送进厨房。

函数细节3
can_request_original_image_detail6–8 ↗
fn can_request_original_image_detail(model_info: &ModelInfo) -> bool

作用:这个函数回答一个很简单的问题:当前模型能不能请求图片的原始细节。有人在决定是否允许使用 Original 之前,会先问它一声。

数据流:进去的是一份模型信息 ModelInfo,里面有一个标记说明是否支持原始图片细节;函数只读取这个标记,不改任何东西;出来的是 true 或 false,表示能不能请求 Original。

调用关系:它是判断能力的小关卡。normalize_output_image_detail 在整理图片细节设置时会调用它,用它的答案决定是否保留 Original。

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

normalize_output_image_detail10–21 ↗
fn normalize_output_image_detail(
    model_info: &ModelInfo,
    detail: Option<ImageDetail>,
) -> Option<ImageDetail>

作用:这个函数把用户或上游给的图片细节要求整理成“适合当前模型”的版本。它主要防止把模型不支持的 Original 继续传下去。

数据流:进去的是模型信息和一个可有可无的图片细节选项;如果要求是 Original,它会先问 can_request_original_image_detail 当前模型支不支持;支持就原样返回 Original,不支持就返回 None。没指定时也返回 None。Auto、Low、High 这些普通选项会原样返回;函数不修改输入对象本身。

调用关系:它处在准备图片输出参数的整理阶段。它把具体的能力判断交给 can_request_original_image_detail,自己负责把不同输入情况变成后续代码更安全、更统一的结果。

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

sanitize_original_image_detail23–38 ↗
fn sanitize_original_image_detail(
    can_request_original_image_detail: bool,
    items: &mut [FunctionCallOutputContentItem],
)

作用:这个函数会检查一批函数调用输出里的图片项,把不该出现的 Original 图片细节替换成默认细节。它适合在内容真正发出去之前做最后清理。

数据流:进去的是一个布尔值,表示当前是否允许请求 Original,以及一组可修改的输出内容项;如果允许,它什么都不做直接返回。如果不允许,它逐个查看内容项,只处理图片项,并把 detail 为 Original 的地方改成 DEFAULT_IMAGE_DETAIL;出来没有单独返回值,但传入的内容列表可能已经被改过。

调用关系:它像发送前的清洁工,专门扫掉不被允许的 Original。内部用 Rust 的 matches! 宏做模式判断,也就是检查某个值是不是符合指定形状;除此之外不把工作再交给别的本项目函数。

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

core/src/original_image_detail.rs源码 ↗
orchestrationcross-cutting

这个文件本身不实现新的算法,也不保存数据。它只做一件很实际的事:把 codex_tools 里的两个功能重新导出到当前模块里。重新导出可以理解成“把仓库里的工具摆到前台货架上”,方便项目内部其他地方直接使用。这里的两个工具都和“原始图片细节”有关:一个用来判断当前请求能不能要求查看原始图片的细节,另一个用来清理或规范这类请求内容,避免传入不合适或不安全的信息。没有这个文件,其他代码可能要到更深的工具模块里找这些函数,依赖关系会更散,也更难统一管理。

core/src/image_preparation.rs源码 ↗
domain_logicrequest handling / history preparation

用户消息或工具输出里可能带图片,而且图片常常是 data URL(一种把图片内容直接塞进文本里的地址)。模型不能无限制地吃图片:尺寸太大不行,某些清晰度选项也不支持。这个文件就像进门安检,先扫描所有回复项,找到其中的图片;如果是内嵌图片,就按“高清”或“原图”的规则缩放、重新编码;如果图片处理失败,就记录一条警告,并把图片替换成一段友好的占位文字。这样后面的对话历史重建、发送给模型等流程,不会被坏图片拖垮。特别要注意:detail 为 low 的图片在这里直接不支持,会被替换成提示用户改用 high、original 或 auto 的文字。

函数细节6
ImagePreparationError::placeholder34–42 ↗
fn placeholder(&self) -> &'static str

作用:把具体的图片处理错误,翻译成可以放进对话里的简短说明文字。这样用户或模型看到的不是技术错误,而是“图片太大”或“图片无法处理”这类能理解的话。

数据流:进去的是一个图片准备错误 → 它判断错误属于不支持 low 清晰度、图片太大,还是其他处理失败 → 出来的是一段固定的占位文本,不修改外部数据。

调用关系:prepare_message_content 和 prepare_tool_output_content 在图片处理失败后会用它拿到替换文字,再把原来的图片项改成文本项。它是错误处理的最后一步,把内部错误变成安全可继续传递的内容。

prepare_response_items45–70 ↗
fn prepare_response_items(items: &mut [ResponseItem])

作用:遍历一批对话回复项,把里面可能夹带的图片都提前处理好。调用它的人不用关心图片藏在普通消息里还是工具输出里。

数据流:进去的是一组可修改的 ResponseItem → 它逐个查看类型:普通消息就处理消息内容,函数或自定义工具输出就处理工具输出内容,其他不含这类图片的项跳过 → 出来时原数组被就地改好,能处理的图片已被缩放重写,不能处理的图片已被替换成说明文字。

调用关系:它是这个文件对外的主入口,会被 apply_rollout_reconstruction 和 prepare_conversation_items_for_history 使用。它自己不直接改图片,而是把普通消息交给 prepare_message_content,把工具输出交给 prepare_tool_output_content。

调用图:调用 2 个内部函数(prepare_message_content, prepare_tool_output_content);被 2 处调用(apply_rollout_reconstruction, prepare_conversation_items_for_history)。

prepare_message_content72–84 ↗
fn prepare_message_content(items: &mut [ContentItem])

作用:专门处理普通消息内容里的输入图片。它只碰 data URL 图片,因为这种图片内容就在文本里,可以在本地检查和改写。

数据流:进去的是一组可修改的消息内容项 → 它找到 InputImage,先用 is_data_url 确认是不是内嵌图片,再调用 prepare_image 缩放和重写图片数据 → 如果成功,图片地址被替换成处理后的新 data URL;如果失败,它记录警告,并把这一项改成一段文本占位说明。

调用关系:它由 prepare_response_items 在遇到 ResponseItem::Message 时调用。真正的图片处理交给 prepare_image;判断是不是 data URL 交给 is_data_url;出错时通过 warn! 记录日志,并用 ImagePreparationError::placeholder 生成替代文字。

调用图:调用 2 个内部函数(is_data_url, prepare_image);被 1 处调用(prepare_response_items);外部调用 1 个(warn!)。

prepare_tool_output_content86–98 ↗
fn prepare_tool_output_content(items: &mut [FunctionCallOutputContentItem])

作用:专门处理工具返回内容里的输入图片。工具也可能把截图或图片作为结果塞回对话里,这个函数负责让这些图片在进入后续流程前变得安全可用。

数据流:进去的是一组可修改的工具输出内容项 → 它找到其中的 InputImage,确认是 data URL 后调用 prepare_image → 成功时改写图片地址;失败时记录警告,并把图片项替换成文本,说明图片为什么被省略。

调用关系:它由 prepare_response_items 在遇到函数调用输出或自定义工具输出时调用。它和 prepare_message_content 做的事很像,只是处理的数据类型不同,都是把具体图片交给 prepare_image。

调用图:调用 2 个内部函数(is_data_url, prepare_image);被 1 处调用(prepare_response_items);外部调用 1 个(warn!)。

is_data_url100–104 ↗
fn is_data_url(image_url: &str) -> bool

作用:判断一个图片地址是不是 data URL。data URL 可以理解成“把图片文件内容直接写在地址里”,只有这种图片这个文件才会尝试本地处理。

数据流:进去的是一段图片地址字符串 → 它只看开头是否是 data:,并且大小写不敏感 → 出来是 true 或 false,不改动任何东西。

调用关系:prepare_message_content 和 prepare_tool_output_content 在动手处理图片前都会先问它一句。这样普通网络图片地址不会被误当成本地内嵌图片去解析。

调用图:被 2 处调用(prepare_message_content, prepare_tool_output_content)。

prepare_image106–118 ↗
fn prepare_image(
    image_url: &mut String,
    detail: Option<ImageDetail>,
) -> Result<(), ImagePreparationError>

作用:真正执行单张图片的准备工作:根据清晰度要求选择大小限制,然后加载、缩放并重新写回图片。它是这个文件里最核心的图片加工步骤。

数据流:进去的是可修改的图片 data URL 和可选的 ImageDetail 清晰度设置 → 它把 auto、high 或未指定按高清限制处理,把 original 按更宽松的原图限制处理,把 low 直接报错 → 成功时调用 load_data_url_for_prompt 读取并按限制缩放图片,再把原字符串替换成新的 data URL;失败时返回 ImagePreparationError。

调用关系:prepare_message_content 和 prepare_tool_output_content 找到需要处理的图片后都会调用它。它把底层图片工具 load_data_url_for_prompt 包起来,并把“不同清晰度该用什么尺寸上限”这条规则集中放在一个地方。

调用图:被 2 处调用(prepare_message_content, prepare_tool_output_content);外部调用 2 个(load_data_url_for_prompt, ResizeWithLimits)。

异步控制基础

这些文件提供小型可复用基础组件,用于取消、就绪信号和感知暂停的超时预算。

async-utils/src/lib.rs源码 ↗
utilcross-cutting

在异步程序里,经常会有一些任务正在等网络、等定时器、等别的事情完成。但如果系统要关闭,或者用户已经不需要结果了,就需要一种办法告诉这些任务:“别等了,收工。”这个文件做的就是这件事。它定义了一个简单的错误类型 CancelErr,用来表示任务不是正常完成,而是被取消了;又定义了 OrCancelExt 这个扩展能力,让任何合适的 Future(可以理解成“未来某个时刻会出结果的异步任务”)都能调用 .or_cancel(...)。核心做法像同时盯着两件事:一边等原任务完成,一边等取消令牌 CancellationToken 发信号;谁先发生就采用谁的结果。下面的测试分别证明了三种情况:任务先完成会返回正常结果,取消信号先到会返回取消错误,令牌一开始就已经取消也会马上返回取消错误。

函数细节4
F::or_cancel25–30 ↗
async fn or_cancel(self, token: &CancellationToken) -> Result<Self::Output, CancelErr>

作用:给一个异步任务套上一层“取消开关”。调用者把任务和一个取消令牌传进来,最后要么拿到任务原本的结果,要么收到“已取消”的错误。

数据流:进去的是一个正在等待结果的异步任务 self,以及一个 CancellationToken(取消令牌,可以理解成多人共用的停止按钮)。函数同时等待两边:如果令牌先被取消,就输出 Err(CancelErr::Cancelled);如果任务先完成,就把任务结果包成 Ok(...) 输出。它不主动改任务的结果,只是在外面判断到底是任务先结束,还是取消信号先到。

调用关系:这是整个文件真正提供给外部使用的工具方法。它内部把等待逻辑交给 tokio::select!,也就是 Tokio 异步运行时提供的“同时等多个事件,谁先来就处理谁”的机制。测试函数会用它来验证正常完成、延迟取消、提前取消这三条路径。

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

tests::returns_ok_when_future_completes_first42–49 ↗
async fn returns_ok_when_future_completes_first()

作用:验证最普通的情况:异步任务先完成时,or_cancel 不应该误报取消,而应该返回原本的结果。

数据流:进去的是一个新建的取消令牌和一个立刻返回 42 的异步任务。测试把这个任务接上 or_cancel 后等待结果;因为没有人取消令牌,任务先完成,所以出来的是 Ok(42)。最后用断言确认结果正是这个值。

调用关系:这是对 F::or_cancel 的第一条保护性测试,说明这个工具不会干扰正常完成的任务。它会创建 CancellationToken,并用 assert_eq! 检查输出是否符合预期。

调用图:外部调用 2 个(new, assert_eq!)。

tests::returns_err_when_token_cancelled_first52–70 ↗
async fn returns_err_when_token_cancelled_first()

作用:验证取消信号比任务结果更早到达时,or_cancel 会立刻返回取消错误,而不是继续等原任务结束。

数据流:进去的是一个取消令牌、它的克隆副本,以及一个需要等待 100 毫秒才返回 7 的异步任务。测试另外启动一个小任务,等 10 毫秒后按下取消令牌。因为取消更早发生,or_cancel 输出 Err(CancelErr::Cancelled)。最后测试等待那个负责取消的小任务结束,并断言结果确实是取消错误。

调用关系:这是对 F::or_cancel 的第二条关键测试,模拟现实里的“任务还没做完,但系统决定停掉它”。它用 task::spawn 启动后台取消动作,用 sleep 和 from_millis 制造先后顺序,再用 assert_eq! 检查 F::or_cancel 是否选择了取消这条路。

调用图:外部调用 5 个(new, from_millis, assert_eq!, spawn, sleep)。

tests::returns_err_when_token_already_cancelled73–85 ↗
async fn returns_err_when_token_already_cancelled()

作用:验证如果取消令牌在调用 or_cancel 之前就已经被取消,函数会马上返回取消错误。

数据流:进去的是一个新建后立刻被取消的令牌,以及一个本来要等 50 毫秒才返回 5 的异步任务。因为令牌已经处于取消状态,or_cancel 不需要等任务睡完,直接输出 Err(CancelErr::Cancelled)。最后用断言确认输出是取消错误。

调用关系:这是对 F::or_cancel 的第三条边界情况测试。它保证这个工具不只会响应“之后发生”的取消,也能识别“早就已经取消”的令牌,避免调用者误以为任务还能继续正常跑。

调用图:外部调用 4 个(new, from_millis, assert_eq!, sleep)。

utils/readiness/src/lib.rs源码 ↗
utilcross-cutting

这个文件像是在程序里放了一个“开业牌”。一开始牌子是“还没好”,某些任务可以先登记,拿到一个令牌;只有拿着有效令牌的人,才能把牌子翻成“准备好了”。其他任务如果需要等,就可以挂起等待,等牌子一翻就继续干活。这里用到了异步等待,也就是任务不用傻等占着线程,而是先让出位置。它还用了互斥锁(一把锁,防止两个任务同时改同一份令牌名单)来保护令牌集合,用原子布尔值(一种可被多线程安全快速读取的真假值)来快速判断是否已经就绪。一个特别的行为是:如果没人订阅,调用 is_ready 会直接把状态变成 ready,表示没有人需要批准,系统可以自然继续。文件末尾的测试覆盖了正常授权、重复授权、非法令牌、等待唤醒、锁超时等关键情况。

函数细节18
ReadinessFlag::new60–68 ↗
fn new() -> Self

作用:创建一个新的就绪开关,初始状态是“还没准备好”。程序需要一个新的等待点时会用它。

数据流:进去没有额外输入 → 它建立一个 false 的就绪状态、一个从 1 开始发号的令牌计数器、一个空的令牌名单,以及一个通知等待者的广播通道 → 出来一个可被共享使用的 ReadinessFlag。

调用关系:这是整个工具的起点。测试和外部代码先调用它造出开关,后面才能订阅、标记 ready、等待 ready;ReadinessFlag::default 也只是转头调用它。

调用图:被 9 处调用(is_ready_without_subscribers_marks_flag_ready, mark_ready_rejects_unknown_token, mark_ready_twice_uses_single_token, subscribe_after_ready_returns_none, subscribe_and_mark_ready_roundtrip, subscribe_avoids_duplicate_tokens, subscribe_returns_error_when_lock_is_held, subscribe_skips_zero_token, wait_ready_unblocks_after_mark_ready);外部调用 5 个(new, new, new, new, channel)。

ReadinessFlag::with_tokens70–78 ↗
async fn with_tokens(
        &self,
        f: impl FnOnce(&mut HashSet<Token>) -> R,
    ) -> Result<R, errors::ReadinessError>

作用:安全地打开令牌名单,让调用者对名单做一次受保护的修改或检查。它还设置了最多等待 1 秒,避免一直卡死。

数据流:进去一个要对令牌集合执行的小操作 → 它尝试拿到互斥锁;如果 1 秒内拿不到,就返回 TokenLockFailed;如果拿到了,就把令牌集合交给这个小操作使用 → 出来这个小操作的结果,或者一个锁失败错误。

调用关系:它是 subscribe 和 mark_ready 共用的内部帮手。subscribe 用它把新令牌放进名单,mark_ready 用它检查并移除令牌。

调用图:被 2 处调用(mark_ready, subscribe);外部调用 1 个(timeout)。

ReadinessFlag::load_ready80–82 ↗
fn load_ready(&self) -> bool

作用:快速读取当前是否已经 ready。它把底层的原子读取包装起来,其他函数不用重复写细节。

数据流:进去的是 ReadinessFlag 自己保存的 ready 状态 → 它用线程安全的方式读取这个真假值 → 出来 true 或 false,不改动任何东西。

调用关系:很多地方都会先问它一声:fmt 显示调试信息时用它,is_ready 判断状态时用它,subscribe 和 mark_ready 也用它避免在已经 ready 后继续做无用操作。

调用图:被 4 处调用(fmt, is_ready, mark_ready, subscribe);外部调用 1 个(load)。

ReadinessFlag::default86–88 ↗
fn default() -> Self

作用:提供默认创建方式,让别人可以用通用的 Default 习惯来创建 ReadinessFlag。

数据流:进去没有输入 → 它直接调用 ReadinessFlag::new → 出来一个新的、尚未 ready 的 ReadinessFlag。

调用关系:它只是创建入口的便捷包装。需要默认值的框架或代码可以通过它间接走 ReadinessFlag::new。

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

ReadinessFlag::fmt92–96 ↗
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result

作用:让 ReadinessFlag 在调试打印时显示出有用信息,主要是当前 ready 状态。

数据流:进去一个格式化器和当前对象 → 它读取 ready 状态,并组装成类似结构体的调试输出 → 出来格式化是否成功的结果,不改变 ReadinessFlag。

调用关系:它属于调试辅助。有人打印这个对象时会自动调用它;它内部只向 ReadinessFlag::load_ready 要当前状态。

调用图:调用 1 个内部函数(load_ready);外部调用 1 个(debug_struct)。

ReadinessFlag::is_ready100–117 ↗
fn is_ready(&self) -> bool

作用:告诉调用者这个开关现在是不是已经 ready。它还有一个规则:如果没有任何人订阅等待授权,就自动认为已经 ready。

数据流:进去没有额外输入,只读取对象内部状态 → 它先快速看 ready 是否已经为 true;如果不是,就尝试查看令牌名单,发现名单为空时会把 ready 翻成 true 并通知等待者;如果拿不到锁或名单不空,就再次读取状态 → 出来当前是否 ready,可能会把状态从 false 改成 true。

调用关系:wait_ready 会先调用它做快速判断,避免不必要地等待。它也可能主动触发通知,让那些等 ready 的任务继续。

调用图:调用 1 个内部函数(load_ready);被 1 处调用(wait_ready);外部调用 2 个(swap, send)。

ReadinessFlag::subscribe119–143 ↗
async fn subscribe(&self) -> Result<Token, errors::ReadinessError>

作用:登记一个“我将来有权把系统标成 ready”的订阅者,并给它一个令牌。没有这个令牌,mark_ready 不会认账。

数据流:进去没有额外输入 → 它先检查是否已经 ready;如果已 ready,就返回 FlagAlreadyReady;否则在锁保护下再次检查状态,然后生成一个非 0、且不和现有令牌重复的新令牌放进名单 → 出来这个 Token,或者错误。

调用关系:这是授权流程的第一步。调用者先用它拿令牌,之后再把令牌交给 mark_ready;它内部通过 with_tokens 修改令牌名单。

调用图:调用 2 个内部函数(load_ready, with_tokens)。

ReadinessFlag::mark_ready145–169 ↗
async fn mark_ready(&self, token: Token) -> Result<bool, errors::ReadinessError>

作用:尝试把开关翻成“准备好了”,但只有有效令牌才能成功。这样可以防止无关代码误触发 ready。

数据流:进去一个 Token → 它先看是否早已 ready,若是就返回 false;令牌为 0 也直接拒绝;然后在锁保护下检查令牌是否在名单里,合法就移除它,把 ready 设为 true,清空其他令牌,并通知等待者 → 出来 true 表示这次真的标记成功,false 表示没有生效,或返回锁错误。

调用关系:这是授权流程的完成动作。subscribe 发出的令牌最终交给它验证;它用 with_tokens 操作令牌集合,并通过通知通道唤醒 wait_ready 里的等待任务。

调用图:调用 2 个内部函数(load_ready, with_tokens);外部调用 1 个(send)。

ReadinessFlag::wait_ready171–186 ↗
async fn wait_ready(&self)

作用:让当前异步任务一直等到开关变成 ready,但等待期间不会阻塞整个线程。

数据流:进去没有额外输入 → 它先调用 is_ready 快速检查;如果还没 ready,就订阅通知通道,再检查一次通道当前值,之后循环等待变化,直到看到 true → 出来没有具体值,只表示等待结束、系统已经 ready 或通知通道结束。

调用关系:这是消费者使用的等待入口。其他任务调用 mark_ready 成功后会发通知,它这里收到通知后放行。

调用图:调用 1 个内部函数(is_ready);外部调用 1 个(subscribe)。

tests::subscribe_and_mark_ready_roundtrip213–220 ↗
async fn subscribe_and_mark_ready_roundtrip() -> Result<(), ReadinessError>

作用:测试最基本的完整流程:先订阅拿令牌,再用令牌标记 ready。

数据流:进去没有外部输入 → 它创建一个新开关,调用 subscribe 拿令牌,再调用 mark_ready,并检查状态确实变成 ready → 出来测试通过或失败。

调用关系:它验证 ReadinessFlag::new、subscribe、mark_ready、is_ready 能按最常见路线配合起来。

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

tests::subscribe_after_ready_returns_none223–230 ↗
async fn subscribe_after_ready_returns_none() -> Result<(), ReadinessError>

作用:测试已经 ready 后不能再订阅。这样可以保证 ready 是终点,不会再产生新的授权令牌。

数据流:进去没有外部输入 → 它创建开关,订阅并标记 ready,然后再次尝试订阅 → 出来预期是第二次订阅报错,否则测试失败。

调用关系:它主要检查 subscribe 在 mark_ready 成功之后会拒绝新订阅。

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

tests::mark_ready_rejects_unknown_token233–239 ↗
async fn mark_ready_rejects_unknown_token() -> Result<(), ReadinessError>

作用:测试随便伪造一个令牌不能把系统标成 ready。它保证授权机制不是摆设。

数据流:进去没有外部输入 → 它创建开关,直接用 Token(42) 调 mark_ready,检查返回 false,且内部 ready 还没被直接改成 true;随后调用 is_ready 验证无订阅者时会自动 ready → 出来测试通过或失败。

调用关系:它检查 mark_ready 对未知令牌的拒绝行为,也顺带覆盖 is_ready 在没有订阅者时的自动 ready 规则。

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

tests::wait_ready_unblocks_after_mark_ready242–256 ↗
async fn wait_ready_unblocks_after_mark_ready() -> Result<(), ReadinessError>

作用:测试等待中的任务会在 ready 后被唤醒。否则等待者可能永远卡住。

数据流:进去没有外部输入 → 它创建可共享的开关,订阅拿令牌,另起一个异步任务等待 ready,然后用令牌标记 ready → 出来等待任务正常结束,测试通过。

调用关系:它把 wait_ready 和 mark_ready 放在两个异步任务里,验证 mark_ready 发出的通知确实能让 wait_ready 继续。

调用图:调用 1 个内部函数(new);外部调用 4 个(clone, new, assert!, spawn)。

tests::mark_ready_twice_uses_single_token259–266 ↗
async fn mark_ready_twice_uses_single_token() -> Result<(), ReadinessError>

作用:测试同一个令牌只能真正生效一次。ready 一旦完成,重复提交不应该再次改变状态。

数据流:进去没有外部输入 → 它创建开关,订阅拿令牌,第一次 mark_ready 应返回 true,第二次用同一个令牌 mark_ready 应返回 false → 出来测试通过或失败。

调用关系:它检查 mark_ready 的一次性语义,确认令牌不会被重复使用。

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

tests::is_ready_without_subscribers_marks_flag_ready269–279 ↗
async fn is_ready_without_subscribers_marks_flag_ready() -> Result<(), ReadinessError>

作用:测试没有订阅者时,调用 is_ready 会直接让开关进入 ready。这个规则避免系统在没人需要授权时无意义等待。

数据流:进去没有外部输入 → 它创建开关,调用 is_ready 并期望返回 true,再次调用仍为 true,之后再订阅应得到 FlagAlreadyReady 错误 → 出来测试通过或失败。

调用关系:它专门验证 is_ready 的特殊自动完成行为,并确认这个行为会影响后续 subscribe。

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

tests::subscribe_returns_error_when_lock_is_held282–313 ↗
async fn subscribe_returns_error_when_lock_is_held()

作用:测试如果令牌名单的锁被别人长时间占着,订阅会返回明确错误,而不是一直卡住。

数据流:进去没有外部输入 → 它创建共享开关,在另一个线程里故意拿住令牌锁,然后主测试尝试 subscribe;因为超时拿不到锁,应返回 TokenLockFailed;最后释放锁并收尾线程 → 出来测试通过或失败。

调用关系:它验证 with_tokens 的超时保护,也就是 subscribe 在锁竞争严重时的错误路径。

调用图:调用 1 个内部函数(new);外部调用 5 个(clone, new, assert_matches!, channel, spawn)。

tests::subscribe_skips_zero_token316–324 ↗
async fn subscribe_skips_zero_token() -> Result<(), ReadinessError>

作用:测试令牌生成器碰到 0 时会跳过。因为 0 被设计成永远无效的令牌。

数据流:进去没有外部输入 → 它创建开关,故意把下一个令牌编号设成 0,然后订阅;检查拿到的不是 Token(0),再用这个令牌成功标记 ready → 出来测试通过或失败。

调用关系:它检查 subscribe 的令牌生成规则,确保 mark_ready 中“0 永远无效”的约定不会误伤正常订阅者。

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

tests::subscribe_avoids_duplicate_tokens327–335 ↗
async fn subscribe_avoids_duplicate_tokens() -> Result<(), ReadinessError>

作用:测试令牌编号即使绕回或被设成已有值,也不会发出重复令牌。

数据流:进去没有外部输入 → 它创建开关,先订阅拿到一个令牌,再把下一个编号强行设成同一个值,随后再次订阅 → 出来第二个令牌必须不同于第一个,否则测试失败。

调用关系:它验证 subscribe 在生成令牌时会检查令牌名单,必要时继续找下一个可用编号。

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

shell-escalation/src/unix/stopwatch.rs源码 ↗
utilcross-cutting;在需要限时运行命令、等待权限提示或用户确认时活跃

普通的超时计时器一启动就一路倒数,哪怕程序正在等用户点确认,也会继续耗时间,这可能导致用户还没来得及回应,操作就被取消了。这个文件里的 Stopwatch 就是为了解决这个问题:它记录已经用掉的时间、当前是否正在计时,以及有多少个地方要求暂停。它还能生成一个取消令牌(CancellationToken,可以理解成“到点就拉响的停止信号”),时间到后通知外部该停了。它的特别之处是支持嵌套暂停:如果两个等待同时发生,短的那个结束后不会马上恢复计时,必须所有暂停都结束才继续。内部用互斥锁(防止多个异步任务同时改同一份状态)保护秒表状态,用 Notify(异步通知铃)叫醒等待中的计时任务。

函数细节10
Stopwatch::new25–35 ↗
fn new(limit: Duration) -> Self

作用:创建一个有时间上限的秒表,并且创建后立刻开始计时。别人用它来给一次操作设定“最多只能跑多久”。

数据流:输入是一段时间长度,比如 50 毫秒 → 它把已用时间设为 0,把“开始计时的时间点”记成现在,并准备好共享状态和通知铃 → 输出一个 Stopwatch,之后可以拿它生成到点取消信号,也可以临时暂停。

调用关系:它是有时限秒表的入口。实际运行里,try_run_zsh_fork、各种权限升级和沙盒测试场景会创建它;测试里的 cancellation_receiver_fires_after_limit、pause_prevents_timeout_until_resumed、overlapping_pauses_only_resume_once 也用它来验证超时和暂停行为。

调用图:被 8 处调用(try_run_zsh_fork, denied_reads_keep_granular_sandbox_rejection_for_escalation, denied_reads_keep_prefix_rule_allow_inside_sandbox, execve_permission_request_hook_short_circuits_prompt, preapproved_additional_permissions_escalate_intercepted_exec, cancellation_receiver_fires_after_limit, overlapping_pauses_only_resume_once, pause_prevents_timeout_until_resumed);外部调用 4 个(new, now, new, new)。

Stopwatch::unlimited37–47 ↗
fn unlimited() -> Self

作用:创建一个没有时间上限的秒表。它仍然像秒表一样有状态,但永远不会因为到点而发出取消信号。

数据流:不需要输入 → 它初始化已用时间、当前开始时间和暂停计数,但把时间上限设为空 → 输出一个 Stopwatch,用起来和普通秒表类似,只是不会自动取消。

调用关系:当外部流程不想受超时限制时会用它,比如 prepare_unified_exec_zsh_fork。测试 unlimited_stopwatch_never_cancels 专门确认它不会误触发取消。

调用图:被 2 处调用(prepare_unified_exec_zsh_fork, unlimited_stopwatch_never_cancels);外部调用 4 个(new, now, new, new)。

Stopwatch::cancellation_token49–91 ↗
fn cancellation_token(&self) -> CancellationToken

作用:生成一个“到时间就取消”的信号。调用者可以等待这个信号,一旦秒表累计运行时间达到上限,就知道该停止当前操作。

数据流:输入是当前 Stopwatch 本身,它会读取时间上限、已走时间、是否正在运行 → 如果没有上限,就直接给出一个永远不会自动取消的令牌;如果有上限,它会启动一个后台异步任务,不断计算还剩多久,运行时就睡到剩余时间,暂停或恢复时就被通知重新计算 → 输出 CancellationToken,并在时间真正用完时把它标记为已取消。

调用关系:它把 Stopwatch 和外部的“停止机制”接起来。创建秒表后,调用者通常会拿这个令牌去等待取消;它内部把实际倒计时工作交给 tokio::spawn 启动的后台任务,并用 sleep、select 和 Notify 在“睡到超时”和“被暂停/恢复叫醒”之间切换。

调用图:外部调用 6 个(clone, new, pin!, select!, spawn, sleep)。

Stopwatch::pause_for97–105 ↗
async fn pause_for(&self, fut: F) -> T

作用:在等待某个异步任务完成时,临时暂停秒表。最典型的用途是等待用户提示时不把等待时间算进超时里。

数据流:输入是一个将来才完成的异步任务 fut → 它先调用 pause 让秒表停住,再等待 fut 完成,最后调用 resume 恢复计时 → 输出 fut 原本的结果,同时保证这段等待时间不会被算作运行时间。

调用关系:它是外部最常用的暂停接口,prompt 会调用它来包住提示流程。它自己不直接改状态,而是把“停表”和“继续走表”分别交给 pause 和 resume。

调用图:调用 2 个内部函数(pause, resume);被 1 处调用(prompt)。

Stopwatch::pause107–116 ↗
async fn pause(&self)

作用:真正执行“停表”的内部函数。它会记录停下之前已经走了多久,并把秒表标记为暂停中。

数据流:输入是当前 Stopwatch → 它锁住内部状态,把暂停计数加 1;如果这是第一个暂停,就把从上次开始计时到现在的时间加进已用时间,并清空正在运行的起点 → 它不返回值,但会改变秒表状态,并通知等待中的倒计时任务重新判断。

调用关系:它只由 pause_for 调用,是暂停流程的第一步。因为支持多个暂停重叠,所以它用 active_pauses 计数,避免第二个暂停重复结算时间。

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

Stopwatch::resume118–128 ↗
async fn resume(&self)

作用:真正执行“恢复计时”的内部函数。只有所有暂停都结束后,它才会让秒表重新开始走。

数据流:输入是当前 Stopwatch → 它锁住内部状态,如果没有暂停就什么也不做;否则把暂停计数减 1;当计数降到 0 时,把新的开始时间记成现在 → 它不返回值,但会更新秒表状态,并通知倒计时任务重新计算剩余时间。

调用关系:它只由 pause_for 在被包住的异步任务结束后调用。它和 pause 配成一对,保证重叠暂停时不会过早恢复计时。

调用图:被 1 处调用(pause_for);外部调用 1 个(now)。

tests::cancellation_receiver_fires_after_limit140–146 ↗
async fn cancellation_receiver_fires_after_limit()

作用:测试有上限的秒表确实会在时间到后发出取消信号。它防止“超时永远不触发”这种严重问题。

数据流:输入是测试里创建的 50 毫秒上限 → 它创建 Stopwatch,拿到取消令牌,记录开始时间,然后等待令牌取消 → 最后检查实际经过时间至少达到 50 毫秒,证明不是提前取消。

调用关系:这是 Stopwatch::new 和 Stopwatch::cancellation_token 的基础验证。它模拟外部调用者等待取消信号的使用方式。

调用图:调用 1 个内部函数(new);外部调用 3 个(from_millis, now, assert!)。

tests::pause_prevents_timeout_until_resumed149–173 ↗
async fn pause_prevents_timeout_until_resumed()

作用:测试暂停期间不会触发超时,恢复后才会继续倒计时。它保证等待用户或其他异步事情时,不会白白耗掉限时额度。

数据流:输入是 50 毫秒上限和一个会暂停 100 毫秒的异步任务 → 它启动 pause_for 包住这段等待,然后在暂停期间尝试等取消信号,确认等不到;暂停任务结束后,再等待取消信号 → 结果证明暂停期间时间没有继续扣,恢复后才会到点取消。

调用关系:它覆盖 Stopwatch::new、Stopwatch::cancellation_token 和 pause_for 的配合。测试里用 tokio::spawn 模拟另一个异步任务正在暂停秒表。

调用图:调用 1 个内部函数(new);外部调用 4 个(from_millis, assert!, spawn, sleep)。

tests::overlapping_pauses_only_resume_once176–224 ↗
async fn overlapping_pauses_only_resume_once()

作用:测试两个暂停重叠时,短暂停结束不会让秒表提前恢复。它保护的是“多个等待同时发生”这种容易出错的情况。

数据流:输入是 50 毫秒上限,以及两个重叠的暂停任务:一个 80 毫秒,一个 30 毫秒 → 它先确认两个暂停期间不会取消;短暂停结束后,再确认仍然不会取消;长暂停结束后,才等待真正的取消信号 → 结果证明秒表等所有暂停都结束才恢复。

调用关系:它专门验证 pause 和 resume 里的暂停计数机制。两个 tokio::spawn 出来的任务都会通过 pause_for 影响同一个 Stopwatch。

调用图:调用 1 个内部函数(new);外部调用 4 个(from_millis, assert!, spawn, sleep)。

tests::unlimited_stopwatch_never_cancels227–236 ↗
async fn unlimited_stopwatch_never_cancels()

作用:测试没有上限的秒表不会自动取消。它防止“本来不该限时的流程被误杀”。

数据流:输入是一个 unlimited 秒表 → 它生成取消令牌,并在 30 毫秒内等待取消信号 → 结果应该是等不到取消,说明无限秒表没有偷偷启动超时。

调用关系:它验证 Stopwatch::unlimited 和 cancellation_token 的特殊分支。这个测试对应实际运行中那些明确不需要超时限制的场景。

调用图:调用 1 个内部函数(unlimited);外部调用 1 个(assert!)。

睡眠抑制后端

这些文件公开跨平台睡眠抑制 API 及其平台专用或空操作实现。

utils/sleep-inhibitor/src/lib.rs源码 ↗
utilcross-cutting:任务开始和结束时被调用,贯穿运行过程

这个文件解决的是一个很现实的问题:程序正在跑一轮工作时,电脑如果因为闲置进入睡眠,任务就可能被中断。它把不同操作系统的做法包成同一个入口,外面的代码不用关心自己是在 macOS、Linux、Windows,还是别的平台。可以把它想成一个“临时请勿打扰牌”:任务开始时挂上,任务结束时摘下。核心结构是 SleepInhibitor,里面记着两件事:这个功能有没有启用,以及当前是否有任务正在运行。真正跟系统打交道的部分交给各个平台自己的实现模块,比如 macOS 用系统电源断言,Linux 调系统抑制工具,Windows 用电源请求;不支持的平台就什么也不做。这样既保证跨平台代码好用,也避免每个调用者重复写一堆系统判断。

函数细节9
SleepInhibitor::new37–43 ↗
fn new(enabled: bool) -> Self

作用:创建一个新的防睡眠工具,并决定它是否真的启用。有人需要在任务运行期间阻止系统睡眠时,会先用它建一个对象。

数据流:进去的是一个 enabled 开关,表示要不要启用防睡眠功能。函数把这个开关存起来,把“任务正在运行”先设成 false,并调用平台实现的 new 创建真正跟系统打交道的内部对象。出来的是一个可以后续开启或关闭防睡眠的 SleepInhibitor。

调用关系:这是整个工具的起点。测试代码会先调用它创建实例,业务代码也会先拿到这个实例,然后再用 set_turn_running 告诉它任务什么时候开始、什么时候结束。它把底层初始化交给平台模块的 new。

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

SleepInhibitor::set_turn_running46–58 ↗
fn set_turn_running(&mut self, turn_running: bool)

作用:告诉工具“任务现在开始了”或“任务现在结束了”。它会根据这个状态决定是挂上防睡眠,还是释放防睡眠。

数据流:进去的是 turn_running,一个真假值。函数先记录这个最新状态;如果防睡眠功能没启用,就直接调用 release,确保不会继续占着系统防睡眠请求。若功能启用且任务正在运行,它调用 acquire;若任务结束,它调用 release。结果是内部状态被更新,系统层面的防睡眠状态也跟着变化。

调用关系:这是外部最常用的控制入口。任务调度代码通常在一轮任务开始时传 true,在结束时传 false。它不自己碰操作系统,而是把具体动作分给 SleepInhibitor::acquire 和 SleepInhibitor::release。

调用图:调用 2 个内部函数(acquire, release)。

SleepInhibitor::acquire60–62 ↗
fn acquire(&mut self)

作用:真正发出“请让电脑保持清醒”的请求。它是一个小转接头,把统一接口转给当前操作系统的实现。

数据流:它不接收额外输入,只读取对象里保存的平台实现对象。函数调用平台对象的 acquire,让系统进入防睡眠状态。出来没有返回值,但可能让系统记录一条“当前程序需要保持运行”的请求。

调用关系:它由 set_turn_running 在任务开始且功能启用时调用。它自己不判断时机,只负责把“申请防睡眠”这件事交给平台模块的 acquire。

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

SleepInhibitor::release64–66 ↗
fn release(&mut self)

作用:撤销之前的防睡眠请求,让电脑恢复正常的自动睡眠规则。任务结束或功能关闭时会用到它。

数据流:它不接收额外输入,只使用内部的平台实现对象。函数调用平台对象的 release,释放系统层面的防睡眠占用。出来没有返回值,但系统不再因为这次请求而被强行保持清醒。

调用关系:它由 set_turn_running 在任务结束时调用;如果防睡眠功能被关闭,也会被调用来兜底清理。它把真正释放资源的工作交给平台模块的 release。

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

SleepInhibitor::is_turn_running69–71 ↗
fn is_turn_running(&self) -> bool

作用:查看当前记录的任务运行状态。它不会去问操作系统,只是返回这个工具最后一次收到的“任务是否运行中”的说法。

数据流:进去没有参数。函数读取内部的 turn_running 字段,然后把这个真假值返回。它不改动任何状态,也不会申请或释放防睡眠。

调用关系:测试会用它确认 set_turn_running 是否正确记录了状态。外部代码也可以用它做简单检查,但它不参与实际的系统防睡眠动作。

tests::sleep_inhibitor_toggles_without_panicking79–85 ↗
fn sleep_inhibitor_toggles_without_panicking()

作用:测试启用防睡眠时,开始任务和结束任务这一来一回不会崩溃,并且状态记录是对的。

数据流:测试先用 new 创建一个启用状态的 SleepInhibitor,然后把任务状态设为 true,检查 is_turn_running 返回 true;再设为 false,检查返回 false。结果是验证基本开关流程能正常跑完。

调用关系:这是给 SleepInhibitor 的基础安全测试。它模拟最普通的使用方式:任务开始、任务结束,并用断言确认外部能看到正确状态。

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

tests::sleep_inhibitor_disabled_does_not_panic88–94 ↗
fn sleep_inhibitor_disabled_does_not_panic()

作用:测试防睡眠功能关闭时,调用同样的开始和结束流程也不会出错。这样配置关掉功能的用户也能安全运行程序。

数据流:测试先用 new 创建一个 disabled 的实例。即使功能关闭,它仍然把任务状态设成 true 和 false,并检查状态值是否按要求记录。结果是确认“不开启防睡眠”和“记录任务状态”互不冲突。

调用关系:这个测试覆盖 set_turn_running 里 enabled 为 false 的分支。它确保关闭功能时会走释放路径,但不会因为没有真正申请过防睡眠而崩溃。

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

tests::sleep_inhibitor_multiple_true_calls_are_idempotent97–103 ↗
fn sleep_inhibitor_multiple_true_calls_are_idempotent()

作用:测试连续多次说“任务正在运行”不会把工具搞坏。这里的“幂等”意思是,同一个动作重复做几次,效果仍然像做一次一样安全。

数据流:测试创建启用的 SleepInhibitor,然后连续多次把任务状态设为 true,最后设为 false。它主要观察过程是否能正常结束,没有返回值检查,但如果内部重复申请处理有问题,测试就可能失败或崩溃。

调用关系:这个测试模拟真实场景里上层代码重复发出“任务开始”信号的情况。它验证 set_turn_running 多次触发 acquire 时,整体接口仍然可安全使用。

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

tests::sleep_inhibitor_can_toggle_multiple_times106–112 ↗
fn sleep_inhibitor_can_toggle_multiple_times()

作用:测试防睡眠可以反复打开和关闭,不只是用一次。真实程序可能一轮一轮地跑任务,所以这个能力很重要。

数据流:测试创建启用的 SleepInhibitor,然后按 true、false、true、false 的顺序多次切换任务状态。结果是确认重复的开始和结束流程不会引发错误。

调用关系:这个测试覆盖多轮任务的常见使用方式。它反复驱动 set_turn_running,让 acquire 和 release 交替发生,检查整个工具在长期运行中能保持稳定。

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

utils/sleep-inhibitor/src/dummy.rs源码 ↗
utilcross-cutting

这个文件里的 SleepInhibitor 像一个“假开关”。正常情况下,睡眠阻止器的作用是告诉操作系统:先别让电脑自动休眠,比如正在执行重要任务时。但这个 dummy 版本什么也不做。它仍然提供 new、acquire、release 这几个接口,让外层代码可以按同样方式使用它。这样做的好处是,项目里其他地方不用到处判断“当前系统支不支持阻止睡眠”。就像墙上装了一个备用按钮,按下去没有实际电路,但不会报错,也不会影响流程。这里的 acquire 表示“开始阻止睡眠”,release 表示“取消阻止睡眠”,但在这个文件中它们都是空操作。

函数细节3
SleepInhibitor::new5–7 ↗
fn new() -> Self

作用:创建一个新的空睡眠阻止器对象。有人会用它,是因为外层代码需要一个统一的东西来代表“睡眠阻止功能”,哪怕当前实现实际上什么也不做。

数据流:进去没有参数 → 它直接构造一个 SleepInhibitor 空对象,不读取系统状态,也不申请任何资源 → 出来一个可以被后续 acquire 和 release 调用的对象,程序状态没有其他变化。

调用关系:它通常被外层的 new 包装函数调用,也会在一些测试里被创建出来。它是整个流程的起点:先有这个对象,后面才能统一调用 acquire 或 release。

调用图:被 7 处调用(new, set_prevent_idle_sleep, new, sleep_inhibitor_can_toggle_multiple_times, sleep_inhibitor_disabled_does_not_panic, sleep_inhibitor_multiple_true_calls_are_idempotent, sleep_inhibitor_toggles_without_panicking)。

SleepInhibitor::acquire9–9 ↗
fn acquire(&mut self)

作用:表示“开始阻止电脑自动睡眠”。但在这个 dummy 版本里,它只是接受这个调用,不实际通知操作系统。

数据流:进去的是当前这个 SleepInhibitor 对象的可修改引用 → 它什么也不改、什么也不申请、也不和操作系统通信 → 出来没有返回值,对象看起来仍然一样。

调用关系:它被外层的 acquire 调用。当上层代码认为需要防止空闲睡眠时,会走到这里;这个文件的职责是保证调用安全通过,而不是完成真正的系统级阻止睡眠。

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

SleepInhibitor::release11–11 ↗
fn release(&mut self)

作用:表示“结束阻止电脑睡眠,恢复正常”。但在这个空实现里,因为之前也没有真正阻止过睡眠,所以这里也不需要做任何事。

数据流:进去的是当前 SleepInhibitor 对象的可修改引用 → 它不释放资源、不改状态、也不通知操作系统 → 出来没有返回值,也没有副作用。

调用关系:它被外层的 release 调用。上层代码在不再需要防止睡眠时会调用它;在真实实现里这里可能会释放系统资源,而这个 dummy 版本只是保持接口一致,避免流程出错。

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

utils/sleep-inhibitor/src/iokit_bindings.rs源码 ↗
generatedcross-cutting,主要在程序需要开启或取消防睡眠时被间接使用

这个文件不是手写业务代码,而是由工具自动生成的绑定文件。所谓“绑定”,可以理解成一张对照表:Rust 这边要叫某个名字、传某种数据,macOS 系统那边才能听懂。这里主要对接的是 macOS 的 IOKit 电源管理接口,用来创建或释放“睡眠阻止声明”。打个比方,程序需要告诉系统:“我现在有重要任务,先别睡觉。”这个文件就提供了说这句话所需的系统函数名、常量和数据类型。因为它直接连到 C 语言系统接口,所以标成 unsafe extern "C",意思是“这是外部系统函数,调用时要特别小心,Rust 本身不能完全保证安全”。没有它,上层 Rust 代码就很难直接使用 macOS 的防睡眠能力。

utils/sleep-inhibitor/src/linux_inhibitor.rs源码 ↗
domain_logiccross-cutting;在 Codex 有活跃任务时 acquire,任务结束或对象销毁时 release

这个文件像是在 Linux 桌面旁边放了一个“请勿睡觉”的牌子。它不会自己改系统电源设置,而是启动系统已有的小工具来申请“暂时不要因为空闲而睡眠”。它优先尝试 systemd-inhibit,如果不行再试 gnome-session-inhibit。为了让这个请求一直有效,它让这些工具去运行一个很久很久的 sleep 命令;只要这个子进程还活着,系统就认为有人在阻止空闲睡眠。LinuxSleepInhibitor 记录当前有没有正在生效的阻止器、用的是哪个后端、以及之前有没有报过“找不到可用后端”的警告,避免反复刷日志。acquire 会启动或检查阻止器,release 会杀掉子进程并收尾。Drop 是保险机制:对象被销毁时自动释放,防止留下没人管的后台进程。

函数细节7
LinuxSleepInhibitor::new39–41 ↗
fn new() -> Self

作用:创建一个新的 Linux 防睡眠器。一开始它什么都没启动,只是准备好以后在需要时申请“别睡眠”。

数据流:进去没有额外参数 → 它使用默认值初始化内部状态,比如“当前未激活”“还没有偏好的后端” → 出来一个可以调用 acquire 和 release 的 LinuxSleepInhibitor。

调用关系:这是使用这个文件功能的起点。外部代码先调用它拿到一个防睡眠器,之后在任务开始时调用 LinuxSleepInhibitor::acquire,在任务结束时调用 LinuxSleepInhibitor::release。

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

LinuxSleepInhibitor::acquire43–143 ↗
fn acquire(&mut self)

作用:开始或保持 Linux 的防睡眠状态。简单说,就是确认“请勿睡觉”的牌子已经挂上;如果原来的牌子掉了,就重新挂一个。

数据流:进去的是这个防睡眠器当前保存的状态 → 它先检查已有子进程是否还活着;如果还活着就直接结束;如果已经退出或检查失败,就清掉旧状态,再按顺序尝试启动 systemd-inhibit 或 gnome-session-inhibit → 成功后保存正在运行的子进程和所用后端;失败时记录警告,并在所有办法都不可用时记住这件事,避免之后重复报同样的警告。

调用关系:这是任务开始或继续运行时最关键的入口。它会把真正启动外部系统工具的工作交给 spawn_backend;如果清理异常子进程时遇到“进程其实已经没了”的错误,会用 child_exited 判断这是不是可以忽略的情况;遇到异常会通过 warn! 写警告日志。

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

LinuxSleepInhibitor::release145–161 ↗
fn release(&mut self)

作用:取消防睡眠状态。也就是把之前启动的后台子进程停掉,让系统恢复正常的空闲睡眠规则。

数据流:进去的是当前防睡眠器的内部状态 → 它把状态取出来并重置为未激活;如果本来就没激活,什么都不做;如果有正在运行的后端子进程,就尝试杀掉它并等待它真正结束 → 出来时对象内部变成未激活,相关子进程也被清理掉;如果清理失败,会写警告日志。

调用关系:外部代码在任务结束时会调用它。LinuxSleepInhibitor::drop 也会调用它,作为最后一道保险,确保对象销毁时不会留下继续阻止睡眠的后台进程。它会用 child_exited 判断某些错误是否只是“进程已经退出了”。

调用图:调用 1 个内部函数(child_exited);被 1 处调用(drop);外部调用 2 个(take, warn!)。

LinuxSleepInhibitor::drop165–167 ↗
fn drop(&mut self)

作用:这是 Rust 的自动清理钩子。防睡眠器对象要被销毁时,它会自动释放防睡眠状态,避免后台进程遗留。

数据流:进去的是即将被销毁的 LinuxSleepInhibitor → 它调用 release 做正式清理 → 出来没有返回值,但会尽力停止之前启动的防睡眠子进程。

调用关系:它不负责具体清理细节,只是把工作交给 LinuxSleepInhibitor::release。这样即使调用方忘了手动 release,程序也还有机会在对象生命周期结束时收尾。

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

spawn_backend170–226 ↗
fn spawn_backend(backend: LinuxBackend) -> Result<Child, std::io::Error>

作用:启动真正向 Linux 系统申请“不要因空闲而睡眠”的外部命令。它根据选择的后端,启动 systemd-inhibit 或 gnome-session-inhibit。

数据流:进去的是一个 LinuxBackend,表示要用哪种系统工具 → 它拼出对应命令,把输入、输出、错误输出都接到空设备,避免干扰主程序;同时在子进程启动前设置“父进程死了就给子进程发 SIGTERM”的规则,SIGTERM 可以理解为请进程停止的信号 → 出来是一个 Child 子进程句柄,或者一个启动失败的系统错误。

调用关系:LinuxSleepInhibitor::acquire 在需要挂起防睡眠牌子时调用它。它是这个文件和 Linux 系统工具之间的桥梁:acquire 负责决定何时启动,spawn_backend 负责把命令真的跑起来。

调用图:被 1 处调用(acquire);外部调用 3 个(null, new, getpid)。

child_exited228–230 ↗
fn child_exited(error: &std::io::Error) -> bool

作用:判断某个操作失败,是不是因为子进程其实已经退出了。这个判断能避免把正常的“已经没了”误报成严重错误。

数据流:进去的是一个输入输出错误对象,也就是系统调用失败时带回来的错误 → 它检查错误类型是否是 InvalidInput,这里被当作“目标子进程已经结束”的信号 → 出来一个布尔值:true 表示可以理解为子进程已退出,false 表示可能是真问题。

调用关系:LinuxSleepInhibitor::acquire 和 LinuxSleepInhibitor::release 在杀进程、等待进程时会用它。它是一个小判断工具,帮助主流程决定要不要写警告日志。

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

tests::sleep_seconds_is_i32_max237–239 ↗
fn sleep_seconds_is_i32_max()

作用:这是一个测试,确认用于保持防睡眠子进程存活的 sleep 秒数,确实等于 32 位整数的最大值。这样可以防止以后有人无意中改坏这个常量。

数据流:进去没有运行时输入 → 它把 BLOCKER_SLEEP_SECONDS 这个字符串常量和 i32::MAX 转成的字符串做比较 → 如果相等测试通过;如果不相等测试失败,提醒开发者这个“睡很久”的数值被改动了。

调用关系:它只在测试时运行,不参与正常防睡眠流程。它保护 spawn_backend 依赖的那个超长 sleep 时间,确保后台阻止器不用频繁重启。

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

utils/sleep-inhibitor/src/macos.rs源码 ↗
domain_logic在一次活跃任务开始到结束之间生效

macOS 不会因为普通程序在运行就一定保持清醒,所以这里要主动向系统申请一张“防睡眠通行证”。文件里的 SleepInhibitor 像一个开关:需要保持清醒时调用 acquire,结束时调用 release。真正和 macOS 打交道的是 MacSleepAssertion,它通过 IOKit(苹果提供的系统电源管理接口)创建一个断言,也就是告诉系统“现在有重要工作”。如果申请失败,程序不会崩溃,只会写一条警告日志。这个设计还有一个重要细节:MacSleepAssertion 被丢弃时会自动释放这张通行证,像借了门禁卡最后自动归还,避免一直阻止电脑睡眠。

函数细节5
SleepInhibitor::new34–36 ↗
fn new() -> Self

作用:创建一个新的防睡眠开关。刚创建时它还没有向 macOS 申请任何东西,只是准备好以后需要时再用。

数据流:进去没有额外输入 → 它生成一个默认的 SleepInhibitor,里面的 assertion 为空 → 出来一个可保存、可后续开启防睡眠的新对象。

调用关系:这是外部代码拿到防睡眠工具的起点。它自己只借用默认初始化,不直接碰 macOS;真正向系统申请防睡眠是在后面的 SleepInhibitor::acquire 里完成。

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

SleepInhibitor::acquire38–54 ↗
fn acquire(&mut self)

作用:开启防睡眠。它会向 macOS 申请一张“系统别睡”的通行证;如果已经申请过,就什么也不做,避免重复申请。

数据流:进去的是一个可修改的 SleepInhibitor → 它先看自己手里有没有现成的 MacSleepAssertion;如果有就直接结束,如果没有就调用 MacSleepAssertion::create 去申请 → 成功后把申请到的通行证存起来,失败后只写警告日志,不让程序崩掉。

调用关系:在任务真正开始、需要电脑保持清醒时会走到这里。它把和 macOS 底层接口打交道的细活交给 MacSleepAssertion::create;如果底层返回错误,它通过 warn! 记日志提醒开发者或用户。

调用图:调用 1 个内部函数(create);外部调用 1 个(warn!)。

SleepInhibitor::release56–58 ↗
fn release(&mut self)

作用:关闭防睡眠。它把之前保存的通行证丢掉,让 macOS 可以按正常规则进入睡眠。

数据流:进去的是一个可修改的 SleepInhibitor → 它把 assertion 设置成空 → 原来的 MacSleepAssertion 如果存在,会因为被丢弃而触发自动释放,最终把系统里的防睡眠申请撤销。

调用关系:通常在一次任务结束、已经不需要强行保持清醒时调用。它本身不直接调用 macOS API,而是依靠 MacSleepAssertion::drop 的自动清理机制完成释放。

MacSleepAssertion::create67–90 ↗
fn create(name: &str) -> Result<Self, IOReturn>

作用:真正向 macOS 申请防睡眠通行证。它把普通文字转换成 macOS 能识别的字符串,然后调用系统的电源管理接口。

数据流:进去的是一段说明文字,比如“Codex 正在执行活跃任务” → 它创建 macOS 使用的 CFString 字符串,把“防止用户空闲导致系统睡眠”这个类型和说明文字交给 IOPMAssertionCreateWithName → 如果系统说成功,就返回带有编号 id 的 MacSleepAssertion;如果失败,就返回系统错误码。

调用关系:它由 SleepInhibitor::acquire 调用,是 acquire 背后真正办手续的人。它会调用 macOS 的 IOPMAssertionCreateWithName;返回的 MacSleepAssertion 以后会由 MacSleepAssertion::drop 负责归还。

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

MacSleepAssertion::drop94–106 ↗
fn drop(&mut self)

作用:在防睡眠通行证不再需要时自动归还给 macOS。这样即使调用者只是把对象丢掉,也不会忘记撤销防睡眠。

数据流:进去的是即将被销毁的 MacSleepAssertion,里面有之前 macOS 给的 id → 它把这个 id 交给 IOPMAssertionRelease → 成功时静默结束,失败时写一条警告日志;对象销毁后,系统里的对应防睡眠申请也应被释放。

调用关系:它不是手动直接调用的普通函数,而是在 MacSleepAssertion 生命周期结束时自动运行。SleepInhibitor::release 把 assertion 清空时会间接触发它;整个 SleepInhibitor 被销毁时,如果还握着通行证,也会靠它收尾。

调用图:外部调用 2 个(IOPMAssertionRelease, warn!)。

utils/sleep-inhibitor/src/windows_inhibitor.rs源码 ↗
domain_logic在 Codex 有活跃任务时启用,任务结束或对象销毁时清理

电脑如果长时间没人动,Windows 可能会自动进入睡眠。对正在运行的 Codex 来说,这会把一次正在进行的工作打断。这个文件用 Windows 自带的电源请求接口,创建一个“系统必须保持唤醒”的请求,但不会强行点亮屏幕。WindowsSleepInhibitor 是外面会用到的简单外壳:new 创建空对象,acquire 开始阻止睡眠,release 结束阻止睡眠。真正和 Windows 打交道的是 PowerRequest,它保存系统给的句柄(可以理解成一张临时凭证)。当 PowerRequest 被丢弃时,Rust 的 Drop 会自动清除请求并关闭句柄,避免系统一直被误拦着不睡。

函数细节5
WindowsSleepInhibitor::new27–29 ↗
fn new() -> Self

作用:创建一个新的 Windows 防睡眠器,一开始还没有向系统申请任何“别睡”的请求。外部代码需要一个可控开关时会先调用它。

数据流:进去没有额外输入 → 它使用默认值建立一个 WindowsSleepInhibitor,里面的 request 是空的 → 出来一个可用于后续 acquire 和 release 的对象,不会立刻改变系统电源状态。

调用关系:这是整个小工具的起点。它只调用默认初始化,之后真正开始防睡眠的动作交给 WindowsSleepInhibitor::acquire。

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

WindowsSleepInhibitor::acquire31–47 ↗
fn acquire(&mut self)

作用:开始阻止 Windows 因为空闲而让系统睡眠。它会避免重复申请:如果已经拿着一个有效请求,就什么也不做。

数据流:进去的是一个可变的防睡眠器对象 → 它先看自己是否已经保存了 PowerRequest;如果没有,就用固定原因文字向 Windows 申请“系统需要保持运行” → 成功后把请求保存起来,失败时只写一条警告日志,不让程序因此崩掉。

调用关系:这是外部在任务开始时会调用的主入口。它把真正调用 Windows 电源接口的工作交给 PowerRequest::new_system_required;如果那一步失败,它调用 warn! 记录原因。

调用图:调用 1 个内部函数(new_system_required);外部调用 1 个(warn!)。

WindowsSleepInhibitor::release49–51 ↗
fn release(&mut self)

作用:结束防睡眠状态,让 Windows 可以按正常规则睡眠。它的作用就是把之前保存的请求丢掉。

数据流:进去的是一个可变的防睡眠器对象 → 它把 request 设为空;如果里面原来有 PowerRequest,那个对象会随之被销毁 → 销毁时会自动清除 Windows 电源请求并关闭系统句柄。

调用关系:这是外部在任务结束时会调用的收尾动作。它本身不直接调用 Windows API,而是依靠 PowerRequest::drop 自动完成底层清理。

PowerRequest::new_system_required61–95 ↗
fn new_system_required(reason: &str) -> Result<Self, String>

作用:真正向 Windows 申请一个“系统需要保持唤醒”的电源请求。它把人能看懂的原因文字交给系统,并拿回一个之后要关闭的句柄。

数据流:进去的是一段原因文字 → 它先把文字转换成 Windows API 需要的宽字符格式,再创建 REASON_CONTEXT(说明申请原因的结构);随后调用 PowerCreateRequest 创建请求句柄,再调用 PowerSetRequest 设置为“系统必需保持运行” → 成功时返回 PowerRequest,失败时返回错误文字,并在必要时关闭已经创建的句柄。

调用关系:它只在 WindowsSleepInhibitor::acquire 需要真正申请防睡眠时被调用。它负责和 PowerCreateRequest、PowerSetRequest、CloseHandle 这些 Windows 系统函数打交道,把复杂细节包装成一个简单的 Result。

调用图:被 1 处调用(acquire);外部调用 7 个(new, last_os_error, format!, once, CloseHandle, PowerCreateRequest, PowerSetRequest)。

PowerRequest::drop99–118 ↗
fn drop(&mut self)

作用:在 PowerRequest 不再需要时自动做清理,撤销“别睡”的请求并关闭 Windows 句柄。Drop 是 Rust 的自动析构机制,可以理解成对象离场时自动打扫卫生。

数据流:进去的是即将被销毁的 PowerRequest,里面有 Windows 句柄和请求类型 → 它先调用 PowerClearRequest 告诉系统这次防睡眠结束了,再调用 CloseHandle 关闭句柄 → 正常情况下没有返回值;如果清除或关闭失败,它会记录警告日志。

调用关系:它通常由 WindowsSleepInhibitor::release 把 request 设为空时触发,也可能在防睡眠器整体被销毁时触发。它不需要别人手动调用,是这个文件防止资源泄漏和误阻止睡眠的安全网。

调用图:外部调用 4 个(last_os_error, warn!, CloseHandle, PowerClearRequest)。

运行时和 UI 支持辅助工具

这些文件提供紧凑的支持工具,用于运行时值转换、回放过滤、帧率限制和轻量缓存。

code-mode/src/runtime/value.rs源码 ↗
utilcross-cutting,主要在代码模式的各类回调处理输出、存取数据、加载数据和异常时使用

这份文件像一个“翻译员”和“安检员”。代码模式里会运行 JavaScript,JavaScript 产生的东西可能是字符串、数字、对象、图片块,也可能是异常。Rust 不能直接把这些东西当成自己的数据用,所以这里负责把 V8(JavaScript 引擎)里的值转成普通文本、serde_json 的 JSON 值,或者协议里规定的图片输出。它还会检查图片是否合规:比如不允许直接返回 http 或 https 的远程图片地址,而是要求用 base64 data URI,这样输出更可控、更安全。遇到不合格的输入,它会把错误变成 JavaScript 的 TypeError(类型错误,意思是“你传的东西形状不对”)抛回去。整体上,它让 JavaScript 世界和 Rust/协议世界之间的数据边界更清楚,避免“看起来能传,实际不能用”的混乱。

函数细节9
serialize_output_text11–37 ↗
fn serialize_output_text(
    scope: &mut v8::PinScope<'_, '_>,
    value: v8::Local<'_, v8::Value>,
) -> Result<String, String>

作用:把 JavaScript 里要输出的值变成一段文本。别人会在文本输出或通知输出时用它,保证不管传进来的是数字、字符串还是对象,最后都尽量变成用户能看的字符串。

数据流:进去的是一个 V8 里的 JavaScript 值和当前运行环境 → 如果它本来就是简单值,比如空值、布尔值、数字或字符串,就直接转成 Rust 字符串;如果是对象,就先尝试用 JSON.stringify 变成 JSON 文本 → 出来的是一段字符串;如果 stringify 过程中 JavaScript 抛异常,就把异常整理成错误文字返回。

调用关系:它会被 notify_callback 和 text_callback 调用,也就是当代码想发通知或输出文本时,这个函数站在边界处把 JavaScript 值翻译成 Rust 文本。遇到复杂对象时,它会借助 V8 的 JSON.stringify;如果 stringify 出错,就用异常信息说明失败原因。

调用图:被 2 处调用(notify_callback, text_callback);外部调用 9 个(is_big_int, is_boolean, is_null, is_number, is_string, is_undefined, to_rust_string_lossy, pin!, stringify)。

normalize_output_image39–96 ↗
fn normalize_output_image(
    scope: &mut v8::PinScope<'_, '_>,
    value: v8::Local<'_, v8::Value>,
    detail_override: Option<String>,
) -> Result<FunctionCallOutputContentItem, ()>

作用:把 JavaScript 传来的图片输出整理成协议规定的图片格式。它会检查图片地址、清洗清晰度选项,并在格式不对时把错误抛回 JavaScript。

数据流:进去的是一个 JavaScript 值,以及可选的图片清晰度覆盖值 → 它接受三类输入:图片 URL 字符串、带 image_url/detail 的普通对象、或者 MCP 图片块;然后检查图片地址不能为空,也不能是 http/https 远程地址;再把 detail 统一成 auto、low、high、original 这些合法值 → 成功时出来的是 FunctionCallOutputContentItem::InputImage;失败时会抛出 TypeError,并返回错误。

调用关系:它会被 generated_image_callback 和 image_callback 调用,也就是 JavaScript 想输出图片时走这里。它是图片输出的总入口,内部会分辨输入形状,最后如果发现问题,会把报错交给 throw_type_error 抛回给 JavaScript 调用者。

调用图:调用 1 个内部函数(throw_type_error);被 2 处调用(generated_image_callback, image_callback)。

parse_non_mcp_output_image98–117 ↗
fn parse_non_mcp_output_image(
    scope: &mut v8::PinScope<'_, '_>,
    object: v8::Local<'_, v8::Object>,
) -> Result<Option<(String, Option<String>)>, String>

作用:解析一种比较简单的图片对象,也就是类似包含 image_url 和可选 detail 的普通 JavaScript 对象。它让用户不用写完整 MCP 图片块,也能方便地输出图片。

数据流:进去的是一个 JavaScript 对象 → 它先找 image_url 字段;如果没有或是 undefined,就表示这不是这种简单图片格式;如果 image_url 不是字符串,就报错;然后读取 detail 字段,并检查它是不是字符串、空值或没给 → 出来的是可选的图片地址和清晰度文字。

调用关系:它是 normalize_output_image 处理对象输入时会用到的第一道尝试。读取 detail 时,它会把这部分检查交给 parse_image_detail_value,这样 detail 的规则集中在一个小函数里。

调用图:调用 1 个内部函数(parse_image_detail_value);外部调用 2 个(get, new)。

parse_mcp_output_image119–164 ↗
fn parse_mcp_output_image(
    scope: &mut v8::PinScope<'_, '_>,
    value: v8::Local<'_, v8::Value>,
) -> Result<(String, Option<String>), String>

作用:解析 MCP 风格的图片块。MCP 可以理解成一种工具通信格式,这里负责把那种标准图片数据转成内部使用的图片地址。

数据流:进去的是一个 JavaScript 值 → 它先把这个值转成 JSON;再确认它是对象、type 字段是 image,并且有非空 data 字段;如果 data 已经是 data: 开头的 data URI,就直接用;否则根据 mimeType 或 mime_type 拼成 data:<类型>;base64,<数据> 的形式;最后还会从 _meta 里读取 Codex 图片清晰度信息 → 出来的是图片地址和可选清晰度。

调用关系:它是 normalize_output_image 在普通 image_url 对象不匹配时的另一条解析路线。它会调用 v8_value_to_json,因为 MCP 图片块更适合先变成 JSON 再按字段读取。

调用图:调用 1 个内部函数(v8_value_to_json);外部调用 1 个(format!)。

parse_image_detail_value166–176 ↗
fn parse_image_detail_value(
    scope: &mut v8::PinScope<'s, '_>,
    value: Option<v8::Local<'s, v8::Value>>,
) -> Result<Option<String>, String>

作用:检查图片 detail 字段是不是一个合适的文字值。它的作用很小但很关键:防止有人把数字、对象之类奇怪的东西当作清晰度传进来。

数据流:进去的是可能存在的 JavaScript 字段值 → 如果是字符串,就转成 Rust 字符串;如果是 null、undefined 或根本没有,就当作没提供;如果是其他类型,就返回错误 → 出来的是可选的清晰度文字,或者一条说明类型不对的错误。

调用关系:它被 parse_non_mcp_output_image 调用,专门负责 detail 字段的类型检查。这样外层解析函数不用把所有细碎规则都写在一起。

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

v8_value_to_json178–196 ↗
fn v8_value_to_json(
    scope: &mut v8::PinScope<'_, '_>,
    value: v8::Local<'_, v8::Value>,
) -> Result<Option<JsonValue>, String>

作用:把 JavaScript 值转成 Rust 里的 JSON 值。它用于那些需要按 JSON 字段读取数据的地方,比如工具调用参数、存储内容或 MCP 图片块。

数据流:进去的是一个 V8 JavaScript 值 → 它先用 JSON.stringify 把值变成 JSON 字符串;如果 stringify 抛异常,就把异常转成错误文字;如果 stringify 没结果,就返回 None;如果有字符串,就用 serde_json 解析成 Rust 的 JsonValue → 出来的是可选 JSON 值,或者解析失败的错误信息。

调用关系:它会被 store_callback、tool_callback 和 parse_mcp_output_image 使用。也就是说,当外层流程需要把 JavaScript 数据当作标准 JSON 来处理时,都会经过这个转换门。

调用图:被 3 处调用(store_callback, tool_callback, parse_mcp_output_image);外部调用 3 个(from_str, pin!, stringify)。

json_to_v8198–205 ↗
fn json_to_v8(
    scope: &mut v8::PinScope<'s, '_>,
    value: &JsonValue,
) -> Option<v8::Local<'s, v8::Value>>

作用:把 Rust 里的 JSON 值转回 JavaScript 能用的值。它用于把已经准备好的数据交还给 JavaScript 代码。

数据流:进去的是一个 serde_json 的 JSON 值 → 它先把 JSON 序列化成字符串,再在 V8 里创建对应的 JavaScript 字符串,最后让 V8 按 JSON 解析成真正的 JavaScript 值 → 出来的是一个 JavaScript 值;如果中间任一步失败,就返回 None。

调用关系:它会被 load_callback 和 resolve_tool_response 调用。也就是当系统从 Rust 侧加载数据,或拿到工具响应后要交给 JavaScript 使用时,这个函数负责把数据送回 JavaScript 世界。

调用图:被 2 处调用(load_callback, resolve_tool_response);外部调用 3 个(to_string, new, parse)。

value_to_error_text207–220 ↗
fn value_to_error_text(
    scope: &mut v8::PinScope<'_, '_>,
    value: v8::Local<'_, v8::Value>,
) -> String

作用:把 JavaScript 抛出的异常值整理成一段适合显示的错误文字。它优先取 stack,也就是错误堆栈,通常比单纯错误名更有帮助。

数据流:进去的是一个 JavaScript 值,通常是异常对象 → 如果它是对象,并且有字符串类型的 stack 字段,就返回这个 stack;否则就把整个值粗略转成字符串 → 出来的是一段错误说明文字。

调用关系:它会被 completion_state 和 evaluate_main_module 调用,用在运行主模块或收尾状态时整理异常。其他转换函数在捕获到 JavaScript 异常时也依赖同类思路,把异常变成外层能记录、能展示的文本。

调用图:被 2 处调用(completion_state, evaluate_main_module);外部调用 4 个(is_object, to_rust_string_lossy, try_from, new)。

throw_type_error222–226 ↗
fn throw_type_error(scope: &mut v8::PinScope<'_, '_>, message: &str)

作用:在 JavaScript 运行环境里抛出一个类型错误。简单说,就是当用户传错参数形状时,用 JavaScript 能理解的方式告诉他“这里不接受这种东西”。

数据流:进去的是当前 V8 运行环境和一段错误消息 → 它把 Rust 字符串变成 V8 字符串,然后作为异常抛进 JavaScript 世界 → 没有正常返回值,但会改变当前执行状态:JavaScript 侧会看到一次异常。

调用关系:它会被 clear_timeout_callback、generated_image_callback、image_callback、load_callback、notify_callback、set_timeout_callback、store_callback、text_callback 等多个回调用到。它是统一的报错出口,让不同回调遇到参数类型错误时都用同一种方式通知 JavaScript。

调用图:被 10 处调用(clear_timeout_callback, generated_image_callback, image_callback, load_callback, notify_callback, set_timeout_callback, store_callback, text_callback, tool_callback, normalize_output_image);外部调用 2 个(throw_exception, new)。

tui/src/app/replay_filter.rs源码 ↗
domain_logicthread switching / event replay

这个文件像一个“筛子”,专门给线程切换时的事件回放用。程序里每个线程都会缓存一些事件,比如服务器发来的通知,或者需要用户点同意、输入内容的请求。切到某个线程时,界面不能盲目回放所有东西:如果里面有还没处理的互动请求,就要特别注意,因为用户可能必须马上做决定;如果只是警告、配置提醒这类 notice(提醒消息),也要能单独识别出来。这里有两个小判断函数:一个检查整份线程快照里有没有“等用户回应”的请求;另一个判断某个事件是不是提醒类通知。它们不负责显示界面,也不改数据,只负责给上层的 replay_thread_snapshot 提供清楚的判断结果。

函数细节2
snapshot_has_pending_interactive_request9–22 ↗
fn snapshot_has_pending_interactive_request(snapshot: &ThreadEventSnapshot) -> bool

作用:检查一个线程的事件快照里,是否藏着还没处理的“需要用户参与”的请求。比如要用户批准执行命令、批准改文件、输入工具所需内容等。

数据流:进去的是一份 ThreadEventSnapshot,也就是某个线程当前缓存事件的快照;函数逐个查看里面的事件,只关心其中的 Request(请求)类型;如果发现请求属于需要用户回应的几类,就返回 true,否则看完都没有就返回 false。它不修改快照,只给出一个是或否的判断。

调用关系:它会在 replay_thread_snapshot 回放线程快照时被使用。上层在准备重新展示某个线程的事件前,先问它:这里面有没有必须让用户处理的东西?这样回放逻辑就能决定是否突出、保留或优先处理这些互动请求。

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

event_is_notice24–33 ↗
fn event_is_notice(event: &ThreadBufferedEvent) -> bool

作用:判断单个缓存事件是不是一条“提醒类通知”。这里的提醒包括普通警告、Guardian 警告和配置警告。

数据流:进去的是一个 ThreadBufferedEvent,也就是缓存中的一条事件;函数用 Rust 的 matches! 宏(按形状检查数据是不是某几种情况的工具)判断它是不是 Notification(通知)里的几类警告;如果是就返回 true,不是就返回 false。它只读取这条事件,不改任何内容。

调用关系:它也被 replay_thread_snapshot 使用。回放线程事件时,上层会拿每条事件来问它:这是不是一条提醒?这个判断能帮助回放流程把提醒消息和其他事件区分开来,避免把警告类信息当成普通对话或请求处理。

调用图:被 1 处调用(replay_thread_snapshot);外部调用 1 个(matches!)。

tui/src/tui/frame_rate_limiter.rs源码 ↗
util绘制调度期间

界面里的小组件有时会不停喊“快重画一帧”。如果每次都立刻画,机器会做很多用户根本看不出来的重复工作。这个文件用一个很小的工具 FrameRateLimiter 来限制刷新速度,最高约 120 FPS,也就是两次真正发出绘制通知之间至少隔 8.33 毫秒。它只记住一件事:上一次真正发出绘制通知是什么时候。外部传进来一个“我想在这个时间画”的时间点,它会判断是不是离上次太近;如果太近,就把时间往后挪到允许的最早时间。真正发出通知后,外部再调用它记录这次时间。文件底部的测试确认了两种情况:一开始不会乱拦截;发过一次后,太早的下一次会被推迟。

函数细节4
FrameRateLimiter::clamp_deadline23–31 ↗
fn clamp_deadline(&self, requested: Instant) -> Instant

作用:这个函数决定一次重画请求最早能在什么时候发生。它会防止两次绘制通知挨得太近,从而把刷新频率压到人眼够用、机器也不浪费的范围。

数据流:进去的是外部希望绘制的时间 requested,以及对象内部记住的上次绘制时间。它先看有没有上次记录;没有就原样放行。有的话,就算出“上次时间加上最小间隔”这个底线,再用 max 取两个时间里更晚的那个。出来的是最终允许的绘制时间;它不会改动对象内部状态。

调用关系:它通常会被异步的帧调度器在安排下一次绘制前调用,像门口保安一样先判断时间合不合适。它内部只借用标准时间比较里的 max 来选较晚时间,不把工作再交给项目里的其他模块。

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

FrameRateLimiter::mark_emitted34–36 ↗
fn mark_emitted(&mut self, emitted_at: Instant)

作用:这个函数用来告诉限速器:“刚刚已经真的发出了一次绘制通知”。没有这一步,限速器就不知道下一次该从什么时候开始算间隔。

数据流:进去的是刚刚发出绘制通知的时间 emitted_at。它把这个时间存进 last_emitted_at,覆盖旧记录。出来没有返回值,但对象内部从“可能不知道上次时间”变成“记住了这次时间”。

调用关系:它应该在一次绘制通知真正发出之后被调用。之后 FrameRateLimiter::clamp_deadline 会读取这条记录,用它来判断后续请求是不是太早。

tests::default_does_not_clamp45–49 ↗
fn default_does_not_clamp()

作用:这个测试确认新建的限速器不会凭空推迟第一次绘制。也就是说,在还没有任何历史记录时,请求什么时间就应该得到什么时间。

数据流:进去的是当前时间 t0,测试通过默认方式创建一个新的 FrameRateLimiter。它把 t0 交给 clamp_deadline,再用断言检查返回值是否还是 t0。结果是测试通过或失败;不会影响真实运行中的程序状态。

调用关系:它位于文件自带的测试模块里,只在运行测试时活跃。它使用系统的 now 取当前时间,用 default 创建默认对象,并用 assert_eq! 检查结果是否符合“第一次不拦截”的规则。

调用图:外部调用 3 个(now, assert_eq!, default)。

tests::clamps_to_min_interval_since_last_emit52–61 ↗
fn clamps_to_min_interval_since_last_emit()

作用:这个测试确认限速器在已经发过一帧后,会把过早的下一帧推迟到最小间隔之后。它证明这个文件真正起到了“限速”的作用。

数据流:进去的是当前时间 t0 和一个新建限速器。测试先确认第一次请求不被推迟,然后调用 mark_emitted 记录 t0 已经发出过一帧。接着造出一个只晚 1 毫秒的请求时间,交给 clamp_deadline,最后检查结果是不是被推到 t0 + MIN_FRAME_INTERVAL。出来的是测试结果:规则对就通过,不对就失败。

调用关系:它同样只在测试时运行。它用 now 取起点时间,用 from_millis 制造“太早”的下一次请求,用 assert_eq! 验证 mark_emittedclamp_deadline 两个核心动作能配合工作。

调用图:外部调用 4 个(from_millis, now, assert_eq!, default)。

utils/cache/src/lib.rs源码 ↗
utilcross-cutting

这个文件像是给项目准备了一个带容量上限的小抽屉。常用结果可以先放进去,下次再要同一个东西就不用重新算;抽屉满了,就扔掉最久没被翻过的那件。核心类型是 BlockingLruCache,它里面包着 LRU 缓存和 Tokio 的互斥锁(互斥锁就是一把锁,防止两个任务同时改同一份数据)。比较特别的是:如果当前代码不在 Tokio 运行时里,它不会真的去碰缓存,而是直接当缓存不存在。这样能避免在普通同步环境里误用异步锁。文件还提供 sha1_digest,用内容算出一个固定长度的指纹,适合拿来当缓存钥匙,避免只按文件路径缓存时内容变了却还拿旧结果。

函数细节15
BlockingLruCache::new23–27 ↗
fn new(capacity: NonZeroUsize) -> Self

作用:创建一个有固定容量的 LRU 缓存。容量必须大于 0,因为一个永远不能放东西的缓存没有意义。

数据流:输入一个非零容量 → 用这个容量创建底层 LRU 缓存 → 再把它放进 Tokio 互斥锁里,输出一个可以安全共享使用的 BlockingLruCache。

调用关系:这是使用缓存前的第一步。测试里的 stores_and_retrieves_values、evicts_least_recently_used 和 disabled_without_runtime 都先调用它来搭一个缓存样品。

调用图:被 3 处调用(disabled_without_runtime, evicts_least_recently_used, stores_and_retrieves_values);外部调用 2 个(new, new)。

BlockingLruCache::get_or_insert_with30–44 ↗
fn get_or_insert_with(&self, key: K, value: impl FnOnce() -> V) -> V

作用:按钥匙取缓存值;如果没有,就现场算一个并放进去。适合“结果可能重复用,重新计算又有点贵”的场景。

数据流:输入一个 key 和一个能生成 value 的函数 → 先通过 lock_if_runtime 尝试进入缓存 → 如果已有值,就克隆一份返回;如果没有,就运行生成函数、存入缓存、返回新值;如果没有 Tokio 运行时,就只计算并返回,不保存。

调用关系:它是缓存最常见的入口之一,把“查、没有就算、算完再存”这三步包成一次操作。真正能不能访问缓存,由 lock_if_runtime 决定。

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

BlockingLruCache::get_or_try_insert_with47–64 ↗
fn get_or_try_insert_with(
        &self,
        key: K,
        value: impl FnOnce() -> Result<V, E>,
    ) -> Result<V, E>

作用:和 get_or_insert_with 类似,但生成值的过程可能失败。比如读文件、解析内容、请求外部资源时,就需要把错误传回去。

数据流:输入 key 和一个返回 Result 的生成函数 → 能锁缓存时先查已有值;命中就返回 Ok(值);没命中就调用生成函数,成功则存入缓存并返回 Ok,失败则直接返回错误;没有 Tokio 运行时则只运行生成函数,不缓存。

调用关系:它服务于“缓存可失败计算结果”的流程。它同样依赖 lock_if_runtime 来判断缓存是否可用,不自己处理锁的细节。

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

BlockingLruCache::try_with_capacity68–70 ↗
fn try_with_capacity(capacity: usize) -> Option<Self>

作用:用普通数字创建缓存,但如果容量是 0,就返回 None 表示不创建。这样配置里写 0 时可以自然表示“关闭缓存”。

数据流:输入一个 usize 容量数字 → 尝试把它变成非零容量 → 如果成功就创建 BlockingLruCache;如果是 0,就输出 None。

调用关系:这是更适合配置文件或命令行参数的构造入口。它把合法容量交给 BlockingLruCache::new,自己只负责处理 0 这个特殊情况。

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

BlockingLruCache::get73–81 ↗
fn get(&self, key: &Q) -> Option<V>

作用:只查询缓存里有没有某个 key 对应的值,不会自己计算新值。找到就返回克隆出来的一份。

数据流:输入一个 key 的引用 → 通过 lock_if_runtime 尝试拿到缓存 → 拿到后查找这个 key;命中则返回 Some(值的克隆),没命中或没有运行时则返回 None。

调用关系:这是最直接的读取接口。测试用它确认 insert 后能读到值,也确认缓存满时旧值会被淘汰。

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

BlockingLruCache::insert84–87 ↗
fn insert(&self, key: K, value: V) -> Option<V>

作用:手动把一个 key 和 value 放进缓存。如果这个 key 以前有旧值,就把旧值还给调用者。

数据流:输入 key 和 value → 通过 lock_if_runtime 尝试进入缓存 → 成功后写入新值,并返回被替换掉的旧值;如果没有 Tokio 运行时,就什么也不存,返回 None。

调用关系:这是手动填缓存的入口。测试用它放入数据,再用 get 检查读取和淘汰行为。

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

BlockingLruCache::remove90–97 ↗
fn remove(&self, key: &Q) -> Option<V>

作用:从缓存里删掉某个 key。适合知道某份缓存已经过期或不可信时手动清理。

数据流:输入一个 key 的引用 → 通过 lock_if_runtime 尝试进入缓存 → 如果找到对应条目,就移除并返回它;找不到或没有运行时,就返回 None。

调用关系:它是单条删除接口,和 insert、get 一起组成基本的缓存增删查能力。是否能真的删除,仍由 lock_if_runtime 决定。

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

BlockingLruCache::clear100–104 ↗
fn clear(&self)

作用:清空整个缓存。适合配置变化、数据整体失效,或者想释放缓存内容的时候使用。

数据流:没有业务输入 → 通过 lock_if_runtime 尝试拿到缓存 → 如果拿到了,就把所有条目删光;如果没有运行时,就不做任何事。

调用关系:它是批量清理入口。它不直接碰锁,而是把“能不能安全访问缓存”的判断交给 lock_if_runtime。

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

BlockingLruCache::with_mut107–114 ↗
fn with_mut(&self, callback: impl FnOnce(&mut LruCache<K, V>) -> R) -> R

作用:给调用者一个临时机会,直接操作底层 LRU 缓存。适合普通接口不够用、需要做更细的缓存操作时使用。

数据流:输入一个回调函数 → 如果能进入真实缓存,就把真实缓存的可修改引用交给回调;如果没有 Tokio 运行时,就临时建一个不限制大小的假缓存给回调用 → 返回回调的结果。没有运行时时,回调里写入的东西不会保存到真实缓存。

调用关系:这是一个“高级接口”。它仍先调用 lock_if_runtime;如果缓存不可用,就用临时 unbounded 缓存保证回调还能运行,但不会留下持久效果。

调用图:调用 1 个内部函数(lock_if_runtime);外部调用 1 个(unbounded)。

BlockingLruCache::blocking_lock117–119 ↗
fn blocking_lock(&self) -> Option<MutexGuard<'_, LruCache<K, V>>>

作用:把底层缓存的锁直接交给调用者。适合少数需要自己连续做多步缓存操作的代码。

数据流:没有业务输入 → 调用 lock_if_runtime → 如果当前有 Tokio 运行时,就返回缓存锁的守卫;否则返回 None。

调用关系:这是最底层的公开访问口。它几乎不做额外包装,只把工作交给 lock_if_runtime,所以调用者需要更清楚自己在做什么。

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

lock_if_runtime122–128 ↗
fn lock_if_runtime(m: &Mutex<LruCache<K, V>>) -> Option<MutexGuard<'_, LruCache<K, V>>>

作用:判断当前是否处在 Tokio 运行时里;只有是,才去锁住缓存。它是这个文件“有运行时才启用缓存”的开关。

数据流:输入一个 Tokio 互斥锁包住的 LRU 缓存 → 先尝试取得当前 Tokio 运行时句柄 → 成功后用 block_in_place 进入可阻塞的小区域并拿锁 → 输出锁守卫;如果没有运行时,输出 None。

调用关系:它被 get、insert、remove、clear、get_or_insert_with、get_or_try_insert_with、with_mut 和 blocking_lock 共同调用。也就是说,几乎所有缓存操作都先问它:“现在能安全碰缓存吗?”

调用图:被 8 处调用(blocking_lock, clear, get, get_or_insert_with, get_or_try_insert_with, insert, remove, with_mut);外部调用 2 个(try_current, block_in_place)。

sha1_digest135–142 ↗
fn sha1_digest(bytes: &[u8]) -> [u8; 20]

作用:把一段字节内容算成 SHA-1 指纹。SHA-1 是一种摘要算法,可以把任意长度内容变成 20 字节的固定标识。

数据流:输入一串 bytes → 创建 SHA-1 计算器,把 bytes 喂进去 → 得到 20 字节结果并复制到固定数组里 → 返回这个数组。

调用关系:它不依赖 BlockingLruCache,但常和缓存配合使用:用内容指纹当 key,比只用文件路径更不容易拿到过期缓存。

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

tests::stores_and_retrieves_values150–156 ↗
async fn stores_and_retrieves_values()

作用:测试缓存能不能正常存入再读出。它验证最基本的“放进去的东西还能拿出来”。

数据流:先创建容量为 2 的缓存 → 查询 first,确认一开始没有 → 插入 first=1 → 再查询 first,确认返回 Some(1)。

调用关系:这是测试运行器调用的异步测试。它先用 BlockingLruCache::new 建缓存,然后间接覆盖 insert 和 get 的基本行为。

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

tests::evicts_least_recently_used159–170 ↗
async fn evicts_least_recently_used()

作用:测试 LRU 淘汰规则是否正确:缓存满了以后,最久没用的那条会被丢掉。

数据流:创建容量为 2 的缓存 → 放入 a 和 b → 读取 a,让 a 变成最近用过 → 再放入 c,缓存超容量 → 检查 b 被淘汰,而 a 和 c 还在。

调用关系:这是测试运行器调用的异步测试。它通过 new、insert、get 和断言,确认底层 LRU 缓存的核心规则没有被包装层破坏。

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

tests::disabled_without_runtime173–192 ↗
fn disabled_without_runtime()

作用:测试没有 Tokio 运行时时,缓存确实不会真正工作。这样能保证普通同步代码里调用这些接口不会偷偷保存状态。

数据流:创建缓存但不进入 Tokio 运行时 → 尝试 insert 后再 get,确认读不到 → get_or_insert_with 只返回计算值但不保存 → remove、clear 不产生持久效果 → with_mut 只操作临时缓存 → blocking_lock 返回 None。

调用关系:这是普通同步测试,由测试运行器调用。它专门验证 lock_if_runtime 的“没有运行时就关闭缓存”这个设计,覆盖多个公开接口在禁用状态下的表现。

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

沙箱摘要和 V8 探测

这些文件打包人类可读的沙箱/配置摘要,以及一个用于验证 V8 链接和行为的小型概念验证 crate。

utils/sandbox-summary/src/lib.rs源码 ↗
orchestrationcross-cutting

这个文件解决的是“别人怎么方便地使用这个库”的问题。库里面真正干活的代码分在两个文件里:一个偏配置摘要,一个偏沙箱策略摘要。这里先声明这两个内部模块存在,然后把几个主要函数重新导出。重新导出可以理解成商店把仓库里的常用商品摆到前台:使用者不需要知道商品原来放在哪个货架,只要从这个库的入口拿就行。它导出的能力包括:生成配置摘要条目、总结权限配置、总结沙箱策略。没有这个文件,外部代码可能必须知道内部文件名和模块层级,使用起来更麻烦,也更容易被内部结构变化影响。

utils/sandbox-summary/src/config_summary.rs源码 ↗
utilstartup / config display

这个文件像一张“开工前检查单”。程序真正运行时,配置可能来自默认值、配置文件、命令行参数等很多地方,用户很难一眼看出最后到底采用了什么。这里的 create_config_summary_entries 会接收已经算好的 Config,把关键内容挑出来,变成一串容易展示的键值对,比如工作目录、模型名、服务提供方、审批策略,以及沙箱策略。沙箱可以理解成“安全围栏”,限制程序能碰哪些文件、能做哪些事。文件还会根据模型服务使用的接口类型,额外加入“推理强度”和“推理摘要”这些只对 Responses 接口有意义的信息。这样,上层界面或工具只要拿到这份列表,就能直接打印或展示配置摘要。

函数细节1
create_config_summary_entries7–44 ↗
fn create_config_summary_entries(config: &Config, model: &str) -> Vec<(&'static str, String)>

作用:这个函数把一份已经生效的配置变成一组适合展示给用户看的文字条目。别人调用它,是为了生成“当前程序会怎么运行”的摘要,而不是自己到配置对象里逐项翻找。

数据流:进去的是 Config 配置对象和一个模型名称字符串。函数先读取工作目录、模型名、模型提供方、审批策略,再把沙箱策略交给 summarize_sandbox_policy 转成好懂的文字;如果当前模型提供方使用 Responses 这种接口,它还会读取推理强度和推理摘要,没有设置就写成“none”。最后出来的是一个列表,每一项都是固定标题加对应文字内容,函数本身不修改配置。

调用关系:它处在“把内部配置翻译成人能看的摘要”的位置。上层代码在需要展示配置概览时会调用它;它自己主要把条目装进一个列表,并把沙箱策略的解释交给 summarize_sandbox_policy 这个更专门的函数来完成。代码里使用 vec! 只是 Rust 创建列表的常用工具。

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

utils/sandbox-summary/src/sandbox_summary.rs源码 ↗
utilcross-cutting

沙箱可以理解成给程序活动范围画的一道安全线:有的模式完全放开,有的只能读,有的只能在工作目录里写。这个文件不负责真正拦截文件或网络访问,它只负责“说清楚现在是什么权限”。核心函数会根据不同的沙箱策略生成类似 read-onlyworkspace-write [workdir, /tmp]network access enabled 这样的摘要。另一个函数会先把更高层的权限配置 PermissionProfile 转成旧的沙箱策略;如果能转成功,就按常规方式说明;如果是无法归类的自定义权限,就退回到 custom permissions。这里还有一个重要细节:展示 workspace-write 时,会优先显示运行时真正的工作区根目录,并避免把内部隐藏写入路径误展示给用户。

函数细节7
summarize_sandbox_policy6–52 ↗
fn summarize_sandbox_policy(sandbox_policy: &SandboxPolicy) -> String

作用:把一个具体的沙箱策略变成简短文字,方便给用户看。比如把“只读但允许联网”说成 read-only (network access enabled)

数据流:输入是一份 SandboxPolicy,也就是程序当前被允许做什么的规则。它先判断规则属于哪一类:完全放开、只读、外部沙箱、还是工作区可写;然后拼出对应的文字,并在允许联网时加上联网提示。输出是一段摘要字符串,不会改动原来的策略。

调用关系:这是最底层的摘要生成器。summarize_permission_profile 在把权限配置转换成普通沙箱策略后会调用它;多个测试函数也直接调用它,确认不同沙箱模式显示出来的文字没有写错。

调用图:被 5 处调用(summarize_permission_profile, summarizes_external_sandbox_with_enabled_network, summarizes_external_sandbox_without_network_access_suffix, summarizes_read_only_with_enabled_network, workspace_write_summary_still_includes_network_access);外部调用 3 个(new, format!, matches!)。

summarize_permission_profile54–96 ↗
fn summarize_permission_profile(
    permission_profile: &PermissionProfile,
    cwd: &AbsolutePathBuf,
    workspace_roots: &[AbsolutePathBuf],
) -> String

作用:把更高层的权限配置整理成用户能读懂的摘要。它适合在只知道 PermissionProfile,还不知道最终该怎么展示时使用。

数据流:输入包括权限配置、当前工作目录 cwd,以及运行时的工作区根目录列表。它先尝试把权限配置转成旧版沙箱策略;如果是工作区可写模式,就用实际工作区根目录来拼展示文字,并跳过当前目录本身;如果是其他普通策略,就交给 summarize_sandbox_policy;如果转换失败,就输出 custom permissions,并按需要附加联网提示。

调用关系:它位于权限配置和展示文字之间,像一个翻译中转站。测试 tests::permission_profile_summary_uses_runtime_workspace_roots_and_hides_internal_writes 会调用它,验证它确实使用运行时工作区根目录,而不是把内部隐藏路径暴露出来。

调用图:调用 4 个内部函数(network_sandbox_policy, to_legacy_sandbox_policy, as_path, summarize_sandbox_policy);被 1 处调用(permission_profile_summary_uses_runtime_workspace_roots_and_hides_internal_writes);外部调用 3 个(format!, iter, vec!)。

tests::summarizes_external_sandbox_without_network_access_suffix106–111 ↗
fn summarizes_external_sandbox_without_network_access_suffix()

作用:这个测试确认外部沙箱在没有明确允许联网时,不会多显示“允许联网”的字样。这样可以避免用户误以为网络是开放的。

数据流:它构造一个网络受限的 ExternalSandbox 策略,交给 summarize_sandbox_policy。函数返回摘要后,测试把结果和期望的 external-sandbox 对比;如果多了或少了文字,测试就会失败。

调用关系:这是针对 summarize_sandbox_policy 的一个边界检查。它专门守住“网络受限时不要显示联网提示”这个展示规则。

调用图:调用 1 个内部函数(summarize_sandbox_policy);外部调用 1 个(assert_eq!)。

tests::summarizes_external_sandbox_with_enabled_network114–119 ↗
fn summarizes_external_sandbox_with_enabled_network()

作用:这个测试确认外部沙箱在允许联网时,会明确显示网络已开启。这样用户不会漏看一个重要的安全信息。

数据流:它构造一个网络开启的 ExternalSandbox 策略,调用 summarize_sandbox_policy 得到文字,再检查结果必须是 external-sandbox (network access enabled)

调用关系:它和前一个外部沙箱测试是一组相反情况:一个检查不该显示时不显示,一个检查该显示时一定显示。

调用图:调用 1 个内部函数(summarize_sandbox_policy);外部调用 1 个(assert_eq!)。

tests::summarizes_read_only_with_enabled_network122–127 ↗
fn summarizes_read_only_with_enabled_network()

作用:这个测试确认“只读”沙箱如果允许联网,摘要里也会写出来。因为只读只限制文件写入,不等于不能访问网络。

数据流:它创建一个 ReadOnly 策略,并把 network_access 设为 true。然后调用 summarize_sandbox_policy,检查输出是否包含 read-only 和联网开启提示。

调用关系:它继续验证 summarize_sandbox_policy 对不同沙箱类型的一致处理:只要网络开启,就要在摘要里提醒用户。

调用图:调用 1 个内部函数(summarize_sandbox_policy);外部调用 1 个(assert_eq!)。

tests::workspace_write_summary_still_includes_network_access130–146 ↗
fn workspace_write_summary_still_includes_network_access()

作用:这个测试确认工作区可写模式在允许联网时,摘要不会漏掉联网提示。同时也检查可写路径列表会被正确放进文字里。

数据流:它先按不同操作系统准备一个路径,把它转成绝对路径;然后构造 WorkspaceWrite 策略,设置为允许联网,但排除 /tmp$TMPDIR。调用 summarize_sandbox_policy 后,它检查输出是否只列出 workdir 和指定根目录,并带上联网开启提示。

调用关系:这是 summarize_sandbox_policy 的综合测试,覆盖了工作区可写、路径展示、临时目录排除、联网提示这些规则如何一起生效。

调用图:调用 2 个内部函数(try_from, summarize_sandbox_policy);外部调用 3 个(assert_eq!, cfg!, vec!)。

tests::permission_profile_summary_uses_runtime_workspace_roots_and_hides_internal_writes149–181 ↗
fn permission_profile_summary_uses_runtime_workspace_roots_and_hides_internal_writes()

作用:这个测试确认权限配置的摘要会展示用户关心的工作区根目录,而不是暴露内部用的隐藏写入目录。它保护的是展示结果的清晰度和安全感。

数据流:它准备当前目录、一个额外工作区根目录、一个内部隐藏目录,再创建一个 workspace-write 权限配置。随后调用 summarize_permission_profile,传入运行时工作区根目录列表。最后检查输出包含 workdir、临时目录和额外根目录,但不包含那个内部隐藏目录。

调用关系:这是专门验证 summarize_permission_profile 的测试。它说明这个函数不是简单照搬底层权限数据,而是会按运行时上下文整理出更适合给用户看的摘要。

调用图:调用 3 个内部函数(workspace_write_with, try_from, summarize_permission_profile);外部调用 3 个(assert_eq!, cfg!, from_ref)。

v8-poc/src/lib.rs源码 ↗
domain_logiccross-cutting

这个文件像一块“连线测试板”:它本身不做复杂业务,而是确认 Rust 代码、Bazel 构建、V8 JavaScript 引擎三者真的接通了。上半部分有三个公开函数:一个返回这个库在 Bazel 里的标签,一个问 V8 自己的版本号,一个通过底层 C 接口检查 V8 是否启用了进程内沙箱(沙箱可以理解成给危险代码加的一圈护栏)。下半部分是测试代码,会先只初始化一次 V8,然后真的创建一个 V8 运行环境,执行简单的 JavaScript 表达式,比如“1 + 2”和字符串拼接。它还测试了 CRDTP 消息解析,CRDTP 是 Chrome 调试协议用的一种消息格式。没有这个文件,项目可能能编译一部分,但没人能快速证明 V8 这条链路真的可用。

函数细节11
bazel_target5–7 ↗
fn bazel_target() -> &'static str

作用:返回这个小库在 Bazel 构建系统里的固定名字。别人可以用它确认当前代码对应的是哪个构建目标,避免连错库或测错目标。

数据流:它不接收任何输入,也不读取外部状态;调用后直接给出一段固定文字“//codex-rs/v8-poc:v8-poc”;它不修改任何东西。

调用关系:这是最简单的公开查询函数。测试里的 tests::exposes_expected_bazel_target 会调用它,检查这个对外暴露的 Bazel 标签没有被意外改掉。

embedded_v8_version11–13 ↗
fn embedded_v8_version() -> &'static str

作用:向已经链接进来的 V8 引擎询问版本号。这样可以确认程序实际用的是哪一版 V8,而不是只看配置文件猜测。

数据流:它不接收输入;它把问题交给 V8 的 get_version 接口;最后返回一个版本号字符串,不改动程序状态。

调用关系:这是对 V8 官方接口的一层很薄包装。测试里的 tests::exposes_embedded_v8_version 会调用它,确认返回值不是空的,说明 V8 版本信息能正常读到。

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

linked_v8_has_sandbox17–24 ↗
fn linked_v8_has_sandbox() -> bool

作用:检查当前真正链接到程序里的 V8 库是否启用了沙箱。沙箱就是一层安全隔离,能降低某些内存错误带来的风险。

数据流:它不接收输入;它声明并调用一个来自底层 V8/Rusty V8 的 C 风格函数,询问沙箱是否开启;最后返回 true 或 false,不改动其他数据。

调用关系:这个函数用来把“构建配置里说启用了什么”和“实际链接的 V8 具有什么能力”对上号。测试里的 tests::sandbox_feature_matches_linked_v8 会拿它和 Rust 的 feature 配置做比较。

tests::initialize_v833–40 ↗
fn initialize_v8()

作用:在测试开始执行 JavaScript 之前,把 V8 引擎启动起来。它用 Once 保证只初始化一次,避免重复启动造成混乱。

数据流:它不接收输入;第一次被调用时会创建默认的 V8 平台并初始化 V8,之后再被调用就什么也不重复做;结果是全局 V8 环境准备好了。

调用关系:它是测试里运行 V8 的前置开关。tests::evaluate_expression 会先调用它,再去创建隔离环境、上下文和脚本。

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

tests::evaluate_expression42–55 ↗
fn evaluate_expression(expression: &str) -> String

作用:在测试中执行一小段 JavaScript 表达式,并把结果转成 Rust 字符串。它用来证明 V8 不只是能链接,还真的能跑代码。

数据流:输入是一段 JavaScript 表达式文本;它先确保 V8 已初始化,再创建一个隔离的 V8 执行环境、上下文、字符串源码和脚本,然后编译并运行;最后把运行结果转成普通 Rust 字符串返回。

调用关系:这是多个 V8 功能测试共用的小工具。tests::evaluates_integer_addition 和 tests::evaluates_string_concatenation 都通过它运行表达式,再用断言检查结果是否正确。

调用图:外部调用 8 个(default, initialize_v8, new, new, new, compile, new, scope!)。

tests::exposes_expected_bazel_target58–60 ↗
fn exposes_expected_bazel_target()

作用:检查 bazel_target 返回的构建目标名字是不是预期值。这样可以防止这个库的 Bazel 标识被无意改坏。

数据流:它不接收输入;它调用 bazel_target 拿到实际字符串,再和写死的预期字符串比较;如果不同,测试失败,不产生正常业务输出。

调用关系:这是针对 bazel_target 的保护性测试。它用 assert_eq! 做比较,发现不一致时会让测试套件报警。

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

tests::exposes_embedded_v8_version63–65 ↗
fn exposes_embedded_v8_version()

作用:确认 embedded_v8_version 能拿到一个非空的 V8 版本号。重点不是验证具体版本,而是证明版本查询通路通了。

数据流:它不接收输入;它调用 super::embedded_v8_version 取得版本字符串,再检查这个字符串不是空的;如果为空,测试失败。

调用关系:这是 embedded_v8_version 的基本连通性测试。它通过 assert! 表达“只要能读到版本信息就算通过”。

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

tests::sandbox_feature_matches_linked_v868–70 ↗
fn sandbox_feature_matches_linked_v8()

作用:确认 Rust 编译时声明的 sandbox 功能,和实际链接的 V8 沙箱状态一致。这样能避免配置写着开启,结果实际库没开启的危险错配。

数据流:它不接收输入;它调用 super::linked_v8_has_sandbox 读取实际 V8 状态,再和 cfg!(feature = "sandbox") 这个编译期配置比较;两者不一致时测试失败。

调用关系:这是 linked_v8_has_sandbox 的一致性测试。它把底层 V8 的真实能力和 Rust feature 配置连接起来检查。

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

tests::evaluates_integer_addition73–75 ↗
fn evaluates_integer_addition()

作用:测试 V8 能正确执行最简单的整数加法。它是一个小而直接的冒烟测试,证明 JavaScript 执行链路能跑通。

数据流:它把字符串“1 + 2”交给 tests::evaluate_expression;拿到返回结果后和“3”比较;如果不是“3”,测试失败。

调用关系:它依赖 tests::evaluate_expression 完成真正的 V8 初始化、编译和运行工作,自己只负责给一个例子并检查答案。

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

tests::evaluates_string_concatenation78–80 ↗
fn evaluates_string_concatenation()

作用:测试 V8 能正确执行 JavaScript 字符串拼接。它补充验证 V8 不只会算数字,也能处理字符串。

数据流:它把表达式“'hello ' + 'world'”交给 tests::evaluate_expression;得到结果后和“hello world”比较;不一致就让测试失败。

调用关系:它和 tests::evaluates_integer_addition 一样,是 tests::evaluate_expression 的使用者,只是换成了字符串场景。

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

tests::parses_crdtp_dispatchable_messages83–91 ↗
fn parses_crdtp_dispatchable_messages()

作用:测试 V8 附带的 CRDTP 工具能把一段调试协议 JSON 消息转成内部格式,并正确读出消息编号和方法名。CRDTP 可以理解成 Chrome 调试工具之间传纸条的格式。

数据流:它先准备一段 JSON 字节内容,里面有 id、method 和 params;再用 json_to_cbor 把 JSON 转成 CBOR(二进制数据格式),然后创建 Dispatchable 消息对象;最后检查消息有效、编号是 7、方法名是 Runtime.evaluate。

调用关系:这是对 V8 调试协议辅助功能的集成测试。它把 json_to_cbor 和 Dispatchable::new 串起来,证明这套消息解析工具在当前链接的 V8 里可用。

调用图:外部调用 4 个(assert!, assert_eq!, new, json_to_cbor)。