Files
clawd/lib/frpc.js
2026-03-15 14:29:03 +08:00

189 lines
5.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
const { execSync, spawn } = 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后台运行轮询日志文件等待 Dashboard URL 出现,
* 解析并返回 { dashboard_token, dashboard_port }。
* 超时10s或命令不存在时返回 {}。
*/
function getDashboardInfo() {
return new Promise((resolve) => {
const tmpLog = '/tmp/clawd-dashboard.log';
// 后台启动 dashboard输出重定向到日志文件
try {
execSync(`openclaw dashboard > ${tmpLog} 2>&1 &`, { shell: true, timeout: 3000 });
} catch (e) {
// 已在运行或命令不存在,继续轮询
}
// 每秒读一次日志文件,最多等 10 秒
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];
console.log(`[frpc] openclaw dashboard: port=${port}, token=${token.substring(0, 8)}...`);
resolve({ dashboard_port: port, dashboard_token: token });
return;
}
} catch (e) { /* 文件暂时不存在 */ }
if (attempts >= 10) {
clearInterval(interval);
resolve({});
}
}, 1000);
});
}
/**
* 根据当前系统架构下载对应的 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 };