# agent-notify 第二期:远端状态仪表盘 设计规格 **日期:** 2026-06-02 **状态:** 已批准(brainstorming) **目标:** 各开发机 Hook 将 Agent 实时状态上报到自托管 HTTP Server,Web 页面按机器、工作目录、Agent 类型分类查看;详情含最后一次用户与 Agent 消息(各 ≤2000 字符)。 --- ## 1. 背景与目标 第一期(v0.2 inbox)在本地聚合通知记录(`inbox serve` + SSH `RemoteForward` + CLI/TUI)。第二期改为**中心化远端 Server + Web 仪表盘**,满足: - **实时状态**(非纯事件流):每台机器、每个工作目录、每种 Agent 类型一行当前状态 - 上报包含 **hostname + IP**、**工作目录** - 详情:从 hook 提供的 **transcript** 解析最后 user/assistant 各一条,**单条最多 2000 字符** - Web 支持按机器 / 目录 / Agent / 状态 **分类筛选** - **单机自托管**,共享 **Bearer Token**;网络暴露由用户自行配置 - **弃用本地 inbox**:Hook 默认只报远端;`inbox` 子命令标记 deprecated,代码下个大版本再删 ### 成功标准 - `agent-notify server` 可启动 HTTP 服务(API + 内嵌 Web) - 各机器配置 `[remote]` 后,Cursor/Claude hook 在发通知后上报状态,失败不影响 hook 退出码 - Web 展示所有机器的合并视图,默认按 `updated_at` 降序;`offline` 由 server 根据 5 分钟无更新判定 - 同一 `hostname + ip + cwd + agent` 仅一条记录,多会话取最近活跃 ### 非目标(第二期) - 历史事件流、告警推送 - 多用户 RBAC、每机器独立 token - `beforeSubmitPrompt` 实时更新 `last_user` - 删除 inbox 源码(仅 deprecated) - macOS/Windows server 部署文档(实现保持可交叉编译,优先 Linux 验证) --- ## 2. 方案选择 | 方案 | 描述 | 结论 | |------|------|------| | A | 同仓库 `agent-notify server` + SQLite + embed Web | **选用** | | B | 独立 server 二进制 | 版本对齐成本高,YAGNI | | C | 内存态 + JSON 快照 | 查询与持久化弱 | --- ## 3. 架构 ``` ┌─────────────────┐ hook ┌──────────────────┐ │ Cursor / Claude │ ────────────► │ agent-notify hook │ └─────────────────┘ └────────┬─────────┘ │ OSC 777(不变) │ POST /api/v1/status ▼ ┌──────────────────────┐ │ agent-notify server │ │ SQLite + HTTP API │ │ embed Web (轮询 3s) │ └──────────────────────┘ ``` ### 组件 1. **`internal/remote`** — 状态模型、`session_key`、HTTP 上报客户端 2. **`internal/transcript`** — 从 `transcript_path` jsonl 解析最后 user/assistant,截断 2000 3. **`internal/hostmeta`** — `hostname` + 非 loopback IP 列表 4. **`internal/server`** — SQLite store、Bearer 中间件、REST handlers、`offline` 计算 5. **`web/`** — 静态仪表盘(embed) 6. **Hook 改动** — `recordInbox` → `reportRemote`;`[inbox]` 默认关闭并 deprecated --- ## 4. 数据模型 ### Session 键(合并规则 C) ``` session_key = SHA256(hostname + "\0" + primary_ip + "\0" + cwd + "\0" + agent)[:32] hex ``` - `primary_ip`:上报 `ips` 中第一个 IPv4,无则空字符串 - 同一键 **upsert** 覆盖,不保留历史行 - `conversation_id` 仅存字段,不拆行 ### 状态枚举 | status | 含义 | 典型 hook | |--------|------|-----------| | `running` | Agent 刚产出回复 | `response` | | `waiting` | 等待用户输入 | `stop` | | `tool` | 工具执行中 | `tool` | | `idle` | 长时间空闲 | `idle` | | `offline` | 5 分钟无更新 | server 计算 | ### 上报 JSON(`POST /api/v1/status`) ```json { "hostname": "dev-box", "ips": ["192.168.1.10"], "agent": "Cursor", "cwd": "/home/user/proj", "status": "waiting", "event": "stop", "conversation_id": "uuid", "last_user": "…", "last_agent": "…", "updated_at": "2026-06-02T12:00:00Z" } ``` - `last_user` / `last_agent`:可选;有 transcript 时更新,截断 2000;读失败时不覆盖已有字段(server merge:空字符串不覆盖非空列) - `updated_at`:客户端 UTC;server 亦可写入 `received_at` ### SQLite 表 `sessions` | 列 | 类型 | 说明 | |----|------|------| | session_key | TEXT PK | | | hostname | TEXT | | | ips | TEXT | JSON 数组 | | agent | TEXT | | | cwd | TEXT | | | status | TEXT | | | event | TEXT | 最近触发事件名 | | conversation_id | TEXT | | | last_user | TEXT | | | last_agent | TEXT | | | updated_at | TEXT | ISO8601 | | received_at | TEXT | server 写入 | 索引:`hostname`, `cwd`, `agent`, `updated_at`。 --- ## 5. API | 方法 | 路径 | 鉴权 | 说明 | |------|------|------|------| | POST | `/api/v1/status` | Bearer | Upsert | | GET | `/api/v1/status` | Bearer | 列表;query: `host`, `cwd`, `agent`, `status` | | GET | `/api/v1/meta` | Bearer | distinct hosts / cwds / agents | | GET | `/healthz` | 无 | 健康检查 | | GET | `/` | 无 | Web UI | - Token:server `--token` 或 `AGENT_NOTIFY_TOKEN`(启动必填) - 401:缺失或错误 token --- ## 6. Hook 与 Transcript ### 流程 ``` hook → 通知(不变)→ build report → 解析 transcript(可选)→ POST remote ``` - 失败仅 `logx.Append`,返回码仍为 0 - 替换 `recordInbox`;`cfg.Remote.Enabled` 为 false 时跳过上报 ### 事件 → status | event | status | |-------|--------| | stop | waiting | | response | running | | tool | tool | | idle | idle | ### Transcript - Cursor stop payload:`transcript_path` → 读 jsonl 尾部窗口(最大 256KB)→ 最后 user/assistant - Claude:有 path 则同逻辑;无则跳过文本 - 字段映射:支持 `role`+`content` 或 Cursor transcript 常见行格式(实现时以实测样本为准) ### 机器标识 - `hostname`:`os.Hostname()` - `ips`:网卡非 loopback IPv4(可含 IPv6),去重排序 --- ## 7. Web UI - 内嵌静态页,`GET /api/v1/status` 每 3s 轮询 - 侧栏筛选:机器(hostname + ip)、Agent、状态;工作目录可侧栏或下拉 - 主表列:机器 | 目录(basename,完整路径 title)| Agent | 状态 | 更新时间 | 详情展开(last_user / last_agent) - Token:首次输入存 `sessionStorage`,请求带 `Authorization` - `offline` 行样式变灰(由 API 返回 `status=offline`) --- ## 8. 配置 ```toml [remote] enabled = true url = "http://your-server:8080" token = "shared-secret" timeout_ms = 2000 [inbox] enabled = false # deprecated ``` - `install` 默认写入 `[remote]` 占位与 `[inbox] enabled = false` - `doctor` 检查 `remote.url`、`remote.token` 非空(enabled 时) ### 命令 ```bash agent-notify server --listen :8080 --db ./agent-notify.db --token "$AGENT_NOTIFY_TOKEN" ``` `inbox` 子命令保留,执行时打印 deprecation 警告。 --- ## 9. 错误处理与测试 | 场景 | 行为 | |------|------| | 远端不可达 | log,通知照常 | | Token 错误 | 401 | | Transcript 过大 | 只读尾部 256KB | | 同键多次上报 | upsert | 测试:`session_key` 稳定、截断 2000、Bearer、offline 5min、hook 失败不影响 RunCursor/RunClaude。 --- ## 10. 迁移说明 1. 在自托管机启动 `agent-notify server` 2. 各开发机 `config.toml` 配置 `[remote]` 3. 停止依赖 `inbox serve` / SSH RemoteForward(可选保留至下版本删除)