Files
agent-dashboard/docs/superpowers/specs/2026-06-02-agent-notify-remote-dashboard-design.md
laily 1e7efd9c3b 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>
2026-06-02 14:24:06 +08:00

239 lines
7.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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(可选保留至下版本删除)