1 Commits

Author SHA1 Message Date
cc807484fc docs: refresh README — accuracy pass + HarborForge platform context
Verified against current code; fixed stale/inaccurate sections and
documented previously-undocumented features/flags/endpoints. Added a
"Part of the HarborForge platform" reference and role/port.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 17:50:01 +01:00
3 changed files with 175 additions and 107 deletions

192
README.md
View File

@@ -1,84 +1,107 @@
# HarborForge OpenClaw Plugin # HarborForge OpenClaw Plugin
OpenClaw 插件:向 HarborForge Monitor 暴露 OpenClaw 侧元数据,并提供可选的本地桥接能力;安装时也可顺带安装 `hf` CLI OpenClaw plugin that exposes OpenClaw-side metadata to the HarborForge Monitor, provides an optional local Monitor bridge, drives the HarborForge Calendar scheduler, and can optionally install the `hf` CLI.
## 当前状态 Part of the [HarborForge](../README.md) platform.
- 插件注册名:`harbor-forge` - Role: OpenClaw integration layer for HarborForge (registered plugin id: `harbor-forge`).
- 旧 sidecar `server/` 架构已移除 - Talks to the HarborForge backend (`backendUrl`, default `https://monitor.hangman-lab.top`) for Calendar APIs.
- 监控桥接走本地 `monitor_port` - Talks to a local HarborForge.Monitor bridge over `127.0.0.1:<monitor_port>` (no fixed default; commonly `9100`).
- 安装脚本支持 `--install-cli`
- `skills/hf/` 仅在 `--install-cli` 时一并安装
## 项目结构 ## Current State
- Plugin registration id: `harbor-forge` (was `harborforge-monitor`)
- Plugin version: `0.2.0` (package manifests); telemetry reports `pluginVersion` `0.3.1`
- Legacy sidecar `server/` architecture removed — telemetry is served directly by the plugin
- Monitor bridge runs over the local `monitor_port`
- Calendar scheduler integration (PLG-CAL-001 / 002 / 004)
- Installer supports `--install-cli` and an optional managed Monitor (`--install-monitor`)
- `skills/hf/` is installed only with `--install-cli`
## Project Structure
```text ```text
HarborForge.OpenclawPlugin/ HarborForge.OpenclawPlugin/
├── package.json ├── package.json
├── README.md ├── README.md
├── docs/ # design notes (calendar, monitor connector)
├── plugin/ ├── plugin/
│ ├── openclaw.plugin.json │ ├── openclaw.plugin.json # plugin manifest + config schema
│ ├── index.ts │ ├── index.ts # plugin entry, tool registration
│ ├── tsconfig.json
│ ├── core/ │ ├── core/
│ │ ├── config.ts │ │ ├── config.ts # config defaults / resolution
│ │ ├── managed-monitor.ts │ │ ├── managed-monitor.ts # optionally spawn HarborForge.Monitor
│ │ ── monitor-bridge.ts │ │ ── monitor-bridge.ts # client for the Monitor bridge
│ │ └── openclaw-agents.ts # enumerate OpenClaw agents
│ ├── calendar/ # Calendar scheduler + bridge
│ │ ├── index.ts
│ │ ├── scheduler.ts
│ │ ├── calendar-bridge.ts
│ │ └── types.ts
│ ├── hooks/ │ ├── hooks/
│ │ ├── gateway-start.ts │ │ ├── gateway-start.ts
│ │ └── gateway-stop.ts │ │ └── gateway-stop.ts
│ └── package.json │ └── package.json
├── skills/ ├── skills/
│ └── hf/ │ └── hf/
│ └── SKILL.md │ └── SKILL.md # installed only with --install-cli
└── scripts/ └── scripts/
└── install.mjs └── install.mjs
``` ```
## 安装 ## Installation
### 普通安装 ### Standard Install
```bash ```bash
node scripts/install.mjs node scripts/install.mjs
``` ```
这会: This will:
- 构建并安装 OpenClaw 插件 - Build and install the OpenClaw plugin
- 复制常规 skills - Copy regular skills
- **不会**安装 `hf` 二进制 - **Not** install the `hf` binary
- **不会**复制 `skills/hf/` - **Not** copy `skills/hf/`
### 安装插件 + `hf` CLI ### Plugin + `hf` CLI
```bash ```bash
node scripts/install.mjs --install-cli node scripts/install.mjs --install-cli
``` ```
这会额外: This additionally:
- 构建 `HarborForge.Cli` - Builds `HarborForge.Cli` (`go build ./cmd/hf`)
- 安装 `hf` `~/.openclaw/bin/hf` - Installs `hf` to `<openclaw>/bin/hf` (default `~/.openclaw/bin/hf`) and `chmod +x`
- `chmod +x ~/.openclaw/bin/hf` - Copies `skills/hf/` into the OpenClaw skills directory
- 复制 `skills/hf/` 到 OpenClaw profile skills 目录
### 常用选项 ### Common Options
```bash ```bash
# 仅构建 # Build only (no install / config)
node scripts/install.mjs --build-only node scripts/install.mjs --build-only
# 指定 OpenClaw profile # Install only, skip dependency checks
node scripts/install.mjs --skip-check
# Specify OpenClaw profile path (also honors OPENCLAW_PATH env)
node scripts/install.mjs --openclaw-profile-path /custom/path/.openclaw node scripts/install.mjs --openclaw-profile-path /custom/path/.openclaw
# 详细日志 # Build and install a managed HarborForge.Monitor binary alongside the plugin
node scripts/install.mjs --install-monitor yes --monitor-branch main
# Verbose logs
node scripts/install.mjs --verbose node scripts/install.mjs --verbose
# 卸载 # Uninstall (plugin, config entries, hf binary, managed monitor)
node scripts/install.mjs --uninstall node scripts/install.mjs --uninstall
``` ```
## 配置 The installer also updates OpenClaw config (`plugins.load.paths`, `plugins.allow`, `plugins.entries.harbor-forge.enabled`) via `openclaw config`.
编辑 `~/.openclaw/openclaw.json` ## Configuration
Edit `~/.openclaw/openclaw.json`:
```json ```json
{ {
@@ -94,7 +117,9 @@ node scripts/install.mjs --uninstall
"monitor_port": 9100, "monitor_port": 9100,
"reportIntervalSec": 30, "reportIntervalSec": 30,
"httpFallbackIntervalSec": 60, "httpFallbackIntervalSec": 60,
"logLevel": "info" "logLevel": "info",
"calendarEnabled": true,
"calendarHeartbeatIntervalSec": 60
} }
} }
} }
@@ -102,51 +127,72 @@ node scripts/install.mjs --uninstall
} }
``` ```
然后重启: Then restart:
```bash ```bash
openclaw gateway restart openclaw gateway restart
``` ```
## 配置项 ## Config Options
| 选项 | 类型 | 默认值 | 说明 | | Option | Type | Default | Description |
|------|------|--------|------| |--------|------|---------|-------------|
| `enabled` | boolean | `true` | 是否启用插件 | | `enabled` | boolean | `true` | Enable the plugin |
| `backendUrl` | string | `https://monitor.hangman-lab.top` | HarborForge Monitor 后端地址 | | `backendUrl` | string | `https://monitor.hangman-lab.top` | HarborForge backend base URL (Monitor + Calendar APIs) |
| `identifier` | string | 主机名 | 服务器标识符 | | `identifier` | string | hostname | Server / claw identifier |
| `apiKey` | string | 无 | HarborForge Monitor 生成的服务器 API Key | | `apiKey` | string | (none) | Server API key from the HarborForge Monitor admin panel |
| `monitor_port` | number | 无 | 本地桥接端口;插件通过 `127.0.0.1:<monitor_port>` 与 HarborForge.Monitor 通信 | | `monitor_port` | number | (none) | Local bridge port; plugin talks to HarborForge.Monitor via `127.0.0.1:<monitor_port>` |
| `reportIntervalSec` | number | `30` | 报告间隔(秒) | | `reportIntervalSec` | number | `30` | Metadata push interval (seconds) |
| `httpFallbackIntervalSec` | number | `60` | HTTP 回退间隔(秒) | | `httpFallbackIntervalSec` | number | `60` | HTTP heartbeat interval when WS unavailable |
| `logLevel` | string | `info` | 日志级别:`debug` / `info` / `warn` / `error` | | `logLevel` | string | `info` | Log level: `debug` / `info` / `warn` / `error` |
| `calendarEnabled` | boolean | `true` | Enable Calendar scheduler integration (PLG-CAL-001) |
| `calendarHeartbeatIntervalSec` | number | `60` | Calendar heartbeat interval (seconds) |
| `calendarApiKey` | string | (none) | API key for Calendar API auth; falls back to `apiKey` / `X-Agent-ID` |
| `managedMonitor` | string | (none) | Absolute path to a HarborForge.Monitor binary; if set, gateway start/stop hooks spawn/stop it |
## 本地桥接说明 ## Local Monitor Bridge
当插件配置了 `monitor_port`,并且 HarborForge.Monitor 也使用相同的 `MONITOR_PORT` 时: When the plugin has `monitor_port` configured and HarborForge.Monitor uses the same `MONITOR_PORT`:
- Monitor `127.0.0.1:<MONITOR_PORT>` 提供本地桥接服务 - Monitor serves a local bridge on `127.0.0.1:<MONITOR_PORT>`
- 插件可探测 `GET /health` - The plugin probes `GET /health`
- 插件工具 `harborforge_monitor_telemetry` 可读取 `GET /telemetry` - The plugin tool `harborforge_monitor_telemetry` reads `GET /telemetry`
- 如果桥接端口未配置或不可达,插件仍可正常运行 - The plugin pushes OpenClaw metadata (version, plugin version, agents) via `POST /openclaw` on the `reportIntervalSec` cadence, enriching Monitor heartbeats
- If the bridge port is unconfigured or unreachable, the plugin still works normally
也就是说,这条链路是**可选增强**,不是插件启动或 Monitor 心跳的前置条件。 This link is an **optional enhancement**, not a precondition for the plugin to start or for Monitor heartbeats.
## 插件提供的信息 ## Managed Monitor
### OpenClaw 元数据 If `managedMonitor` points to an installed HarborForge.Monitor binary, the `gateway_start` hook spawns it (passing `--backend-url`, `--identifier`, `--api-key`, `--monitor-port`, `--report-interval`, `--log-level` from the plugin config) and `gateway_stop` terminates it. Use `install.mjs --install-monitor yes` to build and wire this automatically.
- OpenClaw version
- plugin version
- 标识符 / 主机名
- 时间戳
### 系统快照 ## Calendar Scheduler
- uptime
- memory total/free/used/usagePercent
- load avg1/avg5/avg15
- platform
## 开发 When `calendarEnabled` is true, on gateway start the plugin starts a Calendar scheduler that heartbeats the backend (`/calendar/agent/heartbeat`, `/calendar/agent/status`, `/calendar/agent/notify`) to receive and run scheduled TimeSlots, waking/spawning agents via the OpenClaw `spawn` API (with a notification fallback). Scheduler state is persisted to a state file; gateway restarts can be requested by the backend (PLG-CAL-004).
## Tools Provided
| Tool | Description |
|------|-------------|
| `harborforge_status` | Plugin status, resolved config, Monitor bridge health, calendar status, telemetry snapshot |
| `harborforge_telemetry` | Current system telemetry snapshot from this host |
| `harborforge_monitor_telemetry` | Query the Monitor bridge for host hardware telemetry |
| `harborforge_calendar_status` | Calendar scheduler status and current slot |
| `harborforge_calendar_complete` | Complete the current calendar slot with actual duration |
| `harborforge_calendar_abort` | Abort the current calendar slot |
| `harborforge_calendar_pause` | Pause the current calendar slot |
| `harborforge_calendar_resume` | Resume the paused calendar slot |
| `harborforge_restart_status` | Check whether a gateway restart is pending |
### Telemetry Snapshot Fields
- `identifier`, `hostname`, `platform`, `timestamp`
- `uptime`
- `memory`: `total` / `free` / `used` / `usagePercent`
- `load`: `avg1` / `avg5` / `avg15`
- `openclaw`: `version` / `pluginVersion`
## Development
```bash ```bash
cd plugin cd plugin
@@ -154,14 +200,16 @@ npm install
npm run build npm run build
``` ```
## 依赖 The build runs `tsc` and emits `dist/` (`dist/index.js` is the plugin entry).
## Dependencies
- Node.js 18+ - Node.js 18+
- OpenClaw Gateway - OpenClaw Gateway
- Go 1.20+(仅 `--install-cli` 需要) - Go 1.20+ (only for `--install-cli` / `--install-monitor`)
## 相关提示 ## Tips
- 安装 `hf` 后,建议把 `~/.openclaw/bin` 加到 `PATH` - After installing `hf`, add `~/.openclaw/bin` to your `PATH`
- Agent 使用 `hf` 时,优先试 `hf --help-brief` - When an agent uses `hf`, try `hf --help-brief` first
- 完整命令树看 `hf --help` - For the full command tree, see `hf --help`

View File

@@ -1,3 +1,8 @@
import { execFile } from 'child_process';
import { promisify } from 'util';
const execFileAsync = promisify(execFile);
export interface OpenClawAgentInfo { export interface OpenClawAgentInfo {
name: string; name: string;
isDefault?: boolean; isDefault?: boolean;
@@ -9,38 +14,70 @@ export interface OpenClawAgentInfo {
routing?: string; routing?: string;
} }
export async function listOpenClawAgents(_logger?: { debug?: (...args: any[]) => void; warn?: (...args: any[]) => void }): Promise<OpenClawAgentInfo[]> { export async function listOpenClawAgents(logger?: { debug?: (...args: any[]) => void; warn?: (...args: any[]) => void }): Promise<OpenClawAgentInfo[]> {
return []; try {
const { stdout } = await execFileAsync('openclaw', ['agents', 'list'], {
timeout: 15000,
maxBuffer: 1024 * 1024,
});
return parseOpenClawAgents(stdout);
} catch (err) {
logger?.warn?.('Failed to run `openclaw agents list`', err);
return [];
}
} }
export function parseOpenClawAgents(text: string): OpenClawAgentInfo[] { export function parseOpenClawAgents(text: string): OpenClawAgentInfo[] {
const lines = text.split(/\r?\n/); const lines = text.split(/\r?\n/);
const out: OpenClawAgentInfo[] = []; const out: OpenClawAgentInfo[] = [];
let current: OpenClawAgentInfo | null = null; let current: OpenClawAgentInfo | null = null;
const push = () => { if (current) out.push(current); current = null; };
const push = () => {
if (current) out.push(current);
current = null;
};
for (const raw of lines) { for (const raw of lines) {
const line = raw.trimEnd(); const line = raw.trimEnd();
if (!line.trim() || line.startsWith("Agents:") || line.startsWith("Routing rules map") || line.startsWith("Channel status reflects")) continue; if (!line.trim() || line.startsWith('Agents:') || line.startsWith('Routing rules map') || line.startsWith('Channel status reflects')) continue;
if (line.startsWith("- ")) { if (line.startsWith('- ')) {
push(); push();
const m = line.match(/^-\s+(.+?)(?:\s+\((default)\))?$/); const m = line.match(/^-\s+(.+?)(?:\s+\((default)\))?$/);
current = { name: m?.[1] || line.slice(2).trim(), isDefault: m?.[2] === "default" }; current = {
name: m?.[1] || line.slice(2).trim(),
isDefault: m?.[2] === 'default',
};
continue; continue;
} }
if (!current) continue; if (!current) continue;
const trimmed = line.trim(); const trimmed = line.trim();
const idx = trimmed.indexOf(":"); const idx = trimmed.indexOf(':');
if (idx === -1) continue; if (idx === -1) continue;
const key = trimmed.slice(0, idx).trim(); const key = trimmed.slice(0, idx).trim();
const value = trimmed.slice(idx + 1).trim(); const value = trimmed.slice(idx + 1).trim();
switch (key) { switch (key) {
case "Identity": current.identity = value; break; case 'Identity':
case "Workspace": current.workspace = value; break; current.identity = value;
case "Agent dir": current.agentDir = value; break; break;
case "Model": current.model = value; break; case 'Workspace':
case "Routing rules": { const n = Number(value); current.routingRules = Number.isFinite(n) ? n : undefined; break; } current.workspace = value;
case "Routing": current.routing = value; break; break;
default: break; case 'Agent dir':
current.agentDir = value;
break;
case 'Model':
current.model = value;
break;
case 'Routing rules': {
const n = Number(value);
current.routingRules = Number.isFinite(n) ? n : undefined;
break;
}
case 'Routing':
current.routing = value;
break;
default:
break;
} }
} }
push(); push();

View File

@@ -14,7 +14,7 @@
import { hostname, freemem, totalmem, uptime, loadavg, platform } from 'os'; import { hostname, freemem, totalmem, uptime, loadavg, platform } from 'os';
import { getPluginConfig } from './core/config'; import { getPluginConfig } from './core/config';
import { MonitorBridgeClient, type OpenClawMeta } from './core/monitor-bridge'; import { MonitorBridgeClient, type OpenClawMeta } from './core/monitor-bridge';
import type { OpenClawAgentInfo } from './core/openclaw-agents'; import { listOpenClawAgents } from './core/openclaw-agents';
import { registerGatewayStartHook } from './hooks/gateway-start'; import { registerGatewayStartHook } from './hooks/gateway-start';
import { registerGatewayStopHook } from './hooks/gateway-stop'; import { registerGatewayStopHook } from './hooks/gateway-stop';
import { import {
@@ -32,12 +32,6 @@ interface PluginAPI {
warn: (...args: any[]) => void; warn: (...args: any[]) => void;
}; };
version?: string; version?: string;
runtime?: {
version?: string;
config?: {
loadConfig?: () => any;
};
};
config?: Record<string, unknown>; config?: Record<string, unknown>;
pluginConfig?: Record<string, unknown>; pluginConfig?: Record<string, unknown>;
on: (event: string, handler: () => void) => void; on: (event: string, handler: () => void) => void;
@@ -102,7 +96,7 @@ export default {
avg15: load[2], avg15: load[2],
}, },
openclaw: { openclaw: {
version: api.runtime?.version || api.version || 'unknown', version: api.version || 'unknown',
pluginVersion: '0.3.1', // Bumped for PLG-CAL-004 pluginVersion: '0.3.1', // Bumped for PLG-CAL-004
}, },
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
@@ -124,21 +118,10 @@ export default {
const bridgeClient = getBridgeClient(); const bridgeClient = getBridgeClient();
if (!bridgeClient) return; if (!bridgeClient) return;
let agentNames: string[] = [];
try {
const cfg = api.runtime?.config?.loadConfig?.();
const agentsList = cfg?.agents?.list;
if (Array.isArray(agentsList)) {
agentNames = agentsList
.map((a: any) => typeof a === 'string' ? a : a?.name)
.filter(Boolean);
}
} catch { /* non-fatal */ }
const meta: OpenClawMeta = { const meta: OpenClawMeta = {
version: api.runtime?.version || api.version || 'unknown', version: api.version || 'unknown',
plugin_version: '0.3.1', plugin_version: '0.3.1',
agents: agentNames.map(name => ({ name })), agents: await listOpenClawAgents(logger),
}; };
const ok = await bridgeClient.pushOpenClawMeta(meta); const ok = await bridgeClient.pushOpenClawMeta(meta);