diff --git a/docs/OPENCLAW_PLUGIN_DEV_PLAN.md b/docs/OPENCLAW_PLUGIN_DEV_PLAN.md
new file mode 100644
index 0000000..c22c829
--- /dev/null
+++ b/docs/OPENCLAW_PLUGIN_DEV_PLAN.md
@@ -0,0 +1,494 @@
+# OpenClaw Plugin 开发计划
+
+**文档版本**: 0.1.0
+**日期**: 2026-03-19
+**状态**: 开发中
+
+---
+
+## 1. 概述
+
+本文档定义 HarborForge.OpenclawPlugin 的开发计划,以及 Backend 需要提供的接口支持。
+
+### 1.1 目标
+
+开发一个 OpenClaw 插件,将服务器遥测数据(系统指标 + OpenClaw 状态)实时传输到 HarborForge Monitor。
+
+### 1.2 架构关系
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ 远程服务器 (VPS) │
+│ ┌──────────────────────────────────────────────────────┐ │
+│ │ OpenClaw Gateway │ │
+│ │ ┌────────────────────────────────────────────────┐ │ │
+│ │ │ HarborForge.OpenclawPlugin │ │ │
+│ │ │ - 生命周期管理 (随 Gateway 启动/停止) │ │ │
+│ │ │ - 启动 sidecar 进程 │ │ │
+│ │ └────────────────────────────────────────────────┘ │ │
+│ │ │ │ │
+│ │ ▼ 启动/管理 │ │
+│ │ ┌────────────────────────────────────────────────┐ │ │
+│ │ │ Sidecar (独立 Node 进程) │ │ │
+│ │ │ - 收集系统指标 (CPU/内存/磁盘) │ │ │
+│ │ │ - 读取 OpenClaw 状态 (agents) │ │ │
+│ │ │ - HTTP/WebSocket 上报到 Monitor │ │ │
+│ │ └────────────────────────────────────────────────┘ │ │
+│ └──────────────────────────────────────────────────────┘ │
+└─────────────────────────────────────────────────────────────┘
+ │
+ │ HTTP / WebSocket
+ ▼
+ ┌─────────────────────────┐
+ │ HarborForge.Backend │
+ │ - /monitor/* 接口 │
+ │ - 数据存储 │
+ └─────────────────────────┘
+```
+
+---
+
+## 2. Backend 当前能力评估
+
+### 2.1 已实现接口 ✅
+
+| 接口 | 功能 | 完整度 | 说明 |
+|------|------|--------|------|
+| `GET /monitor/public/server-public-key` | 获取 RSA 公钥 | ✅ 100% | 用于插件加密 |
+| `POST /admin/servers` | 注册服务器 | ✅ 100% | 返回 server_id |
+| `POST /admin/servers/{id}/challenge` | 生成 challenge | ✅ 100% | 10分钟有效期 |
+| `WS /monitor/server/ws` | WebSocket 连接 | ✅ 100% | 完整验证逻辑 |
+| `POST /monitor/server/heartbeat` | HTTP 心跳 | ⚠️ 50% | 缺少安全验证 |
+
+### 2.2 当前 HTTP Heartbeat 问题 🔴
+
+```python
+# 当前实现 (app/api/routers/monitor.py:191-207)
+@router.post('/server/heartbeat')
+def server_heartbeat(payload: ServerHeartbeat, db: Session = Depends(get_db)):
+ server = db.query(MonitoredServer).filter(
+ MonitoredServer.identifier == payload.identifier
+ ).first()
+ # 问题:只验证 identifier 存在,不验证 challenge!
+ # 任何人知道 identifier 就可以伪造数据
+```
+
+**对比 WebSocket 实现**:
+```python
+# WebSocket 有完整验证
+ch = db.query(ServerChallenge).filter(
+ ServerChallenge.challenge_uuid == challenge_uuid,
+ ServerChallenge.server_id == server.id
+).first()
+if not ch or ch.used_at is not None or ch.expires_at < now():
+ await websocket.close(code=4401) # 验证失败
+```
+
+---
+
+## 3. Backend 需要补充的接口
+
+### 3.1 方案 A:增强 HTTP Heartbeat(推荐短期方案)
+
+添加 challenge_uuid 验证:
+
+```python
+@router.post('/server/heartbeat')
+def server_heartbeat(
+ payload: ServerHeartbeatSecure, # 包含 challenge_uuid
+ db: Session = Depends(get_db)
+):
+ # 1. 验证服务器
+ server = db.query(MonitoredServer).filter(...).first()
+ if not server:
+ raise HTTPException(404, 'unknown server')
+
+ # 2. 验证 challenge
+ ch = db.query(ServerChallenge).filter(
+ ServerChallenge.challenge_uuid == payload.challenge_uuid,
+ ServerChallenge.server_id == server.id
+ ).first()
+
+ if not ch or ch.expires_at < now():
+ raise HTTPException(401, 'invalid or expired challenge')
+
+ # 3. 存储数据...
+```
+
+**优点**: 与现有 WebSocket 验证逻辑一致
+**缺点**: Challenge 10分钟过期,需要定期重新注册
+
+### 3.2 方案 B:API Key 模式(推荐长期方案)
+
+添加长期有效的 API Key:
+
+```python
+# 1. 模型添加 api_key 字段
+class MonitoredServer(Base):
+ ...
+ api_key = Column(String(64), nullable=True, unique=True, index=True)
+
+# 2. 新增接口:生成/重置 API Key
+@router.post('/admin/servers/{id}/api-key')
+def generate_api_key(server_id: int, ...):
+ api_key = secrets.token_urlsafe(32)
+ # 存储并返回 (仅显示一次)
+
+# 3. 心跳接口验证 API Key
+@router.post('/server/heartbeat-v2')
+def server_heartbeat_v2(
+ payload: ServerHeartbeat,
+ x_api_key: str = Header(...),
+ db: Session = Depends(get_db)
+):
+ server = db.query(MonitoredServer).filter(
+ MonitoredServer.identifier == payload.identifier,
+ MonitoredServer.api_key == x_api_key
+ ).first()
+ if not server:
+ raise HTTPException(401, 'invalid credentials')
+```
+
+**优点**: 长期有效,适合自动化 Agent
+**缺点**: 需要新增数据库字段和接口
+
+### 3.3 方案 C:加密 Payload(最高安全)
+
+参考 WebSocket 的 encrypted_payload:
+
+```python
+@router.post('/server/heartbeat')
+def server_heartbeat(
+ encrypted_payload: str = Body(...), # RSA-OAEP 加密
+ db: Session = Depends(get_db)
+):
+ # 1. 解密
+ data = decrypt_payload_b64(encrypted_payload)
+
+ # 2. 验证时间戳 (防重放)
+ if not ts_within(data['ts'], max_minutes=10):
+ raise HTTPException(401, 'expired')
+
+ # 3. 验证 challenge
+ ch = db.query(ServerChallenge).filter(
+ challenge_uuid=data['challenge_uuid']
+ ).first()
+ ...
+```
+
+**优点**: 最高安全性
+**缺点**: 客户端实现复杂,需要 RSA 加密
+
+---
+
+## 4. OpenclawPlugin 开发计划
+
+### Phase 1: 基础功能开发(2-3天)
+
+**目标**: 可运行的基础版本(开发环境)
+
+| 任务 | 说明 | 依赖 |
+|------|------|------|
+| 1.1 Sidecar 基础架构 | Node.js 项目结构,配置加载 | 无 |
+| 1.2 系统指标收集 | CPU/内存/磁盘/运行时间 | 无 |
+| 1.3 OpenClaw 状态读取 | 读取 agents.json,版本信息 | 无 |
+| 1.4 HTTP 心跳上报 | 使用当前 /heartbeat 接口 | ⚠️ 不安全,仅开发 |
+| 1.5 Plugin 生命周期 | 随 Gateway 启动/停止 Sidecar | 无 |
+
+**验收标准**:
+- [ ] 可以收集系统指标
+- [ ] 可以上报到 Backend
+- [ ] 可以在 Monitor 面板看到数据
+
+### Phase 2: 安全增强(2-3天)
+
+**目标**: 生产环境可用的安全版本
+
+| 任务 | 说明 | 依赖 |
+|------|------|------|
+| 2.1 WebSocket 支持 | 实现 WS 连接和加密握手 | Backend WS 接口 ✅ |
+| 2.2 或:等待 HTTP 增强 | Backend 添加 challenge 验证 | Backend 更新 |
+| 2.3 重试/退避逻辑 | 连接失败时指数退避 | 无 |
+| 2.4 离线缓存 | 暂时存储,恢复后批量上报 | 无 |
+
+**验收标准**:
+- [ ] 连接需要验证(WebSocket 或增强 HTTP)
+- [ ] 网络中断后自动恢复
+- [ ] 数据不丢失
+
+### Phase 3: 生产就绪(1-2天)
+
+**目标**: 稳定可靠的监控系统
+
+| 任务 | 说明 | 依赖 |
+|------|------|------|
+| 3.1 日志和诊断 | 结构化日志,调试接口 | 无 |
+| 3.2 性能优化 | 减少资源占用 | 无 |
+| 3.3 安装脚本完善 | 参考 PaddedCell 格式 | 无 |
+| 3.4 文档编写 | 部署指南,故障排查 | 无 |
+
+**验收标准**:
+- [ ] 长时间稳定运行(7天+)
+- [ ] 资源占用 < 1% CPU,< 50MB 内存
+- [ ] 安装脚本一键部署
+
+---
+
+## 5. 接口规格详细定义
+
+### 5.1 当前可用接口
+
+#### GET /monitor/public/server-public-key
+```yaml
+Response:
+ public_key_pem: string # RSA 公钥 (PEM 格式)
+ key_id: string # 公钥指纹
+```
+
+#### POST /admin/servers
+```yaml
+Headers:
+ Authorization: Bearer {admin_token}
+Body:
+ identifier: string # 唯一标识 (如 "vps.t1")
+ display_name: string # 显示名称
+Response:
+ id: int
+ identifier: string
+ challenge_uuid: string # 10分钟有效
+ expires_at: ISO8601
+```
+
+#### WS /monitor/server/ws
+```yaml
+连接流程:
+ 1. Client -> Server: GET /monitor/server/ws (Upgrade)
+ 2. Client -> Server: {
+ "encrypted_payload": "base64_rsa_encrypted_json"
+ }
+ # 或明文(向后兼容):
+ # {
+ # "identifier": "vps.t1",
+ # "challenge_uuid": "...",
+ # "nonce": "...",
+ # "ts": "2026-03-19T14:00:00Z"
+ # }
+ 3. Server -> Client: { "ok": true, "server_id": 1 }
+ 4. Client -> Server: {
+ "event": "server.metrics",
+ "payload": { "cpu_pct": 12.5, "mem_pct": 41.2, ... }
+ }
+```
+
+#### POST /monitor/server/heartbeat(当前版本,不安全)
+```yaml
+Body:
+ identifier: string
+ openclaw_version: string
+ agents: [{id, name, status}]
+ cpu_pct: float
+ mem_pct: float
+ disk_pct: float
+ swap_pct: float
+Response:
+ ok: true
+ server_id: int
+ last_seen_at: ISO8601
+```
+
+### 5.2 建议新增接口
+
+#### POST /server/heartbeat-secure(增强版)
+```yaml
+Body:
+ identifier: string
+ challenge_uuid: string # 新增:必填
+ openclaw_version: string
+ agents: [...]
+ cpu_pct: float
+ mem_pct: float
+ disk_pct: float
+ swap_pct: float
+ timestamp: ISO8601 # 可选:防重放
+Response:
+ ok: true
+ server_id: int
+ last_seen_at: ISO8601
+ challenge_expires_at: ISO8601
+Error:
+ 401: { detail: "invalid or expired challenge" }
+```
+
+---
+
+## 6. 数据模型
+
+### 6.1 当前 Backend 模型
+
+```python
+# app/models/monitor.py
+
+class MonitoredServer:
+ id: int
+ identifier: str # 唯一标识
+ display_name: str
+ is_enabled: bool
+ created_by: int
+ created_at: datetime
+ # 建议添加:
+ # api_key: str # 长期有效的 API Key
+
+class ServerChallenge:
+ id: int
+ server_id: int
+ challenge_uuid: str # 10分钟有效
+ expires_at: datetime
+ used_at: datetime # 首次使用时间
+ created_at: datetime
+
+class ServerState:
+ id: int
+ server_id: int
+ openclaw_version: str
+ agents_json: str # JSON 序列化
+ cpu_pct: float
+ mem_pct: float
+ disk_pct: float
+ swap_pct: float
+ last_seen_at: datetime
+ updated_at: datetime
+```
+
+### 6.2 Plugin 配置模型
+
+```typescript
+// ~/.openclaw/openclaw.json
+{
+ "plugins": {
+ "harborforge-monitor": {
+ "enabled": true,
+ "backendUrl": "https://monitor.hangman-lab.top",
+ "identifier": "vps.t1", // 服务器标识
+ "challengeUuid": "uuid-here", // 从 /admin/servers/{id}/challenge 获取
+ "apiKey": "key-here", // 如果使用 API Key 模式(可选)
+ "reportIntervalSec": 30,
+ "httpFallbackIntervalSec": 60,
+ "logLevel": "info"
+ }
+ }
+}
+```
+
+---
+
+## 7. 开发时序图
+
+### 7.1 首次部署流程
+
+```mermaid
+sequenceDiagram
+ participant Admin
+ participant Backend
+ participant Plugin
+ participant Server as Server State
+
+ Admin->>Backend: POST /admin/servers
{identifier: "vps.t1"}
+ Backend->>Admin: {id: 1, identifier: "vps.t1"}
+
+ Admin->>Backend: POST /admin/servers/1/challenge
+ Backend->>Admin: {challenge_uuid: "abc-123", expires_at: "..."}
+
+ Admin->>Server: 配置 challenge_uuid
+
+ Note over Server: ~/.openclaw/openclaw.json
+
+ Server->>Backend: openclaw gateway restart
+
+ Plugin->>Backend: GET /monitor/public/server-public-key
+ Backend->>Plugin: {public_key_pem: "..."}
+
+ alt WebSocket 模式
+ Plugin->>Backend: WS /monitor/server/ws
+ Plugin->>Backend: {challenge_uuid, nonce, ts}
+ Backend->>Plugin: {ok: true}
+ loop 每 30 秒
+ Plugin->>Backend: {event: "server.metrics", payload: {...}}
+ end
+ else HTTP 模式
+ loop 每 30 秒
+ Plugin->>Backend: POST /server/heartbeat
{challenge_uuid, ...}
+ Backend->>Plugin: {ok: true}
+ end
+ end
+```
+
+### 7.2 数据上报格式
+
+```json
+{
+ "identifier": "vps.t1",
+ "challenge_uuid": "550e8400-e29b-41d4-a716-446655440000",
+ "timestamp": "2026-03-19T14:30:00Z",
+
+ "cpu_pct": 12.5,
+ "mem_pct": 41.2,
+ "mem_used_mb": 4096,
+ "mem_total_mb": 8192,
+ "disk_pct": 62.0,
+ "disk_used_gb": 500.5,
+ "disk_total_gb": 1000.0,
+ "swap_pct": 0.0,
+ "uptime_sec": 86400,
+ "load_avg_1m": 0.5,
+ "platform": "linux",
+ "hostname": "vps.t1",
+
+ "openclaw_version": "1.2.3",
+ "openclaw_agent_count": 2,
+ "openclaw_agents": [
+ {"id": "dev", "name": "Developer", "status": "running"},
+ {"id": "ops", "name": "Operator", "status": "idle"}
+ ]
+}
+```
+
+---
+
+## 8. 风险与缓解
+
+| 风险 | 影响 | 缓解措施 |
+|------|------|----------|
+| HTTP Heartbeat 无验证 | 数据伪造 | 使用 WebSocket 或等待 Backend 修复 |
+| Challenge 10分钟过期 | 需要频繁更新 | Backend 添加 API Key 模式 |
+| 网络中断 | 数据丢失 | Plugin 实现离线缓存 |
+| 资源占用过高 | 影响业务 | 控制采集频率,优化实现 |
+| Sidecar 崩溃 | 监控中断 | Plugin 自动重启 Sidecar |
+
+---
+
+## 9. 下一步行动
+
+### Backend 团队
+- [ ] 决定采用方案 A/B/C 增强 HTTP Heartbeat 安全
+- [ ] 实现 `/server/heartbeat-secure` 或增强现有接口
+- [ ] (可选)添加 API Key 支持
+
+### Plugin 开发团队
+- [ ] Phase 1: 基础功能开发(使用当前不安全 HTTP,仅开发测试)
+- [ ] Phase 2: 集成 WebSocket(立即可用,最安全)
+- [ ] 等待 Backend 更新后,切换到安全 HTTP
+
+---
+
+## 10. 参考文档
+
+- 原始设计文档: `docs/monitor-server-connector-plan.md`
+- Backend 代码: `app/api/routers/monitor.py`
+- Backend 模型: `app/models/monitor.py`
+- 加密服务: `app/services/crypto_box.py`
+- PaddedCell 安装脚本参考: `https://git.hangman-lab.top/nav/PaddedCell`
+
+---
+
+**文档维护者**: HarborForge Team
+**更新频率**: 随开发进度更新
diff --git a/docs/examples/monitor_heartbeat_secure.py b/docs/examples/monitor_heartbeat_secure.py
new file mode 100644
index 0000000..a4b4af2
--- /dev/null
+++ b/docs/examples/monitor_heartbeat_secure.py
@@ -0,0 +1,108 @@
+"""
+Backend 监控接口需要补充的安全验证代码
+添加到 app/api/routers/monitor.py
+"""
+
+from fastapi import Header
+
+class ServerHeartbeatSecure(BaseModel):
+ identifier: str
+ challenge_uuid: str # 新增:必须提供 challenge
+ openclaw_version: str | None = None
+ agents: List[dict] = []
+ cpu_pct: float | None = None
+ mem_pct: float | None = None
+ disk_pct: float | None = None
+ swap_pct: float | None = None
+
+
+@router.post('/server/heartbeat')
+def server_heartbeat(
+ payload: ServerHeartbeatSecure,
+ x_challenge_uuid: str = Header(..., description='Challenge UUID from registration'),
+ db: Session = Depends(get_db)
+):
+ """
+ 安全版本的心跳接口,验证 challenge_uuid
+ """
+ # 1. 验证服务器存在且启用
+ server = db.query(MonitoredServer).filter(
+ MonitoredServer.identifier == payload.identifier,
+ MonitoredServer.is_enabled == True
+ ).first()
+ if not server:
+ raise HTTPException(status_code=404, detail='unknown server identifier')
+
+ # 2. 验证 challenge_uuid 存在且有效
+ ch = db.query(ServerChallenge).filter(
+ ServerChallenge.challenge_uuid == x_challenge_uuid,
+ ServerChallenge.server_id == server.id
+ ).first()
+
+ if not ch:
+ raise HTTPException(status_code=401, detail='invalid challenge')
+
+ if ch.expires_at < datetime.now(timezone.utc):
+ raise HTTPException(status_code=401, detail='challenge expired')
+
+ # 3. 可选:检查 challenge 是否已被使用过
+ # 如果是首次验证,标记为已使用
+ if ch.used_at is None:
+ ch.used_at = datetime.now(timezone.utc)
+
+ # 4. 存储状态
+ st = db.query(ServerState).filter(ServerState.server_id == server.id).first()
+ if not st:
+ st = ServerState(server_id=server.id)
+ db.add(st)
+
+ st.openclaw_version = payload.openclaw_version
+ st.agents_json = json.dumps(payload.agents, ensure_ascii=False)
+ st.cpu_pct = payload.cpu_pct
+ st.mem_pct = payload.mem_pct
+ st.disk_pct = payload.disk_pct
+ st.swap_pct = payload.swap_pct
+ st.last_seen_at = datetime.now(timezone.utc)
+
+ db.commit()
+
+ return {
+ 'ok': True,
+ 'server_id': server.id,
+ 'last_seen_at': st.last_seen_at,
+ 'challenge_valid_until': ch.expires_at.isoformat()
+ }
+
+
+# 或者,如果需要长期有效的 API Key 方式:
+
+class ServerHeartbeatApiKey(BaseModel):
+ identifier: str
+ openclaw_version: str | None = None
+ agents: List[dict] = []
+ cpu_pct: float | None = None
+ mem_pct: float | None = None
+ disk_pct: float | None = None
+ swap_pct: float | None = None
+
+
+@router.post('/server/heartbeat-v2')
+def server_heartbeat_v2(
+ payload: ServerHeartbeatApiKey,
+ x_api_key: str = Header(..., description='Server API Key'),
+ db: Session = Depends(get_db)
+):
+ """
+ 使用 API Key 的心跳接口(长期有效,不需要 challenge)
+ 需要在 MonitoredServer 模型中添加 api_key 字段
+ """
+ server = db.query(MonitoredServer).filter(
+ MonitoredServer.identifier == payload.identifier,
+ MonitoredServer.is_enabled == True,
+ MonitoredServer.api_key == x_api_key # 需要添加 api_key 字段
+ ).first()
+
+ if not server:
+ raise HTTPException(status_code=401, detail='invalid identifier or api key')
+
+ # ... 存储状态 ...