docs: add remote dashboard design spec and implementation plan

Second-phase brainstorming: centralized HTTP server, Web dashboard,
and hook status reporting to replace deprecated local inbox.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-02 14:24:06 +08:00
parent 68b5c8bc81
commit 1e7efd9c3b
2 changed files with 669 additions and 0 deletions
@@ -0,0 +1,238 @@
# 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 |
- Tokenserver `--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(可选保留至下版本删除)