Files
clawd/lib/frpc.js
2026-05-02 18:07:33 +08:00

224 lines
6.5 KiB
JavaScript
Raw Permalink 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');
const log = require('./logger');
const { Watchdog } = require('./watchdog');
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');
const FRP_VERSION = '0.62.0';
const TTYD_PORT = 7681;
function findTtydBin() {
const candidates = ['/usr/bin/ttyd', '/usr/local/bin/ttyd', path.join(CONFIG_DIR, 'ttyd')];
for (const p of candidates) {
if (fs.existsSync(p)) return p;
}
return null;
}
/** openclaw 持久化配置JSON结构与原 YAML 解析结果一致。 */
const OPENCLAW_JSON_CANDIDATES = [
path.join(os.homedir(), '.openclaw', 'openclaw.json'),
'/home/sts/.openclaw/openclaw.json',
'/root/.openclaw/openclaw.json',
];
/**
* 解析到第一个存在的 openclaw.json 路径(与 dashboard / 联动更新共用)。
*/
function resolveOpenclawConfigFile() {
for (const p of OPENCLAW_JSON_CANDIDATES) {
try {
if (fs.existsSync(p)) return p;
} catch (_) { /* ignore */ }
}
return null;
}
/**
* 从 openclaw 配置文件中提取 dashboard token 和端口。
* openclaw 将配置持久化存储在 ~/.openclaw/openclaw.json 中,
* 直接读取比执行命令更可靠(不依赖 PATH、不需要进程启动等待
* systemd 服务的 ProtectHome=read-only 允许读取 /home 下的文件。
*/
function getDashboardInfo() {
for (const cfgPath of OPENCLAW_JSON_CANDIDATES) {
try {
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 });
}
} catch (_) { /* 文件不存在或格式错误,尝试下一个路径 */ }
}
log.debug('dashboard', 'openclaw 配置文件未找到或无 token跳过 dashboard 信息获取');
return Promise.resolve({});
}
async function downloadFrpc() {
const arch = os.arch();
const platform = os.platform();
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}`;
log.info('frpc', `下载 frpc ${FRP_VERSION} (${platform}/${frpArch})...`);
await downloadFile(url, tmpFile);
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);
log.info('frpc', `frpc 已安装到 ${FRPC_BIN}`);
}
/**
* 启动 ttyd由 install.sh 通过 apt 安装)。
* ttyd 绑定 127.0.0.1:7681供 frpc 代理。
*/
async function startTtyd() {
const ttydBin = findTtydBin();
if (!ttydBin) {
log.warn('ttyd', '未找到 ttyd请重新运行 install.sh');
return false;
}
// 终止所有 ttyd 进程,避免端口冲突
try {
execSync('pkill -x ttyd 2>/dev/null; true', { timeout: 3000, shell: true });
await new Promise(r => setTimeout(r, 500));
} catch (_) {}
try {
const ttydUser = process.env.CLAWD_TTY_USER || 'sts';
const ttyEnv = { ...process.env };
delete ttyEnv.NOTIFY_SOCKET;
const proc = spawn(ttydBin, ['-p', String(TTYD_PORT), '-i', '127.0.0.1', '-W', '-t', 'cursorBlink=true', '/bin/su', '-', ttydUser], {
stdio: 'ignore',
detached: true,
env: ttyEnv,
});
proc.unref();
log.info('ttyd', `已启动,端口 ${TTYD_PORT},用户=${ttydUser}bin=${ttydBin}`);
return true;
} catch (e) {
log.warn('ttyd', '启动失败:', e.message);
return false;
}
}
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); });
});
}
function writeFrpcConfig(clawId, frpConfig, sshSecretKey) {
const { auth_token, dashboard_local_port = 18789 } = frpConfig;
const ttyRemotePort = 10000 + Number(clawId);
const stcpBlock = sshSecretKey ? `
[[proxies]]
name = "ssh-${clawId}-secret"
type = "stcp"
secretKey = "${sshSecretKey}"
localPort = 22
` : '';
const toml = `# 由 clawd 自动生成,请勿手动修改
serverAddr = "frp.claw.cutos.ai"
serverPort = 443
[auth]
method = "token"
token = "${auth_token}"
[transport]
tls.enable = true
[[proxies]]
name = "dashboard-${clawId}"
type = "http"
localPort = ${dashboard_local_port}
subdomain = "${clawId}"
[[proxies]]
name = "tty-${clawId}"
type = "tcp"
localPort = ${TTYD_PORT}
remotePort = ${ttyRemotePort}
${stcpBlock}`;
fs.mkdirSync(CONFIG_DIR, { recursive: true });
fs.writeFileSync(FRPC_CONFIG, toml, 'utf8');
log.info('frpc', `frpc.toml 已写入: dashboard subdomain=${clawId}, tty tcp-port=${ttyRemotePort}${sshSecretKey ? ', ssh stcp=enabled' : ''}`);
}
/**
* FrpcManager —— 基于 Watchdog 的 frpc 进程管理器。
* 崩溃自动重启5 分钟内最多重启 10 次。
*/
class FrpcManager {
constructor() {
this._watchdog = null;
}
async start(clawId, frpConfig, sshSecretKey) {
this.stop();
if (!fs.existsSync(FRPC_BIN)) {
try {
await downloadFrpc();
} catch (e) {
log.error('frpc', '下载 frpc 失败:', e.message);
return;
}
}
writeFrpcConfig(clawId, frpConfig, sshSecretKey);
this._watchdog = new Watchdog('frpc', FRPC_BIN, ['-c', FRPC_CONFIG], {
maxRestarts: 10,
windowMs: 300_000,
restartDelay: 5_000,
});
this._watchdog.start();
}
stop() {
if (this._watchdog) {
this._watchdog.stop();
this._watchdog = null;
}
}
}
module.exports = { getDashboardInfo, resolveOpenclawConfigFile, startTtyd, FrpcManager };