OpenCodex 是一个运行在本地机器上的飞书机器人桥接服务,用来把“飞书消息”“本地持久 shell”“Codex CLI”连到一起。
你可以在飞书里:
- 用
/cmd远程执行本地终端命令 - 用
/codex先选择 model 和 permission,再进入 Codex 对话模式,按任务查看执行状态与回答 - 用
教我做题进入数学辅导模式,支持图片题、多轮追问、文档沉淀 - 在多台电脑同时在线时,用
/clients查看各客户端,用/use <client_id>选择当前要接管会话的那台机器
整个项目当前是一个单文件服务(app.py),使用飞书 Stream Mode 收消息,不需要自建公网回调地址。
- 在手机或飞书聊天窗口里远程驱动本地开发机
- 把 Codex CLI 的执行过程实时推送到飞书卡片
- 用固定会话把终端、问答、历史记录统一沉淀下来
- 给学生或家人做数学题讲解,并把题目与答案自动汇总到飞书文档
/cmd <command>会把命令发送到本地持久 shell,会话默认是zsh -li- shell 是常驻的,所以上一条命令留下来的当前目录、环境变量、shell 上下文会继续保留
/ctrlc会向当前 shell 发送 Ctrl+C- 普通模式下的终端输出会直接回推到绑定会话
/codex不会立刻进入模式,而是会先依次询问model和permissionmodel支持回复任意模型名;回复default表示使用本地 Codex 默认模型permission支持:read-only/workspace-write/danger-full-access- 完成选择后才会正式进入模式;后续每条自然语言消息都会触发一次独立的 Codex 任务
- 实际执行命令为:
codex exec --json --color never --skip-git-repo-check [--model <model>] -s <permission> '<你的请求>' - 每个请求都有自己的任务卡片:
- 状态区展示命令执行轨迹、完成/中断状态
- 回答区累积展示模型输出
- 卡片内容按节流频率更新,并带有“打字机”式逐步展示效果
/exitcodex可用于取消进入过程,或退出模式;若此时仍有任务在跑,会先中断当前请求/history系列命令可查看 Codex 任务历史
教我做题进入模式,支持文字题和图片题- 图片会先下载到本地
history/math_assets/,再通过codex exec --image ...交给 Codex 分析 - 同一道题会复用同一张卡片;追问会继续写入该卡片
下一题会结束当前题目的沉淀,并为新题创建新卡片结束做题/退出做题/退出数学辅导会退出模式- 支持自定义数学辅导提示词:
设置数学辅导提示词 <内容>查看数学辅导提示词清空数学辅导提示词
- 支持把每道题同步到飞书文档:
创建数学总结文档 [标题]绑定数学总结文档 <doc_id 或链接>查看数学总结文档关闭数学总结文档
- 数学总结文档使用机器人/应用身份创建与写入;创建后会尽量保持租户内链接可访问,并返回文档直达链接
- 如果还没绑定文档,系统会在首次同步时自动创建一个固定文档,后续题目持续追加到同一份文档
- 同一道题会持续更新文档里的同一段内容;
下一题会在同一文档里追加新的题目小节
/bind会把当前 chat 设为推送目标,同时把当前发送者设为授权账号- 绑定后,其他飞书账号发来的控制命令会被拒绝
- 绑定信息持久化在
.bridge_state.json,服务重启后仍会保留 - 每个实例都有独立
client_id;多机并行时,需要额外用/use <client_id>选择哪个实例处理当前会话 - 服务启动时,如果之前已经绑定且当前实例仍被选中,会尝试向该会话发一条启动提示
- 多台电脑同时运行同一个飞书机器人时,所有实例都会收到同一条消息
- 为避免多机重复执行,OpenCodex 现在把“绑定会话”和“接管会话”拆开:
/bind:绑定 chat,并记录授权用户/clients:让每台机器各自回报自己的client_id、模式、当前是否已接管/use <client_id>:选中某一台机器来处理当前会话;其他实例会自动释放当前会话/leaveclient:释放当前会话上的客户端选择
- 只有当前被选中的客户端,才会继续处理
/cmd、/codex、数学辅导、图片题等普通业务命令 - 如果切换到另一台客户端,原客户端会停止当前 shell / Codex / 数学辅导执行,避免继续往同一聊天刷输出
- 优先使用飞书模板卡(Schema 2.0)发送和更新消息
- 若未配置模板卡,或模板卡发送失败,会自动降级为
post或普通文本消息 - 普通系统反馈会尽量复用同一张“系统消息卡片”,避免刷屏
- Codex 模式按“每个请求一张卡片”组织
- 数学辅导模式按“每道题一张卡片”组织
- Codex 任务会写入
history/tasks/<task_id>.json - 历史内容包括:请求、状态、执行过的命令、模型回答、事件流、卡片消息 ID、时间戳
- 数学辅导图片会写入
history/math_assets/ - 运行日志写入
bridge.log
飞书消息
-> Stream Mode 事件
-> OpenCodex (`app.py`)
-> 授权校验 / 模式分发
-> `/cmd` -> 本地持久 shell
-> `/codex` -> `codex exec --json ...`
-> 数学辅导 -> `codex exec --image ...` / `codex exec resume ...`
-> 卡片更新 / 普通消息回推 / 历史落盘 / 文档同步
.
├── app.py # 主服务,包含配置、状态、飞书事件处理、shell/Codex/math 逻辑
├── README.md # 项目说明
├── JSON_PARSE_LOGIC.md # Codex JSON 输出的解析说明
├── requirements.txt # Python 依赖
├── .bridge_state.json # 绑定状态、授权用户、数学辅导配置(运行后生成)
├── bridge.log # 运行日志(运行后生成)
└── history/
├── tasks/ # Codex 任务历史
└── math_assets/ # 数学辅导下载的图片题素材
- Python 3.10+
- 本机可执行
codex命令 - 本机可正常连接飞书开放平台(如公司网络使用私有 CA,可配证书路径)
- 已创建飞书应用,并启用机器人与 Stream Mode
至少需要让机器人具备以下能力:
- 接收消息事件
- 发送消息到会话
- 如果要用图片题,需要读取图片资源
- 如果要用数学总结文档,需要创建/读取/写入飞书文档
权限项的精确名称会随飞书控制台展示方式变化,实际以你当前控制台为准;只要覆盖消息、图片资源、文档三类能力即可。
本项目通过长连接方式接收 im.message.receive_v1 事件,不依赖公网回调 URL。
如果希望获得更好的卡片复用与流式更新体验,建议在飞书 CardKit 中创建并发布一个模板卡。
推荐模板变量:
status_contentanswer_content
然后把模板 ID 填到 .env。如果你只维护一张通用模板卡,填写 CARD_TEMPLATE_ID 即可;如果你已经分别导入并发布了三张专用卡,建议分别填写:
GENERAL_CARD_TEMPLATE_IDCODEX_CARD_TEMPLATE_IDMATH_CARD_TEMPLATE_ID
如果你只想用单变量模板,也可以:
- 清空
CARD_STATUS_VAR_NAME - 清空
CARD_ANSWER_VAR_NAME - 把
CARD_TEMPLATE_VAR_NAME设为模板里的那个单变量名
不过当前项目默认是“双区块卡片”用法,更适合 Codex 状态和回答分开展示。
仓库内提供了三张可导入 CardKit 的示例导出文件,可作为新版 OpenCodex 专用卡的起点:
examples/OpenCodex_Codex.cardexamples/OpenCodex_System.cardexamples/OpenCodex_Math.card
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt依赖很少,只有:
lark-oapipexpectpython-dotenv
最小必填:
FEISHU_APP_ID=cli_xxx
FEISHU_APP_SECRET=xxx
FEISHU_VERIFICATION_TOKEN=xxx一个更完整的示例:
FEISHU_APP_ID=cli_xxx
FEISHU_APP_SECRET=xxx
FEISHU_VERIFICATION_TOKEN=xxx
FEISHU_ENCRYPT_KEY=
SHELL_PATH=/bin/zsh
STATE_FILE=.bridge_state.json
LOG_FILE=bridge.log
HISTORY_DIR=history
FLUSH_INTERVAL_SECONDS=1.2
CODEX_FLUSH_INTERVAL_SECONDS=0.35
CARD_UPDATE_INTERVAL_SECONDS=0.8
CARD_STATUS_MAX_LINES=18
CARD_ANSWER_MAX_CHARS=2600
TYPEWRITER_STATUS_CHARS_PER_TICK=8
TYPEWRITER_ANSWER_CHARS_PER_TICK=24
MAX_MESSAGE_CHARS=1200
ALLOWED_COMMAND_PREFIXES=
FEISHU_CA_CERT_PATH=
FEISHU_INSECURE_SKIP_VERIFY=false
CARD_TEMPLATE_ID=
GENERAL_CARD_TEMPLATE_ID=
CODEX_CARD_TEMPLATE_ID=
MATH_CARD_TEMPLATE_ID=
CARD_TEMPLATE_VAR_NAME=content
CARD_STATUS_VAR_NAME=status_content
CARD_ANSWER_VAR_NAME=answer_content
MATH_TUTOR_SYSTEM_PROMPT=
MATH_TUTOR_DOC_FOLDER_TOKEN=
MATH_TUTOR_DOC_TITLE=OpenCodex 数学辅导总结python app.py启动成功后,程序会:
- 创建本地持久 shell
- 启动飞书 Stream Mode 长连接
- 若之前已经绑定过 chat,则向该会话推送一条启动消息
| 变量 | 说明 |
|---|---|
FEISHU_APP_ID |
飞书应用 App ID |
FEISHU_APP_SECRET |
飞书应用 App Secret |
FEISHU_VERIFICATION_TOKEN |
飞书事件校验 Token |
| 变量 | 默认值 | 说明 |
|---|---|---|
FEISHU_ENCRYPT_KEY |
空 | 事件加密开启时使用;不开启可留空 |
SHELL_PATH |
/bin/zsh |
本地持久 shell 路径 |
OPENCODEX_CLIENT_NAME |
当前机器 hostname | 当前实例的展示名称,建议多机时手动区分,如 mac-mini、work-mbp |
STATE_FILE |
.bridge_state.json |
绑定状态与数学辅导配置文件 |
LOG_FILE |
bridge.log |
运行日志文件 |
HISTORY_DIR |
history |
历史任务与数学图片目录 |
ALLOWED_COMMAND_PREFIXES |
空 | 命令前缀白名单,逗号分隔;为空表示不限制 |
| 变量 | 默认值 | 说明 |
|---|---|---|
FLUSH_INTERVAL_SECONDS |
1.2 |
普通 shell 输出回推节流时间 |
CODEX_FLUSH_INTERVAL_SECONDS |
0.35 |
Codex 模式输出处理节流时间 |
CARD_UPDATE_INTERVAL_SECONDS |
0.8 |
卡片最短更新间隔 |
CARD_STATUS_MAX_LINES |
18 |
卡片状态区最多保留的行数 |
CARD_ANSWER_MAX_CHARS |
2600 |
卡片回答区最多保留的字符数 |
TYPEWRITER_STATUS_CHARS_PER_TICK |
8 |
状态区每次“打字机推进”的字符数 |
TYPEWRITER_ANSWER_CHARS_PER_TICK |
24 |
回答区每次“打字机推进”的字符数 |
MAX_MESSAGE_CHARS |
1200 |
预留参数,当前版本未实际消费 |
| 变量 | 默认值 | 说明 |
|---|---|---|
FEISHU_CA_CERT_PATH |
空 | 自定义 CA 证书路径;适合公司代理或私有根证书场景 |
FEISHU_INSECURE_SKIP_VERIFY |
false |
关闭 TLS 校验,仅建议临时排障使用 |
| 变量 | 默认值 | 说明 |
|---|---|---|
CARD_TEMPLATE_ID |
空 | 飞书模板卡 ID;为空时自动降级为 post/text |
GENERAL_CARD_TEMPLATE_ID |
空 | 系统消息 / 普通终端模式专用模板卡 ID;为空时回退到 CARD_TEMPLATE_ID |
CODEX_CARD_TEMPLATE_ID |
空 | Codex 对话模式专用模板卡 ID;为空时回退到 CARD_TEMPLATE_ID |
MATH_CARD_TEMPLATE_ID |
空 | 数学辅导模式专用模板卡 ID;为空时回退到 CARD_TEMPLATE_ID |
CARD_TEMPLATE_VAR_NAME |
content |
单变量模板模式下使用 |
CARD_STATUS_VAR_NAME |
status_content |
双变量模板里的状态区变量 |
CARD_ANSWER_VAR_NAME |
answer_content |
双变量模板里的回答区变量 |
| 变量 | 默认值 | 说明 |
|---|---|---|
MATH_TUTOR_SYSTEM_PROMPT |
空 | 默认数学辅导提示词;也可以在飞书中动态覆盖 |
MATH_TUTOR_DOC_FOLDER_TOKEN |
空 | 自动创建数学总结文档时使用的目标文件夹 |
MATH_TUTOR_DOC_TITLE |
OpenCodex 数学辅导总结 |
自动创建文档时的默认标题 |
| 命令 | 说明 |
|---|---|
/bind |
绑定当前会话,并把当前发送者记为授权账号 |
/help |
查看帮助 |
/clients |
查看当前实例的客户端信息;多机时所有在线实例都会各自回复一条 |
/use <client_id> |
选择某个客户端处理当前会话 |
/leaveclient / /unuse |
释放当前会话上的客户端选择 |
/status / /ps |
查看 bridge、模式、当前任务、卡片 ID 等状态 |
/ctrlc |
向当前 shell 或当前数学辅导进程发送 Ctrl+C |
| 命令 | 说明 |
|---|---|
/cmd <command> |
在本地持久 shell 里执行命令 |
/codex |
先选择 model 和 permission,再进入 Codex 对话模式 |
/exitcodex |
退出或取消进入 Codex 对话模式 |
/history |
查看最近 10 个 Codex 任务 |
/history <task_id> |
查看指定 Codex 任务详情 |
/history clear |
清空 Codex 历史任务 |
| 命令 | 说明 |
|---|---|
教我做题 |
进入数学辅导模式 |
下一题 |
结束当前题目并准备新题 |
结束做题 / 退出做题 / 退出数学辅导 |
退出数学辅导模式 |
设置数学辅导提示词 <内容> |
设置当前持久化提示词 |
查看数学辅导提示词 |
查看当前生效提示词 |
清空数学辅导提示词 |
恢复默认数学辅导提示词 |
创建数学总结文档 [标题] |
在飞书创建总结文档并绑定 |
绑定数学总结文档 <doc_id 或链接> |
绑定已有文档 |
查看数学总结文档 |
查看当前绑定的文档 ID |
关闭数学总结文档 |
关闭文档同步 |
- 在飞书里发送
/bind - 如果有多台客户端同时在线,发送
/clients - 选择目标客户端:
/use <client_id> - 发送
/status确认当前接管的是预期机器 - 如需执行 shell 命令,直接发送
/cmd pwd、/cmd git status等 - 如需进入 Codex 模式,发送
/codex - 按提示回复 model,再回复 permission
- 进入后直接发送自然语言需求,例如“帮我分析当前项目的 README 还缺什么”
- 如需结束,发送
/exitcodex
- 发送
教我做题 - 发送文字题目,或直接发图片
- 继续追问时,直接发后续问题即可,仍然会写入同一题卡片
- 切到下一题时发送
下一题 - 结束辅导时发送
结束做题 - 每轮讲解结束后,题干和答案会自动写入固定飞书文档;同题追问会更新原小节
| 模式 | 触发方式 | 执行后端 | 卡片策略 | 是否记入 /history |
|---|---|---|---|---|
| 普通终端模式 | /cmd |
本地持久 shell | 复用系统消息卡片或普通消息 | 否 |
| Codex 对话模式 | /codex 后先设置 model / permission,再发送自然语言 |
codex exec --json ... |
每个请求一张任务卡片 | 是 |
| 数学辅导模式 | 教我做题 后发送题目/图片 |
codex exec --image ... / codex exec resume ... |
每道题一张卡片 | 否 |
项目会解析 codex exec --json 的结构化输出,只重点抽取几类事件:
item.started+command_execution:记录“正在执行的命令”item.completed+agent_message:累积模型回答turn.completed:标记任务完成
因此卡片里的“状态区”和“回答区”是分开维护的,不是简单把终端全文直接贴过去。
更细的解析说明可参考 JSON_PARSE_LOGIC.md。
- 第一轮会创建一个新的 Codex 会话
- 后续追问会复用该会话的
thread_id - 如果附带图片,会先下载图片到本地再传给 Codex
- 讲解内容会先做 Markdown/公式规范化,以便在飞书卡片中更稳定展示
保存以下状态:
- 当前授权用户
authorized_open_id - 当前绑定会话
bound_chat_id - 当前数学辅导提示词
- 当前绑定的数学总结文档 ID 和标题
每个文件对应一个 Codex 任务,包含:
task_idpromptstatuscommandsanswer_partseventscard_message_idstarted_at/updated_at/finished_at
保存数学辅导模式下载的图片题文件。该目录中的素材可能包含敏感内容,建议按需清理。
运行日志会记录:
- 收到的飞书消息
- 下发到 shell 或 Codex 的命令
- shell / 数学辅导的原始输出
- 消息发送与卡片更新结果
- 证书与 TLS 相关日志
- 这个项目本质上可以从飞书远程控制本地机器,只建议运行在你完全信任的设备上
- 强烈建议先发送
/bind,避免未授权账号误操作 - 对
/cmd和/codex都建议配置ALLOWED_COMMAND_PREFIXES限制高风险命令 .env、.bridge_state.json、bridge.log、history/都可能包含敏感信息,不要提交到公共仓库- 数学辅导模式会把图片写到本地磁盘,注意素材留存风险
FEISHU_INSECURE_SKIP_VERIFY=true只应用于临时排障,不要长期启用
检查 ALLOWED_COMMAND_PREFIXES。
如果你配置了白名单,那么:
/cmd <command>必须以前缀白名单开头/codex内部实际要执行的是codex exec ...,所以白名单里至少要允许codex exec
优先检查:
CARD_TEMPLATE_ID是否为空- 模板是否已发布
- 模板变量名是否与
.env中配置一致 - 如果不想折腾模板卡,留空
CARD_TEMPLATE_ID也能工作,只是会退回普通消息体验
检查:
- 服务是否真的在运行:
python app.py - 是否已经在目标会话里发送过
/bind - 飞书应用是否启用了 Stream Mode 和消息事件订阅
- 你的飞书账号是否在应用可用范围内
检查:
- 当前是否已经进入数学辅导模式
- 飞书应用是否具备读取图片资源的权限
history/math_assets/是否可写
检查:
- 飞书应用是否具备文档创建/写入权限
- 如果是自动创建文档,
MATH_TUTOR_DOC_FOLDER_TOKEN是否有效 - 已绑定的
doc_id是否来自你当前可访问的文档
优先方案:
- 配置
FEISHU_CA_CERT_PATH=/path/to/your-ca.pem
仅临时排障时可使用:
FEISHU_INSECURE_SKIP_VERIFY=true
修改完代码后,至少可以做一次语法检查:
python -m py_compile app.pyapp.py:主逻辑入口README.md:项目使用说明JSON_PARSE_LOGIC.md:Codex JSON 输出解析补充说明
当前仓库未提供单独的许可证文件;如果你准备公开发布,建议补一个明确的 LICENSE。