干运维这些年,我发现自己最常用的三样东西就是 SSH 客户端、记事本和聊天框。改配置的时候要开终端,记变更的时候要开文档,遇到拿不准的命令还得切到聊天工具去问,来回切窗口一天下来心态基本是崩的。后来我实在受不了,动手做了一款 AI 运维工作台 Mini Assistant,把 SSH 会话管理、操作笔记和大模型问答全部放进同一个界面。简单说,就是不用再“三开”,一边连服务器、一边写记录、一边问 AI,所有上下文都在一个地方闭环。这篇文章把我从需求拆解、技术选型到落地实现和踩坑记录完整梳理一遍,适合那些每天要连多台服务器、又想让 AI 帮自己写脚本查日志的运维同学参考。
1. 先拆解痛点:为什么运维会陷入“三开”
1.1 三开场景的日常,远比你想象的费神
我见过不少同行,桌面长期挂着三个窗口:左边是 SSH 客户端,右边是记事本,下面还开着聊天框。表面上看是习惯了,实际上每切换一次窗口,大脑就要重新加载一遍“刚才做到哪儿”的上下文。这种隐性成本在故障处理时尤其明显:登录服务器查看日志,发现异常,切到记事本复制一行关键信息,再切到聊天框问 AI 这是什么报错,拿到的答案里又带着新命令,再切回终端执行。一圈下来,至少四五次上下文切换。
更麻烦的是记录和工作流是割裂的。你在记事本里写“今天改了 nginx 配置”,但和你实际执行了哪些命令没有关联;你在聊天框里问了一个脚本怎么写,转头要用时又找不到了;SSH 会话列表也不会告诉你上次在这台机器上做了什么。工具各管各的,有效信息被切割得七零八落。Mini Assistant 这次的核心目标,不是做一个花哨的终端模拟器,而是把“连、记、问”三个动作统一到同一个工作流里,让 AI 能知道你在哪台机器上、正在处理什么、之前执行过什么,从而给出更贴合当前场景的回答。
1.2 合并工具的关键不是界面,而是上下文
一开始我考虑过做一个纯 Web 平台,浏览器里放终端和聊天框,但是体验下来有两个问题。第一,浏览器对 SSH 的原生支持为零,必须额外维护一套服务端转发;第二,记事本和聊天记录如果放在远端,隐私性和离线可用性都打折扣。后来我选了本地桌面应用方案,基于 Electron 做壳,界面用 React,SSH 连接通过 Node 的 ssh2 库直接建立,AI 调用走大模型的标准接口。这样所有敏感信息都留在本地,只有需要 AI 处理的内容才会发送到模型服务端。
为什么用 Electron 而不是其他框架?我身边有人推荐过 Tauri,但那套方案对系统依赖比较多,而且我需要用 Node 生态里现成的 SSH 库,Electron 在这一块确实省心。主进程负责建立 SSH 连接、读写本地笔记、调用 AI 接口,渲染进程只做界面展示和交互。进程边界清晰之后,连接管理就变成了一个状态机,界面只是状态的可视化。这也是 Mini Assistant 后续能够扩展多标签、临时会话、脚本确认这些功能的基础。
2. Mini Assistant 的核心模块与设计细节
2.1 SSH 会话管理:多主机连接与快捷切换设计
SSH 会话管理是工作台的底座,我按“连接组 + 主机标签”的方式组织。比如一组叫“生产环境”,里面放 web-01、web-02、db-01 这些标签,点击标签就建立连接,再次点击可以断开或复用已有的会话。Mini Assistant 里的会话不是一次性终端,而是有生命周期的。连接之后,该会话会一直保留在后台,AI 模块可以读取这个会话的原始输出,笔记模块可以关联到这个会话的备注信息。
连接参数我用了系统自带的 openssh 格式来管理,支持密码、密钥以及常用的跳板机场景。实际落地时发现,最影响使用体验的不是连接速度,而是断线重连。所以在设计里做了一个自动重连机制:连接断开后先判断是否属于网络抖动,如果是就按指数退避重试,并保留当时的工作目录和历史记录。这块需要特别注意,不能在重连时直接把旧的终端清掉,否则用户刚复制的关键输出就没了。我的做法是把所有输出都写入一个环形缓冲区,重连后可以手动回滚查看。
2.2 内置记事本:让操作记录跟会话走
记事本不是简单的一个 textarea,而是按“会话维度”自动归档的。每次在某个 SSH 会话里执行命令,Mini Assistant 会把这条命令、执行时间、返回码和关键输出片段自动附加到当前会话的笔记页。用户也可以手动补充文字,比如“2025-XX-XX 更新了证书,下次续期时间是什么时候”。这样,笔记不再是孤立文档,而是和具体服务器、具体操作绑定在一起的历史时间线。
我做过对比,用传统方式记录操作,事后翻看时经常想不起来当时为什么那么改;但有了会话维度的笔记,上下文是连续的。比如你在这台机器上先改了防火墙,然后又改了 nginx,AI 在你提问时能结合这些历史记录给出更合理的判断。实现上,笔记数据采用本地 JSON 文件加 SQLite 索引。为什么用 SQLite?因为后续我想按主机、按时间、按关键词搜索历史操作,纯 JSON 的过滤效率不够。SQLite 这边只需要维护一个简单的表结构,写入后异步更新索引,基本不影响操作流畅度。
2.3 AI 聊天框:从问答到命令生成与日志解读
AI 模块是 Mini Assistant 最核心的部分,但也是最需要谨慎对待的部分。我的定位不是让 AI 直接接管服务器,而是让 AI 成为一个能看懂当前场景的副驾驶。聊天框支持三种典型用法。
第一是问答解释,用户把报错信息贴进去,AI 解释原因并给出排查建议。第二是命令生成,用户用自然语言描述目标,AI 返回命令,命令展示在单独的确认区里,用户确认后才执行。第三是日志解读,用户选中终端里某段日志,把它发送给 AI,AI 按时间线和严重程度做摘要,直接生成排查结论。
这三种用法对应了三种不同的 Prompt 模板。命令生成模板强调“只输出命令本身、不要解释、不要 Markdown 代码块”,因为实际执行时需要干净的命令文本;日志解读模板强调“区分事实与推测”,避免 AI 把可能性当成确定性结论。这块我踩了不少坑,后面专门讲。
2.4 模块之间的联动:三合一之后的化学反应
三合一最大的价值不是界面干净了,而是模块之间可以共享上下文。Mini Assistant 里设了一个叫“当前场景”的对象,包含当前主机、当前目录、当前执行过的最近 10 条命令、当前选中文本、当前笔记关键词。当用户在聊天框提问时,系统会自动把这些上下文组装到 Prompt 里,然后在 UI 上展示“本次提问附带了下述上下文”,用户可以手动删掉其中任意一项再发送。
我之前没加这个逻辑的时候,AI 经常答非所问。比如我问“这个配置为什么没生效”,它不知道我在说哪台机器、哪个文件。加了当前场景之后,AI 会主动说“根据你在 web-01 上的 nginx 配置,xxx 选项的优先级被覆盖了”。这种上下文联动必须显式展示给用户,否则用户会担心自己到底发了什么数据出去。所以在聊天框顶部有个小标签,点击可以展开当前发送的上下文详情。这一步对建立信任很重要,毕竟运维手里的服务器信息都很敏感。
3. 实操过程:从零搭一个 AI 运维工作台
3.1 初始化项目与构建主界面
先说整体结构。我用的技术栈是 Electron + React + Vite,后端能力统一放在 Electron 主进程里。主进程里开了三个模块:SSH 连接管理器、本地笔记存储、AI 请求封装。渲染进程只管调用主进程暴露的能力。
第一步是搭建项目骨架,我用了一个很常规的模板结构。下面是一个简化版的主进程启动代码,重点是注册好 IPC 接口,让渲染进程可以发消息来建立 SSH 连接:
const { app, BrowserWindow, ipcMain } = require('electron'); const { connectSSH, listSessions } = require('./sshManager'); const { createNote, queryNotes } = require('./noteStore'); const { askAI } = require('./aiClient'); function createWindow() { const win = new BrowserWindow({ width: 1400, height: 900, webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false } }); win.loadURL(process.env.VITE_DEV_SERVER_URL || `file://${__dirname}/dist/index.html`); } ipcMain.handle('ssh:connect', (event, sessionConfig) => { return connectSSH(sessionConfig); }); ipcMain.handle('ssh:list', () => { return listSessions(); }); ipcMain.handle('note:create', (event, payload) => { return createNote(payload.sessionId, payload.content); }); ipcMain.handle('note:query', (event, query) => { return queryNotes(query); }); ipcMain.handle('ai:ask', (event, payload) => { return askAI(payload.context, payload.question); }); app.whenReady().then(createWindow);这里的核心设计是 IPC 处理器足够薄,只做参数透传,真正的逻辑都在各管理器里。这样后续就算把界面换成别的框架,后端能力也能复用。
界面布局我用三栏结构:最左侧是主机列表,中间是终端区,右侧是笔记和 AI 聊天。右侧再分成上下两个面板,上面是笔记,下面是聊天框。三栏宽度可以拖拽调节。这个布局不是拍脑袋想的,我把日常操作路径走了一遍,最终确定这个顺序:从左侧选主机,中间执行命令,右侧记录和提问,符合从左到右的操作习惯。
3.2 接入 SSH 终端组件
终端部分我原本想自己写一个终端渲染器,后来发现工作量太大且容易踩兼容性坑,就用了现成的组件库,配合 node-pty 在 Node 层创建伪终端。这样做的优势在于,可以拿到 SSH 的所有原始输出,并且能把这些输出同步给 AI 模块。
关键连接逻辑像这样:
const { Client } = require('ssh2'); const { spawn } = require('node-pty'); function connectSSH(config) { const sshClient = new Client(); sshClient.on('ready', () => { // config.width / config.height 来自前端终端的实际尺寸 sshClient.shell({ term: 'xterm-256color', rows: config.rows, cols: config.cols }, (err, stream) => { const pty = spawn('bash', [], { name: 'xterm-256color', cols: config.cols, rows: config.rows, cwd: config.cwd, env: process.env }); stream.on('data', (data) => { pty.write(data); }); pty.onData((data) => { stream.write(data); // 把输出写入内存里的环形缓冲区,供 AI 和笔记模块读取 buffer.push(data); }); pty.onExit(() => { sshClient.end(); }); }); }); sshClient.connect({ host: config.host, port: config.port || 22, username: config.username, password: config.password, privateKey: config.privateKey }); return { sshClient, buffer }; }这里有一个容易被忽略的细节:SSH 连接建立后,需要用 node-pty 在本地创建一个伪终端,再和 SSH 的 stream 做双向数据转发。这样做的原因是,SSH 服务端返回的流数据如果直接交给浏览器终端组件,很多控制字符处理不好,尤其是交互式命令时的光标移动和清屏。经过本地伪终端中转后,终端渲染组件拿到的就是规范化的输入输出流。
3.3 AI 服务与命令生成
AI 接入部分我写了一个通用的 aiClient,不绑定特定厂商,统一走接口。请求时会把当前场景上下文一起携带,然后根据问题类型选择不同的 Prompt 模板。
这里给出一个经过反复调试的命令生成 Prompt 模板:
你是一个运维助手。根据用户输入,生成一条可执行的 Linux 命令。 要求: 1. 只输出命令本身,不要输出解释、不要输出 Markdown 代码块标记。 2. 命令必须能在常见的 Linux 发行版下执行。 3. 如果命令存在风险,在命令后加一行以 # RISK 开头的注释,说明风险点。 4. 如果无法确定,请输出 # ERROR 并说明缺少什么信息。 用户当前主机:{host} 用户当前目录:{cwd} 最近执行命令: {history} 用户需求:{question}这个模板解决了我最早遇到的最大问题:AI 回答里夹杂了太多废话。尤其在终端场景,用户希望拿到的是可以直接粘贴执行的命令,而不是娓娓道来的教程。加上“只输出命令本身”的约束后,输出干净很多。为了安全,我还在前端做了一个“命令确认面板”,AI 返回的命令不会直接进入终端,而是先出现在下方一个黄色条里,用户点击执行才会真正执行。
调用大模型接口的封装也简单,核心就是一个 fetch,不过要做好超时和流式输出。流式输出很关键,运维人员在等待 AI 回复时,如果界面一直空白会非常焦虑。我用了流式接口,AI 生成的字会一行一行出现,虽然实际上没有更快,但体验好很多。
3.4 让笔记、会话、AI 在同一上下文里工作
这三个模块各自单独做都不难,难的是一起工作。我的实现是在主进程里维护一个 sessionContext 对象,每当终端有新的输出、用户切换主机、用户在笔记里新增内容,都会更新这个对象。AI 请求发出前,从对象里摘取必要的字段。
实际写逻辑时,有几点要特别注意。一是不要把所有终端原始数据都塞进 Prompt,那样上下文会爆炸,模型会迷失重点。我会先做截断,只保留最近 2000 个字符,并用正则把明显的密码、Token 等敏感内容替换成占位符。二是 AI 的上下文和笔记要双向联动。用户提问时自动带上最近笔记,同时 AI 的回答也可以一键保存进当前笔记,相当于把 AI 变成了一个会做记录的副手。
这个联动流程我把它做成了一个小型的事件循环:
const context = { hostId: 'web-01', cwd: '/etc/nginx', history: [], selectedText: '', noteKeywords: [] }; function updateContext(partial) { Object.assign(context, partial); // 每次上下文变化都触发一次轻量级持久化 saveContextSnapshot(context); } async function askWithContext(question) { const payload = buildPrompt(context, question); const answer = await aiClient.ask(payload); // 自动保存到当前会话笔记 noteStore.append(context.hostId, `Q: ${question}\nA: ${answer}`); return answer; }这套逻辑跑通后,“三开”的场景基本就消失了。我只需要关注中间的操作区,右侧笔记会自动记录,AI 也知道我在做什么。用了一段时间后,最大的感受是心理负担减轻了很多,不用担心某个关键信息被落在哪个窗口里。
4. 常见问题与排查实录
4.1 SSH 连接建立不起来,问题出在哪
我在开发过程中第一个常遇的问题是 SSH 连接超时。排查下来,很大一部分原因是 Electron 主进程里的 SSH 连接请求被渲染进程的界面渲染任务占了线程资源。后来我把连接动作放到子进程里执行,界面卡顿才解决。如果是小项目,也可以在 IPC 处理器里加一个简单的异步队列,避免并发连接过多导致 socket 阻塞。
另一个坑是密钥文件的权限。很多机器对私钥文件的权限有严格限制,如果从 Windows 复制过来的私钥权限不对,连接会直接被拒绝。我加了一个首次启动时的自动检查,遇到权限问题直接提示用户 chmod 600,而不是让用户面对一串晦涩的报错。这些细节虽然不涉及核心业务,但确实决定了工具能不能被日常使用。
4.2 AI 生成的命令能信多少,怎么防呆
这是我最想强调的一点。大模型非常擅长生成看起来合理、实际有坑的命令。比如它可能会给出rm -rf相关的命令,或者在不合适的目录下执行清空操作。所以 Mini Assistant 从一开始就设计了命令确认区,任何 AI 生成的命令都必须经过用户手动确认才能发送到终端。
即使这样,我也遇到过 AI 给出的命令语法正确但语义离谱的情况。比如有一次,AI 建议用cp /var/log/nginx/access.log /tmp/access.log.backup备份日志,但在生产环境上这样做会瞬间复制一个大文件到 /tmp,磁盘可能被打满。后来我加了两个机制:一是在 Prompt 里明确要求 AI 考虑磁盘空间和风险;二是对包含重定向、删除、格式化等敏感关键词的命令,给出二次强提醒。AI 是个好帮手,但前提是你得给它装上安全带。
4.3 笔记记录混乱,找不到上次操作
起初我把所有笔记按每台主机一个文件存储,但时间一长,文件内容越来越多,翻起来非常痛苦。后来引入了 SQLite 索引,支持按主机、按日期、按关键词搜索。界面右下角加了一个搜索框,输入 pid、nginx、cert 这类关键词,笔记和命令历史会一起过滤出来。这个功能后来成为我用得最多的功能之一,尤其是在排查“上次到底改了哪儿”的问题时,能省下大把时间。
还有一个容易被忽略的点:多窗口操作时的并发写入。因为 Electron 主进程和渲染进程都可能写笔记,最开始我的实现存在偶发覆盖问题。解决方式是所有写入都走主进程同一个队列,按时间戳排序,后写的不能覆盖先写的。这里的经验是,本地工具也别嫌麻烦,数据完整性设计得越早,后面越省心。
4.4 上下文过长导致 AI 回答质量下降
当我把终端历史、笔记摘要、当前目录这些信息一股脑塞给 AI 时,开始确实挺爽,但很快就发现回答开始东拉西扯。因为模型可用的上下文窗口有限,塞得太多反而丢掉了重点。我后来做了三个优化。
第一,截断历史命令,只保留最近 10 条,且每条只保留命令本身和执行结果的第一行。第二,笔记摘要用抽取式方法生成,只保留包含主机名、路径、关键词的句子。第三,允许用户手动选择要发送哪些上下文,而不是每次全量发送。这三步做完之后,AI 的回答质量明显提升,也更可控。
5. 进阶玩法与实际使用体会
5.1 把终端历史拉进 AI 分析
Mini Assistant 跑通基础功能后,我又做了两个进阶玩法。第一个是故障前后的历史回溯。当一台服务器出问题时,我只需在聊天框里输入“分析一下这台机器最近的操作历史”,AI 会自动整理当前会话的记录,把可能导致问题的变更按时间列出来。这个过程比人工翻笔记高效得多,因为操作记录是结构化的,AI 能一眼看出哪条命令最可疑。
第二个是把 AI 接入日志采样。当用户选中终端里的几行日志,Mini Assistant 会先做一次本地正则预解析,提取时间戳、错误级别、关键词,然后带着这些结构化信息去问 AI。这样 AI 不需要从头读原文,回答更精准,也能减少发送到模型服务端的数据量。这块的实现不复杂,但对隐私保护很有价值。
5.2 别让 AI 看到不该看的内容
安全边界是我在设计时一直紧绷的一根弦。运维的终端里可能出现密码、密钥、内网 IP、业务数据,这些东西不该无条件发给大模型服务。Mini Assistant 做了两级过滤:第一级是在发送前用正则扫描,把疑似密码、Token 的内容替换成[REDACTED];第二级是在上下文中引入“可发送”标记,只有用户显式选择的文本片段才会进入 AI 请求。
我会在界面右上角放一个当前已脱敏字段的计数,让用户知道刚才那次 AI 请求里有几处数据被遮掉了。这个设计花费了不少精力,但我觉得非常值得。运维工具首先要让使用者安心,才谈得上效率和智能。
5.3 回看这三个月的使用效果
现在我在日常工作中已经不太依赖单独的 SSH 客户端和聊天工具了。Mini Assistant 最大的收益不是省了多少秒,而是减少了我来回切换时丢失的注意力。以前排查一个问题可能要花半小时,其中十分钟都花在找上下文上;现在打开工作台,所有线索都在同一个时间线里,心里踏实很多。
如果你也想做类似的东西,我的建议是不要一上来就追求大而全。先把 SSH 会话管理做好,再加上最简单的笔记功能,体验一天;第二步接 AI 命令生成,但一定要做命令确认机制;最后再考虑上下文联动。每一步都能独立使用,不会因为其中一个环节出问题导致整套工具不可用。工具是拿来用的,不是拿来展示架构的。做运维工作台这件事,最重要的不是技术多新,而是它真正陪你解决了那些重复了几百次的小麻烦。