Skip to content

codex-cli 架构深度解析

包名: @openai/codex (v0.1.2504251709) 语言: TypeScript + TSX (React/Ink) 运行时: Node.js >= 22 (ESM) 构建: esbuild → 单一 ESM bundle 测试: Vitest (72 个测试文件)


一、目录结构

codex-cli/
├── bin/codex.js                          # CLI 可执行入口(ESM 引导脚本)
├── build.mjs                             # esbuild 构建脚本(prod/dev 双模式)
├── package.json                          # NPM 包配置 (dependencies + scripts)
├── tsconfig.json                         # TypeScript 配置(ESNext + strict + bundler)
├── vite.config.ts                        # Vitest 配置
├── require-shim.js                       # CJS require 兼容 shim
├── ignore-react-devtools-plugin.js       # esbuild 插件:过滤 react-devtools-core
├── Dockerfile                            # Docker 容器运行配置
├── .eslintrc.cjs                         # ESLint 配置

├── src/                                  # ========== 源代码 ==========
│   ├── cli.tsx                ★ [19KB]   # 主入口:meow 参数解析 + 多模式分发
│   ├── cli-singlepass.tsx       [583B]   # 全上下文模式入口
│   ├── app.tsx                  [2.9KB]  # React 根组件:Git 检查 + 路由
│   ├── approvals.ts           ★ [15KB]   # 审批策略引擎(三级安全评估)
│   ├── text-buffer.ts         ★ [28KB]   # 多行文本编辑器核心(Unicode-aware)
│   ├── format-command.ts        [1.9KB]  # 命令格式化展示
│   ├── parse-apply-patch.ts     [2.7KB]  # Patch 前缀常量定义
│   ├── typings.d.ts             [2.4KB]  # 全局类型声明
│   ├── shims-external.d.ts      [736B]   # 外部模块声明
│   │
│   ├── components/                       # ── UI 组件层 ──
│   │   ├── chat/                         # 核心聊天界面(12个组件)
│   │   │   ├── terminal-chat.tsx       ★ [25KB]  # 主聊天容器:AgentLoop 生命周期 + Overlay 系统
│   │   │   ├── terminal-chat-input.tsx ★ [29KB]  # 输入处理:斜杠命令 + 历史 + 文件补全
│   │   │   ├── terminal-chat-response-item.tsx  [8KB]  # 响应渲染:message/function_call/reasoning
│   │   │   ├── terminal-chat-command-review.tsx [9KB]  # 命令审批交互界面
│   │   │   ├── terminal-chat-input-thinking.tsx [3KB]  # 加载动画(滚动球 + 计时)
│   │   │   ├── terminal-chat-tool-call-command.tsx [4KB]  # 工具调用命令展示
│   │   │   ├── multiline-editor.tsx    [14KB]  # 多行编辑器组件(封装 TextBuffer)
│   │   │   ├── terminal-chat-completions.tsx [2KB]  # Tab 补全列表
│   │   │   ├── terminal-chat-past-rollout.tsx [2KB]  # 历史会话回放
│   │   │   ├── terminal-header.tsx      [3KB]  # 顶部状态栏
│   │   │   ├── terminal-message-history.tsx [3KB]  # 消息历史列表
│   │   │   ├── message-history.tsx      [3KB]  # 消息历史数据
│   │   │   └── use-message-grouping.ts  [255B] # 消息分组 hook
│   │   │
│   │   ├── approval-mode-overlay.tsx   [1KB]   # 审批模式切换覆盖层
│   │   ├── diff-overlay.tsx            [3KB]   # Git diff 查看覆盖层
│   │   ├── help-overlay.tsx            [3KB]   # 帮助信息覆盖层
│   │   ├── history-overlay.tsx         [7KB]   # 对话历史覆盖层
│   │   ├── model-overlay.tsx           [5KB]   # 模型/Provider 切换覆盖层
│   │   ├── typeahead-overlay.tsx       [5KB]   # 自动补全覆盖层
│   │   ├── singlepass-cli-app.tsx      [18KB]  # 单次执行模式 UI
│   │   │
│   │   ├── onboarding/                         # 新手引导
│   │   │   └── onboarding-approval-mode.tsx
│   │   ├── select-input/                       # 自定义选择组件
│   │   │   ├── select-input.tsx / indicator.tsx / item.tsx
│   │   └── vendor/                             # 第三方 vendor 组件
│   │       ├── ink-spinner.tsx / ink-text-input.tsx
│   │       ├── cli-spinners/index.js
│   │       └── ink-select/ (7 个文件)
│   │
│   ├── hooks/                            # ── React Hooks ──
│   │   ├── use-confirmation.ts  [2KB]    # 基于队列的确认对话框机制
│   │   └── use-terminal-size.ts [634B]   # 终端尺寸监听
│   │
│   └── utils/                            # ── 工具函数层 ──
│       ├── agent/                        # Agent 核心引擎
│       │   ├── agent-loop.ts  ★★ [62KB]  # 核心!Agent 循环 + 流式处理 + 重试 + 系统提示
│       │   ├── handle-exec-command.ts [12KB]  # 命令执行编排(审批→沙盒→执行)
│       │   ├── apply-patch.ts   [22KB]   # Patch 解析与应用引擎
│       │   ├── exec.ts          [4KB]    # 执行器:apply_patch vs shell 分发
│       │   ├── parse-apply-patch.ts [3KB] # Patch 操作类型解析
│       │   ├── platform-commands.ts [2KB] # 跨平台命令适配
│       │   ├── review.ts       [382B]    # ReviewDecision 枚举定义
│       │   └── sandbox/                  # 沙盒执行层
│       │       ├── interface.ts [663B]   # SandboxType 枚举 + ExecInput/ExecResult 类型
│       │       ├── raw-exec.ts  [7KB]    # 底层进程 spawn(进程组管理 + 信号处理)
│       │       ├── macos-seatbelt.ts [5KB] # macOS Seatbelt 沙盒策略
│       │       └── create-truncating-collector.ts [2KB] # 输出截断收集器
│       │
│       ├── config.ts          ★ [19KB]   # 配置管理(JSON/YAML + 多级优先级)
│       ├── responses.ts       ★ [21KB]   # 双 API 协议适配(Responses ↔ Chat Completions)
│       ├── providers.ts         [1KB]    # 8 种 LLM Provider 注册表
│       ├── model-utils.ts       [7KB]    # 模型列表获取 + 上下文窗口计算
│       ├── model-info.ts        [5KB]    # 模型元数据注册表
│       ├── parsers.ts           [3KB]    # 工具调用参数解析
│       ├── session.ts           [1.5KB]  # Session ID + 版本管理
│       ├── compact-summary.ts   [3KB]    # 对话压缩摘要生成
│       ├── approximate-tokens-used.ts [2KB] # Token 用量估算(4字符≈1token)
│       ├── auto-approval-mode.ts [217B]  # 审批模式枚举
│       ├── slash-commands.ts    [1KB]    # 斜杠命令定义
│       ├── input-utils.ts       [1KB]    # 用户输入构建
│       ├── check-in-git.ts      [1KB]    # Git 仓库检测
│       ├── check-updates.ts     [4KB]    # CLI 更新检测
│       ├── get-diff.ts          [4KB]    # Git diff 获取
│       ├── bug-report.ts        [2KB]    # Bug report URL 生成
│       ├── file-system-suggestions.ts [1KB] # 文件路径补全
│       ├── extract-applied-patches.ts [1KB] # 已应用 patch 提取
│       ├── package-manager-detector.ts [2KB] # 包管理器检测
│       ├── short-path.ts        [777B]   # 路径缩短
│       ├── terminal.ts          [3KB]    # 终端控制(清屏 + 退出清理)
│       │
│       ├── logger/                       # 日志系统
│       │   └── log.ts           [4KB]    # AsyncLogger(DEBUG=1 启用)
│       ├── storage/                      # 持久化存储
│       │   ├── command-history.ts [3KB]  # 命令历史(~/.codex/history.json)
│       │   └── save-rollout.ts  [1KB]    # 会话回放保存(~/.codex/sessions/)
│       └── singlepass/                   # 全上下文单次执行模式
│           ├── context.ts       [2KB]    # TaskContext 接口 + XML 渲染
│           ├── context_files.ts [9KB]    # LRU 文件缓存 + 忽略规则
│           ├── context_limit.ts [6KB]    # 目录大小分析
│           ├── code_diff.ts     [6KB]    # Diff 生成与着色
│           └── file_ops.ts      [1KB]    # Zod Schema(文件操作定义)

├── tests/                                # ========== 测试 ==========
│   ├── 72 个测试文件 (.test.ts / .test.tsx)
│   ├── ui-test-helpers.tsx               # UI 测试辅助
│   ├── __fixtures__/                     # 测试数据
│   └── __snapshots__/                    # 快照

├── scripts/                              # Docker 容器脚本
│   ├── build_container.sh
│   ├── init_firewall.sh
│   └── run_in_container.sh

└── examples/                             # 使用示例
    ├── README.md / prompting_guide.md
    ├── build-codex-demo/
    ├── camerascii/
    ├── impossible-pong/
    └── prompt-analyzer/

文件统计

指标数量
源文件 (src/)87 个
测试文件 (tests/)72 个
最大文件agent-loop.ts (62KB, ~1522行)
总估算代码量~15,000-18,000 行

二、模块功能与关系

2.1 七大核心模块

#模块位置核心职责
M1CLI 入口cli.tsx + app.tsx参数解析、模式分发、React 渲染启动
M2UI 组件层components/ + hooks/终端交互界面(React/Ink 组件树)
M3Agent 引擎utils/agent/agent-loop.ts核心循环:请求→流消费→工具调用→响应
M4命令执行层utils/agent/handle-exec-command.ts + sandbox/审批→沙盒选择→进程执行
M5审批策略approvals.ts三级安全评估(suggest/auto-edit/full-auto)
M6API 适配层utils/responses.ts + utils/providers.ts双协议适配 + 8 种 Provider 支持
M7配置与存储utils/config.ts + utils/storage/多格式配置加载 + 会话持久化

2.2 模块关系 Mermaid 图

2.3 数据流:一次完整交互

2.4 各模块详细说明

M1 - CLI 入口 (cli.tsx)

核心职责:解析 CLI 参数,根据不同模式分发到对应的执行路径。

用户输入 → meow 解析参数

    ┌─────────┼──────────┬──────────┬─────────┐
    │         │          │          │         │
  --help   --config   --view    --quiet   默认交互
  (帮助)   (编辑器)  (回放)   (静默模式)  (React/Ink)
    │         │          │          │         │
  exit    exit      exit      runQuiet   render(<App>)
                              Mode()

支持的命令行参数

参数简写类型说明
--model-mstring指定模型 (默认 o4-mini)
--provider-pstring指定 Provider (默认 openai)
--image-istring[]附加图片路径
--quiet-qboolean静默非交互模式
--approval-mode-astring审批策略 (suggest/auto-edit/full-auto)
--full-autoboolean全自动模式
--auto-editboolean自动编辑模式
--full-context-fboolean全上下文单次执行模式
--flex-modebooleanFlex 处理模式 (仅 o3/o4-mini)
--disable-response-storageboolean禁用服务端响应存储
--writable-root-wstring[]沙盒可写目录
--view-vstring查看历史回放
--notifyboolean启用桌面通知
--full-stdoutboolean不截断命令输出

M2 - UI 组件层

采用 React + Ink 架构,在终端中渲染 React 组件树:

<App>
  ├── <TerminalChatPastRollout>          # --view 模式
  ├── <ConfirmInput>                      # 非 Git 仓库警告
  └── <TerminalChat>                      # 正常交互模式
       ├── <TerminalMessageHistory>       # 消息历史展示
       │    └── <TerminalChatResponseItem> × N
       │         ├── <Markdown>           # marked + marked-terminal 渲染
       │         ├── <TerminalChatResponseToolCall>
       │         └── <TerminalChatResponseToolCallOutput>

       ├── <TerminalChatInput>            # 输入区域
       │    ├── <TerminalChatInputThinking>  # 加载状态(滚动球动画)
       │    ├── <MultilineTextEditor>        # 多行编辑器
       │    │    └── TextBuffer              # 底层缓冲区
       │    ├── <TextCompletions>            # Tab 补全列表
       │    └── <TerminalChatCommandReview>  # 命令审批交互

       └── Overlay 系统 (互斥)
            ├── <HistoryOverlay>     /history
            ├── <ModelOverlay>       /model
            ├── <ApprovalModeOverlay> /approval
            ├── <HelpOverlay>        /help
            └── <DiffOverlay>        /diff

斜杠命令系统

命令功能
/help显示帮助信息
/model切换模型/Provider
/approval切换审批模式
/history查看对话历史
/diff查看 Git diff
/compact压缩上下文(生成摘要)
/clear清除当前会话
/clearhistory清除命令历史
/bug生成 Bug report URL

M3 - Agent 引擎 (agent-loop.ts)

这是整个项目的核心,1522 行代码实现了完整的 AI Agent 循环。

核心类 AgentLoop 的关键字段

字段类型作用
oaiOpenAIOpenAI SDK 客户端实例
generationnumber防止旧 run 的过时事件泄漏到新 run
canceledboolean用户取消标志
transcriptResponseInputItem[]本地会话记录(ZDR 模式使用)
pendingAbortsSet<string>被取消但未应答的 function call ID
currentStreamunknown当前活跃的 SSE 流引用
execAbortControllerAbortController用于中止进行中的工具调用
hardAbortAbortController终止信号(terminate() 触发)

Agent Loop 状态机

                        ┌─────────────────────┐
                        │   IDLE              │
                        │   (等待用户输入)     │
                        └─────────┬───────────┘
                                  │ run(input)

                        ┌─────────────────────┐
                 ┌──────│   REQUESTING        │──────┐
                 │      │   (发送 API 请求)    │      │
                 │      └─────────┬───────────┘      │
                 │                │ stream 开始       │ 超时/错误
                 │                ▼                   │ → 重试(max 8次)
                 │      ┌─────────────────────┐      │
                 │      │   STREAMING         │◄─────┘
                 │      │   (消费流式事件)     │
                 │      └─────────┬───────────┘
                 │                │
           cancel()        ┌─────┴─────┐
                 │         │           │
                 │    文本输出     function_call
                 │         │           │
                 │         ▼           ▼
                 │     stageItem   handleFunctionCall()
                 │         │           │
                 │         │     handleExecCommand()
                 │         │           │
                 │         │     ┌─────┴─────┐
                 │         │     │           │
                 │         │  canAutoApprove  askUser
                 │         │     │           │
                 │         │     ▼           ▼
                 │         │   exec()    用户决策
                 │         │     │           │
                 │         │     ▼           │
                 │         │  function_call_output
                 │         │     │
                 │         │     ▼
                 │         │  turnInput = [output]
                 │         │     │
                 │         └──┬──┘
                 │            │
                 │            ▼
                 │  turnInput.length > 0?
                 │     ├── Yes → 回到 REQUESTING
                 │     └── No  ↓
                 │             ▼
                 └────►┌─────────────────────┐
                       │   FLUSH / DONE      │
                       │   (清理 + onLoading) │
                       └─────────────────────┘

M4 - 命令执行层

handleExecCommand()

    ├── 1. 检查 alwaysApprovedCommands 缓存 → 直接执行

    ├── 2. canAutoApprove() 安全评估
    │       ├── "auto-approve" → 执行 (可能在沙盒中)
    │       ├── "ask-user"     → askUserPermission()
    │       └── "reject"       → 返回 "aborted"

    ├── 3. execCommand()
    │       ├── apply_patch → execApplyPatch()
    │       │                  └── Parser.parse() → apply_changes()
    │       └── shell 命令 → exec()
    │                          └── getSandbox()
    │                               ├── macOS → MACOS_SEATBELT
    │                               │           └── execWithSeatbelt()
    │                               │                └── sandbox-exec -p <policy> -- <cmd>
    │                               └── 其他 → NONE
    │                                           └── raw-exec.exec()
    │                                                └── spawn(prog, args, {detached:true})

    └── 4. 沙盒执行失败 + fullAutoErrorMode=ASK_USER
            → 再次询问用户 → 非沙盒重试

M5 - 审批策略 (approvals.ts)

三级审批策略的安全评估逻辑:

策略安全命令apply_patch其他命令
suggest✅ 自动 (无沙盒)❌ 询问用户❌ 询问用户
auto-edit✅ 自动 (无沙盒)✅ 自动 (可写路径内)❌ 询问用户
full-auto✅ 自动 (无沙盒)✅ 自动 (沙盒)✅ 自动 (沙盒)

安全命令白名单 (isSafeCommand()):

导航: cd, pwd
搜索: ls, find(排除-exec/-delete), grep, rg, which
读取: cat, head, tail, wc, sed -n
版本: git status/branch/log/diff/show
其他: echo, true, cargo check

M6 - API 适配层

                          ┌────────────────────┐
                          │    AgentLoop       │
                          └────────┬───────────┘

                     ┌─────────────┼─────────────┐
                     │                           │
              provider=openai              provider=其他
                     │                           │
                     ▼                           ▼
           oai.responses.create()    responsesCreateViaChatCompletions()
                     │                           │
                     │                  ┌────────┴────────┐
                     │                  │ Chat Completions │
                     │                  │    → 转换为       │
                     │                  │ Responses 事件流   │
                     │                  └─────────────────┘
                     │                           │
                     └─────────┬─────────────────┘

                    AsyncIterable<ResponseEvent>

                    统一的事件消费逻辑

支持的 8 种 Provider

ProviderBase URLAPI Key 环境变量
OpenAIapi.openai.com/v1OPENAI_API_KEY
OpenRouteropenrouter.ai/api/v1OPENROUTER_API_KEY
Geminigenerativelanguage.googleapis.com/v1beta/openaiGEMINI_API_KEY
Ollamalocalhost:11434/v1OLLAMA_API_KEY
Mistralapi.mistral.ai/v1MISTRAL_API_KEY
DeepSeekapi.deepseek.comDEEPSEEK_API_KEY
xAIapi.x.ai/v1XAI_API_KEY
Groqapi.groq.com/openai/v1GROQ_API_KEY

M7 - 配置与存储

配置加载优先级(高→低):
  CLI flags > config.json/yaml > 环境变量 > .env 文件 > ~/.codex.env > 默认值

配置文件路径:
  ~/.codex/config.json (或 .yaml / .yml)
  ~/.codex/instructions.md
  <project>/codex.md (或 .codex.md / CODEX.md)

存储位置:
  ~/.codex/history.json          # 命令历史
  ~/.codex/sessions/rollout-*.json  # 会话回放

三、如何使用

3.1 安装与运行

bash
# 安装
npm install -g @openai/codex

# 设置 API Key
export OPENAI_API_KEY="sk-..."

# 交互模式(默认)
codex

# 带提示词启动
codex "Write a Python script that sorts a list"

# 静默模式(CI/脚本)
codex -q "fix all TypeScript errors"

# 全自动模式
codex --full-auto "add unit tests for utils.ts"

# 指定模型和 Provider
codex -m gpt-4.1 -p openai "explain this codebase"

# 使用 Ollama 本地模型
codex -p ollama -m llama3 "refactor this function"

# 全上下文模式(一次性加载整个仓库)
codex -f "restructure the project layout"

# 查看历史会话
codex --view ~/.codex/sessions/rollout-2025-04-25-abc123.json

# 生成 shell 补全脚本
codex completion bash >> ~/.bashrc

3.2 配置文件示例

~/.codex/config.yaml

yaml
model: o4-mini
provider: openai
approvalMode: suggest
disableResponseStorage: false
reasoningEffort: high
notify: false

history:
  maxSize: 1000
  saveHistory: true
  sensitivePatterns: []

providers:
  openai:
    name: OpenAI
    baseURL: https://api.openai.com/v1
    envKey: OPENAI_API_KEY

~/.codex/instructions.md(自定义指令):

markdown
You are working on a TypeScript project that uses pnpm.
Always use `pnpm` instead of `npm` for package management.
Follow the existing code style with 2-space indentation.

<project>/codex.md(项目级文档,自动发现):

markdown
# Project Context
This is a React Native app using Expo.
The main entry is `app/index.tsx`.
Run tests with `pnpm test`.

3.3 交互模式中的操作

操作按键/命令
发送消息Enter
换行Shift+Enter
历史导航 /
文件补全Tab
中断生成Esc × 2 (快速双击)
退出Ctrl+C 或输入 exit / q
帮助/help
切换模型/model
切换审批/approval
查看差异/diff
压缩上下文/compact
清除会话/clear

3.4 审批模式详解

当模型请求执行命令时,不同审批模式下的行为:

suggest 模式(默认)

codex  fix the build error

command
$ npm run build

  (y)es  (n)o continue  (n)o abort  (e)xplain  (a)lways approve
> █

auto-edit 模式:文件编辑自动执行,shell 命令仍需确认。

full-auto 模式:所有操作在沙盒中自动执行,仅在失败时询问。

3.5 从源码构建

bash
# 克隆仓库
git clone https://github.com/openai/codex
cd codex/codex-cli

# 安装依赖
pnpm install

# 生产构建
pnpm run build          # → dist/cli.js (minified)

# 开发构建 + 运行
pnpm run build:dev      # → dist/cli-dev.js (sourcemap)

# 运行测试
pnpm test

# 类型检查
pnpm run typecheck

# 代码规范
pnpm run lint
pnpm run format

四、关键函数代码解析

4.1 AgentLoop.run() — Agent 核心循环

文件: src/utils/agent/agent-loop.ts (L436-1431, ~1000行)

这是整个 Codex CLI 最核心的函数,实现了完整的 Agent 循环。

typescript
// src/utils/agent/agent-loop.ts

public async run(
  input: Array<ResponseInputItem>,
  previousResponseId: string = "",
): Promise<void> {
  try {
    // ① 终止检查 + generation 递增(防止旧事件泄漏)
    if (this.terminated) throw new Error("AgentLoop has been terminated");
    const thisGeneration = ++this.generation;
    this.canceled = false;

    // ② 处理上次取消遗留的 pendingAborts
    //    为每个未应答的 function_call 生成虚拟的 "aborted" 输出
    const abortOutputs: Array<ResponseInputItem> = [];
    if (this.pendingAborts.size > 0) {
      for (const id of this.pendingAborts) {
        abortOutputs.push({
          type: "function_call_output",
          call_id: id,
          output: JSON.stringify({ output: "aborted", metadata: { exit_code: 1 } }),
        });
      }
      this.pendingAborts.clear();
    }

    // ③ 构建本次请求的 input
    //    - disableResponseStorage=true: 发送完整 transcript(本地维护的会话历史)
    //    - disableResponseStorage=false: 仅发送增量,依赖 previous_response_id
    let turnInput = this.disableResponseStorage
      ? [...this.transcript, ...abortOutputs].map(stripInternalFields)
      : [...abortOutputs, ...input].map(stripInternalFields);

    // ④ 主循环:turnInput 不为空就一直请求
    while (turnInput.length > 0) {
      if (this.canceled) return;

      // ⑤ 发送请求到 OpenAI(支持最多 8 次重试)
      const responseCall = provider === "openai"
        ? (params) => this.oai.responses.create(params)      // Responses API
        : (params) => responsesCreateViaChatCompletions(...);  // Chat Completions 兼容

      stream = await responseCall({
        model: this.model,
        instructions: mergedInstructions,  // 系统提示 prefix + 用户 instructions
        input: turnInput,
        stream: true,
        tools: [shellTool],                // 唯一的工具:shell
        tool_choice: "auto",
        reasoning,                         // o3/o4-mini 支持 summary: "auto"
        ...(this.disableResponseStorage ? { store: false } : {
          store: true, previous_response_id: lastResponseId
        }),
      });

      // ⑥ 消费流式事件
      for await (const event of stream) {
        if (event.type === "response.output_item.done") {
          if (item.type === "function_call") {
            this.pendingAborts.add(callId);  // 追踪待处理的工具调用
          } else {
            stageItem(item);  // 3ms 延迟交付 UI
          }
        }
        if (event.type === "response.completed") {
          // ⑦ 处理工具调用 → 生成新的 turnInput
          newTurnInput = await this.processEventsWithoutStreaming(
            event.response.output, stageItem
          );
        }
      }

      turnInput = newTurnInput;  // 有新的工具输出 → 继续循环
    }
    // ⑧ 循环结束,flush 暂存项
  } catch (err) {
    // ⑨ 错误处理:网络错误、速率限制、model_not_found 等
  }
}

关键设计要点

  1. generation 计数器:每次 run() 递增,cancel() 也递增。stageItem() 在交付前检查 generation 是否匹配,过时事件直接丢弃。

  2. pendingAborts 机制:如果用户在 function_call 返回后、执行完成前取消了操作,call_id 会被记录。下次 run() 时会发送虚拟的 "aborted" 输出,满足 OpenAI API 的"每个 function_call 必须有对应 output"的约定。

  3. 双模式存储

    • store: true + previous_response_id:服务端保留上下文,每次只发增量
    • store: false + 完整 transcript:本地维护完整历史,每次发全量

4.2 canAutoApprove() — 安全审批评估

文件: src/approvals.ts (L72-167)

typescript
// src/approvals.ts

export function canAutoApprove(
  command: ReadonlyArray<string>,
  workdir: string | undefined,
  policy: ApprovalPolicy,
  writableRoots: ReadonlyArray<string>,
): SafetyAssessment {

  // ① apply_patch 命令特殊处理
  if (command[0] === "apply_patch") {
    return canAutoApproveApplyPatch(command[1], workdir, writableRoots, policy);
    //  suggest → ask-user (总是询问)
    //  auto-edit → 检查路径是否在可写范围内 → auto-approve 或 ask-user
    //  full-auto → auto-approve (沙盒执行)
  }

  // ② 检查已知安全命令白名单
  const isSafe = isSafeCommand(command);
  if (isSafe != null) {
    return { type: "auto-approve", runInSandbox: false, ...isSafe };
    //  ls, cat, grep, git status 等 → 直接批准,不需要沙盒
  }

  // ③ bash -lc "..." 包装的命令:需要解析内部脚本
  if (command[0] === "bash" && command[1] === "-lc") {
    // 尝试解析为 apply_patch
    const applyPatchArg = tryParseApplyPatch(command[2]);
    if (applyPatchArg != null) {
      return canAutoApproveApplyPatch(applyPatchArg, workdir, writableRoots, policy);
    }

    // 使用 shell-quote 解析复合表达式
    const bashCmd = parse(command[2]);
    //  "ls && cat file.txt" → ['ls', {op:'&&'}, 'cat', 'file.txt']

    // 检查整个 shell 表达式是否全部由安全命令组成
    const shellSafe = isEntireShellExpressionSafe(bashCmd);
    if (shellSafe != null) {
      return { type: "auto-approve", runInSandbox: false, ...shellSafe };
    }
  }

  // ④ 兜底:full-auto 模式在沙盒中执行,其他模式询问用户
  return policy === "full-auto"
    ? { type: "auto-approve", runInSandbox: true, reason: "Full auto mode" }
    : { type: "ask-user" };
}

评估流程图

command 输入

    ├── apply_patch? ──► canAutoApproveApplyPatch()
    │                     ├── suggest → ask-user
    │                     ├── auto-edit → 检查可写路径
    │                     └── full-auto → auto-approve(sandbox)

    ├── isSafeCommand()? ──► auto-approve(无沙盒)

    ├── bash -lc "..."?
    │   ├── 内含 apply_patch? ──► canAutoApproveApplyPatch()
    │   ├── shell-quote 解析成功?
    │   │   └── isEntireShellExpressionSafe()? ──► auto-approve(无沙盒)
    │   └── 解析失败?
    │       ├── full-auto → auto-approve(沙盒)
    │       └── 其他 → ask-user

    └── 兜底
        ├── full-auto → auto-approve(沙盒)
        └── 其他 → ask-user

4.3 handleExecCommand() — 命令执行编排

文件: src/utils/agent/handle-exec-command.ts (L74-189)

typescript
// src/utils/agent/handle-exec-command.ts

export async function handleExecCommand(
  args: ExecInput,
  config: AppConfig,
  policy: ApprovalPolicy,
  additionalWritableRoots: ReadonlyArray<string>,
  getCommandConfirmation: (...) => Promise<CommandConfirmation>,
  abortSignal?: AbortSignal,
): Promise<HandleExecCommandResult> {
  const { cmd: command, workdir } = args;
  const key = deriveCommandKey(command);
  //  deriveCommandKey(): "apply_patch" 命令 → key="apply_patch"
  //                      "bash -lc 'npm test'" → key="npm"
  //                      其他 → key=程序名

  // ① 会话级"总是批准"缓存 → 跳过一切检查
  if (alwaysApprovedCommands.has(key)) {
    return execCommand(args, undefined, false, ...).then(convertSummaryToResult);
  }

  // ② 安全评估
  const safety = canAutoApprove(command, workdir, policy, [process.cwd()]);

  switch (safety.type) {
    case "ask-user": {
      // 调用 UI 层的确认回调
      const review = await askUserPermission(args, safety.applyPatch, getCommandConfirmation);
      // review 为 null → 用户批准; 非 null → 用户拒绝
      if (review != null) return review;
      runInSandbox = false;
      break;
    }
    case "auto-approve":
      runInSandbox = safety.runInSandbox;
      break;
    case "reject":
      return { outputText: "aborted", metadata: { error: "command rejected" } };
  }

  // ③ 执行命令
  const summary = await execCommand(args, applyPatch, runInSandbox, ...);

  // ④ 沙盒执行失败 + fullAutoErrorMode=ASK_USER → 非沙盒重试
  if (summary.exitCode !== 0 && runInSandbox &&
      config.fullAutoErrorMode === FullAutoErrorMode.ASK_USER) {
    const review = await askUserPermission(...);
    if (review != null) return review;
    // 用户批准 → 非沙盒重试
    return execCommand(args, applyPatch, false, ...).then(convertSummaryToResult);
  }

  return convertSummaryToResult(summary);
}

4.4 raw-exec.exec() — 底层进程执行

文件: src/utils/agent/sandbox/raw-exec.ts (L20-201)

typescript
// src/utils/agent/sandbox/raw-exec.ts

export function exec(
  command: Array<string>,
  options: SpawnOptions,
  _writableRoots: ReadonlyArray<string>,
  abortSignal?: AbortSignal,
): Promise<ExecResult> {
  // ① 跨平台命令适配(如 Windows 上 ls → dir)
  const adaptedCommand = adaptCommandForPlatform(command);

  // ② 使用 spawn 而非 exec/execFile
  //    原因:exec/execFile 对 stdin 的处理会导致 ripgrep 等工具挂起
  const child = spawn(prog, adaptedCommand.slice(1), {
    ...options,
    stdio: ["ignore", "pipe", "pipe"],  // stdin=ignore 防止挂起
    detached: true,  // ★ 独立进程组,便于终止整棵进程树
  });

  // ③ abort 信号处理:先 SIGTERM,2秒后 SIGKILL
  if (abortSignal) {
    const abortHandler = () => {
      // 发送到 -child.pid(整个进程组)
      process.kill(-child.pid, "SIGTERM");
      setTimeout(() => {
        if (!child.killed) process.kill(-child.pid, "SIGKILL");
      }, 2000).unref();
    };
    abortSignal.addEventListener("abort", abortHandler, { once: true });
  }

  // ④ 收集输出(支持截断)
  return new Promise((resolve) => {
    const stdoutCollector = createTruncatingCollector(child.stdout);
    const stderrCollector = createTruncatingCollector(child.stderr);

    child.on("exit", (code, signal) => {
      // 映射退出码:code 优先,否则 128 + signal 编号
      let exitCode = code ?? (signal ? 128 + os.constants.signals[signal] : 1);
      resolve(addTruncationWarningsIfNecessary(
        { stdout, stderr, exitCode }, stdoutCollector.hit, stderrCollector.hit
      ));
    });

    child.on("error", (err) => {
      resolve({ stdout: "", stderr: String(err), exitCode: 1 });
    });
  });
}

关键设计

  • detached: true 创建独立进程组,process.kill(-pid, signal) 可一次性终止包括所有子进程的进程树
  • stdio: ["ignore", "pipe", "pipe"] 解决了 ripgrep 等工具检测 stdin 是否为 TTY 时的挂起问题
  • 先 SIGTERM 优雅终止,2 秒超时后 SIGKILL 强制终止

4.5 responsesCreateViaChatCompletions() — 双协议适配

文件: src/utils/responses.ts (L150-718, 核心约 400 行)

这个函数是整个多 Provider 支持的关键:将 Chat Completions API 的响应实时转换为 Responses API 的事件流格式。

typescript
// src/utils/responses.ts (简化说明)

export async function* responsesCreateViaChatCompletions(
  oai: OpenAI,
  params: ResponseCreateParams & { stream: true },
): AsyncIterable<ResponseEvent> {
  // ① 将 Responses API 参数转换为 Chat Completions 格式
  const messages = convertInputToMessages(params.input, params.instructions);
  const tools = convertToolsToFunctions(params.tools);

  // ② 发起 Chat Completions 流式请求
  const stream = await oai.chat.completions.create({
    model: params.model,
    messages,
    tools,
    stream: true,
    ...
  });

  // ③ 实时转换事件格式
  yield { type: "response.created", response: { id: responseId, ... } };
  yield { type: "response.in_progress", response: { ... } };

  for await (const chunk of stream) {
    const delta = chunk.choices[0]?.delta;

    if (delta?.content) {
      // 文本内容 → response.output_text.delta
      yield { type: "response.output_text.delta", delta: delta.content, ... };
    }

    if (delta?.tool_calls) {
      // 工具调用 → response.function_call_arguments.delta
      yield { type: "response.function_call_arguments.delta", delta: args, ... };
    }
  }

  // ④ 流结束,组装完整响应
  yield { type: "response.completed", response: fullResponse };
}

事件映射关系

Chat Completions 事件Responses API 事件
流开始response.created + response.in_progress
delta.contentresponse.output_text.delta
delta.tool_callsresponse.function_call_arguments.delta
工具调用完成response.output_item.done (function_call)
消息完成response.output_item.done (message)
流结束response.completed

4.6 TextBuffer — 多行文本编辑器核心

文件: src/text-buffer.ts (896 行)

终端内的完整多行编辑器实现,支持 Unicode、撤销/重做、Emacs 快捷键。

typescript
// src/text-buffer.ts (关键方法)

export default class TextBuffer {
  private lines: Array<string>;       // 按行存储
  private cursorRow = 0;              // 光标行
  private cursorCol = 0;              // 光标列
  private preferredCol: number | null; // 垂直移动时记忆的列位置
  private undoStack / redoStack;       // 100 步历史
  private clipboard: string | null;    // 剪贴板

  // ★ 高级入口:接收 Ink 的 input/key 对象,映射到所有编辑操作
  handleInput(input: string | undefined, key: Record<string, boolean>, vp: Viewport): boolean {
    // 方向键
    if (key["leftArrow"]) this.move("left");
    if (key["rightArrow"]) this.move("right");
    if (key["upArrow"]) this.move("up");
    if (key["downArrow"]) this.move("down");

    // Option+Arrow → 按词移动(兼容 macOS Terminal ESC-b/ESC-f)
    if (key["meta"] && input === "b") this.move("wordLeft");
    if (key["meta"] && input === "f") this.move("wordRight");

    // 删除
    if ((key["ctrl"] || key["meta"]) && key["backspace"]) this.deleteWordLeft();
    if (key["backspace"] || input === "\x7f") this.backspace();

    // Emacs/readline 快捷键
    if (key["ctrl"] && input === "a") this.moveToStartOfDocument(); // Ctrl+A
    if (key["ctrl"] && input === "e") this.moveToEndOfDocument();   // Ctrl+E
    if (key["ctrl"] && input === "k") this.deleteToLineEnd();       // Ctrl+K (kill)
    if (key["ctrl"] && input === "u") this.deleteToLineStart();     // Ctrl+U
    if (key["ctrl"] && input === "w") this.deleteWordLeft();        // Ctrl+W

    // 普通输入
    if (input && !key["ctrl"] && !key["meta"]) this.insert(input);

    this.ensureCursorInRange();
    this.ensureCursorVisible(vp);
    return /* true if changed */;
  }
}

Unicode 处理:使用 Intl.Segmenter字素簇(grapheme cluster)而非 UTF-16 code unit 操作,正确处理 emoji 等多码点字符:

typescript
function toCodePoints(str: string): Array<string> {
  if (typeof Intl !== "undefined" && "Segmenter" in Intl) {
    const seg = new Intl.Segmenter();
    return [...seg.segment(str)].map(seg => seg.segment);
  }
  return Array.from(str); // fallback
}

4.7 系统提示 prefix — Agent 角色定义

文件: src/utils/agent/agent-loop.ts (L1462-1507)

typescript
const prefix = `You are operating as and within the Codex CLI, a terminal-based
agentic coding assistant built by OpenAI...

You can:
- Receive user prompts, project context, and files.
- Stream responses and emit function calls (e.g., shell commands, code edits).
- Apply patches, run commands, and manage user approvals based on policy.
- Work inside a sandboxed, git-backed workspace with rollback support.

You are an agent - please keep going until the user's query is completely
resolved, before ending your turn...

CODING GUIDELINES:
- Fix the problem at the root cause rather than applying surface-level patches
- Avoid unneeded complexity in your solution
- Keep changes consistent with the style of the existing codebase
- Use apply_patch to edit files: {"cmd":["apply_patch","*** Begin Patch\\n..."]}
- Once you finish coding:
    - Check git status to sanity check your changes
    - Remove all inline comments you added
    - Try to run pre-commit if available`;

这段约 45 行的系统提示定义了 Codex CLI Agent 的:

  1. 身份认知:终端编码助手,不是旧版 Codex 模型
  2. 核心能力:流式响应、工具调用、patch 应用、沙盒执行
  3. 行为准则:持续解决问题直到完成、不猜测答案
  4. 编码规范:根因修复、最小化变更、保持代码风格、检查 git status

五、技术亮点总结

亮点实现方式
终端内 React UIReact + Ink 框架,在终端中渲染组件树
双协议适配responsesCreateViaChatCompletions() 将 Chat Completions 实时转换为 Responses 事件流
generation 防泄漏每次 run/cancel 递增计数器,过时事件被自动丢弃
pendingAborts 机制记录被取消的 function_call,下次请求补发虚拟输出
进程组管理detached: true + kill(-pid) 终止整棵进程树
macOS 沙盒Seatbelt 策略文件动态生成,参数化可写目录
Unicode 编辑器Intl.Segmenter 按字素簇操作,正确处理 emoji
多级安全评估shell-quote 解析 + 白名单 + 路径约束 + 复合表达式分析
Token 估算4 字符 ≈ 1 token 启发式,无需完整 tokenizer 依赖
LRU 文件缓存singlepass 模式下避免重复读取未变化的文件