feat: integrate frpc manager and send dashboard info via WebSocket
Made-with: Cursor
This commit is contained in:
@@ -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
172
lib/frpc.js
Normal 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 };
|
||||
Reference in New Issue
Block a user