摘要:2026 年了,为什么还有人用 Electron 写 SSH 客户端?PixShell 的作者给出了另一种答案:同一个 monorepo,macOS 端用 Swift + AppKit + SwiftTerm + SwiftNIO 手写 SFTP v3 协议,Windows 端用 C# WPF + WebView2 + SSH.NET,双端原生、零 Electron。功能上它几乎照着 FinalShell 抄了一遍作业(同屏目录同步、chmod、打包传输、一键迁移),又加上了 2026 年的新东西:MCP、Agent Bridge、AI 工具一键接管 SSH。更有意思的是——这个项目几乎是一个人带着几个 AI 编码代理写出来的。本文拆解它的功能定位、架构取舍、协议实现细节和那些藏在注释里的踩坑记录。
一个 SSH 客户端的"去 Electron 化"实验
打开 FinalShell、Termius、Tabby 的进程列表,你大概率会看到一堆 --type=renderer 的 Chromium 子进程。一个用来连服务器的工具,自己先吃掉 800MB 内存,这件事大家似乎已经习惯了。
PixShell(github.com/lyu0805/pixshell)的作者显然没习惯。这个项目的前身就是一个 Electron 应用——仓库里的蓝图文档还留着旧账:renderer 约 1.6 万行 + main 约 5700 行 JS。从 v0.1.1 开始,他做了一个相当激进的决定:推倒重来,双端全原生,同一个 monorepo 维护。
| 平台 | UI | 终端渲染 | SSH/SFTP 栈 |
|---|---|---|---|
| 🍎 macOS | Swift / AppKit | SwiftTerm(原生渲染) | SwiftNIO + 自研 SFTP v3 |
| 🪟 Windows | C# / WPF | WebView2 + xterm.js | SSH.NET |
注意这张表里最反直觉的一点:macOS 端没有用 xterm.js。Mac 的终端是 SwiftTerm 原生渲染的,只有 Windows 端因为 WPF 生态里没有像样的终端控件,才走了 WebView2 内嵌 xterm.js 这条路。作者在 README 里专门加了个警告框强调这件事——大概是被问烦了。
代码量上,mac 端 Swift 约 1.9 万行,win 端 C#/XAML 约 1.7 万行。作为一个个人项目,这个体量不算小。作者自己的定位是三个词:轻量、紧凑、功能丰富——五区高密度布局,面向运维场景,而不是又一个"好看但只能连一台机器"的极简终端。
功能盘点:FinalShell 有的,它再加 AI
看功能列表之前先说结论:PixShell 对标的就是 FinalShell。证据有三——终端与 SFTP 同屏、目录同步切换是 FinalShell 的招牌交互;chmod 弹窗官方文档直接写"1:1 对齐 FinalShell 设计";甚至连迁移工具都做好了,一键扫描解密 FinalShell 本机存的主机和密码(实测 50 台主机 + 48 个口令批量导入)。这是典型的"抢存量用户"打法:不教育市场,直接接盘。
| 类别 | 功能 | 说明 |
|---|---|---|
| 会话 | 多标签 + 批量服务器管理 | 每 tab 独立会话,支持重连、PTY resize、快速连接历史 |
| 会话 | 终端 + SFTP 同屏,目录同步切换 | FinalShell 招牌交互,原生复刻 |
| 文件 | 打包传输 | 大文件/目录自动 tar → 传输 → 目标端解压 → 两端临时包自动清理 |
| 文件 | Chmod 权限弹窗 | 9 项读写执复选框联动八进制值、递归子目录、按文件/目录类型过滤 |
| 文件 | 内置文本编辑器 | 远程文本编辑、查找替换、一键保存回写 |
| 监控 | 实时性能面板 | CPU / 内存 / 磁盘 / 网卡上下行速率 |
| 安全 | credentials.dat 本地加密存储 | 零 Keychain 授权弹窗、零本地网络权限弹窗 |
| 安全 | 主机指纹管理 | known_hosts 查看、单条删除、导入导出备份 |
| 兼容 | OpenWrt / Dropbear | CTR/Chacha20/RSA 算法分支 + OpenSSH fallback |
| 兼容 | SFTP PTY 回落 | 无 sftp subsystem 的设备走伪终端管道拉起 sftp-server |
| 兼容 | FinalShell 一键迁移 | 自动扫描、解密、导入主机与口令 |
| 网络 | 密码 / 私钥认证 | 密钥优先、密码兜底;SOCKS5 / SOCKS4 / HTTP 代理 |
| AI | Agent Bridge / MCP / Headless | 127.0.0.1:8766 本地桥,驱动持久交互会话(下文详述) |
| AI | pixshell-ssh 一键注册 | 检测本机 Claude Code / Codex / Grok 等,注册为默认 SSH 包装工具 |
| AI | Web SSH 网页终端 | GET /webssh,浏览器 xterm.js 操作当前会话 |
一句话总结这张表:FinalShell 的肌肉 + AI 时代的接口 - Electron 的脂肪。前一半负责让老用户无缝搬家,后一半负责讲新故事。下面拆开看它怎么实现的。
架构:一套契约,两套实现
双端原生最大的难题不是写代码,而是怎么保证两端行为一致。Electron 再臃肿,至少是一套代码跑两边。PixShell 的解法是"契约对齐"——仓库里专门有一份 LAYOUT-PARITY.md 布局契约和一份 BLUEPRINT.md 模块蓝图,把 18 个子系统(SSH 引擎、会话 hub、SFTP、代理、高亮引擎……)逐个拆开,规定好两端的共享接口:
- SSH 会话统一接口:mac 是
SSHSession协议,Windows 定义等价的ISshSession(连接/开 PTY/发数据/resize/关闭 + 输出回调); - 主机模型字段两端逐字段一致:
id/name/host/port/username/auth/group/osId; - 连配色和语义高亮的"语义键"集合都要求两端一致。
UI 上则是一个"五区工作台":顶栏(品牌 + 会话 tab)、侧栏(主机列表)、中间终端区、底部文件/命令坞、状态栏。功能对齐、布局对齐,但控件和终端栈各走各的平台最优路径。
布局·模型·协议] B --> B1[AppKit 五区布局] B --> B2[SwiftTerm 终端] B --> B3[SwiftNIO SSH
+ 自研 SFTP v3] C --> C1[WPF 五区布局] C --> C2[WebView2 + xterm.js] C --> C3[SSH.NET] B --> E[本地 Agent Bridge
127.0.0.1:8766] C --> E
这套打法的好处是每一端都能用上平台最趁手的东西:Mac 有 Keychain、SwiftTerm、SwiftNIO;Windows 有 DPAPI、SSH.NET、WebView2。代价是双倍的工作量——这就引出了这个项目最有意思的部分。
一个人 + 一队 AI 代理
翻开 win/BLUEPRINT.md,你会看到一份罕见的"人员"分工表:
| 角色 | 负责 |
|---|---|
| 作者本人 | 架构 / 关键路径 / 集成 / 审核所有产出 / 端到端验证(编译+运行+截屏) |
| local-codex(grok-4.5) | mac Swift 模块 |
| win-opencode / win-codex / Hermes | Windows C# 模块 |
这不是团队项目,是一个人当架构师兼 QA,带着一队 AI 编码代理干活。作者给自己定的规矩很明确:AI 写模块,他掌控关键路径、做集成、审核每一份产出,并且每个阶段都要"编译 + 运行 + 截屏"端到端验证。蓝图里对 P0 阶段的状态记录是"mac ✅ 渲染;win ✅ 编译,待运行验证 + 接 SSH"——AI 写的代码编译通过不算完,跑不起来就不算数。
这大概是 2026 年独立开发者的一种典型工作流:你不再需要等一个会 Swift 的合伙人,而是自己定契约、拆模块、验收产出。前提是你得有能力定义清楚"共享契约"——不然两个 AI 代理能给你写出两个互相不认识的世界。
硬核部分:在 SwiftNIO 上手写 SFTP v3
整个项目技术含量最高的一块,是 mac 端的 SFTP 实现。
为什么不用现成的?因为 swift-nio-ssh(Apple 官方的 NIO SSH 库)只实现了 SSH 传输层和 channel,根本没有 SFTP 子系统。于是作者对着 draft-ietf-secsh-filexfer-02(OpenSSH 实际使用的 v3 版本),自己把线协议撸了一遍。SFTPProtocol.swift 里是完整的报文类型定义和编解码:
enum SFTP {
static let version: UInt32 = 3
static let OPEN: UInt8 = 3
static let READ: UInt8 = 5
static let WRITE: UInt8 = 6
static let OPENDIR: UInt8 = 11
static let READDIR: UInt8 = 12
// ...
/// 单次 READ/WRITE 分块大小(32 KiB,OpenSSH 兼容的保守值)
static let chunkSize = 32 * 1024
}
帧格式是经典的 uint32 length | byte type | payload,除 INIT/VERSION 外每个 payload 开头都是 uint32 request-id 做请求/响应关联。32 KiB 的分块大小特意注明是"OpenSSH 兼容的保守值"——这种注释一看就是被某些实现坑过之后留下的。
更麻烦的是 SSH 算法协商。swift-nio-ssh 的算法支持是库级硬限制:加密只有 AES-GCM 两档,host key 只有 ed25519 和 ECDSA,没有 RSA、没有 DH-group。现代服务器没问题,但运维场景里你永远不知道会遇到什么老古董。作者的策略很务实——NIOSSH 协商失败就回落到系统的 /usr/bin/ssh(OpenSSHSession),用最大兼容参数兜底。代码注释写得很直白:
老设备(RSA host key + aes-ctr + dh-group*)会在 shell 打开前协商失败,由上层回落到 OpenSSHSession。
v0.1.2 又补了 OpenWrt / Dropbear 的兼容(CTR/Chacha20/RSA 分支 + OpenSSH fallback),以及一个很接地气的功能:对没开 subsystem sftp 的设备,自动回落到 PTY 伪终端管道跑 sftp-server,兼容 /usr/libexec/sftp-server 和 /usr/lib/sftp-server 两个常见路径。写过路由器固件的人都知道,Dropbear 那套东西和标准 OpenSSH 的脾气完全不一样。
Agent Bridge:给 AI 工具留一扇门
PixShell 最对 2026 年胃口的功能,是它的 CLI / Agent Bridge。
双端都会在 127.0.0.1:8766 起一个本地 HTTP 服务(mac 用 NWListener,Windows 用 HttpListener,协议对齐),外部 CLI 和 AI 工具可以通过它驱动正在运行的桌面端:列出主机、开会话、发命令、读屏幕、传文件。
关键设计是会话复用:Claude Code、Codex 这类工具执行 SSH 命令时,通常每条命令都新开一个 SSH 连接——慢,而且很多运维操作(su、交互式确认)根本没法无状态重放。通过 Agent Bridge,AI 工具可以直接复用 PixShell 里已经建立的持久交互式会话。配套还有一个 pixshell-ssh 包装脚本:一键软链到 ~/.local/bin/ssh,让 AI 工具以为自己在调系统 ssh,实际上走的是 PixShell 的会话;桥没就绪时自动回落真正的 /usr/bin/ssh。
作为一个本地服务,它的安全注释值得逐条读,基本是一份"本地回环服务安全清单":
- 只绑
127.0.0.1,绝不监听0.0.0.0(Windows 端特意注明前缀写字面量 IP,不用+/*/机器名——那会暴露到局域网,还要管理员权限做 URL-ACL); - 每个连接 accept 后再核实一次远端地址是否回环;
- 每个请求都要 token(0600 权限文件);
- 不发任何 CORS 头,带
Origin的跨站请求一律 403,只放行本机同源(给 Web SSH 页用); - 请求体上限 8 MiB,防失控客户端打爆内存;
- token 绝不进日志,只记长度。
另外还内置了一个 GET /webssh 网页终端(xterm.js + 本地桥),浏览器打开就能操作当前会话。一个桌面 SSH 客户端同时充当本地 API 网关和 Web 终端服务器,这个形态确实少见。
藏在注释里的踩坑清单
读这个项目的源码,乐趣很大一部分来自注释。作者显然把 debug 过程直接写进了代码里,随手摘几个:
Swift 6 严格并发。Package.swift 里顶层语言模式特意降回 Swift 5:“避开 Swift 6 严格并发误报”。NIOSSHHandler 和 SSHClientConfiguration 都不是 Sendable,于是出现了 NIOSSHClientConfigBox 这种手动装箱消警告的写法,以及用 pipeline.syncOperations 在 event loop 上同步装 handler 来绕开 @Sendable 捕获。
macOS 15 本地网络隐私。系统偶尔会把首次连接打成 EHOSTUNREACH (errno 65),代码里专门加了短延迟重试:
// macOS 15+ 本地网络隐私偶发把首连打成 EHOSTUNREACH(65);短延迟重试一次,
// 仍失败则 emitClose,上层会归类为 network 并保留钥匙串。
Keychain 弹窗骚扰。用 Keychain 存密码本来是"政治正确",但 macOS 会频繁弹授权框。v0.1.2 干脆改成局部加密的 credentials.dat 文件——安全和体验之间,作者选了少烦用户。这个取舍在安全原教旨主义者看来可能刺眼,但作为每天要连几十台服务器的运维工具,弹窗疲劳是真实的生产力杀手。
WPF + WebView2 的 DPI 坑。csproj 里专门加了 PerMonitorV2 清单,注释一针见血:“不加的话 WPF 外壳被位图拉伸、WebView2 终端按真实 DPI 渲染,内外缩放对不上”。混合渲染架构(原生壳 + Web 内容)的经典老毛病。
NWConnection 的生命周期。mac 端 bridge 用一个字典强引用所有活跃连接,注释解释了为什么:NWConnection 由调用方负责保活,accept() 返回后不存住它,ARC 会立刻释放,“连接刚建立就被悄悄拆掉,receive 永远不回调”。
这些注释比任何博客都真实——它们不是事后总结,是战壕里留下的弹痕。
值得偷师的几个点
抛开"原生 vs Electron"的站队,PixShell 有几个做法对独立开发者很有参考价值:
- 契约先行。双端开发先定接口、模型、布局契约,再分头实现。这套方法同样适用于"人 + AI 代理"的协作——你定义契约的能力,决定了 AI 产出的上限。
- 回落策略比完美协议栈更实用。NIOSSH 算法不全?回落系统 ssh。设备没有 sftp subsystem?回落 PTY 管道。运维工具的用户不关心你用了什么优雅的库,只关心"这台老机器能不能连上"。
- 给自动化留接口。一个本地 HTTP bridge 就让桌面应用变成了 AI 工作流的一环。2026 年写工具,值得默认考虑"AI 代理会怎么用它"。
- 注释写原因,不只写是什么。
chunkSize = 32 * 1024后面那句"OpenSSH 兼容的保守值",比 32 这个数字本身值钱得多。
当然它也有明显的短板:Linux 端缺席、UI 文案以中文为主、没有公证签名(CI 产物 unsigned)、协议栈受限于 swift-nio-ssh 的算法能力。作为一个 0.1.x 的个人项目,这些都不算苛责。
如果你也受够了 SSH 客户端里的 Chromium 进程,或者想看看 Swift 手写网络协议到底长什么样,这个仓库值得 clone 下来翻翻。推荐从 mac/Sources/PixShell/SFTP/SFTPProtocol.swift 和 Bridge/AgentBridge.swift 读起——一个是协议实现的样板,一个是本地服务安全的样板。