feat: add ttyd terminal support, parallel startup in frpc.js
Made-with: Cursor
This commit is contained in:
@@ -4,7 +4,7 @@ const WebSocket = require('ws');
|
|||||||
const config = require('./config');
|
const config = require('./config');
|
||||||
const { getBoxId } = require('./fingerprint');
|
const { getBoxId } = require('./fingerprint');
|
||||||
const { collect } = require('./metrics');
|
const { collect } = require('./metrics');
|
||||||
const { getDashboardInfo, FrpcManager } = require('./frpc');
|
const { getDashboardInfo, startTtyd, FrpcManager } = require('./frpc');
|
||||||
|
|
||||||
const MAX_BACKOFF_MS = 60_000;
|
const MAX_BACKOFF_MS = 60_000;
|
||||||
|
|
||||||
@@ -22,7 +22,12 @@ class ClawClient {
|
|||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
console.log(`[clawd] 启动中... 服务器 = ${this._cfg.server}`);
|
console.log(`[clawd] 启动中... 服务器 = ${this._cfg.server}`);
|
||||||
this._dashInfo = await getDashboardInfo();
|
// 并行:获取 openclaw dashboard 信息 + 启动 ttyd
|
||||||
|
const [dashInfo] = await Promise.all([
|
||||||
|
getDashboardInfo(),
|
||||||
|
startTtyd().catch(e => console.warn('[ttyd] 启动失败:', e.message)),
|
||||||
|
]);
|
||||||
|
this._dashInfo = dashInfo || {};
|
||||||
this._connect();
|
this._connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
73
lib/frpc.js
73
lib/frpc.js
@@ -11,9 +11,12 @@ const CONFIG_DIR = process.env.CLAWD_CONFIG_DIR
|
|||||||
|| (process.getuid && process.getuid() === 0 ? '/etc/clawd' : path.join(os.homedir(), '.clawd'));
|
|| (process.getuid && process.getuid() === 0 ? '/etc/clawd' : path.join(os.homedir(), '.clawd'));
|
||||||
const FRPC_BIN = path.join(CONFIG_DIR, 'frpc');
|
const FRPC_BIN = path.join(CONFIG_DIR, 'frpc');
|
||||||
const FRPC_CONFIG = path.join(CONFIG_DIR, 'frpc.toml');
|
const FRPC_CONFIG = path.join(CONFIG_DIR, 'frpc.toml');
|
||||||
|
const TTYD_BIN = path.join(CONFIG_DIR, 'ttyd');
|
||||||
|
|
||||||
// frp 版本
|
// frp / ttyd 版本
|
||||||
const FRP_VERSION = '0.62.0';
|
const FRP_VERSION = '0.62.0';
|
||||||
|
const TTYD_VERSION = '1.7.7';
|
||||||
|
const TTYD_PORT = 7681;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 启动 openclaw dashboard(后台运行),轮询日志文件等待 Dashboard URL 出现,
|
* 启动 openclaw dashboard(后台运行),轮询日志文件等待 Dashboard URL 出现,
|
||||||
@@ -86,6 +89,59 @@ async function downloadFrpc() {
|
|||||||
console.log(`[frpc] frpc 已安装到 ${FRPC_BIN}`);
|
console.log(`[frpc] frpc 已安装到 ${FRPC_BIN}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载 ttyd 静态二进制。
|
||||||
|
*/
|
||||||
|
async function downloadTtyd() {
|
||||||
|
const arch = os.arch();
|
||||||
|
const archMap = { arm64: 'aarch64', x64: 'x86_64', arm: 'armv7l', ia32: 'i686' };
|
||||||
|
const ttydArch = archMap[arch] || 'x86_64';
|
||||||
|
const url = `https://github.com/tsl0922/ttyd/releases/download/${TTYD_VERSION}/ttyd.${ttydArch}`;
|
||||||
|
|
||||||
|
console.log(`[ttyd] 下载 ttyd ${TTYD_VERSION} (${ttydArch})...`);
|
||||||
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||||
|
await downloadFile(url, TTYD_BIN);
|
||||||
|
fs.chmodSync(TTYD_BIN, 0o755);
|
||||||
|
console.log(`[ttyd] ttyd 已安装到 ${TTYD_BIN}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动 ttyd(如未安装先下载)。
|
||||||
|
* ttyd 绑定 127.0.0.1:7681,供 frpc 代理。
|
||||||
|
* 返回 true 表示启动成功,false 表示失败。
|
||||||
|
*/
|
||||||
|
async function startTtyd() {
|
||||||
|
if (!fs.existsSync(TTYD_BIN)) {
|
||||||
|
try {
|
||||||
|
await downloadTtyd();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[ttyd] 下载失败:', e.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 终止旧进程(重启 clawd 时可能残留)
|
||||||
|
try {
|
||||||
|
execSync(`pkill -f "${TTYD_BIN}"`, { timeout: 3000 });
|
||||||
|
// 稍等旧进程退出
|
||||||
|
await new Promise(r => setTimeout(r, 500));
|
||||||
|
} catch (_) { /* 无进程可杀,忽略 */ }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const shell = fs.existsSync('/bin/bash') ? '/bin/bash' : '/bin/sh';
|
||||||
|
const proc = spawn(TTYD_BIN, ['-p', String(TTYD_PORT), shell], {
|
||||||
|
stdio: 'ignore',
|
||||||
|
detached: true,
|
||||||
|
});
|
||||||
|
proc.unref();
|
||||||
|
console.log(`[ttyd] 已启动,端口 ${TTYD_PORT},shell=${shell}`);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[ttyd] 启动失败:', e.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function downloadFile(url, dest) {
|
function downloadFile(url, dest) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const file = fs.createWriteStream(dest);
|
const file = fs.createWriteStream(dest);
|
||||||
@@ -102,6 +158,9 @@ function downloadFile(url, dest) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成 frpc.toml 配置文件。
|
* 生成 frpc.toml 配置文件。
|
||||||
|
* 包含两条代理:
|
||||||
|
* - dashboard-{clawId} → openclaw dashboard
|
||||||
|
* - tty-{clawId} → ttyd 终端
|
||||||
*/
|
*/
|
||||||
function writeFrpcConfig(clawId, frpConfig) {
|
function writeFrpcConfig(clawId, frpConfig) {
|
||||||
const { server, port, auth_token, dashboard_local_port = 18789 } = frpConfig;
|
const { server, port, auth_token, dashboard_local_port = 18789 } = frpConfig;
|
||||||
@@ -118,10 +177,16 @@ name = "dashboard-${clawId}"
|
|||||||
type = "http"
|
type = "http"
|
||||||
localPort = ${dashboard_local_port}
|
localPort = ${dashboard_local_port}
|
||||||
subdomain = "${clawId}"
|
subdomain = "${clawId}"
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "tty-${clawId}"
|
||||||
|
type = "http"
|
||||||
|
localPort = ${TTYD_PORT}
|
||||||
|
subdomain = "tty-${clawId}"
|
||||||
`;
|
`;
|
||||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||||
fs.writeFileSync(FRPC_CONFIG, toml, 'utf8');
|
fs.writeFileSync(FRPC_CONFIG, toml, 'utf8');
|
||||||
console.log(`[frpc] frpc.toml 已写入: subdomain=${clawId}, localPort=${dashboard_local_port}`);
|
console.log(`[frpc] frpc.toml 已写入: dashboard subdomain=${clawId}, tty subdomain=tty-${clawId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
class FrpcManager {
|
class FrpcManager {
|
||||||
@@ -185,4 +250,4 @@ class FrpcManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { getDashboardInfo, FrpcManager };
|
module.exports = { getDashboardInfo, startTtyd, FrpcManager };
|
||||||
|
|||||||
Reference in New Issue
Block a user