224 lines
6.5 KiB
JavaScript
224 lines
6.5 KiB
JavaScript
'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 };
|