SSLTool V3.1 开发记:会话与工程 —— 从"连接列表"到"多环境工作区"
V3.0 把 SSLTool 从"能用的连接工具"推到了"可信的连接工具":主机密钥 TOFU、断点续传、跳板机。但仍然有一个经常被低估的问题——工具里的"连接"是有上下文的。一个后端工程师的日常是:生产、预发、测试、客户 A 的内网、客户 B 的内网……如果所有会话都躺在同一张列表里,每次操作都要先做一次"这是哪个环境"的心算。V3.1 的主题就是把这层心算从人脑挪进工具:会话与工程。
一、为什么是"工程",而不是"文件夹"
我们讨论过两种组织方式:给会话加"目录树"(像文件系统)或加"工程"(像 IDE 的 workspace)。
- 目录树只是一层标签,切换目录不会改变任何上下文;
- 工程是隔离容器:一个工程有自己的会话列表、自己的标签页工作区、自己的"最近连接"。切到"生产"工程时,你看到的就是生产环境该有的东西,测试环境的标签页不会混进来。
对目标用户(多环境运维)来说,第二种才是真实需求。于是 V3.1 的核心模型定为:工程 → 会话(隶属关系)→ 标签页工作区(每个工程一份快照)。
工程清单保存在 ~/.ssltool/projects.json,结构刻意保持极简:
[{"id": "default", "name": "默认工程"}, {"id": "p_3f2a", "name": "生产环境"}]
每个会话通过 project 字段归属工程;旧版本升级上来的会话自动归入 default——向后兼容是迁移的第一优先级,否则升级即事故。
二、工作区快照:记录"打开了什么",而不是"重连什么"
“关掉再打开,标签页全没了"是最烦的体验损失。我们实现了工作区快照:切换工程或退出时,把当前打开的 console / sftp 标签页记到该工程下:
"workspaces": {"default": [["console", "a1b2c3"], ["sftp", "d4e5f6"]]}
快照里只有 (kind, sid) 二元组:只记录"开了哪个会话、开成什么标签”,不记录密码,也不记录终端里的历史。这是有意为之的边界:
- 凭据走系统钥匙串(keyring),快照里出现密码就等于把加密层又打开了一个洞;
- 终端历史属于会话日志(
~/.ssltool/logs/),那是审计范畴,不是"恢复"范畴; - 恢复时对需要认证的标签页按需弹框,而不是在启动时批量弹出一排密码框——快照服务于"一键回到昨天的现场",但现场的第一条命令仍然由人确认。
恢复动作的交互设计也遵循同一原则:默认不自动恢复(避免启动时意外连上生产),勾选「启动时自动恢复上次工作区」才启用;Ctrl+R 随时手动恢复。
三、状态点:新增一条窄接口,而不是改写旧契约
标签页标题上的状态点(● 已连接 / ○ 已断开 / ◌ 连接中)看起来是小事,但它是跨组件契约:ConnectionTab 只在 _set_status 里改文字和颜色,主窗口并不知道"这个标签此刻是什么状态"。
两种做法:把 _set_status 的语义改掉(加参数、改渲染),或者新增一个极窄的信号 stateChanged(str)。我们选了后者:
# terminal_tab.py —— 旧契约完全不变
def _set_status(self, text, color="#9aa4b2"):
self.status.setText(text)
self.status.setStyleSheet(f"color: {color};")
self.stateChanged.emit(_STATE_BY_COLOR.get(color, "idle"))
主窗口只订阅信号,用 ●/○/◌ + 文本 重画标签。这样:
- 旧代码(冒烟测试里对
_set_status的直接断言)一行不用改; - 新功能(右键菜单的"重新连接"只在已断开时可用)只依赖状态文字,不依赖内部实现;
- 未来再加"静默/繁忙"等状态,只需扩映射表。
教训:在"展示层"加信息,优先新增只读、单向的接口,而不是改造已有语义——后者会把所有消费者拖进改动面。
四、Ctrl+Tab 与标签页菜单:把高频操作降到零思考
Ctrl+Tab/Ctrl+Shift+Tab:主窗口级事件过滤器,终端未接管时生效;Ctrl+1~Ctrl+9:第 N 个标签;Ctrl+W:关闭当前标签(先shutdown()再移除,避免连接泄漏);- 右键标签页:关闭 / 关闭其它 / 关闭全部 / 重新连接——重连仅对「已断开的 console 标签」可用(SSL 标签与 SFTP 重建路径不同,V3.1 不做,宁缺毋滥)。
“关闭全部"跳过了欢迎页——欢迎页不是工作区,不该被"批量关闭"波及。这类边界条件,最终都变成了测试断言。
五、最近连接:按时间排序,而不是按次数
顶部「最近连接」分组取 last_used 时间戳排序、最多 5 个。为什么不是"使用次数”?
- 次数是累积统计,对"我现在想回到哪"没有帮助;
- 时间戳是上下文记忆:今天上午连的机器,下午大概还要连;
- 5 是经验值:再多就与完整列表重复,再少就不够覆盖一天的轮换。
连接成功时写入 last_used(_touch_last_used),之后每次刷新列表都用同一套分组渲染逻辑——最近连接只是分组规则,不是另一种数据。
六、测试:把"工作区"变成可断言的对象
V3.1 的测试没有引入新框架,延续两条线:
smoke(100 项)新增 4a 段:构造两个工程、三个会话,断言——
- 工程下拉列出全部工程、默认工程为当前;
- 会话树只显示当前工程,且「最近连接」置顶;
- 切换工程后列表过滤正确、
current_project持久化; _snapshot_workspace()输出[["console", sid]]、_save_workspace()写入 settings;- 用一个
_FakeTab(带tabClosed信号与shutdown())驱动_update_tab_state→ 标签变●、_close_all_tabs()保留欢迎页且假标签被 shutdown。
e2e(11 项)保持不变:V3.1 没有动网络路径,e2e 的职责是守网络层不回归;主窗口的工程/工作区逻辑放在离屏 smoke 里,成本低、断言直接。
一条踩过的坑:菜单项数量断言在"视图"菜单从 3 项变 4 项时失败——Ctrl+R 加进了视图菜单,而 smoke 里写死了 actions() == 3。菜单也是接口,新增命令要同步更新计数断言。
七、一条上下文重构的插曲
这个迭代中途换过模型。切换后第一件事不是写代码,而是用上下文压缩/检索把"当前做到哪、哪些文件改过、测试跑过什么"重新捞出来:把旧摘要里的文件状态与 git status 对一遍,确认没有半截改动,再继续。对 AI 辅助开发来说,“重新对齐当前状态"本身就是一项技能。
结语
V3.1 没有新协议、没有新密码学——它解决的是组织问题:让"我在哪个环境"从人的记忆变成工具的模型。工程隔离是容器,工作区快照是记忆,状态点与快捷键是肌肉记忆,最近连接是上下文。这些加起来,才让一个 SSH 客户端真正成为"工作台”。
Copyright (c) 2026 Urtaome0211. All rights reserved.