3 Commits

Author SHA1 Message Date
operator
248adfaafd fix: use runtime API for version and agent list instead of subprocess
Use api.runtime.version for openclaw version and
api.runtime.config.loadConfig() for agent list. Eliminates the
periodic openclaw agents list subprocess that caused high CPU usage.
2026-04-16 15:53:20 +00:00
operator
e4ac7b7af3 fix: disable periodic openclaw agents list subprocess
Spawning a full openclaw CLI process every 30s to list agents is too
heavy — each invocation loads all plugins (~16s) and hangs until killed.
Return empty array for now until a lighter mechanism is available.
2026-04-16 15:26:55 +00:00
operator
2088cd12b4 fix: use OPENCLAW_SERVICE_VERSION for real version and increase agent list timeout
api.version returns plugin API version (0.2.0), not the openclaw release
version. Use OPENCLAW_SERVICE_VERSION env var set by the gateway instead.
Also increase listOpenClawAgents timeout from 15s to 30s since plugin
loading takes ~16s on T2.
2026-04-16 15:12:35 +00:00
3 changed files with 107 additions and 175 deletions

192
README.md
View File

@@ -1,107 +1,84 @@
# HarborForge OpenClaw Plugin # HarborForge OpenClaw Plugin
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. OpenClaw 插件:向 HarborForge Monitor 暴露 OpenClaw 侧元数据,并提供可选的本地桥接能力;安装时也可顺带安装 `hf` CLI
Part of the [HarborForge](../README.md) platform. ## 当前状态
- Role: OpenClaw integration layer for HarborForge (registered plugin id: `harbor-forge`). - 插件注册名:`harbor-forge`
- Talks to the HarborForge backend (`backendUrl`, default `https://monitor.hangman-lab.top`) for Calendar APIs. - 旧 sidecar `server/` 架构已移除
- Talks to a local HarborForge.Monitor bridge over `127.0.0.1:<monitor_port>` (no fixed default; commonly `9100`). - 监控桥接走本地 `monitor_port`
- 安装脚本支持 `--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 # plugin manifest + config schema │ ├── openclaw.plugin.json
│ ├── index.ts # plugin entry, tool registration │ ├── index.ts
│ ├── tsconfig.json
│ ├── core/ │ ├── core/
│ │ ├── config.ts # config defaults / resolution │ │ ├── config.ts
│ │ ├── managed-monitor.ts # optionally spawn HarborForge.Monitor │ │ ├── managed-monitor.ts
│ │ ── monitor-bridge.ts # client for the Monitor bridge │ │ ── monitor-bridge.ts
│ │ └── 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 # installed only with --install-cli │ └── SKILL.md
└── scripts/ └── scripts/
└── install.mjs └── install.mjs
``` ```
## Installation ## 安装
### Standard Install ### 普通安装
```bash ```bash
node scripts/install.mjs node scripts/install.mjs
``` ```
This will: 这会:
- Build and install the OpenClaw plugin - 构建并安装 OpenClaw 插件
- Copy regular skills - 复制常规 skills
- **Not** install the `hf` binary - **不会**安装 `hf` 二进制
- **Not** copy `skills/hf/` - **不会**复制 `skills/hf/`
### Plugin + `hf` CLI ### 安装插件 + `hf` CLI
```bash ```bash
node scripts/install.mjs --install-cli node scripts/install.mjs --install-cli
``` ```
This additionally: 这会额外:
- Builds `HarborForge.Cli` (`go build ./cmd/hf`) - 构建 `HarborForge.Cli`
- Installs `hf` to `<openclaw>/bin/hf` (default `~/.openclaw/bin/hf`) and `chmod +x` - 安装 `hf` `~/.openclaw/bin/hf`
- Copies `skills/hf/` into the OpenClaw skills directory - `chmod +x ~/.openclaw/bin/hf`
- 复制 `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
# Install only, skip dependency checks # 指定 OpenClaw profile
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`. ## 配置
## Configuration 编辑 `~/.openclaw/openclaw.json`
Edit `~/.openclaw/openclaw.json`:
```json ```json
{ {
@@ -117,9 +94,7 @@ Edit `~/.openclaw/openclaw.json`:
"monitor_port": 9100, "monitor_port": 9100,
"reportIntervalSec": 30, "reportIntervalSec": 30,
"httpFallbackIntervalSec": 60, "httpFallbackIntervalSec": 60,
"logLevel": "info", "logLevel": "info"
"calendarEnabled": true,
"calendarHeartbeatIntervalSec": 60
} }
} }
} }
@@ -127,72 +102,51 @@ Edit `~/.openclaw/openclaw.json`:
} }
``` ```
Then restart: 然后重启:
```bash ```bash
openclaw gateway restart openclaw gateway restart
``` ```
## Config Options ## 配置项
| Option | Type | Default | Description | | 选项 | 类型 | 默认值 | 说明 |
|--------|------|---------|-------------| |------|------|--------|------|
| `enabled` | boolean | `true` | Enable the plugin | | `enabled` | boolean | `true` | 是否启用插件 |
| `backendUrl` | string | `https://monitor.hangman-lab.top` | HarborForge backend base URL (Monitor + Calendar APIs) | | `backendUrl` | string | `https://monitor.hangman-lab.top` | HarborForge Monitor 后端地址 |
| `identifier` | string | hostname | Server / claw identifier | | `identifier` | string | 主机名 | 服务器标识符 |
| `apiKey` | string | (none) | Server API key from the HarborForge Monitor admin panel | | `apiKey` | string | 无 | HarborForge Monitor 生成的服务器 API Key |
| `monitor_port` | number | (none) | Local bridge port; plugin talks to HarborForge.Monitor via `127.0.0.1:<monitor_port>` | | `monitor_port` | number | 无 | 本地桥接端口;插件通过 `127.0.0.1:<monitor_port>` 与 HarborForge.Monitor 通信 |
| `reportIntervalSec` | number | `30` | Metadata push interval (seconds) | | `reportIntervalSec` | number | `30` | 报告间隔(秒) |
| `httpFallbackIntervalSec` | number | `60` | HTTP heartbeat interval when WS unavailable | | `httpFallbackIntervalSec` | number | `60` | HTTP 回退间隔(秒) |
| `logLevel` | string | `info` | Log level: `debug` / `info` / `warn` / `error` | | `logLevel` | string | `info` | 日志级别:`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 ## 本地桥接说明
When the plugin has `monitor_port` configured and HarborForge.Monitor uses the same `MONITOR_PORT`: 当插件配置了 `monitor_port`,并且 HarborForge.Monitor 也使用相同的 `MONITOR_PORT` 时:
- Monitor serves a local bridge on `127.0.0.1:<MONITOR_PORT>` - Monitor `127.0.0.1:<MONITOR_PORT>` 提供本地桥接服务
- The plugin probes `GET /health` - 插件可探测 `GET /health`
- The plugin tool `harborforge_monitor_telemetry` reads `GET /telemetry` - 插件工具 `harborforge_monitor_telemetry` 可读取 `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
This link is an **optional enhancement**, not a precondition for the plugin to start or for Monitor heartbeats. 也就是说,这条链路是**可选增强**,不是插件启动或 Monitor 心跳的前置条件。
## Managed Monitor ## 插件提供的信息
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 元数据
- 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
@@ -200,16 +154,14 @@ 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+ (only for `--install-cli` / `--install-monitor`) - Go 1.20+(仅 `--install-cli` 需要)
## Tips ## 相关提示
- After installing `hf`, add `~/.openclaw/bin` to your `PATH` - 安装 `hf` 后,建议把 `~/.openclaw/bin` 加到 `PATH`
- When an agent uses `hf`, try `hf --help-brief` first - Agent 使用 `hf` 时,优先试 `hf --help-brief`
- For the full command tree, see `hf --help` - 完整命令树看 `hf --help`

View File

@@ -1,8 +1,3 @@
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;
@@ -14,70 +9,38 @@ 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[]> {
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 []; 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 = { current = { name: m?.[1] || line.slice(2).trim(), isDefault: m?.[2] === "default" };
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': case "Identity": current.identity = value; break;
current.identity = value; case "Workspace": current.workspace = value; break;
break; case "Agent dir": current.agentDir = value; break;
case 'Workspace': case "Model": current.model = value; break;
current.workspace = value; case "Routing rules": { const n = Number(value); current.routingRules = Number.isFinite(n) ? n : undefined; break; }
break; case "Routing": current.routing = value; break;
case 'Agent dir': default: break;
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 { listOpenClawAgents } from './core/openclaw-agents'; import type { OpenClawAgentInfo } 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,6 +32,12 @@ 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;
@@ -96,7 +102,7 @@ export default {
avg15: load[2], avg15: load[2],
}, },
openclaw: { openclaw: {
version: api.version || 'unknown', version: api.runtime?.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(),
@@ -118,10 +124,21 @@ 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.version || 'unknown', version: api.runtime?.version || api.version || 'unknown',
plugin_version: '0.3.1', plugin_version: '0.3.1',
agents: await listOpenClawAgents(logger), agents: agentNames.map(name => ({ name })),
}; };
const ok = await bridgeClient.pushOpenClawMeta(meta); const ok = await bridgeClient.pushOpenClawMeta(meta);