ARTICLE DETAIL

资讯详情

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

Codex CLI 接入飞书机器人:搭建团队智能体桥接服务全记录

Codex CLI 接入飞书机器人:搭建团队智能体桥接服务全记录 最近我把 Codex CLI 接到飞书群之后身边的同事第一反应都是这玩意儿还能这么玩。说真的Codex CLI 本身已经够强了能直接在终端里用自然语言驱动它写代码、脚本甚至改文件。但问题也很明显任务入口被锁死在终端里人在外面、手边只有手机的时候想让它跑个任务就非常不方便组里几个人想共用同一个环境也不可能把终端权限交出去。所以我花了点时间做了一件事把 Codex CLI 作为后端执行引擎前端接到飞书机器人和飞书文档上。现在团队里可以直接在飞书群里 机器人输入一段需求描述机器人把它转成 Codex CLI 的 prompt在本机执行完再把结果回到群里。如果想把结果沉淀下来还能自动创建一篇飞书文档归档或者写进多维表格做后续统计。这篇文章就是从零到可用的完整记录覆盖了整体架构怎么设计、飞书开放平台怎么配置、桥接服务怎么写、Codex CLI 在子进程里经常出现的 PATH 问题怎么排查、以及飞书文档和多维表格是怎么接进来的。整个过程踩了不少坑我都会列出来类似ChatGPT failed to start. Unable to locate the codex cli binary or required runtime components这种报错我也单独拿出来分析。1. 项目初衷与整体架构设计1.1 为什么要把 Codex CLI 接到飞书最初的需求其实只有三个都很朴素。第一移动端能触发任务。我在路上经常收到这个数据帮我算一下这个脚本帮我跑一下之类的请求如果每次都要打开电脑、切到终端、敲一长串 prompt那 Codex CLI 的便利性就打了折扣。飞书作为日常办公入口把智能体挂在里面是顺理成章的事。第二过程要留痕。终端里跑完就过了结果没人记下来复盘的时候什么都找不到。飞书群聊天然有聊天记录结果发到群里就是一种留痕再进一步自动写进飞书文档或多维表格就成了团队知识库的一部分。第三多人共享能力。团队里不可能每个人都去配置 Codex CLI也没必要让所有人直接接触本机终端。通过飞书机器人作为统一入口大家只需要会发消息就行真正调用智能体的动作收敛到一个受控的服务上。1.2 三条技术路线怎么选在动手之前我认真评估过三种方案这里直接列个对比表。方案核心思路优点缺点飞书 AI 能力直接接模型在飞书开放平台配置 AI 插件让机器人直接调用大模型接口不需要额外写桥接服务链路最短受限于飞书平台支持的模型和调用方式自定义能力弱不方便控制 Codex CLI 这类本地工具飞书 Webhook 机器人只把任务结果推到飞书群里实现快纯粹做通知只能单向推送没法接收用户消息并响应交互能力几乎没有飞书自建应用机器人 本地桥接服务机器人接收消息事件本地服务解析后调用 Codex CLI再把结果回传完全可控既能收发消息又能操作本地文件、调用各种工具需要自己写服务、处理并发和异常前期工作量稍大我最终选了第三种。原因很简单只有这条链路能把飞书消息和本机 Codex CLI完整地串联起来后续接飞书文档、多维表格也顺理成章。1.3 最终架构与工作流程整个系统的运行时流程大概是这样的用户在飞书群里 机器人发送一段文本指令。飞书服务器通过长连接WebSocket 模式把消息事件推送给本地桥接服务。桥接服务对消息做解析去掉 部分提取真正的 prompt。桥接服务创建一个异步任务调用 Codex CLI 子进程执行。执行完成后桥接服务把 stdout 捕获回来做长度和格式处理。通过飞书机器人把结果发送到原会话中。可选步骤把结果导入到飞书文档或者写入多维表格。这里有个很关键的细节第 2 步我选择了飞书长连接模式而不是传统的回调 URL 模式。长连接模式下飞书服务器会主动连接我们的本地 SDK不需要公网 IP也不需要单独做内网穿透对个人项目和内部小团队来说省了很大事。2. 环境准备与前置条件2.1 安装 Codex CLI 并解决 PATH 问题Codex CLI 的安装方式在不同版本里略有差异我这边是用 npm 全局安装的安装完成后直接执行codex --version验证。的命令如下npm install -g openai/codex codex --version很多人在这一步会卡在一个非常典型的报错上而且这个问题后面做桥接服务时还会再遇到ChatGPT failed to start. Unable to locate the codex cli binary or required runtime components. Check your terminals PATH settings.这个报错的本质不是 Codex CLI 本身坏了而是它依赖的 Node.js 运行时或 CLI 可执行文件不在当前进程的 PATH 环境变量里。交互式终端之所以能用是因为 shell 启动时加载了用户的配置文件但当你从一个独立服务进程里调用 codex 时它继承到的 PATH 往往不完整。解决办法也不难先确认 codex 到底装在哪which codex # mac / linux where codex # windows拿到完整路径后在启动桥接服务的地方显式把 PATH 设置好或者把 codex 所在目录加入系统 PATH。这个问题我在第 5 章还会专门细讲因为它在 Windows 上更隐蔽。还要确认 Codex CLI 是否支持非交互执行模式。不同版本命令有差异我用的是codex exec直接把 prompt 作为参数传过去执行完成后退出如果你手头的版本不支持就运行codex --help或codex --help-exec查看可用子命令。这一步确认好后面封装子进程才不至于传错参数。2.2 飞书开放平台创建应用、开机器人、订阅事件飞书这边的配置是整个链路里最繁琐但必须仔细的部分。我先在飞书开放平台后台创建了一个企业自建应用然后做了下面这些事在添加应用能力里开启机器人能力。在权限管理里申请消息收发、云文档等相关权限。在事件与回调里订阅im.message.receive_v1事件即接收消息事件。拿到应用的App ID和App Secret这一步后面所有 API 调用都要用。这里要特别提醒一下权限范围。只是发消息和收消息还不够如果后面要操作飞书文档和多维表格至少要把这几个权限也申请下来权限标识用途im:message读取消息im:message:send_as_bot以机器人身份发送消息docx:document创建和编辑飞书文档drive:drive访问云盘文件企业自建应用提交权限申请后一般需要管理员审批内部开发者自己审批倒是很快但别忽略这一步否则后面调用文档 API 时经常会遇到权限不足的错误。2.3 网络通道优先用长连接省掉公网回调飞书事件订阅有两种模式一种是传统的回调 URL 模式另一种是长连接模式。回调 URL 模式要求你提供一个公网可访问的 HTTPS 地址飞书服务器把事件 POST 过来。这个模式最大的麻烦在于本地开发环境没有公网地址很多时候得借助内网穿透。虽然临时调试可以用但生产环境完全不推荐因为它把服务暴露到公网后必须自己处理验签、加解密和 HTTPS 证书问题。长连接模式就省心很多。飞书官方提供了 Python/Node 等语言的 SDKSDK 会在本地进程里维护一个 WebSocket 长连接飞书把事件直接推到这个连接上。整个过程不需要公网 IP不需要穿透稳定性也不错。我自己最终就是用长连接模式跑的。如果说后面有更复杂的场景比如服务端不在本地、状态需要中心化管理那时候再考虑回调 URL 也不迟。3. 桥接服务核心实现3.1 工程结构和技术栈桥接服务我选了 Python 3.10 FastAPI lark-oapi 官方 SDK。选 Python 主要看中两点一是飞书官方 SDK 对 Python 的支持很完善长连接和消息解析都有现成封装二是 Python 的asyncio处理子进程很方便Codex CLI 的执行是异步的不会阻塞消息接收。工程目录结构如下codex-feishu-bridge/ ├── main.py # 入口负责启动长连接和 HTTP 服务 ├── config.py # 配置项App ID / App Secret / PATH ├── codex_runner.py # 封装 Codex CLI 子进程调用 ├── feishu_client.py # 飞书 API 封装发消息、写文档、写多维表格 ├── message_handler.py # 消息解析和任务分发 └── requirements.txtmain.py的主要职责就是初始化飞书长连接客户端注册事件处理器。调试的时候保持日志全开方便看回调内容。3.2 用官方 SDK 拉起事件监听长连接模式示例这里直接给一份关键代码我截取了最核心的监听和解析部分import os import asyncio import lark_oapi as lark from lark_oapi.api.im.v1 import ( P2ImMessageReceiveV1, ImMessageReceiveV1Data, ) app_id os.environ[FEISHU_APP_ID] app_secret os.environ[FEISHU_APP_SECRET] def handle_message(data: P2ImMessageReceiveV1) - None: event: ImMessageReceiveV1Data data.event message event.message # 飞书消息体里content 是 JSON 字符串 import json content json.loads(message.content) text content.get(text, ) # 只处理 了机器人的消息 bot_mention f{event.sender.sender_id.open_id} if bot_mention not in text: return prompt text.replace(bot_mention, ).strip() chat_id message.chat_id message_id message.message_id # 异步执行避免阻塞长连接 asyncio.create_task(execute_and_reply(prompt, chat_id, message_id)) def on_message(data: P2ImMessageReceiveV1) - None: handle_message(data) event_handler lark.EventDispatcherHandler.builder(, ) \ .register_p2_im_message_receive_v1(on_message) \ .build() ws_client lark.ws.Client( app_id, app_secret, event_handlerevent_handler, log_levellark.LogLevel.DEBUG, ) if __name__ __main__: ws_client.start()注意content是 JSON 字符串必须先json.loads再取text。飞书事件里机器人的形式不是单纯的机器人而是一个很长的 open_id 格式所以判断是否 机器人时最稳妥的方式是直接检查event.sender.sender_id.open_id对应的那个 mention 字符串。上面代码里我用的就是动态拼接不要写死。3.3 调用 Codex CLI 的封装层codex_runner.py是整个服务的核心模块。它要解决三个问题怎么传 prompt、怎么拿到输出、怎么控制执行时长。我用的方式是通过asyncio.create_subprocess_exec启动子进程然后捕获 stdout 和 stderrimport asyncio import os CODEX_BIN /usr/local/bin/codex # 用 which codex 确认不能直接写 codex TIMEOUT_SECONDS 300 async def run_codex(prompt: str) - str: env os.environ.copy() # 关键补齐 PATH否则服务进程可能找不到 node / codex env[PATH] /usr/local/bin: env.get(PATH, ) proc await asyncio.create_subprocess_exec( CODEX_BIN, exec, prompt, stdoutasyncio.subprocess.PIPE, stderrasyncio.subprocess.PIPE, envenv, ) try: stdout, stderr await asyncio.wait_for(proc.communicate(), timeoutTIMEOUT_SECONDS) except asyncio.TimeoutError: proc.kill() return 任务执行超时已强制终止。 if proc.returncode ! 0: error_msg stderr.decode(utf-8, errorsignore) return fCodex CLI 执行出错{error_msg} return stdout.decode(utf-8, errorsignore)这里有两个细节特别值得讲。第一个是CODEX_BIN不要直接写codex而是用which codex查到的完整路径。因为服务进程可能不会继承你终端里的 PATH直接写codex很容易触发找不到命令。第二个是超时控制。Codex CLI 在复杂任务上可能会思考很久但飞书长连接本身对单次任务没有严格超时如果你用 HTTP 回调模式飞书那边通常几秒内就会要求响应所以回调模式下绝对不能同步等 Codex CLI 跑完。我的做法是收到消息后立刻返回任务开始执行然后通过异步任务继续跑跑完再主动发消息到群里。这也意味着飞书回调的响应是即时的不会因为 Codex CLI 执行时间长而报错。3.4 发消息回飞书发消息的核心是先拿到tenant_access_token这个 token 是应用身份访问飞书 API 的凭证。import requests FEISHU_BASE https://open.feishu.cn/open-apis app_id os.environ[FEISHU_APP_ID] app_secret os.environ[FEISHU_APP_SECRET] def get_tenant_token(): resp requests.post( f{FEISHU_BASE}/auth/v3/tenant_access_token/internal, json{app_id: app_id, app_secret: app_secret}, ) data resp.json() if data.get(code) ! 0: raise RuntimeError(f获取 tenant_access_token 失败{data}) return data[tenant_access_token] def send_text(chat_id: str, text: str): token get_tenant_token() body { receive_id: chat_id, msg_type: text, content: {text: text.replace(, \\) }, } headers {Authorization: fBearer {token}} resp requests.post( f{FEISHU_BASE}/im/v1/messages?receive_id_typechat_id, headersheaders, jsonbody, ) return resp.json()注意content字段是字符串不是对象。很多人第一次写飞书消息接口时直接传 JSON 对象结果被报格式错误。我上面用字符串拼接虽然简单但如果文本里有换行、引号必须做好转义。更稳妥的做法是json.dumps({text: text})然后整个塞进去。如果输出内容太长建议用 post 文本或者其他格式。飞书文本消息对超长内容会截断所以后面我会讲分段发送或者生成文档来处理长结果。3.5 结果同步到飞书文档Codex CLI 跑出来的结果有时是一段代码有时是一篇分析报告。如果只在群里发文本后续搜索和归档都不方便。我加了一个可选步骤把结果写入飞书文档。写文档我优先用导入接口直接把 Markdown 文本导入成一篇新的飞书文档。这个流程分两步第一步创建导入任务第二步轮询导入结果。def import_markdown_to_doc(title: str, content_md: str): token get_tenant_token() headers {Authorization: fBearer {token}} # Step 1: 创建导入任务 resp requests.post( f{FEISHU_BASE}/drive/v1/import_tasks, headersheaders, json{ file_extension: md, file_name: title, type: docx, file_token: , # 这里走的是直接传入内容的方式实际按官方文档调整 }, ) data resp.json() ticket data[data][ticket] # Step 2: 轮询结果 for _ in range(30): resp requests.get( f{FEISHU_BASE}/drive/v1/import_tasks/{ticket}, headersheaders, ) result resp.json().get(data, {}) if result.get(result): return result[result][token] time.sleep(2) return None这段代码里file_token参数在不同时间里可能有两种语义一种是从云盘已有文件导入另一种是直接传入文件内容。我建议你实际对接时打开飞书开放平台的导入任务文档看清楚当前接口是依赖file_token还是可以传file_content因为这块官方更新过。流程核心就是提交任务拿 ticket轮询得到文档 token理解了这个机制接口细节变化就不慌了。如果你不想用导入接口也可以用文档 block 接口逐块插入先创建空文档然后把 Markdown 分段解析成文本块、代码块再通过blocks接口插入。这样做控制粒度更细但代码量会大不少。3.6 写入飞书多维表格多维表格适合做结构化记录。比如 Codex CLI 每次任务完成后把任务描述、执行时间、结果摘要、状态写进一张表里后面可以直接做筛选和统计。多维表格写入记录的关键是先拿到app_token和table_id。创建多维表格后在 URL 里就能看到这两个参数。写入记录的接口类似def append_bitable_record(app_token: str, table_id: str, record: dict): token get_tenant_token() headers {Authorization: fBearer {token}} resp requests.post( f{FEISHU_BASE}/bitable/v1/apps/{app_token}/tables/{table_id}/records, headersheaders, json{fields: record}, ) return resp.json()这里的record[fields]是一个字典key 对应多维表格的字段名value 是字段值。多维表格在写入前要先建好对应的字段不然会出现...不存在该字段的错误。另一个坑是如果字段类型是日期value 需要用毫秒时间戳不是字符串。这些细节都写在错误码文档里报错时逐个对照就行。4. 关键参数与细节设计4.1 Codex CLI 命令与超时控制Codex CLI 本身有很多可用参数比如指定模型、最大 token 数、温度系数等。做桥接服务时我建议把这些参数都抽象成配置不要写死在代码里。我自己的默认配置大概是这样的参数项推荐值说明exec子命令使用非交互模式避免进入 REPL 后无法退出超时时间300 秒根据任务复杂度调整太短容易误杀长任务最大输出长度按飞书消息上限截断群聊文本过长会被截断需要分段或生成文档环境变量 PATH显式补齐防止子进程找不到 codex / node注意codex exec的输出可能是逐步流式的也可能一次性输出。桥接服务里我选择一次性捕获最终输出处理更简单。如果你需要实时流式分发到飞书可以用 WebSocket 或者长轮询把每一段增量推过去但那样复杂度会明显上升。4.2 飞书消息与文档 API 的权限、限流飞书开放平台的权限控制非常细。你没有申请对应 scope调用接口时会报permission denied或者invalid scope。我在实际开发中遇到过多次都是因为只申请了消息相关权限没申请文档权限。建议在服务启动时把所有需要的权限一次性申请齐全避免后面调试到一半临时补权限又要等管理员审批。常见权限清单我在 2.2 表格里列过这里再补充两个docx:document创建和管理云文档。bitable:app访问多维表格。限流方面飞书接口有频控短时间内发送大量消息会返回特定的限流错误码。我的经验是在桥接服务里对发送消息做简单的高频控制比如同一会话两次发送之间至少间隔 300 毫秒对文档导入任务也要控制并发因为导入是异步任务频繁创建会堆积很多中间文件。4.3 群聊交互细节飞书群里可能有多个机器人也可能有人 别人。消息解析的时候需要精确判断是不是 了自己的机器人这一点我在 3.2 的代码里已经体现。真正使用时还要注意几个细节第一消息内容里的 结构在飞书的content字段里并不永远是纯文本xxx可能是带 JSON 结构的 mention 数组。不同 API 版本表现不一样所以我更建议直接从事件结构里取mentions字段找到自己机器人对应的 open_id再把对应的文本片段从text里剔除。第二回复消息时使用chat_id还是message_id。如果你只想在群里发一条新的消息用chat_id就行。如果想引用原消息回复可以在发送接口里带上message_id或者使用reply接口。我个人习惯用chat_id发新消息因为 Codex CLI 执行完通常已经过了很久引用原消息意义不大反而更容易触达所有群成员。5. 常见问题与踩坑实录5.1 ChatGPT failed to start. Unable to locate the codex cli binary or required runtime components.这是我在网上看到被问得最多的问题也是我自己踩过的。这个报错通常不是出现在终端里而是出现在某些第三方程序试图调用 Codex CLI 的时候比如 IDE 插件、桌面端配置、或者我这种桥接服务。根因就一句话调用进程的 PATH 环境变量里找不到 codex 可执行文件或者找不到它依赖的 Node.js 运行时组件。解决步骤用which codexmac/linux或where codexwindows查出 codex 的绝对路径。确认 node 也在 PATH 中which node或where node。在服务启动脚本中显式设置 PATH比如export PATH/usr/local/bin:/opt/homebrew/bin:$PATH如果服务已经启动必须重启让新的 PATH 生效。5.2 Windows 上codex --version可用但服务里报找不到有个用户场景很有代表性Windows 命令行里安装了 Codex CLIcodex --version也能正常输出但通过 Windows Terminal 启动某个服务后服务内部调用 codex 就是报unable to locate the codex cli binary。这个现象在 Windows 上特别常见原因是 Windows 用户级 PATH 和管理员级 PATH 是两套服务进程如果以不同权限启动继承的 PATH 可能完全不同。另外如果你是用 npx 方式调用的npx的缓存路径也可能不在服务进程的 PATH 里。我的建议是不管用什么方式启动服务先打印一下当前环境的 PATH 日志。用where codex和where node拿到完整路径。如果服务是作为 Windows 服务运行的直接在系统环境变量里加上对应目录然后重新注册服务。如果服务是普通控制台进程在启动脚本里使用set PATHC:\...;%PATH%强制覆盖。5.3 飞书长连接一直断连/订阅不生效飞书长连接 SDK 正常情况下很稳定但如果你发现消息事件收不到先别怀疑 Codex CLI先检查长连接是否真正建立成功。我遇到过一次日志里显示 WebSocket 连接建立后马上被断开。排查发现是事件订阅里配置了加密模式但代码中解密用的Encrypt Key没有填。解决方法是在开发者后台的事件与回调里把加密策略改成明文或者把 Encrypt Key 填到 SDK 配置里。还有一个坑长连接模式下新申请的事件类型必须保存并发布新版本后才生效。你只是改了订阅事件但是没有发布版本长连接收到的事件里就一直没有你想要的类型。这个问题表现得很诡异因为不是报错就是收不到。5.4 消息收到但没有回复如果你确认长连接日志里收到了消息事件但机器人没有回复按下面顺序排查是否 了机器人飞书机器人默认不会响应没有 它的消息除非你在后台设置了接收群里所有消息但更稳妥的还是判断 。权限是否够用发送消息需要im:message:send_as_bot权限没有申请就会静默失败或收到错误。任务是否超时Codex CLI 执行时间超过了我设置的 300 秒会被杀死然后回传的是超时提示。如果你没做超时后的消息通知就会表现为没回复。是否抛了未捕获异常异步任务里如果出现异常而且没有全局try-except消息就丢了。我建议在execute_and_reply里把整个流程用try-except包起来异常时也发一条错误消息到群里。5.5 文档内容写入不全/乱码导入 Markdown 生成飞书文档时我碰到过两个典型问题。一个是内容过长导致导入失败或文档内容截断。飞书导入任务对单文件大小有限制Codex CLI 如果输出了几千行代码最好先做压缩比如只保留核心片段或者把完整内容拆成多篇文档。另一个是 Markdown 语法兼容性。Codex CLI 输出里经常有表格、嵌套代码块飞书导入接口对某些 Markdown 扩展语法支持不好可能导致渲染乱掉。我的做法是在导入前用markdown库做一次解析清洗把不支持的扩展语法转成普通文本或者直接把完整原文放到代码块里。5.6 多人同时使用导致任务串了当团队里好几个人同时 机器人时如果桥接服务不做并发控制多个 Codex CLI 子进程会同时跑轻则资源耗尽重则两个任务都互相干扰甚至写坏同一份文件。我用的方案是给每个任务生成一个唯一的task_id然后用asyncio.Semaphore控制同时执行的 Codex CLI 任务数。默认我只允许两个任务并行超过的排在队列里。sem asyncio.Semaphore(2) async def execute_with_limit(prompt, chat_id, message_id): async with sem: await execute_and_reply(prompt, chat_id, message_id)另外如果 Codex CLI 的任务会写本地文件我强烈建议每个任务都放到独立的临时目录里执行任务结束后清理。这样至少能避免文件级冲突。5.7 常见问题速查表现象可能原因解决建议服务里找不到 codex 命令PATH 不完整用绝对路径调用并显式设置 PATH请求飞书 API 报权限错误scope 未申请在开放平台补齐权限并发布版本长连接建立失败Encrypt Key 未配置改为明文模式或填入加解密密钥事件订阅不生效未发布新版本保存订阅后发布应用新版本收到消息但无回复权限不足/任务超时/异常未捕获按 5.4 排查链路文档导入失败内容过长/格式不兼容压缩内容、清洗 Markdown多任务互相干扰并发未控制用 Semaphore 限制并发数量6. 扩展思路定时任务与多维表格大盘这个桥接服务跑起来之后能玩的花样其实还有很多。比如定时任务。Codex CLI 本身是等待 prompt 才执行的但你可以用飞书多维表格作为任务队列然后写一个定时器每隔一段时间扫描表格里状态为待执行的记录自动构造 prompt 跑一次 Codex CLI再把结果回写到多维表格对应字段。这样团队里所有任务都汇聚到一张表上谁提交的、什么状态、结果是什么一目了然。再比如结果汇总。Codex CLI 每天跑了一批分析任务后可以把结果统一写入多维表格然后用飞书仪表盘做一个结果大盘按时间、任务类型、执行状态做统计。这个步骤不需要额外写代码多维表格本身就有统计和仪表盘能力。另外很多人会好奇能不能把这类智能体能力接到自动打卡、自动化办公流程上。我的建议是尽量用于做提醒、做日报汇总、做审批材料整理不要去做绕过考勤和流程的事情既不合规也容易惹麻烦。把 Codex CLI 当作知识处理和编码辅助工具来用价值已经很大了。最后分享一个我自己的体会整个对接做完之后我最满意的地方不是能从飞书发消息调用 Codex CLI这个点而是我终于把本地工具的入口打开了。以前 Codex CLI 只是给我一个人用的终端工具现在它成了团队里一个真正被大家使用的服务。同事不需要知道 Codex CLI 是什么不需要搭环境只要会发消息就行。如果你也要做类似的项目我建议从最小闭环开始先跑通飞书消息收发再加上 Codex CLI 调用最后再加文档和多维表格。不要一开始就想着把所有能力都搬上去链路越长排查问题越难。另外日志一定要从一开始就开全飞书 SDK 的调试日志、Codex CLI 的 stderr、每个任务的 task_id都记录清楚后面会帮你省下大量排查时间。
返回列表