从新线程卡死到钉钉实时告警:Codex Plus 额度全链路监控实战

从新线程卡死到钉钉实时告警:Codex Plus 额度全链路监控实战

摘要

本文解决两个独立但常见的 Codex Desktop 痛点:

  1. 每次新建线程都先刷一轮"Reconnecting... 2/5"的原因,以及一行配置的修复方法。
  2. 从零搭建一套本地 Python 监控脚本,定时读取 Codex 额度数据,支持双窗口阈值告警和钉钉推送;最后扩展到双账号独立监控。

一、新线程 Reconnecting 问题排查

1. 现象复现

打开 Codex Desktop,点击"New Thread",界面上连续出现:

Reconnecting... 2/5
Reconnecting... 3/5
Reconnecting... 4/5
Reconnecting... 5/5

等待一段时间后通常能正常对话,但每次都要经历这段等待,体验很差。

2. 根因定位

查看 Codex 运行日志(~/.codex/sessions 下最新的 .jsonl),会看到两个关键信号:

wss://chatgpt.com/backend-api/codex/responses → tls handshake eof
unhandled responses event: response.function_call_arguments.delta/done

Codex Desktop 新线程启动时有一个 WebSocket 预热流程——它会先尝试连接 wss://chatgpt.com。如果这条 WebSocket 连接握手失败(常见于某些网络环境下 TLS 握手被中断),客户端会连续重试 5 次,这就是你看到 Reconnecting... 2/5 ~ 5/5 的原因。

重试耗尽后 Codex 会降级到 HTTP/SSE 传输继续完成请求,所以最终能用,但启动阶段一直在等。

3. 修复方法

~/.codex/config.toml 里加一行,直接跳过 WebSocket 尝试:

transport = "responses_http"

完整示例(现有配置参考):

model = "gpt-5.4"
model_reasoning_effort = "high"
transport = "responses_http"

改完后重启 Codex Desktop,新线程启动时不再经历 WebSocket 预热,直接走 HTTP/SSE,Reconnecting 提示消失。


二、本地额度监控脚本

Codex Desktop 只在接近限制时才主动提示,平时完全不知道还剩多少额度。下面从零构建一套本地监控,零外部依赖,纯标准库 Python。

1. 数据来源

Codex Desktop 会把每次请求的 token 统计写入本地 session 文件:

~/.codex/sessions/2026/04/16/rollout-*.jsonl

每个 .jsonl 文件里,类型为 event_msgpayload.type == "token_count" 的行包含了你需要的额度信息:

{
  "type": "event_msg",
  "payload": {
    "type": "token_count",
    "rate_limits": {
      "primary": {
        "used_percent": 72.3,
        "resets_at": 1745000000,
        "window_minutes": 300
      },
      "secondary": {
        "used_percent": 18.1,
        "resets_at": 1745604800,
        "window_minutes": 10080
      }
    }
  }
}

其中:

  • primary:5 小时窗口
  • secondary:7 天窗口
  • used_percent:已使用比例,100 - used_percent 即剩余比例
  • resets_at:Unix 时间戳,下次重置时间

2. 核心解析逻辑

def find_latest_token_count(session_root: Path):
    """扫描所有 session 文件,返回最新一条 token_count 事件。"""
    files = sorted(
        session_root.rglob("rollout-*.jsonl"),
        key=lambda p: p.stat().st_mtime,
        reverse=True,
    )
    for path in files:
        with path.open("r", encoding="utf-8") as f:
            latest = None
            for line in f:
                event = json.loads(line)
                if (
                    event.get("type") == "event_msg"
                    and event.get("payload", {}).get("type") == "token_count"
                ):
                    latest = event
            if latest:
                return latest
    return None

3. 阈值状态机

为了避免同一个阈值在一次重置周期内重复发送通知,需要维护一个状态文件记录"已通知过的阈值"。

设计规则:

  • thresholds.primary:5 小时窗口触发阈值,如 [30, 0](剩余 30%、0% 各发一次)
  • thresholds.secondary:7 天窗口触发阈值,如 [30, 20, 10, 5, 0]
  • 每个阈值在当前 resets_at 周期内只发一次
  • resets_at 变化(新周期)时,自动清空已通知记录
  • 支持"恢复通知":额度回升超过恢复阈值时发一条"额度已恢复"
// state.json 结构
{
  "windows": {
    "primary": {
      "resets_at": 1745000000,
      "notified_thresholds": [30],
      "notified_recovery_thresholds": []
    },
    "secondary": {
      "resets_at": 1745604800,
      "notified_thresholds": [30, 20],
      "notified_recovery_thresholds": []
    }
  }
}

4. 钉钉通知格式

通知消息示例:

Codex Plus额度提醒
7天 阈值提醒: 剩余 5%

窗口: 7天
剩余: 5.2%
重置时间: 2026-04-17 09:49:31
距离重置: 0天23小时12分钟

关键字段:

  • 不带时区缩写(避免 CST 出现在 webhook 消息里)
  • 时间格式统一为 YYYY-MM-DD HH:MM:SS
  • 距离重置用"天小时分钟"格式,直观

发送代码(标准库 urllib.request,无需 requests):

def send_dingtalk(webhook: str, content: str) -> None:
    payload = json.dumps({"msgtype": "text", "text": {"content": content}}).encode("utf-8")
    req = urllib.request.Request(
        webhook,
        data=payload,
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    with urllib.request.urlopen(req, timeout=15) as resp:
        result = json.loads(resp.read().decode("utf-8"))
        if result.get("errcode") != 0:
            raise RuntimeError(f"DingTalk error: {result}")

5. 目录结构

~/.codex/codex-usage-monitor/
├── config.json          # webhook、阈值、订阅到期时间
├── state.json           # 已通知阈值的状态(自动维护)
└── logs/
    ├── monitor.stdout.log
    └── monitor.stderr.log

config.json 示例:

{
  "dingtalk_webhook": "(钉钉机器人 webhook 完整地址)",
  "poll_interval_minutes": 5,
  "message_prefix": "Codex Plus额度提醒",
  "thresholds": {
    "primary": [30, 0],
    "secondary": [30, 20, 10, 5, 0]
  },
  "notify_on_reset": true
}

6. launchd 定时运行

用 macOS launchd 每 5 分钟拉起一次脚本,取代手动 cron:

<!-- ~/Library/LaunchAgents/com.openai.codex-usage-dingtalk.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.openai.codex-usage-dingtalk</string>
  <key>ProgramArguments</key>
  <array>
    <string>/usr/bin/python3</string>
    <string>/Users/yourname/.codex/tools/codex_usage_dingtalk.py</string>
    <string>check</string>
  </array>
  <key>StartInterval</key>
  <integer>300</integer>
  <key>RunAtLoad</key>
  <true/>
  <key>StandardOutPath</key>
  <string>/Users/yourname/.codex/codex-usage-monitor/logs/monitor.stdout.log</string>
  <key>StandardErrorPath</key>
  <string>/Users/yourname/.codex/codex-usage-monitor/logs/monitor.stderr.log</string>
</dict>
</plist>

安装:

# 生成并加载(脚本内置 install-launch-agent 命令)
python3 ~/.codex/tools/codex_usage_dingtalk.py install-launch-agent

# 手动加载
launchctl load ~/Library/LaunchAgents/com.openai.codex-usage-dingtalk.plist

# 立即触发一次检查(验证 webhook 是否通)
python3 ~/.codex/tools/codex_usage_dingtalk.py check

三、双账号独立监控

如果你有两个 Codex 账号要分别监控,只要把脚本变成"配置驱动的多实例"——不同账号用不同的 config.jsonstate.json,再各装一个 launchd 任务。

1. 账号 A(本地 session 读取)

适合当前设备登录的账号,直接读 ~/.codex/sessions

python3 codex_usage_dingtalk.py check \
  --config ~/.codex/codex-usage-monitor/config.json \
  --state ~/.codex/codex-usage-monitor/state.json

如果另一个账号没有在本地 Codex Desktop 登录,可以通过浏览器 Cookie 拉取 ChatGPT 的 usage 数据。

获取 Cookie 的方法:

  1. 在浏览器打开 chatgpt.com,按 Option + Command + I 打开 DevTools
  2. 切到 Network 面板 → 刷新页面
  3. 点任意一个发往 chatgpt.com 的请求 → 找 Request Headers → 复制整个 Cookie:

注意:只复制 Cookie: 后面的值,去掉 Max-AgeDomainPathExpiresHttpOnlySecureSameSite 这些元信息(那些是 Set-Cookie 响应头里的内容,不是请求 Cookie)。

配置方式:

{
  "auth_source": "manual_cookie",
  "manual_cookie_header": "(从浏览器 DevTools 复制的完整 Cookie 请求头值)",
  "account_email": "(填写该账号的登录邮箱,用于校验防止串号)",
  "dingtalk_webhook": "(钉钉机器人 webhook 完整地址)",
  "message_prefix": "Codex 账号B额度提醒"
}

脚本会先用 cookie 调 /api/auth/session 接口校验当前登录邮箱,与 account_email 精确匹配后再拉取额度,避免串号。

3. 两个 launchd 任务并存

分别安装成不同 Label 的 LaunchAgent,彼此互不干扰:

com.openai.codex-usage-dingtalk        ← 账号 A
com.openai.codex-usage-dingtalk-b      ← 账号 B(不同 config/state 路径)

小结

以上是两个独立问题的修复总结:

  • WebSocket 问题:一行 transport = "responses_http" 解决,不需要改应用本体。
  • 额度监控:纯标准库 Python 脚本 + launchd,不依赖任何第三方包,可以直接在 Python 3.9+ 上运行;状态文件保证阈值不重复通知,扩展到多账号只需多份配置。
posted @ 2026-04-16 15:34  难删亦删  阅读(8)  评论(0)    收藏  举报