feat: integrate frpc manager and send dashboard info via WebSocket

Made-with: Cursor
This commit is contained in:
stswangzhiping
2026-03-15 11:10:33 +08:00
parent a9a7816e16
commit 516d0d26ee
2 changed files with 189 additions and 2 deletions

View File

@@ -2,8 +2,9 @@
const WebSocket = require('ws');
const config = require('./config');
const { getBoxId } = require('./fingerprint');
const { collect } = require('./metrics');
const { getBoxId } = require('./fingerprint');
const { collect } = require('./metrics');
const { getDashboardInfo, FrpcManager } = require('./frpc');
const MAX_BACKOFF_MS = 60_000;
@@ -15,16 +16,21 @@ class ClawClient {
this._hbTimer = null; // 心跳定时器
this._backoff = 1_000; // 重连等待ms
this._stopped = false;
this._frpc = new FrpcManager();
this._dashInfo = {}; // { dashboard_token, dashboard_port }
}
start() {
console.log(`[clawd] 启动中... 服务器 = ${this._cfg.server}`);
// 启动前提取 openclaw dashboard 信息(耗时操作放后台,不阻塞连接)
this._dashInfo = getDashboardInfo();
this._connect();
}
stop() {
this._stopped = true;
this._clearHeartbeat();
this._frpc.stop();
if (this._ws) this._ws.terminate();
console.log('[clawd] 已停止');
}
@@ -77,6 +83,8 @@ class ClawClient {
box_id: this._boxId,
claw_id: this._cfg.claw_id ?? null,
token: this._cfg.token ?? null,
// dashboard 信息可选openclaw 未安装时为空)
...this._dashInfo,
};
this._send(msg);
}
@@ -139,6 +147,13 @@ class ClawClient {
console.log(`[clawd] 已激活 claw_id = ${msg.claw_id}`);
}
// 启动 frpc如果 VPS 下发了 frp 配置)
if (msg.frp && msg.frp.server && msg.frp.auth_token) {
this._frpc.start(msg.claw_id, msg.frp).catch(e => {
console.error('[frpc] 启动失败:', e.message);
});
}
// 开始心跳
this._startHeartbeat();
}

172
lib/frpc.js Normal file
View File

@@ -0,0 +1,172 @@
'use strict';
const { execSync, spawn, execFileSync } = require('child_process');
const fs = require('fs');
const os = require('os');
const path = require('path');
const https = require('https');
// frpc 配置目录(与 clawd config 同目录)
const CONFIG_DIR = process.env.CLAWD_CONFIG_DIR
|| (process.getuid && process.getuid() === 0 ? '/etc/clawd' : path.join(os.homedir(), '.clawd'));
const FRPC_BIN = path.join(CONFIG_DIR, 'frpc');
const FRPC_CONFIG = path.join(CONFIG_DIR, 'frpc.toml');
// frp 版本
const FRP_VERSION = '0.62.0';
/**
* 提取 openclaw dashboard 的访问 token 和端口。
* 执行 `openclaw dashboard`,从输出中解析 Dashboard URL。
* 返回 { dashboard_token, dashboard_port } 或 {}(命令不存在/失败时)。
*/
function getDashboardInfo() {
try {
const out = execSync(
`openclaw dashboard 2>&1 | grep 'Dashboard URL' | sed -E 's|.*:([0-9]+)/.*#token=([a-f0-9]+).*|\\1 \\2|'`,
{ timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'] }
).toString().trim();
if (!out) return {};
const [portStr, token] = out.split(' ');
const port = parseInt(portStr, 10);
if (!token || isNaN(port)) return {};
console.log(`[frpc] openclaw dashboard: port=${port}, token=${token.substring(0, 8)}...`);
return { dashboard_port: port, dashboard_token: token };
} catch (e) {
// openclaw 未安装或命令失败,跳过
return {};
}
}
/**
* 根据当前系统架构下载对应的 frpc 二进制。
*/
async function downloadFrpc() {
const arch = os.arch(); // 'x64', 'arm64', 'arm', ...
const platform = os.platform(); // 'linux'
const archMap = {
x64: 'amd64', arm64: 'arm64',
arm: 'arm', ia32: '386',
};
const frpArch = archMap[arch] || 'amd64';
const filename = `frp_${FRP_VERSION}_${platform}_${frpArch}.tar.gz`;
const url = `https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/${filename}`;
const tmpFile = `/tmp/${filename}`;
console.log(`[frpc] 下载 frpc ${FRP_VERSION} (${platform}/${frpArch})...`);
await downloadFile(url, tmpFile);
// 解压并复制 frpc
fs.mkdirSync(CONFIG_DIR, { recursive: true });
execSync(`tar -xzf ${tmpFile} -C /tmp && cp /tmp/frp_${FRP_VERSION}_${platform}_${frpArch}/frpc ${FRPC_BIN}`, {
stdio: 'inherit'
});
fs.chmodSync(FRPC_BIN, 0o755);
console.log(`[frpc] frpc 已安装到 ${FRPC_BIN}`);
}
function downloadFile(url, dest) {
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(dest);
https.get(url, (res) => {
if (res.statusCode === 302 || res.statusCode === 301) {
file.close();
return downloadFile(res.headers.location, dest).then(resolve).catch(reject);
}
res.pipe(file);
file.on('finish', () => { file.close(); resolve(); });
}).on('error', (e) => { fs.unlink(dest, () => {}); reject(e); });
});
}
/**
* 生成 frpc.toml 配置文件。
*/
function writeFrpcConfig(clawId, frpConfig) {
const { server, port, auth_token, dashboard_local_port = 18789 } = frpConfig;
const toml = `# 由 clawd 自动生成,请勿手动修改
serverAddr = "${server}"
serverPort = ${port}
[auth]
method = "token"
token = "${auth_token}"
[[proxies]]
name = "dashboard-${clawId}"
type = "http"
localPort = ${dashboard_local_port}
subdomain = "${clawId}"
`;
fs.mkdirSync(CONFIG_DIR, { recursive: true });
fs.writeFileSync(FRPC_CONFIG, toml, 'utf8');
console.log(`[frpc] frpc.toml 已写入: subdomain=${clawId}, localPort=${dashboard_local_port}`);
}
class FrpcManager {
constructor() {
this._proc = null;
this._stopped = false;
this._restartTimer = null;
}
/**
* 启动 frpc如未安装先下载写配置然后 spawn。
*/
async start(clawId, frpConfig) {
this._stopped = false;
// 下载 frpc如果不存在
if (!fs.existsSync(FRPC_BIN)) {
try {
await downloadFrpc();
} catch (e) {
console.error('[frpc] 下载 frpc 失败:', e.message);
return;
}
}
writeFrpcConfig(clawId, frpConfig);
this._spawn();
}
_spawn() {
if (this._stopped) return;
console.log('[frpc] 启动 frpc...');
this._proc = spawn(FRPC_BIN, ['-c', FRPC_CONFIG], {
stdio: ['ignore', 'pipe', 'pipe'],
});
this._proc.stdout.on('data', d => {
const line = d.toString().trim();
if (line) console.log(`[frpc] ${line}`);
});
this._proc.stderr.on('data', d => {
const line = d.toString().trim();
if (line) console.warn(`[frpc] ${line}`);
});
this._proc.on('exit', (code) => {
console.warn(`[frpc] 进程退出 (code=${code})`);
if (!this._stopped) {
this._restartTimer = setTimeout(() => this._spawn(), 5000);
}
});
}
stop() {
this._stopped = true;
if (this._restartTimer) clearTimeout(this._restartTimer);
if (this._proc) {
this._proc.kill('SIGTERM');
this._proc = null;
}
}
}
module.exports = { getDashboardInfo, FrpcManager };