1. 先说说那个让我抓狂的“三开”时刻
在动手写这篇分享之前,我想先描述一个场景,做运维的朋友应该秒懂:白天正在处理线上告警,服务器 CPU 飙高、接口超时、日志刷屏。我左手边开着终端窗口,连着跳板机再跳内网的一台机器敲命令;右手边开着记事本,里面存着一堆临时抄下来的报错关键字、IP 列表、上一次改过的配置项;屏幕角落还挂着和 AI 助手的聊天框,一边粘报错日志、一边问“这个异常一般是什么原因”“下一步怎么排查”。
三个窗口来回切,每次要 Alt+Tab 好几次。命令输完,切到记事本确认某个参数;再切回终端看输出;如果信息不够,又要去聊天框里追问,等回复的过程中还得切回终端继续敲。这种状态下最怕的就是切着切着忘记刚才执行到哪一步,尤其是查一个复杂问题的时候,中间上下文一断,前面的排查思路全乱了。
我不是没想过用现成的工具。终端类的工具很成熟,但只管连接服务器;AI 工具也很火,但只能聊天问答,拿不到我当前终端的实时状态,更不知道我本地记录里有哪些历史线索。记事本类工具就不用说了,和终端、AI 完全是断开的。工具之间互相不通信,真正干活的人反而是连接这三者的那位“中间件”——就是我本人。时间一长,我意识到问题不在工具本身,而在工具之间的割裂。所以我决定自己做一个能把这些事收到一起的东西:一台把 SSH 会话、终端操作记录、临时笔记、AI 排查能力打通的工作台。也就是这个项目的原型:Mini Assistant。
这篇内容我会从需求拆解、模块设计、核心实现几个方面说清楚我做了什么、踩了什么坑,顺便把一些可以直接抄走的思路整理出来。适合被“多窗口来回切”折磨的运维同学,也适合想给自己做一个专用工具、但不知道从哪入手的开发者。
2. 需求拆解:工作的痛点不只在“窗口多”
2.1 真实工作流里的隐性成本
很多人以为痛点只是“窗口多、切换烦”,实际去拆的时候会发现有三个隐性成本。
第一个是上下文断裂。排查一个线上问题时,我们在终端里敲过的命令、看到的报错、查过的配置、改过的参数,这些信息是分散在不同地方的。终端有输出的历史,记事本有零散记录,AI 对话里有问答结果。但没有任何一个地方能告诉你“这条命令是在什么背景下执行的”“这个笔记对应哪台机器、哪次会话”。一旦中断,恢复思路的时间成本往往比实际操作还高。
第二个是信息孤岛。记事本里的 IP 列表是静态的,它不会自动告诉你哪台机器现在连不上;AI 的回答是通用的,它不知道你当前终端的输出、不知道你在哪台机器上、不知道你刚执行过什么。你必须在聊天框里手动把上下文粘过去,粘得越完整,AI 的回答才越靠谱。但维护这套“人工上下文”本身就是负担。
第三个是切换造成的精力损耗。表面上只是几个快捷键的事,但在连续高强度排障时,频繁切换上下文,大脑需要重新定位“我在哪、我要干什么”。这个损耗是真的存在,一天处理十几个问题下来会异常疲惫。
2.2 Mini Assistant 的设计原则
基于这些痛点,我给自己定了三条设计原则。
第一,默认上下文,拒绝手动同步。终端会话、AI 问答、笔记必须共享一套上下文。比如当前会话连接的是哪台机器、最近执行过的命令是什么、当前目录在哪里——AI 在回答时默认知道这些,不需要我再粘一遍。
第二,记录优先,尽量不打断操作流。终端里执行过的命令、输出里的关键报错、你随手保存的片段,这些都要自动留痕。不是让你再开一个笔记页面去抄一遍,而是你在终端里操作的同时,工作台已经在后台为你做积累。
第三,轻量可用,先解决 80% 的问题。我不打算做一个大而全的重型平台。第一版能覆盖“SSH 连接、终端执行、自动记录、AI 问答”这四件事,就像一个随身工具一样靠谱,就足够在日常工作中产生实际价值了。
3. 核心模块拆分与工作流设计
3.1 四大模块怎么分
我最终把 Mini Assistant 拆成了四个模块:连接层、终端层、记录层、AI 层。
连接层负责 SSH 连接管理。你可以在工作台里保存多台机器的连接信息,支持密钥和口令两种认证方式,还预留了跳板机的支持。这一层解决的是“连接信息散落在记事本里”的问题——不用每次敲很长一串 ssh 命令,也不用到处找 IP 和账号。
终端层是一个内嵌的终端仿真组件。它负责渲染终端界面、处理输入输出、维护终端大小和滚动缓冲区。这一层解决的是“终端会话本身”的问题,让你在工作台里就能像在系统终端里一样操作。
记录层是工作台的记忆中枢。它自动收集当前会话的关键信息,比如连接的机器名、执行过的命令列表、某些输出中被你标记的片段,以及你手动保存的笔记。这一层解决的是“上下文断裂”问题,所有记录都按会话归档,和具体时间、机器关联在一起。
AI 层负责问答和辅助排查。它不只是一个聊天框,而是能主动读取当前会话上下文的智能助手:知道你在哪台机器上、最近执行了什么命令、有哪些历史笔记,然后在这个上下文基础上做分析建议。
整个工作流是这样的:打开工作台,选一台机器连接;在终端里操作,所有命令被记录;遇到问题,选中报错或直接问 AI;AI 根据当前上下文给出排查建议;如果发现有用的信息,一键存成笔记,笔记自动挂到当前会话下。全部操作在一个窗口里完成,三开变一开。
3.2 上下文如何共享
模块之间要协作,关键是上下文怎么设计。我这里用了一个贯穿全局的“会话对象”。每次建立 SSH 连接,就生成一个新的会话对象,它包含如下字段:
interface SessionContext { id: string host: string user: string connectedAt: number currentDir: string recentCommands: Array<{ cmd: string; time: number }> markedFragments: Array<string> notes: Array<{ content: string; createdAt: number }> aiHistory: Array<{ role: 'user' | 'assistant'; content: string }> }终端层执行命令时,会把命令追加到recentCommands;AI 层提问时,会从recentCommands里截取最近几条命令、读取当前目录、读取笔记列表,组装成上下文,一起发送给模型。记录层的笔记也挂在同一个会话对象上。这样一个会话就是一个完整的排障档案。
这个设计的好处在排障场景中非常明显。比如你在 A 机器上查一个问题,问了 AI 几个问题,然后切到 B 机器查另一个问题。两个会话的上下文完全隔离,不会互相污染。过几天回来看记录,你能看到当时排查问题的完整路径:连接了哪台机器、执行了哪些命令、问了哪些问题、存了哪些笔记。这对事后复盘非常有用。
4. 技术选型与关键实现过程
4.1 为什么选 Electron + xterm.js + SSH 协议
技术选型上我纠结过一段时间。最终选的组合是 Electron 做外壳,xterm.js 做终端渲染,SSH 协议的 Node 实现库做连接层。选这套组合的考量其实很直接。
桌面端要能真正调用本机能力,比如创建伪终端、处理密钥权限、读写本地文件,Web 页面的能力边界太受限。Electron 的好处是生态成熟、方案资料多,而且可以用 Node 层的原生能力,遇到问题容易找到解决方案。Electron 跨平台,对于我这种平时用系统终端比较多、偶尔要切到其他平台的人很重要。体积是大了点,但作为内部工具可以接受。
终端渲染用 xterm.js,它是目前浏览器里模拟终端最成熟的选择,对 ANSI 转义序列的支持很完整。配合node-pty可以在本机开一个伪终端,用它来跑命令。但这里有个关键点:我连接的是远程机器,不是本机 shell。所以最终链路变成:界面用 xterm.js 渲染,输入输出通过 SSH 连接传输到远程 shell。
链路简化后是这样的:
xterm.js(界面渲染) ↓ SSH 客户端库(网络连接,维护远程会话) ↓ 远程服务器 shell命令从界面输入后,走 SSH 连接传给远程 shell;远程输出回来后被 xterm.js 渲染成终端画面。这样一个完整的 SSH 终端就搭起来了。相比直接调用系统 ssh 命令,用库实现的好处是可以在应用内统一管理连接参数、密钥、跳板机,还能在命令输入输出的链路上插入记录逻辑。
连接认证上,第一版支持口令和密钥。密钥部分要注意,远程服务器对密钥文件权限要求很严格,建议在界面上直接处理。极速上手模式,我推荐用口令登录,省去权限配置的麻烦;日常使用再换成密钥。
4.2 SSH 通道与终端数据流实现
SSH 连接建立后,终端数据流是一段比较核心的代码。在 Electron 的主进程中,我用 ssh 客户端库建立一个连接,然后通过shell()方法打开一个会话通道。这个通道有两端:一端是远程 shell 的输入输出流,另一端通过 IPC 和渲染进程里的 xterm.js 对接。
xterm.js 的onData事件会拿到用户在终端里敲入的字符,这些字符需要原样发给远程 shell;远程 shell 返回的标准输出和标准错误,又会通过write方法交给 xterm.js 渲染。
// 主进程侧简化逻辑 term.onData(data => { sshStream.write(data) }) sshStream.onData(data => { webContents.send('terminal:output', data.toString('utf8')) }) sshStream.onClose(() => { webContents.send('terminal:closed') })这个流程看起来简单,但有几个细节很容易出问题。
第一是编码。远程服务器如果输出的是带中文的日志,必须保证 UTF-8 编码正确,否则会出现乱码。解决方案是在建立会话后,通过命令显式设置远程环境变量为 UTF-8,比如在 shell 初始化脚本里设置LANG=en_US.UTF-8或C.UTF-8。我踩过坑,曾经因为漏了这一步,日志里的中文全变成乱码,排查起来非常费劲。
第二是终端大小。xterm.js 渲染的终端有行数和列数,远程 shell 也需要知道这个大小。当窗口尺寸变化时,要发送stty rows N cols N给远程 shell,否则文本换行会错乱。有两个方法可以在终端 resize 时同步远程 shell 的窗口大小,我把这个逻辑写在了 resize 事件里。
第三是 RDP 式的会话重建。SSH 连接断开之后,终端状态会丢失,需要重连。重连之后,工作台会自动把最近历史命令展示出来,作为恢复参考,这样即使断线也不至于完全失去上下文。
4.3 记录与 AI 上下文的实现细节
记录层和 AI 层是 Mini Assistant 区别于普通终端工具的核心。记录层启动后,会自动拦截终端数据流里的命令。拦截方式是分析 xterm.js 收到的数据,当检测到回车符时,把当前一行输入提取出来,追加到会话对象的recentCommands里。这里不需要很复杂的解析,简单的按行切分就够用。
AI 层接入时,需要把上下文拼成提示词。我的做法是定义一个系统提示词,把会话信息填充进去:
你是运维助手。当前用户正在通过工作台连接 {host},用户是 {user}。 当前工作目录:{currentDir} 最近执行的命令: {commandList} 用户的临时笔记: {notes} 请基于以上上下文回答问题。如果用户粘贴了报错,先解释报错含义,再给出排查步骤。提问时,用户输入的原始问题会作为用户消息追加在后面。这样 AI 能结合真实操作状态给出建议,而不是脱离上下文的泛泛解答。实测下来,在排查问题时回答准确率提升明显。让 AI 知道“我刚刚执行了df -h,显示磁盘使用率 100%”,和单纯贴那句输出,效果天差地别。
上下文长度方面需要注意。模型对单次输入有 token 限制,recentCommands不能无限追加。我做了一个滑动窗口,只保留最近 20 条命令;如果命令输出太长,AI 层的 history 只会保留最近几轮对话摘要,避免 token 爆掉。如果你也做类似功能,建议提前定好“上下文压缩策略”,否则用一段时间就会遇到长度超标的问题。
5. 开发中的踩坑记录与排查思路
5.1 终端卡顿与渲染性能优化
第一版跑通后,我用它连着服务器敲了几十行命令,发现输出量大的时候,比如cat一个大日志文件或者find递归扫目录,界面会有明显卡顿,输入响应变慢。这个现象很影响操作体验。
排查后发现卡顿有几个原因。一是 xterm.js 的缓冲区刷得太频繁,日志量大时每帧重绘的任务过重。二是单行输出过长,导致渲染引擎做了大量计算。三是主进程和渲染进程之间的 IPC 消息发送次数太多,大量的输出数据频繁走 IPC,开销就上去了。
我的优化方式是:将输出数据做分批处理,不每收到一小段就立刻写入 xterm.js,而是累积到一定量(比如 16ms 一个批次)再一次性写入,这样减少渲染次数。同时对超长行做“截断显示”处理,只显示行首部分,完整内容存到缓冲区里。实际效果是从卡顿变成流畅滚动,操作感受提升了一大截。
5.2 SSH 断连与重连策略
断连是运维场景最常遇到的问题。闲置超过一定时间,服务器会主动断开连接;网络抖动也会导致连接中断。早期版本断连后终端直接卡在那里,任何输入都没反应,非常难受。
后来我加了心跳和自动重连机制。心跳是每隔一段时间发送一个空命令,或者检测 SSH 连接的 keepalive 信号,避免闲置超时。断连时,终端显示“连接已断开”状态,同时工作台保存当前会话上下文,等用户点击重连时自动恢复。重连成功后,把最近命令列表重新展示,用户能快速回到断开前的工作状态。这块建议做成“手动重连优先”,不要一味自动重连。有时候服务器已经出现异常,自动重连反而会有持续不断的连接尝试。
5.3 上下文自动记录的干扰问题
第二个版本加了自动记录命令后,我发现一个尴尬的情况:有些命令是敏感操作,比如rm -rf、修改生产配置、重启服务。如果所有命令都被自动记录,历史记录里混着这些高危操作,一方面是不安全,另一方面恢复上下文时也会造成干扰,分不清哪些是关键命令。
解决方案是加了一个“重要命令标记”功能。用户可以把某条命令标记为重要,标记后这条命令会单独归档,并在 AI 问答时被特别提到。这样自动记录仍然保留全部命令用于上下文恢复,但 AI 的注意力会集中在被标记的重要操作上。这个小改动听上去简单,实际用起来非常舒服,能有效减少日记噪音。
自动记录还有一个需要注意的隐私问题。工作台会保存每台机器的连接信息和操作记录,如果这台设备被其他人拿到,这些记录就暴露了。所以我把记录数据做了本地加密存储,主密码由用户首次启动时设置,之后每次启动需要输入。虽然多了一步操作,但换来的是安全性,很值。
5.4 常见问题速查表
我把开发过程中遇到的高频问题整理成了表格,给同行一个快速参考。不一定每个问题你都会碰到,但碰到了能省不少排查时间。
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 中文日志乱码 | 远程 shell 未启用 UTF-8 | 连接后执行export LANG=C.UTF-8,或在 shell 配置里设置 |
| 终端卡顿、输入延迟 | 输出数据一次性写入过多 | 对输出做分批写入,16ms 一批次,超长行截断显示 |
| 空闲后连接断开 | 服务器 keepalive 超时 | 增加 SSH 心跳包,检测 keepalive 信号 |
| 连接断开后无法恢复 | 未保存会话上下文 | 断连时保存 session 对象,重连时恢复 recentCommands |
| 密钥登录报权限错误 | 密钥文件权限过宽 | 将密钥权限改为 600,目录改为 700 |
| 命令历史被高危命令干扰 | 自动记录无筛选 | 增加重要命令标记,AI 只关注标记后的命令 |
| 内存占用越来越高 | 终端缓冲区无限增大 | 限制滚动缓冲区行数,超长输出做截断或丢弃 |
| 保存的连接信息丢失 | 存储文件损坏或未加密 | 存储采用 JSON + 加密文件,定期备份一份 |
5.5 多会话并行的体验优化
运维经常同时要看好几台机器。早期版本我只能在一个终端标签页里维持一个会话,要看另一台机器就要再开一个标签页,但标签页之间的会话上下文是独立的。如果 A 机器的报错和 B 机器有关联,AI 无法跨越两个会话一起分析。这其实又回到了信息孤岛。
后来我在会话对象里加了一个“关联”字段,允许用户手动把两个会话标记为关联。被关联的会话,AI 问答时会同时读取双方的上下文。比如 A 机器报错访问 B 机器超时,我可以把 A、B 两个会话关联在一起,然后问 AI:“A 访问 B 超时,两边最近的网络配置和连接状态一起看”,AI 就能把两边信息关联起来分析。这个功能设计得比较轻,但确实能解决多机排查时的上下文联动问题。
6. 一些你可能关心的实现议题与延伸思考
6.1 数据安全与权限边界
做这类工具,数据安全是绕不开的话题。因为工作台会自动记录命令、保存连接信息,这些数据一旦泄露后果很严重。我的做法是:本地存储的数据全部加密,默认不联网上传任何数据;AI 请求是唯一的外发行为,每次发起 AI 请求前会在界面上明确高亮提示“即将发送当前会话上下文到模型服务”,让用户确认。敏感机器可以做标记,标记后的机器不会自动发送上下文给 AI,只有用户显式选择时才会发送。如果你要做类似工具,建议默认保守,宁可多一次确认也不要自动外发敏感信息。
6.2 命令执行权限与高危操作引导
自动记录命令后,我还做了一件事:在输入rm、mkfs、reboot、shutdown这类高风险命令时,终端会弹出确认框,要求用户二次确认。确认内容显示完整命令和影响范围描述。这个功能不是强制拦截,而是提醒。线上事故很多都是“手快敲错了”。看起来是加了一次确认,实际上能挡住不少误操作。
6.3 跟现有工具链的衔接
有朋友问:这个工作台是不是要把我现有的终端工具完全替换掉?我的理解是未必。Mini Assistant 更像是一个“带上下文的运维入口”,适合排查复杂问题、记录排障过程、需要 AI 辅助分析的场景。日常快速登录一台机器跑一两个命令,用系统终端直接连也完全没问题。这两者不冲突,关键是看场景。我自己平时会按需切换,复杂问题用工作台,简单操作用系统终端。但最近我发现,用工作台的习惯一旦养成,切换的频率会越来越少,因为上下文沉淀的价值在高频操作场景里太明显了。
6.4 后期能扩展的方向
第一版做完后,我把精力放在了稳定性和日常使用的顺手程度上。后续如果要继续扩展,我最想做的几个方向大概是这样:一是把 AI 上下文从“最近命令”延伸到“系统状态”,比如自动采集top、df的摘要,让 AI 能感知运行趋势;二是增加批量执行能力,多台机器同一个命令并行执行,结果汇总展示;三是做一个“排障报告”导出功能,把一次完整排障过程自动汇总成报告,直接分享给同事或录入工单系统。这些方向都是顺着“上下文自动沉淀”这个核心思路延展的,不会跑偏。
我个人在实际使用中的体会是,这类工具最大的价值不是把三个窗口合成一个这么简单,而是它让操作、记录、分析这三件事开始围绕同一个上下文协作。以前是我在三个工具之间当中间人,现在工作台替我把这个担子接过去了,我只需要专注在当前的操作本身。踩过几次坑之后,我最大的心得是:做这类小工具,第一个版本不要贪多,把“连接、执行、记录、问答”这条主线打通就够用,剩下的功能等你在真实使用中感到痛了的时候,再一个个补上去,那种补法远比一开始就设计一堆功能要扎实得多。如果你也正被“多开窗口”折磨,想自己动手做个趁手的工具,顺着这条主线迭代,应该能少走不少弯路。