1. Ternimal 不是另一个 SSH 客户端它重构了“终端会话”的所有权边界Ternimal 这个名字乍看像 terminal 的变体但它的核心动作——“手机扫码接管桌面终端会话”——已经彻底跳出了传统远程终端的思维框架。它不解决“怎么连上服务器”而是直击一个被长期忽视的痛点终端会话的控制权与可见性不该被锁死在单一物理设备上。我第一次用它时正卡在一个 CI 流水线的调试环节本地 IDE 里跑着 Python 脚本终端里开着docker logs -f实时观察容器输出同时还要切到浏览器查文档、回 Slack 回复同事。这时手机微信弹出一条消息对方发来一个二维码说“你看看这个日志片段是不是你改的”。我下意识掏出手机扫了一下——下一秒我的手机屏幕直接映射出我电脑上那个正在滚动的docker logs窗口而且我能点击、复制、甚至按 CtrlC 中断它。那一刻我才意识到Ternimal 接管的不是“连接”而是“会话本身”。这背后的技术逻辑非常干净它没有在客户端做任何 SSH 协议解析也不依赖服务端的 OpenSSH 配置。它基于xterm.js node-pty构建了一个轻量级的 Web 终端前端而真正的会话管理由 node-pty 在本地进程内创建并维持。也就是说你的bash或zsh进程从启动那一刻起就运行在你本机的一个独立 pty伪终端中Ternimal 只是把这个 pty 的输入/输出流通过 WebSocket 实时推送到任意扫码接入的设备。手机、平板、另一台电脑甚至智能电视——只要能打开网页、能扫码就能成为这个会话的“副屏”或“遥控器”。这和 TeamViewer 或 AnyDesk 的屏幕镜像有本质区别后者传输的是像素帧而 Ternimal 传输的是结构化的终端数据流ANSI 转义序列、光标位置、键盘事件带宽占用极低响应延迟几乎为零且天然支持复制粘贴、快捷键穿透比如你在手机上按 CtrlShiftV它真的会向原会话发送粘贴指令。关键词里的 “AI Agent 任务远程监督” 并非营销话术。当你的 AI Agent 正在后台执行一个耗时的自动化任务比如用 LangChain 调用多个 API 并聚合结果、用 LlamaIndex 构建知识图谱、或者跑一个 RAG pipeline 的完整评估你不需要守在电脑前盯着curl返回的 JSON。你可以让 Agent 启动一个专属的 Ternimal 会话把所有console.log、print()、进度条、错误堆栈都输出到那里然后你扫个码在通勤地铁上用手机实时查看任务状态、手动中断异常流程、甚至输入命令临时干预——整个过程就像你在操作自己电脑上的终端一样自然。这不是“远程监控”而是“会话共享”是把终端从一个独占式工具变成了一个可协作、可分发、可嵌入的计算上下文。这也是为什么它和 AI Agent 开发强相关Agent 的调试、可观测性、人工介入点都需要这种细粒度、低延迟、高保真的终端交互能力。2. 拆解 Ternimal 的三层架构为什么它能绕过 SSH 和 VNC 的历史包袱要真正理解 Ternimal 的价值必须拆开它的技术栈看清每一层的设计取舍。它不是凭空出现的黑盒而是对现有开源组件的一次精准缝合与逻辑重构。整个系统分为三个清晰的层次会话层Session Layer、传输层Transport Layer、呈现层Presentation Layer。这三层之间没有冗余耦合每层都可以独立替换或增强这也是它能快速适配 AI Agent 场景的关键。2.1 会话层node-pty 是真正的“会话引擎”而非“连接代理”绝大多数远程终端方案包括 VS Code 的 Remote-SSH、WebSSH都将重点放在“如何建立安全连接”上于是陷入 SSH 密钥管理、端口转发、防火墙配置的泥潭。Ternimal 则反其道而行之它默认只在本地启动 node-pty 实例。node-pty是一个 Node.js 的 C 扩展它能直接调用操作系统底层的forkpty()系统调用创建一个真实的伪终端对master/slave。这意味着当你在 Ternimal 界面里输入ls -la这个命令不是发给远端服务器而是直接交给本地bash进程执行pwd返回的是你本机当前工作目录git status读取的是你本机仓库的状态。它本质上是一个“本地终端的 Web 化外壳”。这个设计带来了三个决定性优势零配置启动无需生成密钥、无需修改/etc/ssh/sshd_config、无需开放 22 端口。npm run start启动后它自动监听http://localhost:3000生成一个一次性二维码。全权限继承本地终端拥有的所有权限文件系统访问、环境变量、GPU 设备节点、Docker socket全部继承。你可以直接在扫码后的手机界面上运行docker build -t myapp .它和你在电脑上敲的效果完全一致。会话生命周期自主会话的存续不依赖网络连接。即使你关掉手机页面、断开 WiFi本地的node-pty进程仍在后台运行ps aux | grep python依然能看到你的 Agent 进程。重新扫码会话状态包括命令历史、光标位置、未完成的vim编辑全部恢复。提示node-pty的跨平台兼容性极佳Windows 上通过 Windows Pseudo Console APImacOS 和 Linux 上通过标准 POSIX pty。但要注意某些需要root权限的操作如sudo apt update在扫码终端里仍会触发密码输入这是安全机制无法绕过。2.2 传输层WebSocket 是唯一协议JSON-RPC 是唯一信令Ternimal 放弃了 HTTP 长轮询、SSEServer-Sent Events等替代方案坚定选择 WebSocket 作为唯一的数据通道。这不是为了时髦而是由终端数据的特性决定的双向、实时、低延迟、小数据包高频次。一个ls命令的输出可能只有几 KB但光标闪烁、字符回显、按键响应要求毫秒级的往返时延。WebSocket 的全双工特性完美匹配。更关键的是它的信令协议自定义的轻量级 JSON-RPC。每次通信都是一个 JSON 对象包含id请求 ID用于响应匹配、method方法名如resize、write、key、params参数。例如当你在手机上按下字母aTernimal 前端会发送{id: 1, method: key, params: {key: a, ctrl: false, shift: false, alt: false}}后端收到后将a字符写入node-pty的 master fdbash进程立刻读取并处理。同样node-pty从 slave fd 读取到bash的输出比如userhost:~$ ls会打包成{id: 0, method: data, params: {data: \u001b[?25luserhost:~$ ls\r\n\u001b[?25h}}前端 xterm.js 解析 ANSI 序列\u001b[?25l隐藏光标和\u001b[?25h显示光标并渲染文本。整个过程没有 XML、没有 HTML 模板、没有 DOM 操作纯数据流驱动效率极高。注意Ternimal 默认不加密 WebSocket 通信即ws://而非wss://。这在本地开发环境是合理的因为localhost本身是可信的。但若需公网部署必须在反向代理如 Nginx层面启用 TLS并将wss://请求代理到后端ws://localhost:3000。切勿在应用层自行实现 TLS那会破坏 WebSocket 的性能优势。2.3 呈现层xterm.js 不是“渲染器”而是“终端协议解释器”很多人误以为 xterm.js 是一个“画字符的库”其实它是一个完整的VT100/VT220/ECMA-48 终端协议解释器。它不关心你连接的是 Linux、FreeBSD 还是 Windows Subsystem for LinuxWSL它只认 ANSI 转义序列。当你在终端里运行htophtop发送的不是“画一个绿色进度条”而是\u001b[32m\u001b[47m███████\u001b[0m这样的字符串。xterm.js 解析\u001b[32m设置前景色为绿色、\u001b[47m设置背景色为白色、\u001b[0m重置所有属性然后在 Canvas 上绘制对应颜色的像素块。这种协议级抽象让 Ternimal 天然支持所有符合 POSIX 标准的 CLI 工具vim、tmux、neovim、lazygit、fzf甚至nvidia-smi的 GPU 监控界面都能完美渲染无需任何额外适配。xterm.js 的另一个强大之处是它的扩展机制。Ternimal 利用addons加载了webLinks自动识别 URL 并可点击、searchCtrlF 全局搜索、unicode11支持 Emoji 和中文符号。更重要的是它预留了customKeyEventHandler钩子这正是 AI Agent 集成的入口——你可以拦截特定的组合键比如CtrlAltA不把它传给bash而是触发一个 JavaScript 函数调用你的 Agent API。3. AI Agent 监督实战从“盲跑”到“全程可视”的四步落地法把 Ternimal 和 AI Agent 结合不是简单地把 Agent 的日志输出到一个网页终端里而是构建一套完整的“人机协同调试闭环”。我经历过太多次 Agent 在后台静默失败它调用了一个返回 404 的 API但错误被try...catch吞掉只在日志里留下一行Error occurred或者它陷入了无限循环CPU 占用 100%但你根本不知道它卡在哪一行代码。Ternimal 的价值在于把这种“黑盒”变成“玻璃盒”。以下是我在三个真实项目中验证过的、可立即复用的四步落地法。3.1 第一步为 Agent 创建专属会话命名空间隔离干扰默认情况下Ternimal 启动后只有一个会话。但你的开发环境里可能同时运行着一个 LangChain Chain 的调试会话、一个本地 LLM 的ollama run llama3会话、一个数据库迁移脚本。如果所有输出都混在一个终端里信息就会爆炸。Ternimal 支持通过 URL 参数指定会话名称这是隔离的第一步。在启动 Agent 时不要直接node agent.js而是封装一个启动脚本start_agent.sh#!/bin/bash # 生成唯一会话ID基于时间戳进程PID SESSION_IDagent-$(date %s)-$$ # 启动 Ternimal 并指定会话名 npx ternimal --session $SESSION_ID --port 3001 TERNIMAL_PID$! # 启动 Agent并将其 stdout/stderr 重定向到 Ternimal 的 stdin/stdout node agent.js 21 | npx ternimal-client --session $SESSION_ID --url http://localhost:3001 # 清理Agent 结束后杀掉 Ternimal 进程 wait $! kill $TERNIMAL_PID 2/dev/null这里用到了ternimal-clientTernimal 官方提供的 CLI 工具它能将任意进程的输出流注入到指定名称的 Ternimal 会话中。这样你的手机扫码时URL 就是http://localhost:3001/?sessionagent-1717023456-12345看到的只有这个 Agent 的专属输出干净、聚焦。实操心得ternimal-client的--url参数必须指向 Ternimal 的 Web 服务地址通常是http://localhost:3001而不是 WebSocket 地址。它内部会自动建立 WebSocket 连接。如果 Agent 进程崩溃ternimal-client会自动退出不会留下僵尸连接。3.2 第二步在 Agent 代码中植入结构化日志与交互钩子仅仅输出console.log(Processing item...)是不够的。你需要让日志对人类和机器都友好。我推荐采用一种混合日志策略机器可读的 JSON 日志使用pino或winston将关键状态步骤开始/结束、API 调用、LLM 输入/输出 token 数以 JSON 格式输出到stderr。例如logger.info({ event: llm_call_start, model: llama3, input_tokens: 128, prompt: Summarize the following text... });人类可读的 ANSI 彩色日志使用chalk或kleur在stdout输出带颜色的进度提示便于手机端一眼识别状态。例如console.log(chalk.green.bold(✓) LLM response received (24 tokens)); console.log(chalk.yellow(⏳) Waiting for database write...);更重要的是在关键决策点插入人工确认钩子。比如当 Agent 计划执行一个高风险操作删除数据库记录、发送邮件、调用支付 API它不应该自动执行而是暂停并等待你的批准// 在 Agent 的决策链中 if (action.type delete_record) { console.log(chalk.red.bold(⚠️ CRITICAL ACTION DETECTED)); console.log(Will delete record with ID: ${action.id}); console.log(Type APPROVE to continue, or anything else to abort:); // 这里阻塞等待用户输入 const input await waitForUserInput(); // 自定义函数监听 stdin if (input.trim().toUpperCase() ! APPROVE) { throw new Error(Action aborted by user); } }waitForUserInput()的实现就是监听process.stdin。而process.stdin在 Ternimal 会话中就是扫码设备的键盘输入。你在手机上输入APPROVE并回车Agent 就继续执行。这个钩子把 AI 的“自主权”和人的“最终裁决权”无缝衔接。3.3 第三步利用 Ternimal 的 WebSocket API 实现双向控制Ternimal 的前端页面index.html暴露了一个全局的ternimal对象其中ternimal.ws就是底层的 WebSocket 实例。你可以通过它绕过 xterm.js 的 UI 层直接与会话进行编程式交互。这在 AI Agent 场景中极其有用。假设你的 Agent 正在运行一个长时间的ffmpeg视频转码任务。你想在手机上随时查看当前进度百分比或者强制终止它。你可以在 Ternimal 页面里注入一段自定义 JS!-- 在 Ternimal 的 index.html 末尾添加 -- script // 获取当前会话的 WebSocket 连接 const ws ternimal.ws; // 监听来自 Agent 的自定义事件通过特殊 ANSI 序列触发 ws.addEventListener(message, (event) { const data JSON.parse(event.data); if (data.method data) { const text data.params.data; // 检测 Agent 发送的进度标记例如[PROGRESS: 42%] const progressMatch text.match(/\[PROGRESS:\s*(\d)%\]/); if (progressMatch) { document.getElementById(progress-bar).style.width ${progressMatch[1]}%; document.getElementById(progress-text).textContent Progress: ${progressMatch[1]}%; } } }); // 提供一个手机端按钮用于发送终止信号 document.getElementById(stop-btn).addEventListener(click, () { // 发送一个特殊的 JSON-RPC 请求通知 Agent 终止 ws.send(JSON.stringify({ id: Date.now(), method: custom, params: { type: terminate, reason: user_request } })); }); /script在 Agent 代码中监听这个自定义事件// 在 Agent 的主循环中 process.stdin.on(data, (chunk) { const input chunk.toString().trim(); if (input.startsWith({) input.endsWith(})) { try { const cmd JSON.parse(input); if (cmd.type terminate) { console.log(chalk.red( Agent terminated by user.)); process.exit(0); } } catch (e) { // 忽略无效 JSON } } });这样你就拥有了一个完全定制化的“Agent 控制面板”而不仅仅是看日志。3.4 第四步构建会话快照与回溯分析能力一次成功的 AI Agent 任务往往涉及数十个 API 调用、数百行 LLM 输出、数 GB 的中间数据。当任务失败时你需要的不是“现在发生了什么”而是“刚才发生了什么”。Ternimal 本身不提供日志存储但它的数据流特性让它极易集成。最简单的方案是在ternimal-client启动时将输出同时 tee 到一个文件node agent.js 21 | tee /tmp/agent-session-$(date %Y%m%d-%H%M%S).log | npx ternimal-client --session $SESSION_ID --url http://localhost:3001但更好的方式是利用 Ternimal 的--log-file参数如果版本支持或者在 WebSocket 层做拦截。我推荐一个轻量级方案用一个 Node.js 的中间件监听node-pty的data事件并将原始数据含 ANSI 序列写入文件const fs require(fs); const { spawn } require(child_process); const pty require(node-pty); const shell pty.spawn(bash, [], { name: xterm-256color, cols: 100, rows: 40, cwd: process.env.HOME, env: process.env }); // 创建日志文件流 const logStream fs.createWriteStream(/tmp/agent-${Date.now()}.log, { flags: a }); // 拦截所有从 pty 读取的数据 pty.on(data, (data) { logStream.write(data); // 原始字节流保留 ANSI }); // 同时将数据转发给 Ternimal 的 WebSocket 服务 // ...此处省略 WebSocket 转发逻辑这个日志文件可以用cat查看原始内容也可以用less -R-R参数保留颜色进行彩色回放甚至可以用ansi-to-html工具转换成带样式的 HTML 报告发给团队成员复盘。这才是真正意义上的“可审计、可回溯”的 AI Agent 运行记录。4. 避坑指南那些官方文档不会告诉你的 7 个硬核细节Ternimal 的 README 写得简洁优雅但真实世界远比文档复杂。我在将它集成进生产级 AI Agent 平台时踩过不少坑有些甚至导致了线上任务的意外中断。以下是我总结的 7 个必须提前知道的硬核细节每一个都附带了实测验证的解决方案。4.1 坑一node-pty在 Docker 容器内默认无法工作报错Error: ENOENT: no such file or directory, open /dev/pts/0这是新手遇到的第一个拦路虎。当你把 Ternimal 打包进 Docker 镜像docker run -it my-ternimal-app启动后页面一片空白控制台报错找不到/dev/pts/0。原因在于node-pty需要访问宿主机的伪终端设备节点而默认的 Docker 容器是隔离的。解决方案启动容器时必须挂载/dev/pts目录并赋予--privileged权限或更精细的--cap-addSYS_ADMINdocker run -d \ --name ternimal \ -p 3000:3000 \ --cap-addSYS_ADMIN \ -v /dev/pts:/dev/pts \ -v /run/udev:/run/udev:ro \ my-ternimal-app注意--privileged权限过大不推荐在生产环境使用。--cap-addSYS_ADMIN是最小必要权限它允许容器调用forkpty()等系统调用。同时-v /dev/pts:/dev/pts是必须的否则node-pty无法创建新的 pts 设备。4.2 坑二手机扫码后键盘输入中文乱码显示为或这个问题在 iOS Safari 和部分安卓浏览器上高频出现。根本原因是xterm.js 默认的字体栈Courier New, monospace不包含中文字体而浏览器又未能正确 fallback 到系统中文字体。解决方案修改 Ternimal 的index.html在style标签中强制指定中文字体style #terminal { font-family: SF Mono, Segoe UI, Microsoft YaHei, PingFang SC, Hiragino Sans GB, WenQuanYi Micro Hei, monospace; } /style同时在xterm.js初始化时显式设置fontSize和fontFamilyconst term new Terminal({ fontSize: 14, fontFamily: SF Mono, Segoe UI, Microsoft YaHei, monospace, // ...其他配置 });实测心得Microsoft YaHei微软雅黑在 Windows 和大部分安卓设备上效果最好PingFang SC是 macOS 和 iOS 的系统字体WenQuanYi Micro Hei是 Linux 下的开源中文字体。这个字体栈能覆盖 99% 的设备。4.3 坑三tmux或vim在扫码终端里无法正常进入“模式”总是卡在普通模式tmux的CtrlB前缀、vim的Esc键在手机软键盘上没有物理Esc键导致用户无法退出插入模式。这是一个经典的终端兼容性问题。解决方案在 Ternimal 的前端代码中为软键盘映射虚拟Esc键。在index.html的script中添加// 为手机端添加虚拟 Esc 键 if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) { const escButton document.createElement(button); escButton.textContent ESC; escButton.style.cssText position:fixed;bottom:20px;right:20px;z-index:100;background:#ff6b6b;color:white;border:none;border-radius:50%;width:50px;height:50px;font-size:16px;; escButton.onclick () { ternimal.ws.send(JSON.stringify({ id: Date.now(), method: key, params: { key: \u001b, ctrl: false, shift: false, alt: false } })); }; document.body.appendChild(escButton); }\u001b就是 ASCII 的 ESC 字符27。点击这个按钮就等同于按下了物理Esc键vim立刻退出插入模式。4.4 坑四长按手机屏幕选择文本时光标定位错误复制的内容不完整xterm.js 的文本选择逻辑在移动端 touch 事件下有时会失准。用户想选中一行curl -X POST ...结果只选中了curl -X。解决方案禁用 xterm.js 的默认选择行为改用浏览器原生的document.execCommand(copy)。在Terminal初始化后添加term.onKey((e) { // 拦截 CtrlC改为触发浏览器复制 if (e.key \u0003 e.domEvent.ctrlKey) { e.domEvent.preventDefault(); document.execCommand(copy); } }); // 同时禁用 xterm.js 的鼠标选择 term.options.cursorBlink false; term.options.disableStdin false;提示document.execCommand(copy)在现代浏览器中已被废弃但它是目前移动端最可靠的复制方案。未来可迁移到navigator.clipboard.writeText()。4.5 坑五Agent 任务运行数小时后手机页面自动断开 WebSocket 连接显示Disconnected这是 WebSocket 的心跳超时问题。浏览器或中间代理如 Nginx、CDN会在一段时间通常是 60-90 秒没有数据交换时主动关闭空闲连接。解决方案在 Ternimal 后端添加 WebSocket 心跳保活。修改server.js在ws.on(connection)回调中ws.on(connection, (socket) { // 每 30 秒发送一次 ping const pingInterval setInterval(() { if (socket.readyState WebSocket.OPEN) { socket.send(JSON.stringify({ method: ping, id: Date.now() })); } }, 30000); socket.on(close, () { clearInterval(pingInterval); }); socket.on(message, (data) { const msg JSON.parse(data); if (msg.method pong) { // 忽略 pong 响应 return; } // 处理其他消息... }); });同时在前端index.html中监听ping并回复pongternimal.ws.onmessage function(event) { const data JSON.parse(event.data); if (data.method ping) { ternimal.ws.send(JSON.stringify({ method: pong, id: data.id })); } // 处理其他消息... };这样连接就永远不会因空闲而断开。4.6 坑六CtrlC在扫码终端里无法中断正在运行的python脚本脚本继续输出这是一个经典的信号传递问题。CtrlC在终端里发送的是SIGINT信号但node-pty默认不会将这个信号转发给子进程的进程组。解决方案在node-pty.spawn()时显式设置env和shell选项并确保子进程在自己的进程组中const pty require(node-pty); const shell pty.spawn(bash, [-i], { // -i 表示交互式 shell name: xterm-256color, cols: 100, rows: 40, cwd: process.env.HOME, env: process.env, // 关键启用 process group setEnv: true });更重要的是在 Agent 脚本中不要用child_process.exec()而要用child_process.spawn()并设置stdio: inherit// ❌ 错误exec 会创建 shell 子进程信号传递链断裂 const child exec(python long_task.py); // ✅ 正确spawn 直接创建子进程信号可直达 const child spawn(python, [long_task.py], { stdio: inherit // 继承父进程的 stdin/stdout/stderr });4.7 坑七多个用户同时扫码看到的是同一个会话但彼此输入会互相干扰Ternimal 默认是“广播模式”所有连接到同一会话的客户端共享同一个输入流。A 用户在手机上输入lsB 用户在平板上也会看到ls被执行。这在协作场景下是优点但在 AI Agent 监督场景下往往是灾难——你不想让同事的测试命令意外中断你的生产任务。解决方案启用 Ternimal 的--multi-user模式如果版本支持或者更通用的做法是为每个扫码用户创建独立的node-pty实例。修改后端逻辑不再复用一个 pty而是为每个 WebSocket 连接分配一个新 ptyws.on(connection, (socket) { // 为每个连接创建独立的 pty const pty require(node-pty).spawn(bash, [], { // ...配置 }); // 将 pty 的数据流与 socket 绑定 pty.on(data, (data) { socket.send(JSON.stringify({ method: data, params: { data } })); }); socket.on(message, (data) { const msg JSON.parse(data); if (msg.method key) { pty.write(msg.params.key); // 只写入这个用户的输入 } }); socket.on(close, () { pty.kill(); // 断开时清理专属 pty }); });这样每个扫码设备都拥有一个完全独立的终端会话副本互不干扰。代价是内存占用稍高但对于单个 Agent 任务来说完全可以接受。5. 从 Ternimal 到 AI Agent 工作流一个可立即部署的最小可行架构前面讲了原理、避坑、实战现在我们把它整合成一个可立即部署、开箱即用的最小可行架构MVP。这个架构不追求大而全而是聚焦于“让一个 Python 编写的 AI Agent能被手机扫码实时监督和干预”所有组件都选用最轻量、最易维护的方案总代码量不超过 200 行。5.1 架构图三层极简模型整个系统由三个核心组件构成它们之间通过标准协议通信松耦合易于替换[手机浏览器] ↓ (WebSocket, wss://your-domain.com/ternimal) [Ternimal Web Server] ←→ [Agent Process] ↑ (stdin/stdout/stderr pipe) [Node.js Runtime]Ternimal Web Server基于 Express node-pty 的轻量级服务负责创建 pty、管理 WebSocket 连接、提供扫码页面。它不包含任何 AI 逻辑。Agent Process一个独立的 Python 进程agent.py它执行业务逻辑将结构化日志输出到stdout/stderr并监听stdin等待人工指令。手机浏览器纯粹的客户端只负责展示和输入所有计算都在服务端完成。5.2 部署清单5 个文件10 分钟搞定文件 1package.jsonNode.js 服务依赖{ name: ternimal-agent-mvp, version: 1.0.0, main: server.js, scripts: { start: node server.js }, dependencies: { express: ^4.18.2, node-pty: ^1.0.0, ws: ^8.14.2, xterm: ^5.3.0 } }文件 2server.js核心服务const express require(express); const http require(http); const WebSocket require(ws); const pty require(node-pty); const path require(path); const app express(); const server http.createServer(app); const wss new WebSocket.Server({ server }); // 提供静态文件xterm.js 和页面 app.use(express.static(path.join(__dirname, public))); // WebSocket 连接处理 wss.on(connection, (ws, req) { // 为每个连接创建独立 pty const ptyProcess pty.spawn(bash, [], { name: xterm-256color, cols: 100, rows: 40, cwd: process.env.HOME, env: process.env }); // pty - WebSocket ptyProcess.on(data, (data) { ws.send(JSON.stringify({ method: data, params: { data } })); }); // WebSocket - pty ws.on(message, (data) { const msg JSON.parse(data); if (msg.method key) { ptyProcess.write(msg.params.key); } }); // 连接关闭时清理 ws.on(close, () { ptyProcess.kill(); }); }); // 启动服务 const PORT process.env.PORT || 3000; server.listen(PORT, () { console.log(Ternimal Agent MVP running on http://localhost:${PORT}); });文件 3public/index.html扫码页面!DOCTYPE html html head meta charsetutf-8 titleTernimal Agent Supervisor/title link relstylesheet hrefhttps://cdn.jsdelivr.net/npm/xterm5.3.0/lib/xterm.css style body { margin: 0; padding: 0; height: 100vh; overflow: hidden; } #terminal { height: 100%; } #qrcode { position: fixed; top: 20px; right
