通用 HTTP 客户端、TLS、cookies 和流式传输基础
这一层是全系统出门上网的“水电管线”,不算主流程,却一直在幕后支撑。request/transport把内部请求包成HTTP并发出去,retry负责失败后等一等再试,custom_ca和受限Cookie让企业证书、Cloudflare验证能安全通过。各类默认客户端统一加身份、请求头和日志。SSE、WebSocket负责拆流式消息。provider、session和目录文件把地址、认证、重试、上传、远端配置等能力集中给大家用。
传输基础组件
这些文件定义共享请求模型、重试行为、具体的 reqwest 传输、SSE 解码,以及公开它们的 crate 门面。
codex-client/src/request.rs源码 ↗
可以把这个文件理解成“发快递前的打包台”。外面的人先创建一个 Request,填上请求方法、网址、请求头和正文。正文可能是 JSON,也可能是已经准备好的原始字节。真正发送前,它会把 JSON 序列化(把结构化数据变成一串字节)、按需要用 zstd 压缩(一种压缩算法)、补上 Content-Type 和 Content-Encoding 这些 HTTP 头。特别重要的是,into_prepared 会把最终要发出的字节存起来,之后重试或做请求签名时看到的都是同一份内容,不会出现“签的是一份,发的是另一份”的问题。文件末尾的测试检查了 JSON 会被正确打包、压缩头冲突会报错、压缩后的正文能被复用。
EncodedJsonBody::encode23–29 ↗
fn encode(value: &T) -> Result<Self, serde_json::Error>
作用:把任意可序列化的数据提前转成 JSON 字节,并装进一个可以便宜克隆的容器里。这样后面重试请求时,不用一遍遍重新把 JSON 变成字节。
数据流:输入是一份可以转成 JSON 的数据 → 函数用 serde_json 把它变成字节数组,再放进 Bytes 这种可共享的字节容器 → 输出 EncodedJsonBody;如果 JSON 转换失败,就返回错误。
调用关系:它是准备 JSON 正文的入口之一。流式请求、普通请求发送前准备、以及测试都会用它;Request::prepare_body_for_send 遇到还没编码的 JSON 时,也会把活交给它。
调用图:被 4 处调用(stream, stream_request, prepare_body_for_send, into_prepared_stores_compressed_body_for_reuse);外部调用 1 个(to_vec)。
EncodedJsonBody::as_bytes32–34 ↗
fn as_bytes(&self) -> &[u8]
作用:取出当前保存的正文 bytes,给后续压缩或发送使用。它不复制正文,只是借出来看。
数据流:输入是一个 EncodedJsonBody → 函数直接返回里面保存的字节切片 → 不产生新数据,也不改动原对象。
调用关系:Request::prepare_encoded_json 在需要压缩 JSON 正文时会调用它,把这些字节交给 zstd 压缩器。
调用图:被 1 处调用(prepare_encoded_json)。
EncodedJsonBody::trace_bytes36–38 ↗
fn trace_bytes(&self) -> &[u8]
作用:拿到用于跟踪日志的正文内容。压缩发送时,它可以保留压缩前的 JSON,方便日志里看到人能读懂的内容。
数据流:输入是一个 EncodedJsonBody → 如果里面有专门保存的 trace_bytes,就返回它;否则返回当前正文 bytes → 不修改任何数据。
调用关系:它服务于请求正文的调试追踪。正常发送链路主要用 as_bytes;需要打印更友好的调试内容时,会通过这个函数拿日志用的版本。
RequestBody::json56–61 ↗
fn json(&self) -> Option<&Value>
作用:判断这个请求正文是不是还保持为 JSON 值,并在是的时候把它借出来。调用者可以用它安全地查看 JSON 正文。
数据流:输入是一个 RequestBody → 如果它是 Json,就返回里面的 JSON 引用;如果已经编码成字节或是原始字节,就返回 None → 不改动正文。
调用关系:它是给外部代码检查请求正文形态的小工具。它不会参与发送打包,只提供“这是不是 JSON”的温和访问方式。
PreparedRequestBody::body_bytes71–73 ↗
fn body_bytes(&self) -> Bytes
作用:取出已经准备好的请求正文字节;如果没有正文,就给一份空字节。这样调用者不用自己处理空值。
数据流:输入是 PreparedRequestBody → 函数复制一份 Bytes 句柄;有正文就返回正文,没有正文就返回空 Bytes → 原对象不变。
调用关系:它通常给签名、发送或测试代码使用。PreparedRequestBody 是 Request::prepare_body_for_send 的结果,这个函数让后续步骤能直接拿到最终字节。
Request::new87–96 ↗
fn new(method: Method, url: String) -> Self
作用:创建一个最基础的 HTTP 请求对象,只填请求方法和网址,其他内容先留空。它是拼装请求的起点。
数据流:输入是 HTTP 方法和 URL 字符串 → 函数创建空请求头、空正文、不压缩、无超时的 Request → 输出这份默认请求。
调用关系:很多地方都会先调用它,再链式加 JSON、原始正文或压缩设置。测试和配置加载相关代码也用它来构造请求样本。
调用图:被 6 处调用(into_prepared_stores_compressed_body_for_reuse, prepare_body_for_send_rejects_existing_content_encoding_when_compressing, prepare_body_for_send_serializes_json_and_sets_content_type, load_thread_config_request, direct_connector_allows_non_public_target_when_local_binding_enabled, direct_connector_rejects_non_public_target_when_local_binding_disabled);外部调用 1 个(new)。
Request::with_json98–101 ↗
fn with_json(mut self, body: &T) -> Self
作用:给请求加上一份 JSON 正文。它让调用者不用手动处理 JSON Value,直接把普通数据塞进请求里。
数据流:输入是已有 Request 和一份可序列化数据 → 函数尝试把数据转成 serde_json::Value → 成功就把它放进 RequestBody::Json,失败就不放正文;最后返回更新后的 Request。
调用关系:它通常接在 Request::new 后面使用。之后真正发送前,Request::prepare_body_for_send 会再把这份 JSON 变成最终字节。
调用图:外部调用 1 个(to_value)。
Request::with_raw_body103–106 ↗
fn with_raw_body(mut self, body: impl Into<Bytes>) -> Self
作用:给请求加上一份已经是字节形式的原始正文。适合调用者自己已经准备好了要发送的内容,不需要这里再转 JSON。
数据流:输入是已有 Request 和一段可转成 Bytes 的数据 → 函数把它变成 Bytes,并包装成 RequestBody::Raw → 返回带原始正文的新 Request。
调用关系:它也是 Request::new 后的拼装步骤之一。后面 Request::prepare_body_for_send 会直接使用这份字节,但如果同时要求压缩,会报错,因为原始正文不由这里解释和改写。
调用图:外部调用 2 个(into, Raw)。
Request::with_compression108–111 ↗
fn with_compression(mut self, compression: RequestCompression) -> Self
作用:给请求设置正文压缩方式,比如 zstd。它只是登记“之后发送前要压缩”,不会立刻压缩。
数据流:输入是已有 Request 和压缩选项 → 函数把 compression 字段改成指定值 → 返回更新后的 Request。
调用关系:它常在构造请求时使用。真正压缩发生在 Request::prepare_encoded_json 里,由 Request::prepare_body_for_send 或 Request::into_prepared 间接触发。
Request::into_prepared118–149 ↗
fn into_prepared(mut self) -> Result<Self, String>
作用:把一个请求彻底准备好,并把最终会发送的正文和请求头存回请求里。这样后续克隆、重试、签名看到的都是同一份最终结果。
数据流:输入是一个 Request → 函数判断正文是不是 JSON,必要时为跟踪日志保留压缩前字节,然后调用 prepare_body_for_send 得到最终请求头和正文字节 → 输出更新后的 Request;压缩标记会被清掉,因为正文已经处理完了。
调用关系:它是“发送前定稿”的步骤。它内部把关键工作交给 Request::prepare_body_for_send;完成后,传输层或重试逻辑可以放心复用这份请求。
调用图:调用 1 个内部函数(prepare_body_for_send);外部调用 6 个(from, EncodedJson, Raw, matches!, to_vec, enabled!)。
Request::prepare_body_for_send156–178 ↗
fn prepare_body_for_send(&self) -> Result<PreparedRequestBody, String>
作用:把请求正文转换成真正要发到网络上的字节,同时准备好对应的 HTTP 头。它不会改原请求,所以也适合签名前先看一眼最终内容。
数据流:输入是 Request 的只读引用 → 函数复制请求头,检查正文类型:原始字节就直接用,JSON 就先编码,再交给 prepare_encoded_json;没有正文就保持为空 → 输出 PreparedRequestBody,里面有最终请求头和可发送正文。
调用关系:它是发送、签名和 into_prepared 都会依赖的核心准备步骤。它自己不压缩细节,而是把 JSON 正文交给 Request::prepare_encoded_json 处理。
调用图:调用 2 个内部函数(encode, prepare_encoded_json);被 3 处调用(into_prepared, build, apply_auth);外部调用 1 个(clone)。
Request::prepare_encoded_json180–238 ↗
fn prepare_encoded_json(
&self,
mut headers: HeaderMap,
body: &EncodedJsonBody,
) -> Result<PreparedRequestBody, String>
作用:专门处理已经编码好的 JSON 正文:决定要不要压缩,补齐 JSON 的请求头,并返回最终正文字节。它保证头和正文是一套匹配的东西。
数据流:输入是 Request、一份请求头副本和 EncodedJsonBody → 如果正文已经 prepared,就直接返回;如果需要 zstd 压缩,就检查不能已有 Content-Encoding,压缩字节并写入 Content-Encoding: zstd;最后确保有 Content-Type: application/json → 输出 PreparedRequestBody。
调用关系:它由 Request::prepare_body_for_send 调用,是 JSON 正文准备的最后一道工序。它会调用 EncodedJsonBody::as_bytes 取得原始 JSON 字节,并在压缩时写调试日志。
调用图:调用 2 个内部函数(as_bytes, new);被 1 处调用(prepare_body_for_send);外部调用 8 个(from, contains_key, insert, from_static, now, debug!, unreachable!, encode_all)。
tests::prepare_body_for_send_serializes_json_and_sets_content_type249–273 ↗
fn prepare_body_for_send_serializes_json_and_sets_content_type()
作用:这个测试确认:普通 JSON 请求在发送前会被转成正确字节,并自动加上 application/json 这个内容类型。
数据流:测试先用 Request::new 创建 POST 请求,再加 JSON 正文 → 调用 prepare_body_for_send → 检查得到的字节、Content-Type、原请求正文和压缩设置都符合预期。
调用关系:它覆盖 Request::new、Request::with_json 和 Request::prepare_body_for_send 的基础路径,保证最常见的 JSON 请求不会被打包错。
调用图:调用 1 个内部函数(new);外部调用 2 个(assert_eq!, json!)。
tests::prepare_body_for_send_rejects_existing_content_encoding_when_compressing276–294 ↗
fn prepare_body_for_send_rejects_existing_content_encoding_when_compressing()
作用:这个测试确认:如果请求已经手动写了 Content-Encoding,又要求这里压缩,会被拒绝。这样可以避免正文实际压缩方式和请求头说法互相打架。
数据流:测试创建一个带 JSON 和 zstd 压缩的请求,再手动塞入 Content-Encoding: gzip → 调用 prepare_body_for_send → 期望得到明确错误信息,而不是继续发送一个矛盾请求。
调用关系:它主要保护 Request::prepare_encoded_json 里的冲突检查。这个测试让以后改代码的人不容易误删这道安全门。
调用图:调用 1 个内部函数(new);外部调用 3 个(from_static, assert_eq!, json!)。
tests::into_prepared_stores_compressed_body_for_reuse297–321 ↗
fn into_prepared_stores_compressed_body_for_reuse()
作用:这个测试确认:into_prepared 会把压缩后的正文存进请求里,之后可以直接复用,不需要再次编码或压缩。
数据流:测试先把 JSON 编码成 EncodedJsonBody,再创建要求 zstd 压缩的请求 → 调用 into_prepared → 取出保存的正文并解压,确认内容仍是原 JSON,同时检查压缩标记已清空、相关请求头已写好。
调用关系:它覆盖 EncodedJsonBody::encode、Request::new 和 Request::into_prepared 的配合,验证“定稿后可重用”这个设计真的成立。
调用图:调用 3 个内部函数(encode, new, new);外部调用 5 个(assert_eq!, EncodedJson, json!, panic!, decode_all)。
codex-client/src/retry.rs源码 ↗
这个文件像是给客户端请求配了一个“耐心重试按钮”。网络请求不一定每次都顺利:可能超时,可能断网,也可能服务器返回 429(请求太多,被限流)或 5xx(服务器自己出错)。这里用 RetryPolicy 说明最多试几次、第一次等多久、哪些错误值得重试;用 RetryOn 判断某个错误该不该再试;用 backoff 算下一次要等多久,而且等待时间会逐步变长,并加一点随机抖动,避免很多客户端同时重试把服务器再次压垮。run_with_retry 是主流程:每次重新生成一个请求,执行它;成功就立刻返回;遇到可重试错误就睡一会儿再来;遇到不可重试错误就直接失败。这样调用方不用自己写一堆重复的重试代码。
RetryOn::should_retry23–35 ↗
fn should_retry(&self, err: &TransportError, attempt: u64, max_attempts: u64) -> bool
作用:这个函数判断一次失败值不值得再试。它会看失败原因、当前已经试了第几次、以及最大允许次数,避免无休止地重试。
数据流:进去的是一个错误、当前尝试次数和最大尝试次数;它先检查次数是否已经用完,如果用完就说“不重试”。如果没用完,它再看错误类型:HTTP 429、HTTP 5xx、超时或网络错误,只有配置里允许的那几类才返回可以重试;其他错误返回不重试。
调用关系:它是重试流程里的“裁判”。run_with_retry 每次请求失败后都会问它:这个错误还要不要再试?如果它说可以,流程才会进入等待和下一轮请求;如果它说不行,错误就直接交回给调用方。
backoff38–47 ↗
fn backoff(base: Duration, attempt: u64) -> Duration
作用:这个函数计算下一次重试前要等多久。它会让等待时间越来越长,并加一点随机变化,防止大家同时重试造成拥堵。
数据流:进去的是基础等待时间和第几次尝试;如果是最早的情况,就直接用基础时间。否则它把等待时间按指数方式放大,也就是越失败等得越久;再乘上 0.9 到 1.1 之间的随机数,最后输出一个新的等待时长。
调用关系:它被 run_with_retry 调用,用来决定失败后睡多久再试。它自己会用系统时间长度转换方法和随机数生成器来算出最终等待时间,然后把这个结果交给 sleep 去真正暂停。
调用图:被 1 处调用(run_with_retry);外部调用 3 个(as_millis, from_millis, rng)。
run_with_retry49–73 ↗
async fn run_with_retry(
policy: RetryPolicy,
mut make_req: impl FnMut() -> Request,
op: F,
) -> Result<T, TransportError>
作用:这个函数是真正执行“请求失败就按规则重试”的主流程。调用方把重试规则、造请求的方法、实际发送请求的方法交给它,它负责把这些串起来。
数据流:进去的是重试策略、一个每次都能新建 Request 的函数、以及真正执行请求的异步操作。它从第 0 次开始循环:先造一个新请求,再执行;如果成功,就把结果返回;如果失败但允许重试,就计算等待时间并暂停一会儿;如果失败且不该重试,就返回这个错误。循环结束还没成功时,会返回“重试次数用完”的错误。
调用关系:它是这个文件里的总调度员。请求发送方通常直接调用它;它把是否重试的判断交给 RetryOn::should_retry,把等待时间计算交给 backoff,再用 sleep 暂停当前异步任务,最后把成功结果或最终错误交还给上层。
codex-client/src/transport.rs源码 ↗
如果把整个客户端比作寄信系统,这个文件就是邮递员:上层代码只管写好“寄到哪里、用什么方法、带什么内容”,这里负责真正投递,并把回信拿回来。它定义了 HttpTransport 这个接口,意思是“任何会发 HTTP 请求的东西都要会做两件事”:一次性拿完整响应,或按数据流一段段拿响应。ReqwestTransport 是具体实现,底层使用 reqwest(Rust 里常用的 HTTP 网络库)。它会先准备请求正文和请求头,设置超时时间,再发送出去。普通请求会把响应全部读完;流式请求会把返回内容做成一条“字节流水线”,方便边来边处理。它还统一把网络超时、网络失败、HTTP 状态码错误变成项目自己的 TransportError,避免上层到处判断不同错误格式。开启 TRACE 日志时,它还会记录请求内容,方便排查问题,但原始二进制正文只显示大小,避免日志太乱。
ReqwestTransport::new42–46 ↗
fn new(client: reqwest::Client) -> Self
作用:创建一个可以发 HTTP 请求的传输器。别人已经准备好 reqwest::Client 后,用这个函数把它包成项目内部统一使用的 ReqwestTransport。
数据流:进去的是一个 reqwest::Client,也就是底层网络客户端 → 函数把它交给 CodexHttpClient::new 包一层,变成项目自己的 HTTP 客户端 → 出来的是 ReqwestTransport,后面就能用它发送普通请求或流式请求。
调用关系:很多上层功能在要访问网络前会先调用它,比如列模型、调用接口、压缩对话历史、总结记忆、实时调用和响应流接口。它本身只做初始化,不发请求;真正发请求的工作交给 ReqwestTransport::execute 或 ReqwestTransport::stream。
调用图:调用 1 个内部函数(new);被 8 处调用(models_client_hits_models_endpoint, compact_conversation_history, create_realtime_call_with_headers, summarize_memories, stream_responses_api, client, handle_call, list_models)。
ReqwestTransport::build48–74 ↗
fn build(&self, req: Request) -> Result<CodexRequestBuilder, TransportError>
作用:把项目内部的 Request,整理成 reqwest 能直接发送的请求构造器。它解决的是“内部请求格式”和“底层网络库格式”不一样的问题。
数据流:进去的是一个 Request,里面有方法、网址、请求头、正文、压缩设置和超时时间 → 它先调用 prepare_body_for_send 准备好正文和请求头,再把方法转成 HTTP 方法,设置网址、超时、请求头和正文 → 出来的是 CodexRequestBuilder,也就是一个已经基本装配好的待发送请求;如果准备正文失败,就返回 TransportError::Build。
调用关系:它是发送前的装配工。ReqwestTransport::execute 和 ReqwestTransport::stream 都会先找它把请求装好,然后再真正 send。它内部会用到底层客户端的 request 方法,也会把方法名通过 from_bytes 转成标准 HTTP 方法。
调用图:调用 2 个内部函数(request, prepare_body_for_send);被 2 处调用(execute, stream);外部调用 1 个(from_bytes)。
ReqwestTransport::map_error76–82 ↗
fn map_error(err: reqwest::Error) -> TransportError
作用:把 reqwest 返回的底层网络错误,翻译成项目自己的 TransportError。这样上层不用认识 reqwest 的错误类型,只看项目统一的错误就行。
数据流:进去的是 reqwest::Error → 它先检查这个错误是不是超时;如果是,就变成 TransportError::Timeout;如果不是,就把错误文字取出来,包装成 TransportError::Network → 出来的是统一格式的传输错误。
调用关系:它通常在请求发送失败、读取响应失败或读取流失败时使用。它会调用底层错误的 is_timeout 来分辨超时,也会调用 to_string 把其它网络错误变成可读文字。
调用图:外部调用 3 个(is_timeout, to_string, Network)。
request_body_for_trace85–94 ↗
fn request_body_for_trace(req: &Request) -> String
作用:把请求正文变成适合写进调试日志的文字。它不是为了发送请求,而是为了排查问题时能看见“我们到底发了什么”。
数据流:进去的是一个 Request 的引用 → 它查看请求正文类型:JSON 就转成 JSON 字符串,已经编码过的 JSON 就按 UTF-8 尽量还原成文字,原始二进制内容只写明有多少字节,没有正文就返回空字符串 → 出来的是一段日志用的文本。
调用关系:ReqwestTransport::execute 和 ReqwestTransport::stream 在 TRACE 级别日志开启时会调用它。它配合 tracing 的 trace 日志使用,帮助开发者定位请求内容问题,但不会把原始二进制全部塞进日志。
调用图:外部调用 3 个(from_utf8_lossy, new, format!)。
ReqwestTransport::execute97–127 ↗
async fn execute(&self, req: Request) -> Result<Response, TransportError>
作用:发送一个普通 HTTP 请求,并等完整响应回来。适合那种结果不大、可以一次性读完的接口调用。
数据流:进去的是一个 Request → 如果 TRACE 日志开启,它先记录方法、网址和请求正文摘要;然后保存网址用于出错提示,调用 build 装配请求,发送出去,读取状态码、响应头和完整响应字节 → 如果状态码表示失败,就把状态码、网址、响应头和可能的错误正文打包成 TransportError::Http;如果成功,就返回 Response,里面有状态码、响应头和完整正文。
调用关系:它是 HttpTransport 接口里的“一次性请求”实现。上层只要想拿完整响应,就会走到这里。它先把装配工作交给 ReqwestTransport::build,网络错误会通过 ReqwestTransport::map_error 统一翻译,日志内容由 request_body_for_trace 帮忙生成。
调用图:调用 1 个内部函数(build);外部调用 3 个(from_utf8, enabled!, trace!)。
ReqwestTransport::stream129–161 ↗
async fn stream(&self, req: Request) -> Result<StreamResponse, TransportError>
作用:发送一个 HTTP 请求,但不等全部内容下载完,而是把响应按字节块一段段交出来。适合服务器持续返回内容的场景,比如流式响应。
数据流:进去的是一个 Request → 如果 TRACE 日志开启,它先记录请求信息;然后调用 build 装配并发送请求,读取状态码和响应头 → 如果状态码失败,它尝试读取错误正文并返回 TransportError::Http;如果成功,它把响应的 bytes_stream 转成项目自己的 ByteStream,把每段读取错误也翻译成 TransportError → 出来的是 StreamResponse,包含状态码、响应头和一个可持续读取的字节流。
调用关系:它是 HttpTransport 接口里的“流式请求”实现。需要边接收边处理数据的上层功能会用它。它和 ReqwestTransport::execute 共用 build 来装配请求,也共用 request_body_for_trace 做日志,只是最后不把正文一次性读完,而是用 Box::pin 固定成统一的流对象交给调用者。
codex-client/src/sse.rs源码 ↗
SSE(Server-Sent Events,服务器持续往客户端推消息的一种简单协议)像一根一直开着的水管,服务器会不断往里塞事件。这个文件做的事,就是守在水管旁边,把每个 data: 里的内容取出来,变成 UTF-8 字符串,通过一个通道送出去。通道可以理解成程序内部的传送带,一边放消息,另一边取消息。它不负责理解消息内容,只负责安全、及时地转发原始文字。如果底层网络流报错、服务器提前关掉连接,或者超过指定时间没有任何新事件,它都会发出一个明确的错误,然后结束后台任务。这样调用方不用直接面对复杂的数据流解析,只要从通道里接收“成功的文字”或“失败的原因”即可。
sse_stream12–48 ↗
fn sse_stream(
stream: ByteStream,
idle_timeout: Duration,
tx: mpsc::Sender<Result<String, StreamError>>,
)
作用:启动一个后台任务,读取传入的 SSE 字节流,把每条事件里的 data 文本发送到指定通道里。有人会用它来把持续返回的服务器响应,变成程序内部更容易消费的一条条字符串消息。
数据流:进去的是一个字节流、一个空闲超时时间,以及一个发送通道。函数先把字节流转换成 SSE 事件流;之后循环等待下一条事件,但每次等待都受超时时间限制。读到正常事件时,它把事件里的文字发送出去;读到错误、流提前结束,或等太久没等到消息时,它发送对应的 StreamError,然后结束后台任务。它本身不返回数据,而是通过通道不断把结果送给外面。
调用关系:它通常会在发起一次需要流式响应的请求后被调用,作为网络接收流程里的搬运工。内部它把原始流交给 eventsource 解析,再用 next 等下一条事件,用 timeout 防止无限等待,用 send 把结果交给接收方。整个读取过程被 spawn 放到 Tokio 后台任务里运行,所以调用它的一方不用停下来等流读完。
codex-client/src/lib.rs源码 ↗
可以把这个文件想成一家商店的前台。真正干活的货架在后面,比如请求怎么组装、网络怎么发送、失败怎么重试、服务端事件流怎么读取、自定义证书怎么配置,都放在各自模块里。这个文件本身不实现这些细节,而是用 mod 把内部模块接进来,再用 pub use 把外部最常用的类型和函数公开出去。这样别人使用这个库时,只需要从 codex-client 这个总入口拿东西,不必关心 retry、transport、request 这些内部文件的路径。它还特别把一个只给测试子进程用的自定义证书 helper 标成隐藏文档,意思是:能公开访问,但普通用户不该直接用。
HTTP 客户端配置
这些文件围绕跟踪、自定义 CA 处理和受限 cookie 存储,为经过身份验证的网络通信构建可复用的出站客户端设置。
codex-client/src/chatgpt_cloudflare_cookies.rs源码 ↗
访问 chatgpt.com 时,Cloudflare 可能会发一些 Cookie,用来表示“这个访问者已经通过检查”或做负载均衡。这个文件就是专门存这些 Cookie 的。它像一个公共钥匙盒:全进程共享,但只准放 Cloudflare 认可的几把钥匙,绝不能放用户登录钥匙。核心结构是 ChatGptCloudflareCookieStore,里面包了 reqwest 的 Jar,也就是 HTTP 客户端用的 Cookie 容器。收到服务器 Set-Cookie 时,它先确认网址必须是 HTTPS,并且主机必须是允许的 ChatGPT 主机;再检查 Cookie 名字是不是 Cloudflare 的白名单。发请求时,它也只把这些白名单 Cookie 拿出来。这样既能让 ChatGPT 请求更顺畅,又把“全局共享 Cookie”带来的隐私风险压到最低。文件后半部分是一组测试,专门验证它不会存错域名、不会存普通 HTTP、不会泄露非 Cloudflare Cookie。
ChatGptCloudflareCookieStore::set_cookies23–35 ↗
fn set_cookies(
&self,
cookie_headers: &mut dyn Iterator<Item = &HeaderValue>,
url: &reqwest::Url,
)
作用:服务器返回 Set-Cookie 时,这个函数决定哪些 Cookie 可以被存下来。它只接受来自安全的 ChatGPT 地址、并且名字属于 Cloudflare 白名单的 Cookie。
数据流:输入是一批 Set-Cookie 头和当前响应的网址。它先用 is_chatgpt_cookie_url 判断网址是不是 HTTPS 的 ChatGPT 允许域名;不是就直接丢掉。是的话,再筛掉非 Cloudflare Cookie,最后把留下来的交给内部 Jar 保存。
调用关系:这是 reqwest HTTP 客户端在收到 Cookie 时会调用的入口。它先请 is_chatgpt_cookie_url 把住“网址关”,再通过过滤器挑出允许的 Cookie,最后把真正保存的工作交给 reqwest 的 Cookie Jar。
调用图:调用 1 个内部函数(is_chatgpt_cookie_url);外部调用 2 个(filter, set_cookies)。
ChatGptCloudflareCookieStore::cookies37–43 ↗
fn cookies(&self, url: &reqwest::Url) -> Option<HeaderValue>
作用:发请求前,这个函数决定要不要把已保存的 Cloudflare Cookie 带上。它只会给合格的 ChatGPT HTTPS 地址返回 Cookie。
数据流:输入是将要访问的网址。它先检查网址是否允许;允许时从内部 Jar 取出 Cookie,再只保留 Cloudflare 白名单里的部分;不允许时返回空,表示这个请求不带这些 Cookie。
调用关系:这是 reqwest HTTP 客户端准备请求头时会用到的函数。它依赖 is_chatgpt_cookie_url 判断目标地址,并把 Jar 取出的原始 Cookie 再交给 only_cloudflare_cookies 做最后清洗。
调用图:调用 1 个内部函数(is_chatgpt_cookie_url);外部调用 1 个(cookies)。
with_chatgpt_cloudflare_cookie_store52–56 ↗
fn with_chatgpt_cloudflare_cookie_store(
builder: reqwest::ClientBuilder,
) -> reqwest::ClientBuilder
作用:这个函数把全进程共享的 ChatGPT Cloudflare Cookie 罐装到 reqwest 客户端构建器里。调用者用它来让之后创建的 HTTP 客户端自动保存和发送这些特定 Cookie。
数据流:输入是一个 reqwest::ClientBuilder。它克隆共享 Cookie 罐的引用,设置成客户端的 cookie_provider,然后返回更新后的构建器;它不会立刻发请求,只是完成客户端配置。
调用关系:这是外部代码接入本文件能力的主要入口。HTTP 客户端创建阶段会调用它;它把后续的 Cookie 收发工作交给 ChatGptCloudflareCookieStore::set_cookies 和 ChatGptCloudflareCookieStore::cookies。
调用图:外部调用 2 个(clone, cookie_provider)。
is_chatgpt_cookie_url58–69 ↗
fn is_chatgpt_cookie_url(url: &reqwest::Url) -> bool
作用:这个函数判断一个网址是否有资格使用这个 Cookie 罐。规则很硬:必须是 HTTPS,并且主机名必须是项目认可的 ChatGPT 主机。
数据流:输入是一个 URL。它读取协议名和主机名;协议不是 https 就返回 false,没有主机名也返回 false;最后把主机名交给 is_allowed_chatgpt_host 判断,输出 true 或 false。
调用关系:它是存 Cookie 和取 Cookie 前的第一道门。ChatGptCloudflareCookieStore::set_cookies 和 ChatGptCloudflareCookieStore::cookies 都会先问它:这个地址能不能碰共享 Cookie。
调用图:调用 1 个内部函数(is_allowed_chatgpt_host);被 2 处调用(cookies, set_cookies);外部调用 2 个(host_str, scheme)。
is_allowed_cloudflare_set_cookie_header71–77 ↗
fn is_allowed_cloudflare_set_cookie_header(header: &HeaderValue) -> bool
作用:这个函数检查一个 Set-Cookie 响应头是不是 Cloudflare 允许保存的 Cookie。它主要看 Cookie 名字,而不是看整段内容。
数据流:输入是一个 HTTP 头值。它先尝试把头值当成普通字符串读取;读不了就判定不合格。读出来后提取等号前的 Cookie 名字,再判断这个名字是否在 Cloudflare 白名单里,最后输出 true 或 false。
调用关系:它在保存 Cookie 的筛选过程中使用。set_cookies 会把服务器给的一批 Cookie 逐个过滤,只有这个函数认可的才会进入内部 Jar。
调用图:外部调用 1 个(to_str)。
set_cookie_name79–83 ↗
fn set_cookie_name(header: &str) -> Option<&str>
作用:这个小函数从一整条 Set-Cookie 字符串里拿出 Cookie 名字。比如从“cf_clearance=xxx; Path=/”里拿出“cf_clearance”。
数据流:输入是一段 Set-Cookie 文本。它按第一个等号切开,取左边并去掉前后空格;如果没有等号或名字为空,就返回空;否则返回 Cookie 名字。
调用关系:它是 is_allowed_cloudflare_set_cookie_header 的辅助步骤。后者需要先知道 Cookie 叫什么,才能判断它是不是 Cloudflare 允许的名字。
only_cloudflare_cookies85–102 ↗
fn only_cloudflare_cookies(header: HeaderValue) -> Option<HeaderValue>
作用:这个函数把准备发出去的 Cookie 头再清理一遍,只留下 Cloudflare 白名单 Cookie。它是最后一道保险,防止非 Cloudflare Cookie 被带到请求里。
数据流:输入是一个 Cookie 请求头。它先转成字符串,再按分号拆成一个个 Cookie;每个 Cookie 取出名字并查白名单。留下来的重新拼成一个 Cookie 头;如果一个都没留下,就返回空。
调用关系:ChatGptCloudflareCookieStore::cookies 从内部 Jar 拿到 Cookie 后,会让它做二次过滤。它还会调用 HeaderValue::from_str 把清理后的字符串重新变成 HTTP 头。
调用图:外部调用 3 个(from_str, split, to_str)。
is_allowed_cloudflare_cookie_name104–119 ↗
fn is_allowed_cloudflare_cookie_name(name: &str) -> bool
作用:这个函数定义了哪些 Cookie 名字被认为是 Cloudflare 基础设施 Cookie。它是整个安全边界的核心白名单。
数据流:输入是一个 Cookie 名字。它检查名字是否等于一组明确列出的 Cloudflare 名称,或者是否以“cf_chl_”开头;符合就返回 true,不符合就返回 false。
调用关系:多个检查步骤都会依赖它:保存服务器 Cookie 时要用,发送请求前清理 Cookie 时也要用。测试也专门验证这个名单不会误放用户登录 Cookie。
调用图:外部调用 1 个(matches!)。
tests::stores_and_returns_cloudflare_cookies_for_chatgpt_hosts128–155 ↗
fn stores_and_returns_cloudflare_cookies_for_chatgpt_hosts()
作用:这个测试确认:对合法的 ChatGPT HTTPS 地址,Cloudflare Cookie 能被存下,并能在之后取出来。
数据流:它创建一个新的 Cookie 罐,构造 chatgpt.com 的 HTTPS 地址和两个 Cloudflare Cookie。先调用 set_cookies 保存,再调用 cookies 取回,最后把结果排序并断言正好是那两个 Cookie。
调用关系:这是测试套件验证正常路径的用例。它通过 reqwest 的 URL 解析和 HeaderValue 构造模拟真实 HTTP 场景,间接覆盖 set_cookies、cookies 和白名单判断。
调用图:外部调用 4 个(from_static, assert_eq!, default, parse)。
tests::ignores_non_chatgpt_cookies158–166 ↗
fn ignores_non_chatgpt_cookies()
作用:这个测试确认:即使 Cookie 名字像 Cloudflare,只要来源不是 ChatGPT 允许主机,也不能被保存。
数据流:它创建 Cookie 罐,使用 api.openai.com 的地址和一个 Cloudflare Cookie。保存后再读取,期望结果是 None,也就是没有任何 Cookie 可用。
调用关系:这是针对 is_chatgpt_cookie_url 的保护测试。它证明 set_cookies 在非 ChatGPT 主机上会直接停手,不会把外站 Cookie 放进共享罐。
调用图:外部调用 5 个(from_static, assert_eq!, default, parse, once)。
tests::ignores_non_cloudflare_cookies_for_chatgpt_hosts169–179 ↗
fn ignores_non_cloudflare_cookies_for_chatgpt_hosts()
作用:这个测试确认:即使来源是 ChatGPT,普通账号或登录类 Cookie 也不能被保存。
数据流:它创建 ChatGPT HTTPS 地址和一个看起来像登录会话的 Cookie。调用 set_cookies 后再取 cookies,期望为空,说明这个敏感 Cookie 没有进入存储。
调用关系:这是白名单安全边界的关键测试。它验证保存阶段会用 Cookie 名字过滤,不会因为域名正确就什么都收。
调用图:外部调用 5 个(from_static, assert_eq!, default, parse, once)。
tests::ignores_mixed_non_cloudflare_cookies_for_chatgpt_hosts182–197 ↗
fn ignores_mixed_non_cloudflare_cookies_for_chatgpt_hosts()
作用:这个测试确认:一批 Cookie 里有好有坏时,只保存 Cloudflare 的那一个,坏的不会“搭便车”进去。
数据流:它给合法 ChatGPT 地址同时传入一个 _cfuvid Cookie 和一个 chatgpt_session Cookie。保存后读取,期望只拿到 _cfuvid=visitor。
调用关系:这是对 set_cookies 过滤行为的混合场景测试。它证明过滤是逐条 Cookie 做的,不是整批放行。
调用图:外部调用 4 个(from_static, assert_eq!, default, parse)。
tests::does_not_return_chatgpt_cloudflare_cookies_for_other_hosts200–210 ↗
fn does_not_return_chatgpt_cloudflare_cookies_for_other_hosts()
作用:这个测试确认:已经为 ChatGPT 保存的 Cloudflare Cookie,不会被发给别的主机。
数据流:它先在 chatgpt.com 地址下保存一个 Cloudflare Cookie,然后尝试从 api.openai.com 地址读取。期望读取结果为空。
调用关系:这是发送阶段的隔离测试。它验证 cookies 会再次检查目标网址,避免把 ChatGPT 相关 Cookie 泄露到其他域名。
调用图:外部调用 5 个(from_static, assert_eq!, default, parse, once)。
tests::rejects_plain_http_chatgpt_cookie_urls213–225 ↗
fn rejects_plain_http_chatgpt_cookie_urls()
作用:这个测试确认:普通 HTTP 的 chatgpt.com 也不合格,必须是 HTTPS。HTTPS 是加密连接,能减少 Cookie 被中途看到或篡改的风险。
数据流:它创建一个 http://chatgpt.com 地址和一个 https://chatgpt.com 地址。先尝试用 HTTP 地址保存 Cookie,再分别从 HTTP 和 HTTPS 地址读取,期望都读不到。
调用关系:这是对 is_chatgpt_cookie_url 协议检查的测试。它证明 set_cookies 不会接受明文 HTTP 来源,也不会让这类 Cookie 后续出现在请求里。
调用图:外部调用 5 个(from_static, assert_eq!, default, parse, once)。
tests::only_allows_https_urls228–236 ↗
tests::allows_only_known_cloudflare_cookie_names239–263 ↗
fn allows_only_known_cloudflare_cookie_names()
作用:这个测试确认 Cloudflare Cookie 白名单只放行已知名称,并拒绝账号、会话、认证令牌这类敏感名字。
数据流:它准备两组名字:一组是允许的 Cloudflare 名称,另一组是应拒绝的非 Cloudflare 或敏感名称。逐个调用 is_allowed_cloudflare_cookie_name,并断言结果符合预期。
调用关系:这是白名单函数的直接测试。因为 is_allowed_cloudflare_cookie_name 是保存和发送 Cookie 的共同依据,所以这个测试保护了整个文件最重要的安全规则。
调用图:外部调用 1 个(assert!)。
codex-client/src/custom_ca.rs源码 ↗
很多公司会用代理检查加密流量,这时服务器证书可能不是公共机构签的,而是公司自己的 CA(证书颁发者)签的。这个文件统一规定:Codex 从环境变量 CODEX_CA_CERTIFICATE 读取证书文件;如果没有,再看 SSL_CERT_FILE;空值当作没设置。它会读 PEM 文件(常见的文本证书格式),把 OpenSSL 的 TRUSTED CERTIFICATE 这种变体改成普通 CERTIFICATE,并忽略格式正确的 CRL(证书吊销列表)。然后它把证书加到 reqwest 的 HTTP 客户端,或 rustls 的 WebSocket TLS 配置里。出错时,它不会只说“失败”,而是告诉用户是文件读不到、格式不对,还是某张证书不能注册,并附上修复提示。
Error::from148–161 ↗
fn from(error: BuildCustomCaTransportError) -> Self
作用:把本文件自己的自定义 CA 错误,转换成标准的 io::Error(输入输出错误)。这样调用方即使不关心细分原因,也能按普通文件或网络错误来处理。
数据流:进去的是一个 BuildCustomCaTransportError → 它根据错误类型挑合适的 io::ErrorKind,比如文件读失败保留原来的错误种类,证书格式问题标成 InvalidData → 出来的是一个标准 io::Error,里面仍包着原始说明。
调用关系:它是错误对外兼容的一层包装。真正产生错误的是读证书、解析证书、构建客户端等函数;当外层代码需要 io::Error 时,会走到这里。
调用图:外部调用 2 个(new, other)。
build_reqwest_client_with_custom_ca179–183 ↗
fn build_reqwest_client_with_custom_ca(
builder: reqwest::ClientBuilder,
) -> Result<reqwest::Client, BuildCustomCaTransportError>
作用:给普通 HTTP 客户端加上 Codex 的自定义 CA 规则。调用者传入一个 reqwest::ClientBuilder(HTTP 客户端建造器),它负责在最终构建前补上证书信任设置。
数据流:进去的是调用方已经配置好的 HTTP 客户端建造器 → 它使用真实进程环境变量检查是否指定了 CA 文件,并把工作交给 build_reqwest_client_with_env → 出来的是可以发请求的 reqwest::Client,或者清楚说明哪里坏了的错误。
调用关系:这是生产代码最常用的 HTTP 入口。它本身不做复杂处理,只把真实环境 ProcessEnv 和建造器交给 build_reqwest_client_with_env。
调用图:调用 1 个内部函数(build_reqwest_client_with_env)。
maybe_build_rustls_client_config_with_custom_ca196–199 ↗
fn maybe_build_rustls_client_config_with_custom_ca() -> Result<Option<Arc<ClientConfig>>, BuildCustomCaTransportError>
作用:为安全 WebSocket 准备 rustls 客户端配置。rustls 是 Rust 里的 TLS 加密库;这里的配置决定 WebSocket 连接信任哪些根证书。
数据流:进去不需要参数,它读取真实环境变量 → 如果配置了 CA 文件,就生成一个带系统根证书和自定义根证书的 ClientConfig;如果没配置,就返回 None → 出来是可选的 TLS 配置或错误。
调用关系:这是 WebSocket 一侧的公开入口。它把真实环境交给 maybe_build_rustls_client_config_with_env;没有自定义 CA 时,调用方可以继续用默认 TLS 路径。
调用图:调用 1 个内部函数(maybe_build_rustls_client_config_with_env)。
build_reqwest_client_for_subprocess_tests209–213 ↗
fn build_reqwest_client_for_subprocess_tests(
builder: reqwest::ClientBuilder,
) -> Result<reqwest::Client, BuildCustomCaTransportError>
作用:专门给子进程集成测试构建 HTTP 客户端。它和正式入口很像,但会关闭代理自动探测,避免测试在某些系统环境里被代理设置影响甚至崩溃。
数据流:进去的是 reqwest 客户端建造器 → 它先调用 no_proxy 关闭代理自动发现,再按同一套自定义 CA 规则构建客户端 → 出来是测试用 HTTP 客户端或错误。
调用关系:它只服务测试场景。它调用 build_reqwest_client_with_env 复用核心逻辑,但通过 no_proxy 改掉测试不希望碰到的代理行为。
调用图:调用 1 个内部函数(build_reqwest_client_with_env);外部调用 1 个(no_proxy)。
maybe_build_rustls_client_config_with_env215–262 ↗
fn maybe_build_rustls_client_config_with_env(
env_source: &dyn EnvSource,
) -> Result<Option<Arc<ClientConfig>>, BuildCustomCaTransportError>
作用:这是 WebSocket TLS 配置的核心实现。它用给定的环境来源决定是否加载自定义 CA,并把证书放进 rustls 的根证书仓库。
数据流:进去的是一个 EnvSource(环境变量来源,可以是真实环境,也可以是测试假环境)→ 它先找 CA 文件;没有就返回 None;有的话加载 rustls 加密提供者、读取系统根证书、再读取并加入自定义证书 → 出来是 Some(Arc<ClientConfig>) 或详细错误。
调用关系:公开函数 maybe_build_rustls_client_config_with_custom_ca 会调用它,测试也直接调用它。它会向 EnvSource::configured_ca_bundle 询问选择哪个证书文件,再让 ConfiguredCaBundle::load_certificates 真正读证书。
调用图:调用 1 个内部函数(configured_ca_bundle);被 3 处调用(maybe_build_rustls_client_config_with_custom_ca, rustls_config_reports_invalid_ca_file, rustls_config_uses_custom_ca_bundle_when_configured);外部调用 6 个(new, builder, empty, ensure_rustls_crypto_provider, load_native_certs, warn!)。
build_reqwest_client_with_env271–343 ↗
fn build_reqwest_client_with_env(
env_source: &dyn EnvSource,
mut builder: reqwest::ClientBuilder,
) -> Result<reqwest::Client, BuildCustomCaTransportError>
作用:这是 HTTP 客户端自定义 CA 的核心实现。它决定是否启用自定义证书,并把证书逐张注册到 reqwest 客户端里。
数据流:进去的是环境来源和 reqwest 客户端建造器 → 如果找到 CA 文件,它启用 rustls TLS,读取证书,把每张 DER 证书转成 reqwest 能接受的证书并加入根证书;如果没找到,就直接用系统根证书构建 → 出来是 HTTP 客户端或构建/证书错误。
调用关系:build_reqwest_client_with_custom_ca 和 build_reqwest_client_for_subprocess_tests 都把活交给它。它向 EnvSource::configured_ca_bundle 要配置,再向 ConfiguredCaBundle::load_certificates 要证书,最后调用 reqwest 的 build 完成客户端。
调用图:调用 1 个内部函数(configured_ca_bundle);被 2 处调用(build_reqwest_client_for_subprocess_tests, build_reqwest_client_with_custom_ca);外部调用 8 个(add_root_certificate, build, use_rustls_tls, BuildClientWithSystemRoots, ensure_rustls_crypto_provider, info!, from_der, warn!)。
EnvSource::non_empty_path362–366 ↗
fn non_empty_path(&self, key: &str) -> Option<PathBuf>
作用:从环境变量里取一个非空路径。它把空字符串当成“没设置”,避免把 VAR="" 误当成一个文件路径。
数据流:进去的是环境变量名 → 它调用 var 取值,过滤掉空字符串,再把剩下的字符串转成 PathBuf 路径 → 出来是 Some(路径) 或 None。
调用关系:EnvSource::configured_ca_bundle 用它来检查 CODEX_CA_CERTIFICATE 和 SSL_CERT_FILE。它是环境变量选择规则里的小过滤器。
调用图:被 1 处调用(configured_ca_bundle)。
EnvSource::configured_ca_bundle373–386 ↗
fn configured_ca_bundle(&self) -> Option<ConfiguredCaBundle>
作用:决定到底用哪个 CA 文件。规则是 Codex 专用的 CODEX_CA_CERTIFICATE 优先;没有时才用通用的 SSL_CERT_FILE。
数据流:进去的是一个环境来源 → 它先查 CODEX_CA_CERTIFICATE 的非空路径,再查 SSL_CERT_FILE → 出来是 ConfiguredCaBundle,里面带着选中的变量名和文件路径;如果都没有,则返回 None。
调用关系:HTTP 和 WebSocket 的构建函数都会先调用它。它依赖 EnvSource::non_empty_path 来避免空值干扰。
调用图:调用 1 个内部函数(non_empty_path);被 2 处调用(build_reqwest_client_with_env, maybe_build_rustls_client_config_with_env)。
ProcessEnv::var397–399 ↗
fn var(&self, key: &str) -> Option<String>
作用:从真实进程环境里读取一个环境变量。生产代码用它接入操作系统提供的环境变量。
数据流:进去的是变量名 → 它调用标准库 env::var 读取真实值,读取失败就当作没有 → 出来是 Some(字符串) 或 None。
调用关系:ProcessEnv 实现 EnvSource。公开构建函数把 ProcessEnv 传给内部核心函数,让核心逻辑既能用于生产,也能在测试里换成假环境。
调用图:外部调用 1 个(var)。
ConfiguredCaBundle::load_certificates420–443 ↗
fn load_certificates(
&self,
) -> Result<Vec<CertificateDer<'static>>, BuildCustomCaTransportError>
作用:加载某个已经选定的 CA 文件,并负责记录成功或失败日志。它是“开始真正读证书”的入口。
数据流:进去的是 ConfiguredCaBundle,里面有环境变量名和文件路径 → 它调用 parse_certificates 读取并解析证书;成功就记录证书数量,失败就记录错误 → 出来是证书列表或错误。
调用关系:HTTP 构建和 rustls 配置都会调用它。它把具体解析工作交给 ConfiguredCaBundle::parse_certificates,并统一做日志。
调用图:调用 1 个内部函数(parse_certificates);外部调用 2 个(info!, warn!)。
ConfiguredCaBundle::parse_certificates451–497 ↗
fn parse_certificates(
&self,
) -> Result<Vec<CertificateDer<'static>>, BuildCustomCaTransportError>
作用:把 PEM 证书文件变成程序能注册的 DER 证书列表。PEM 是带 BEGIN/END 的文本格式,DER 是证书的二进制格式。
数据流:进去的是选定的 CA 文件信息 → 它先读文件,再做 PEM 文本规范化,逐段扫描;遇到证书就取出 DER,遇到格式正确的 CRL 就忽略;如果没有任何证书或解析失败就生成配置错误 → 出来是 CertificateDer 列表。
调用关系:它由 load_certificates 调用,是证书文件处理的主干。它会调用 read_pem_data 读文件,调用 NormalizedPem::from_pem_data 兼容 OpenSSL 格式,并用 pem_parse_error/invalid_ca_file 包装错误。
调用图:调用 3 个内部函数(pem_parse_error, read_pem_data, from_pem_data);被 1 处调用(load_certificates);外部调用 3 个(from, new, info!)。
ConfiguredCaBundle::read_pem_data504–510 ↗
fn read_pem_data(&self) -> Result<Vec<u8>, BuildCustomCaTransportError>
作用:从磁盘读取 CA 文件的原始字节,并在失败时保留具体文件错误。比如文件不存在、没权限,都会在这里变成带路径的错误。
数据流:进去的是 CA 文件路径和来源环境变量名 → 它调用 fs::read 读取文件字节 → 成功返回 Vec<u8>;失败返回 ReadCaFile,里面包含路径、变量名和原始 io 错误。
调用关系:parse_certificates 在解析前先调用它。它只负责读文件,不负责判断内容是不是证书。
调用图:被 1 处调用(parse_certificates);外部调用 1 个(read)。
ConfiguredCaBundle::pem_parse_error517–524 ↗
fn pem_parse_error(&self, error: &pem::Error) -> BuildCustomCaTransportError
作用:把底层 PEM 解析器的错误,改写成用户能看懂的“CA 文件不可用”错误。
数据流:进去的是 pem::Error → 如果是没找到任何项目,就写成“PEM 文件里没有证书”;其他错误写成“解析 PEM 文件失败:具体原因” → 出来是 InvalidCaFile 错误。
调用关系:parse_certificates 在遇到 PEM 解析失败或证书为空时调用它。它继续调用 invalid_ca_file 来统一填路径和来源变量。
调用图:调用 1 个内部函数(invalid_ca_file);被 1 处调用(parse_certificates);外部调用 1 个(format!)。
ConfiguredCaBundle::invalid_ca_file531–537 ↗
fn invalid_ca_file(&self, detail: impl std::fmt::Display) -> BuildCustomCaTransportError
作用:创建一个格式统一的“CA 文件无效”错误。这样不同解析分支报错时,路径、环境变量名和详细原因都一致。
数据流:进去的是一段详细说明 → 它复制当前 CA 文件路径,把说明转成字符串 → 出来是 BuildCustomCaTransportError::InvalidCaFile。
调用关系:pem_parse_error 和 parse_certificates 会用它。它是错误格式化的集中出口。
调用图:被 1 处调用(pem_parse_error);外部调用 2 个(to_string, clone)。
NormalizedPem::from_pem_data577–592 ↗
fn from_pem_data(source_env: &'static str, path: &Path, pem_data: &[u8]) -> Self
作用:把读到的 PEM 文本调整成后续解析器更容易识别的形状。重点是兼容 OpenSSL 生成的 TRUSTED CERTIFICATE。
数据流:进去的是来源变量名、文件路径和文件字节 → 它先按 UTF-8 宽松转成文本;如果发现 TRUSTED CERTIFICATE,就把 BEGIN/END 标签替换成普通 CERTIFICATE 并记录日志;否则保持原样 → 出来是 NormalizedPem。
调用关系:parse_certificates 读完文件后调用它。后续 sections 和 certificate_der 都基于它的结果工作。
调用图:被 1 处调用(parse_certificates);外部调用 4 个(Standard, TrustedCertificate, from_utf8_lossy, info!)。
NormalizedPem::contents595–599 ↗
fn contents(&self) -> &str
作用:取出规范化后的 PEM 文本内容。不管它原来是普通证书还是 TRUSTED CERTIFICATE,都用同一种方式给后续代码读取。
数据流:进去的是 NormalizedPem → 它匹配 Standard 或 TrustedCertificate 两种形态 → 出来是内部保存的字符串引用。
调用关系:NormalizedPem::sections 调用它来获得要扫描的文本。它是枚举两种形态和统一文本访问之间的小桥。
调用图:被 1 处调用(sections)。
NormalizedPem::sections606–608 ↗
fn sections(&self) -> impl Iterator<Item = Result<PemSection, pem::Error>> + '_
作用:把规范化后的 PEM 文本拆成一段段可识别的内容,比如证书段或 CRL 段。
数据流:进去的是 NormalizedPem → 它取出文本内容,再交给 pem_slice_iter 这个解析器逐段扫描 → 出来是一个迭代器,每次给出一个 PEM 段或解析错误。
调用关系:parse_certificates 用它遍历 CA 文件里的所有段。它依赖 contents 拿文本,再依赖外部 PEM 解析器做实际拆分。
调用图:调用 1 个内部函数(contents);外部调用 1 个(pem_slice_iter)。
NormalizedPem::certificate_der615–620 ↗
fn certificate_der(&self, der: &'a [u8]) -> Option<&'a [u8]>
作用:从一个证书段里拿到真正的证书 DER 字节。普通证书直接返回;OpenSSL TRUSTED CERTIFICATE 需要切掉尾部额外的信任信息。
数据流:进去的是某段 DER 字节 → 如果是 Standard,就原样返回;如果是 TrustedCertificate,就调用 first_der_item 只保留第一个完整 DER 对象 → 出来是证书字节切片,或 None 表示找不到合法边界。
调用关系:parse_certificates 在遇到证书段时调用它。它把 OpenSSL 兼容问题交给 first_der_item 处理。
调用图:调用 1 个内部函数(first_der_item)。
first_der_item635–637 ↗
fn first_der_item(der: &[u8]) -> Option<&[u8]>
作用:从一串 DER 字节里切出第一个完整对象。这里用来把 OpenSSL 附加的 X509_AUX 元数据丢掉,只留下证书本体。
数据流:进去的是 DER 字节数组 → 它调用 der_item_length 算出第一个对象总长度 → 出来是数组开头那一段完整对象;如果长度算不出来,就返回 None。
调用关系:NormalizedPem::certificate_der 在处理 TRUSTED CERTIFICATE 时调用它。它不验证证书内容,只负责找到安全切片边界。
调用图:调用 1 个内部函数(der_item_length);被 1 处调用(certificate_der)。
der_item_length663–687 ↗
fn der_item_length(der: &[u8]) -> Option<usize>
作用:计算第一个 DER 对象占多少字节。DER 是一种二进制编码,开头会写明后面内容有多长。
数据流:进去的是 DER 字节数组 → 它读取第二个字节判断是短长度还是长长度,计算内容长度,并检查声明的长度没有超过输入数组 → 出来是总长度;格式不够、长度溢出或越界则返回 None。
调用关系:first_der_item 调用它来决定该切到哪里。它只解析最外层长度,不负责检查证书里面的细节。
调用图:被 1 处调用(first_der_item);外部调用 1 个(from)。
tests::MapEnv::var711–713 ↗
fn var(&self, key: &str) -> Option<String>
作用:测试用的假环境变量读取器。它不用真实系统环境,而是从内存里的 HashMap 查值。
数据流:进去的是变量名 → 它在 values 这个映射表里查找并复制字符串 → 出来是 Some(值) 或 None。
调用关系:测试里的 MapEnv 实现 EnvSource。这样测试 configured_ca_bundle 和 rustls 配置时,不需要真的改进程环境变量。
tests::map_env716–723 ↗
fn map_env(pairs: &[(&str, &str)]) -> MapEnv
作用:快速创建一个测试用的 MapEnv。测试可以用几组键值对模拟不同环境变量设置。
数据流:进去的是字符串键值对数组 → 它把每对值转成拥有所有权的 String,并放进 HashMap → 出来是 MapEnv。
调用关系:多个测试函数都会调用它,来搭建 CODEX_CA_CERTIFICATE 和 SSL_CERT_FILE 的不同组合。
tests::write_cert_file725–731 ↗
tests::ca_path_prefers_codex_env734–744 ↗
fn ca_path_prefers_codex_env()
作用:验证 CODEX_CA_CERTIFICATE 比 SSL_CERT_FILE 优先。也就是两者都设置时,Codex 专用变量说了算。
数据流:进去的是测试里写死的两组环境变量 → 它创建 MapEnv,调用 configured_ca_bundle,检查返回路径是不是 /tmp/codex.pem → 测试通过表示优先级正确。
调用关系:它测试 EnvSource::configured_ca_bundle 的核心选择规则,使用 map_env 搭假环境。
调用图:外部调用 2 个(assert_eq!, map_env)。
tests::ca_path_falls_back_to_ssl_cert_file747–754 ↗
fn ca_path_falls_back_to_ssl_cert_file()
作用:验证没有 CODEX_CA_CERTIFICATE 时,会退回使用 SSL_CERT_FILE。
数据流:进去的是只包含 SSL_CERT_FILE 的假环境 → 它调用 configured_ca_bundle → 出来的路径应该是 /tmp/fallback.pem,否则测试失败。
调用关系:它补充覆盖环境变量选择规则,确认 fallback(后备选择)能工作。
调用图:外部调用 2 个(assert_eq!, map_env)。
tests::ca_path_ignores_empty_values757–767 ↗
fn ca_path_ignores_empty_values()
作用:验证空字符串不会被当成有效路径。这样 CODEX_CA_CERTIFICATE="" 不会挡住 SSL_CERT_FILE。
数据流:进去的是 CODEX_CA_CERTIFICATE 为空、SSL_CERT_FILE 有路径的假环境 → 它调用 configured_ca_bundle → 出来应该选择 SSL_CERT_FILE 的路径。
调用关系:它测试 EnvSource::non_empty_path 和 configured_ca_bundle 的配合,防止空环境变量造成错误文件读取。
调用图:外部调用 2 个(assert_eq!, map_env)。
tests::rustls_config_uses_custom_ca_bundle_when_configured770–780 ↗
fn rustls_config_uses_custom_ca_bundle_when_configured()
作用:验证 WebSocket 的 rustls 配置在设置了 CA 文件时真的会生成。它用一个测试证书文件模拟用户配置。
数据流:进去的是临时目录和内置测试证书内容 → 它写出 ca.pem,构造包含 CODEX_CA_CERTIFICATE 的假环境,调用 maybe_build_rustls_client_config_with_env → 出来应该是 Some 配置,并且配置里 SNI 开启。
调用关系:它直接测试 maybe_build_rustls_client_config_with_env。过程中使用 map_env 和 write_cert_file 准备输入。
调用图:调用 1 个内部函数(maybe_build_rustls_client_config_with_env);外部调用 4 个(new, assert!, map_env, write_cert_file)。
tests::rustls_config_reports_invalid_ca_file783–794 ↗
fn rustls_config_reports_invalid_ca_file()
作用:验证 WebSocket TLS 配置遇到无效 CA 文件时会报 InvalidCaFile,而不是悄悄忽略或报模糊错误。
数据流:进去的是一个空的 PEM 文件路径 → 它构造假环境,调用 maybe_build_rustls_client_config_with_env → 出来应该是错误,并且错误类型是 InvalidCaFile。
调用关系:它覆盖 maybe_build_rustls_client_config_with_env 到证书解析失败的路径,间接检查 parse_certificates 和 pem_parse_error 的错误包装。
调用图:调用 1 个内部函数(maybe_build_rustls_client_config_with_env);外部调用 4 个(new, assert!, map_env, write_cert_file)。
codex-client/src/default_client.rs源码 ↗
这个文件像是在普通网络请求工具外面套了一层“项目专用外壳”。底层真正发请求的是 reqwest(Rust 里常用的 HTTP 请求库),这里的 CodexHttpClient 负责把它包装起来,提供 get、post、request 这些更统一的入口。创建请求后,会得到 CodexRequestBuilder,它像点餐单一样,可以继续加请求头、认证 token、超时时间、JSON 内容或原始 body。真正发出去是在 send 里:它会先调用 trace_headers,把当前追踪上下文写进 HTTP 头。追踪上下文可以理解成“快递单号”,让后端服务知道这个请求属于哪一次调用链。请求完成后,它还会记录方法、网址、状态码、响应头等调试日志;失败时也会记录错误。文件底部的测试专门确认:生成的追踪头确实来自当前正在运行的 tracing span(一次可追踪的操作范围)。
CodexHttpClient::new22–24 ↗
fn new(inner: reqwest::Client) -> Self
作用:把一个现成的 reqwest 客户端包成项目自己的 CodexHttpClient。这样项目里其他地方不用直接碰底层客户端,而是走统一的请求入口。
数据流:输入是一个 reqwest::Client,也就是实际会发网络请求的底层工具。函数把它放进 CodexHttpClient 的 inner 字段里。输出是一个新的 CodexHttpClient,不会立刻发请求,也不会改外部状态。
调用关系:它通常在创建客户端时被调用,比如 create_client 这类搭建网络客户端的流程会先准备好底层 reqwest 客户端,再交给这里包装。后面的 get、post、request 都依赖这个包装好的对象继续工作。
调用图:被 3 处调用(new, create_client, revoke_request_times_out)。
CodexHttpClient::get26–31 ↗
fn get(&self, url: U) -> CodexRequestBuilder
作用:创建一个 GET 请求。GET 通常用来“取数据”,比如读取某个 token 或查询服务状态。
数据流:输入是一个网址。函数没有自己拼请求,而是把 HTTP 方法固定成 GET,再把网址交给 CodexHttpClient::request。输出是 CodexRequestBuilder,调用者可以继续加头、认证、超时等设置。
调用关系:它是更好用的快捷入口。比如 hydrate_personal_access_token 要读取个人访问 token 时会用它;它内部把真正的建请求工作交给 CodexHttpClient::request。
调用图:调用 1 个内部函数(request);被 1 处调用(hydrate_personal_access_token)。
CodexHttpClient::post33–38 ↗
fn post(&self, url: U) -> CodexRequestBuilder
作用:创建一个 POST 请求。POST 通常用来“提交数据”或“触发动作”,比如刷新 token、撤销 token。
数据流:输入是一个网址。函数把 HTTP 方法固定成 POST,然后调用 CodexHttpClient::request 来创建请求。输出是 CodexRequestBuilder,后续可以继续设置请求体、认证信息和超时。
调用关系:它是 POST 场景的快捷入口。request_chatgpt_token_refresh 和 revoke_oauth_token 这类需要向服务端提交操作的流程会用它;真正的请求构造仍由 CodexHttpClient::request 完成。
调用图:调用 1 个内部函数(request);被 2 处调用(request_chatgpt_token_refresh, revoke_oauth_token)。
CodexHttpClient::request40–46 ↗
fn request(&self, method: Method, url: U) -> CodexRequestBuilder
作用:按指定 HTTP 方法和网址创建一个请求构建器。它是 GET、POST 这些快捷方法背后的共同入口。
数据流:输入是 HTTP 方法和网址。它先把网址转成字符串留作日志用,再让底层 reqwest 客户端创建真正的请求对象,最后把请求对象、方法、网址一起装进 CodexRequestBuilder。输出是一个还没发送的请求构建器。
调用关系:CodexHttpClient::get 和 CodexHttpClient::post 都会调用它,其他需要自定义方法的构建流程也可以用它。它把底层 reqwest 的请求构造结果交给 CodexRequestBuilder::new 包装。
调用图:调用 1 个内部函数(new);被 3 处调用(get, post, build);外部调用 3 个(clone, as_str, request)。
CodexRequestBuilder::new58–64 ↗
fn new(builder: reqwest::RequestBuilder, method: Method, url: String) -> Self
作用:创建项目自己的请求构建器,并记住底层请求、HTTP 方法和网址。记住方法和网址主要是为了之后发请求时能写清楚日志。
数据流:输入是底层 reqwest::RequestBuilder、HTTP 方法和网址字符串。函数把三者存进 CodexRequestBuilder。输出是一个可继续添加配置、最后调用 send 发送的请求对象。
调用关系:它由 CodexHttpClient::request 调用,是从“底层请求对象”变成“项目专用请求对象”的转换点。后面的 headers、json、timeout、send 等方法都在这个对象上继续操作。
调用图:被 1 处调用(request)。
CodexRequestBuilder::map66–72 ↗
fn map(self, f: impl FnOnce(reqwest::RequestBuilder) -> reqwest::RequestBuilder) -> Self
作用:这是一个内部小工具,用来统一修改底层请求构建器,同时保留方法和网址这些日志信息。它避免每个设置函数都重复写一遍包装代码。
数据流:输入是当前 CodexRequestBuilder 和一个会修改 reqwest::RequestBuilder 的函数。它把底层 builder 交给这个函数处理,然后用处理后的 builder 加上原来的 method 和 url 重新组成新的 CodexRequestBuilder。输出是更新后的请求构建器。
调用关系:headers、header、bearer_auth、timeout、json、body 都调用它。它不直接被外部业务流程使用,而是作为这些链式设置方法的共同零件。
调用图:被 6 处调用(bearer_auth, body, header, headers, json, timeout)。
CodexRequestBuilder::headers74–76 ↗
fn headers(self, headers: HeaderMap) -> Self
作用:一次性给请求加上一整组 HTTP 请求头。请求头可以理解成请求随身携带的说明卡,比如内容类型、认证信息等。
数据流:输入是一张 HeaderMap,也就是多条请求头。函数通过 CodexRequestBuilder::map 把这些头加到底层请求上。输出是带有这些请求头的新请求构建器。
调用关系:它把具体修改工作交给 CodexRequestBuilder::map。调用者通常在发送前用它补充一批固定请求头,然后再调用 send。
调用图:调用 1 个内部函数(map)。
CodexRequestBuilder::header78–86 ↗
fn header(self, key: K, value: V) -> Self
作用:给请求加一条单独的 HTTP 请求头。适合只补一个字段,比如某个自定义标记。
数据流:输入是请求头的名字和值。底层 reqwest 会尝试把它们转换成合法的 HTTP 头格式;函数通过 CodexRequestBuilder::map 把这条头加进去。输出是更新后的请求构建器。
调用关系:它和 headers 类似,但一次只加一条。它依赖 CodexRequestBuilder::map 做统一包装,最后仍然由 send 发送。
调用图:调用 1 个内部函数(map)。
CodexRequestBuilder::bearer_auth88–93 ↗
fn bearer_auth(self, token: T) -> Self
作用:给请求加 Bearer token 认证。Bearer token 可以理解成“拿着这张通行证的人就被允许访问”。
数据流:输入是 token。函数把 token 交给底层请求构建器,让它生成标准的 Authorization 请求头。输出是带认证信息的新请求构建器。
调用关系:需要访问受保护接口时,调用者会在 send 前用它。它内部仍通过 CodexRequestBuilder::map 修改底层请求。
调用图:调用 1 个内部函数(map)。
CodexRequestBuilder::timeout95–97 ↗
fn timeout(self, timeout: Duration) -> Self
作用:设置这次请求最多等多久。这样服务端卡住时,客户端不会一直傻等。
数据流:输入是一个 Duration,也就是时间长度。函数把这个超时时间设置到底层请求上。输出是带超时限制的新请求构建器。
调用关系:调用者在需要控制等待时间时使用它,比如测试超时或避免网络请求无限挂起。它通过 CodexRequestBuilder::map 完成实际设置。
调用图:调用 1 个内部函数(map)。
CodexRequestBuilder::json99–104 ↗
fn json(self, value: &T) -> Self
作用:把一个 Rust 数据对象作为 JSON 请求体发出去。JSON 是网络接口里很常见的一种文本数据格式。
数据流:输入是一个可以序列化的数据对象,也就是能转换成 JSON 的值。函数让底层 reqwest 把它变成 JSON 请求体,并设置相应内容。输出是带 JSON body 的请求构建器。
调用关系:需要向服务端提交结构化数据时会用它。它把转换和设置交给底层 reqwest,并通过 CodexRequestBuilder::map 保持链式调用。
调用图:调用 1 个内部函数(map)。
CodexRequestBuilder::body106–111 ↗
fn body(self, body: B) -> Self
作用:直接设置请求体内容。它比 json 更底层,适合发送已经准备好的文本、字节流或其他 body。
数据流:输入是能转换成 reqwest::Body 的内容。函数把它放到底层请求中。输出是带请求体的新请求构建器。
调用关系:当调用者不想用 JSON 自动转换,而是要自己提供原始内容时会用它。它同样通过 CodexRequestBuilder::map 修改请求,最后由 send 发出。
调用图:调用 1 个内部函数(map)。
CodexRequestBuilder::send113–141 ↗
async fn send(self) -> Result<Response, reqwest::Error>
作用:真正把请求发到网络上。它还会自动加追踪请求头,并把成功或失败写进调试日志。
数据流:输入是已经配置好的 CodexRequestBuilder。函数先调用 trace_headers 生成追踪头,再把这些头加到底层请求上并等待网络响应。成功时输出 Response,并记录状态码、响应头等信息;失败时输出 reqwest::Error,并记录错误和可能的状态码。
调用关系:前面的 get、post、header、json 等都只是“准备请求”,send 才是执行点。它依赖 trace_headers 生成跨服务追踪信息,再调用底层 reqwest 的发送能力;日志通过 tracing::debug! 写出,方便后续排查。
调用图:调用 1 个内部函数(trace_headers);外部调用 2 个(headers, debug!)。
HeaderMapInjector::set147–154 ↗
fn set(&mut self, key: &str, value: String)
作用:把追踪系统给出的键值对塞进 HTTP 请求头里。它是 OpenTelemetry 和 HTTP HeaderMap 之间的“小转接头”。
数据流:输入是一个字符串形式的 key 和 value。函数尝试把 key 转成合法的 HTTP 头名,把 value 转成合法的 HTTP 头值;两者都合法时,就插入到 HeaderMap。输出没有返回值,但会修改那张请求头表;不合法的键值会被跳过。
调用关系:trace_headers 会把 HeaderMapInjector 交给 OpenTelemetry 的传播器使用。传播器负责决定要写哪些追踪字段,HeaderMapInjector::set 负责把字段真正放进 HTTP 请求头。
调用图:外部调用 2 个(from_bytes, from_str)。
trace_headers157–166 ↗
fn trace_headers() -> HeaderMap
作用:根据当前正在执行的 tracing span 生成一组 HTTP 追踪头。简单说,就是给即将发出的请求贴上“这属于哪条调用链”的标签。
数据流:输入不是显式参数,而是读取当前 Span 的上下文和全局 OpenTelemetry 文本传播器。函数创建一个空 HeaderMap,让传播器把当前追踪信息写进去。输出是装好追踪字段的 HeaderMap。
调用关系:CodexRequestBuilder::send 每次发请求前都会调用它。测试 tests::inject_trace_headers_uses_current_span_context 也会直接调用它,确认生成的请求头确实能还原出当前 span 的 trace id 和 span id。
调用图:被 2 处调用(send, inject_trace_headers_uses_current_span_context);外部调用 2 个(new, get_text_map_propagator)。
tests::inject_trace_headers_uses_current_span_context182–205 ↗
fn inject_trace_headers_uses_current_span_context()
作用:测试 trace_headers 是否真的使用了当前 span 的追踪上下文。它防止以后改代码时,不小心把跨服务追踪弄断。
数据流:测试先设置 OpenTelemetry 的 TraceContextPropagator,再创建 tracer 和 tracing subscriber。接着进入一个名为 client_request 的 span,记录这个 span 的 trace id 和 span id,然后调用 trace_headers 生成请求头。最后用 HeaderMapExtractor 把请求头读回追踪上下文,并断言读出的 trace id、span id 和原 span 一样。
调用关系:它直接验证 trace_headers 的核心行为。它还使用 tests::HeaderMapExtractor::get 和 tests::HeaderMapExtractor::keys,让 OpenTelemetry 的提取器能从 HeaderMap 里读数据。
调用图:调用 1 个内部函数(trace_headers);外部调用 8 个(builder, new, assert!, assert_eq!, set_text_map_propagator, trace_span!, layer, registry)。
tests::HeaderMapExtractor::get210–212 ↗
fn get(&self, key: &str) -> Option<&str>
作用:测试用的小工具:按名字从 HTTP 请求头里取出一个值。它让 OpenTelemetry 的测试代码能像读普通文本键值表一样读取 HeaderMap。
数据流:输入是一个请求头名字。函数在 HeaderMap 里查找这个名字,如果找到了并且值能转成字符串,就返回这个字符串;否则返回空。它不修改任何数据。
调用关系:它属于测试里的 HeaderMapExtractor,实现 OpenTelemetry 的 Extractor 接口。tests::inject_trace_headers_uses_current_span_context 通过这个提取器把 trace_headers 生成的头再解析回来。
tests::HeaderMapExtractor::keys214–216 ↗
fn keys(&self) -> Vec<&str>
作用:测试用的小工具:列出 HeaderMap 里所有请求头名字。这样 OpenTelemetry 在解析时知道有哪些字段可以看。
数据流:输入是测试提取器里引用的 HeaderMap。函数遍历所有请求头名,把它们转成字符串列表返回。它不改请求头内容。
调用关系:它和 tests::HeaderMapExtractor::get 一起实现测试所需的 Extractor 接口。测试里的 TraceContextPropagator 会通过它们读取 trace_headers 生成的追踪头。
login/src/auth/default_client.rs源码 ↗
这个文件像是 Codex 出门访问服务器前的“统一名片和交通工具”。服务器需要知道请求是谁发的、运行在哪个系统上、是不是有地区驻留要求,所以这里集中生成 originator(来源标记)、User-Agent(告诉服务器客户端名称、版本、系统信息的一串文字)和 residency header(地区要求请求头)。它还负责创建 reqwest HTTP 客户端,也就是实际发网络请求的工具。为了让全进程保持一致,它用全局变量保存默认来源和地区要求;这有点像公司前台统一给所有员工发同一种访客证。文件还会处理一些容易踩坑的事:如果 User-Agent 里有非法字符,会尽量替换掉;如果自定义证书或客户端构建失败,会记录警告并退回到普通客户端,保证旧调用方不被错误打断。
get_originator_value57–76 ↗
fn get_originator_value(provided: Option<String>) -> Originator
作用:把“这个请求是谁发起的”整理成一个可放进 HTTP 请求头的来源标记。它会优先看环境变量,其次看调用方传入的值,最后才用默认值。
数据流:进去的是一个可选的来源字符串,以及它会读取环境变量 CODEX_INTERNAL_ORIGINATOR_OVERRIDE → 它挑出最终要用的文字,并尝试把它转成 HTTP 请求头能接受的格式 → 出来的是 Originator,里面既有原始文字,也有已经验证过的请求头值;如果文字不合法,就记录错误并退回默认来源。
调用关系:set_default_originator 用它来保存全局默认来源;originator 在需要现算来源时也会用它。它是来源标记进入整个 HTTP 请求体系前的“验票口”。
调用图:被 2 处调用(originator, set_default_originator);外部调用 4 个(from_static, from_str, var, error!)。
set_default_originator78–91 ↗
fn set_default_originator(value: String) -> Result<(), SetOriginatorError>
作用:设置整个进程默认使用的来源标记。有人会在程序启动或初始化时调用它,让之后所有默认客户端都带上同一个 originator。
数据流:进去的是一个来源字符串 → 它先检查这个字符串能不能安全放进 HTTP 请求头,再交给 get_originator_value 做最终整理,然后尝试写入全局保存位置 → 成功后后续请求会用这个来源;如果字符串不合法或已经设置过,就返回错误。
调用关系:它会被 initialize 和 run_main 这类启动流程调用,通常只应该早早设置一次。它把具体整理工作交给 get_originator_value,之后 originator 会读到这个设置。
调用图:调用 1 个内部函数(get_originator_value);被 2 处调用(initialize, run_main);外部调用 1 个(from_str)。
set_default_client_residency_requirement93–99 ↗
fn set_default_client_residency_requirement(enforce_residency: Option<ResidencyRequirement>)
作用:设置默认 HTTP 客户端是否要带地区驻留要求。比如要求数据走美国区域时,后续请求头里会带上对应标记。
数据流:进去的是一个可选的 ResidencyRequirement(地区驻留要求) → 它拿到全局写锁,把这个要求存起来 → 之后 default_headers 生成请求头时会读取它;如果锁拿不到,只记录警告,不让程序崩溃。
调用关系:它会在 initialize、run_main、run_ratatui_app 等启动或界面运行流程里被调用。它本身不发请求,只是给 default_headers 准备一条之后要用的规则。
调用图:被 6 处调用(sync_default_client_residency_requirement, initialize, run_main, run_main, run_main, run_ratatui_app);外部调用 1 个(warn!)。
originator101–120 ↗
fn originator() -> Originator
作用:取出当前应该使用的来源标记。项目里很多地方需要知道“我是哪个 Codex 客户端”,都会从这里拿。
数据流:它先读全局缓存的来源 → 如果已有,就直接返回副本;如果没有但环境变量指定了覆盖值,就生成并尽量缓存起来;如果都没有,就按默认规则生成 → 出来的是可用于请求头和日志/元数据的 Originator。
调用关系:它被元数据生成、插件连接器筛选、工具发现、User-Agent 生成、默认请求头生成等很多流程调用。它把来源标记集中到一个入口,避免不同模块各自猜。
调用图:调用 1 个内部函数(get_originator_value);被 21 处调用(codex_app_metadata, codex_plugin_metadata, ingest_skill_invoked, connectors_for_plugin_apps, merge_and_filter_plugin_connectors, merge_connectors_with_accessible, list_accessible_connectors_from_mcp_tools_with_mcp_manager, list_tool_suggest_discoverable_tools_with_auth, refresh_accessible_connectors_cache_from_mcp_tools, maybe_prompt_and_install_mcp_dependencies (+11 more));外部调用 1 个(var)。
is_first_party_originator122–127 ↗
fn is_first_party_originator(originator_value: &str) -> bool
作用:判断一个来源标记是不是 Codex 自家客户端。这个判断会影响某些官方客户端才能走的行为。
数据流:进去的是一个来源字符串 → 它和几个已知的官方名字比较,也接受以“Codex ”开头的名字 → 出来是 true 或 false,表示是不是第一方来源。
调用关系:maybe_prompt_and_install_mcp_dependencies 会用它来决定是否提示或安装 MCP 依赖。它只是一个小判断函数,不改任何全局状态。
调用图:被 1 处调用(maybe_prompt_and_install_mcp_dependencies)。
is_first_party_chat_originator129–131 ↗
fn is_first_party_chat_originator(originator_value: &str) -> bool
作用:判断一个来源是不是 ChatGPT 相关的官方聊天客户端。它用于给某些连接器权限做区分。
数据流:进去的是来源字符串 → 它检查是否等于 codex_atlas 或 codex_chatgpt_desktop → 出来是布尔值,表示是不是受认可的官方聊天来源。
调用关系:is_connector_id_allowed_for_originator 会调用它,用来判断某个连接器是否允许给当前来源使用。
调用图:被 1 处调用(is_connector_id_allowed_for_originator)。
get_codex_user_agent133–157 ↗
fn get_codex_user_agent() -> String
作用:生成 Codex HTTP 请求使用的 User-Agent。User-Agent 可以理解成客户端递给服务器的一张自我介绍卡,里面有名称、版本、系统和终端环境信息。
数据流:它读取编译时版本号、当前操作系统信息、当前 originator、终端检测信息,以及可选的全局后缀 → 把这些拼成一条 User-Agent 字符串 → 再交给 sanitize_user_agent 清理非法字符,最后返回可用于请求头的字符串。
调用关系:initialize、认证流程、后端初始化、ChatGPT 请求头构建和 default_headers 都会用它。它依赖 originator 确定客户端名字,并把最后的安全检查交给 sanitize_user_agent。
调用图:调用 2 个内部函数(originator, sanitize_user_agent);被 5 处调用(initialize, from_auth, init_backend, build_chatgpt_headers, default_headers);外部调用 3 个(env!, format!, get)。
sanitize_user_agent164–189 ↗
fn sanitize_user_agent(candidate: String, fallback: &str) -> String
作用:把 User-Agent 清理成 HTTP 请求头能接受的安全字符串。它主要防止用户或外部系统提供的后缀里混入非法字符。
数据流:进去的是候选 User-Agent 和一个备用字符串 → 它先尝试直接作为请求头解析;不行就把不可见或非 ASCII 范围字符替换成下划线;还不行就退回备用字符串;再不行就退回 originator 的值 → 出来总是一个尽量可用的字符串,同时在降级时记录警告。
调用关系:它只被 get_codex_user_agent 调用,是 User-Agent 生成流程最后的安全网。它必要时会再调用 originator 拿最保守的兜底值。
调用图:调用 1 个内部函数(originator);被 1 处调用(get_codex_user_agent);外部调用 2 个(from_str, warn!)。
create_client192–195 ↗
fn create_client() -> CodexHttpClient
作用:创建项目通用的 CodexHttpClient。调用方不需要关心请求头、证书、代理等细节,只拿到一个能发 Codex 请求的客户端。
数据流:它没有外部输入 → 先调用 build_reqwest_client 创建底层 reqwest 客户端,再包装成 CodexHttpClient → 出来的是更符合 Codex 项目使用习惯的 HTTP 客户端。
调用关系:发送埋点、ChatGPT 请求、加载认证、撤销令牌、检查更新等地方都会调用它。它把底层创建工作交给 build_reqwest_client,自己负责包一层 Codex 专用外壳。
调用图:调用 2 个内部函数(new, build_reqwest_client);被 8 处调用(send_track_events_request, chatgpt_get_request_with_timeout, create_dummy_chatgpt_auth_for_testing, from_auth_dot_json, load, revoke_auth_tokens, check_for_update, fetch_latest_github_release_version)。
build_reqwest_client203–216 ↗
fn build_reqwest_client() -> reqwest::Client
作用:创建默认的 reqwest HTTP 客户端,并且尽量保证不会把错误抛给旧调用方。reqwest 是 Rust 里常用的发 HTTP 请求的库。
数据流:它没有直接输入 → 先调用 try_build_reqwest_client 尝试按完整规则创建客户端;如果失败,就记录警告,并尝试创建带 ChatGPT Cloudflare cookie 存储的备用客户端;如果备用也失败,就退回 reqwest::Client::new() → 出来总会尽量给调用方一个可用客户端。
调用关系:很多普通网络请求会直接用它,比如客户端管理、配对状态、远程控制、插件详情等。它是兼容旧代码的稳妥入口,具体的严格构建逻辑放在 try_build_reqwest_client。
调用图:调用 1 个内部函数(try_build_reqwest_client);被 38 处调用(send_client_management_request_once, pairing_status, start_pairing, send_remote_control_server_request, http_get_probe_status_with_timeout, http_probe_url_with_timeout, fetch_plugin_detail, fetch_recommended_plugins, fetch_remote_plugin_skill_detail, get_remote_plugin_installed_page (+15 more))。
try_build_reqwest_client222–230 ↗
fn try_build_reqwest_client() -> Result<reqwest::Client, BuildCustomCaTransportError>
作用:按完整规则尝试创建默认 reqwest 客户端,并把自定义证书相关错误交给调用方处理。它适合需要知道失败原因的地方。
数据流:它从默认请求头开始创建 builder(客户端建造器) → 如果检测到沙箱环境,就禁用代理;再加上 ChatGPT Cloudflare cookie 存储;最后交给自定义 CA 证书构建函数 → 成功返回 reqwest 客户端,失败返回结构化错误。
调用关系:build_reqwest_client 会调用它作为第一选择。它会用 default_headers 准备统一请求头,用 is_sandboxed 判断代理策略,并把证书加载交给 build_reqwest_client_with_custom_ca。
调用图:调用 2 个内部函数(default_headers, is_sandboxed);被 1 处调用(build_reqwest_client);外部调用 3 个(builder, build_reqwest_client_with_custom_ca, with_chatgpt_cloudflare_cookie_store)。
default_headers232–248 ↗
fn default_headers() -> HeaderMap
作用:生成每个默认 HTTP 请求都会带上的请求头。这里集中放 originator、User-Agent,以及可选的地区驻留要求。
数据流:它新建一个空请求头表 → 插入当前 originator;调用 get_codex_user_agent 生成并插入 User-Agent;再读取全局地区要求,如果有就插入 x-openai-internal-codex-residency → 出来是一组可直接挂到 HTTP 客户端或 WebSocket 请求上的默认头。
调用关系:WebSocket 检查、WebSocket 连接、后台启动、WebRTC 辅助任务和 try_build_reqwest_client 都会用它。它把 originator 和 get_codex_user_agent 的结果汇总成真正随请求发出去的东西。
调用图:调用 2 个内部函数(get_codex_user_agent, originator);被 5 处调用(websocket_reachability_check, connect_websocket, start_inner, spawn_webrtc_sideband_input_task, try_build_reqwest_client);外部调用 3 个(new, from_static, from_str)。
is_sandboxed250–252 ↗
fn is_sandboxed() -> bool
作用:判断当前程序是不是运行在 Codex 特定的沙箱环境里。沙箱里代理策略可能不同,所以要单独识别。
数据流:它读取环境变量 CODEX_SANDBOX → 判断它是否等于 seatbelt → 返回 true 或 false,不修改任何状态。
调用关系:try_build_reqwest_client 会用它来决定是否禁用代理。它是客户端构建流程里的一个小环境探测器。
调用图:被 1 处调用(try_build_reqwest_client);外部调用 1 个(var)。
面向后端的服务客户端
这些文件将共享传输基础应用到具体的认证客户端,用于后端 API、云任务、远程配置加载、ChatGPT 辅助工具和 LM Studio。
backend-client/src/client.rs源码 ↗
可以把这个文件想成一名前台代办员:别人告诉它“我要查任务列表”或“我要创建任务”,它负责找到正确窗口、带上身份证明、递交表格、拿回结果。这里的 Client 保存服务器地址、HTTP 工具、登录信息、用户代理等;PathStyle 用来区分两套后端路径,一套是 /api/codex/...,另一套是 ChatGPT 的 /wham/...;RequestError 则把请求失败的情况说清楚,尤其能判断是不是没登录或登录过期。文件里还把后端返回的额度、套餐、余额等原始数据,转换成项目内部更统一的格式。这样上层代码不用关心后端细节,也不用到处重复写请求代码。
RequestError::status46–51 ↗
fn status(&self) -> Option<StatusCode>
作用:从一次请求错误里取出 HTTP 状态码。HTTP 状态码就是服务器用数字告诉你结果,比如 401 表示未授权。
数据流:输入是一条 RequestError → 如果它是服务器返回了非成功状态,就拿出里面的状态码;如果只是普通内部错误,就没有状态码 → 输出一个可能存在的状态码。
调用关系:它主要被 RequestError::is_unauthorized 使用,后者借它判断错误是不是“没登录或权限不够”。
调用图:被 1 处调用(is_unauthorized)。
RequestError::is_unauthorized53–55 ↗
fn is_unauthorized(&self) -> bool
作用:判断这次请求失败是不是 401 未授权。调用者可以用它决定是否提示用户重新登录。
数据流:输入是一条请求错误 → 先调用 RequestError::status 取状态码 → 如果状态码正好是 401,就输出 true,否则输出 false。
调用关系:它站在错误处理的入口位置,把底层状态码翻译成上层更关心的问题:是不是认证失败。
调用图:调用 1 个内部函数(status)。
RequestError::fmt59–73 ↗
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
作用:把请求错误变成一段人能看懂的文字。这样日志、报错信息里不会只出现晦涩的内部结构。
数据流:输入是一条错误和一个文字输出器 → 如果是服务器状态异常,就把请求方法、网址、状态码、内容类型、返回正文拼成一句话;如果是普通错误,就直接显示原错误 → 输出格式化后的错误文本。
调用关系:当错误被打印、记录日志或展示给调用者时会走到这里;它把 RequestError 接到 Rust 标准错误显示机制上。
调用图:外部调用 1 个(write!)。
RequestError::source77–82 ↗
fn source(&self) -> Option<&(dyn std::error::Error + 'static)>
作用:告诉错误系统,这个错误背后有没有更底层的原始错误。这样排查问题时可以顺藤摸瓜。
数据流:输入是一条请求错误 → 如果它包着一个 anyhow 错误,就把这个底层错误交出去;如果只是 HTTP 状态不成功,就没有更底层来源 → 输出可选的错误来源。
调用关系:它是标准错误接口的一部分,供日志库、错误链展示工具等在需要时查看根因。
RequestError::from86–88 ↗
fn from(err: anyhow::Error) -> Self
作用:把通用错误转换成本文件自己的请求错误。这样函数可以统一返回 RequestError。
数据流:输入一个 anyhow::Error 通用错误 → 包装成 RequestError::Other → 输出统一类型的请求错误。
调用关系:它让 get_config_bundle 等使用详细错误类型的函数,能方便地接住 JSON 解析等普通错误。
调用图:外部调用 1 个(Other)。
PathStyle::from_base_url112–118 ↗
fn from_base_url(base_url: &str) -> Self
作用:根据服务器地址判断应该使用哪套路由路径。简单说,就是看请求该发到 Codex 风格接口,还是 ChatGPT 后端接口。
数据流:输入一个基础网址字符串 → 检查里面是否包含 /backend-api → 包含就输出 ChatGptApi,否则输出 CodexApi。
调用关系:它被 Client::new 调用,在客户端创建时一次性决定后面所有接口该怎么拼 URL。
调用图:被 1 处调用(new)。
Client::fmt133–145 ↗
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
作用:把 Client 打印成调试信息,但不会把认证提供器的敏感细节直接暴露出来。
数据流:输入一个客户端对象和调试输出器 → 写出基础地址、用户代理、账号 ID、FedRAMP 标记、路径风格等信息,并把认证提供器隐藏成占位文字 → 输出调试文本。
调用关系:当开发者打印 Client 排查问题时会用到它;它服务于诊断,不参与实际发请求。
调用图:外部调用 1 个(debug_struct)。
Client::new149–175 ↗
fn new(base_url: impl Into<String>) -> Result<Self>
作用:创建一个新的后端客户端。它会整理服务器地址、准备 HTTP 连接工具,并选择正确的路径风格。
数据流:输入一个基础网址 → 去掉末尾多余斜杠;如果是 chatgpt.com 或 chat.openai.com 且缺少 /backend-api,就自动补上;然后创建带自定义证书和 Cloudflare cookie 支持的 HTTP 客户端;最后填入默认未登录认证状态 → 输出可用的 Client。
调用关系:这是大多数使用流程的起点。Client::from_auth 会在它基础上加登录信息,测试辅助函数也会构造类似客户端。
调用图:调用 1 个内部函数(from_base_url);被 18 处调用(websocket_transport_serves_health_endpoints_on_same_listener, test_client, test_client, new, models_client_hits_models_endpoint, responses_post_drains_request_body, revoke_request_times_out, cancels_previous_login_server_when_port_is_in_use, creates_missing_codex_home_dir, forced_chatgpt_workspace_id_mismatch_blocks_login (+8 more));外部调用 10 个(contains, ends_with, into, pop, starts_with, builder, build_reqwest_client_with_custom_ca, with_chatgpt_cloudflare_cookie_store, unauthenticated_auth_provider, format!)。
Client::from_auth177–181 ↗
fn from_auth(base_url: impl Into<String>, auth: &CodexAuth) -> Result<Self>
作用:用已有登录信息创建客户端。适合用户已经登录,后续请求需要带上身份凭证的场景。
数据流:输入基础网址和 CodexAuth 登录对象 → 先调用 Client::new 建基础客户端;再设置默认用户代理;再把登录对象转换成认证头提供器 → 输出带认证能力的客户端。
调用关系:它把“创建连接工具”和“套上登录身份”串起来,是上层登录后访问后端的便捷入口。
调用图:调用 1 个内部函数(get_codex_user_agent);外部调用 2 个(new, auth_provider_from_auth)。
Client::with_auth_provider183–186 ↗
fn with_auth_provider(mut self, auth: SharedAuthProvider) -> Self
作用:给客户端换上指定的认证提供器。认证提供器就是负责往请求里加登录凭证的组件。
数据流:输入一个客户端和新的认证提供器 → 把客户端内部的认证提供器替换掉 → 输出修改后的客户端。
调用关系:常在构建客户端时链式调用,比如 Client::from_auth 会用它把登录信息装进客户端。
Client::with_user_agent188–193 ↗
Client::with_chatgpt_account_id195–198 ↗
fn with_chatgpt_account_id(mut self, account_id: impl Into<String>) -> Self
作用:指定 ChatGPT 账号 ID,让后端知道请求对应哪个账号或工作区。
数据流:输入一个客户端和账号 ID → 把账号 ID 存到客户端里 → 输出修改后的客户端。
调用关系:设置后,Client::headers 会在每个请求头里加入 ChatGPT-Account-Id。
调用图:外部调用 1 个(into)。
Client::with_fedramp_routing_header200–203 ↗
fn with_fedramp_routing_header(mut self) -> Self
作用:打开 FedRAMP 路由标记。FedRAMP 可以理解为美国政府合规环境相关的特殊通道标识。
数据流:输入一个客户端 → 把内部的 FedRAMP 标记设为 true → 输出修改后的客户端。
调用关系:设置后,Client::headers 会给请求加上 X-OpenAI-Fedramp: true,让服务器按特殊环境处理。
Client::with_path_style205–208 ↗
fn with_path_style(mut self, style: PathStyle) -> Self
作用:手动指定客户端使用哪种后端路径风格。测试或特殊场景可以绕过自动判断。
数据流:输入一个客户端和路径风格 → 替换客户端里的路径风格 → 输出修改后的客户端。
调用关系:影响所有后续拼 URL 的函数,比如查任务、查配置、发邮件提醒等。
Client::headers210–230 ↗
fn headers(&self) -> HeaderMap
作用:为一次 HTTP 请求准备通用请求头。请求头可以理解为随信附上的身份、来源和特殊说明。
数据流:输入客户端当前状态 → 创建空头表;放入用户代理;让认证提供器加入登录凭证;如果有账号 ID 和 FedRAMP 标记,也加入对应头 → 输出完整的请求头集合。
调用关系:几乎所有真正访问后端的函数都会先调用它,再把这些头贴到 GET 或 POST 请求上。
调用图:被 8 处调用(create_task, get_accounts_check, get_config_bundle, get_task_details_with_body, get_token_usage_profile, list_sibling_turns, list_tasks, send_add_credits_nudge_email);外部调用 5 个(new, from_bytes, from_static, from_str, add_auth_headers)。
Client::exec_request232–251 ↗
async fn exec_request(
&self,
req: reqwest::RequestBuilder,
method: &str,
url: &str,
) -> Result<(String, String)>
作用:发送普通 HTTP 请求,并把成功响应的正文拿回来。失败时直接用通用错误说明原因。
数据流:输入一个已准备好的请求、请求方法和网址 → 发到网络;读取状态码、内容类型和返回正文;如果状态码不是成功,就报错;成功则输出正文和内容类型。
调用关系:它是多数接口函数的共同执行器,比如查账号、查任务、创建任务都会把请求交给它发送。
调用图:被 6 处调用(create_task, get_accounts_check, get_task_details_with_body, get_token_usage_profile, list_sibling_turns, list_tasks);外部调用 2 个(send, bail!)。
Client::exec_request_detailed253–278 ↗
async fn exec_request_detailed(
&self,
req: reqwest::RequestBuilder,
method: &str,
url: &str,
) -> std::result::Result<(String, String), RequestError>
作用:发送 HTTP 请求,但失败时保留更详细、更结构化的错误信息。特别适合需要判断 401 未授权的调用。
数据流:输入请求、方法和网址 → 发请求并读取状态码、内容类型、正文;如果失败,就构造 RequestError::UnexpectedStatus,里面保留这些细节;成功则输出正文和内容类型。
调用关系:它被 get_config_bundle 和 send_add_credits_nudge_email 使用,因为这些地方更需要精确知道失败类型。
调用图:被 2 处调用(get_config_bundle, send_add_credits_nudge_email);外部调用 1 个(send)。
Client::decode_json280–287 ↗
fn decode_json(&self, url: &str, ct: &str, body: &str) -> Result<T>
作用:把服务器返回的 JSON 文本解析成程序里的结构。JSON 是常见的数据文本格式,像带层级的表格。
数据流:输入网址、内容类型和返回正文 → 尝试按目标类型解析 JSON;成功输出对应对象;失败就报出网址、内容类型和原文,方便排查服务器返回了什么。
调用关系:它接在 exec_request 之后,被查账号、查任务详情、查 token 使用画像等函数使用。
调用图:被 3 处调用(get_accounts_check, get_task_details_with_body, get_token_usage_profile);外部调用 1 个(bail!)。
Client::get_rate_limits289–296 ↗
async fn get_rate_limits(&self) -> Result<RateLimitSnapshot>
作用:获取当前用户最主要的一份额度限制快照。额度限制就是还剩多少可用次数或资源的状态。
数据流:输入客户端 → 调用 Client::get_rate_limits_many 拿到多份限制快照;优先挑 limit_id 为 codex 的那份;没有的话就用第一份 → 输出单个额度快照。
调用关系:它是上层只关心“主要额度状态”时的入口,把多额度的复杂情况简化成一份结果。
调用图:调用 1 个内部函数(get_rate_limits_many)。
Client::get_rate_limits_many298–300 ↗
async fn get_rate_limits_many(&self) -> Result<Vec<RateLimitSnapshot>>
作用:获取所有额度限制快照,而不是只拿主要的一项。适合界面或逻辑需要展示多个限制的场景。
数据流:输入客户端 → 调用同一实现块中由 rate_limit_resets 模块补充的请求逻辑,拿到带重置和积分信息的结果 → 取出其中的 rate_limits 列表输出。
调用关系:它被 Client::get_rate_limits 调用;真正联网获取额度状态的细节放在相邻的 rate_limit_resets 模块里。
调用图:被 1 处调用(get_rate_limits)。
Client::get_accounts_check302–310 ↗
async fn get_accounts_check(&self) -> Result<AccountsCheckResponse>
作用:向后端询问当前账号是否可用、状态如何。常用于启动或进入功能前做资格检查。
数据流:输入客户端 → 根据路径风格拼出账号检查 URL;加上通用请求头;发送 GET 请求;把 JSON 响应解析成 AccountsCheckResponse → 输出账号检查结果。
调用关系:它把 headers、exec_request、decode_json 串起来,是一个典型的“查后端并解析结果”的接口函数。
调用图:调用 3 个内部函数(decode_json, exec_request, headers);外部调用 2 个(get, format!)。
Client::get_token_usage_profile312–317 ↗
async fn get_token_usage_profile(&self) -> Result<TokenUsageProfile>
作用:获取当前用户的 token 使用画像。token 可以粗略理解为模型处理文字时计算用量的单位。
数据流:输入客户端 → 调用 Client::token_usage_profile_url 得到正确网址;加请求头;发送 GET;解析 JSON → 输出 TokenUsageProfile。
调用关系:它负责对外提供清楚的业务动作;具体 URL 拼法交给 Client::token_usage_profile_url,网络发送交给 Client::exec_request。
调用图:调用 4 个内部函数(decode_json, exec_request, headers, token_usage_profile_url);外部调用 1 个(get)。
Client::token_usage_profile_url319–324 ↗
fn token_usage_profile_url(&self) -> String
作用:拼出“查看我的 token 使用画像”的接口地址。它隐藏了两套后端路径的差别。
数据流:输入客户端里的基础地址和路径风格 → 如果是 Codex 风格,拼 /api/codex/profiles/me;如果是 ChatGPT 风格,拼 /wham/profiles/me → 输出完整 URL 字符串。
调用关系:它只被 Client::get_token_usage_profile 调用,让主函数不用塞进路径判断细节。
调用图:被 1 处调用(get_token_usage_profile);外部调用 1 个(format!)。
Client::send_add_credits_nudge_email326–339 ↗
async fn send_add_credits_nudge_email(
&self,
credit_type: AddCreditsNudgeCreditType,
) -> std::result::Result<(), RequestError>
作用:请求后端发送一封“提醒加额度/加用量限制”的邮件。这个动作常用于用户额度不够时提醒管理员或相关人员。
数据流:输入客户端和提醒类型 → 拼出发送邮件的 URL;构造 POST 请求;带上认证头和 JSON 正文;用详细请求执行器发送;成功输出空结果,失败输出 RequestError。
调用关系:它调用 Client::send_add_credits_nudge_email_url 负责地址,调用 Client::exec_request_detailed 保留失败细节。
调用图:调用 3 个内部函数(exec_request_detailed, headers, send_add_credits_nudge_email_url);外部调用 2 个(from_static, post)。
Client::list_tasks341–375 ↗
async fn list_tasks(
&self,
limit: Option<i32>,
task_filter: Option<&str>,
environment_id: Option<&str>,
cursor: Option<&str>,
) -> Result<PaginatedListTask
作用:获取任务列表,并可按数量、过滤条件、环境、翻页游标来筛选。游标可以理解为“下一页从这里继续”的标记。
数据流:输入可选的 limit、task_filter、environment_id、cursor → 拼任务列表 URL;逐个把存在的筛选条件加到查询参数里;发送 GET;解析 JSON → 输出分页任务列表。
调用关系:它被上层的列表命令或列表流程调用,内部复用 headers 和 exec_request 完成实际网络访问。
调用图:调用 2 个内部函数(exec_request, headers);被 1 处调用(list);外部调用 2 个(get, format!)。
Client::get_task_details377–380 ↗
async fn get_task_details(&self, task_id: &str) -> Result<CodeTaskDetailsResponse>
作用:按任务 ID 获取一个任务的详细信息。它只返回解析后的结果,不关心原始响应文本。
数据流:输入任务 ID → 调用 Client::get_task_details_with_body 获取详情、原文和内容类型 → 丢掉原文,只输出解析后的 CodeTaskDetailsResponse。
调用关系:它是运行流程里常用的简洁入口;需要调试原始响应时则用更底层的 get_task_details_with_body。
调用图:调用 1 个内部函数(get_task_details_with_body);被 1 处调用(run)。
Client::get_task_details_with_body382–394 ↗
async fn get_task_details_with_body(
&self,
task_id: &str,
) -> Result<(CodeTaskDetailsResponse, String, String)>
作用:获取任务详情,同时保留服务器原始返回正文和内容类型。适合既要业务数据,又要调试信息的场景。
数据流:输入任务 ID → 根据路径风格拼任务详情 URL;发送带认证头的 GET 请求;解析 JSON 成任务详情;同时保留原始 body 和 content-type → 输出三者组成的结果。
调用关系:它被 Client::get_task_details 和需要原始正文的详情流程调用,是任务详情请求的核心实现。
调用图:调用 3 个内部函数(decode_json, exec_request, headers);被 2 处调用(get_task_details, details_with_body);外部调用 2 个(get, format!)。
Client::list_sibling_turns396–414 ↗
async fn list_sibling_turns(
&self,
task_id: &str,
turn_id: &str,
) -> Result<TurnAttemptsSiblingTurnsResponse>
作用:列出同一个任务里某一轮对话的“兄弟轮次”。可以理解为同一位置上的不同尝试或分支。
数据流:输入任务 ID 和轮次 ID → 拼出 sibling_turns 地址;加请求头;发送 GET;解析 JSON → 输出兄弟轮次列表响应。
调用关系:它被上层列表功能调用,帮助界面或命令行展示某个任务分支周围的其他尝试。
调用图:调用 2 个内部函数(exec_request, headers);被 1 处调用(list);外部调用 2 个(get, format!)。
Client::get_config_bundle420–431 ↗
async fn get_config_bundle(
&self,
) -> std::result::Result<ConfigBundleResponse, RequestError>
作用:从后端获取云端管理的配置包。配置包会影响客户端该启用哪些功能或采用哪些设置。
数据流:输入客户端 → 按路径风格拼配置包 URL;发送带认证头的 GET;如果请求失败,用 RequestError 保留状态细节;成功后解析 JSON → 输出 ConfigBundleResponse。
调用关系:它通常在配置加载或刷新时使用;因为配置失败可能和登录状态有关,所以用 exec_request_detailed。
调用图:调用 2 个内部函数(exec_request_detailed, headers);外部调用 2 个(get, format!)。
Client::create_task435–466 ↗
async fn create_task(&self, request_body: serde_json::Value) -> Result<String>
作用:向后端创建一个新任务,也就是提交一次新的用户请求或对话轮次。成功后拿回新任务 ID。
数据流:输入 JSON 请求正文 → 根据路径风格拼任务创建 URL;发送 POST,并声明正文是 JSON;读取响应;从返回 JSON 中优先找 task.id,找不到再找顶层 id;找到则输出任务 ID,否则报错。
调用关系:它被上层创建任务流程调用;网络发送交给 Client::exec_request,请求头交给 Client::headers。
调用图:调用 2 个内部函数(exec_request, headers);被 1 处调用(create);外部调用 4 个(from_static, bail!, post, format!)。
Client::rate_limit_snapshots_from_payload469–505 ↗
fn rate_limit_snapshots_from_payload(
payload: RateLimitStatusPayload,
) -> Vec<RateLimitSnapshot>
作用:把后端原始的额度状态数据,整理成项目内部统一的额度快照列表。这样上层不用理解后端返回的复杂嵌套格式。
数据流:输入 RateLimitStatusPayload → 转换套餐类型、达到限制的原因、个人花费控制、主额度和附加额度;先生成 codex 主快照,再把附加限制逐个变成额外快照 → 输出快照列表。
调用关系:它主要被测试验证;实际额度获取流程也依赖这种转换思路,把后端模型变成协议层模型。
调用图:被 4 处调用(usage_payload_maps_every_rate_limit_reached_type, usage_payload_maps_primary_and_additional_rate_limits, usage_payload_maps_zero_rate_limit_when_primary_absent, usage_payload_preserves_absent_rate_limit_reached_type);外部调用 2 个(map_plan_type, vec!)。
Client::make_rate_limit_snapshot507–533 ↗
fn make_rate_limit_snapshot(
limit_id: Option<String>,
limit_name: Option<String>,
rate_limit: Option<crate::types::RateLimitStatusDetails>,
credits: Option<crate::type
作用:组装一份单独的额度快照。它把窗口、积分、套餐、限制原因等零件装成一个完整对象。
数据流:输入限制 ID、名字、窗口信息、积分信息、个人限制、套餐和触发原因 → 调用窗口和积分转换函数;把所有字段填入 RateLimitSnapshot → 输出一份标准快照。
调用关系:它被 Client::rate_limit_snapshots_from_payload 间接用于主额度和附加额度的统一构造。
调用图:外部调用 2 个(map_credits, map_rate_limit_window)。
Client::map_rate_limit_reached_type535–556 ↗
fn map_rate_limit_reached_type(
kind: BackendRateLimitReachedKind,
) -> Option<RateLimitReachedType>
作用:把后端说的“为什么达到限制”翻译成项目内部认识的说法。未知原因会被忽略。
数据流:输入后端的限制原因枚举 → 按每种情况对应到内部枚举,比如普通限流、工作区额度耗尽、成员用量上限达到等;如果是 Unknown,就输出空 → 输出可选的内部原因。
调用关系:它服务于额度状态转换,让上层看到统一的 RateLimitReachedType,不直接依赖后端模型。
Client::send_add_credits_nudge_email_url558–571 ↗
fn send_add_credits_nudge_email_url(&self) -> String
作用:拼出发送加额度提醒邮件的接口地址。它专门处理 Codex 和 ChatGPT 两套路径差异。
数据流:输入客户端里的基础地址和路径风格 → Codex 风格拼 /api/codex/accounts/send_add_credits_nudge_email;ChatGPT 风格拼 /wham/accounts/send_add_credits_nudge_email → 输出完整 URL。
调用关系:它被 Client::send_add_credits_nudge_email 调用,使发送函数只关心业务动作,不堆路径字符串。
调用图:被 1 处调用(send_add_credits_nudge_email);外部调用 1 个(format!)。
Client::map_rate_limit_window573–586 ↗
fn map_rate_limit_window(
window: Option<Option<Box<crate::types::RateLimitWindowSnapshot>>>,
) -> Option<RateLimitWindow>
作用:把后端的一段额度时间窗口转换成内部格式。时间窗口就是“在这段时间内用了多少、何时重置”。
数据流:输入可能为空、且嵌套包装的后端窗口数据 → 如果没有数据就输出空;有数据则取使用百分比、把秒数换算成分钟、记录重置时间 → 输出内部 RateLimitWindow。
调用关系:它被 Client::make_rate_limit_snapshot 用来处理主窗口和次窗口,是额度快照转换的一块小零件。
调用图:外部调用 3 个(window_minutes_from_seconds, from, from)。
Client::map_credits588–596 ↗
fn map_credits(credits: Option<crate::types::CreditStatusDetails>) -> Option<CreditsSnapshot>
作用:把后端的积分/余额信息转换成内部格式。积分可以理解为账户里可用的付费余额或额度。
数据流:输入可选的后端积分详情 → 如果没有详情就输出空;有详情则保留是否有积分、是否无限、余额文本 → 输出 CreditsSnapshot。
调用关系:它被 Client::make_rate_limit_snapshot 调用,把积分信息放进完整额度快照里。
Client::map_individual_limit598–607 ↗
fn map_individual_limit(
details: crate::types::SpendControlLimitDetails,
) -> SpendControlLimitSnapshot
作用:把个人花费控制限制转换成内部格式。花费控制就是某个人或成员被允许使用的上限。
数据流:输入后端个人限制详情 → 取出总限制、已用量、剩余百分比和重置时间 → 输出 SpendControlLimitSnapshot。
调用关系:它在 Client::rate_limit_snapshots_from_payload 处理 spend control 数据时使用,最后进入主额度快照。
调用图:外部调用 1 个(from)。
Client::map_plan_type609–632 ↗
fn map_plan_type(plan_type: crate::types::PlanType) -> AccountPlanType
作用:把后端的套餐类型翻译成协议层使用的套餐类型。比如 Free、Plus、Pro、Team、Enterprise 等。
数据流:输入后端套餐枚举 → 按已知类型一一映射到内部账户套餐枚举;一些旧类型、访客类型或未知类型统一归为 Unknown → 输出内部套餐类型。
调用关系:它被额度转换流程使用,也有专门测试确保用量计费类商业套餐不会被错误归类。
Client::window_minutes_from_seconds634–641 ↗
fn window_minutes_from_seconds(seconds: i32) -> Option<i64>
作用:把秒数转换成向上取整的分钟数。比如 61 秒算 2 分钟,避免显示成不够准确的 1 分钟。
数据流:输入秒数 → 如果小于等于 0,就表示没有有效窗口,输出空;否则转成 64 位整数并向上除以 60 → 输出分钟数。
调用关系:它被 Client::map_rate_limit_window 调用,用于把后端的秒级窗口变成更适合展示的分钟值。
调用图:外部调用 1 个(from)。
tests::map_plan_type_supports_usage_based_business_variants653–662 ↗
fn map_plan_type_supports_usage_based_business_variants()
作用:测试按用量计费的商业套餐能被正确识别。它防止新增或特殊套餐被错误当成未知类型。
数据流:输入固定的两个后端套餐类型 → 调用 Client::map_plan_type → 比较输出是否等于预期的内部套餐类型。
调用关系:它守住套餐映射这块逻辑,避免以后改代码时破坏商业套餐分类。
调用图:外部调用 1 个(assert_eq!)。
tests::usage_payload_maps_primary_and_additional_rate_limits665–771 ↗
fn usage_payload_maps_primary_and_additional_rate_limits()
作用:测试完整额度数据能被正确转换,包括主限制、附加限制、积分、套餐、个人限制和触发原因。
数据流:输入一份手工构造的后端额度 payload → 调用 Client::rate_limit_snapshots_from_payload → 检查输出列表长度和每个字段是否符合预期。
调用关系:它覆盖额度转换的主路径,确保多个小转换函数配合起来不会丢字段或填错字段。
调用图:调用 1 个内部函数(rate_limit_snapshots_from_payload);外部调用 4 个(new, default, assert_eq!, vec!)。
tests::usage_payload_maps_zero_rate_limit_when_primary_absent774–795 ↗
fn usage_payload_maps_zero_rate_limit_when_primary_absent()
作用:测试主额度信息缺失时,转换逻辑仍然能给出合理结果,而不是崩溃。
数据流:输入一份没有主 rate limit、但有附加限制的 payload → 调用转换函数 → 检查输出仍有主快照和附加快照,只是窗口为空。
调用关系:它保证后端少返回某些字段时,客户端仍能稳住,适合真实网络数据不完整的情况。
调用图:调用 1 个内部函数(rate_limit_snapshots_from_payload);外部调用 2 个(assert_eq!, vec!)。
tests::preferred_snapshot_selection_matches_get_rate_limits_behavior798–836 ↗
fn preferred_snapshot_selection_matches_get_rate_limits_behavior()
作用:测试选择主要额度快照时会优先选 codex。这和 Client::get_rate_limits 的行为保持一致。
数据流:输入两份手工快照,一份是其他限制,一份是 codex → 模拟查找逻辑 → 检查最后选中的是 codex。
调用关系:它保护 get_rate_limits 的选择规则,避免以后改动导致界面展示错主额度。
调用图:外部调用 1 个(assert_eq!)。
tests::usage_payload_maps_every_rate_limit_reached_type839–877 ↗
fn usage_payload_maps_every_rate_limit_reached_type()
作用:测试每一种后端“达到限制原因”都能映射到正确的内部原因,未知原因除外。
数据流:输入多组后端原因和预期结果 → 每组构造 payload 并调用转换函数 → 检查输出快照里的原因是否匹配。
调用关系:它覆盖 Client::map_rate_limit_reached_type 的所有分支,防止某种限制原因显示错。
调用图:调用 1 个内部函数(rate_limit_snapshots_from_payload);外部调用 1 个(assert_eq!)。
tests::usage_payload_preserves_absent_rate_limit_reached_type880–892 ↗
fn usage_payload_preserves_absent_rate_limit_reached_type()
作用:测试后端没有提供限制原因时,客户端也保持为空,而不是编造一个原因。
数据流:输入一份没有 rate_limit_reached_type 的 payload → 调用转换函数 → 检查输出中的原因字段是空。
调用关系:它补足额度原因转换的空值场景,保证数据含义不会被客户端误改。
调用图:调用 1 个内部函数(rate_limit_snapshots_from_payload);外部调用 1 个(assert_eq!)。
tests::add_credits_nudge_email_uses_expected_paths_and_bodies895–922 ↗
fn add_credits_nudge_email_uses_expected_paths_and_bodies()
作用:测试发送加额度提醒邮件时,URL 和 JSON 正文都符合预期。
数据流:输入两个测试客户端,分别代表 Codex 路径和 ChatGPT 路径 → 调用 URL 拼接函数并序列化请求体 → 检查路径和 credit_type 字段字符串是否正确。
调用关系:它保护 Client::send_add_credits_nudge_email_url 和请求体格式,避免后端接口因为路径或字段名变化而收不到请求。
调用图:外部调用 2 个(assert_eq!, test_client)。
tests::token_usage_profile_uses_expected_paths925–937 ↗
fn token_usage_profile_uses_expected_paths()
作用:测试 token 使用画像接口在两种路径风格下都能拼出正确地址。
数据流:输入 Codex 风格和 ChatGPT 风格的测试客户端 → 调用 Client::token_usage_profile_url → 检查输出 URL 是否等于预期。
调用关系:它保护 Client::get_token_usage_profile 依赖的 URL 拼接逻辑。
调用图:外部调用 2 个(assert_eq!, test_client)。
tests::test_client939–949 ↗
fn test_client(base_url: &str, path_style: PathStyle) -> Client
作用:为测试快速造一个客户端,不走完整网络客户端初始化流程。这样测试可以专注检查路径和数据转换。
数据流:输入基础网址和路径风格 → 手工填入 Client 的各个字段,使用普通 reqwest 客户端和未登录认证提供器 → 输出测试用客户端。
调用关系:它被多个测试调用,用来生成可控的 Client,特别适合验证 URL 拼接函数。
调用图:调用 1 个内部函数(new);外部调用 1 个(unauthenticated_auth_provider)。
chatgpt/src/chatgpt_client.rs源码 ↗
这个文件解决的是“安全地向 ChatGPT 后端要数据”的问题。程序不能随便发请求,必须先确认用户已经登录,而且登录方式确实能访问 Codex 后端;还要确认账号 ID 存在,否则后端可能不知道是谁在请求。它先从配置里拿到 ChatGPT 的基础网址,再通过 AuthManager(一位“保管登录票据的人”)取出认证信息。之后它创建 HTTP 客户端,把路径拼成完整网址,加上认证头和产品标识头,发出 GET 请求。如果调用方给了超时时间,它还会给这次请求设置“最多等多久”。返回成功时,它把后端返回的 JSON(一种常见的数据文本格式)解析成调用方想要的类型;失败时,它会把状态码和响应内容放进错误里,方便排查。可以把它理解成一个统一的“前台代办窗口”:别人只说要哪个接口的数据,它负责带证件、填地址、发请求、验结果。
chatgpt_get_request13–18 ↗
async fn chatgpt_get_request(
config: &Config,
path: String,
) -> anyhow::Result<T>
作用:这是最简单的 ChatGPT 后端 GET 请求入口。调用者只要给配置和接口路径,它就去请求数据,不额外设置超时时间。
数据流:输入是一份 Config 配置和一个接口 path。它不自己发网络请求,而是把这些原样交给 chatgpt_get_request_with_timeout,并明确告诉它“没有特殊超时限制”。最后输出的是解析好的结果,或者请求、认证、解析过程中产生的错误。
调用关系:它是更方便的包装函数。get_task 会用它来获取任务数据;它把真正复杂的认证、拼网址、发请求工作交给 chatgpt_get_request_with_timeout。
调用图:调用 1 个内部函数(chatgpt_get_request_with_timeout);被 1 处调用(get_task)。
chatgpt_get_request_with_timeout20–72 ↗
async fn chatgpt_get_request_with_timeout(
config: &Config,
path: String,
timeout: Option<Duration>,
) -> anyhow::Result<T>
作用:这是实际发请求的函数。它负责确认用户有正确的 ChatGPT/Codex 登录身份,向指定后端地址发送 GET 请求,并把成功返回的 JSON 转成调用方需要的数据类型。
数据流:输入是一份 Config 配置、一个接口 path,以及可选的 timeout 超时时间。它先读取配置里的 ChatGPT 基础网址,再通过 shared_from_config 找到共享的登录管理器,从里面取认证信息;如果没登录、不是 Codex 后端认证,或缺少账号 ID,就直接返回错误。接着它用 create_client 建 HTTP 客户端,把基础网址和 path 拼成完整 URL,加上认证头、产品标识头和 JSON 内容类型头。然后发送请求:成功时读取响应 JSON 并解析成 T;失败时读取状态码和响应正文,返回带原因的错误。
调用关系:它是这个文件的核心执行者。chatgpt_get_request 会调用它来做普通请求;codex_plugins_enabled_for_workspace 也会直接调用它,因为那里可能需要自定义超时时间。它内部会借助 create_client 创建网络客户端,借助 shared_from_config 取得登录信息,借助 auth_provider_from_auth 把认证信息变成 HTTP 请求头。
调用图:调用 2 个内部函数(create_client, shared_from_config);被 2 处调用(chatgpt_get_request, codex_plugins_enabled_for_workspace);外部调用 4 个(bail!, ensure!, auth_provider_from_auth, format!)。
cloud-tasks-client/src/http.rs源码 ↗
这个文件像一个“前台窗口”:外面的人说“列出任务、看任务详情、取补丁、应用补丁、创建任务”,它负责去云端后台问、把后台返回的 JSON 数据翻译成本项目自己的类型,再把结果交回来。它还处理一些容易出错的地方,比如后台有时把消息放在不同字段里,它会做备用解析;补丁必须是 git 能看懂的统一 diff(统一 diff,就是带有文件改动位置和加减行的标准补丁格式),否则不会硬套到本地代码里。文件里主要有三块小 API:Tasks 管任务信息,Attempts 管同一个任务的多次尝试,Apply 管把云端补丁应用到本地。出错或异常时,它会往 error.log 写线索,方便之后查问题。
HttpClient::new31–35 ↗
fn new(base_url: impl Into<String>) -> anyhow::Result<Self>
作用:创建一个新的云端 HTTP 客户端。调用方只要给一个后台地址,它就准备好后续发请求所需的底层客户端。
数据流:进去的是 base_url,也就是后台服务地址 → 它把地址转成字符串,并用这个地址创建底层 backend 客户端 → 出来的是 HttpClient,里面保存了地址和真正发 HTTP 请求的工具;如果地址或客户端初始化失败,就返回错误。
调用关系:它是整个文件的起点之一,init_backend 会在初始化云端后端时调用它。它内部把具体建连接的活交给 backend::Client::new。
调用图:调用 1 个内部函数(new);被 1 处调用(init_backend);外部调用 2 个(clone, into)。
HttpClient::with_user_agent37–40 ↗
fn with_user_agent(mut self, ua: impl Into<String>) -> Self
作用:给客户端加上 User-Agent。User-Agent 可以理解为请求时告诉服务器“我是谁、什么版本”的名片。
数据流:进去的是一个 User-Agent 字符串 → 它复制一份底层客户端并设置这个标识 → 出来的是更新后的 HttpClient,之后发请求都会带上这个标识。
调用关系:它通常在创建 HttpClient 后链式调用,用来补充请求头信息;真正设置请求名片的工作交给底层 backend 客户端。
调用图:外部调用 1 个(clone)。
HttpClient::with_auth_provider42–45 ↗
fn with_auth_provider(mut self, auth: SharedAuthProvider) -> Self
作用:给客户端加上认证提供者。认证提供者就是帮请求带上登录凭证的东西,避免服务器把请求当成陌生人拒绝。
数据流:进去的是共享的认证提供者 → 它把认证能力装到底层客户端上 → 出来的是能带认证信息发请求的 HttpClient。
调用关系:它一般在客户端创建后、真正访问云端前调用;后续所有 Tasks、Attempts、Apply 发出的请求都会共用这份认证设置。
调用图:外部调用 1 个(clone)。
HttpClient::with_chatgpt_account_id47–50 ↗
fn with_chatgpt_account_id(mut self, account_id: impl Into<String>) -> Self
作用:给客户端指定 ChatGPT 账号 ID。这样后台知道这次请求应该归到哪个账号下面。
数据流:进去的是账号 ID → 它复制并更新底层客户端的账号设置 → 出来的是带账号上下文的 HttpClient。
调用关系:它是配置客户端身份的一步,通常和认证、User-Agent 设置一起使用;实际保存和发送账号信息由 backend 客户端完成。
调用图:外部调用 1 个(clone)。
HttpClient::tasks_api52–54 ↗
fn tasks_api(&self) -> api::Tasks<'_>
作用:生成一个专门处理任务相关请求的小工具。比如列任务、看详情、取消息、创建任务都走它。
数据流:进去的是当前 HttpClient 的引用 → 它取出 base_url 和底层 backend 客户端引用 → 出来的是 api::Tasks,后续用它和任务接口打交道。
调用关系:list_tasks、get_task_summary、get_task_diff、get_task_messages、get_task_text、create_task 都会先调用它,相当于把“大客户端”拆成“任务柜台”。
调用图:被 6 处调用(create_task, get_task_diff, get_task_messages, get_task_summary, get_task_text, list_tasks);外部调用 1 个(new)。
HttpClient::attempts_api56–58 ↗
fn attempts_api(&self) -> api::Attempts<'_>
作用:生成一个专门处理“任务尝试次数”的小工具。同一个任务可能有多次回答或尝试,它负责查询这些兄弟尝试。
数据流:进去的是当前 HttpClient → 它取出底层 backend 客户端引用 → 出来的是 api::Attempts。
调用关系:list_sibling_attempts 会调用它,然后把查询兄弟尝试的具体工作交给 api::Attempts::list。
调用图:被 1 处调用(list_sibling_attempts);外部调用 1 个(new)。
HttpClient::apply_api60–62 ↗
fn apply_api(&self) -> api::Apply<'_>
作用:生成一个专门处理“把云端补丁应用到本地”的小工具。它把下载补丁和运行 git apply 这类事情集中起来。
数据流:进去的是当前 HttpClient → 它拿到底层 backend 客户端引用 → 出来的是 api::Apply。
调用关系:apply_task 和 apply_task_preflight 会调用它,再把真正应用或预检查补丁的工作交给 api::Apply::run。
调用图:被 2 处调用(apply_task, apply_task_preflight);外部调用 1 个(new)。
HttpClient::list_tasks66–73 ↗
fn list_tasks(
&'a self,
env: Option<&'a str>,
limit: Option<i64>,
cursor: Option<&'a str>,
) -> CloudBackendFuture<'a, TaskListPage>
作用:实现统一后端接口里的“列出任务”。调用方可以按环境、数量和分页游标来取一页任务。
数据流:进去的是环境筛选、数量限制和 cursor(分页位置标记)→ 它创建 Tasks 小工具,并把异步请求包装成统一的 future(未来会完成的异步结果)→ 出来的是 TaskListPage,里面有任务列表和下一页 cursor。
调用关系:这是 CloudBackend 接口的一部分,外部只认这个统一方法;它自己不解析数据,而是转交给 api::Tasks::list。
调用图:调用 1 个内部函数(tasks_api);外部调用 1 个(pin)。
HttpClient::get_task_summary75–77 ↗
fn get_task_summary(&self, id: TaskId) -> CloudBackendFuture<'_, TaskSummary>
作用:实现“获取某个任务摘要”。摘要包括标题、状态、更新时间、改了多少文件等给列表和详情页看的信息。
数据流:进去的是 TaskId → 它创建 Tasks 小工具并发起异步详情读取 → 出来的是 TaskSummary。
调用关系:这是 CloudBackend 的统一入口之一;具体怎么向后台要详情、怎么拼摘要,由 api::Tasks::summary 完成。
调用图:调用 1 个内部函数(tasks_api);外部调用 1 个(pin)。
HttpClient::get_task_diff79–81 ↗
fn get_task_diff(&self, id: TaskId) -> CloudBackendFuture<'_, Option<String>>
作用:获取某个任务生成的代码补丁。补丁就是一段描述哪些文件要加删改的文本。
数据流:进去的是 TaskId → 它通过 Tasks 小工具请求任务详情并提取 diff → 出来的是可能存在的补丁字符串;没有补丁就返回 None。
调用关系:外部通过 CloudBackend 调它;它把实际读取和提取工作交给 api::Tasks::diff。
调用图:调用 1 个内部函数(tasks_api);外部调用 1 个(pin)。
HttpClient::get_task_messages83–85 ↗
fn get_task_messages(&self, id: TaskId) -> CloudBackendFuture<'_, Vec<String>>
作用:获取某个任务里助手回复的文字消息。用于展示云端任务到底回答了什么。
数据流:进去的是 TaskId → 它通过 Tasks 小工具读取详情并寻找助手消息 → 出来的是一组文本消息;找不到时可能返回说明性错误。
调用关系:这是 CloudBackend 的消息入口;具体兼容不同后台返回格式的工作由 api::Tasks::messages 做。
调用图:调用 1 个内部函数(tasks_api);外部调用 1 个(pin)。
HttpClient::get_task_text87–89 ↗
fn get_task_text(&self, id: TaskId) -> CloudBackendFuture<'_, TaskText>
作用:一次性获取任务的文字上下文。它包含用户原始问题、助手消息、当前尝试 ID 和尝试状态等。
数据流:进去的是 TaskId → 它委托 Tasks 小工具读取详情并整理文字信息 → 出来的是 TaskText。
调用关系:外部想展示任务完整文本时会走这里;它把解析细节交给 api::Tasks::task_text。
调用图:调用 1 个内部函数(tasks_api);外部调用 1 个(pin)。
HttpClient::list_sibling_attempts91–97 ↗
fn list_sibling_attempts(
&self,
task: TaskId,
turn_id: String,
) -> CloudBackendFuture<'_, Vec<TurnAttempt>>
作用:列出同一个任务、同一轮回答下的其他尝试。通俗说,就是查看“同题多答”的不同版本。
数据流:进去的是任务 ID 和当前 turn_id(某一轮回答的编号)→ 它创建 Attempts 小工具并发起异步查询 → 出来的是排序后的 TurnAttempt 列表。
调用关系:这是 CloudBackend 暴露给上层的尝试列表入口;实际请求和排序由 api::Attempts::list 处理。
调用图:调用 1 个内部函数(attempts_api);外部调用 1 个(pin)。
HttpClient::apply_task99–109 ↗
fn apply_task(
&self,
id: TaskId,
diff_override: Option<String>,
) -> CloudBackendFuture<'_, ApplyOutcome>
作用:把某个任务的补丁真正应用到当前本地代码目录。它会改动工作区,所以属于有实际副作用的操作。
数据流:进去的是任务 ID,以及可选的补丁覆盖内容 → 它创建 Apply 小工具,并以 preflight=false 运行 → 出来的是 ApplyOutcome,说明是否应用成功、是否冲突、哪些路径被跳过。
调用关系:上层用户点击或执行“应用任务”时会走这里;它把下载补丁、检查格式、调用 git 应用补丁都交给 api::Apply::run。
调用图:调用 1 个内部函数(apply_api);外部调用 1 个(pin)。
HttpClient::apply_task_preflight111–121 ↗
fn apply_task_preflight(
&self,
id: TaskId,
diff_override: Option<String>,
) -> CloudBackendFuture<'_, ApplyOutcome>
作用:预检查某个任务的补丁能不能干净地应用。它像试穿衣服,只检查合不合身,不真正改代码。
数据流:进去的是任务 ID 和可选补丁覆盖内容 → 它创建 Apply 小工具,并以 preflight=true 运行 → 出来的是 ApplyOutcome,说明如果应用会不会成功、哪里可能冲突。
调用关系:上层在真正应用前可调用它做安全检查;它和 apply_task 走同一个 api::Apply::run,只是打开了预检查模式。
调用图:调用 1 个内部函数(apply_api);外部调用 1 个(pin)。
HttpClient::create_task123–136 ↗
fn create_task(
&'a self,
env_id: &'a str,
prompt: &'a str,
git_ref: &'a str,
qa_mode: bool,
best_of_n: usize,
) -> CloudBackendFuture<'a, crate::Cr
作用:在云端创建一个新任务。调用方给环境、提示词、代码分支等信息,它负责提交给后台。
数据流:进去的是环境 ID、用户提示词、git 引用、是否 QA 模式、best_of_n 数量 → 它通过 Tasks 小工具组装请求并发送 → 出来的是 CreatedTask,里面有新任务 ID。
调用关系:这是 CloudBackend 的创建任务入口;请求体如何组织、是否带起始 diff、失败如何记日志,都由 api::Tasks::create 做。
调用图:调用 1 个内部函数(tasks_api);外部调用 1 个(pin)。
api::Tasks::new151–156 ↗
fn new(client: &'a HttpClient) -> Self
作用:创建任务接口小工具。它不拥有数据,只借用 HttpClient 里的地址和底层请求客户端。
数据流:进去的是 HttpClient 引用 → 它取出 base_url 和 backend 引用 → 出来的是 api::Tasks。
调用关系:HttpClient::tasks_api 会调用它;之后 list、summary、diff、messages、task_text、create 都靠这个对象完成。
api::Tasks::list158–191 ↗
async fn list(
&self,
env: Option<&str>,
limit: Option<i64>,
cursor: Option<&str>,
) -> Result<TaskListPage>
作用:向后台请求任务列表,并把后台的列表项翻译成本项目的任务摘要。它还记录一条日志,方便知道取了多少任务、分页到哪里。
数据流:进去的是环境筛选、数量限制和 cursor → 它把数量转成后台需要的格式,调用 backend 的 list_tasks,再把每个返回项转成 TaskSummary → 出来的是 TaskListPage,同时往 error.log 追加一条列表请求记录。
调用关系:它由 HttpClient::list_tasks 间接调用;它负责和后台列表接口打交道,并使用 map_task_list_item_to_summary 做单条数据转换。
调用图:调用 2 个内部函数(list_tasks, append_error_log);外部调用 1 个(format!)。
api::Tasks::summary193–260 ↗
async fn summary(&self, id: TaskId) -> Result<TaskSummary>
作用:读取一个任务的详细信息,并整理成适合展示的摘要。它会尽量从后台状态里拿改动统计,拿不到时再从 diff 文本里自己数。
数据流:进去的是 TaskId → 它通过 details_with_body 拿到解析后的详情和原始 JSON,再提取标题、状态、更新时间、环境、是否 review、尝试总数和改动统计 → 出来的是 TaskSummary;如果 JSON 缺关键字段会返回 HTTP 类错误。
调用关系:它服务于 HttpClient::get_task_summary;过程中会调用 map_status、diff_summary_from_status_display、diff_summary_from_diff、parse_updated_at 等小工具,把杂乱后台数据整理成稳定结构。
调用图:调用 1 个内部函数(details_with_body);外部调用 7 个(attempt_total_from_status_display, diff_summary_from_diff, diff_summary_from_status_display, env_label_from_status_display, map_status, parse_updated_at, from_str)。
api::Tasks::diff262–272 ↗
async fn diff(&self, id: TaskId) -> Result<Option<String>>
作用:从任务详情里取出统一 diff 补丁。它只负责拿补丁,不负责应用补丁。
数据流:进去的是 TaskId → 它通过 details_with_body 请求详情,然后询问详情对象有没有 unified_diff → 出来的是 Some(diff) 或 None。
调用关系:它由 HttpClient::get_task_diff 调用;如果后续要真正应用补丁,会走 api::Apply::run,而不是这里。
调用图:调用 1 个内部函数(details_with_body)。
api::Tasks::messages274–298 ↗
async fn messages(&self, id: TaskId) -> Result<Vec<String>>
作用:提取任务里的助手文字回复。它会先用结构化字段,找不到再从原始 JSON 里“翻箱倒柜”找备用位置。
数据流:进去的是 TaskId → 它读取任务详情,先拿 assistant_text_messages;为空时调用 extract_assistant_messages_from_body 从原始响应解析;如果还是没有但有错误信息,就返回“任务失败”消息 → 出来的是消息列表,或带有请求地址、内容类型和响应体的错误。
调用关系:它由 HttpClient::get_task_messages 调用;details_path 用来拼诊断用的网址,extract_assistant_messages_from_body 负责兼容备用 JSON 结构。
调用图:调用 1 个内部函数(details_with_body);外部调用 5 个(Http, details_path, extract_assistant_messages_from_body, format!, vec!)。
api::Tasks::task_text300–327 ↗
async fn task_text(&self, id: TaskId) -> Result<TaskText>
作用:把任务的用户提问、助手回答和当前尝试信息整理到一起。适合详情页一次性展示完整文本上下文。
数据流:进去的是 TaskId → 它读取详情,取用户 prompt、助手消息、当前 assistant turn 的 ID、兄弟 turn ID、位置和状态 → 出来的是 TaskText。
调用关系:它由 HttpClient::get_task_text 调用;当结构化消息为空时,也会借助 extract_assistant_messages_from_body 做备用解析,并用 attempt_status_from_str 翻译状态。
调用图:调用 1 个内部函数(details_with_body);外部调用 2 个(attempt_status_from_str, extract_assistant_messages_from_body)。
api::Tasks::create329–390 ↗
async fn create(
&self,
env_id: &str,
prompt: &str,
git_ref: &str,
qa_mode: bool,
best_of_n: usize,
) -> Result<crate::C
作用:组装并发送“新建云端任务”的请求。它把用户提示词、环境、分支和一些可选设置变成后台能懂的 JSON。
数据流:进去的是环境 ID、提示词、git 引用、QA 模式和 best_of_n → 它构造 input_items;如果环境变量 CODEX_STARTING_DIFF 里有起始补丁,也一起放进去;best_of_n 大于 1 时写入 metadata → 成功出来 CreatedTask,失败则写日志并返回 HTTP 错误。
调用关系:它由 HttpClient::create_task 调用;真正发请求的是 backend.create_task,成功或失败都会通过 append_error_log 留线索。
调用图:调用 2 个内部函数(create_task, append_error_log);外部调用 6 个(new, Http, new, format!, json!, var)。
api::Tasks::details_with_body392–398 ↗
async fn details_with_body(
&self,
id: &str,
) -> anyhow::Result<(backend::CodeTaskDetailsResponse, String, String)>
作用:同时拿任务详情的结构化版本和原始响应文本。这样既能正常解析,也能在字段缺失时查看原始 JSON。
数据流:进去的是任务 ID 字符串 → 它调用底层 get_task_details_with_body → 出来的是三件东西:解析后的详情、原始 body、content-type。
调用关系:summary、diff、messages、task_text 都会调用它;它是这些详情类功能共同的取数入口。
调用图:调用 1 个内部函数(get_task_details_with_body);被 4 处调用(diff, messages, summary, task_text)。
api::Attempts::new406–410 ↗
fn new(client: &'a HttpClient) -> Self
作用:创建尝试列表接口小工具。它只借用底层 backend 客户端。
数据流:进去的是 HttpClient 引用 → 它取出 backend 引用 → 出来的是 api::Attempts。
调用关系:HttpClient::attempts_api 会调用它;之后查询兄弟尝试由 api::Attempts::list 完成。
api::Attempts::list412–426 ↗
async fn list(&self, task: TaskId, turn_id: String) -> Result<Vec<TurnAttempt>>
作用:查询同一个任务同一轮里的其他尝试,并整理成按顺序排列的列表。
数据流:进去的是任务 ID 和 turn_id → 它调用后台 list_sibling_turns,过滤掉无法解析的项,把每个 turn 转成 TurnAttempt,然后排序 → 出来的是 TurnAttempt 列表。
调用关系:它由 HttpClient::list_sibling_attempts 间接调用;turn_attempt_from_map 负责单个尝试的拆解,compare_attempts 负责排出稳定顺序。
调用图:调用 1 个内部函数(list_sibling_turns)。
api::Apply::new434–438 ↗
fn new(client: &'a HttpClient) -> Self
作用:创建补丁应用接口小工具。它负责后续从后台拿补丁,并调用本地 git 工具应用。
数据流:进去的是 HttpClient 引用 → 它取出 backend 引用 → 出来的是 api::Apply。
调用关系:HttpClient::apply_api 会调用它;真正的大动作在 api::Apply::run 里完成。
api::Apply::run440–571 ↗
async fn run(
&self,
task_id: TaskId,
diff_override: Option<String>,
preflight: bool,
) -> Result<ApplyOutcome>
作用:执行补丁应用或预检查。它会先拿到补丁,确认格式像 git diff,再调用本地 git apply;预检查模式只试不改。
数据流:进去的是任务 ID、可选补丁覆盖内容和 preflight 开关 → 如果没有覆盖补丁,它先从后台任务详情取 unified diff;然后检查是否是统一 diff;格式不对就返回错误结果并写日志;格式正确就构造 ApplyGitRequest,调用 apply_git_patch → 出来的是 ApplyOutcome,包含成功、部分成功或失败,以及跳过和冲突的路径;真正应用模式可能改动本地文件。
调用关系:它由 HttpClient::apply_task 和 HttpClient::apply_task_preflight 调用。它会用 is_unified_diff 防止错误格式伤到工作区,用 summarize_patch_for_logging、tail 和 append_error_log 记录失败细节,实际打补丁交给 apply_git_patch。
调用图:调用 2 个内部函数(get_task_details, append_error_log);外部调用 9 个(new, new, is_unified_diff, summarize_patch_for_logging, apply_git_patch, format!, matches!, current_dir, writeln!)。
api::details_path574–582 ↗
fn details_path(base_url: &str, id: &str) -> Option<String>
作用:根据 base_url 拼出任务详情接口地址,主要用于错误信息里提示用户到底请求了哪里。
数据流:进去的是 base_url 和任务 ID → 它判断地址里是否包含 backend-api 或 api/codex → 出来的是对应格式的详情 URL;不认识的地址格式就返回 None。
调用关系:api::Tasks::messages 在找不到消息时会用它生成诊断信息,方便排查后台返回为什么不符合预期。
调用图:外部调用 1 个(format!)。
api::extract_assistant_messages_from_body584–625 ↗
fn extract_assistant_messages_from_body(body: &str) -> Vec<String>
作用:从原始 JSON 响应文本里手动挖出助手消息。它是结构化解析失败时的备用方案。
数据流:进去的是原始 body 字符串 → 它尝试按 JSON 解析,找到 current_assistant_turn.worklog.messages,筛选作者是 assistant 的消息,再从文本片段里取文字 → 出来的是字符串列表;解析失败或找不到就返回空列表。
调用关系:api::Tasks::messages 和 api::Tasks::task_text 会在标准字段为空时调用它,用来兼容后台响应格式变化。
调用图:外部调用 1 个(new)。
api::turn_attempt_from_map627–642 ↗
fn turn_attempt_from_map(turn: &HashMap<String, Value>) -> Option<TurnAttempt>
作用:把后台返回的一条 turn 字典转换成项目里的 TurnAttempt。turn 可以理解为一次回答尝试。
数据流:进去的是一张字符串到 JSON 值的表 → 它读取 turn_id、尝试位置、创建时间、状态、diff 和消息 → 出来的是 TurnAttempt;如果没有必需的 id,就返回 None。
调用关系:api::Attempts::list 会对每条 sibling turn 调它;它再把时间、状态、补丁和消息的提取交给 parse_timestamp_value、attempt_status_from_str、extract_diff_from_turn、extract_assistant_messages_from_turn。
调用图:外部调用 4 个(attempt_status_from_str, extract_assistant_messages_from_turn, extract_diff_from_turn, parse_timestamp_value)。
api::compare_attempts644–656 ↗
fn compare_attempts(a: &TurnAttempt, b: &TurnAttempt) -> Ordering
作用:给多次尝试排序。优先按后台给的尝试序号排,没有序号时再按创建时间和 ID 兜底。
数据流:进去的是两个 TurnAttempt → 它比较 attempt_placement;如果缺失,再比较 created_at;还不够就比较 turn_id → 出来的是 Ordering,告诉排序函数谁在前谁在后。
调用关系:api::Attempts::list 在整理完尝试列表后用它排序,让用户看到的顺序稳定、符合直觉。
api::extract_diff_from_turn658–684 ↗
fn extract_diff_from_turn(turn: &HashMap<String, Value>) -> Option<String>
作用:从一次尝试的输出项里找补丁文本。后台可能用不同类型存补丁,它会识别几种常见位置。
数据流:进去的是 turn 字典 → 它查看 output_items,遇到 output_diff 就取 diff,遇到 pr 就取 output_diff.diff → 出来的是补丁字符串;没有有效补丁就返回 None。
调用关系:api::turn_attempt_from_map 调用它,把每次尝试里可能存在的代码改动带到 TurnAttempt 中。
api::extract_assistant_messages_from_turn686–706 ↗
fn extract_assistant_messages_from_turn(turn: &HashMap<String, Value>) -> Vec<String>
作用:从一次尝试的输出项里提取助手文字消息。它只收集类型是 message 且内容是文本的部分。
数据流:进去的是 turn 字典 → 它遍历 output_items,筛出 message 类型,再从 content 数组里取 content_type 为 text 的非空文字 → 出来的是消息列表。
调用关系:api::turn_attempt_from_map 调用它,让兄弟尝试列表不仅有补丁和状态,也能带上该尝试的文字说明。
调用图:外部调用 1 个(new)。
api::attempt_status_from_str708–716 ↗
fn attempt_status_from_str(raw: Option<&str>) -> AttemptStatus
作用:把后台的状态字符串翻译成项目内部的 AttemptStatus。这样上层不用到处记后台用了哪些英文状态。
数据流:进去的是可选状态字符串 → 它把 failed、completed、in_progress、pending 映射到对应枚举;未知或缺失都当 pending → 出来的是 AttemptStatus。
调用关系:api::Tasks::task_text 和 api::turn_attempt_from_map 会调用它,用统一的状态值展示当前尝试或兄弟尝试。
api::parse_timestamp_value718–725 ↗
fn parse_timestamp_value(v: Option<&Value>) -> Option<DateTime<Utc>>
作用:把 JSON 里的 Unix 时间戳转成真正的 UTC 时间。Unix 时间戳就是从 1970 年开始算的秒数。
数据流:进去的是可选 JSON 值 → 它读取浮点秒数,拆成秒和纳秒,再生成 DateTime<Utc> → 出来的是可选时间;没有数字就返回 None。
调用关系:api::turn_attempt_from_map 用它解析每次尝试的创建时间,后续 compare_attempts 可能用这个时间排序。
api::map_task_list_item_to_summary727–743 ↗
fn map_task_list_item_to_summary(src: backend::TaskListItem) -> TaskSummary
作用:把后台列表接口的一条任务数据,转换成项目内部统一的 TaskSummary。
数据流:进去的是 backend::TaskListItem → 它取 ID、标题、状态显示、更新时间、环境标签、diff 统计、是否有 PR 和尝试总数 → 出来的是 TaskSummary。
调用关系:api::Tasks::list 会对每个列表项调用它;它依赖 map_status、parse_updated_at、env_label_from_status_display、diff_summary_from_status_display、attempt_total_from_status_display 做细节转换。
调用图:外部调用 6 个(new, attempt_total_from_status_display, diff_summary_from_status_display, env_label_from_status_display, map_status, parse_updated_at)。
api::map_status745–772 ↗
fn map_status(v: Option<&HashMap<String, Value>>) -> TaskStatus
作用:把后台复杂的状态信息翻译成简单的任务状态。比如完成就是 Ready,失败或取消就是 Error。
数据流:进去的是可选状态显示表 → 它优先看 latest_turn_status_display.turn_status,再看旧的 state 字段 → 出来的是 TaskStatus;看不懂时默认 Pending。
调用关系:api::Tasks::summary 和 api::map_task_list_item_to_summary 都用它,保证详情页和列表页对状态的理解一致。
api::parse_updated_at774–783 ↗
api::env_label_from_status_display785–790 ↗
fn env_label_from_status_display(v: Option<&HashMap<String, Value>>) -> Option<String>
作用:从状态显示信息里取环境名称。环境名称是给人看的标签,不一定等同于机器用的环境 ID。
数据流:进去的是可选状态显示表 → 它查找 environment_label 字符串 → 出来的是可选字符串。
调用关系:api::Tasks::summary 和 api::map_task_list_item_to_summary 调用它,让任务摘要能显示更友好的环境信息。
api::diff_summary_from_diff792–818 ↗
fn diff_summary_from_diff(diff: &str) -> DiffSummary
作用:直接从 diff 文本里粗略统计改了多少文件、加了多少行、删了多少行。它是后台没给统计时的备用算法。
数据流:进去的是 diff 字符串 → 它逐行查看,遇到 diff --git 就算一个文件,遇到真正的加号行算新增,减号行算删除,跳过 diff 头和 hunk 头 → 出来的是 DiffSummary。
调用关系:api::Tasks::summary 在状态里的统计全是 0、但详情里有 diff 时调用它,避免摘要页面显示“没有改动”。
api::diff_summary_from_status_display820–839 ↗
fn diff_summary_from_status_display(v: Option<&HashMap<String, Value>>) -> DiffSummary
作用:从后台状态显示里读取 diff 统计。能直接读后台给的数字,就不用自己扫补丁文本。
数据流:进去的是可选状态显示表 → 它找到 latest_turn_status_display.diff_stats,读取 files_modified、lines_added、lines_removed,并把负数压成 0 → 出来的是 DiffSummary;字段缺失就返回全 0。
调用关系:api::Tasks::summary 和 api::map_task_list_item_to_summary 都优先用它生成改动统计。
调用图:外部调用 1 个(default)。
api::latest_turn_timestamp841–850 ↗
fn latest_turn_timestamp(v: Option<&HashMap<String, Value>>) -> Option<f64>
作用:从最新一轮尝试的状态里找更新时间或创建时间。它用于任务本身没有更新时间时的兜底。
数据流:进去的是可选状态显示表 → 它找到 latest_turn_status_display,再优先取 updated_at,取不到就取 created_at → 出来的是可选浮点时间戳。
调用关系:api::Tasks::summary 在 task.updated_at 和 task.created_at 都没有时调用它,让摘要仍然有一个合理时间。
api::attempt_total_from_status_display852–859 ↗
fn attempt_total_from_status_display(v: Option<&HashMap<String, Value>>) -> Option<usize>
作用:估算一个任务这一轮共有多少次尝试。后台给的是兄弟 turn ID 列表,所以总数等于兄弟数量加当前这一次。
数据流:进去的是可选状态显示表 → 它找到 latest_turn_status_display.sibling_turn_ids 数组 → 出来的是 sibling 数量加 1;没有这些信息就返回 None。
调用关系:api::Tasks::summary 和 api::map_task_list_item_to_summary 用它填 attempt_total,方便界面显示“共有几种尝试”。
api::is_unified_diff861–869 ↗
fn is_unified_diff(diff: &str) -> bool
作用:判断一段文本是不是 git 能识别的统一 diff。这样可以防止把错误格式的文本当补丁硬应用。
数据流:进去的是补丁文本 → 它检查是否以 diff --git 开头,或同时包含 ---、+++ 和 @@ 这些统一 diff 的典型标记 → 出来的是 true 或 false。
调用关系:api::Apply::run 在调用 git apply 前必须先用它把关;不通过就返回错误结果并写日志。
api::tail871–877 ↗
fn tail(s: &str, max: usize) -> String
作用:截取一段字符串的末尾。常用于日志里只保留命令输出最后一部分,避免 error.log 被刷爆。
数据流:进去的是字符串和最大长度 → 如果字符串不超过最大长度就原样返回,否则只取最后 max 个字节 → 出来的是截短后的字符串。
调用关系:api::Apply::run 在记录 git apply 的 stdout 和 stderr 时会用它,只保存尾部关键信息。
api::summarize_patch_for_logging879–905 ↗
fn summarize_patch_for_logging(patch: &str) -> String
作用:给补丁生成一段日志摘要。它会说明补丁大概是什么格式、有多少行多少字符、当前目录在哪里、开头长什么样。
数据流:进去的是补丁文本 → 它判断格式类型,统计行数和字符数,读取当前工作目录,截取前 20 行并限制长度 → 出来的是一段适合写进日志的摘要字符串。
调用关系:api::Apply::run 在补丁格式不对、应用失败或部分成功时调用它,帮助开发者不用打开完整补丁也能先判断问题。
调用图:外部调用 2 个(format!, current_dir)。
append_error_log908–918 ↗
fn append_error_log(message: &str)
作用:把诊断信息追加写入当前目录的 error.log。它是这个文件里统一的简易故障记录工具。
数据流:进去的是一段消息 → 它生成当前时间戳,打开或创建 error.log,并把“时间 + 消息”追加到文件末尾 → 出来没有业务结果;如果日志文件打不开,它会静默放弃,不打断主流程。
调用关系:api::Tasks::list、api::Tasks::create 和 api::Apply::run 会调用它。它不参与正常返回值,只在请求、创建或应用补丁需要留线索时工作。
调用图:被 3 处调用(run, create, list);外部调用 3 个(now, new, writeln!)。
config/src/thread_config/remote.rs源码 ↗
可以把这个文件理解成一个“远程配置取货员”。程序需要知道当前线程该用哪个模型服务、哪些功能开关打开、认证命令怎么跑等信息;这些信息有时不在本机,而在一个远端服务里。这里用 gRPC(一种让程序像调用本地函数一样调用远端服务的网络协议)去请求配置,并设置 5 秒超时,避免程序一直卡住。远端返回的是 proto(双方约好的消息格式)里的数据,本文件会逐项检查:模型提供商有没有 id、接口类型是否合法、认证超时时间是不是非零、工作目录是不是绝对路径。检查通过后,它才会转成本地的 ThreadConfigSource、SessionThreadConfig、ModelProviderInfo 等类型。这样做的好处是:网络、格式、权限、超时这些问题都会被包装成统一错误,上层不用猜哪里坏了。文件后半部分是测试,用一个假的远端服务器确认请求内容、超时设置和数据转换都正确。
RemoteThreadConfigLoader::new33–37 ↗
fn new(endpoint: impl Into<String>) -> Self
作用:创建一个远程线程配置加载器,告诉它应该去哪个远端地址取配置。有人配置系统选择“远程加载”时,就会用它来做准备。
数据流:进去的是一个远端地址,比如 http://127.0.0.1:1234;函数把这个地址转成字符串,放进 RemoteThreadConfigLoader 里;出来的是一个保存好地址的加载器对象,不会立刻联网。
调用关系:它通常由 configured_thread_config_loader 在组装配置加载方式时调用;测试 tests::load_thread_config_calls_remote_service 也会用它连到假的测试服务器。真正联网发生在后面的 RemoteThreadConfigLoader::client 和 RemoteThreadConfigLoader::load。
调用图:被 3 处调用(configured_thread_config_loader, configured_thread_config_loader, load_thread_config_calls_remote_service);外部调用 1 个(into)。
RemoteThreadConfigLoader::client39–51 ↗
async fn client(
&self,
) -> Result<ThreadConfigLoaderClient<tonic::transport::Channel>, ThreadConfigLoadError>
作用:根据加载器里保存的地址,建立一个能和远端配置服务说话的 gRPC 客户端。它像先拨通电话,后面才能问对方要配置。
数据流:进去的是 self 里的 endpoint 地址;函数尝试连接远端 ThreadConfigLoader 服务;成功就返回客户端,失败就把连接错误包装成 ThreadConfigLoadError,错误类型是 RequestFailed。
调用关系:它被 RemoteThreadConfigLoader::load 调用。load 需要先拿到这个客户端,才能发送真正的加载请求。
RemoteThreadConfigLoader::load74–79 ↗
fn load(
&self,
context: ThreadConfigContext,
) -> ThreadConfigLoaderFuture<'_, Vec<ThreadConfigSource>>
作用:执行一次完整的远程配置加载:连远端、发请求、拿响应、把远端格式转成本地格式。上层只关心“给我配置”,不用自己处理网络和格式转换。
数据流:进去的是 ThreadConfigContext,里面可能有线程 id 和当前工作目录;函数先创建客户端,再用 load_thread_config_request 把上下文包装成带 5 秒超时的 gRPC 请求,发给远端;回来后把每个 proto 配置源转成 ThreadConfigSource,最后输出配置列表,或者输出统一的加载错误。
调用关系:它是 RemoteThreadConfigLoader 实现 ThreadConfigLoader 接口时的核心动作。流程中会调用 RemoteThreadConfigLoader::client 拿客户端,调用 load_thread_config_request 造请求,远端报错时会通过 remote_status_to_error 转成本地错误,返回数据时会交给 thread_config_source_from_proto 继续翻译。
调用图:调用 2 个内部函数(client, load_thread_config_request);外部调用 1 个(pin)。
load_thread_config_request82–91 ↗
fn load_thread_config_request(
context: ThreadConfigContext,
) -> tonic::Request<proto::LoadThreadConfigRequest>
作用:把本地的加载上下文做成远端服务能看懂的请求,并加上超时限制。没有这个超时,远端没反应时程序可能等太久。
数据流:进去的是 ThreadConfigContext,包含 thread_id 和 cwd;函数把 cwd 转成普通字符串,放进 proto::LoadThreadConfigRequest;同时给 tonic::Request 设置 5 秒 gRPC 超时;出来的是可直接发送给远端的请求对象。
调用关系:它被 RemoteThreadConfigLoader::load 用来准备网络请求。tests::load_thread_config_request_sets_timeout 专门检查它确实设置了正确的超时。
调用图:调用 1 个内部函数(new);被 2 处调用(load, load_thread_config_request_sets_timeout)。
remote_status_to_error93–119 ↗
fn remote_status_to_error(status: tonic::Status) -> ThreadConfigLoadError
作用:把远端 gRPC 返回的失败状态,翻译成本项目统一认识的配置加载错误。这样上层不用懂 gRPC 的各种状态码。
数据流:进去的是 tonic::Status,也就是远端调用失败的状态;函数查看状态码:认证或权限问题归为 Auth,超时归为 Timeout,其它大多归为 RequestFailed;出来的是 ThreadConfigLoadError,并带上可读的错误消息。
调用关系:它在 RemoteThreadConfigLoader::load 发送请求失败时使用。它是网络错误和配置系统错误之间的翻译层。
thread_config_source_from_proto121–133 ↗
fn thread_config_source_from_proto(
source: proto::ThreadConfigSource,
) -> Result<ThreadConfigSource, ThreadConfigLoadError>
作用:把远端返回的一个“配置来源”翻译成本地的 ThreadConfigSource。配置来源可能是会话配置,也可能是用户配置。
数据流:进去的是 proto::ThreadConfigSource;如果里面是 Session,就继续交给 session_thread_config_from_proto 转换;如果是 User,就生成默认的 UserThreadConfig;如果远端没有填真正内容,就返回 Parse 类型的错误。
调用关系:它在远程加载返回结果后逐条处理配置源。遇到解析问题时,它会调用 parse_error 生成统一错误;遇到会话配置时,它会把细节交给 session_thread_config_from_proto。
调用图:调用 2 个内部函数(parse_error, session_thread_config_from_proto);外部调用 2 个(User, default)。
session_thread_config_from_proto135–149 ↗
fn session_thread_config_from_proto(
config: proto::SessionThreadConfig,
) -> Result<SessionThreadConfig, ThreadConfigLoadError>
作用:把远端的会话级配置转成本地会话配置。这里主要处理当前要用哪个模型提供商、有哪些模型提供商、功能开关是什么。
数据流:进去的是 proto::SessionThreadConfig;函数把 model_providers 列表逐个转成本地 ModelProviderInfo,并收进 HashMap;把 features 收进有序的 BTreeMap;最后输出 SessionThreadConfig。
调用关系:它由 thread_config_source_from_proto 在发现远端返回的是会话配置时调用。模型提供商的细节转换会继续交给 model_provider_from_proto。
调用图:被 1 处调用(thread_config_source_from_proto)。
model_provider_from_proto151–195 ↗
fn model_provider_from_proto(
provider: proto::ModelProvider,
) -> Result<(String, ModelProviderInfo), ThreadConfigLoadError>
作用:把远端描述的一个模型服务提供商,变成本地真正能使用的 ModelProviderInfo。它会认真检查关键字段,避免坏配置悄悄混进来。
数据流:进去的是 proto::ModelProvider;函数先检查 id 不能为空,再检查 wire_api 是否是认识的类型;然后复制名称、地址、请求头、重试次数、WebSocket 支持等字段;如果带认证信息,就交给 model_provider_auth_from_proto 转换;出来的是一对值:提供商 id 和 ModelProviderInfo。
调用关系:它在会话配置转换模型提供商列表时使用;tests::model_provider_proto_roundtrips_through_domain_type 也用它确认 proto 格式和本地格式能正确互转。遇到字段缺失或未知枚举值时,它会调用 parse_error。
调用图:调用 1 个内部函数(parse_error);被 1 处调用(model_provider_proto_roundtrips_through_domain_type);外部调用 2 个(try_from, format!)。
model_provider_to_proto198–241 ↗
fn model_provider_to_proto(
id: impl Into<String>,
provider: ModelProviderInfo,
) -> proto::ModelProvider
作用:这是测试专用函数,把本地的 ModelProviderInfo 转回 proto 格式,用来验证转换过程不会丢信息。正常运行代码不会依赖它发送配置。
数据流:进去的是一个提供商 id 和本地 ModelProviderInfo;函数拆出各个字段,把认证信息、接口类型、字符串字典等转成 proto 结构;出来的是 proto::ModelProvider。
调用关系:它只在测试里被 tests::model_provider_proto_roundtrips_through_domain_type 调用。转换接口类型时会调用 proto_wire_api,认证信息会交给 model_provider_auth_to_proto,字符串映射会用 proto_string_map。
调用图:调用 1 个内部函数(proto_wire_api);被 1 处调用(model_provider_proto_roundtrips_through_domain_type);外部调用 1 个(into)。
model_provider_auth_from_proto243–262 ↗
fn model_provider_auth_from_proto(
auth: proto::ModelProviderAuthInfo,
) -> Result<ModelProviderAuthInfo, ThreadConfigLoadError>
作用:把远端发来的模型认证配置转成本地认证配置,并检查它能不能安全使用。尤其会防止 0 毫秒超时和非法目录这种明显不合理的值。
数据流:进去的是 proto::ModelProviderAuthInfo,包含命令、参数、超时时间、刷新间隔、工作目录;函数把 timeout_ms 转成 NonZeroU64,也就是保证不是 0 的数字;再把 cwd 检查为绝对路径;成功后输出 ModelProviderAuthInfo,失败则输出 Parse 错误。
调用关系:它由 model_provider_from_proto 在模型提供商带有认证配置时调用。它保证后续真正执行认证命令时,拿到的是已经过基本校验的数据。
调用图:调用 1 个内部函数(from_absolute_path_checked);外部调用 1 个(new)。
model_provider_auth_to_proto265–281 ↗
fn model_provider_auth_to_proto(auth: ModelProviderAuthInfo) -> proto::ModelProviderAuthInfo
作用:这是测试专用函数,把本地认证配置转成 proto 认证配置,用来配合往返转换测试。
数据流:进去的是 ModelProviderAuthInfo;函数取出命令、参数、非零超时、刷新间隔和工作目录;把非零超时还原成普通数字,把路径转成字符串;出来的是 proto::ModelProviderAuthInfo。
调用关系:它主要被测试辅助函数 model_provider_to_proto 间接使用,用来构造一份远端格式的数据,再测试能不能转回本地格式。
proto_string_map284–286 ↗
fn proto_string_map(values: HashMap<String, String>) -> proto::StringMap
作用:这是测试专用的小工具,把普通的字符串键值表包成 proto 需要的 StringMap。它只是做一层包装。
数据流:进去的是 HashMap<String, String>;函数把它放进 proto::StringMap 的 values 字段;出来的是 proto::StringMap。
调用关系:它在测试构造 proto::ModelProvider 时使用,帮助 query_params、http_headers、env_http_headers 这些字段变成远端消息格式。
proto_wire_api289–293 ↗
fn proto_wire_api(wire_api: WireApi) -> proto::WireApi
作用:这是测试专用函数,把本地的 WireApi 枚举转成 proto 里的 WireApi 枚举。WireApi 表示和模型服务通信时使用哪种接口格式。
数据流:进去的是本地 WireApi;当前只支持 Responses;函数把它映射成 proto::WireApi::Responses;出来的是 proto 枚举值。
调用关系:它被 model_provider_to_proto 调用,用于测试里的格式转换。生产方向主要是 model_provider_from_proto 从 proto 转回本地。
调用图:被 1 处调用(model_provider_to_proto)。
parse_error295–301 ↗
fn parse_error(message: impl Into<String>) -> ThreadConfigLoadError
作用:统一创建“远端配置格式不对”的错误。这样所有解析失败都会长得一样,上层更容易处理和展示。
数据流:进去的是一段错误说明文字;函数把它放进 ThreadConfigLoadError,错误类型设为 Parse,没有 HTTP 状态码;出来的是标准配置加载错误。
调用关系:它被 thread_config_source_from_proto 和 model_provider_from_proto 等转换函数调用。只要远端数据缺字段、字段非法或出现未知值,就会走到这里。
调用图:调用 1 个内部函数(new);被 2 处调用(model_provider_from_proto, thread_config_source_from_proto);外部调用 1 个(into)。
tests::TestServer::load350–366 ↗
fn load(
&'a self,
request: Request<proto::LoadThreadConfigRequest>,
) -> std::pin::Pin<
Box<
dyn std::future::Future<
作用:这是测试用的假远端服务接口。它检查客户端发来的请求是不是符合预期,然后返回事先准备好的配置。
数据流:进去的是测试客户端发来的 LoadThreadConfigRequest;函数断言 thread_id 和 cwd 与测试预期一致;然后把 self.sources 包装成 LoadThreadConfigResponse 返回。
调用关系:它实现测试服务器的核心响应逻辑。tests::load_thread_config_calls_remote_service 启动这个假服务器后,RemoteThreadConfigLoader 会像访问真服务一样访问它。
调用图:外部调用 4 个(pin, assert_eq!, new, load)。
tests::load_thread_config_calls_remote_service370–405 ↗
async fn load_thread_config_calls_remote_service()
作用:这个测试确认远程加载器真的会连到 gRPC 服务、发送正确请求,并把返回配置转成本地对象。
数据流:测试先准备一个临时监听地址和假的 TestServer;再创建 RemoteThreadConfigLoader 指向这个地址;然后调用 load 传入线程 id 和工作目录;最后关闭服务器,并断言加载结果等于 expected_sources。
调用关系:它把 RemoteThreadConfigLoader::new、RemoteThreadConfigLoader::load、tests::proto_sources、tests::expected_sources 和 tests::workspace_dir 串起来,模拟一次完整的远程配置加载。
调用图:调用 1 个内部函数(new);外部调用 10 个(builder, assert_eq!, new, proto_sources, workspace_dir, format!, bind, spawn, channel, new)。
tests::load_thread_config_request_sets_timeout408–418 ↗
fn load_thread_config_request_sets_timeout()
作用:这个测试确认请求里确实带了 5 秒超时。它防止以后有人改代码时不小心去掉超时保护。
数据流:进去的是默认 ThreadConfigContext;测试调用 load_thread_config_request 生成请求;然后读取请求元数据里的 grpc-timeout;最后断言它等于 5000000u,也就是 5 秒的微秒表示。
调用关系:它直接覆盖 load_thread_config_request 的一个关键行为。这个行为会影响 RemoteThreadConfigLoader::load 发出的所有远程请求。
调用图:调用 1 个内部函数(load_thread_config_request);外部调用 2 个(assert_eq!, default)。
tests::model_provider_proto_roundtrips_through_domain_type421–428 ↗
fn model_provider_proto_roundtrips_through_domain_type()
作用:这个测试确认模型提供商配置从本地格式转成 proto,再转回本地格式后,内容没有丢也没有变。
数据流:测试先用 expected_provider 生成一份本地配置;再用 model_provider_to_proto 转成 proto;接着用 model_provider_from_proto 转回来;最后检查 id 和配置内容都和原来一致。
调用关系:它主要验证 model_provider_to_proto、model_provider_from_proto 以及相关认证和接口类型转换函数配合正确。
调用图:调用 2 个内部函数(model_provider_from_proto, model_provider_to_proto);外部调用 2 个(assert_eq!, expected_provider)。
tests::proto_sources430–490 ↗
fn proto_sources() -> Vec<proto::ThreadConfigSource>
作用:这个测试辅助函数构造一批“远端会返回的 proto 配置”。它相当于给假服务器准备货物。
数据流:函数读取测试工作目录,拼出一个包含 Session 配置和 User 配置的列表;Session 里包括模型提供商、认证命令、请求头、功能开关等;出来的是 Vec<proto::ThreadConfigSource>。
调用关系:它被 tests::load_thread_config_calls_remote_service 用来填充 TestServer 的返回数据。随后这些 proto 数据会被远程加载器转换成本地 expected_sources 对应的结构。
调用图:外部调用 2 个(workspace_dir, vec!)。
tests::expected_sources492–504 ↗
fn expected_sources() -> Vec<ThreadConfigSource>
作用:这个测试辅助函数构造“加载器最终应该得到的本地配置”。它是测试里的标准答案。
数据流:函数创建一个 ThreadConfigSource 列表:第一个是会话配置,包含 local 模型提供商和功能开关;第二个是默认用户配置;出来的是本地 Vec<ThreadConfigSource>。
调用关系:它被 tests::load_thread_config_calls_remote_service 用来和实际加载结果比较。里面的模型提供商来自 tests::expected_provider。
调用图:外部调用 1 个(vec!)。
tests::expected_provider506–541 ↗
fn expected_provider() -> ModelProviderInfo
作用:这个测试辅助函数构造一份标准的本地模型提供商配置,用来和转换结果对比。
数据流:函数填入名称、base_url、认证命令、超时、请求参数、请求头、重试次数、WebSocket 支持等字段;工作目录来自 tests::workspace_dir;出来的是 ModelProviderInfo。
调用关系:它被 tests::expected_sources 和 tests::model_provider_proto_roundtrips_through_domain_type 使用,是测试中判断模型提供商转换是否正确的基准。
调用图:外部调用 4 个(from, new, workspace_dir, vec!)。
tests::workspace_dir543–547 ↗
fn workspace_dir() -> AbsolutePathBuf
作用:这个测试辅助函数生成一个稳定的绝对工作目录路径。认证配置要求工作目录是绝对路径,所以测试也必须满足这个条件。
数据流:函数先读取当前进程所在目录;再在后面接上 workspace;出来的是 AbsolutePathBuf 类型的绝对路径。
调用关系:它被多个测试辅助函数调用,包括 tests::proto_sources 和 tests::expected_provider,用来保证 proto 数据和本地期望数据里的 cwd 一致。
调用图:调用 1 个内部函数(current_dir)。
lmstudio/src/client.rs源码 ↗
LM Studio 可以在用户电脑上跑大语言模型,但项目不能假设它一定装好了、服务一定开着、模型一定存在。这个文件就像一个前台接待员:先从配置里找到 LM Studio 的服务地址,再用 HTTP 请求(可以理解成程序之间发消息)去问服务器“你在吗”“有哪些模型”。如果服务没响应,它会给出明确提示,告诉用户安装 LM Studio 并启动 lms server。它还提供两个常用动作:load_model 会向 /responses 发一个很小的空请求,让服务器把指定模型预热起来;fetch_models 会读取 /models 返回的 JSON(文本格式的数据包),提取模型 id 列表。另一个重要部分是 find_lms 和 download_model:它们在系统里找 lms 命令,找不到 PATH 里的就去 LM Studio 常见安装目录找,然后执行 lms get --yes 下载模型。文件底部的测试用假服务器模拟各种返回,确保成功、缺字段、服务器报错这些情况都能被正确处理。
LMStudioClient::try_from_provider15–44 ↗
async fn try_from_provider(config: &Config) -> std::io::Result<Self>
作用:从项目配置里创建一个可用的 LM Studio 客户端。有人需要连接本机 LM Studio 时,会先用它拿到服务地址,并确认服务器真的能访问。
数据流:进去的是 Config 配置对象;它从里面找内置的 LM Studio 提供方和 base_url 地址,创建一个带 5 秒连接超时的 HTTP 客户端,然后检查服务器是否在线;出来的是可用的 LMStudioClient,或者一个说明配置缺失、地址不对、服务没开等问题的错误。
调用关系:它通常由 ensure_oss_ready 在准备本地开源模型环境时调用。它自己完成“从配置变成客户端”的第一步,随后会让客户端去确认 LM Studio 服务能正常回应。
调用图:被 1 处调用(ensure_oss_ready);外部调用 2 个(builder, from_secs)。
LMStudioClient::check_server46–62 ↗
async fn check_server(&self) -> io::Result<()>
作用:检查 LM Studio 服务器是不是活着。它不是做复杂诊断,只是向 /models 问一句,能正常回答就认为服务可用。
数据流:进去的是客户端里保存的 base_url 和 HTTP 客户端;它拼出 /models 地址并发送 GET 请求;如果收到成功状态就返回空成功结果,如果服务器返回错误码或根本连不上,就返回带提示文字的错误。
调用关系:它是创建客户端时的把关动作,也被测试直接调用来验证成功和失败两种服务器响应。它把网络请求交给 reqwest 这样的外部 HTTP 工具完成。
LMStudioClient::load_model65–92 ↗
async fn load_model(&self, model: &str) -> io::Result<()>
作用:让 LM Studio 把某个模型加载起来。做法是发一个几乎不生成内容的小请求,用最小代价唤醒模型。
数据流:进去的是模型名称;它把模型名、空输入、最多输出 1 个 token 的限制组装成 JSON 请求,POST 到 /responses;如果服务器成功,就记录一条成功日志,否则返回包含 HTTP 状态码的错误。
调用关系:它在已经有 LMStudioClient 之后使用,通常发生在用户选定模型、系统准备开始对话之前。它把实际加载动作交给 LM Studio 服务器完成。
LMStudioClient::fetch_models95–124 ↗
async fn fetch_models(&self) -> io::Result<Vec<String>>
作用:向 LM Studio 查询当前可用的模型列表。用户界面或准备流程可以用它知道本机有哪些模型能选。
数据流:进去的是客户端保存的服务地址;它请求 /models,解析返回的 JSON,并从 data 数组里逐个取出 id 字段;出来的是模型 id 字符串列表,或者在请求失败、JSON 格式不对、缺少 data 数组时返回错误。
调用关系:它是读取服务器状态的核心方法。多个测试会用假服务器调用它,分别确认正常返回、缺少 data 字段、服务器 500 报错时的行为。
LMStudioClient::find_lms127–129 ↗
fn find_lms() -> std::io::Result<String>
作用:寻找本机的 lms 命令。lms 是 LM Studio 提供的命令行工具,用来下载模型等。
数据流:它不需要外部输入;它把任务交给 find_lms_with_home_dir,并让后者自己判断用户主目录;出来的是可执行命令路径,或者“没找到 LM Studio”的错误。
调用关系:download_model 会先调用它,只有找到 lms 才能继续下载模型。test_find_lms 也会调用它,确认有安装时能找到、没安装时能给出合理错误。
调用图:被 1 处调用(test_find_lms);外部调用 1 个(find_lms_with_home_dir)。
LMStudioClient::find_lms_with_home_dir131–166 ↗
fn find_lms_with_home_dir(home_dir: Option<&str>) -> std::io::Result<String>
作用:按更细的规则寻找 lms 命令,并允许测试传入假的用户主目录。这样既能支持真实机器,也方便测试路径拼接。
数据流:进去的是一个可选的 home_dir;它先查系统 PATH 里有没有 lms,如果没有,就取传入目录或 HOME、USERPROFILE 环境变量,再拼出 .lmstudio/bin/lms 或 lms.exe 的备用路径;如果文件存在就返回路径,否则返回未找到错误。
调用关系:它是 find_lms 背后的实际查找者,也被 test_find_lms_with_mock_home 直接调用。它依赖 which 查 PATH,依赖环境变量和文件路径检查做兜底。
调用图:被 1 处调用(test_find_lms_with_mock_home);外部调用 5 个(new, new, format!, var, which)。
LMStudioClient::download_model168–190 ↗
async fn download_model(&self, model: &str) -> std::io::Result<()>
作用:通过 LM Studio 的 lms 命令下载指定模型。它适合在发现模型缺失时自动补齐模型文件。
数据流:进去的是模型名称;它先找到 lms 命令,向用户输出正在下载,然后启动外部进程执行 lms get --yes <model>,把标准输出直接显示给用户、隐藏错误输出;如果命令退出成功就记录日志,否则返回下载失败和退出码。
调用关系:它在模型准备阶段使用。它先依赖 find_lms 找工具,再把真正的下载工作交给系统进程和 LM Studio 的命令行程序。
调用图:外部调用 8 个(find_lms, new, other, eprintln!, format!, inherit, null, info!)。
LMStudioClient::from_host_root194–203 ↗
fn from_host_root(host_root: impl Into<String>) -> Self
作用:用一个原始服务器地址直接造出 LMStudioClient。它主要给测试用,绕过配置文件和服务器预检查。
数据流:进去的是类似 http://localhost:1234 的地址;它创建一个带 5 秒连接超时的 HTTP 客户端,并把地址保存到 base_url;出来的是一个可以请求该地址的 LMStudioClient。
调用关系:它只在测试配置下存在。多个测试用它把客户端指向 wiremock 假服务器,从而验证 fetch_models 和 check_server 的行为。
调用图:被 6 处调用(test_check_server_error, test_check_server_happy_path, test_fetch_models_happy_path, test_fetch_models_no_data_array, test_fetch_models_server_error, test_from_host_root);外部调用 3 个(into, builder, from_secs)。
tests::test_fetch_models_happy_path212–241 ↗
async fn test_fetch_models_happy_path()
作用:验证 fetch_models 在服务器正常返回模型列表时,真的能读出模型 id。
数据流:进去的是测试里搭好的假服务器;测试让假服务器对 GET /models 返回带 data 数组的 JSON,然后用 from_host_root 创建客户端并调用 fetch_models;最后检查结果里包含 openai/gpt-oss-20b。
调用关系:它是 fetch_models 的成功场景测试。如果检测到沙盒禁用了网络,就跳过,避免测试环境限制导致误报。
调用图:调用 1 个内部函数(from_host_root);外部调用 9 个(assert!, json!, var, info!, given, start, new, method, path)。
tests::test_fetch_models_no_data_array244–272 ↗
async fn test_fetch_models_no_data_array()
作用:验证服务器虽然返回成功,但 JSON 里没有 data 数组时,fetch_models 会明确报错。
数据流:进去的是一个返回空 JSON 的假服务器;测试创建客户端、调用 fetch_models;出来的预期不是模型列表,而是错误,并且错误文字包含 No 'data' array in response。
调用关系:它覆盖 fetch_models 的坏数据场景,确保服务器返回格式不合预期时不会悄悄给出空结果。
调用图:调用 1 个内部函数(from_host_root);外部调用 9 个(assert!, json!, var, info!, given, start, new, method, path)。
tests::test_fetch_models_server_error275–300 ↗
async fn test_fetch_models_server_error()
作用:验证 LM Studio 服务器返回 500 这类错误码时,fetch_models 会把错误传出来。
数据流:进去的是一个对 /models 返回 500 的假服务器;测试创建客户端并调用 fetch_models;结果应该是错误,错误文字里包含 Failed to fetch models: 500。
调用关系:它覆盖 fetch_models 的服务器故障场景,保证上层能知道不是“没有模型”,而是服务器出错了。
调用图:调用 1 个内部函数(from_host_root);外部调用 8 个(assert!, var, info!, given, start, new, method, path)。
tests::test_check_server_happy_path303–324 ↗
tests::test_check_server_error327–352 ↗
async fn test_check_server_error()
作用:验证 check_server 在服务器返回错误码时会失败,并带上具体状态码。
数据流:进去的是一个对 /models 返回 404 的假服务器;测试创建客户端并调用 check_server;结果应该是错误,错误文字包含 Server returned error: 404。
调用关系:它测试健康检查的失败路径,确保 try_from_provider 这类准备流程能及时发现 LM Studio 地址不对或服务异常。
调用图:调用 1 个内部函数(from_host_root);外部调用 8 个(assert!, var, info!, given, start, new, method, path)。
tests::test_find_lms355–367 ↗
tests::test_find_lms_with_mock_home370–387 ↗
fn test_find_lms_with_mock_home()
作用:验证备用安装路径的拼接逻辑,不依赖测试机器真实的 HOME 或 USERPROFILE。
数据流:进去的是测试给出的假用户目录;它调用 find_lms_with_home_dir;如果没找到文件,就检查错误文字是否合理。
调用关系:它直接测试 find_lms_with_home_dir。这样不同操作系统下,Unix 的 /test/home 和 Windows 的 C:\test\home 路径规则都能被覆盖。
调用图:调用 1 个内部函数(find_lms_with_home_dir);外部调用 1 个(assert!)。
tests::test_from_host_root390–396 ↗
fn test_from_host_root()
作用:验证测试用构造函数会原样保存传入的服务器地址。
数据流:进去的是两个不同形式的 URL;它分别调用 from_host_root 创建客户端;出来的客户端应当把 base_url 保存成完全相同的字符串。
调用关系:它保护 from_host_root 这个测试工具函数,确保其他网络测试把客户端指向假服务器时不会被地址处理逻辑干扰。
调用图:调用 1 个内部函数(from_host_root);外部调用 1 个(assert_eq!)。
API 端点会话层
这些文件定义提供方配置,以及高级 API 客户端用于组装和执行请求的端点/会话脚手架。
codex-api/src/provider.rs源码 ↗
可以把 Provider 理解成一张“外部 API 服务的名片”:上面写着服务名字、基础网址、默认请求头、固定查询参数、重试规则,以及流式连接多久没动静算超时。别的地方要发请求时,不需要自己拼完整网址,只要把路径交给它,它会把基础网址、路径和查询参数合成最终地址。RetryConfig 则是“失败后要不要再试”的规则,比如遇到太频繁请求、服务器错误或网络断开时是否重试。这个文件还特别照顾 Azure 这类部署:有些接口因为服务商不同,请求格式会不一样,所以它提供了判断“这是不是 Azure Responses 接口”的方法。最后,它还能把普通 http/https 地址换成 ws/wss,也就是 WebSocket 用的长连接地址,供实时通信使用。
RetryConfig::to_policy25–35 ↗
fn to_policy(&self) -> RetryPolicy
作用:把这个项目里更好理解的重试配置,转换成底层网络客户端真正会使用的重试策略。有人要发请求前,会用它告诉客户端:最多试几次、每次间隔多久、哪些错误值得再试。
数据流:进去的是 RetryConfig 里保存的几个设置:最大尝试次数、基础等待时间,以及是否重试 429、5xx 和网络错误。它把这些字段原样整理成 codex-client 认识的 RetryPolicy。出来的是一个 RetryPolicy,不会改动原来的 RetryConfig。
调用关系:它是高层配置和底层请求客户端之间的翻译员。Provider 保存的是 RetryConfig,而真正执行网络重试的 codex-client 需要 RetryPolicy,所以发请求的流程会在需要底层策略时用到这个转换。
Provider::url_for_path53–75 ↗
fn url_for_path(&self, path: &str) -> String
作用:根据服务的基础网址和一个接口路径,拼出最终要访问的完整网址。这样其他代码不用关心斜杠多一个少一个,也不用重复追加固定查询参数。
数据流:进去的是一个路径,比如 “/v1/responses”,以及 Provider 里已有的 base_url 和 query_params。它先去掉基础网址末尾多余的斜杠,再去掉路径开头多余的斜杠,然后把两段接起来;如果配置里有固定查询参数,就追加成 ?k=v&k2=v2。出来的是一个完整 URL 字符串,Provider 本身不被修改。
调用关系:它是构造各种连接地址的基础零件。Provider::build_request 会用它生成普通 HTTP 请求地址,Provider::websocket_url_for_path 会先用它生成普通地址,再改成 WebSocket 地址。
调用图:被 2 处调用(build_request, websocket_url_for_path);外部调用 1 个(format!)。
Provider::build_request77–86 ↗
fn build_request(&self, method: Method, path: &str) -> Request
作用:快速做出一个还没带请求体的 HTTP 请求模板。调用者只要给请求方法和路径,它就补上完整网址、默认请求头等通用信息。
数据流:进去的是 HTTP 方法,比如 GET 或 POST,以及接口路径。它调用 Provider::url_for_path 拼出完整网址,复制一份默认 headers,请求体先设为空,压缩先设为不压缩,超时先不单独指定。出来的是一个 Request 对象,供后续代码继续填 body 或直接发送。
调用关系:它被 make_request 这类真正准备发请求的流程调用。它自己不负责联网,只负责把 Provider 里的通用配置装进 Request;其中网址部分交给 Provider::url_for_path 完成。
调用图:调用 1 个内部函数(url_for_path);被 1 处调用(make_request);外部调用 1 个(clone)。
Provider::is_azure_responses_endpoint88–90 ↗
fn is_azure_responses_endpoint(&self) -> bool
作用:判断当前这个 Provider 指向的是否像 Azure 的 Responses 接口。这个判断很重要,因为 Azure 部署有时需要走和普通 OpenAI 接口不同的请求格式或路径规则。
数据流:进去的是 Provider 自己的 name 和 base_url。它把这两个信息交给 is_azure_responses_provider 去判断。出来的是 true 或 false,不会修改 Provider。
调用关系:它被 build_responses_request 使用,用来决定构造 Responses 请求时是否要按 Azure 的方式处理。真正的判断细节不放在这里,而是交给 is_azure_responses_provider。
调用图:调用 1 个内部函数(is_azure_responses_provider);被 1 处调用(build_responses_request)。
Provider::websocket_url_for_path92–103 ↗
fn websocket_url_for_path(&self, path: &str) -> Result<Url, url::ParseError>
作用:把某个接口路径变成 WebSocket 连接用的网址。WebSocket 可以理解成一根保持打开的电话线,适合实时通信,而不是每次都重新打一次电话。
数据流:进去的是路径,以及 Provider 里的基础网址和查询参数。它先调用 Provider::url_for_path 得到普通 URL,再解析成 URL 对象;如果开头是 http,就改成 ws;如果是 https,就改成 wss;如果本来就是 ws 或 wss,就直接返回;其他协议则保持不变。出来的是 Url 对象,或者在网址格式不合法时返回解析错误。
调用关系:它被 connect 和 probe_handshake 这类建立或试探 WebSocket 连接的流程调用。它把普通地址拼接交给 Provider::url_for_path,自己只负责协议头的转换和 URL 解析。
调用图:调用 1 个内部函数(url_for_path);被 2 处调用(connect, probe_handshake);外部调用 1 个(parse)。
is_azure_responses_provider106–114 ↗
fn is_azure_responses_provider(name: &str, base_url: Option<&str>) -> bool
作用:根据服务名字和基础网址,判断它是不是 Azure Responses 提供方。它给外层代码一个简单答案:要不要按 Azure 的特殊规则来处理。
数据流:进去的是 provider 名字和一个可选的 base_url。它先看名字是不是忽略大小写等于 “azure”;如果是,就直接返回 true。否则,如果给了 base_url,就交给 matches_azure_responses_base_url 看网址里有没有 Azure 特征;如果连网址也没有,就返回 false。出来的是布尔值。
调用关系:它是 Azure 判断的公共入口,被 Provider::is_azure_responses_endpoint 调用。它把更细的网址匹配工作交给 matches_azure_responses_base_url。
调用图:调用 1 个内部函数(matches_azure_responses_base_url);被 1 处调用(is_azure_responses_endpoint)。
matches_azure_responses_base_url116–127 ↗
fn matches_azure_responses_base_url(base_url: &str) -> bool
作用:检查一个基础网址里是否包含常见的 Azure OpenAI 地址特征。它像一个关键词筛查器,用来识别那些名字不叫 azure、但网址明显属于 Azure 的服务。
数据流:进去的是 base_url 字符串。它先把网址转成小写,避免大小写影响判断;然后拿它和一组 Azure 常见标记做包含检查,比如 openai.azure、cognitiveservices.azure、azurefd 等。只要命中一个标记就返回 true,否则返回 false。
调用关系:它不直接面对外部调用者,主要服务于 is_azure_responses_provider。上层先判断名字,名字不够明确时,才会靠它从网址里找 Azure 的线索。
调用图:被 1 处调用(is_azure_responses_provider)。
tests::detects_azure_responses_base_urls134–168 ↗
fn detects_azure_responses_base_urls()
作用:验证 Azure 识别规则有没有把该认出的 Azure 地址认出来,也有没有误把普通地址当成 Azure。它是防止以后改代码时把判断规则弄坏的安全网。
数据流:进去的是测试里写死的一批正例和反例网址。它逐个调用 is_azure_responses_provider,并用断言检查结果:正例必须是 true,反例必须是 false;另外还检查名字写成 “Azure” 时也能识别。测试通过时没有业务输出,失败时会报告哪个网址判断错了。
调用关系:它只在测试运行时活跃,不参与正常请求流程。它通过一组样例保护 is_azure_responses_provider 和 matches_azure_responses_base_url 的行为,确保 Azure 判断继续可靠。
调用图:外部调用 1 个(assert!)。
codex-api/src/endpoint/session.rs源码 ↗
这个文件定义了 EndpointSession。可以把它理解成访问后端 API 的一张“工作台”:左边放着 Provider,也就是知道请求该发到哪里、该怎么重试的配置;中间放着 auth,也就是给请求加上登录凭证的工具;右边放着 transport,也就是真正把 HTTP 请求发出去的通道。普通请求走 execute 或 execute_with,流式返回的请求走 stream_encoded_json_with。它们都会先拼好路径、请求头和请求体,再套上认证,然后交给传输层发送;同时还会通过 run_with_request_telemetry 记录请求情况并按策略重试。这个文件重要的地方在于,它把“怎么正确、安全、可观测地发请求”集中起来,避免各个 API 功能到处重复写一套容易出错的流程。
EndpointSession::new27–34 ↗
fn new(transport: T, provider: Provider, auth: SharedAuthProvider) -> Self
作用:创建一个新的 EndpointSession,也就是准备好一个能发 API 请求的会话对象。调用者给它传入 HTTP 发送工具、服务商配置和认证工具,它就把这些东西收在一起备用。
数据流:输入是一套 transport、provider 和 auth → 函数把它们放进 EndpointSession 里,并把请求遥测先设为空 → 输出一个新的会话对象,之后所有请求都会靠它来发送。
调用关系:它是各种上层客户端创建会话时的起点。后续如果需要记录请求情况,会再接着调用 EndpointSession::with_request_telemetry;如果直接发请求,则会进入 execute、execute_with 或 stream_encoded_json_with。
EndpointSession::with_request_telemetry36–42 ↗
fn with_request_telemetry(
mut self,
request: Option<Arc<dyn RequestTelemetry>>,
) -> Self
作用:给这个会话加上可选的请求遥测。遥测可以理解成“请求记录员”,用来记录请求是否成功、耗时、重试等信息。
数据流:输入是已有的 EndpointSession 和一个可有可无的 RequestTelemetry → 函数把这个记录工具存到会话里 → 输出同一个会话对象,只是之后发请求时会带上记录能力。
调用关系:它通常在创建会话之后、真正发送请求之前被调用。后面的 EndpointSession::execute_with 和 EndpointSession::stream_encoded_json_with 会把这里保存的遥测交给 run_with_request_telemetry 使用。
调用图:被 7 处调用(with_telemetry, with_telemetry, with_telemetry, with_telemetry, with_telemetry, with_telemetry, with_telemetry)。
EndpointSession::provider44–46 ↗
fn provider(&self) -> &Provider
作用:把会话里保存的 Provider 借给外部查看。Provider 可以理解成“服务商说明书”,里面知道请求基础地址、路径形状、重试规则等信息。
数据流:输入是这个会话本身 → 函数不修改任何东西,只返回里面 provider 的只读引用 → 调用者可以读取服务商配置,但不能通过这里替换它。
调用关系:它被一些需要检查后端请求格式或流式请求设置的地方使用。它不负责发送请求,只是让外部能看到 EndpointSession 当前使用的是哪套服务商配置。
调用图:被 3 处调用(uses_backend_request_shape, stream_encoded, stream_request)。
EndpointSession::make_request48–61 ↗
fn make_request(
&self,
method: &Method,
path: &str,
extra_headers: &HeaderMap,
body: Option<&RequestBody>,
) -> Request
作用:把方法、路径、额外请求头和请求体拼成一个完整 Request。它像写快递单:先按服务商规则填好地址,再补上额外标签和包裹内容。
数据流:输入是 HTTP 方法、API 路径、额外请求头,以及可选请求体 → 函数先让 provider.build_request 生成基础请求,再复制并追加额外请求头,如果有请求体也复制进去 → 输出一个可以继续加工或发送的 Request。
调用关系:它是内部拼请求的公共小步骤。EndpointSession::stream_encoded_json_with 会直接调用它;EndpointSession::execute_with 也通过闭包使用同样的拼装思路,让每次重试都能重新生成请求。
调用图:调用 1 个内部函数(build_request);被 1 处调用(stream_encoded_json_with);外部调用 2 个(clone, clone)。
EndpointSession::execute63–72 ↗
async fn execute(
&self,
method: Method,
path: &str,
extra_headers: HeaderMap,
body: Option<Value>,
) -> Result<Response, ApiError>
作用:发送一个普通的非流式 API 请求,并返回完整响应。它是最省事的入口,适合不需要额外改 Request 细节的调用者。
数据流:输入是 HTTP 方法、路径、额外请求头和可选 JSON 数据 → 它不自己做复杂发送,而是把这些原样交给 EndpointSession::execute_with,并传入一个什么都不改的配置函数 → 输出成功的 Response,或者返回 ApiError 表示出错。
调用关系:它是 summarize、search、post_image_request 这类普通接口会用的快捷入口。真正的认证、重试、遥测和网络发送都交给 EndpointSession::execute_with 完成。
调用图:调用 1 个内部函数(execute_with);被 3 处调用(post_image_request, summarize, search)。
EndpointSession::execute_with80–114 ↗
async fn execute_with(
&self,
method: Method,
path: &str,
extra_headers: HeaderMap,
body: Option<Value>,
configure: C,
) -> Result<Response, ApiErro
作用:发送一个普通 API 请求,但允许调用者在发送前再细调 Request。比如某些接口要额外改请求参数或头信息,就会用这个更灵活的版本。
数据流:输入是方法、路径、请求头、可选 JSON 请求体,以及一个 configure 函数 → 它把 JSON 包成 RequestBody,准备一个能重新生成 Request 的步骤,每次生成后都让 configure 再修改一下;随后 run_with_request_telemetry 按重试策略执行请求,发送前先用 auth.apply_auth 加认证,再用 transport.execute 发出去 → 输出完整 Response;如果构建、认证、网络或后端返回过程出错,就变成 ApiError。
调用关系:它是普通请求的核心通道。EndpointSession::execute 会把简单请求转给它;compact、list_models、create_with_headers 等更复杂的接口也会直接调用它。它自己不直接管统计和重试细节,而是交给 run_with_request_telemetry 包住整个发送过程。
调用图:调用 1 个内部函数(run_with_request_telemetry);被 5 处调用(compact, list_models, create_with_headers, create_with_session_architecture_and_headers, execute)。
EndpointSession::stream_encoded_json_with122–155 ↗
async fn stream_encoded_json_with(
&self,
method: Method,
path: &str,
extra_headers: HeaderMap,
body: Option<EncodedJsonBody>,
configure: C,
) -> Re
作用:发送一个会返回流式数据的请求。流式数据可以理解成“边生成边传回来”,比如长回答不是一次性拿到,而是一段一段收到。
数据流:输入是方法、路径、请求头、已经编码好的 JSON 请求体,以及一个可修改 Request 的 configure 函数 → 它先用 make_request 拼出请求,再让 configure 调整,接着把请求准备成可发送格式并克隆给重试流程;发送时同样先加认证,再调用 transport.stream 打开流 → 输出 StreamResponse,也就是后续可以持续读取的数据流;构建、认证或网络出错会返回 ApiError。
调用关系:它是 stream_encoded 这类流式接口的核心出口。它复用 EndpointSession::make_request 来拼请求,并和 EndpointSession::execute_with 一样,把重试和遥测交给 run_with_request_telemetry,只是最后调用的是 transport.stream,而不是普通的 transport.execute。
调用图:调用 2 个内部函数(make_request, run_with_request_telemetry);被 1 处调用(stream_encoded)。
codex-api/src/endpoint/mod.rs源码 ↗
这个文件本身不做网络请求,也不处理具体业务。它的作用是把 endpoint 这个文件夹下面的各个功能模块整理好,并把常用的类型重新导出。可以把它想成商场的楼层导览:compact、images、models、responses、search 等模块是不同店铺,这个文件告诉 Rust 编译器这些店铺存在,并把门牌号放到入口处。这样外部代码可以直接使用 CompactClient、ImagesClient、ResponsesClient、RealtimeWebsocketClient 等名字,而不必知道它们具体藏在哪个子文件里。这里还有一个 session 模块没有公开导出,说明它更像内部零件,只给这个 endpoint 子系统自己用。没有这个文件,项目的 API 客户端入口会变得零散,调用方需要记住很多内部路径,维护起来也更容易出错。
codex-api/src/requests/mod.rs源码 ↗
这个文件本身不做网络请求,也不处理数据,它的作用是给“请求”这一块代码搭一个入口。它声明了两个子模块:headers 和 responses,前者通常放请求头相关的东西,后者放响应处理相关的东西。然后它把 responses 里的 Compression 暴露出去,让外部可以直接使用“压缩方式”这个概念;又把 attach_item_ids 在当前 crate 内部重新导出,方便项目内部其他地方给响应里的条目补上编号。可以把它理解成超市的导购牌:货物不在牌子里,但牌子告诉大家去哪里拿,并把常用货品摆到更显眼的位置。
流式与上传辅助工具
这些文件涵盖 API 侧的专用传输辅助工具,用于 SSE 暴露、实时 WebSocket 事件解析和文件上传工作流。
codex-api/src/sse/mod.rs源码 ↗
SSE 指的是 Server-Sent Events,可以理解成服务器像水龙头一样持续把消息推给客户端,而不是一次性回完。这个文件的作用很像一个前台接待处:真正干活的代码在 responses 这个子模块里,但外面的代码不需要知道里面文件怎么分,只要从这里拿到需要的东西就行。它声明了 responses 模块,并把几个关键入口重新导出:有的只给当前 crate 内部用,比如事件类型和事件处理函数;有的对外公开,比如启动响应流的函数。这样做的好处是接口更干净,后面即使内部文件调整,使用方也不一定要跟着改。
codex-api/src/endpoint/realtime_websocket/protocol_common.rs源码 ↗
实时 WebSocket 可以理解成一条一直开着的电话线,服务端会不断发来各种消息,比如“会话更新了”“转写文字新增了一小段”“转写结束了”“出错了”。这个文件做的事,就是先把收到的字符串当作 JSON(一种常见的数据格式)解析出来,再从里面挑出关键字段,包装成项目自己的事件类型。它不会强行相信外部传来的内容:如果 JSON 格式不对,或者缺少必要的 type、session.id 等字段,它会记录一条调试日志,然后返回空结果,让上层代码决定跳过这条坏消息。这里的几个小函数像一组统一的拆信工具,被 v1 和 v2 两套实时协议解析器共同使用,保证不同版本遇到相同结构时用同一套规则。
parse_realtime_payload7–25 ↗
fn parse_realtime_payload(payload: &str, parser_name: &str) -> Option<(Value, String)>
作用:把 WebSocket 收到的一整段文字先解析成 JSON,并取出里面的 type 字段。别人会用它来判断这条消息到底是哪一种事件。
数据流:进去的是原始 payload 字符串,以及一个 parser_name 用来写日志。它先用 JSON 解析器把字符串变成可查询的数据;如果失败,就记一条调试信息并返回空。解析成功后,它继续找 type 字段;没有 type 也返回空。有 type 时,出来的是完整 JSON 数据和消息类型字符串。
调用关系:它是 v1 和 v2 实时事件解析流程的第一道门。parse_realtime_event_v1 和 parse_realtime_event_v2 会先叫它拆开原始消息;它内部只把真正的 JSON 解析交给 serde_json::from_str,出错时用 debug! 留线索。
调用图:被 2 处调用(parse_realtime_event_v1, parse_realtime_event_v2);外部调用 2 个(debug!, from_str)。
parse_session_updated_event27–44 ↗
fn parse_session_updated_event(parsed: &Value) -> Option<RealtimeEvent>
作用:从一条“会话已更新”的消息里拿出会话编号和可选的指令内容,并做成内部事件。会话编号是必须的,没有它就不能知道更新的是哪一次实时会话。
数据流:进去的是已经解析好的 JSON。它会沿着 session 这个对象往里找 id,并把它转成字符串;还会尝试找 session.instructions,如果有就带上,没有也可以。找不到 session.id 时,出来的是空;找到了就出来一个 RealtimeEvent::SessionUpdated,同时包含实时会话 ID 和可能存在的 instructions。
调用关系:当 parse_realtime_event_v1 或 parse_realtime_event_v2 已经判断消息类型是会话更新时,会把 JSON 交给它。它只负责提取会话相关字段,不负责判断整体消息类型。
调用图:被 2 处调用(parse_realtime_event_v1, parse_realtime_event_v2);外部调用 1 个(get)。
parse_transcript_delta_event46–55 ↗
fn parse_transcript_delta_event(
parsed: &Value,
field: &str,
) -> Option<RealtimeTranscriptDelta>
作用:从消息里取出一小段新增的转写文字。这里的“delta”就是“新增片段”,适合实时字幕那种一句话慢慢冒出来的场景。
数据流:进去的是已解析的 JSON,以及要读取的字段名 field。它按这个字段名去找字符串;找不到或不是字符串,就返回空。找到了就把这段文字放进 RealtimeTranscriptDelta 里作为结果返回。
调用关系:v1 和 v2 的实时事件解析器在处理“转写新增片段”类消息时会调用它。字段名由上层传进来,所以同一个函数可以兼容不同协议版本里字段名字不完全一样的情况。
调用图:被 2 处调用(parse_realtime_event_v1, parse_realtime_event_v2);外部调用 1 个(get)。
parse_transcript_done_event57–66 ↗
fn parse_transcript_done_event(
parsed: &Value,
field: &str,
) -> Option<RealtimeTranscriptDone>
作用:从消息里取出最终完成的整段转写文字。它用于表示实时转写已经收尾,不再只是一个小片段。
数据流:进去的是已解析的 JSON,以及要读取的字段名 field。它查找该字段,并要求它是字符串;如果不符合就返回空。成功时,它把完整文本装进 RealtimeTranscriptDone 并返回。
调用关系:parse_realtime_event_v1 和 parse_realtime_event_v2 在遇到“转写完成”事件时会用它。它和 parse_transcript_delta_event 很像,但返回的类型表示“最终文本”,语义不同。
调用图:被 2 处调用(parse_realtime_event_v1, parse_realtime_event_v2);外部调用 1 个(get)。
parse_error_event68–83 ↗
fn parse_error_event(parsed: &Value) -> Option<RealtimeEvent>
作用:从错误消息里尽量找出一段人能看懂的错误说明,并包装成内部错误事件。外部服务的错误格式可能不完全一致,所以它会按多种常见位置去找。
数据流:进去的是已解析的 JSON。它先找顶层 message 字段;如果没有,再找 error 对象里的 message;如果还没有,但存在 error 字段,就把整个 error 内容转成字符串。只要找到任何可用说明,就出来一个 RealtimeEvent::Error;完全找不到则返回空。
调用关系:v1 和 v2 的实时事件解析器在识别到错误类消息时会调用它。它不要求错误一定长成同一种格式,这让上层解析流程更稳,不会因为错误返回样式稍有变化就完全读不出来。
调用图:被 2 处调用(parse_realtime_event_v1, parse_realtime_event_v2);外部调用 1 个(get)。
codex-api/src/files.rs源码 ↗
这个文件解决的是“把一个文件安全地交给 OpenAI 后端使用”的问题。它先限制文件大小,超过 512MB 就直接拒绝,避免浪费时间和网络。然后它向后端申请一个文件记录,后端会返回文件编号和真正的上传地址。接着它把文件内容像水流一样传过去,这里的“流”就是不用一次把整个文件塞进内存,而是一块一块发送。上传完后,它还会反复通知后端“我传完了”,直到后端说成功,或者等太久就报错。成功后,它返回 UploadedOpenAiFile,里面有文件编号、sediment:// 开头的内部地址、下载链接、文件名、大小和类型。文件里还统一处理鉴权请求,也就是自动加上登录凭证;并且构建 HTTP 客户端时会尝试使用自定义证书,失败时退回普通客户端。
openai_file_uri80–82 ↗
fn openai_file_uri(file_id: &str) -> String
作用:把 OpenAI 返回的文件编号变成系统内部使用的文件地址。比如 file_123 会变成 sediment://file_123,方便其他地方用统一格式引用这个文件。
数据流:进去的是一个文件编号 → 函数在前面加上固定前缀 sediment:// → 出来的是一个字符串形式的内部文件地址,不改动任何外部状态。
调用关系:它是上传流程最后收尾时用的小工具。upload_openai_file 在确认上传成功后调用它,把后端文件编号包装成调用方能保存和传递的标准地址。
调用图:被 1 处调用(upload_openai_file);外部调用 1 个(format!)。
upload_openai_file84–213 ↗
async fn upload_openai_file(
base_url: &str,
auth: &dyn AuthProvider,
file_name: String,
file_size_bytes: u64,
contents: impl Stream<Item = std::io::Result<Bytes>> + Send + 'static
作用:完成一次完整的 OpenAI 文件上传。调用者只要给它后端地址、鉴权方式、文件名、文件大小和文件内容,它就负责登记、上传、确认,并返回可用的文件信息。
数据流:进去的是基础网址、登录凭证、文件名、文件大小和文件内容流 → 它先检查大小,再带着凭证向 /files 创建文件记录,拿到 file_id 和 upload_url;然后把内容 PUT 到 upload_url;最后反复请求 /files/{file_id}/uploaded 等后端确认 → 成功时出来 UploadedOpenAiFile;失败时出来明确的错误,比如文件太大、网络请求失败、返回内容看不懂、上传还没准备好或后端报告失败。
调用关系:这是本文件的主流程函数,也是外部最可能直接使用的入口。它把具体小活分给 authorized_request 来做带鉴权的请求,分给 build_reqwest_client 来创建网络客户端,最后用 openai_file_uri 生成标准文件地址。测试函数 tests::upload_openai_file_returns_canonical_uri 会调用它来验证整套流程。
调用图:调用 3 个内部函数(authorized_request, build_reqwest_client, openai_file_uri);被 1 处调用(upload_openai_file_returns_canonical_uri);外部调用 6 个(now, wrap_stream, format!, from_str, json!, sleep)。
authorized_request215–228 ↗
fn authorized_request(
auth: &dyn AuthProvider,
method: reqwest::Method,
url: &str,
) -> reqwest::RequestBuilder
作用:创建一个已经带好登录信息的 HTTP 请求。这样上传流程里每次访问 OpenAI 后端时,都不用重复手写鉴权头。
数据流:进去的是鉴权提供者、请求方法和网址 → 它新建一组请求头,让鉴权提供者把 Authorization 等登录信息塞进去,再创建 HTTP 客户端并设置超时时间 → 出来的是一个还没发送的请求构造器,后续可以继续加 JSON 内容再发送。
调用关系:它服务于 upload_openai_file 中访问 OpenAI 后端的两类请求:创建文件记录和确认上传完成。它内部会调用 build_reqwest_client 来拿到真正发请求用的客户端。
调用图:调用 1 个内部函数(build_reqwest_client);被 1 处调用(upload_openai_file);外部调用 2 个(add_auth_headers, new)。
build_reqwest_client230–235 ↗
fn build_reqwest_client() -> reqwest::Client
作用:创建发送 HTTP 请求用的客户端,并尽量支持项目里的自定义证书。自定义证书常用于公司网络、代理或特殊后端环境,能避免 HTTPS 连接因为不认识证书而失败。
数据流:它不需要业务输入 → 先尝试用带自定义 CA 证书的方式构建 reqwest 客户端;如果失败,就记录一条警告并退回普通客户端 → 出来的是一个可以发网络请求的客户端。
调用关系:它是底层网络准备工具。authorized_request 用它来创建带鉴权的请求;upload_openai_file 也直接用它把文件内容上传到后端给出的临时上传地址。
调用图:被 2 处调用(authorized_request, upload_openai_file);外部调用 2 个(builder, build_reqwest_client_with_custom_ca)。
tests::ChatGptTestAuth::add_auth_headers258–264 ↗
fn add_auth_headers(&self, headers: &mut reqwest::header::HeaderMap)
作用:这是测试里用的假鉴权器,用来模拟真实登录信息。它会给请求加上固定的 Authorization 和 ChatGPT-Account-ID 请求头,让测试服务器能检查上传函数有没有正确带上凭证。
数据流:进去的是一份可修改的请求头集合 → 它插入 Bearer token 和 account_id 两个固定值 → 出来时这份请求头集合已经带上测试用登录信息。
调用关系:它实现了 AuthProvider 接口,供 tests::chatgpt_auth 返回给测试。upload_openai_file 运行时会通过 authorized_request 间接调用这个方法,测试因此能确认鉴权头确实被加到了请求里。
调用图:外部调用 2 个(insert, from_static)。
tests::chatgpt_auth267–269 ↗
fn chatgpt_auth() -> ChatGptTestAuth
作用:返回一个测试用的鉴权对象。它让测试代码不用每次都手动创建 ChatGptTestAuth。
数据流:它没有输入 → 直接创建并返回 ChatGptTestAuth → 不修改外部状态。
调用关系:测试函数 tests::upload_openai_file_returns_canonical_uri 调用它,把返回的假鉴权对象传给 upload_openai_file,用来模拟真实用户已经登录的情况。
tests::base_url_for271–273 ↗
fn base_url_for(server: &MockServer) -> String
作用:根据测试服务器的地址拼出后端 API 的基础地址。这样测试不会依赖真实 OpenAI 后端,而是打到本地模拟服务器。
数据流:进去的是一个 MockServer,也就是测试用的假服务器 → 它读取服务器地址,并在后面加上 /backend-api → 出来的是上传函数可以使用的 base_url 字符串。
调用关系:它被测试函数 tests::upload_openai_file_returns_canonical_uri 使用。测试先启动假服务器,再用这个函数生成 upload_openai_file 要访问的基础网址。
调用图:外部调用 1 个(format!)。
tests::upload_openai_file_returns_canonical_uri276–343 ↗
async fn upload_openai_file_returns_canonical_uri()
作用:这是一个自动化测试,用来证明完整上传流程能跑通,并且最终返回的内部地址是 sediment://file_id 这种标准格式。它还检查上传完成确认时遇到一次 retry 后,函数会等待并重试。
数据流:测试先启动一个假服务器,并设置三类假响应:创建文件、接收文件内容、确认上传完成 → 然后准备内容为 hello 的文件流,调用 upload_openai_file → 最后检查返回的 file_id、uri、下载地址、文件名、MIME 类型,以及确认接口是否被调用了两次。
调用关系:它是本文件主函数 upload_openai_file 的验证者。它用 wiremock 假装后端服务,用 tests::chatgpt_auth 提供假登录信息,用 tests::base_url_for 拼测试地址,从外部用户视角检查这套上传机器是否按预期工作。
调用图:调用 1 个内部函数(upload_openai_file);外部调用 17 个(clone, new, new, from_static, given, start, new, assert_eq!, base_url_for, chatgpt_auth (+7 more))。