在日常使用 Codex CLI 的过程中,我们经常会遇到这样一类场景:

交互式后台模式 —— background + PTY 组合拳,搞定长任务与实时监控

简介

在日常使用 Codex CLI 的过程中,我们经常会遇到这样一类场景:

  • 需要启动一个开发服务器(如 npm run devuvicorn),然后让它持续运行
  • 需要执行一个耗时很长的构建任务(如全量编译、模型训练)
  • 需要运行一个交互式 CLI 工具(如 Python REPL、数据库客户端)
  • 需要在后台跑一个长任务,同时还能监控它的输出进度

如果你尝试在前台模式下启动一个永不退出的服务,终端会一直被占用;如果你用 background=true 启动一个需要交互的工具,它可能会因为没有伪终端(PTY)而直接报错退出。

解决这些问题的钥匙,就是 backgroundpty 参数的组合使用。

本文将深入讲解如何在 Codex CLI 中利用后台模式、PTY 模式以及进程监控能力,构建一个完整的"启动-监控-交互-终止"工作流。

一、前台 vs 后台:基本概念

1.1 前台模式(默认)

前台模式是 Codex CLI 的默认行为:命令启动后,终端会阻塞等待命令执行完毕,然后返回输出和退出码。

python
# 前台模式:同步等待完成
result = terminal(command="npm run build")
# 阻塞直到构建完成...
print(result.output)
print(result.exit_code)

适用场景:

  • 快速命令(lscatgit status
  • 需要在下一步立即使用结果的命令
  • 预期会在合理时间内退出的脚本

不适用场景:

  • 开发服务器(永远不会退出)
  • 耗时超过数分钟的构建任务
  • 需要保持运行状态的守护进程

1.2 后台模式(background=true)

后台模式让命令在独立的进程中启动,立即返回一个 session_id,你可以随时查询它的状态和输出。

python
# 后台模式:立即返回,异步执行
session = terminal(
    command="npm run dev",
    background=True
)
print(f"Session ID: {session.session_id}")
# 继续做其他事情...

适用场景:

  • 开发服务器和 Web 服务
  • 长时间运行的任务
  • 守护进程和 watcher

1.3 两种后台任务的分类

后台模式实际上支持两种不同性质的任务:

python
# 类型 1:长生命周期进程(服务器、watcher)
server = terminal(
    command="python -m http.server 8080",
    background=True
)
# 这个进程永远不会自行退出

# 类型 2:有终点的长任务(测试套件、构建、部署)
build = terminal(
    command="npm run build:all",
    background=True,
    notify_on_complete=True  # 完成后自动通知
)
# 这个进程最终会退出,通知机制让你不用反复轮询

二、PTY 模式:交互式工具的救星

2.1 什么是 PTY?

PTY(Pseudo Terminal,伪终端)是一个模拟真实终端的设备。很多命令行工具会检测自己是否运行在终端环境中,如果不是,它们会:

  • 拒绝启动(认为被脚本调用不安全)
  • 关闭彩色输出
  • 禁用交互功能
  • 行为发生改变

2.2 需要 PTY 的典型场景

text
# 需要 PTY 的工具示例:
├── Python REPL (python -i)
├── Node REPL (node)
├── 数据库客户端 (psql, mysql)
├── SSH 交互式连接
├── 带进度条的工具 (htop, top)
├── 分页器 (less, more)
├── 交互式安装脚本
└── Codex CLI 本身(嵌套使用时)

2.3 PTY 模式实战

python
# 启动 Python REPL
repl = terminal(
    command="python -i",
    background=True,
    pty=True  # 关键:启用伪终端
)

# 发送代码执行
process(action="submit", session_id=repl.session_id, data="print('Hello from REPL')")

# 查看输出
output = process(action="log", session_id=repl.session_id)
print(output)

# 退出 REPL
process(action="close", session_id=repl.session_id)  # 发送 EOF

2.4 background + pty 组合

background=truepty=true 同时使用时,你获得了一个运行在后台的交互式终端。这是最强大的组合:

python
# 启动交互式 Python 会话
session = terminal(
    command="python -i",
    background=True,
    pty=True,
    timeout=300  # 给足够的启动时间
)

# 等待 REPL 就绪
import time
time.sleep(1)

# 检查状态
status = process(action="poll", session_id=session.session_id)
print(f"状态: {status.status}")

# 发送第一条命令
process(action="submit", session_id=session.session_id, data="import sys")

# 发送第二条命令
process(action="submit", session_id=session.session_id, data="print(sys.version)")

# 获取完整输出
log = process(action="log", session_id=session.session_id)

# 发送 EOF 结束
process(action="close", session_id=session.session_id)

三、长任务启动与监控

3.1 启动一个长任务

让我们用一个实际的例子:启动一个需要编译的大型项目。

python
# 启动全量构建任务
build_session = terminal(
    command="cd /opt/data/my-project && npm run build:production",
    background=True,
    timeout=600,  # 10 分钟超时
    notify_on_complete=True  # 完成后通知
)

print(f"构建任务已启动,Session ID: {build_session.session_id}")
print("你可以继续做其他事情,完成后会自动通知...")

3.2 进程监控:五种核心操作

process 工具提供了五种核心操作来管理后台进程:

3.2.1 list:查看所有后台进程

python
# 列出所有后台进程
all_sessions = process(action="list")
print(all_sessions)

输出示例:

text
Active Sessions:
  abc123  python -m http.server 8080        running  2m ago
  def456  npm run build:all                  running  5m ago
  ghi789  python -i                          running  1m ago

3.2.2 poll:检查状态和新输出

python
# 检查单个进程状态
status = process(action="poll", session_id="def456")
print(f"状态: {status.status}")
print(f"新输出:\n{status.new_output}")

最佳实践: 定期轮询,但不要太频繁。对于长任务,每 30-60 秒轮询一次即可。

3.2.3 log:获取完整输出

python
# 获取最近 50 行输出
recent = process(action="log", session_id="def456", limit=50)

# 从第 100 行开始获取
from_line = process(action="log", session_id="def456", offset=100, limit=50)

# 获取全部输出(注意可能很大)
full = process(action="log", session_id="def456", offset=1, limit=2000)

3.2.4 wait:阻塞等待完成

python
# 阻塞等待,最多等 300 秒
result = process(action="wait", session_id="def456", timeout=300)
print(f"退出码: {result.exit_code}")
print(f"最终输出:\n{result.output}")

注意: wait 会阻塞当前线程,但会在超时后返回部分输出,不会永远卡住。

3.2.5 kill:终止进程

python
# 终止进程
process(action="kill", session_id="def456")

# 确认终止
status = process(action="poll", session_id="def456")
# 应该显示 killed/stopped

3.3 write vs submit:两种输入方式

python
# submit:发送数据 + Enter(适合执行命令)
process(action="submit", session_id=pty_session, data="print('hello')")

# write:发送原始 stdin 数据(不带换行符)
process(action="write", session_id=pty_session, data="y")

# 组合使用:输入多行
process(action="write", session_id=pty_session, data="def hello():")
process(action="submit", session_id=pty_session, data="    print('world')")

四、实战场景:完整的开发服务器工作流

4.1 场景描述

假设我们需要:

  1. 启动一个 Node.js 开发服务器
  2. 等待服务器就绪
  3. 运行健康检查
  4. 在服务器运行时执行其他任务
  5. 完成后关闭服务器

4.2 完整代码

python
import time

# 步骤 1:启动开发服务器
server = terminal(
    command="cd /opt/data/my-webapp && npm run dev",
    background=True,
    pty=True,  # 开发服务器可能需要 PTY 来显示彩色日志
    timeout=120
)
print(f"服务器已启动: {server.session_id}")

# 步骤 2:等待服务器就绪
# 方法 A:轮询日志直到看到就绪信号
ready = False
for i in range(30):  # 最多等 30 次
    time.sleep(2)
    log = process(action="log", session_id=server.session_id, limit=20)
    if "ready" in log.lower() or "listening" in log.lower() or "3000" in log:
        ready = True
        print("服务器就绪!")
        break

if not ready:
    print("警告:服务器可能未正常启动")
    log = process(action="log", session_id=server.session_id)
    print(log)

# 方法 B:直接发起健康检查
if ready:
    health = terminal(command="curl -s http://localhost:3000/health")
    if health.exit_code == 0:
        print("健康检查通过")
    else:
        print("健康检查失败")

# 步骤 3:在服务器运行时执行其他任务
# 比如运行前端测试
tests = terminal(
    command="cd /opt/data/my-webapp && npm run test:e2e",
    background=True,
    notify_on_complete=True
)

# 步骤 4:等待测试完成
test_result = process(action="wait", session_id=tests.session_id, timeout=600)
print(f"测试结果: {test_result.exit_code}")

# 步骤 5:关闭服务器
process(action="kill", session_id=server.session_id)
print("服务器已关闭")

五、进阶技巧与最佳实践

5.1 超时策略

python
# 短命令:默认 180 秒足够
short = terminal(command="ls -la")

# 中等任务:设置合理超时
medium = terminal(
    command="npm run build",
    background=True,
    timeout=300  # 5 分钟
)

# 长任务:使用 notify_on_complete 而非大超时
long = terminal(
    command="python train_model.py --epochs=100",
    background=True,
    notify_on_complete=True  # 不用反复轮询
)

5.2 日志分页技巧

当进程输出很大时,使用 offset 和 limit 进行分页读取:

python
session_id = "abc123"
page_size = 100
offset = 1

while True:
    page = process(action="log", session_id=session_id, offset=offset, limit=page_size)
    if not page.output.strip():
        break
    print(page.output)
    offset += page_size
    if offset > 10000:  # 安全限制
        break

5.3 多进程并发管理

python
# 同时启动多个构建任务
sessions = []
for project in ["frontend", "backend", "shared"]:
    s = terminal(
        command=f"cd /opt/data/{project} && npm run build",
        background=True,
        notify_on_complete=True
    )
    sessions.append((project, s.session_id))

# 监控所有任务
while sessions:
    for project, sid in sessions[:]:  # 复制列表以便修改
        status = process(action="poll", session_id=sid)
        if status.status != "running":
            print(f"{project} 已完成")
            sessions.remove((project, sid))

    if sessions:
        time.sleep(5)

5.4 错误处理

python
try:
    session = terminal(
        command="some-command",
        background=True,
        timeout=60
    )

    result = process(action="wait", session_id=session.session_id, timeout=120)

    if result.exit_code != 0:
        print(f"命令失败,退出码: {result.exit_code}")
        # 查看错误输出
        error_log = process(action="log", session_id=session.session_id)
        print(f"错误输出:\n{error_log}")

except Exception as e:
    print(f"进程管理出错: {e}")
    # 尝试清理
    if 'session' in locals():
        process(action="kill", session_id=session.session_id)

六、常见陷阱与解决方案

6.1 陷阱一:忘记 PTY 导致交互式工具退出

python
# ❌ 错误:没有 PTY,交互式工具可能立即退出
session = terminal(command="python", background=True)

# ✅ 正确:启用 PTY
session = terminal(command="python", background=True, pty=True)

6.2 陷阱二:后台进程输出缓冲

python
# Python 脚本在后台运行时可能缓冲输出
# 解决:使用 -u 参数或设置环境变量
session = terminal(
    command="python -u my_script.py",  # -u = unbuffered
    background=True
)

# 或者
session = terminal(
    command="PYTHONUNBUFFERED=1 python my_script.py",
    background=True
)

6.3 陷阱三:process 操作时进程已结束

python
# 先检查状态,再操作
status = process(action="poll", session_id=sid)
if status.status == "running":
    log = process(action="log", session_id=sid)
else:
    # 进程已结束,获取最终输出
    log = process(action="log", session_id=sid)
    print(f"进程已退出,退出码: {status.exit_code}")

6.4 陷阱四:watch_patterns 误用

python
# ❌ 错误:对会频繁出现的模式使用 watch_patterns
# 如果日志中频繁出现 "INFO",会触发速率限制
session = terminal(
    command="python app.py",
    background=True,
    watch_patterns=["INFO", "DEBUG"]  # 会被自动禁用
)

# ✅ 正确:只对罕见的一次性信号使用 watch_patterns
session = terminal(
    command="python migrate.py",
    background=True,
    watch_patterns=["migration done"]  # 只出现一次
)

七、综合实战:Codex CLI 嵌套使用

一个高级场景:在 Codex CLI 中启动另一个 Codex CLI 实例来执行子任务。

python
# 主 Codex 实例启动一个子 Codex 来处理特定任务
sub_codex = terminal(
    command="codex exec -p '分析 src/utils.py 中的 bug'",
    background=True,
    pty=True,  # Codex CLI 本身需要 PTY
    timeout=600
)

# 监控子任务进度
while True:
    status = process(action="poll", session_id=sub_codex.session_id)
    log = process(action="log", session_id=sub_codex.session_id, limit=5)

    if status.status != "running":
        print("子任务完成")
        break

    time.sleep(10)

# 获取完整结果
result = process(action="log", session_id=sub_codex.session_id)
print(result)

总结

本文深入讲解了 Codex CLI 的交互式后台模式:

  1. 前台 vs 后台:前台用于短命令,后台用于长任务和持久服务
  2. PTY 模式:交互式工具必须启用 PTY,否则会异常退出
  3. 进程监控五件套listpolllogwaitkill 覆盖全生命周期
  4. 输入方式submit 带换行执行命令,write 发送原始输入
  5. 最佳实践:合理设置超时、分页读取日志、正确处理错误

核心要点:background + pty 组合是运行交互式后台服务的标准模式,配合 process 工具的全套监控能力,你可以优雅地管理任何类型的长生命周期任务。

下篇预告

下一篇:《工作树并行 —— git worktree + 多 Codex 实例,同时修多个 Issue》

我们将学习如何使用 git worktree 创建多个独立的工作目录,在每个工作目录中运行独立的 Codex CLI 实例,实现真正的并行开发——同时修复多个 Issue 而不互相干扰。敬请期待!