@xuzhiyang/ainotify
v0.2.16
Published
Monitor working status of codex / opencode / claude / gemini CLI sessions
Readme
AINotify
监控 codex / opencode / claude / gemini CLI 工作状态的服务,统一暴露为 HTTP JSON + SSE,并可选推送到 Webhook 和桌面通知。
状态模型
每个被观察到的会话(按 cwd 维度组织)映射到以下统一状态之一:
| status | 含义 |
| ------------------- | ----------------------------------------------------- |
| running | 正在工作(思考 / 工具调用 / 流式输出) |
| completed | 已完成本轮任务(陈述性总结,无需用户输入) |
| waiting_for_input | 需要用户回答 / 选择 / 授权(agent 主动询问) |
| idle | 会话存活但无活跃任务 |
| error | 报错 / 重试中 |
| not_running | 该 CLI + cwd 没有活会话 |
completed vs waiting_for_input 的区分:当一个 agent turn 结束时,本服务读取其最后一条 assistant 消息文本,用启发式规则(src/adapters/heuristics.js)判断:
- 含问号(且文本较短)/ 选择类关键词(
请选择/which/option)/ 授权类关键词(approve/授权/y/n)/ 等待类短语(waiting for/请回复)→waiting_for_input - 陈述总结 / 链接列表 / 无文本 →
completed
该启发式应用于 codex / claude / gemini。opencode 的 SQLite 事件日志不持久化 assistant 文本内容(只存工具调用 part),因此无法区分,所有结束的 turn 都标记为 completed(即便 agent 实际上在询问用户)。要获得准确的 opencode waiting_for_input,需启用 OPENCODE_SERVER_URL 让 adapter 从 HTTP API 获取完整消息文本(未来扩展)。
快速开始
npm install
cp .env.example .env # 按需编辑
npm start # 监听 0.0.0.0:3000前端面板
内置一个 React + Vite 构建的实时监控面板(终端风深色 UI),由 Express 静态托管,前后端同源、一个进程。
构建
# 首次需安装前端依赖
npm run install:web
# 构建到 web/dist/ —— 之后 npm start 会自动托管
npm run build:web # 或 npm run build
npm start
# 浏览器打开 http://localhost:3000开发模式(热更新)
# 终端 A:启动后端(带 --watch 热重载)
npm run dev
# 终端 B:启动 Vite dev server(:5173,自动 proxy /status /events /health 到 :3000)
npm run dev:web
# 浏览器打开 http://localhost:5173 —— 前端改动秒级生效面板功能
- 会话卡片:每个
cli:cwd一张卡片,左侧彩色竖条 + 状态点动效(脉冲/呼吸/闪烁),展示 cwd、pid、sessionId、置信度条;detail可折叠展开 - 实时事件流:右侧滚动列表,显示每次状态变化(前态 → 后态 + 相对时间),保留最近 500 条(localStorage 持久化)
- 状态分布图:手写 SVG 堆叠面积图,最近 10 分钟按 5s 桶采样,hover 显示分项计数
- 筛选:按 status / cli 多选、cwd 模糊搜索、是否显示已结束会话;顶部 StatsBar 的 chip 点击即可联动筛选
- SSE 实时推送:状态变化秒级到达;断线自动指数退避重连(1s→2s→…→30s 上限),通过
Last-Event-ID续传
HTTP API
| 方法/路径 | 说明 |
| --------------------- | ------------------------------------------------------------- |
| GET /health | 健康检查 |
| GET /status | 全量快照(可加 ?cli=claude / ?status=running 过滤) |
| GET /status/:cli | 单个 CLI 的快照 |
| GET /events | SSE 流,每次状态变化推送一帧(支持 Last-Event-ID 断线续传)|
/status 响应示例
{
"generatedAt": 1782388287370,
"count": 2,
"sessions": [
{
"cli": "claude",
"cwd": "/root/AINotify",
"sessionId": "71286e0b-...",
"pid": 135036,
"status": "running",
"statusLabel": "工作中",
"confidence": 1,
"since": 1782388290000,
"updatedAt": 1782388300000,
"detail": {}
},
{
"cli": "gemini",
"cwd": "/root/foo",
"status": "waiting_for_input",
"statusLabel": "等待答复",
"confidence": 0.6,
"detail": { "source": "heuristic" }
}
]
}SSE 帧格式
id: 1782388300000
event: change
data: {"cli":"claude","cwd":"/root/AINotify","before":{...},"after":{...},"ts":1782388300000}各 CLI 监控机制
| CLI | 主通道 | 可靠度 |
| --------- | --------------------------------------------------- | --------------- |
| codex | tail ~/.codex/sessions/**/rollout-*.jsonl(fs.watch push)+ pgrep 存活检测 | ⭐⭐⭐⭐ 高(turn 边界明确)|
| opencode | 只读轮询 SQLite ~/.local/share/opencode/opencode.db(event + session 表)+ pgrep 存活过滤;HTTP SSE 可选加速 | ⭐⭐⭐⭐⭐ 高(turn 边界明确)|
| claude | fs.watch ~/.claude/sessions/*.json + 解析 transcript 末行 stop_reason | ⭐⭐⭐⭐ 高 |
| gemini | pgrep + tail transcript + mtime 启发式 | ⭐⭐ 低(见下方)|
关于 codex 的重要说明
VSCode/code-server 扩展启动的 codex 走 stdio 模式(codex app-server,无 --listen),其 JSON-RPC 的 stdin/stdout 被扩展宿主独占,外部进程无法挂载。系统里看到的 /tmp/codex-ipc/ipc.sock 其实归 code-server 所有,连上去会被 ECONNRESET。因此默认走 rollout JSONL 尾读方案——可靠且无侵入。
判定逻辑(按优先级):
- TUI/CLI 会话(有 rollout 文件)——读取
~/.codex/sessions/**/rollout-*.jsonl末尾事件:task_started/turn_started→ running(conf 0.95)task_complete/turn_complete→ waiting_for_input(conf 0.85)error→ error;turn_aborted→ waiting_for_input
- VSCode 扩展拉起的 app-server(无 rollout)——仅能通过
pgrep确认进程存活,无法判断 turn 内状态 → idle(conf 0.7,detail 标注state unknown)。这是真实限制,非实现缺陷:app-server 的 JSON-RPC 走 stdio 被扩展宿主独占。 - 进程消失 → not_running
历史会话(>24h)不进入监控;/status 默认也排除 not_running,需 ?includeEnded=1 查看。
高保真可选方案(Plan B):若要拿到 thread/status/changed 等原生推送(含 waitingOnApproval 等子状态,conf 1.0),自行启动 codex 并设置 CODEX_IPC_SOCKET:
codex app-server --listen unix:///tmp/codex-monitor.sock
# .env: CODEX_IPC_SOCKET=/tmp/codex-monitor.sock关于 opencode 的重要说明
opencode 的 TUI 模式(opencode [project],默认调用方式)不会启动 HTTP server——只有 opencode serve / opencode web 才监听端口。因此本服务不依赖 HTTP API,而是直接只读轮询 opencode 的 SQLite 数据库(~/.local/share/opencode/opencode.db,WAL 模式),通过 Node.js 内置的 node:sqlite(v22+)零依赖访问,不会锁竞争。
判定逻辑:
- 查询最近 2 小时内
time_updated有变化的 session - 取每个 session 最后一条
role=assistant的message.updated事件(注意:不看 user message,因为 user 消息的 seq 可能更大从而掩盖正在生成的 assistant turn) - 该事件的
info.finish字段决定状态:null或"tool-calls"(且无time.completed)→ 正在生成 → running(conf 0.95)"stop"→ turn 正常结束 → completed(conf 0.9)time.completed存在但finish缺失(异常)→ 保持 running(等待 finish 字段写入)
- 用
pgrep opencode+/proc/<pid>/cwd过滤,只报告有活进程的 cwd 下的 session
重要限制:opencode SQLite 的 message.part.updated.1 事件表只持久化工具调用 part(type:"tool"),不写入 assistant 的文本内容。因此本服务无法对 opencode 应用询问检测启发式,所有 finish=stop 的 turn 都标记为 completed——即使用户实际被 agent 问了问题。这是 opencode 本身日志设计的固有限制。需要准确的 waiting_for_input 判定时,请启用 OPENCODE_SERVER_URL 并使用 opencode serve 模式(后续版本可能从 HTTP API 补全文本)。
可选加速:若检测到 opencode serve(:4096)或设置了 OPENCODE_SERVER_URL,会额外订阅其 SSE /event 流,收到 message.updated / session.updated 时立即触发一次 SQLite 轮询,实现亚秒级延迟。TUI 模式下纯靠 1s 轮询。
关于 gemini 的重要说明
Gemini CLI 没有任何原生的状态暴露面(无 socket、无 pid 文件、transcript 里也没有 status 字段)。本服务通过 pgrep 进程检测 + transcript 尾部行类型 + mtime 启发式判定状态:
pgrep -af gemini发现进程 → 读/proc/<pid>/cwd获取工作目录 → 从~/.gemini/projects.json的projects映射查 slug → 定位~/.gemini/tmp/<slug>/chats/session-*.jsonl- transcript 最后有意义的行类型:
user(刚提交 prompt)→ running(conf 0.9)gemini+ 文件 mtime 在阈值内(仍在增长)→ running(conf 0.9)gemini+ 文件 mtime 超过阈值(默认 4s 无增长)→ waiting_for_input(conf 0.6)
- 进程消失 → not_running(conf 1.0)
已知局限:gemini 在长时间思考(网络等待、复杂推理)时 transcript 不会写入,这与"用户正在思考输入"的停顿无法区分,可能导致长时间运行的 turn 被误判为 waiting_for_input。这是 gemini 本身无状态暴露的固有限制,可通过调高 GEMINI_IDLE_THRESHOLD_MS 缓解,但无法根治。因此 gemini 的 waiting_for_input 置信度固定 0.6。
配置覆盖
所有发现路径都支持环境变量覆盖(见 .env.example):
CODEX_IPC_SOCKET:codex IPC socket 路径(默认空;自行--listen unix://启动时才填)OPENCODE_SERVER_URL:opencode HTTP server URL(可选加速通道;TUI 模式无需设置)OPENCODE_DB_PATH:opencode SQLite 数据库路径(默认~/.local/share/opencode/opencode.db)CLAUDE_HOME/GEMINI_HOME:自定义 home 目录GEMINI_IDLE_THRESHOLD_MS:gemini "等待输入"判定阈值(默认 4000ms)
输出通道
- HTTP JSON + SSE:见上
- Webhook:状态变化时 POST 到
WEBHOOK_URL,body 为{before, after, ts};配置了WEBHOOK_SECRET时附带X-AINotify-Signature: sha256=<hmac>头 - 桌面通知:调用
notify-send(Linux),标题[<cli>] <cwd>,内容为中文状态标签
架构
CLI 进程 ──┐
├─→ Adapter ──→ Store (内存态, 按 cli:cwd 维度)
CLI 进程 ──┘ │
├─→ HTTP /status (pull)
├─→ HTTP /events (SSE push)
├─→ Webhook (POST push)
└─→ notify-send (桌面)每个 Adapter 是独立的探测器,只负责把观测到的状态 emitStatus({...}) 到 Store,Store 自动去重后通过 Notifier 分发到所有输出通道。新增 CLI 只需实现一个新 adapter。
测试
目前没有单元测试 fixture;验证方式:
npm start &
# 在另一终端运行任意 CLI,然后:
curl http://localhost:3000/status
curl -N http://localhost:3000/events