diff --git a/README.md b/README.md index c3bd2b0..f754a0b 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ node scripts/install.mjs --verbose ## 配置 -1. 在 HarborForge Monitor 中注册服务器,获取 `challengeUuid` +1. 在 HarborForge Monitor 中注册服务器,获取 `apiKey` 2. 编辑 `~/.openclaw/openclaw.json`: @@ -87,7 +87,7 @@ node scripts/install.mjs --verbose "enabled": true, "backendUrl": "https://monitor.hangman-lab.top", "identifier": "my-server-01", - "challengeUuid": "your-challenge-uuid-here", + "apiKey": "your-api-key-here", "reportIntervalSec": 30, "httpFallbackIntervalSec": 60, "logLevel": "info" @@ -109,7 +109,7 @@ openclaw gateway restart | `enabled` | boolean | `true` | 是否启用插件 | | `backendUrl` | string | `https://monitor.hangman-lab.top` | Monitor 后端地址 | | `identifier` | string | 自动检测 hostname | 服务器标识符 | -| `challengeUuid` | string | 必填 | 注册挑战 UUID | +| `apiKey` | string | 可选 | API Key 认证(从 HarborForge Monitor 获取) | | `reportIntervalSec` | number | `30` | 报告间隔(秒) | | `httpFallbackIntervalSec` | number | `60` | HTTP 回退间隔(秒) | | `logLevel` | string | `"info"` | 日志级别: debug/info/warn/error | @@ -151,7 +151,7 @@ npm run build ```bash cd server -HF_MONITOR_CHALLENGE_UUID=test-uuid \ +HF_MONITOR_API_KEY=your-api-key \ HF_MONITOR_BACKEND_URL=http://localhost:8000 \ HF_MONITOR_LOG_LEVEL=debug \ node telemetry.mjs diff --git a/plugin/index.ts b/plugin/index.ts index 4d70382..080f93a 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -15,7 +15,7 @@ interface PluginConfig { enabled?: boolean; backendUrl?: string; identifier?: string; - challengeUuid?: string; + apiKey?: string; reportIntervalSec?: number; httpFallbackIntervalSec?: number; logLevel?: 'debug' | 'info' | 'warn' | 'error'; @@ -47,10 +47,9 @@ export default function register(api: PluginAPI, config: PluginConfig) { return; } - if (!config.challengeUuid) { - logger.error('Missing required config: challengeUuid'); - logger.error('Please register server in HarborForge Monitor first'); - return; + if (!config.apiKey) { + logger.warn('Missing config: apiKey'); + logger.warn('API authentication will be disabled. Generate apiKey from HarborForge Monitor admin.'); } const serverPath = join(__dirname, '..', 'server', 'telemetry.mjs'); @@ -74,7 +73,7 @@ export default function register(api: PluginAPI, config: PluginConfig) { ...process.env, HF_MONITOR_BACKEND_URL: config.backendUrl || 'https://monitor.hangman-lab.top', HF_MONITOR_IDENTIFIER: config.identifier || '', - HF_MONITOR_CHALLENGE_UUID: config.challengeUuid, + HF_MONITOR_API_KEY: config.apiKey || '', HF_MONITOR_REPORT_INTERVAL: String(config.reportIntervalSec || 30), HF_MONITOR_HTTP_FALLBACK_INTERVAL: String(config.httpFallbackIntervalSec || 60), HF_MONITOR_LOG_LEVEL: config.logLevel || 'info', diff --git a/scripts/install.mjs b/scripts/install.mjs index a66e003..32360c7 100644 --- a/scripts/install.mjs +++ b/scripts/install.mjs @@ -240,8 +240,8 @@ async function configure() { } logOk(`plugins.allow includes ${PLUGIN_NAME}`); - // Note: challengeUuid must be configured manually by user - logOk('Plugin configured (remember to set challengeUuid in ~/.openclaw/openclaw.json)'); + // Note: apiKey must be configured manually by user + logOk('Plugin configured (remember to set apiKey in ~/.openclaw/openclaw.json)'); } catch (err) { logWarn(`Config failed: ${err.message}`); @@ -262,13 +262,13 @@ function summary() { console.log(''); log('Next steps:', 'blue'); - log(' 1. Register server in HarborForge Monitor to get challengeUuid', 'cyan'); + log(' 1. Register server in HarborForge Monitor to get apiKey', 'cyan'); log(' 2. Edit ~/.openclaw/openclaw.json:', 'cyan'); log(' {', 'cyan'); log(' "plugins": {', 'cyan'); log(' "harborforge-monitor": {', 'cyan'); log(' "enabled": true,', 'cyan'); - log(' "challengeUuid": "your-challenge-uuid"', 'cyan'); + log(' "apiKey": "your-api-key"', 'cyan'); log(' }', 'cyan'); log(' }', 'cyan'); log(' }', 'cyan'); diff --git a/server/telemetry.mjs b/server/telemetry.mjs index 3434aac..85437e5 100644 --- a/server/telemetry.mjs +++ b/server/telemetry.mjs @@ -13,15 +13,18 @@ import { platform, hostname, freemem, totalmem, uptime } from 'os'; const execAsync = promisify(exec); // Config from environment (set by plugin) +const openclawPath = process.env.OPENCLAW_PATH || `${process.env.HOME}/.openclaw`; const CONFIG = { backendUrl: process.env.HF_MONITOR_BACKEND_URL || 'https://monitor.hangman-lab.top', identifier: process.env.HF_MONITOR_IDENTIFIER || hostname(), - challengeUuid: process.env.HF_MONITOR_CHALLENGE_UUID, + apiKey: process.env.HF_MONITOR_API_KEY, reportIntervalSec: parseInt(process.env.HF_MONITOR_REPORT_INTERVAL || '30', 10), httpFallbackIntervalSec: parseInt(process.env.HF_MONITOR_HTTP_FALLBACK_INTERVAL || '60', 10), logLevel: process.env.HF_MONITOR_LOG_LEVEL || 'info', - openclawPath: process.env.OPENCLAW_PATH || `${process.env.HOME}/.openclaw`, + openclawPath, openclawVersion: process.env.OPENCLAW_VERSION || 'unknown', + cachePath: process.env.HF_MONITOR_CACHE_PATH || `${openclawPath}/telemetry_cache.json`, + maxCacheSize: parseInt(process.env.HF_MONITOR_MAX_CACHE_SIZE || '100', 10), }; // Logging @@ -37,6 +40,95 @@ let wsConnection = null; let lastSuccessfulSend = null; let consecutiveFailures = 0; let isShuttingDown = false; +let offlineCache = []; + +/** + * Load offline cache from disk + */ +async function loadCache() { + try { + await access(CONFIG.cachePath, constants.R_OK); + const data = JSON.parse(await readFile(CONFIG.cachePath, 'utf8')); + offlineCache = Array.isArray(data) ? data : []; + log.info(`Loaded ${offlineCache.length} cached payloads from disk`); + } catch { + offlineCache = []; + } +} + +/** + * Save offline cache to disk + */ +async function saveCache() { + try { + const { writeFile } = await import('fs/promises'); + // Keep only the most recent entries + const toSave = offlineCache.slice(-CONFIG.maxCacheSize); + await writeFile(CONFIG.cachePath, JSON.stringify(toSave, null, 2)); + log.debug(`Saved ${toSave.length} payloads to cache`); + } catch (err) { + log.warn('Failed to save cache:', err.message); + } +} + +/** + * Add payload to offline cache + */ +async function addToCache(payload) { + offlineCache.push(payload); + // Trim to max size + if (offlineCache.length > CONFIG.maxCacheSize) { + offlineCache = offlineCache.slice(-CONFIG.maxCacheSize); + } + await saveCache(); +} + +/** + * Flush cached payloads to server + */ +async function flushCache() { + if (offlineCache.length === 0) { + return true; + } + + log.info(`Flushing ${offlineCache.length} cached payloads...`); + + const headers = { + 'Content-Type': 'application/json', + 'X-Server-Identifier': CONFIG.identifier, + }; + + if (CONFIG.apiKey) { + headers['X-API-Key'] = CONFIG.apiKey; + } + + let successCount = 0; + const toRetry = []; + + for (const payload of offlineCache) { + try { + const response = await fetch(`${CONFIG.backendUrl}/monitor/server/heartbeat-v2`, { + method: 'POST', + headers, + body: JSON.stringify(payload), + }); + + if (response.ok) { + successCount++; + } else { + toRetry.push(payload); + } + } catch { + toRetry.push(payload); + } + } + + offlineCache = toRetry; + await saveCache(); + + log.info(`Flushed ${successCount} of ${offlineCache.length + successCount} cached payloads`); + return successCount > 0; +} /** * Collect system metrics @@ -177,7 +269,6 @@ async function buildPayload() { return { identifier: CONFIG.identifier, - challenge_uuid: CONFIG.challengeUuid, timestamp: new Date().toISOString(), ...system, openclaw_version: openclaw.version, @@ -191,16 +282,27 @@ async function buildPayload() { */ async function sendHttpHeartbeat() { try { + // First, try to flush any cached data + if (offlineCache.length > 0) { + await flushCache(); + } + const payload = await buildPayload(); log.debug('Sending HTTP heartbeat...'); - const response = await fetch(`${CONFIG.backendUrl}/monitor/server/heartbeat`, { + const headers = { + 'Content-Type': 'application/json', + 'X-Server-Identifier': CONFIG.identifier, + }; + + // Add API Key authentication if configured + if (CONFIG.apiKey) { + headers['X-API-Key'] = CONFIG.apiKey; + } + + const response = await fetch(`${CONFIG.backendUrl}/monitor/server/heartbeat-v2`, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Server-Identifier': CONFIG.identifier, - 'X-Challenge-UUID': CONFIG.challengeUuid, - }, + headers, body: JSON.stringify(payload), }); @@ -215,6 +317,16 @@ async function sendHttpHeartbeat() { } catch (err) { log.error('HTTP heartbeat failed:', err.message); consecutiveFailures++; + + // Add current payload to cache on failure + try { + const payload = await buildPayload(); + await addToCache(payload); + log.info(`Added failed payload to cache (${offlineCache.length} total)`); + } catch (cacheErr) { + log.warn('Failed to cache payload:', cacheErr.message); + } + return false; } } @@ -268,11 +380,16 @@ log.info('Config:', { identifier: CONFIG.identifier, backendUrl: CONFIG.backendUrl, reportIntervalSec: CONFIG.reportIntervalSec, + hasApiKey: !!CONFIG.apiKey, + cachePath: CONFIG.cachePath, + maxCacheSize: CONFIG.maxCacheSize, }); -if (!CONFIG.challengeUuid) { - log.error('Missing HF_MONITOR_CHALLENGE_UUID environment variable'); - process.exit(1); +if (!CONFIG.apiKey) { + log.warn('Missing HF_MONITOR_API_KEY environment variable - API authentication will be disabled'); } +// Load offline cache +await loadCache(); + reportingLoop();