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:
@@ -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 |
|
||||
|
||||
- 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(可选保留至下版本删除)
|
||||
Reference in New Issue
Block a user