【AI】Codex 执行完成后自动发送飞书通知

简直了,让自己完全没时间摸鱼
我愿称作高级牛马的自我修养——自己做皮鞭
踩坑都是我自己遇到的,如果你没有遇到或者与i到别的可以飞书填写表单。

可以帮你安装Hooks,爱发电购买玉米粒(6.6R)就可以!

这篇文章记录一次从 0 到 1 搭建「VSCode Codex 执行完成后自动发送飞书单聊通知」的完整过程。

最终效果是:在 VSCode 中使用 Codex 执行任务,任务结束后,飞书会自动收到一条来自应用机器人的单聊消息。消息中可以包含执行时间、电脑名、项目路径、模型名、Hook 事件类型,以及 Codex 最后一条回复。


1. 总体思路与方案选择

1.1 最终目标

想实现的链路很简单:

Codex 执行结束 ↓ Codex Stop Hook 触发 ↓ 本地 Python 脚本执行 ↓ 获取飞书 tenant_access_token ↓ 调用飞书发送消息接口 ↓ 飞书应用机器人单聊通知我

最终飞书收到的消息类似:

✅ Codex 执行完啦! 时间:2026-06-18 16:09:09 电脑:DESKTOP-XXXXXXX 项目:D:\YourProject 模型:gpt-5.5 事件:Stop 最后回复: Hi. What do you want to work on?

1.2 为什么使用 Codex Stop Hook

Codex Hooks 是 Codex 提供的生命周期扩展机制,可以在特定事件发生时执行本地脚本。这里最适合使用的是Stop事件,因为它会在 Codex 当前回合停止时触发。

官方文档中提到,Hook 配置可以放在:

~/.codex/hooks.json <repo>/.codex/hooks.json

对于本文场景,我使用的是用户全局配置:

C:\Users\YourName\.codex\hooks.json

这样所有项目都可以复用同一套通知逻辑。

1.3 为什么不用群机器人 Webhook

一开始我考虑过飞书「自定义机器人 Webhook」。Webhook 方案很简单:

Codex Hook → 调用 Webhook → 飞书群收到消息

但自定义机器人更偏向群聊通知,如果目标是「机器人直接单聊发给我」,更适合使用飞书开放平台的应用机器人能力。

所以最后采用的是:

飞书自建应用机器人 + tenant_access_token + open_id + 发送消息 API

这样可以让应用机器人直接给指定用户发送单聊消息。

1.4 最终文件结构

最后本地会有两个关键文件:

C:\Users\YourName\.codex │ ├─ hooks.json │ └─ hooks └─ feishu_codex_done.py

其中:

hooks.json

负责告诉 Codex:当Stop事件触发时,运行哪个命令。

feishu_codex_done.py

负责调用飞书接口,发送单聊通知。


2. 飞书开放平台配置

2.1 创建自建应用

进入飞书开放平台,创建一个自建应用,例如:

应用名称:Codex 应用类型:企业自建应用

创建完成后,可以在「凭证与基础信息」中看到:

App ID App Secret

这里要注意:

项目说明
App ID应用 ID,可以写入环境变量
App Secret应用密钥,相当于密码,不要公开
open_id用户在当前应用下的身份 ID,后面需要获取

如果 App Secret 曾经出现在截图、日志、博客草稿中,建议立刻重置。

2.2 添加机器人能力

在应用后台左侧找到:

添加应用能力 → 机器人

添加完成后,可以在飞书客户端中看到这个应用机器人。此时它已经具备作为机器人出现的基础能力,但还不能保证可以主动发消息。

2.3 开通发送消息权限

进入:

权限管理 → 开通权限

搜索:

im:message

需要确认至少有这个权限:

以应用的身份发消息 im:message:send_as_bot

如果已经显示「已添加」,就不需要重复添加。

2.4 发布应用版本

权限或机器人能力发生变更后,需要重新发布应用。

路径一般是:

应用发布 → 版本管理与发布 → 创建版本

版本说明可以写:

添加机器人发送消息权限

如果页面提示「本次发布免审核」,保存并提交发布后即可使用。


3. 获取接收人的 open_id

3.1 为什么不用邮箱

飞书发送消息接口支持多种receive_id_type,例如:

open_id user_id union_id email chat_id

我这里不想用邮箱,而是希望机器人直接发给当前飞书用户,所以选择:

receive_id_type = open_id

3.2 open_id 的坑

最开始我误用了 API 调试页面中的示例 open_id,结果发送失败,返回类似:

{"code":99992351,"msg":"The request you send is not a valid {open_id} or not exists","field_violations":[{"field":"receive_id","description":"id not exist"}]}

这个错误的意思是:

receive_id 不是当前应用下真实存在的 open_id

注意,open_id 是和应用相关的。不要随便复制别的页面、别的应用、示例代码里的ou_xxx

3.3 正确获取 open_id

可以通过飞书开放平台的 API 调试台获取自己的 open_id。

常见方式是:

进入发送消息相关接口 ↓ 根据排查建议进入获取 open_id 的 API 调试台 ↓ 选择当前应用 ↓ 选择成员为自己 ↓ 调用接口 ↓ 复制返回的 open_id

正确的 open_id 通常长这样:

ou_xxxxxxxxxxxxxxxxxxxxx

3.4 在 API 调试台测试发送消息

在「发送消息」接口中测试:

POST /open-apis/im/v1/messages

查询参数:

receive_id_type = open_id

请求头:

Authorization = Bearer tenant_access_token

请求体示例:

{"receive_id":"ou_xxxxxxxxxxxxxxxxxxxxx","msg_type":"text","content":"{\"text\":\"✅ Codex 单聊通知测试成功!\"}","uuid":"codex-test-001"}

这里有一个很容易踩的坑:content必须是字符串,而不是普通 JSON 对象。

正确:

"content":"{\"text\":\"hello\"}"

错误:

"content":{"text":"hello"}

测试成功后,飞书单聊中会收到应用机器人发来的消息。


4. 本地环境准备

4.1 配置环境变量

本地脚本需要使用四个环境变量:

FEISHU_APP_ID FEISHU_APP_SECRET FEISHU_RECEIVE_ID FEISHU_RECEIVE_ID_TYPE

PowerShell 中执行:

setx FEISHU_APP_ID"cli_xxxxxxxxxxxxxxxxx"setx FEISHU_APP_SECRET"你的 App Secret"setx FEISHU_RECEIVE_ID"ou_xxxxxxxxxxxxxxxxxxxxx"setx FEISHU_RECEIVE_ID_TYPE"open_id"

执行后会看到:

成功: 指定的值已得到保存。

4.2 setx 的坑

setx写入的是后续进程的环境变量,当前 PowerShell 窗口不一定能立刻读取到。

所以执行完setx后,需要:

关闭当前 PowerShell 重新打开 PowerShell 重启 VSCode

否则脚本或 Codex 可能读不到环境变量。

4.3 安装 Python

一开始本机没有可用的 Python,运行下面命令都失败:

python--version python3--version py-3--version

错误类似:

Python was not found; run without arguments to install from the Microsoft Store...

可以使用 winget 安装 Python:

winget install--id Python.Python.3.12-e

安装完成后,关闭 PowerShell,重新打开,再检查:

python--version

成功时会显示类似:

Python 3.12.10

4.4 Windows Store Python 别名问题

如果安装后仍然提示 Microsoft Store,可以检查:

Windows 设置 → 应用 → 高级应用设置 → 应用执行别名

关闭:

python.exe python3.exe

5. 编写并测试 Python 通知脚本

5.1 创建脚本目录

创建 Hook 脚本目录:

mkdir"$env:USERPROFILE\.codex\hooks"-Force

创建 Python 脚本:

notepad"$env:USERPROFILE\.codex\hooks\feishu_codex_done.py"

5.2 完整 Python 脚本

写入下面代码(这里是最简单的,只有干完了和时间项目的信息,可以根据自己的需要拓展):

importdatetimeimportjsonimportosimportsysimporttracebackimporturllib.errorimporturllib.request APP_ID=os.environ.get("FEISHU_APP_ID","").strip()APP_SECRET=os.environ.get("FEISHU_APP_SECRET","").strip()RECEIVE_ID=os.environ.get("FEISHU_RECEIVE_ID","").strip()RECEIVE_ID_TYPE=os.environ.get("FEISHU_RECEIVE_ID_TYPE","open_id").strip()LOG_PATH=os.path.join(os.path.expanduser("~"),".codex","hooks","feishu_codex_done.log",)defsafe_unicode(text):iftextisNone:return""ifnotisinstance(text,str):text=str(text)returntext.encode("utf-8","replace").decode("utf-8","replace")defcut(text,max_len=2000):text=safe_unicode(text)ifnottext:return""returntextiflen(text)<=max_lenelsetext[:max_len]+"..."deflog(message):try:os.makedirs(os.path.dirname(LOG_PATH),exist_ok=True)now=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")message=safe_unicode(message)withopen(LOG_PATH,"a",encoding="utf-8",errors="replace")asf:f.write(f"[{now}]{message}\n")exceptException:passdefpost_json(url,payload,headers=None):data=json.dumps(payload,ensure_ascii=False).encode("utf-8","replace")req=urllib.request.Request(url,data=data,headers={"Content-Type":"application/json; charset=utf-8",**(headersor{}),},method="POST",)try:withurllib.request.urlopen(req,timeout=20)asresp:raw=resp.read().decode("utf-8",errors="replace")log(f"POST OK url={url}status={resp.status}body={cut(raw,3000)}")returnjson.loads(raw)excepturllib.error.HTTPErrorase:raw=e.read().decode("utf-8",errors="replace")log(f"POST HTTP ERROR url={url}status={e.code}body={cut(raw,3000)}")raiseexceptException:log("POST EXCEPTION:\n"+traceback.format_exc())raisedefrequire_feishu_ok(name,result):ifnotisinstance(result,dict):raiseRuntimeError(f"{name}失败:返回不是 JSON:{result}")ifresult.get("code")!=0:raiseRuntimeError(f"{name}失败:{result}")defget_tenant_access_token():result=post_json("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",{"app_id":APP_ID,"app_secret":APP_SECRET,},)require_feishu_ok("获取 tenant_access_token",result)returnresult["tenant_access_token"]defsend_feishu_message(token,text):text=safe_unicode(text)result=post_json(f"https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type={RECEIVE_ID_TYPE}",{"receive_id":RECEIVE_ID,"msg_type":"text","content":json.dumps({"text":text},ensure_ascii=False),},headers={"Authorization":f"Bearer{token}",},)require_feishu_ok("发送飞书消息",result)returnresultdefread_hook_stdin():try:raw_stdin=sys.stdin.read()log(f"HOOK RAW STDIN:{cut(raw_stdin,3000)}")ifnotraw_stdin.strip():return{}returnjson.loads(raw_stdin)exceptException:log("HOOK STDIN JSON PARSE FAILED:\n"+traceback.format_exc())return{}defmain():try:hook_data=read_hook_stdin()ifnotAPP_IDornotAPP_SECRETornotRECEIVE_ID:log("MISSING ENV: "f"APP_ID={bool(APP_ID)}, "f"APP_SECRET={bool(APP_SECRET)}, "f"RECEIVE_ID={bool(RECEIVE_ID)}, "f"RECEIVE_ID_TYPE={RECEIVE_ID_TYPE}")print(json.dumps({"continue":True},ensure_ascii=False))returncwd=safe_unicode(hook_data.get("cwd",""))now=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")text=("尊敬的老大!小的干完了!!!🙇🏻‍♂️🙇🏻‍♂️🙇🏻‍♂️\n\n"f"时间:{now}\n"f"项目:{cwdifcwdelse'未知'}")token=get_tenant_access_token()result=send_feishu_message(token,text)log(f"FEISHU SEND SUCCESS:{result}")exceptException:log("FEISHU NOTIFY FAILED:\n"+traceback.format_exc())print(json.dumps({"continue":True},ensure_ascii=False))if__name__=="__main__":main()

5.3 手动测试脚本

不要直接运行:

python"$env:USERPROFILE\.codex\hooks\feishu_codex_done.py"

这样可能看起来「没反应」,因为脚本会等待stdin

正确测试方式是传入一个空 JSON:

echo{}|python"$env:USERPROFILE\.codex\hooks\feishu_codex_done.py"

如果飞书收到消息,说明脚本已经跑通。

5.4 模拟 Codex Hook 输入

可以用下面命令模拟 Codex 传入的数据:

'{"cwd":"D:\\TestProject","model":"codex","hook_event_name":"Stop","last_assistant_message":"Test from local PowerShell"}'|python"$env:USERPROFILE\.codex\hooks\feishu_codex_done.py"

这样飞书通知里就会出现模拟项目路径、模型名和最后回复。


6. 配置 Codex Stop Hook

6.1 创建 hooks.json

创建或打开 Hook 配置文件:

notepad"$env:USERPROFILE\.codex\hooks.json"

写入:

{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"python \"C:\\Users\\YourName\\.codex\\hooks\\feishu_codex_done.py\"","timeout":15,"statusMessage":"Sending Feishu notification"}]}]}}

6.2 为什么建议写绝对路径

一开始我尝试写:

"command":"python \"%USERPROFILE%\\.codex\\hooks\\feishu_codex_done.py\""

但在 Codex Hook 里,%USERPROFILE%不一定会像普通 cmd 那样展开。

最后改成绝对路径后稳定成功:

"command":"python \"C:\\Users\\YourName\\.codex\\hooks\\feishu_codex_done.py\""

6.3 在 VSCode 中确认 Hook

重启 VSCode 后,在 Codex 输入框中输入:

/hooks

如果配置被读取,会出现类似:

Stop command: python "C:\Users\YourName\.codex\hooks\feishu_codex_done.py" timeout: 15 statusMessage: Sending Feishu notification

如果 Hook 没有被信任,需要在这个界面里启用或信任。

6.4 测试 Codex 自动通知

让 Codex 执行一个很小的任务:

请回复一句 hello,不要修改文件。

等待 Codex 结束后,飞书应该自动收到一条通知。

这一步成功后,说明完整链路已经跑通:

Codex Stop Hook → Python 脚本 → 飞书应用机器人 → 单聊通知

7. 踩坑复盘与安全收尾

7.1 这次踩过的坑

问题原因解决方式
自定义机器人不能单聊Webhook 更适合群聊通知改用应用机器人发送消息 API
id not existopen_id 不是当前应用下真实 ID通过 API 调试台重新获取
权限已添加但不能发权限变更后未重新发布重新发布应用版本
content invalidcontent 写成了 JSON 对象改成 JSON 字符串
python不可用本机未安装 Python 或被 Store 别名拦截安装 Python 并检查执行别名
手动运行脚本没反应脚本等待 stdin使用 `echo {}
PowerShell 脚本乱码中文编码和引号复制问题改用 Python 版脚本
Hook 没触发Hook 未信任或路径未展开/hooks确认,改用绝对路径

7.2 App Secret 安全处理

如果 App Secret 曾经出现在截图、日志或文章草稿里,建议立即去飞书开放平台重置。

重置后重新写入环境变量:

setx FEISHU_APP_SECRET"新的 App Secret"

然后重启 VSCode。

7.3 不要提交的内容

不要把下面内容提交到 Git:

真实 App Secret 真实 open_id 真实项目路径 包含密钥的截图

如果要做成团队可复用工具,建议把敏感信息放在:

系统环境变量 .env 文件 凭据管理器 CI/CD Secret

7.4 后续优化方向

当前版本只实现了「执行完成通知」,后续可以继续扩展:

  • 通知中增加任务耗时。
  • 通知中区分成功 / 失败。
  • 通知中列出修改过的文件。
  • 通知里只显示项目名,而不是完整路径。
  • 不同项目发送到不同飞书会话。
  • 把脚本封装成一个可复用小工具。
  • 加入消息卡片,让通知更好看。

8. 官方文档索引

8.1 Codex 相关

  • Codex Hooks 官方文档
  • Codex IDE Extension Features

8.2 飞书机器人与消息

  • 飞书机器人概述
  • 自定义机器人使用指南
  • 发送消息 API
  • 消息常见问题

8.3 飞书鉴权与用户身份

  • 自建应用获取 tenant_access_token
  • 如何获取 Open ID
  • 用户身份概述
  • 发布与审核自建应用

结论

最终稳定方案是:

Codex Stop Hook + Python 本地脚本 + 飞书应用机器人单聊 API

这套方案跑通后非常实用。Codex 在 VSCode 里执行任务时,我不用一直盯着窗口。只要它停下来,飞书就会给我发消息。

这次踩坑最多的地方不是代码本身,而是:

飞书 open_id 权限发布 Python 环境 Codex Hook 路径

把这些点处理好以后,整个链路其实很清晰,也很适合继续扩展成个人自动化通知系统。


后话

后来我又迭代了,现在是这样的。可以简单指令执行“修改”,“回退”等功能。