
简直了让自己完全没时间摸鱼我愿称作高级牛马的自我修养——自己做皮鞭踩坑都是我自己遇到的如果你没有遇到或者与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 HookCodex 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_id3.1 为什么不用邮箱飞书发送消息接口支持多种receive_id_type例如open_id user_id union_id email chat_id我这里不想用邮箱而是希望机器人直接发给当前飞书用户所以选择receive_id_type open_id3.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_xxxxxxxxxxxxxxxxxxxxx3.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_TYPEPowerShell 中执行setx FEISHU_APP_IDcli_xxxxxxxxxxxxxxxxxsetx FEISHU_APP_SECRET你的 App Secretsetx FEISHU_RECEIVE_IDou_xxxxxxxxxxxxxxxxxxxxxsetx FEISHU_RECEIVE_ID_TYPEopen_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 安装 Pythonwinget install--id Python.Python.3.12-e安装完成后关闭 PowerShell重新打开再检查python--version成功时会显示类似Python 3.12.104.4 Windows Store Python 别名问题如果安装后仍然提示 Microsoft Store可以检查Windows 设置 → 应用 → 高级应用设置 → 应用执行别名关闭python.exe python3.exe5. 编写并测试 Python 通知脚本5.1 创建脚本目录创建 Hook 脚本目录mkdir$env:USERPROFILE\.codex\hooks-Force创建 Python 脚本notepad$env:USERPROFILE\.codex\hooks\feishu_codex_done.py5.2 完整 Python 脚本写入下面代码这里是最简单的只有干完了和时间项目的信息可以根据自己的需要拓展importdatetimeimportjsonimportosimportsysimporttracebackimporturllib.errorimporturllib.request APP_IDos.environ.get(FEISHU_APP_ID,).strip()APP_SECRETos.environ.get(FEISHU_APP_SECRET,).strip()RECEIVE_IDos.environ.get(FEISHU_RECEIVE_ID,).strip()RECEIVE_ID_TYPEos.environ.get(FEISHU_RECEIVE_ID_TYPE,open_id).strip()LOG_PATHos.path.join(os.path.expanduser(~),.codex,hooks,feishu_codex_done.log,)defsafe_unicode(text):iftextisNone:returnifnotisinstance(text,str):textstr(text)returntext.encode(utf-8,replace).decode(utf-8,replace)defcut(text,max_len2000):textsafe_unicode(text)ifnottext:returnreturntextiflen(text)max_lenelsetext[:max_len]...deflog(message):try:os.makedirs(os.path.dirname(LOG_PATH),exist_okTrue)nowdatetime.datetime.now().strftime(%Y-%m-%d %H:%M:%S)messagesafe_unicode(message)withopen(LOG_PATH,a,encodingutf-8,errorsreplace)asf:f.write(f[{now}]{message}\n)exceptException:passdefpost_json(url,payload,headersNone):datajson.dumps(payload,ensure_asciiFalse).encode(utf-8,replace)requrllib.request.Request(url,datadata,headers{Content-Type:application/json; charsetutf-8,**(headersor{}),},methodPOST,)try:withurllib.request.urlopen(req,timeout20)asresp:rawresp.read().decode(utf-8,errorsreplace)log(fPOST OK url{url}status{resp.status}body{cut(raw,3000)})returnjson.loads(raw)excepturllib.error.HTTPErrorase:rawe.read().decode(utf-8,errorsreplace)log(fPOST HTTP ERROR url{url}status{e.code}body{cut(raw,3000)})raiseexceptException:log(POST EXCEPTION:\ntraceback.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():resultpost_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):textsafe_unicode(text)resultpost_json(fhttps://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_asciiFalse),},headers{Authorization:fBearer{token},},)require_feishu_ok(发送飞书消息,result)returnresultdefread_hook_stdin():try:raw_stdinsys.stdin.read()log(fHOOK RAW STDIN:{cut(raw_stdin,3000)})ifnotraw_stdin.strip():return{}returnjson.loads(raw_stdin)exceptException:log(HOOK STDIN JSON PARSE FAILED:\ntraceback.format_exc())return{}defmain():try:hook_dataread_hook_stdin()ifnotAPP_IDornotAPP_SECRETornotRECEIVE_ID:log(MISSING ENV: fAPP_ID{bool(APP_ID)}, fAPP_SECRET{bool(APP_SECRET)}, fRECEIVE_ID{bool(RECEIVE_ID)}, fRECEIVE_ID_TYPE{RECEIVE_ID_TYPE})print(json.dumps({continue:True},ensure_asciiFalse))returncwdsafe_unicode(hook_data.get(cwd,))nowdatetime.datetime.now().strftime(%Y-%m-%d %H:%M:%S)text(尊敬的老大小的干完了♂️♂️♂️\n\nf时间{now}\nf项目{cwdifcwdelse未知})tokenget_tenant_access_token()resultsend_feishu_message(token,text)log(fFEISHU SEND SUCCESS:{result})exceptException:log(FEISHU NOTIFY FAILED:\ntraceback.format_exc())print(json.dumps({continue:True},ensure_asciiFalse))if__name____main__:main()5.3 手动测试脚本不要直接运行python$env:USERPROFILE\.codex\hooks\feishu_codex_done.py这样可能看起来「没反应」因为脚本会等待stdin。正确测试方式是传入一个空 JSONecho{}|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 Hook6.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 更适合群聊通知改用应用机器人发送消息 APIid 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 Secret7.4 后续优化方向当前版本只实现了「执行完成通知」后续可以继续扩展通知中增加任务耗时。通知中区分成功 / 失败。通知中列出修改过的文件。通知里只显示项目名而不是完整路径。不同项目发送到不同飞书会话。把脚本封装成一个可复用小工具。加入消息卡片让通知更好看。8. 官方文档索引8.1 Codex 相关Codex Hooks 官方文档Codex IDE Extension Features8.2 飞书机器人与消息飞书机器人概述自定义机器人使用指南发送消息 API消息常见问题8.3 飞书鉴权与用户身份自建应用获取 tenant_access_token如何获取 Open ID用户身份概述发布与审核自建应用结论最终稳定方案是Codex Stop Hook Python 本地脚本 飞书应用机器人单聊 API这套方案跑通后非常实用。Codex 在 VSCode 里执行任务时我不用一直盯着窗口。只要它停下来飞书就会给我发消息。这次踩坑最多的地方不是代码本身而是飞书 open_id 权限发布 Python 环境 Codex Hook 路径把这些点处理好以后整个链路其实很清晰也很适合继续扩展成个人自动化通知系统。后话后来我又迭代了现在是这样的。可以简单指令执行“修改”“回退”等功能。