diff --git a/lib/client.js b/lib/client.js index 3a572c8..c28f40f 100644 --- a/lib/client.js +++ b/lib/client.js @@ -6,7 +6,7 @@ const config = require('./config'); const log = require('./logger'); const { getBoxId } = require('./fingerprint'); const { collect } = require('./metrics'); -const { getDashboardInfo, startTtyd, FrpcManager } = require('./frpc'); +const { getDashboardInfo, startTtyd, FrpcManager } = require('./frpc'); // getDashboardInfo 也用于心跳中定期刷新 const { ProvisionManager } = require('./provisioning'); const { hasInternet } = require('./network'); @@ -30,6 +30,7 @@ class ClawClient { this._stopped = false; this._frpc = new FrpcManager(); this._dashInfo = {}; + this._hbCount = 0; // 心跳计数器,用于定期刷新 dashboard 信息 // WS 层活性检测 this._pingTimer = null; @@ -268,12 +269,24 @@ class ClawClient { async _sendHeartbeat() { if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return; try { + this._hbCount++; + + // 每 10 次心跳(约 5 分钟)刷新一次 dashboard 信息, + // 确保初次提取失败时能自动补偿,或在 token 变化后自动同步 + if (this._hbCount % 10 === 0) { + const freshInfo = await getDashboardInfo().catch(() => null); + if (freshInfo && Object.keys(freshInfo).length > 0) { + this._dashInfo = freshInfo; + } + } + const metrics = await collect(); this._send({ type: 'heartbeat', claw_id: this._cfg.claw_id, token: this._cfg.token, metrics, + ...this._dashInfo, // 携带 dashboard_token / dashboard_port,供 VPS 幂等更新 }); } catch (e) { log.error('clawd', '心跳发送失败:', e.message); diff --git a/lib/frpc.js b/lib/frpc.js index 1d4ed81..344e4c6 100644 --- a/lib/frpc.js +++ b/lib/frpc.js @@ -19,43 +19,33 @@ const TTYD_VERSION = '1.7.7'; const TTYD_PORT = 7681; /** - * 启动 openclaw dashboard(后台运行),轮询日志文件等待 Dashboard URL 出现, - * 解析并返回 { dashboard_token, dashboard_port }。 - * 超时(10s)或命令不存在时返回 {}。 + * 从 openclaw 配置文件中提取 dashboard token 和端口。 + * openclaw 将 gateway token 持久化存储在 ~/.openclaw/openclaw.json 中, + * 直接读取比执行命令更可靠(不依赖 PATH、不需要进程启动等待)。 + * systemd 服务的 ProtectHome=read-only 允许读取 /home 下的文件。 */ function getDashboardInfo() { - return new Promise((resolve) => { - const tmpLog = '/tmp/clawd-dashboard.log'; + const configCandidates = [ + path.join(os.homedir(), '.openclaw', 'openclaw.json'), + '/home/sts/.openclaw/openclaw.json', + '/root/.openclaw/openclaw.json', + ]; + for (const cfgPath of configCandidates) { try { - execSync(`openclaw dashboard > ${tmpLog} 2>&1 &`, { shell: true, timeout: 3000 }); - } catch (e) { - // 已在运行或命令不存在,继续轮询 - } - - let attempts = 0; - const interval = setInterval(() => { - attempts++; - try { - const content = fs.readFileSync(tmpLog, 'utf8'); - const match = content.match(/Dashboard URL:.*:(\d+)\/#token=([a-f0-9]+)/); - if (match) { - clearInterval(interval); - const port = parseInt(match[1], 10); - const token = match[2]; - log.info('dashboard', `openclaw dashboard: port=${port}, token=${token.substring(0, 8)}...`); - resolve({ dashboard_port: port, dashboard_token: token }); - return; - } - } catch (e) { /* 文件暂时不存在 */ } - - if (attempts >= 10) { - clearInterval(interval); - log.debug('dashboard', 'openclaw dashboard 未检测到,跳过'); - resolve({}); + const raw = fs.readFileSync(cfgPath, 'utf8'); + const config = JSON.parse(raw); + const token = config?.gateway?.auth?.token; + const port = config?.gateway?.port || 18789; + if (token) { + log.info('dashboard', `从配置文件读取: port=${port}, token=${token.substring(0, 8)}...`); + return Promise.resolve({ dashboard_port: port, dashboard_token: token }); } - }, 1000); - }); + } catch (_) { /* 文件不存在或格式错误,尝试下一个路径 */ } + } + + log.debug('dashboard', 'openclaw 配置文件未找到或无 token,跳过 dashboard 信息获取'); + return Promise.resolve({}); } async function downloadFrpc() {