From c0c445b61df21f169a03510456fccc467cbe880d Mon Sep 17 00:00:00 2001 From: stswangzhiping <59632378+stswangzhiping@users.noreply.github.com> Date: Sun, 15 Mar 2026 21:39:05 +0800 Subject: [PATCH] feat: add ttyd terminal support, parallel startup in frpc.js Made-with: Cursor --- lib/client.js | 9 +++++-- lib/frpc.js | 73 ++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/lib/client.js b/lib/client.js index c388398..32e502d 100644 --- a/lib/client.js +++ b/lib/client.js @@ -4,7 +4,7 @@ const WebSocket = require('ws'); const config = require('./config'); const { getBoxId } = require('./fingerprint'); const { collect } = require('./metrics'); -const { getDashboardInfo, FrpcManager } = require('./frpc'); +const { getDashboardInfo, startTtyd, FrpcManager } = require('./frpc'); const MAX_BACKOFF_MS = 60_000; @@ -22,7 +22,12 @@ class ClawClient { async start() { 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(); } diff --git a/lib/frpc.js b/lib/frpc.js index 643a97e..ece4077 100644 --- a/lib/frpc.js +++ b/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')); const FRPC_BIN = path.join(CONFIG_DIR, 'frpc'); const FRPC_CONFIG = path.join(CONFIG_DIR, 'frpc.toml'); +const TTYD_BIN = path.join(CONFIG_DIR, 'ttyd'); -// frp 版本 -const FRP_VERSION = '0.62.0'; +// frp / ttyd 版本 +const FRP_VERSION = '0.62.0'; +const TTYD_VERSION = '1.7.7'; +const TTYD_PORT = 7681; /** * 启动 openclaw dashboard(后台运行),轮询日志文件等待 Dashboard URL 出现, @@ -86,6 +89,59 @@ async function downloadFrpc() { 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) { return new Promise((resolve, reject) => { const file = fs.createWriteStream(dest); @@ -102,6 +158,9 @@ function downloadFile(url, dest) { /** * 生成 frpc.toml 配置文件。 + * 包含两条代理: + * - dashboard-{clawId} → openclaw dashboard + * - tty-{clawId} → ttyd 终端 */ function writeFrpcConfig(clawId, frpConfig) { const { server, port, auth_token, dashboard_local_port = 18789 } = frpConfig; @@ -118,10 +177,16 @@ name = "dashboard-${clawId}" type = "http" localPort = ${dashboard_local_port} subdomain = "${clawId}" + +[[proxies]] +name = "tty-${clawId}" +type = "http" +localPort = ${TTYD_PORT} +subdomain = "tty-${clawId}" `; fs.mkdirSync(CONFIG_DIR, { recursive: true }); 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 { @@ -185,4 +250,4 @@ class FrpcManager { } } -module.exports = { getDashboardInfo, FrpcManager }; +module.exports = { getDashboardInfo, startTtyd, FrpcManager };