fix(monitor): harden server delete and remove challenge docs

- Delete server state before monitored server to avoid FK 500s
- Keep legacy cleanup for obsolete challenge tables
- Rewrite monitor docs to API key-only flow
This commit is contained in:
zhi
2026-03-20 08:02:19 +00:00
parent 8e0f158266
commit 9b5e2dc15c
3 changed files with 136 additions and 534 deletions

View File

@@ -5,6 +5,7 @@ from typing import List
from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi import APIRouter, Depends, Header, HTTPException, status
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import text
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.core.config import get_db from app.core.config import get_db
@@ -132,9 +133,20 @@ def delete_server(server_id: int, db: Session = Depends(get_db), _: models.User
obj = db.query(MonitoredServer).filter(MonitoredServer.id == server_id).first() obj = db.query(MonitoredServer).filter(MonitoredServer.id == server_id).first()
if not obj: if not obj:
raise HTTPException(status_code=404, detail='Server not found') raise HTTPException(status_code=404, detail='Server not found')
state = db.query(ServerState).filter(ServerState.server_id == server_id).first()
if state: # Delete dependent rows first to avoid FK errors.
db.delete(state) db.query(ServerState).filter(ServerState.server_id == server_id).delete(synchronize_session=False)
# Backward-compatible cleanup for deprecated challenge tables that may still exist in older DBs.
try:
db.execute(text('DELETE FROM server_handshake_nonces WHERE server_id = :server_id'), {'server_id': server_id})
except Exception:
pass
try:
db.execute(text('DELETE FROM server_challenges WHERE server_id = :server_id'), {'server_id': server_id})
except Exception:
pass
db.delete(obj) db.delete(obj)
db.commit() db.commit()
return None return None

View File

@@ -1,494 +1,76 @@
# OpenClaw Plugin 开发计划 # OpenClaw Plugin 开发计划(当前版)
**文档版本**: 0.1.0 **状态**: API Key 方案已落地challenge / WebSocket 旧方案已废弃。
**日期**: 2026-03-19
**状态**: 开发中
--- ## 当前架构
## 1. 概述 - HarborForge Monitor Backend 提供服务器注册与遥测接收接口
- OpenClaw Gateway 加载 `harborforge-monitor` 插件
- 插件在 `gateway_start` 时启动 sidecar (`server/telemetry.mjs`)
- sidecar 通过 **HTTP + X-API-Key** 向 Backend 上报遥测
本文档定义 HarborForge.OpenclawPlugin 的开发计划,以及 Backend 需要提供的接口支持。 ## 当前后端接口
### 1.1 目标 ### 公开接口
- `GET /monitor/public/overview`
开发一个 OpenClaw 插件,将服务器遥测数据(系统指标 + OpenClaw 状态)实时传输到 HarborForge Monitor。 ### 管理接口
- `GET /monitor/admin/servers`
- `POST /monitor/admin/servers`
- `DELETE /monitor/admin/servers/{id}`
- `POST /monitor/admin/servers/{id}/api-key`
- `DELETE /monitor/admin/servers/{id}/api-key`
### 1.2 架构关系 ### 插件上报接口
- `POST /monitor/server/heartbeat-v2`
- Header: `X-API-Key`
- Body:
- `identifier`
- `openclaw_version`
- `plugin_version`
- `agents`
- `cpu_pct`
- `mem_pct`
- `disk_pct`
- `swap_pct`
- `load_avg`
- `uptime_seconds`
``` ## 数据语义
┌─────────────────────────────────────────────────────────────┐
│ 远程服务器 (VPS) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ OpenClaw Gateway │ │
│ │ ┌────────────────────────────────────────────────┐ │ │
│ │ │ HarborForge.OpenclawPlugin │ │ │
│ │ │ - 生命周期管理 (随 Gateway 启动/停止) │ │ │
│ │ │ - 启动 sidecar 进程 │ │ │
│ │ └────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ 启动/管理 │ │
│ │ ┌────────────────────────────────────────────────┐ │ │
│ │ │ Sidecar (独立 Node 进程) │ │ │
│ │ │ - 收集系统指标 (CPU/内存/磁盘) │ │ │
│ │ │ - 读取 OpenClaw 状态 (agents) │ │ │
│ │ │ - HTTP/WebSocket 上报到 Monitor │ │ │
│ │ └────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ HTTP / WebSocket
┌─────────────────────────┐
│ HarborForge.Backend │
│ - /monitor/* 接口 │
│ - 数据存储 │
└─────────────────────────┘
```
--- - `openclaw_version`: 远程服务器上的 OpenClaw 版本
- `plugin_version`: 远程服务器上的 harborforge-monitor 插件版本
## 2. Backend 当前能力评估 ## 已废弃内容
### 2.1 已实现接口 ✅ 以下旧方案已经废弃,不再作为实现路径:
| 接口 | 功能 | 完整度 | 说明 | - challenge UUID
|------|------|--------|------| - `GET /monitor/public/server-public-key`
| `GET /monitor/public/server-public-key` | 获取 RSA 公钥 | ✅ 100% | 用于插件加密 | - `POST /monitor/admin/servers/{id}/challenge`
| `POST /admin/servers` | 注册服务器 | ✅ 100% | 返回 server_id | - `WS /monitor/server/ws`
| `POST /admin/servers/{id}/challenge` | 生成 challenge | ✅ 100% | 10分钟有效期 | - challenge / nonce 握手逻辑
| `WS /monitor/server/ws` | WebSocket 连接 | ✅ 100% | 完整验证逻辑 |
| `POST /monitor/server/heartbeat` | HTTP 心跳 | ⚠️ 50% | 缺少安全验证 |
### 2.2 当前 HTTP Heartbeat 问题 🔴 ## 前端管理页要求
```python Monitor 管理页应提供:
# 当前实现 (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 实现** - Add Server
```python - Generate API Key
# WebSocket 有完整验证 - Revoke API Key
ch = db.query(ServerChallenge).filter( - Delete Server
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) # 验证失败
```
--- 不再提供 `Generate Challenge`
## 3. Backend 需要补充的接口 ## 运行流程
### 3.1 方案 A增强 HTTP Heartbeat推荐短期方案 1. 管理员在 Monitor 中注册服务器
2. 管理员为服务器生成 API Key
3. 将 API Key 写入 `~/.openclaw/openclaw.json`
4. 重启 OpenClaw Gateway
5. 插件启动 sidecar
6. sidecar 定时向 `/monitor/server/heartbeat-v2` 上报
添加 challenge_uuid 验证: ## 备注
```python 当前保留了对旧 challenge 数据表的**删除兼容清理**(仅为兼容老数据库中的遗留数据),但不再保留 challenge 功能入口与运行时逻辑。
@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 方案 BAPI 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<br/>{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<br/>{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
**更新频率**: 随开发进度更新

View File

@@ -1,68 +1,76 @@
# OpenClaw Monitor Agent Plugin 开发计划(草案) # HarborForge Monitor / OpenClaw Plugin Connector Plan
## 目标 ## 目标
让被监测服务器通过 WebSocket 主动接入 HarborForge Backend并持续上报
- OpenClaw 版本
- agent 列表
- 每 5 分钟主机指标CPU/MEM/DISK/SWAP
- agent 状态变更事件
## 握手流程 使用 **API Key + HTTP heartbeat** 连接 HarborForge Monitor 与远程 OpenClaw 节点。
1. Admin 在 HarborForge 后台添加 server identifier
2. Admin 生成 challenge10 分钟有效)
3. 插件请求 `GET /monitor/public/server-public-key` 获取公钥
4. 插件构造 payload
- `identifier`
- `challenge_uuid`
- `nonce`(随机)
- `ts`ISO8601
5. 使用 RSA-OAEP(SHA256) 公钥加密base64 后作为 `encrypted_payload` 发给 `WS /monitor/server/ws`
6. 握手成功后进入事件上报通道
## 插件事件协议 ## 认证方式
### server.hello
- 管理员为服务器生成 API Key
- 插件通过 `X-API-Key` 调用 heartbeat 接口
- 不再使用 challenge / RSA 公钥 / WebSocket 握手
## 上报接口
`POST /monitor/server/heartbeat-v2`
### Headers
- `X-API-Key: <server-api-key>`
### Payload
```json ```json
{ {
"event": "server.hello", "identifier": "vps.t1",
"payload": { "openclaw_version": "OpenClaw 2026.3.13 (61d171a)",
"openclaw_version": "x.y.z", "plugin_version": "0.1.0",
"agents": [{"id": "a1", "name": "agent-1", "status": "idle"}] "agents": [
{ "id": "agent-bot1", "name": "agent-bot1", "status": "configured" }
],
"cpu_pct": 12.3,
"mem_pct": 45.6,
"disk_pct": 78.9,
"swap_pct": 0,
"load_avg": [0.12, 0.08, 0.03],
"uptime_seconds": 12345
}
```
## 语义
- `openclaw_version`: 远程主机上的 OpenClaw 版本
- `plugin_version`: harborforge-monitor 插件版本
## 插件生命周期
- 插件注册到 Gateway
-`gateway_start` 启动 `server/telemetry.mjs`
-`gateway_stop` 停止 sidecar
## 配置位置
`~/.openclaw/openclaw.json`
```json
{
"plugins": {
"entries": {
"harborforge-monitor": {
"enabled": true,
"config": {
"enabled": true,
"backendUrl": "http://127.0.0.1:8000",
"identifier": "vps.t1",
"apiKey": "your-api-key"
}
}
}
} }
} }
``` ```
### server.metrics每 5 分钟) ## 已废弃
```json
{
"event": "server.metrics",
"payload": {
"cpu_pct": 21.3,
"mem_pct": 42.1,
"disk_pct": 55.9,
"swap_pct": 0.0,
"agents": [{"id": "a1", "name": "agent-1", "status": "busy"}]
}
}
```
### agent.status_changed可选 - challenge UUID
```json - server public key
{ - WebSocket telemetry
"event": "agent.status_changed", - encrypted handshake payload
"payload": {
"agents": [{"id": "a1", "name": "agent-1", "status": "focus"}]
}
}
```
## 实施里程碑
- M1: Node/Python CLI 插件最小握手联通
- M2: 指标采集 + 周期上报
- M3: agent 状态采集与变更事件
- M4: 守护化systemd+ 断线重连 + 本地日志
## 风险与注意事项
- 时钟漂移会导致 `ts` 校验失败(建议 NTP
- challenge 仅一次可用,重复使用会被拒绝
- nonce 重放会被拒绝
- 需要保证插件本地安全保存 identifier/challenge短期