Files
Thing/TERMINAL_MODULE_PLAN.md
2026-09-18 18:28:13 +08:00

55 KiB
Raw Permalink Blame History

终端模块规划(Terminal Module Plan

状态:P0 骨架已落地;P1 进行中(Rust 侧:cwd 跟踪 / 状态事件 / GBK 编码 / SFTP 后端已完成并编译通过) 定位:Thing 工具集的第 11 个模块,id = terminalcategory = 'tool' 作者:砚 | 日期:2026-09-17 关联文档:AI_DEV_GUIDE.md(模块注册机制 / IPC / 进程管理范式)

实施进度与踩坑记录见文末 §9(P0)与 §10(P1)(含与本规划不一致之处,以 §9 / §10 为准)


0. 结论先行

三个关键判断,先摆在这里,后文展开论证:

  1. 终端模块不应复用 ProcessManager。它是为「单例常驻守护进程(mihomo)」设计的:一个模块 ID 对应一个进程,崩溃即重启。而终端要的是「N 个会话、每个会话生命周期独立、能挂起能重连、能写 stdin」——语义不同,硬套会把这套抽象撑坏。正确做法是新建独立的 TerminalManager,与 ClipboardManager / MusicManager / TranslateManager 平级,manage() 进 Tauri State。

  2. 会话进程必须跑在 Rust 侧,不能是前端 shell。这是 Windows 上的硬约束,且与已有基建同构:ProcessManagerdownload_engine 都在 Rust 侧管进程。理由有三——(a) WebView2 无 PTY 访问;(b) 前端持有的子进程会在页面重载时变孤儿;(c) 多标签、后台保活、断线重连都需要一个独立于 UI 生命周期的宿主。

  3. SSH 走「自研客户端 + 真实 PTY」,而不是「拼接 ssh.exe + ConPTY」。后者实现快但天花板低:无法做 SFTP 复用连接、无法读主机密钥指纹、无法做跳板机链、无法统一错误模型、ssh.exe 的输出会与 ConPTY 的 ANSI 处理打架。代价是 Ruffles/ssh2 的移植与 ConPTY 绑定要自己写,收益是整个能力面没有上限。

总工作量估算:P0 骨架(本地 Shell + 多会话 + 密钥管理)约 812 个工作日;P1SSH/SFTP 完整能力)约 1520 个工作日;P2(高级能力)按需。建议按 P0 先行落地可用版本,再迭代。

收官状态(2026-09-18P0–P2 全部落地(ZMODEM 经评估放弃,见实施记录), 并完成一轮全链路审查(修复键盘输入失效、切标签丢缓冲、连接期关闭竞态等 7 项)。 单测 91/91、cargo checkvue-tsc 零错误。实施记录见 §9-11(精编版)。


1. 需求解构

用户提出的四条主干,拆成可执行的规格:

用户原话 解构为 落点
「主要是 ssh」 SSH2 客户端、主机密钥校验、认证(密钥/密码/Agent/键盘交互/2FA)、跳板机、端口转发、连接复用 §4.2 / §5.1
「多会话」 多标签 + 分屏、会话持久化(切页不断连)、状态栏、会话恢复、会话模板 §4.3
「密钥管理」 SSH 密钥生成/导入/列举、passphrase 托管、known_hosts 管理、ssh-agent 集成、私钥不进明文 §4.4
「文件快捷管理」 SFTP 双栏文件管理器、拖拽上传下载、跟随终端 cwd、内联 rz/sz、文件编辑器 §4.5
「方便的快捷键」 终端键盘映射(复制粘贴/搜索/新建标签/分屏/跳转)、可配置、与全局面板联动 §4.6
(我补充) 本地 ShellPowerShell/cmd/WSL/Git-Bash)、命令补全与历史命令片段库AI 命令助手日志与审计快速面板联动 §4.1 / §4.7 / §4.8

2. 现状勘察(论证依据)

以下为 2026-09-17 从仓库实际读取的结果,作为设计约束的来源。

2.1 已具备的基建

能力 现有实现 终端模块可复用的部分
模块注册 src/modules/registry.ts + src/modules/index.ts 静态导入 直接沿用,新增一行导入 + 图标映射
类型绑定 tauri-specta 自动生成 src/lib/bindings.tsdebug 构建时导出 必须复用,终端命令量较大,手写 invoke 类型不可接受
凭据存储 src-tauri/src/secrets.rskeyring + Windows 凭据管理器(DPAPI),服务名固定 "Thing" 直接复用,见 §4.4
全局快捷键 src-tauri/src/shortcut.rs:原子化注册 + 应用内冲突检测 + 占用表 直接复用,见 §4.6
托盘 src-tauri/src/tray_menu.rs 可挂「新建会话」入口(P2
日志 src-tauri/src/logger.rslog_info / log_warn / log_error 继承统一日志,日志页可过滤
窗口常量 src-tauri/src/constants.rswindows / events 需新增窗口与事件常量
弹窗范式 translate-popup 的 NOACTIVATE 预创建窗口 + capabilities/translate-popup.json 终端「快速会话/命令补全」浮层可参照

2.2 关键缺口(需要新增依赖)

缺口 现状 方案
ConPTY 绑定 无。windows-sys 未开启 Win32_System_Console 开启该 feature;或引入 portable-pty(见 §3.1 取舍)
SSH 客户端 引入 russh(纯 Rust)或 ssh2libssh2 绑定)
SFTP 随 SSH 库一并引入
终端渲染 无。node_modules不存在 @xterm/* 引入 @xterm/xterm + @xterm/addon-fit + @xterm/addon-webgl + @xterm/addon-search + @xterm/addon-web-links
前端代码编辑器 按需引入 codemirror 或复用纯 <textarea>(见 §4.5
密码短语输入 无安全输入通道 用 Tauri 原生窗口 + 一次性输入,不经 IPC 明文回传

注意:仓库 Cargo.toml 存在编码损坏(多处注释已是乱码,如第 64、70、120、121、127 行)。新增依赖时建议顺带修复该文件编码,否则后续 diff 会持续污染。这是一个独立的清理项,不阻塞终端模块。


3. 技术选型

3.1 终端进程层:ConPTY

Windows 10 1809+ 提供 ConPTYCreatePseudoConsole),是 Windows Terminal 的底层机制。三条路径:

方案 优势 代价 判断
portable-ptywezterm 提取库) 跨平台、API 干净、久经考验 引入一个非 Tauri 生态的大依赖;其 Windows 后端同样走 ConPTY,出问题时要下钻 可接受
直接绑 windows-sys 的 ConPTY 零额外依赖、完全可控、与项目已有 windows-sys 姿态一致 需自行处理 pseudo console handle 生命周期、read/write 线程、resize 时序 推荐
conpty 窄封装 crate 上手快 维护活跃度不确定 备选

推荐直接绑定 windows-sys,理由:项目已有大量原生 Win32 调用(win32_util.rsscreenshot/wgc_capture.rstranslate/capture/uia_capture.rs),团队对该路径熟悉;且 ConPTY 的坑(下述)无论如何都要踩,多一层封装只增加定位难度。

ConPTY 的三个已知陷阱,必须在设计阶段规避:

  1. ClosePseudoConsole 会阻塞,直到所有引用该 PTY 的句柄关闭。必须在独立线程调用,且先取消挂起的 ReadFile
  2. ResizePseudoConsole 有竞态:进程刚创建、还没开始读 stdout 时 resize 可能被吞掉。需要在首帧输出后再应用队列中的尺寸。
  3. 进程退出不等于 PTY 关闭:要等 ReadFile 返回 0 或 ERROR_BROKEN_PIPE,才算真正结束,否则会漏掉尾部输出。

3.2 SSH 层:russh vs ssh2

维度 russh(纯 Rust,基于 thrussh ssh2libssh2 绑定)
构建 纯 Rust,无 C 依赖,交叉编译友好 需 libssh2Windows 下常走 vendored 编译
async 原生 async,与现有 tokio 运行时契合 同步阻塞,需 spawn_blocking 包装
算法覆盖 新算法跟进快(如 chacha20-poly1305sntrup761x25519 受 libssh2 版本限制
稳定性 API 演进较快,偶有破坏性变更 老牌稳定,几乎不再变化
tokio 集成 直接 需额外线程池,与 ProcessManager 的线程模型并存会增加心智负担

推荐 russh。决定性理由是 async 契合度Cargo.toml 已启用 tokiort-multi-thread / sync / net / fs,而终端会话本质是「一个长连接 + 多个并发数据流(shell channel、SFTP channel、port forward)」,用 async 表达最自然;ssh2 的同步模型会迫使每个会话占一个 OS 线程,多会话场景下线程数线性增长。

russh 在实际接入中出现阻塞性问题,回落方案是 ssh2 + spawn_blocking,本规划的结构(SessionHandle 抽象)可容纳这次替换。

3.3 前端渲染:xterm.js

@xterm/xterm 是事实标准(VS Code 终端同源)。必须装的插件:

用途
@xterm/xterm 核心 VT 解析与渲染
@xterm/addon-fit 容器尺寸 → 行列数,配合 ConPTY resize
@xterm/addon-webgl GPU 渲染,大量输出时的性能关键(无它时大 tail 会卡)
@xterm/addon-search 终端内搜索
@xterm/addon-web-links 链接可点击
@xterm/addon-unicode11 宽字符 / emoji 正确宽度(中文场景重要)
@xterm/addon-serializeP1 会话快照序列化,用于恢复

3.4 数据流架构

┌──────────────────────── WebView (Vue 3) ────────────────────────┐
│  TerminalModule.vue                                             │
│   ├── SessionSidebar.vue      会话/标签/分组                     │
│   ├── TerminalTabs.vue        多标签 + 分屏容器                  │
│   │    └── TerminalPane.vue   xterm 实例(每个会话一个)          │
│   ├── SftpPanel.vue           文件管理器(P1)                   │
│   ├── KeyManagerPanel.vue     密钥管理                           │
│   └── SnippetsPanel.vue       命令片段库                         │
│  stores/terminal.ts          Pinia:会话元数据 / 布局 / 设置      │
└───────────────┬─────────────────────────────────────────────────┘
                │  invoke(命令,请求-响应)
                │  listen(事件,流式输出)
┌───────────────▼──────────────── Rust ───────────────────────────┐
│  TerminalManager  (Tauri State, manage())                       │
│   ├── sessions: DashMap<SessionId, Arc<Mutex<Session>>>         │
│   ├── local: ConPtyBackend       本地 Shell 后端                 │
│   ├── remote: SshBackend         SSH 后端(russh               │
│   │    ├── shell channel   → 终端 I/O                           │
│   │    ├── sftp subsystem  → 文件管理                            │
│   │    └── port forward    → 隧道(P2                          │
│   └── known_hosts: HostKeyStore  主机密钥校验                    │
│  secrets.rs             ← 复用:passphrase / 密码 / 代理凭据      │
│  shortcut.rs            ← 复用:全局快捷键                       │
└─────────────────────────────────────────────────────────────────┘

关键设计Session 是一层 trait 抽象,ConPtyBackendSshBackend 都实现它(write / resize / kill / subscribe_output)。这样上层命令层(terminal_writeterminal_resize)无需区分本地与远程,多会话管理逻辑只需写一遍。


4. 功能规格

4.1 本地 ShellP0

  • Shell 探测:启动时枚举可用 Shell,按顺序探测——
    • PowerShell 7+pwsh.exe,优先)
    • Windows PowerShellpowershell.exe
    • cmdcmd.exe
    • Git Bashbash.exe,从 git --exec-path 反推)
    • WSL 发行版(wsl.exe -l -q 枚举)
  • Shell 配置:每个 Shell 可配可执行路径、启动参数、工作目录、环境变量覆盖、启动时执行命令(如 cd /d/project && claude)。
  • 默认工作目录:记住上次 cwd;新建会话时可选「跟随当前项目目录」。
  • 注意 cwd 同步:ConPTY 拿不到子进程的真实 cwd(GetCurrentDirectory 只反映父进程)。需要注入 shell hookPowerShell 用 $PROMPT 包装输出 OSC 7bash 用 PS1 输出 OSC 7)来跟踪 cwd。这是 SFTP「跟随终端目录」的前提,P0 就要做进去。

4.2 SSH 连接(P0 骨架 / P1 完整)

连接管理

  • 主机条目 CRUD:别名、host、port、user、认证方式、私钥、跳板机、分组、备注、标签色。
  • ~/.ssh/config 导入(P0 就做——用户已有配置不该被要求重录)。
  • 连接超时、keep-alive 间隔、重试次数可配。
  • 连接状态机:idle → connecting → auth → established → degraded → closed,每态可观测。

认证方式P0 覆盖前两项,P1 补齐)

  1. 公钥认证P0):支持 RSA / ECDSA / Ed25519,私钥来自文件或导入的存储。
  2. 密码认证P0):密码存 secrets.rs,键名 terminal-ssh-password-{hostId}
  3. ssh-agent 集成P1):Windows OpenSSH Agent 命名管道 \\.\pipe\openssh-ssh-agent
  4. 键盘交互 / 2FA(P1):需要前端弹窗接收一次性输入,走「弹窗 → 回传 → 继续握手」的异步流程,不能阻塞握手线程。
  5. 证书认证P2)。

主机密钥校验(安全基线,P0 必须做)

  • 首次连接展示指纹,要求用户显式确认(不允许 TOFU 静默接受)。
  • 维护 known_hosts(放在 {app_data_dir}/terminal/known_hosts.json),格式与 OpenSSH 兼容以便导出。
  • 指纹变更时红色告警 + 阻断连接,要求用户明确选择「接受新指纹」或「中止」。这是防 MITM 的核心开关,不能省。
  • 支持 SHA256 / MD5 双格式展示(SHA256 为主,MD5 兼容老文档)。

高级能力(P2

  • 跳板机链(ProxyJump,多级)。
  • 端口转发:本地转发 -L、远程转发 -R、动态转发 -DSOCKS5)。
  • 连接复用(ControlMaster 式):同主机多会话共享 TCP 连接,第二次开标签秒开。

4.3 多会话(P0

组织形态

  • 左侧会话侧栏:树形结构,支持「收藏 / 按主机分组 / 按项目分组」,支持拖拽排序(复用 vue-draggable-plus)。
  • 标签页:会话标签可关闭、可拖动重排、可重命名、可固定(pin)。
  • 分屏:水平/垂直切分,最多 2×2(4 格)。每个格子是独立会话,而非同一会话的两视图(后者需要 SSH 多 channel,复杂度高收益低)。
  • 会话持久化:切换到别的模块时会话不断开(进程在 Rust 侧活着),回来时重新 attach,用 @xterm/addon-serialize 恢复可视区快照。

会话状态可视化

  • 侧栏与标签上显示状态点:绿=已连接、黄=连接中、灰=已断开、红=异常。
  • 状态栏展示:会话类型(Local/SSH)、用户@主机、cwd、编码、终端尺寸、连接延迟。

会话恢复(P2

  • 应用重启后,提供「恢复上次会话」——本地会话重建 shell 并 cd 到原目录;SSH 会话重连(不恢复进程态,这点要在 UI 上说明,避免误解)。
  • 会话模板:把「一组会话 + 布局」存为模板(如「后端开发环境」= 3 个 SSH 会话横向分屏),一键拉起。

4.4 密钥管理(P0

密钥生命周期

  • 生成Ed25519(推荐默认)/ RSA2048/3072/4096/ ECDSAP-256/P-384/P-521)。可设注释、可设 passphrase。
  • 导入:支持 OpenSSH 格式、PEM、PKCS#8;支持带 passphrase 的私钥;支持 PuTTY .ppkWindows 用户存量多,P1)。
  • 导出:导出公钥到剪贴板(一键复制 ssh-ed25519 AAAA... comment,配合用户自己贴到服务器)。
  • 删除:二次确认 + 提示「该密钥还关联 N 个主机」。

存储策略(安全姿态必须与本项目既有约定对齐)

参照 secrets.rs 头部注释里明确批判过的历史问题——「同样是可冒充身份的凭据,不该区别对待」。据此定:

数据 存放位置 理由
私钥文件本身 {app_data_dir}/terminal/keys/ 目录,文件权限收紧 私钥可能几 KB,塞进凭据管理器(单条上限约 2.5KB)不可靠;且用户需要用其他工具引用该路径
私钥 passphrase secrets.rs → 系统凭据管理器 是「可冒充身份的凭据」,必须 DPAPI 保护,键名 terminal-key-passphrase-{keyId}
SSH 密码 secrets.rs 同上,键名 terminal-ssh-password-{hostId}
代理密码(P2 secrets.rs 同上
主机密钥指纹 / known_hosts JSON 文件 非机密,需要人可读、可导出
主机配置 / 会话元数据 / 设置 {app_data_dir}/terminal/settings.json 非机密;#[serde(default)] 容器级默认,保证向后兼容

明文禁令(写入代码注释与评审清单):

  • 私钥明文只允许存在于内存与 keys/ 目录,禁止回写 settings.json
  • 私钥 passphrase / SSH 密码禁止进入 localStorage、禁止进入任何日志行。
  • 前端不存在读取凭据明文的命令——参照 translate 模块的姿态:列表接口只回传 hasPassphrase: bool + 掩码串。

ssh-agent 集成(P1

  • 检测 Windows OpenSSH Agent 服务是否运行。
  • 「添加到 agent」/「从 agent 移除」操作。
  • 指明哪些密钥由 agent 托管(UI 上区分展示)。

known_hosts 管理(P1

  • 列表查看所有已知主机,支持搜索、删除单条、批量导入导出。
  • 变更告警历史留档。

4.5 文件快捷管理(P1

SFTP 双栏文件管理器

  • 左侧本地、右侧远程(或双远程,支持拖拽跨栏传输)。
  • 列视图:名称 / 大小 / 类型 / 权限 / 修改时间 / 所有者。支持排序、多选、框选。
  • 路径面包屑 + 可直接编辑路径 + 前进后退历史。
  • 权限可视化与编辑(rwxr-xr-x755 双向互转)。

文件操作

  • 新建目录 / 新建文件 / 重命名 / 删除(二次确认)/ 复制 / 移动。
  • 上传 / 下载:目录递归、进度显示、并发分片(多小文件并行,大文件单流)、断点续传、失败重试、队列管理。
  • 拖拽:从 Windows 资源管理器拖入上传;从远程栏拖出到本地栏下载。
  • 编辑远程文件:双击打开内置编辑器,保存时上传(P1 用 <textarea>P2 换 CodeMirror 带语法高亮)。

与终端的联动(这是本模块区别于普通 SFTP 客户端的核心)

  • 跟随 cwd:终端里 cd 后,SFTP 面板自动跟随(依赖 §4.1 的 OSC 7 hook)。
  • rz / sz 内联传输:拦截终端里的 sz <file>,自动弹出「保存到本地」对话框;拦截 rz,弹出「选择本地文件上传」。需要实现 ZMODEM 协议或调用 lrzszP2,但价值高)。
  • 选中即操作:终端里双击路径(如 /var/log/nginx/error.log)→ 右键菜单「用 SFTP 打开所在目录」。

本地文件管理(附带)

  • 「本地 Shell」会话同样挂载文件面板,可当轻量双栏文件管理器用(与快速面板的文件能力形成互补,不重复:快速面板面向「搜索定位」,这里面向「浏览操作」)。

4.6 快捷键体系(P0

分三层,边界清晰:

第一层:全局快捷键(走 shortcut.rs,与系统级冲突检测)

功能 默认值 说明
唤起快速会话菜单 Ctrl+Alt+T 类「新建终端」语义,弹浮层选主机/Shell
打开终端模块 无(不抢占) 建议不设,避免与用户既有习惯冲突

注意:shortcut.rs 的应用内冲突检测会拒绝「已被其他模块占用」的组合。截图默认 Ctrl+Alt+A、翻译面板默认 Ctrl+2。终端默认值需与此避让。

第二层:终端内快捷键xterm attachCustomKeyEventHandler 拦截,仅在终端聚焦时生效)

功能 Windows 键位 说明
复制 Ctrl+Shift+C Windows Terminal 惯例。不拦 Ctrl+C(必走 SIGINT
粘贴 Ctrl+Shift+V
选中即复制 可开关 习惯问题,默认关
新建标签 Ctrl+Shift+T
关闭标签 Ctrl+Shift+W 有活动进程时二次确认
下一个/上一个标签 Ctrl+Tab / Ctrl+Shift+Tab
跳转到第 N 标签 Alt+1..9
垂直/水平分屏 Ctrl+Shift+D / Ctrl+Shift+E
关闭分屏 Ctrl+Shift+Q
终端内搜索 Ctrl+Shift+F addon-search
清屏 Ctrl+Shift+K 发送 clearcls(按 shell 判断)
字体放大/缩小/复位 Ctrl+= / Ctrl+- / Ctrl+0
打开 SFTP 面板 Ctrl+Shift+P
命令片段库 Ctrl+Shift+S
重命名标签 F2
会话切换器(快速跳转) Ctrl+Shift+O 模糊搜索所有会话

第三层:Shell 内快捷键(终端原生,不改)

  • Ctrl+LCtrl+RCtrl+A/E/U/K 等一律透传给 shell,终端不拦截。

可配置性

  • 第二层全部可自定义,配置存 terminal/settings.json
  • 冲突检测:同一组合被两个动作占用时高亮提示。
  • 提供「重置为默认」。

4.7 命令增强(P1,体现「全能」)

  • 命令历史搜索:跨会话聚合历史(本地 shell 从 PowerShell 历史文件读,SSH 会话抓取输出流),Ctrl+R 增强版,模糊搜索 + 频次排序。
  • 命令片段库(Snippets:保存常用命令模板,支持 {{变量}} 占位符,选择时弹窗填参;支持分类与搜索;支持一键发送到当前会话。
  • 命令补全(P1):基于历史 + 片段做行内补全(类似 fish 的灰字建议),在 xterm 上叠加一层浮层实现。
  • AI 命令助手(P2:复用 translate 模块已配置的 AI 引擎(translate/settings.rs 里的 TranslateEngineConfig),把自然语言转成命令。「复用引擎配置而非另配一套」是关键——用户在翻译模块填过的 API Key 不该再填一遍。

4.8 与既有模块联动(P1/P2

联动对象 联动方式
快速面板 (a) 快速面板搜索里出现「打开 SSHprod-web-01」条目;(b) 快速面板输入 > ssh prod 直接建会话
剪贴板模块 终端内复制的内容进入剪贴板历史,可回溯找回;剪贴板历史的「粘贴到目标」支持终端
翻译模块 终端选中文本 → Ctrl+Alt+T 之类触发划词翻译(注意:需把终端进程加进 SelectionSettings.blacklist 的思考——实际上终端不在黑名单里,因为终端内 Ctrl+C 是复制语义由 xterm 处理,不会误触发;但需实测确认)
代理模块 SSH 连接可走 mihomo 代理(读 proxy/settings.jsonmixedPort,参照 translate/mod.rs::read_mixed_port 的写法:只读文件不依赖 Manager 状态
日志模块 连接失败、认证失败、主机密钥变更等关键事件写统一日志
下载器 SFTP 传输是否复用下载器的队列/进度 UI?(建议不复用——传输语义与 HTTP 下载差异大,共享 UI 会两边受限)

4.9 其他工程能力(补充项)

  • 终端外观:主题(跟随应用亮/暗 + 内置若干配色)、字体族与字号、行高、光标样式(块/竖线/下划线 + 闪烁)、滚动缓冲区行数(默认 10000)、背景透明度。
  • 编码:默认 UTF-8;SSH 老服务器可能是 GBK,需支持按会话指定编码(encoding_rs crate)。中文环境下这是刚需,不是可选项。
  • 日志与审计(P2:可开启「记录会话输入输出到文件」(合规场景),提供脱敏正则。
  • 安全基线
    • 禁止在日志中出现私钥、passphrase、密码。
    • 会话命令回显中若匹配到疑似密钥(如 -----BEGIN),提示用户。
    • 危险命令(rm -rf /dd)不做拦截(越权),但可做高亮提示(可选功能)。

5. 工程实现

5.1 Rust 侧目录结构

src-tauri/src/terminal/
├── mod.rs                  # TerminalManagerTauri State+ 设置读写
├── settings.rs             # 设置数据模型(#[serde(default)] 容器级默认)
├── commands.rs             # Tauri 命令层(薄:参数整形 / 校验 / 错误归类)
├── session.rs              # Session trait + SessionRegistryDashMap
├── pty/
│   ├── mod.rs
│   └── conpty.rs           # ConPTY 绑定、read/write 线程、resize 时序处理
├── shell.rs                # 本地 Shell 探测与启动参数组装
├── ssh/
│   ├── mod.rs              # SshBackend(实现 Session
│   ├── auth.rs             # 认证方式(公钥/密码/agent/键盘交互)
│   ├── hostkey.rs          # known_hosts 与指纹校验
│   ├── sftp.rs             # SFTP 客户端与传输队列
│   ├── forward.rs          # 端口转发(P2
│   └── config.rs           # ~/.ssh/config 解析
├── keys.rs                 # 密钥生成/导入/列举(含 passphrase 走 secrets.rs
├── snippets.rs             # 命令片段库
├── history.rs              # 命令历史(SQLite,参照 translate/history.rs
└── encoding.rs             # 编码转换(UTF-8 / GBK 等)

命令命名terminal_* 前缀,snake_case。预计 P0 约 30 个、P1 约 45 个命令。

注册顺序(严格按此,缺一不可):

  1. lib.rs mod terminal; + use terminal::{...} 导入命令
  2. lib.rs manage(TerminalManager::new(...))setup.rs 中构造,与 TranslateManager 同法)
  3. lib.rs invoke_handler![...] 追加命令
  4. lib.rs export_bindings()collect_commands![...] 追加同名命令 —— 漏掉这步前端就没有 commands.terminalXxx 类型
  5. RunEvent::ExitRequested 中追加 terminal.cleanup_on_exit()(关闭所有会话与 PTY
  6. constants.rs 新增 windows::TERMINAL_*events::TERMINAL_*

5.2 前端目录结构

src/modules/terminal/
├── index.ts                    # ModuleConfig(含 searchItems / lifecycle / order
├── TerminalModule.vue          # 主组件(布局容器)
├── components/
│   ├── SessionSidebar.vue      # 会话树(拖拽排序)
│   ├── TerminalTabs.vue        # 标签 + 分屏管理
│   ├── TerminalPane.vue        # xterm 实例宿主(单个会话)
│   ├── TerminalToolbar.vue     # 顶部工具条
│   ├── TerminalStatusBar.vue   # 底部状态栏
│   ├── HostEditorDialog.vue    # 主机编辑
│   ├── KeyManagerPanel.vue     # 密钥管理
│   ├── SftpPanel.vue           # 文件管理器(P1
│   ├── SnippetsPanel.vue       # 命令片段
│   └── QuickSessionPopup.vue   # 全局快捷键唤起的快速会话浮层
├── composables/
│   ├── useXterm.ts             # xterm 实例创建 / 插件装配 / 尺寸同步
│   ├── useSessionStream.ts     # 事件订阅 → 写入 xterm(含背压处理)
│   └── useTerminalKeys.ts      # 快捷键拦截与分发
└── settings/TerminalSettings.vue  # 设置页(挂进 settings 模块)

storesrc/stores/terminal.ts — 会话元数据(不持有 xterm 实例)、布局树、当前激活会话、设置缓存。

事件常量constants.ts 对应前端 src/lib/constants.ts):

事件名 负载 触发时机
terminal-output { sessionId, data: Vec<u8>base64 , seq } 会话有输出
terminal-exit { sessionId, code, signal } 会话进程/连接结束
terminal-state { sessionId, state } 状态机变更
terminal-cwd { sessionId, cwd } OSC 7 报告目录变化
terminal-sftp-progress { taskId, transferred, total, speed } 传输进度

背压是重点:大量输出(如 cat 大文件)时,事件频率会压垮 WebView。设计上用批次聚合——Rust 侧 8~16ms 窗口聚合一次,前端按 seq 校验无丢包;xterm 侧用 write(data, callback) 的回调控制写入节奏,配合 addon-webgl 提升渲染吞吐。

5.3 设置模型(terminal/settings.json

#[derive(Debug, Clone, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase", default)]   // 容器级默认:字段增减向后兼容
pub struct TerminalSettings {
    pub version: u32,                     // 结构版本,用于迁移判断(参照 translate 的 heal 模式)
    pub shells: Vec<ShellProfile>,        // 本地 Shell 配置
    pub hosts: Vec<SshHost>,              // SSH 主机条目
    pub layout: LayoutSettings,           // 标签/分屏默认行为
    pub appearance: AppearanceSettings,   // 主题/字体/光标/缓冲区
    pub shortcuts: Vec<ShortcutBinding>,  // 可自定义快捷键
    pub selection: SelectionSettings,     // 终端内选中行为(复制/粘贴策略)
    pub sftp: SftpSettings,               // 传输并发、覆盖策略、时间戳保留
    pub history: HistorySettings,         // 命令历史开关与条数
    pub security: SecuritySettings,       // 主机密钥策略、代理开关、编码默认值
}

version + heal() 模式必须沿用:参照 translate/settings.rs::heal ——老配置缺字段取默认值、失效引用自动回落、版本号推进。这是本项目已确立的向后兼容约定。

5.4 权限与窗口

  • 主窗口已具备 core:default 等权限,终端模块无需新增 capabilities(全部通过自定义命令走 IPC)。若做独立的快速会话浮层窗口,则需新增 capabilities/terminal-quick.json,参照 translate-popup.jsonNOACTIVATE + 预创建)。
  • 若后续要做「终端独立窗口」(P2),同样需要独立 capability。

5.5 依赖清单

RustCargo.toml —— 以下为实施后的实际形态(本节的规划值已被 §9/§10 修正,以这里为准)

# ===== SSH =====
russh = "0.63"             # 规划写 0.5;实际落地版本 0.63.3(见 §9.2 偏差表)
russh-sftp = "3.0"         # 【规划遗漏】russh 不含 SFTP,协议在独立 crate 里
                           # 版本配套:russh-sftp 3.0.0 依赖 `russh ^0.63.2`,与上面同源。
                           # 这一点是硬约束——SFTP 通道必须从**已认证的同一个
                           # Session** 上开,跨 patch 版本的类型不互通。

# ===== 编码 =====
encoding_rs = "0.8"        # GBK/GB18030/Big5/Shift_JIS/EUC-KR/latin1
                           # 依赖树里已存在(reqwest → encoding_rs 0.8.35),
                           # 提升为直接依赖**不新增编译单元**

# ===== 已存在可直接用 =====
# tokio / serde / serde_json / base64 / sha2 / rand / rusqlite / dirs / keyring / dashmap

# ===== 需开启 feature =====
windows-sys = { version = "0.52", features = ["Win32_System_Console", "Win32_System_Pipes",
                                              "Win32_System_Threading", "Win32_Foundation", ...] }
# 注意:InitializeProcThreadAttributeList / UpdateProcThreadAttribute /
# DeleteProcThreadAttributeList 虽属 Win32_System_Threading,但 0.52 未随 feature 导出,
# 用 `unsafe extern "system"` 自行声明(见 conpty.rs 尾部),
# 避免为三个函数开启一个大 feature 而显著拖长编译时间。

前端(package.json

@xterm/xterm
@xterm/addon-fit
@xterm/addon-webgl
@xterm/addon-search
@xterm/addon-web-links
@xterm/addon-unicode11
@xterm/addon-serialize        # P1

5.5.1 依赖数量校验(实施后)

cargo metadata --no-deps 结果:50 个直接依赖,0 重复。重点确认了两件事:

  1. russh 只有一份(russh-sftp 3.0.0 把 russh 列为 dev-dependency,不会重复引入)。
  2. encoding_rs 不会引入第二个 iconv 类 C 依赖——它是纯 Rust 实现。

5.6 模块注册

// src/modules/terminal/index.ts
export const moduleConfig: ModuleConfig = {
  id: 'terminal',
  name: '终端',
  icon: 'terminal',              // 需在 icons.ts 加映射 → lucide 的 SquareTerminal
  description: 'SSH 与本地 Shell 多会话终端,含密钥管理与文件传输',
  category: 'tool',
  defaultEnabled: true,
  loader: () => import('./TerminalModule.vue'),
  searchItems,                   // 见下方搜索项设计
  order: 18                      // 建议:proxy=10 / music=15 / terminal=18 / clipboard=20 / translate=25
}

src/modules/icons.ts 新增:

import { SquareTerminal } from '@lucide/vue'
// moduleIconMap 中追加
terminal: SquareTerminal

全局搜索项searchItems)建议覆盖:终端、本地 Shell、SSH 主机(动态)、密钥管理、known_hosts、命令片段、终端设置、外观、编码、快捷键。


6. 分期路线图

P0 — 骨架可跑(目标:本地 Shell + SSH 基本连得上 + 密钥管理)

Rust

  • TerminalManager 骨架 + Session trait + SessionRegistry
  • ConPTY 绑定(含 resize 时序、阻塞关闭、EOF 处理三个坑)
  • 本地 Shell 探测(PowerShell 7 / Windows PowerShell / cmd / Git Bash / WSL
  • 输出读线程 + 事件聚合(8~16ms 批处理)
  • OSC 7 cwd hook 注入与解析
  • secrets.rs 复用:密码 / passphrase 存取
  • 密钥生成(Ed25519 / RSA / ECDSA)、导入、列举、删除
  • SSH 连接(russh):公钥认证 + 密码认证
  • 主机密钥校验 + known_hosts 存储 + 指纹变更阻断
  • 设置模型 + heal() 迁移骨架
  • lib.rs 六处注册(含 collect_commands!+ cleanup_on_exit
  • constants.rs 窗口/事件常量

前端

  • 模块注册(index.ts / modules/index.ts / icons.ts
  • useXterm.ts:实例装配(fit + webgl + unicode11 + search + web-links
  • TerminalPane.vueI/O 绑定、尺寸同步、焦点管理
  • TerminalTabs.vue:多标签 + 关闭确认
  • SessionSidebar.vue:会话列表 + 状态点
  • 主机编辑对话框 + 密码短语输入
  • KeyManagerPanel.vue
  • 三层快捷键(第二层可配置)
  • 设置页(外观 / Shell / 快捷键)
  • stores/terminal.ts

P0 验收标准:能开 3 个本地 PowerShell 标签 + 2 个 SSH 会话(一个公钥、一个密码),秒级切换不卡顿,切到其他模块再回来会话仍在,Ctrl+Shift+C/V 可复制粘贴,关闭应用无残留进程。

P1 — 完整能力

  • SFTP 双栏文件管理器(浏览 / 上传 / 下载 / 目录递归 / 并发分片 / 断点续传)
  • 分屏(2×2
  • ssh-agent 集成
  • 键盘交互认证 / 2FA
  • ~/.ssh/config 导入
  • 命令历史聚合 + 增强搜索
  • 命令片段库
  • 会话侧栏拖拽分组、收藏
  • 会话快照序列化与恢复
  • 全局快捷键「快速会话浮层」
  • 快速面板联动(搜索项 + > ssh 语法)
  • SFTP 跟随 cwd
  • 编码支持(GBK
  • 终端内搜索、链接点击

P2 — 高级与生态

  • 端口转发(-L / -R)(2026-09-18 第五轮,见 §11.3-D SOCKS5 留作后续)
  • 跳板机链(ProxyJump)(2026-09-18 第四轮,见 §11.2
  • 连接复用(2026-09-18 第八轮,见 §11.8
  • AI 命令助手(复用 translate 引擎配置)(2026-09-18 第七轮,见 §11.7
  • rz / sz ZMODEM 内联传输 —— 已放弃2026-09-18 评审:SFTP 已覆盖主场景, 协议成本 600–800 行且无法单测主流程;详见实施记录 §11.9 评估)
  • 会话模板(一键拉起一组会话 + 布局)(2026-09-18 第六轮,见 §11.6
  • 终端独立窗口(P1 已交付 detach/attach;「拖出标签成窗」手势留后续)
  • 会话日志与审计(2026-09-18 第五轮,见 §11.4
  • PuTTY .ppk 导入(2026-09-18 第四轮,见 §11.1
  • 主机分组同步(导入导出配置)(2026-09-18 第六轮,见 §11.5

7. 风险清单

# 风险 影响 缓解
1 ConPTY resize 竞态导致 TUI 程序(vim/htop)花屏 首帧后延迟应用尺寸;监听 WINDOW_BUFFER_SIZE_EVENT 校正;实测 vim/top/less
2 ClosePseudoConsole 阻塞导致退出卡死 独立线程 + 先取消 ReadFilecleanup_on_exit 带超时(参照 MonitorKernel 的 3s recv_timeout 写法)
3 输出洪流压垮 WebViewcat 大文件) Rust 侧 8~16ms 批次聚合 + seq 校验;xterm write 回调节流;addon-webgl
4 russh API 破坏性变更 锁定小版本;Session trait 隔离,必要时可换 ssh2
5 主机密钥校验被用户习惯性点过(TOFU 疲劳) 高(安全) 首次连接突出展示指纹;变更时红色阻断而非黄色提示;提供「仅本次接受」与「永久接受」区分
6 多会话内存占用(每个 xterm 实例 + 滚动缓冲) 默认缓冲 10000 行;会话数量上限提示;非激活标签暂停渲染
7 中文宽字符对齐错乱 必装 addon-unicode11;实测 ls -l 中文文件名的列对齐
8 SSH 老服务器 GBK 编码乱码 encoding_rs 按会话转码;默认 UTF-8 可选 GBK
9 私钥文件被其他进程读取 中(安全) keys/ 目录权限收紧;passphrase 存凭据管理器;UI 提示用户优先使用带 passphrase 的密钥
10 Cargo.toml 编码损坏影响新增依赖的 diff 独立清理项,建议在动工前修复
11 分屏 × 标签 × 会话的组合复杂度爆炸 分屏上限 2×2;布局用树结构表达并单测
12 全局快捷键与应用内快捷键语义混淆 明确三层边界,UI 上一处分开展示;不做「全局拦截 Ctrl+C」这类危险映射

8. 待确认决策项

动工前需要拍板的四项,我给出倾向但需要用户确认:

  1. SSH 库:倾向 russh(async 契合)。若用户更看重稳定性与既有经验,可改 ssh2
  2. ConPTY:倾向直接绑定 windows-sys(与项目现有原生态一致)。若更看重开发速度,可用 portable-pty
  3. P0 范围:本规划把「SSH + 密钥管理 + 多会话 + 快捷键」全放进 P0,工作量偏大(8~12 天)。若希望更快见到可用版本,可将 SSH 拆到 P0.5,先交付「本地 Shell + 多会话 + 快捷键」。
  4. 是否需要终端独立窗口:影响窗口与 capability 设计,早定早省事。

附录 A:与既有模块的范式对照

范式 既有实现 终端模块对应
模块 ID / 分类 translatecategory: 'tool'
设置持久化 {app_data_dir}/<module>/settings.json {app_data_dir}/terminal/settings.json
设置兼容 容器级 #[serde(default)] + heal() + version 完全沿用
凭据存储 secrets.rs + 服务名 "Thing" 完全复用,仅新增键名约定
命令层姿态 translate/commands.rs「薄」:整形/校验/归类
类型绑定 tauri-spectasrc/lib/bindings.ts 必须复用
事件命名 kebab-casetranslate-stream-chunk terminal-output / terminal-exit / ...
快捷键 shortcut.rs 原子注册 + 冲突检测 完全复用
退出清理 RunEvent::ExitRequested 逐个 cleanup_on_exit 追加 TerminalManager
历史存储 translate/history.rsSQLite terminal/history.rs 同法
原生浮层窗口 translate-popupNOACTIVATE 预创建) 快速会话浮层参照

附录 B:命名规范落点

类型 规范 示例
前端组件 PascalCase TerminalPane.vue
前端文件 kebab-case use-session-stream.tscomposable 目录内用 camelCase 前缀 use
Pinia store camelCase 文件 src/stores/terminal.ts
Rust 模块 snake_case terminal/pty/conpty.rs
Tauri 命令 terminal_ + snake_case terminal_open_session
Tauri 事件 kebab-case terminal-output
凭据键名 terminal-<用途>-<id> terminal-key-passphrase-{keyId}
设置字段 camelCaseserde rename_all maxScrollback

9-11. 实施记录(P0–P2 精编)

本节为 2026-09-18 全链路审查时按「精简」要求压缩的版本:保留全部架构决策、 坑记录与语义备忘,省略逐轮的过程性叙述与重复的验证表。按阶段分节的原始 详版(P0 §9 / P1 §10 / P2 §11,共 8 轮)记录在 git 历史与当日工作日志中。

交付总览

阶段 交付 状态
P0 本地 ShellConPTY)、SSH 连接、多标签、密钥管理、快捷键骨架
P1 SFTP 双栏、分屏 2×2、命令片段库、命令历史(OSC 133)、编码切换、Cargo.toml 修复
P2 .ppk 导入、ProxyJump 跳板链、端口转发 -L/-R、会话日志与审计、主机导入导出、会话模板、AI 命令助手、连接复用
P2 终端独立窗口 P1 交付 detach/attach
P2 ZMODEM 已放弃(评估见下)
审查 全链路审查:修复 2 个 P0 级前端缺陷 + 1 个后端竞态 + 4 个中低问题

分阶段决策摘要

P0(骨架)

  • ConPTY 直接用 windows-sysCreatePseudoConsole 三函数自行声明(避免拖入大 feature)。
  • 会话抽象 Session trait:本地/SSH 双后端共用命令层;ProcessManager 不适用(N 会话 + 双向流 + 退出不重启)。
  • SessionId 用短序号(s1…),会出现在窗口 label 与日志。
  • 密码/密钥 passphrase 分离存储:密码进系统凭据管理器(按 id 键名),私钥本体落 keys/ 目录。

P1(完整能力)

  • SFTPrussh 不含 SFTP → 引入 russh-sftp;通道挂在会话连接上(非独立连接)。
  • 分屏 = 新建会话 + 并排渲染(tmux 语义),上限 4(WebGL 上下文约束);CSS Grid 布局。
  • 命令片段:占位符 ${name} 语法只在 Rust 侧实现一份(前端自己写正则必分叉);两步执行(填入 vs 执行)。
  • 命令历史:OSC 133 + 1337 提取命令边界;本地用 shell hook 上报 cwd;不做 DROP 重建式迁移。
  • 编码切换:解码在前端(用户可切编码重看历史),读写两侧都从会话状态现取。

P2(高级与生态,共 8 轮)

  • .ppk 导入:ssh-keyppk feature(russh 不转发 → 自己声明同版本号 =0.7.0-rc.11);PPK 解析后统一转 OpenSSH 落盘。
  • ProxyJumprussh 无内置 → 逐跳手搭(direct-tcpip 通道流 + connect_stream);跳板与直连同权校验。
  • 端口转发 -L/-Rdirect-tcpip + copy_bidirectional / tcpip_forward + Handler 白名单回调;规则挂会话不持久化。
  • 会话日志:双后端 flush_output 单点挂钩;记原始字节含 ANSI;只记输出不记输入(密码安全)。
  • 主机同步:JSON 备份只含配置不含密码/私钥;导入重编 id(凭据键名冲突)+ 重写跳板链 + 三元组去重。
  • 会话模板:捕获当前可见面板集合;拉起 = 逐条开会话 + addPane;只存 target 引用。
  • AI 助手:复用翻译模块引擎配置(chat_once 通用补全出口);三层解析防御;默认填入不执行。
  • 连接复用:连接池按「用户名|host:port|auth|材料指纹」共享 SSH 连接;引用计数归零才断开。

关键架构语义备忘(跨模块契约)

  1. ssh-key 0.7decrypt()/encrypt() 都是 &self → Result<Self> 转换语义——返回值必须接住; 丢返回值 = 仍在加密态(坑 31,曾导致加密私钥导入从未成功过)。
  2. encrypt() 会清空内存对象的注释(重建 public_key),但加密载荷里含注释(decrypt 可读回); set_comment 必须在 encrypt 之后调用。
  3. collect_commands!(导出绑定)与 generate_handler!(运行时注册)是两份独立清单—— 新增命令必须双清单登记;前端用原生 invoke + 手写镜像类型(translate 先例,terminal 跟随)。
  4. export_bindings() 失败是运行时的:specta 类型注册表全局按名索引, 跨模块同名 Type 派生类型会让应用启动即 paniccargo check 完全看不见)。
  5. tauri-specta derive 路径无法重命名类型#[specta(rename)] 只对函数宏生效)—— 通用词(Settings/HistoryPage/Item…)一律加模块前缀。
  6. Write 契约:前端 store.write(string) 必须 TextEncoder 编码后 base64 (后端严格解码);xterm onData / 粘贴走字符串分支。
  7. vue-draggable-plustarget 是跨容器专用 prop,且 querySelector 不匹配元素自身—— 单容器排序禁止传 target。
  8. 连接池槽位是 tokio Mutex(连接建立期跨 .await 持锁,天然串行化同主机并发连接); sftp/转发的同步访问走 spawn_blocking + block_on,锁在 block_on 内获取。
  9. 跳板 Handle 挂池条目而非首建会话——否则首建会话关闭剪断他人隧道。
  10. -R 入站路由按端口全局匹配:连接级 Handler 的 session_id 属于首建会话; 远程监听端口全局唯一(add 时强制)。
  11. chat_oncetranslate 根 re-export)是终端 AI 助手的唯一 API 配置源—— 终端不持有任何引擎配置副本。
  12. 面板常驻挂载renderPanes 含全部会话,v-show 切可见性—— 切标签/分屏绝不销毁 xterm 实例(缓冲与隐藏期输出不丢)。

坑记录(35 条精编)

# 一句话 修复/规避
1 ssh-key 双版本分叉(0.6 vs russh 钉的 0.7 只用 russh re-export;例外须同版本号声明
2 ssh-key 的 getrandom feature 门控(rand_core 0.10 直接依赖 getrandom 0.4 + UnwrapErr(SysRng)
3 #[specta::specta]#[tauri::command] 必须成对 漏一个 = 绑定缺失或运行时不可调
4 windows-sys 0.52 的 HANDLE/HPCON 是 isize 注意类型转换
5 ConPTY 三个时序陷阱(先建管道再建 PTY 等) 见 pty::conpty 注释
6 create_pipe() 已返回 File,不要再转一次
7 xterm 无 selectWordAt(自实现选择词语) 右键菜单自定义
8 PowerShell 写文件产出 UTF-16LE 让程序自己写或 Python 落盘
9 密码与配置分离存储(凭据管理器 vs settings.json 永不明文落盘
10 新建主机先向后端要 id(密码按 id 存取) id 规则单点
11 russh 无 SFTP → 引入 russh-sftp 通道复用连接
12 SFTP 通道借用 Handle 需 spawn_blocking+block_on Handle 不可 Clone、不能跨 await 持锁
13 WebGL 上下文上限 4 个(黑屏风险) maxPanes 封顶 + onContextLoss 回退
14 分屏容器是标签级的,切标签要重置 resetPanesTo(见坑 36 修正)
15 本机 Bash 缺 coreutils,管道全部失真 验证命令重定向到文件后用 Python 读
16 npx 触发 wsl.exe 黑名单拦截 直接调 JS 入口
17 PowerShell 重定向产出 UTF-16LE 同 8
18 vite build 重定向+后台 = 假死(非 OOM 构建一律前台跑
19 impl 块放错位置 → trait 方法「已实现却报未实现」
20 Session trait 未引入时报错指不到成因 显式 use
21 OSC 133 命令文本与结束标记是两个独立序列 必须累积
22 1337133 共享前缀,判断顺序错了静默失效 先判长前缀
23 ${x#"$y"} 类语法在 Rust 字符串里写不出 换等价写法
24 两份 scan_control_sequences 拷贝按后端分支出诡异 bug 收敛到一处
25 命令历史遵守 HISTCONTROL=ignorespace 惯例 前导空格不记录
26 FTS 与 LIKE 双路径查询需一致性测试
27 export_bindings() 失败是运行时的(编译全绿 ≠ 能启动) 新增 Type 必须实际跑二进制
28 两份命令清单不自动同步 双清单登记 + 交叉注释
29 重定向/后台让验证命令本身不可信 前台对照实验
30 vue-draggable-plustarget 是跨容器专用(querySelector 不搜自身) 单容器禁用 target
31 decrypt()/encrypt() 是转换语义,丢返回值 = 加密私钥导入从未成功 接住 Result<Self>
32 encrypt() 清空内存对象注释(载荷里有) set_comment 在 encrypt 后
33 trait object 不能挂两个非 auto traitE0225 合并 trait + blanket impl
34 russh 对 forwarded-tcpip 默认全收 Handler 白名单覆写
35 连接复用后 -R 入站按 session_id 路由永不命中 全局端口匹配 + 唯一性
36 [审查轮] 切标签销毁 xterm 实例、隐藏期输出被丢弃 renderPanes 常驻全部会话 + v-show
37 [审查轮] store.write 字符串分支未编码 → 键盘输入完全失效 TextEncoder 后 base64

全链路审查(2026-09-18P2 收官)

探查代理 + 人工复核,确认并修复 7 项(另排除 2 项误报):

# 级别 问题 修复
1 P0 store.write 字符串分支未 base64 编码——xterm 键盘输入/粘贴全部被后端拒绝,终端无法打字(坑 37 TextEncoder 编码后再 base64
2 P0 切标签卸载其他会话的 TerminalPane:xterm 缓冲丢失、隐藏期输出被丢弃(坑 36) renderPanes 常驻全部会话 + v-show
3 连接期间关闭标签的竞态:do_connect 复活会话(Established 覆盖 Closed)、连接写进已拆除的池条目永不断开 do_connect 三处 closed 检查点,命中则断开新连接并放弃
4 closeTab 分屏组误判:分屏激活时关后台标签会误关分屏组而非目标 仅当目标在分屏组内才按组关闭
5 SFTP 面板在 SSH 会话间切换不关旧通道(泄漏) <SftpPanel :key="sessionId"> 强制重建
6 HistoryPanel 防抖定时器卸载不清理 onBeforeUnmount clearTimeout
7 Alt+1..9 要求焦点在 .xterm 内(侧栏/对话框下失效) 只在输入控件聚焦时让路

已排除的误报:「兜底 watch 只看 sessions.length」(实际有 activeSessionId 有效性校验)等。

性能结论:输出管线(8ms 聚合窗口 + base64 + 事件)与渲染(WebGL + 回退)无热点; 面板常驻化后 xterm 实例数 = 会话数,WebGL 超限已有回退兜底。无需要改动的热路径。

ZMODEM 评估(已放弃)

完整协议(帧结构 / CRC-16+32 / 转义编码 / 滑动窗口重同步 / 双向状态机)约 600–800 行, 调试依赖真实 rz/sz 对端,无法用单测覆盖主流程。SFTP 已覆盖绝大多数文件传输场景, ZMODEM 剩余价值主要在串口/老旧嵌入式设备。成本收益不成立,正式放弃; 若未来出现需求,建议独立一轮且优先做 sz 下载方向。

验证汇总(收官状态)

检查 结果
cargo check exit 0(警告数与 P0 基线一致)
cargo test --lib 91/91 通过keys 17 + commands 9 + assistant 6 + audit 5 + pool 5 + 既有 49
vue-tsc --noEmit exit 0
类型重名扫描 / 池引用计数 / 解析防御 单测覆盖

待用户真机验证清单

  1. 终端键盘输入与粘贴(审查轮修复 #1——此前从未被测出)。
  2. 多标签切换不丢缓冲、隐藏期输出不丢(修复 #2)。
  3. 连接复用:同主机双标签秒连、关一个另一个不受影响、全关后连接断开。
  4. ProxyJump 跳板链、端口转发 -L/-R、.ppk 导入、会话模板拉起、AI 助手(需翻译引擎配置)。

附:P1 新增命令清单(供前端对接与后续维护)

命令 参数 返回
terminal_sftp_is_open sessionId bool
terminal_sftp_open sessionId ActionOutcome(幂等)
terminal_sftp_close sessionId ()
terminal_sftp_list sessionId, path RemoteDir
terminal_sftp_parent path string
terminal_sftp_read_link sessionId, path string
terminal_sftp_mkdir sessionId, path ()
terminal_sftp_delete sessionId, path, isDir ActionOutcome
terminal_sftp_rename sessionId, from, to ()
terminal_sftp_upload sessionId, localPath, remotePath ActionOutcome
terminal_sftp_download sessionId, remotePath, localPath ActionOutcome
terminal_session_cwd_value sessionId string
terminal_open_local_path path ()
terminal_reveal_local_path path ()
terminal_list_snippets SnippetView[]
terminal_save_snippet snippet SnippetView[]
terminal_delete_snippet snippetId SnippetView[]
terminal_render_snippet snippetId, values string
terminal_run_snippet sessionId, snippetId, values, submit ActionOutcome
terminal_restore_default_snippets SnippetView[]

新增事件terminal-transfer-progress(负载 TransferProgress200ms 节流)。

追加:P1 第四 / 第五批(字符编码 + 命令历史)

命令 参数 返回 备注
terminal_set_encoding sessionId, encoding string 返回后端 normalize 后的规范名,前端须以此回写;内部已 emit TERMINAL_STATE,前端不要重复刷新
terminal_history_query query: HistoryQuery HistoryPage keyword 短于 3 字符自动走 LIKE 回退(trigram 索引对 ls/cd 无效)
terminal_history_sources HistorySource[] 只返回有历史记录的来源,不是全部主机列表
terminal_history_toggle_favorite id bool 返回切换后的状态
terminal_history_delete id ActionOutcome
terminal_history_clear keepFavorites: Option<bool> ActionOutcome 省略时默认 true(保留收藏);ActionOutcome 带删除条数
terminal_history_run sessionId, command, submit: Option<bool> ActionOutcome submit 省略即 false(只填入不执行);命令内 \r/\n 折叠为空格

前端配套约定

  • SessionInfo.encoding 是唯一编码事实源;状态栏下拉改值走 terminal_set_encoding,成功后回写 store 里的 session.encoding
  • 输出侧解码在 useXtermdecoderFor(encoding) 缓存 TextDecoder);Rust 侧永不转码,只发原始字节的 base64。
  • 输入侧重编码在 SshSession::write(本地会话不需要 —— Windows 控制台收的是 UTF-8UTF-16 转换由 ConPTY 负责)。
  • 历史面板 Ctrl+Shift+H;单击填入、双击 / Enter 填入并执行(默认不执行是刻意的取舍,理由见 §10.12)。