MENU

• 2026 年 07 月 07 日 • 已有 4 只咪围观过 • -学海无涯-

简直了,让自己完全没时间摸鱼
我愿称作高级牛马的自我修养——自己做皮鞭
踩坑都是我自己遇到的,如果你没有遇到或者与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"
}

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

2026-07-07T13:56:30.jpg


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 脚本

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

import datetime
import json
import os
import sys
import traceback
import urllib.error
import urllib.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",
)


def safe_unicode(text):
    if text is None:
        return ""

    if not isinstance(text, str):
        text = str(text)

    return text.encode("utf-8", "replace").decode("utf-8", "replace")


def cut(text, max_len=2000):
    text = safe_unicode(text)
    if not text:
        return ""
    return text if len(text) <= max_len else text[:max_len] + "..."


def log(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)

        with open(LOG_PATH, "a", encoding="utf-8", errors="replace") as f:
            f.write(f"[{now}] {message}\n")
    except Exception:
        pass


def post_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",
            **(headers or {}),
        },
        method="POST",
    )

    try:
        with urllib.request.urlopen(req, timeout=20) as resp:
            raw = resp.read().decode("utf-8", errors="replace")
            log(f"POST OK url={url} status={resp.status} body={cut(raw, 3000)}")
            return json.loads(raw)

    except urllib.error.HTTPError as e:
        raw = e.read().decode("utf-8", errors="replace")
        log(f"POST HTTP ERROR url={url} status={e.code} body={cut(raw, 3000)}")
        raise

    except Exception:
        log("POST EXCEPTION:\n" + traceback.format_exc())
        raise


def require_feishu_ok(name, result):
    if not isinstance(result, dict):
        raise RuntimeError(f"{name} 失败:返回不是 JSON:{result}")

    if result.get("code") != 0:
        raise RuntimeError(f"{name} 失败:{result}")


def get_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)
    return result["tenant_access_token"]


def send_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)
    return result


def read_hook_stdin():
    try:
        raw_stdin = sys.stdin.read()
        log(f"HOOK RAW STDIN: {cut(raw_stdin, 3000)}")

        if not raw_stdin.strip():
            return {}

        return json.loads(raw_stdin)

    except Exception:
        log("HOOK STDIN JSON PARSE FAILED:\n" + traceback.format_exc())
        return {}


def main():
    try:
        hook_data = read_hook_stdin()

        if not APP_ID or not APP_SECRET or not RECEIVE_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))
            return

        cwd = safe_unicode(hook_data.get("cwd", ""))
        now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")

        text = (
            "尊敬的老大!小的干完了!!!🙇🏻‍♂️🙇🏻‍♂️🙇🏻‍♂️\n\n"
            f"时间:{now}\n"
            f"项目:{cwd if cwd else '未知'}"
        )

        token = get_tenant_access_token()
        result = send_feishu_message(token, text)
        log(f"FEISHU SEND SUCCESS: {result}")

    except Exception:
        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"

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

2026-07-07T13:57:15.png

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 {}python ...`
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 相关

8.2 飞书机器人与消息

8.3 飞书鉴权与用户身份


结论

最终稳定方案是:

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

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

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

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

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


后话

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

返回文章列表 打赏
本页链接的二维码
打赏二维码