ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Claude Code与Codex双向桥接:Python实现本地协作方案

Claude Code与Codex双向桥接:Python实现本地协作方案 当开发团队同时使用 Claude Code 和 Codex 两个 AI 编程代理时最头疼的问题通常不是单次问答质量而是上下文不互通。Claude Code 在长会话拆解、跨文件重构和需求分析上表现稳定Codex 在执行修改、调用 OpenAI 生态接口时更加直接。但两者安装在同一台机器上时默认互不知道对方已经改过哪些文件、回答过哪些问题、留下过哪些决策。与其不断复制粘贴任务描述不如在本地搭一座双向桥通过一个共享目录、一组 JSON 消息和一个本地 HTTP 服务让两个代理可以互相派发任务、回传结果、共享仓库变更记录。这篇文章围绕这个 local bridge 的最小实现展开会先说明设计边界再给出可运行的 Python 桥接脚本最后覆盖安装和运行阶段常见的 codex cli binary not found、模型名不被识别、代理端点失败等高频问题。1. 双向协作的核心难点Claude Code 和 Codex 各自维护一套会话1.1 两个代理的互补场景在实际项目里两个代理不一定是竞争关系更多是分工关系。比如一个较大的后端重构任务可以先让 Claude Code 分析现有接口、梳理依赖关系、给出分阶段改造方案然后再让 Codex 去改代码、跑测试、处理编译错误。问题在于Claude Code 和 Codex 各自的会话上下文是独立的。Claude Code 记得的方案Codex 并不知道Codex 改动的文件列表Claude Code 也看不到。如果人工把一段描述复制过去短任务还能接受长任务很容易丢失关键信息比如要绕过哪个模块、哪些文件可以改、哪些文件不能动、验收标准是什么。这类需求催生了一个并不复杂但非常实用的工程组件本地桥。它不改变两个 AI 代理本身的运行方式也不要求它们共享同一个后台模型。桥只负责在两端之间传递结构化信息让一方能向另一方派发任务、接收结果、查看执行状态。实现地点放在本机不依赖云服务因此离线可用也便于审计。1.2 本地桥接的定义与边界本地桥接可以理解为一组轻量协议和工具。协议定义一个消息该包含哪些字段工具提供 push、pull、update、list 等命令让 Claude Code 和 Codex 都可以通过执行 shell 命令来读写消息。这里要明确桥接的边界。桥不负责让两个代理直接看到对方的终端输出也不负责替它们做语义理解。桥只解决三件事任务派发Claude Code 创建一条 task_requestCodex 能看到。结果回传Codex 完成或失败后创建一条 task_resultClaude Code 能看到。状态同步消息是否已经读取、任务是否完成、失败原因是什么。边界清晰能避免把桥做成“又大又难维护的系统”。实际使用中桥的角色更像一个邮局而不是翻译器。两端仍然按照自己的方式理解任务桥只保证信息不丢失、可追踪、可回放。1.3 为什么优先选择本地文件加 HTTP 的轻量方案设计桥接方案时可以考虑数据库、消息队列、共享文件、HTTP 服务等形式。针对两个 CLI 代理协作的场景本地文件加轻量 HTTP 是性价比最高的组合。文件作为存储层有几个明显优势。消息是 JSON 文件可以直接cat查看消息修改支持 diff任务状态可以纳入 git 审计即使桥的 HTTP 服务没有启动代理仍然可以通过 CLI 命令直接读写文件。HTTP 服务只作为可选的查看入口不承担主要存储职责。这样即使 HTTP 进程崩溃桥接数据也不会丢只要.bridge/目录还在就能恢复。这里也要说明如果团队使用人数较多或需要一个常驻服务做任务路由、超时重试、权限控制那么后续可以迁移到 SQLite 或真正的消息队列。文章先给出最小闭环避免一开始就引入过重依赖。2. 先摸清环境CLI 安装、路径和可用扩展点2.1 检查 Claude Code 与 Codex CLI 是否可用在配置桥之前先确认两个 CLI 在终端里能正常运行。输入以下命令claude --version codex --version如果命令行找不到claude或codex说明 CLI 没有加入当前用户 PATH或者安装目录不在预期位置。还可以用which查看实际可执行文件路径which claude which codex对桥接脚本来说知道codex的完整路径很重要。因为桥接工具可能会在脚本内部调用另一个代理如果只写codex而运行环境 PATH 不完整就会触发unable to locate the codex cli binary这类错误。这个问题在 VSCode 扩展、桌面应用等场景中尤其常见因为图形界面进程的环境变量往往和终端不一致。2.2 识别常见的 codex cli binary not found 问题在一个已有的 Claude Code 会话里直接执行codex --version如果报错内容类似unable to locate the codex cli binary. set codex cli path or ensure the executable is in PATH这说明当前进程没有找到 Codex CLI。可能原因有三类PATH 不完整Codex 安装在~/.codex/bin或 npm 全局目录但当前 PATH 没包含该目录。环境变量丢失桌面应用或插件启动时没有继承终端里的 shell 配置。安装不完整Codex CLI 还没有安装成功或安装后没有重新打开终端。检查方式如下echo $PATH ls -l ~/.codex/bin/codex 2/dev/null npm root -g 2/dev/null如果确定 Codex 已安装但路径不在 PATH可以临时导出export PATH$HOME/.codex/bin:$PATH如果使用本地桥脚本建议在脚本里检测codex可执行文件找不到时输出友好提示而不是让上层代理误以为桥本身坏了。2.3 Claude Code 与 Codex 的可扩展接口对比要接入本地桥不需要修改代理内部代码只需要使用它们提供的调用外部命令能力。两者对比如下能力Claude CodeCodex CLI执行 shell 命令支持能直接运行 Bash 命令支持可通过命令行或脚本执行项目级说明文件CLAUDE.mdAGENTS.md扩展指令Agent Skill / Hook / MCP自定义指令、配置文件、skills状态存储本地会话本地会话适合桥接的方式通过 Bash 调用 bridge.py通过 shell 调用 bridge.py基于这个对比桥接方案可以只依赖“执行 shell 命令”这一能力。在 Claude Code 侧把桥接命令写进项目说明或 Skill 中在 Codex 侧把桥接命令写进AGENTS.md中。两个代理都能在需要时调用同一套bridge.py从而实现双向协作。3. 设计桥接协议先定义“消息”和“状态”两个实体3.1 消息分类桥接并不是简单的“一边发一句话另一边回一句话”。为了让代理知道该怎么处理需要给消息一个明确的 type。常见类型包括task_request请求对方完成一个任务。task_result任务执行完成或失败后的结果回传。query询问对方状态、上下文、文件信息。notify同步一次变更不要求对方必须回复。在实现中task_result必须与某个task_request关联这样发起方才知道结果对应哪个任务。关联字段使用task_id。一条task_request消息创建后会有一个唯一 IDtask_result在创建时把这个 ID 带入task_id字段。3.2 桥接目录结构与 JSON Schema在本地方案中所有消息都存放在项目根目录下的.bridge/messages/目录里。目录结构如下. ├── bridge.py ├── CLAUDE.md ├── AGENTS.md └── .bridge/ └── messages/ ├── 20250321102000-ab12cd.json ├── 20250321103000-ab34ef.json └── 20250321104000-ab56cd.json每条消息是一个 JSON 文件。核心字段如下字段含义示例id消息唯一 ID20250321102000-ab12cdsender发送方取值为claude、codex或userclauderecipient接收方取值为claude、codex或allcodextype消息类型task_requesttitle简短标题重构 /users 接口content正文内容把列表接口改为分页...task_id关联的任务 ID无关联可为空字符串20250321102000-ab12cdstatus消息状态opencreated_at创建时间2025-03-21T10:20:000800updated_at最近更新时间2025-03-21T10:30:000800context可选的上下文信息如目录、分支、改动文件{cwd: /workspace/project}示例消息如下{ id: 20250321102000-ab12cd, sender: claude, recipient: codex, type: task_request, title: 重构 /users 接口, content: 将现有列表接口改为分页并补充单元测试。, task_id: 20250321102000-ab12cd, status: open, created_at: 2025-03-21T10:20:000800, updated_at: 2025-03-21T10:20:000800, context: { cwd: /workspace/project, branch: feature/user-pagination, changed_files: [server/routes/users.py] } }status的生命周期建议保持简单open - delivered - done | failed | cancelled发起方创建消息后状态是open。接收方执行 pull 后状态变成delivered表示已经被看到。接收方完成任务后通过 push 发送task_result同时把原消息状态更新为done或failed。3.3 文件存储的读取策略和冲突处理使用文件存储最常见的问题是并发写。两个代理可能同时执行 push或者一个 pull 一个 update 同时发生。为避免文件互相覆盖可以采用“原子写”策略先写一个临时文件再通过os.replace或Path.replace替换目标文件。读取时不要依赖文件系统的修改时间做排序因为消息 ID 已经包含时间信息所以直接按文件名排序即可。pull 之后把消息标记为delivered但不删除文件。这样后续可以通过 list 命令查看完整历史也能避免误删导致追踪困难。4. 用标准库实现本地桥一个 Python 脚本完成存储与交互4.1 脚本入口与目录初始化实现采用 Python 标准库不依赖第三方包。脚本bridge.py放在项目根目录运行时会在自身目录下创建.bridge/messages/。如果希望把桥的目录放在别处可以修改BRIDGE_HOME常量的定义。#!/usr/bin/env python3 bridge.py - a local bridge for Claude Code and Codex. import argparse import json import time import uuid from pathlib import Path BRIDGE_HOME Path(__file__).resolve().parent / .bridge MSG_DIR BRIDGE_HOME / messages def now(): return time.strftime(%Y-%m-%dT%H:%M:%S%z) def ensure_dirs(): MSG_DIR.mkdir(parentsTrue, exist_okTrue) def save_message(msg): ensure_dirs() path MSG_DIR / f{msg[id]}.json tmp path.with_suffix(.tmp) tmp.write_text(json.dumps(msg, ensure_asciiFalse, indent2), encodingutf-8) tmp.replace(path) return path def load_messages(): ensure_dirs() result [] for path in sorted(MSG_DIR.glob(*.json)): try: data json.loads(path.read_text(encodingutf-8)) except json.JSONDecodeError: continue result.append(data) return resultsave_message中先用.tmp后缀写临时文件再用replace覆盖正式文件。这样即使写过程中进程被中断旧文件也不会被破坏。load_messages按文件名排序读取时不改变消息顺序。4.2 CLI 子命令push、pull、update、list接下来实现四个子命令。push创建一条消息pull读取并标记当前代理的未读消息update更新状态list输出消息概览。def create_message(sender, recipient, msg_type, title, content, task_id, contextNone): if sender not in (claude, codex, user): raise ValueError(funknown sender: {sender}) if recipient not in (claude, codex, all): raise ValueError(funknown recipient: {recipient}) msg { id: time.strftime(%Y%m%d%H%M%S) - uuid.uuid4().hex[:6], sender: sender, recipient: recipient, type: msg_type, title: title, content: content, task_id: task_id, status: open, created_at: now(), updated_at: now(), } if context: msg[context] context save_message(msg) return msg def cmd_push(args): context {cwd: args.cwd} if args.cwd else None if args.files: context context or {} context[changed_files] args.files.split(,) msg create_message( args.sender, args.recipient, args.type, args.title, args.content, args.task_id, context, ) print(json.dumps(msg, ensure_asciiFalse, indent2)) def cmd_pull(args): pulled [] for msg in load_messages(): if msg[recipient] not in (args.agent, all): continue if msg[status] ! open: continue msg[status] delivered msg[updated_at] now() save_message(msg) pulled.append(msg) if pulled: print(json.dumps(pulled, ensure_asciiFalse, indent2)) else: print(no open messages) return 1 return 0 def cmd_update(args): updated False for msg in load_messages(): if args.message_id and msg[id] ! args.message_id: continue if args.task_id and msg[task_id] ! args.task_id: continue msg[status] args.status if args.note: msg[note] args.note msg[updated_at] now() save_message(msg) updated True if not updated: print(message not found) return 1 print(updated) def cmd_list(args): for msg in load_messages(): line f{msg[id]} {msg[status]} {msg[sender]}-{msg[recipient]} [{msg[type]}] {msg[title]} print(line)pull 命令是“读取并标记”的语义。它每次只处理open状态的消息处理完以后状态变为delivered不会重复拉取。这样即使代理在抓取到消息之后中途崩溃任务仍然可以从delivered状态恢复。4.3 可选的本地 HTTP 状态服务CLI 命令已经足够实现双向协作。增加 HTTP 服务是为了方便人类开发者查看状态或给其他工具提供只读接口。服务只监听127.0.0.1不允许外部网络访问。from http.server import BaseHTTPRequestHandler, HTTPServer class BridgeHandler(BaseHTTPRequestHandler): def _send_json(self, data, status200): body json.dumps(data, ensure_asciiFalse, indent2).encode(utf-8) self.send_response(status) self.send_header(Content-Type, application/json; charsetutf-8) self.send_header(Content-Length, str(len(body))) self.end_headers() self.wfile.write(body) def do_GET(self): if self.path /status: messages load_messages() summary { total: len(messages), open: sum(1 for m in messages if m[status] open), delivered: sum(1 for m in messages if m[status] delivered), } self._send_json({bridge_home: str(BRIDGE_HOME), summary: summary}) return if self.path.startswith(/messages): query self.path.split(?, maxsplit1)[-1] agent all if query.startswith(agent): agent query.split(, maxsplit1)[-1] messages [ m for m in load_messages() if m[recipient] in (agent, all) ] self._send_json({messages: messages}) return self.send_error(404, Not Found) def cmd_server(args): httpd HTTPServer((127.0.0.1, args.port), BridgeHandler) print(fbridge server on http://127.0.0.1:{args.port}) try: httpd.serve_forever() except KeyboardInterrupt: pass/status返回消息总数和未读状态数量/messages?agentcodex返回某个接收方的全部消息。HTTP 服务不用于处理创建和删除消息只做状态查看。原因很简单创建消息需要校验发送方和接收方CLI 已经承担了这部分逻辑再重复实现会增加维护成本。4.4 主函数与命令行参数主函数将子命令和对应处理函数绑定。所有参数都通过命令行传入方便 Claude Code 和 Codex 在执行 shell 命令时展开变量。def main(): parser argparse.ArgumentParser(descriptionLocal bridge between Claude Code and Codex) sub parser.add_subparsers(destcommand, requiredTrue) push sub.add_parser(push) push.add_argument(--sender, requiredTrue)
返回列表