opencodex Design B 深度解析:用 openai_base_url 注入替代 model_provider 重打标签,让 Codex 线程原生直通代理
opencodex Design B 深度解析:用 openai_base_url 注入替代 model_provider 重打标签,让 Codex 线程原生直通代理
导读
本文聚焦 opencodex 项目中一项名为 Design B(openai_base_url injection, no re-tag) 的核心设计改造:在保留 Codex 原生 openai provider 标识的前提下,通过向 Codex 的 config.toml 注入官方内置的根级 openai_base_url 配置,把原生 codex 请求路由进 opencodex 代理,从而彻底消除以往"重打标签(re-tag)"方案带来的线程历史重映射与恢复问题。全文以 devlog 工作单元 devlog/_fin/260706_design-b-openai-base-url 的 000~999 七份文档为骨架,并结合当前仓库 src/codex、src/server、src/cli 下的真实实现展开。读完本文,你将掌握:Design B 的触发背景与目标、zstd 压缩请求体解压原理、TOML 注入与摘除的完整规则、线程历史一次性迁移与守护机制,以及 Windows/Linux 跨平台下的换行符(EOL)与只读探测加固细节。
一、背景:从"重打标签"到"官方覆盖"的架构演进
1.1 触发事件与问题根源
Design B 的立项直接源于 2026-07-05 的一次 spike(macmini-cf):实验证明,在 Codex 配置中注入 openai_base_url 可以让原生 codex 把请求路由进代理,且线程 tag 保持 openai——"重打标签失败"这一类问题随之消失。而在此之前,opencodex 采用的注入方式是把 model_provider 改写为 "opencodex",并追加 [model_providers.opencodex] 表。这种 re-tag 方式带来的核心痛点是:Codex 线程历史按 provider id 归档与恢复,一旦 provider 被改写,历史线程就需要映射、回滚或重放,任何一步失败都会导致线程"消失"。
1.2 目标与非目标
该工作单元的 000_plan.md 明确划定了边界:
- 目标:原生
codex无需改变model_provider即可路由进 ocx,Codex 线程历史从此不再需要重映射或恢复。 - 非目标:npm 发布、本机代理重启、GUI 改动、用量归因/目录路由变更(已核实不受影响)、移除
recover-history。 - 验证器:完整
bun test全绿,且新增"解压 + Design B 注入"专项测试。 - 停止条件:三个 phase 均以全新测试证据完成 D 关闭。
- HOTL 约束:只写本仓库(src/、tests/、devlog/、README);不推送、不全局安装、不杀掉运行中的代理(端口 10100)、不改动
~/.opencodex或~/.codex的线上配置。
1.3 已验证的关键事实(carry-over)
计划文档沉淀了四条决定设计走向的验证事实:
[model_providers.openai]表覆盖会被拒绝(RESERVED_MODEL_PROVIDER_IDS,config_toml.rs:61),配置加载直接失败——Design B 绝不能输出该表。openai_base_url根键是官方内置 OpenAI 覆盖项(config_toml.rs:359 → built_in_model_providers → create_openai_provider),优先级同时高于api.openai.com和CHATGPT_CODEX_BASE_URL(to_api_provider, lib.rs:246)。- Spike(ocx 2.6.25 + codex 0.142.5,隔离 HOME)确认:请求 200 到达代理、线程存储
model_provider='openai'、WS 升级到ws://…/v1/responses正常。 - zstd 风险:
enable_request_compression(Stable,默认开)在"认证为 codex-backend 且 provideris_openai()"时触发(client.rs:1213)。WS 路径不受影响,但 HTTP 回退路径会发送content-encoding: zstd的请求体——当时的 ocx 会返回 400 "Invalid JSON body"(已用 curl 复现),因此 Design B 上线前必须先支持解压。 - 分析与用量旁路请求直连 chatgpt.com,不受影响。
更底层的 codex-rs 事实存放在 devlog/_fin/260702_codex-history-sync-hardening,本单元只携带 Design B 增量事实。
二、Phase 1:/v1/responses 请求体解压(zstd/gzip/deflate)
2.1 为什么必须解压
Design B 下 provider id 就是 openai,于是 Codex 的 enable_request_compression 必然触发:HTTP 回退路径(WS 失败或被禁用)会发送 zstd 压缩的 JSON 请求体。旧实现里 req.json() 直接抛错 → 400。Phase 1 的目标就是让代理先解压再解析。
2.2 实现:src/server/request-decompress.ts
按 010_phase1_request_decompression.md,新增模块提供两个核心能力:
export class UnsupportedContentEncodingError extends Error {
constructor(readonly encoding: string) {
super(`Unsupported content-encoding: ${encoding}`);
}
}
export function decodeRequestBody(raw: Uint8Array, contentEncoding: string | null): Uint8Array {
const encoding = (contentEncoding ?? "").trim().toLowerCase();
if (encoding === "" || encoding === "identity") return raw;
if (encoding === "zstd") return Bun.zstdDecompressSync(raw);
if (encoding === "gzip" || encoding === "x-gzip") return Bun.gunzipSync(raw);
if (encoding === "deflate") return Bun.inflateSync(raw);
throw new UnsupportedContentEncodingError(encoding); // incl. multi-codings like "zstd, gzip"
}
export async function readJsonRequestBody(req: Request): Promise<unknown> {
const encoding = req.headers.get("content-encoding");
if (!encoding) return await req.json(); // fast path, no buffering change
const decoded = decodeRequestBody(new Uint8Array(await req.arrayBuffer()), encoding);
return JSON.parse(new TextDecoder().decode(decoded));
}
关键点:
- 快速路径:无
content-encoding头时直接req.json(),不引入缓冲变化,零性能回归。 - 编码归一:
trim().toLowerCase()容忍大小写与空白差异;identity与空值等同直通。 - 基于 Bun 内置能力:
Bun.zstdDecompressSync/Bun.gunzipSync/Bun.inflateSync,无第三方依赖。 - 明确失败语义:遇到
zstd, gzip这类多重编码或未知编码抛出UnsupportedContentEncodingError。
2.3 服务端接入与错误码细化
handleResponses 的 body 解析从直接 req.json() 切换为 readJsonRequestBody(req),并把"不支持的编码"与"JSON 非法"区分开:
let body: unknown;
try {
body = await readJsonRequestBody(req);
} catch (err) {
if (err instanceof UnsupportedContentEncodingError) {
return formatErrorResponse(415, "invalid_request_error", err.message);
}
return formatErrorResponse(400, "invalid_request_error", "Invalid JSON body");
}
- 未知编码返回 415 Unsupported Media Type(语义更准确),损坏的 JSON 仍返回 400。
- 作用域严格限定为
/v1/responsesPOST:Codex 只压缩这一条路径;管理 API 与 WS 帧不受影响。
2.4 测试与验收
单元测试覆盖:identity/缺省直通、zstd 往返(Bun.zstdCompressSync)、gzip 往返、deflate 往返、未知编码抛错、损坏 zstd 抛错;集成式测试构造带压缩体与头的 Request,断言 readJsonRequestBody 正确还原对象。验收标准为 bun test tests/request-decompress.test.ts 与全量 bun test 全绿,并复现"zstd body 原先 400 → 现在可解析"。
三、Phase 2:codex-inject 的 Design B 重写
3.1 模式分流(核心决策)
020_phase2_inject_rewrite.md 引入双模式分流,这是理解整个改造的关键:
- Loopback(默认,Design B):只注入 marker 注释 + 根级
openai_base_url。线程保持openaitag,历史无需前向重映射。 - Non-loopback(远程绑定):
shouldInjectApiAuthHeader为 true 时,必须沿用旧式[model_providers.opencodex]表注入,因为内置 provider 无法携带x-opencodex-api-key环境头。旧路径保留前向历史同步,Design B 路径不做。
3.2 TOML 注入工具集(src/codex/inject/config-toml.ts)
当前仓库 config-toml.ts 实现了计划中的全部工具:
buildOpenaiBaseUrlLine(port, hostname?):生成openai_base_url = "http://<host>:<port>/v1",hostname 经providerBaseHost归一。setRootOpenaiBaseUrl(content, port, hostname):在首个表头之前插入OCX_SECTION_MARKER注释 + 该行(与既有setRootModelProvider的插入纪律一致)。三种分支:- 已有非 marker 所属的用户自建
openai_base_url→ 保留不注入重复行(journal 仍快照原始值以备恢复),消息提示 "kept user openai_base_url"; - 已有 marker 所属行 → 覆盖重写(幂等重注入);
- 无 → 正常插入。
- 已有非 marker 所属的用户自建
stripInjectedOpenaiBaseUrl(content):只移除 marker 行及其紧邻的根级openai_base_url(仅限 marker 相邻行,用户自己的覆盖在别处则幸存),并容忍 marker 孤儿行。- 配套的
contextCompatibleBaseLine与setRootOpenaiBaseUrlForTarget(含 realtime sideband 覆盖),说明该机制已扩展为可复用目标(含 service/orchestration.ts、server/management-api.ts 等调用点)。
3.3 injectCodexConfig 的完整保留清单与分支
injectCodexConfig 保留既有职责:journal 写入、旧式清理(removeOcxSection、removeProfileSection、stripExistingModelProvider——现在移除旧根级 model_provider 成为升级路径)、上下文窗口裁剪、service-tier 归一、fast_mode、catalog。分支逻辑:
shouldInjectApiAuthHeader(config)为真 → 旧式 body 原样(根 model_provider + provider 表 + 前向历史同步);- 否则(Design B)→
setRootOpenaiBaseUrl,不调用setRootModelProvider,不追加 provider 表,历史交由 Phase 3 迁移。消息文案同步更新为 "Codex built-in openai provider now points at the proxy; threads keep their native provider tag."。
buildProfileFile(回退用的 opencodex.config.toml)同样分型:Design-B 变体为根 openai_base_url + 可选 catalog + [features] fast_mode,去掉 model_provider 与表;legacy 变体仅用于非 loopback。stripOpencodexConfig 额外执行 stripInjectedOpenaiBaseUrl;hasOpencodexRouting 在存在 marker 所属 base_url 行时也返回 true,保证 removeCodexConfig 报告正确。restoreNativeCodex 流程不变(journal → 回退 strip → catalog → syncCodexHistoryProvider("openai"),迁移完成后该调用是廉价 no-op,同时兜底被打断的旧状态)。
3.4 审查修复清单(2026-07-06 reviewer verdict)
- blocker 1 — stripRootRoutedModel 门控:
stripOpencodexConfig在"旧式根 model_provider == opencodex"或存在 marker 所属openai_base_url(hadInjectedBaseUrl)时触发 routed-model 剥离,否则 TUI 持久化的根model = "provider/slug"会在回退恢复后残留,导致原生 codex 对真实 OpenAI 失效。 - blocker 2 — shutdown-launcher 断言更新:
tests/shutdown-launcher.test.ts:96原断言注入配置含model_providers.opencodex,改为断言 Design B 形态(marker +openai_base_url);:112的not.toContain("opencodex")恢复断言依赖回退 strip 也移除 marker 行。 - blocker 3 — buildProfileFile 判别键:Design-B-vs-legacy 变体以
includeApiAuthHeader为键(与injectCodexConfig从shouldInjectApiAuthHeader(config)派生的是同一布尔),不从 hostname 重新推导,保证tests/codex-inject.test.ts:135-139的 legacy 形状用例保持绿色。 - adv 4 — profile 文件语义:
opencodex.config.toml是独立参考/回退文件(并非 codex--profile消费),其 Design-B 变体文档化根openai_base_url;过时的 "--profile opencodex" 头注释改写为手工使用说明。 - adv 6 — journal 哈希:首次在旧会话 journal 上做 Design-B 注入会产生一次预期哈希不匹配 → 走回退 strip 路径,可接受;不回刷哈希(journal 始终以 ocx 前原始快照为真相源)。
- adv 7 — 解压上限:
decodeRequestBody将解压后大小限制在 64 MiB,超限抛错。
3.5 测试矩阵
新增用例:Design B 注入输出 marker + 根 base_url 且在首个表头之前、无 provider 表、无根 model_provider;端口变更后重注入幂等;升级路径(旧表 + 根 model_provider=opencodex → 注入后仅剩 Design B 形态);用户自建 base_url 保留不重复注入;strip 仅移除 marker 行、用户行幸存;非 loopback 仍输出旧表 + env_http_headers(回归);profile 文件 Design B 形态。验收还要求 rg 证明:loopback 配置下没有任何调用路径再注入 opencodex 表。
四、Phase 3:一次性历史迁移(opencodex → openai)
4.1 为什么要迁移
Design B 之后,历史遗留的 opencodex tag 线程对 Codex 不可见(Codex 按当前 provider id = openai 列列表)。因此需要一次性把残留线程 eject 为 openai,此后 Design B 路径不再运行前向同步。
4.2 实现:src/codex/history-provider.ts
030_phase3_history_migration.md 定义迁移入口为薄别名——恢复机制本身已做"manifest 恢复 + eject 剩余 + 清 backup":
export function migrateHistoryToOpenai(stateDbPath = STATE_DB_PATH, backupPath = HISTORY_BACKUP_PATH): CodexHistorySyncResult {
return syncCodexHistoryProvider("openai", stateDbPath, backupPath);
}
injectCodexConfig 的 Design B 分支在 config?.syncResumeHistory !== false 时调用 migrateHistoryToOpenai(),消息分三档:rows/ejected > 0 → "N opencodex-tagged thread(s) migrated back to openai (one-time).";失败 → 提示关闭 Codex 应用后重跑;0 → "Codex resume history: no migration needed."。legacy 分支保持前向同步。测试覆盖:预置 opencodex 行 + backup manifest → 全部转 openai、backup 清空;二次运行幂等(0 行)。
4.3 文档与 SoT 同步
README 的 How-it-works 补充 openai_base_url 覆盖说明与 Design B 下历史不被触碰的注解;~/.codex/skills/opencodex/SKILL.md 属用户级技能目录,标注为仓库循环外、在 D 总结中提示;260702_codex-history-sync-hardening 追加指向本单元的收尾注释。
五、Phase 4:升级路径加固与后台迁移守护
5.1 问题:升级途中 DB 被锁
大多数用户是从 re-tag 安装升级而来,一次性 opencodex→openai 迁移必须在首次尝试遇锁(state_5.sqlite 被 Codex 应用占用,ocx start 时是常见情形)时也能成功。旧行为只打印警告,线程保持不可见直到手动重跑。
5.2 审查修复:把"耐心"放在守护进程而非注入路径
040_phase4_upgrade_hardening.md 的关键决策是职责分离:
- blocker 1 — 禁止在 inject 内做同步 sleepSync 重试:
injectCodexConfig可从存活守护进程触达(/api/sync→ codex-sync.ts:59 → server.ts:1724),3×750ms sleepSync + 5s busy_timeout 会阻塞每个请求。Inject 维持现有syncCodexHistoryProvider("openai")(2 次尝试 / 500ms)。耐心只存在于守护进程:60s unref tick,每 tick{attempts: 1}。 - blocker 2 — 待迁移计数谓词与 eject 完全一致:
WHERE model_provider = 'opencodex' AND trim(coalesce(first_user_message, '')) != ''(无 source 过滤、无 RESUMABLE_SOURCES——eject 自己处理 exec→cli)。计数探测以{ readonly: true }打开 sqlite(先例 kiro-credentials.ts:186),busy_timeout 仅 100ms,锁库不会拖住守护 tick 或 doctor。 - 守护只由 cli.ts handleStart 启动(绝不从 inject 启动,否则
/api/sync重跑 inject 会双开循环),句柄随 token guardian 一起在syncCleanup停掉。 - 导出
shouldInjectApiAuthHeader(原私有,codex-inject.ts:53)供门控使用;migrateHistoryToOpenai保持薄别名,但新增attempts选项供守护 tick 使用。
5.3 新增模块
src/codex/history-migration-guardian.ts 的 startHistoryMigrationGuardian(deps?):unref'd setTimeout 循环(默认 60s,最多 60 tick),每 tick:计数 pending;0 pending + 0 backup → 静默停止;否则以 {attempts: 1} 迁移(守护不 sleepSync,tick 节奏即重试);成功 → 记录迁移数并停止;达到上限 → 最终警告并停止。依赖可注入以便测试。countPendingOpencodexHistory 返回 { pendingRows, backupEntries, failed? } 只读计数,供守护与 doctor 使用。withHistoryRetry(fn, io) 增加 attempts(默认 2 = 现行为)与 delayMs 选项。
5.4 接线与 doctor
cli.ts handleStart:startTokenGuardian()之后,当处于 Design B(loopback)且syncResumeHistory !== false时启动迁移守护,syncCleanup中停止。doctor.ts:新增 "Codex history migration" 段落,显示 pending 行 / backup 条目 / 锁库状态,并给出关闭应用提示。- 失败迁移的消息文案提示"守护进程会在后台持续重试"。
测试:withHistoryRetry 尊重 attempts(失败 3 次、attempts=4 时第 4 次成功);countPendingOpencodexHistory 在 legacy fixture 上有计数、迁移后为 0;migrateHistoryToOpenai 清空 legacy 行 + backup;守护在 0 pending 立即停止、迁移失败时重试并在成功后记日志停止、尊重 maxTicks、stop() 可取消。验收:定向 + 全量 bun test 绿、tsc 干净。
六、Phase 5:Windows/Linux 稳态锁规避与 CRLF 换行保留
6.1 稳态问题:每次 start/stop 都写开 DB
Phase 5 解决了 Design B 引入后的稳态噪音:050_phase5_windows_linux_hardening.md 指出,即便没有任何待迁移内容,ocx start 与 ocx stop 的稳态路径仍会对 state_5.sqlite 做写打开。在 Windows 上 Codex 应用持有 DB(WAL、busy_timeout 5s),每次写开可能阻塞约 10.5s(2 次尝试 × 5s busy + 500ms 延迟),并弹出误导性的 "migration deferred / history could NOT be restored" 警告(实际 pending 为 0)。WAL 允许并发读,因此只读探测可穿过写阻塞。
验证事实(本周期复核):
migrateHistoryToOpenai()(history-provider.ts:539)在每次 loopback start/sync 时由injectCodexConfig(codex-inject.ts:396)调用,即使 pending=0、backup=0 也会走写openStateDb(busy_timeout 5000)。restoreNativeCodex()(codex-inject.ts:524)在每次ocx stop/shutdown 时调用syncCodexHistoryProvider("openai"),同样写开;失败时打印 "routed threads stay hidden",但 Design B 稳态下线程本就是 openai,这个警告是假的。countPendingOpencodexHistory(history-provider.ts:564)是只读探测(busy_timeout 100ms),谓词与ejectRemainingOpencodexHistory完全一致,并计数 backup manifest;{pendingRows:0, backupEntries:0, failed:undefined}即证明 openai 方向恢复是 no-op。- CRLF 复现:
setRootOpenaiBaseUrl在 CRLF 配置上插入\n结尾的 marker + key 行 → 产生混合 EOL 文件(解析仍可用,属于打磨而非正确性问题;codex-rs 的 toml crate 两种结尾都接受)。
6.2 审查修复:边界归一化 EOL,而非逐函数手术
- major 1 — EOL 归一放在边界:注入管线里每个变换都用硬
"\n"重建内容(stripExistingModelProvider、ensureFastModeFeature、removeOcxSection、setRootModelCatalogPath 等),setRootOpenaiBaseUrl的幂等重写(lines[i] = key)会让 CRLF 文件的某一行变 LF。采用方案:在边界统一处理——记录一次dominantEol(original),先归一为 LF 再走变换管线,落盘前转回记录的 EOL。removeCodexConfig同样包裹;仅在变换真正改变内容时做字节级 EOL 统一(无变化的快速路径在 LF 空间比较并跳过写盘)。legacy 分支(setRootModelProvider + provider 表)也因此免费获得 EOL 正确性。 - major 2 — 不静默改变导出恢复 API 语义:
syncCodexHistoryProvider("openai")保持默认写开行为;稳态门控为显式 opt-in:新opts.skipWhenProvablyNoop标志,仅由restoreNativeCodex在加载配置为 loopback(Design B,!shouldInjectApiAuthHeader(config))时传入。legacy 非 loopback 的 stop/restore 路径逐字节不变。TOCTOU 说明:Design B 下不再有任何写入方产生opencodex行,probe-then-skip 不会丢失并发行;legacy 模式从不设置该标志。migrateHistoryToOpenai内置该门控(按契约仅限 inject/guardian 调用),守护的 recount 保护(history-migration-guardian.ts:75-80)因 backupEntries>0 或探测失败仍会落到写尝试而保留。 - minor 3 — 措辞修复:DB 缺失时
countPendingOpencodexHistory仍计数 backup 条目,因此"全新重装 + 遗留 backup manifest"不会满足 noop 门控(新增对应测试)。
6.3 门控实现
/** True when a readonly probe PROVES the openai-direction restore would be a no-op. */
function openaiRestoreIsNoop(stateDbPath: string, backupPath: string): boolean {
const pending = countPendingOpencodexHistory(stateDbPath, backupPath);
return !pending.failed && pending.pendingRows === 0 && pending.backupEntries === 0;
}
migrateHistoryToOpenai:existsSync早退之后插入if (openaiRestoreIsNoop(...)) return { rows: 0, files: 0 };。syncCodexHistoryProvider(provider, stateDbPath?, backupPath?, opts?):仅在设置了标志、provider 为 "openai" 且 DB 存在时应用门控;默认行为(无标志)不变;opencodex(前向)方向永不门控。- 探测失败(
failed: true)落到既有写尝试——锁库且状态未知时保持现行为与警告。 - 缺失 DB:backup 条目仍被计数,遗留 manifest 的重装不跳过;
existsSync早退顺序保证缺失 DB 行为与今天一致。
injectCodexConfig 侧新增 dominantEol(content)(CRLF 序列数 ≥ 裸 LF 数且至少存在一个时判 CRLF)与 applyEol(content, eol)(先归一 \r\n→\n 再按目标 EOL 展开),写盘用 atomicWriteFile 前的最后一步转换,journal 记录最终字节。
6.4 D 关闭(2026-07-06)
Phase 5 关闭时交付:history-provider.ts 的只读门控(内置进 migrateHistoryToOpenai + skipWhenProvablyNoop opt-in);codex-inject.ts 的 dominantEol/applyEol 边界归一(inject 与 remove 管线都包裹);restoreNativeCodex 仅在 loopback 下传 noop-skip 标志。新增 9 个测试(稳态字节恒等、pending 穿透、重装 manifest 守卫、带标志的 sync 跳过/恢复、dominantEol/applyEol 单元、CRLF/LF 集成往返)。证据:全量 bun test 1473 通过 / 0 失败、bun x tsc --noEmit 干净、隐私扫描通过、git diff --check 干净;独立 gpt-5.5 审查 A-gate FAIL-with-fixes(全部吸收)、C-gate PASS。未采纳/死假设:逐函数 CRLF 手术是错误形态,边界归一一次解决所有变换(含首版计划划出范围的 legacy 分支);孤立 \r(旧 Mac 结尾)刻意不动(转换它们可能触碰 TOML 中本就非法的 CR 字节)。
七、整体验收与最终状态
整个工作单元以 999_closed.md 收尾:commit 22561a4 在注入/历史迁移提交之后最终确定 Design B openai_base_url 加固;2026-07-07 验证最新全量套件绿色,1555 通过 / 0 失败。完整链路为:
- 解压层(request-decompress.ts):zstd/gzip/deflate 解码,415/400 错误细分,64 MiB 上限;
- 注入层(config-toml.ts、inject.ts、remove.ts、restore.ts):marker + 根
openai_base_url注入/摘除,幂等、用户配置优先、loopback/非 loopback 双模分流; - 历史层(history-provider.ts、history-migration-guardian.ts):一次性 opencodex→openai 迁移、只读 noop 门控、后台守护重试、doctor 可视化;
- 跨平台层:边界 EOL 归一,Windows 稳态零写开、零假警告。
八、实践要点速查
| 场景 | 行为 |
|---|---|
| Loopback 默认注入 | marker + 根 openai_base_url = "http://<host>:<port>/v1",不写 provider 表、不改 model_provider |
| 非 loopback(远程绑定) | 保留 legacy 表 + env_http_headers(需携带 x-opencodex-api-key) |
| 用户自建 base_url | 保留,不重复注入;journal 仍快照原始值 |
| 重注入/端口变更 | 幂等改写 marker 所属行 |
| 卸载(restore) | 移除 marker 相邻行;用户行幸存;legacy 清理照旧 |
| 历史迁移 | 一次性 eject opencodex→openai;稳态下只读探测证明 no-op 则零写开 |
| 升级遇锁 | 守护进程 60s tick 后台重试(最多 60 次),inject 路径不阻塞 |
| CRLF 配置 | 边界归一,注入/摘除后文件 EOL 与原始一致,不产生混合换行 |
如需深入阅读,建议按序查看本工作单元 000_plan.md → 010/020/030/040/050 → 999_closed.md,再对照 src/codex/inject/config-toml.ts、src/codex/history-provider.ts、src/server/request-decompress.ts 与 tests/codex-inject.test.ts、tests/request-decompress.test.ts 等测试文件验证上述行为。
更多推荐
所有评论(0)