☰
浏览器里的真实终端?DSH-better-sidebar用xterm.js+node-pty实现真Shell的完整原理(含断线重连回放)
2026/9/25 15:18:36 网站建设 项目流程

浏览器里的真实终端?DSH-better-sidebar用xterm.js+node-pty实现真Shell的完整原理(含断线重连回放)

【免费下载链接】DSH-better-sidebar开放的侧边栏底座,支持三方拓展注册新侧边栏页面。内置文件渲染编辑/终端/侧边对话/Git/子代理页面 | Open sidebar foundation, supports third-party extensions to register new sidebar pages. Built-in file rendering/editing, terminal, side chat, Git, and sub-agent pages.项目地址: https://gitcode.com/gh_mirrors/ds/DSH-better-sidebar

DSH-better-sidebar 是一款 DSH 网页插件,它在浏览器侧边栏里内置了文件编辑、终端、侧边对话、Git 与子代理页面。其中的终端不是"模拟出来的假终端",而是基于xterm.js + node-pty + WebSocket实现的真实 Shell:页面刷新、切换会话、网络抖动之后还能重连回同一个进程,并把断线期间的历史输出完整回放。下面用最少代码量,完整讲清这条链路的原理。

为什么浏览器终端分"前后两半"

浏览器出于安全限制不能直接启动系统进程,所以"浏览器终端"必然拆成两半:

组件位置职责
node-pty服务端(Node 进程内)真正fork出 shell 进程,分配伪终端(pty),持有进程生命周期
WebSocket双向通道把键盘输入送到 pty,把 pty 输出流推回浏览器
xterm.js浏览器前端解析 ANSI 转义序列、渲染字符网格、处理光标与滚动

这个项目里,前后两半分别落在 src/client/TerminalView.tsx 与 src/pty-manager.ts、src/index.ts。理解终端的一切行为(包括断线重连),本质上就是理解这两半如何"续命"。

node-pty 端:如何在一个 Web 服务里开出真 Shell

Shell 解析链:配置 → 环境变量 → 登录 Shell

服务端并不是随便启动一个bash。src/pty-manager.ts 中的defaultShell()按以下优先级解析:

  1. 用户显式配置的shell(见 src/config.ts 的shell/shellArgs字段);
  2. POSIX 下的$SHELL环境变量;
  3. 系统账户的登录 Shell(比如 zsh);
  4. 兜底/bin/bash。

Windows 侧则是:DSH_SIDEBAR_SHELL→ PATH 或已知安装目录里的pwsh.exe(优先 PowerShell 7)→ 兜底powershell.exe。这样即使部署环境没设置SHELL,打开的仍然是用户本人的登录 Shell,而不是悄悄降级成 bash。

启动时还有一个容易被忽视的细节:POSIX 下 shell 以login shell(-l)方式拉起(shellSpawnArgs()),保证~/.profile、~/.zprofile等配置文件正常加载——这是"真 Shell"和"一次性命令行"的分水岭。

spawn-helper 权限修复

node-pty 的预编译二进制里带有一个spawn-helper辅助程序,pnpm 安装时会剥掉它的可执行位,导致所有终端都报posix_spawnp failed。src/pty-manager.ts 的ensureSpawnHelper()在插件激活时幂等地chmod 755修复它——这就是为什么安装脚本(scripts/install.sh)要专门处理构建权限。

PtyManager:终端会话的核心状态机

src/pty-manager.ts 中的PtyManager是整个终端的"调度中心",每个终端用sessionId:tabId作为注册表键,这决定了"重连"时如何找回同一个进程:

  • 每个会话最多 N 个并发进程:由terminalsPerSession配置(默认 3),超限直接返回pty-error;
  • 进程比连接活得久:WebSocket 断开不杀进程,只有"关标签页"或"插件卸载"才会真正kill;
  • 工作目录一致性:重连时如果解析到的权威cwd和进程当初启动的cwd不一致(页面加载竞态的产物),会直接重开一个进程,绝不让 Shell 停留在错误目录里。

三种断开方式,三种命运

这是理解断线重连的关键。终端视图卸载时,前端会区分三种情况发送不同的控制帧:

断开场景控制帧服务端行为
用户关闭标签页{type:'close'}立即释放进程(0ms 延迟)
用户切到其他会话{type:'park'}进程无限期挂起,不启动重连倒计时,切回来即续上
页面刷新 / 崩溃 / 插件重建无帧(裸断开)启动reconnectGraceMs重连宽限期,窗口内重连就复用原进程

为什么要有park帧?如果不区分"会话切换"和"页面崩溃",切换会话后那 30 秒宽限期一过就会把用户还在用的 Shell 杀掉。源码注释里把这叫做"错误地杀掉了用户正在用的 Shell"——正是 src/pty-manager.ts 花大段注释解释的动机。

WebSocket 线路协议:输入、缩放与回放

客户端连接的是/sidebar/ws/terminal(见 src/client/TerminalView.tsx),服务端在 src/index.ts 的attachTerminal()中接线,规则非常简洁:

  • 上行:普通文本帧 = 原始键盘输入;JSON 帧{type:'resize', cols, rows}= 调整 pty 尺寸;{type:'close'}/{type:'park'}= 生命周期控制;
  • 下行:纯字符串帧 = pty 原始输出流,xterm.js 直接write;
  • 背压保护:发送前检查ws.bufferedAmount < 4MB,慢连接不会把服务端内存撑爆;
  • 回放优先:attachTerminal()接通后第一件事是ws.send(handle.transcript)——先把历史缓冲区整段发过去,再订阅实时输出。这就是"断线回放"在协议层的落点。

断线重连回放:1MB 转录 + 30 秒宽限

回放能力来自 src/pty-manager.ts 中的有界转录环(transcript ring):

  • 每个终端自启动起持续累积 pty 输出;
  • 上限TRANSCRIPT_LIMIT = 1 << 20(1MB),超出时丢最旧的头部;
  • 即使 Shell 已退出,转录仍然保留可回放(你会看到[process exited with code N])。

配合默认30 秒重连宽限期(reconnectGraceMs,可在 src/config.ts 中调整),完整链路是:

  1. 刷新页面 → 前端 xterm.js 组件重建 → 自动重连(2 秒重试间隔);
  2. 服务端发现sessionId:tabId键对应的进程还活着 →不新开进程;
  3. 先整段回放 ≤1MB 的历史输出,再续接实时流;
  4. 若超过宽限期才回来,进程已被回收,则透明地启动一个新 Shell。

前端这一侧同样克制:连续无原因失败达到 3 次才弹错误横幅,服务端明确拒绝(close code 1011)则立即停止重试并显示原因,横幅永不空转(src/client/TerminalView.tsx)。

前端渲染:xterm.js 的几个关键配置

src/client/TerminalView.tsx 中的终端并非裸 xterm.js,有几个值得一提的细节:

  • 主题联动:16 色 ANSI 调色板与应用的代码高亮同源(one-dark / one-light,见 src/client/one-dark-palette.ts),明暗模式切换时原地重着色;
  • FitAddon + rAF 合帧:面板展开动画期间 ResizeObserver 每帧触发,fit()被合帧到每动画帧一次,避免测量抖动;
  • 4000 行 scrollback+ 服务端 1MB 转录,双保险覆盖常用回看范围;
  • URL 可点击:pty 输出里的 http(s) 链接支持 Ctrl+Click 打开(file://等方案被安全策略拒绝);
  • 零尺寸保护:容器高度为 0 时延迟open(),靠 xterm.js 的 WriteBuffer 缓冲期间写入,避免 WKWebView 下白屏崩溃。

降级模式:node-pty 不可用时的"软着陆"

node-pty 是原生模块,安装失败(pnpm 11 的 strict-dep-builds、预编译下载失败等)很常见。src/pty-deps.ts 的设计原则是:绝不静态 import node-pty——否则一次加载失败会拖垮整个 Web 服务。

改为惰性加载 + 缓存结果:加载失败时插件保持挂载,仅终端功能降级。前端收到 close code 1011 +pty-deps-missing标记后,通过/sidebar/api/terminal.deps拉取完整的可粘贴修复命令(scripts/install.sh 或 scripts/install.ps1 的--repair模式),横幅上一键复制、粘贴到本机执行即可修复。为什么不用 WS 直接带命令?因为 WS close reason 上限只有 123 字节——这条约束在注释里写得明明白白。

关键参数速查

参数默认值作用
terminalsPerSession3单会话最大并发终端数
reconnectGraceMs30000裸断开后进程存活宽限期(ms)
shell/shellArgs自动解析覆盖 Shell 及其启动参数(shellArgs非空时完全替换平台默认-l)
转录上限1MB每终端可回放的历史输出字节数

以上均可在 src/config.ts 的SidebarConfig中配置,更多细节见 README.md 与 docs/plans/2026-08-14-terminal-font-design.md、docs/plans/2026-08-26-pinned-terminal-design.md。

小结

DSH-better-sidebar 的浏览器终端之所以是"真 Shell",靠的是四件事各司其职:node-pty在服务端持有真实的进程与 pty;WebSocket以极简协议(原始输入 / resize / close / park)双向泵数据;xterm.js在前端完成 ANSI 渲染与自动重连;PtyManager用sessionId:tabId键 + 1MB 有界转录 + 30 秒宽限期,让"刷新页面"和"关掉标签页"有了截然不同的命运。想继续深挖,推荐直接读 src/pty-manager.ts 的头部注释——它把整个生命周期设计讲得比本文更细。

【免费下载链接】DSH-better-sidebar开放的侧边栏底座,支持三方拓展注册新侧边栏页面。内置文件渲染编辑/终端/侧边对话/Git/子代理页面 | Open sidebar foundation, supports third-party extensions to register new sidebar pages. Built-in file rendering/editing, terminal, side chat, Git, and sub-agent pages.项目地址: https://gitcode.com/gh_mirrors/ds/DSH-better-sidebar

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询