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

842 lines
55 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 终端模块规划(Terminal Module Plan
> 状态:**P0 骨架已落地;P1 进行中**(Rust 侧:cwd 跟踪 / 状态事件 / GBK 编码 / SFTP 后端已完成并编译通过)
> 定位:Thing 工具集的第 11 个模块,`id = terminal``category = '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 上的硬约束,且与已有基建同构:`ProcessManager``download_engine` 都在 Rust 侧管进程。理由有三——(a) WebView2 无 PTY 访问;(b) 前端持有的子进程会在页面重载时变孤儿;(c) 多标签、后台保活、断线重连都需要一个独立于 UI 生命周期的宿主。
3. **SSH 走「自研客户端 + 真实 PTY」,而不是「拼接 ssh.exe + ConPTY」**。后者实现快但天花板低:无法做 SFTP 复用连接、无法读主机密钥指纹、无法做跳板机链、无法统一错误模型、`ssh.exe` 的输出会与 ConPTY 的 ANSI 处理打架。代价是 Ruffles/ssh2 的移植与 ConPTY 绑定要自己写,收益是整个能力面没有上限。
**总工作量估算**:P0 骨架(本地 Shell + 多会话 + 密钥管理)约 8~12 个工作日;P1(SSH/SFTP 完整能力)约 15~20 个工作日;P2(高级能力)按需。**建议按 P0 先行落地可用版本,再迭代。**
> **收官状态(2026-09-18**P0P2 全部落地(ZMODEM 经评估放弃,见实施记录),
> 并完成一轮全链路审查(修复键盘输入失效、切标签丢缓冲、连接期关闭竞态等 7 项)。
> 单测 91/91、`cargo check` 与 `vue-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 |
| (我补充) | **本地 Shell**PowerShell/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.ts`debug 构建时导出 | **必须复用**,终端命令量较大,手写 `invoke` 类型不可接受 |
| 凭据存储 | `src-tauri/src/secrets.rs``keyring` + Windows 凭据管理器(DPAPI),服务名固定 `"Thing"` | **直接复用**,见 §4.4 |
| 全局快捷键 | `src-tauri/src/shortcut.rs`:原子化注册 + 应用内冲突检测 + 占用表 | **直接复用**,见 §4.6 |
| 托盘 | `src-tauri/src/tray_menu.rs` | 可挂「新建会话」入口(P2) |
| 日志 | `src-tauri/src/logger.rs``log_info` / `log_warn` / `log_error`) | 继承统一日志,日志页可过滤 |
| 窗口常量 | `src-tauri/src/constants.rs``windows` / `events` | 需新增窗口与事件常量 |
| 弹窗范式 | `translate-popup` 的 NOACTIVATE 预创建窗口 + `capabilities/translate-popup.json` | 终端「快速会话/命令补全」浮层可参照 |
### 2.2 关键缺口(需要新增依赖)
| 缺口 | 现状 | 方案 |
|---|---|---|
| ConPTY 绑定 | 无。`windows-sys` 未开启 `Win32_System_Console` | 开启该 feature;或引入 `portable-pty`(见 §3.1 取舍) |
| SSH 客户端 | 无 | 引入 `russh`(纯 Rust)或 `ssh2`libssh2 绑定) |
| 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+ 提供 **ConPTY**`CreatePseudoConsole`),是 Windows Terminal 的底层机制。三条路径:
| 方案 | 优势 | 代价 | 判断 |
|---|---|---|---|
| `portable-pty`(wezterm 提取库) | 跨平台、API 干净、久经考验 | 引入一个非 Tauri 生态的大依赖;其 Windows 后端同样走 ConPTY,出问题时要下钻 | 可接受 |
| **直接绑 `windows-sys` 的 ConPTY** | 零额外依赖、完全可控、与项目已有 `windows-sys` 姿态一致 | 需自行处理 pseudo console handle 生命周期、read/write 线程、resize 时序 | **推荐** |
| `conpty` 窄封装 crate | 上手快 | 维护活跃度不确定 | 备选 |
**推荐直接绑定 `windows-sys`**,理由:项目已有大量原生 Win32 调用(`win32_util.rs``screenshot/wgc_capture.rs``translate/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` | `ssh2`libssh2 绑定) |
|---|---|---|
| 构建 | 纯 Rust,无 C 依赖,交叉编译友好 | 需 libssh2Windows 下常走 vendored 编译 |
| async | 原生 async,与现有 `tokio` 运行时契合 | 同步阻塞,需 `spawn_blocking` 包装 |
| 算法覆盖 | 新算法跟进快(如 `chacha20-poly1305``sntrup761x25519` | 受 libssh2 版本限制 |
| 稳定性 | API 演进较快,偶有破坏性变更 | 老牌稳定,几乎不再变化 |
| 与 `tokio` 集成 | 直接 | 需额外线程池,与 `ProcessManager` 的线程模型并存会增加心智负担 |
**推荐 `russh`**。决定性理由是 **async 契合度**Cargo.toml 已启用 `tokio``rt-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-serialize`(P1) | 会话快照序列化,用于恢复 |
### 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 抽象,`ConPtyBackend``SshBackend` 都实现它(`write` / `resize` / `kill` / `subscribe_output`)。这样上层命令层(`terminal_write``terminal_resize`)无需区分本地与远程,多会话管理逻辑只需写一遍。
---
## 4. 功能规格
### 4.1 本地 ShellP0
- **Shell 探测**:启动时枚举可用 Shell,按顺序探测——
- PowerShell 7+`pwsh.exe`,优先)
- Windows PowerShell`powershell.exe`
- cmd`cmd.exe`
- Git Bash`bash.exe`,从 `git --exec-path` 反推)
- WSL 发行版(`wsl.exe -l -q` 枚举)
- **Shell 配置**:每个 Shell 可配可执行路径、启动参数、工作目录、环境变量覆盖、启动时执行命令(如 `cd /d/project && claude`)。
- **默认工作目录**:记住上次 cwd;新建会话时可选「跟随当前项目目录」。
- **注意 cwd 同步**ConPTY 拿不到子进程的真实 cwd(`GetCurrentDirectory` 只反映父进程)。需要**注入 shell hook**PowerShell 用 `$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`、动态转发 `-D`SOCKS5)。
- 连接复用(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 `.ppk`**Windows 用户存量多,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-x``755` 双向互转)。
**文件操作**
- 新建目录 / 新建文件 / 重命名 / 删除(二次确认)/ 复制 / 移动。
- 上传 / 下载:目录递归、进度显示、**并发分片**(多小文件并行,大文件单流)、断点续传、失败重试、队列管理。
- 拖拽:从 Windows 资源管理器拖入上传;从远程栏拖出到本地栏下载。
- 编辑远程文件:双击打开内置编辑器,保存时上传(P1 用 `<textarea>`P2 换 CodeMirror 带语法高亮)。
**与终端的联动(这是本模块区别于普通 SFTP 客户端的核心)**
- **跟随 cwd**:终端里 `cd` 后,SFTP 面板自动跟随(依赖 §4.1 的 OSC 7 hook)。
- **`rz` / `sz` 内联传输**:拦截终端里的 `sz <file>`,自动弹出「保存到本地」对话框;拦截 `rz`,弹出「选择本地文件上传」。需要实现 ZMODEM 协议或调用 `lrzsz`P2,但价值高)。
- **选中即操作**:终端里双击路径(如 `/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` | 发送 `clear``cls`(按 shell 判断) |
| 字体放大/缩小/复位 | `Ctrl+=` / `Ctrl+-` / `Ctrl+0` | |
| 打开 SFTP 面板 | `Ctrl+Shift+P` | |
| 命令片段库 | `Ctrl+Shift+S` | |
| 重命名标签 | `F2` | |
| 会话切换器(快速跳转) | `Ctrl+Shift+O` | 模糊搜索所有会话 |
**第三层:Shell 内快捷键**(终端原生,不改)
- `Ctrl+L``Ctrl+R``Ctrl+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.json``mixedPort`,参照 `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 模块)
```
**store**`src/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`
```rust
#[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.json`NOACTIVATE + 预创建)。
- 若后续要做「终端独立窗口」(P2),同样需要独立 capability。
### 5.5 依赖清单
**Rust`Cargo.toml`** —— 以下为**实施后的实际形态**(本节的规划值已被 §9/§10 修正,以这里为准)
```toml
# ===== 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 模块注册
```typescript
// 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` 新增:
```typescript
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.vue`I/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 — 高级与生态
- [x] 端口转发(`-L` / `-R`)(2026-09-18 第五轮,见 §11.3`-D` SOCKS5 留作后续)
- [x] 跳板机链(ProxyJump)(2026-09-18 第四轮,见 §11.2
- [x] 连接复用(2026-09-18 第八轮,见 §11.8
- [x] AI 命令助手(复用 translate 引擎配置)(2026-09-18 第七轮,见 §11.7
- [x] `rz` / `sz` ZMODEM 内联传输 —— **已放弃**2026-09-18 评审:SFTP 已覆盖主场景,
协议成本 600–800 行且无法单测主流程;详见实施记录 §11.9 评估)
- [x] 会话模板(一键拉起一组会话 + 布局)(2026-09-18 第六轮,见 §11.6
- [x] 终端独立窗口(P1 已交付 detach/attach;「拖出标签成窗」手势留后续)
- [x] 会话日志与审计(2026-09-18 第五轮,见 §11.4
- [x] PuTTY `.ppk` 导入(2026-09-18 第四轮,见 §11.1
- [x] 主机分组同步(导入导出配置)(2026-09-18 第六轮,见 §11.5
---
## 7. 风险清单
| # | 风险 | 影响 | 缓解 |
|---|---|---|---|
| 1 | ConPTY resize 竞态导致 TUI 程序(vim/htop)花屏 | 中 | 首帧后延迟应用尺寸;监听 `WINDOW_BUFFER_SIZE_EVENT` 校正;实测 vim/top/less |
| 2 | `ClosePseudoConsole` 阻塞导致退出卡死 | **高** | 独立线程 + 先取消 `ReadFile``cleanup_on_exit` 带超时(参照 `MonitorKernel` 的 3s `recv_timeout` 写法) |
| 3 | 输出洪流压垮 WebView`cat` 大文件) | **高** | 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 / 分类 | `translate``category: 'tool'` | 同 |
| 设置持久化 | `{app_data_dir}/<module>/settings.json` | `{app_data_dir}/terminal/settings.json` |
| 设置兼容 | 容器级 `#[serde(default)]` + `heal()` + `version` | 完全沿用 |
| 凭据存储 | `secrets.rs` + 服务名 `"Thing"` | 完全复用,仅新增键名约定 |
| 命令层姿态 | `translate/commands.rs`「薄」:整形/校验/归类 | 同 |
| 类型绑定 | `tauri-specta``src/lib/bindings.ts` | 必须复用 |
| 事件命名 | `kebab-case``translate-stream-chunk` | `terminal-output` / `terminal-exit` / ... |
| 快捷键 | `shortcut.rs` 原子注册 + 冲突检测 | 完全复用 |
| 退出清理 | `RunEvent::ExitRequested` 逐个 `cleanup_on_exit` | 追加 `TerminalManager` |
| 历史存储 | `translate/history.rs`SQLite | `terminal/history.rs` 同法 |
| 原生浮层窗口 | `translate-popup`NOACTIVATE 预创建) | 快速会话浮层参照 |
## 附录 B:命名规范落点
| 类型 | 规范 | 示例 |
|---|---|---|
| 前端组件 | PascalCase | `TerminalPane.vue` |
| 前端文件 | kebab-case | `use-session-stream.ts`composable 目录内用 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. 实施记录(P0P2 精编)
> 本节为 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-sys``CreatePseudoConsole` 三函数自行声明(避免拖入大 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-key``ppk` feature(russh 不转发 → 自己声明同版本号 `=0.7.0-rc.11`);PPK 解析后统一转 OpenSSH 落盘。
- ProxyJumprussh 无内置 → 逐跳手搭(`direct-tcpip` 通道流 + `connect_stream`);跳板与直连同权校验。
- 端口转发 -L/-R`direct-tcpip` + `copy_bidirectional` / `tcpip_forward` + Handler 白名单回调;规则挂会话不持久化。
- 会话日志:双后端 `flush_output` 单点挂钩;记原始字节含 ANSI;只记输出不记输入(密码安全)。
- 主机同步:JSON 备份只含配置不含密码/私钥;导入重编 id(凭据键名冲突)+ 重写跳板链 + 三元组去重。
- 会话模板:捕获当前可见面板集合;拉起 = 逐条开会话 + addPane;只存 target 引用。
- AI 助手:复用翻译模块引擎配置(`chat_once` 通用补全出口);三层解析防御;默认填入不执行。
- 连接复用:连接池按「用户名|host:port|auth|材料指纹」共享 SSH 连接;引用计数归零才断开。
### 关键架构语义备忘(跨模块契约)
1. **`ssh-key 0.7``decrypt()`/`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` 派生类型会让应用启动即 panic`cargo check` 完全看不见)。
5. **`tauri-specta` derive 路径无法重命名类型**(`#[specta(rename)]` 只对函数宏生效)——
通用词(Settings/HistoryPage/Item…)一律加模块前缀。
6. **`Write` 契约**:前端 `store.write(string)` 必须 TextEncoder 编码后 base64
(后端严格解码);xterm onData / 粘贴走字符串分支。
7. **`vue-draggable-plus``target` 是跨容器专用 prop**,且 `querySelector` 不匹配元素自身——
单容器排序禁止传 target。
8. **连接池槽位是 tokio Mutex**(连接建立期跨 `.await` 持锁,天然串行化同主机并发连接);
sftp/转发的同步访问走 `spawn_blocking + block_on`,锁在 block_on 内获取。
9. **跳板 Handle 挂池条目**而非首建会话——否则首建会话关闭剪断他人隧道。
10. **`-R` 入站路由按端口全局匹配**:连接级 Handler 的 session_id 属于首建会话;
远程监听端口全局唯一(add 时强制)。
11. **`chat_once`translate 根 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 | `1337``133` 共享前缀,判断顺序错了静默失效 | 先判长前缀 |
| 23 | `${x#"$y"}` 类语法在 Rust 字符串里写不出 | 换等价写法 |
| 24 | 两份 `scan_control_sequences` 拷贝按后端分支出诡异 bug | 收敛到一处 |
| 25 | 命令历史遵守 `HISTCONTROL=ignorespace` 惯例 | 前导空格不记录 |
| 26 | FTS 与 LIKE 双路径查询需一致性测试 | — |
| 27 | `export_bindings()` 失败是运行时的(编译全绿 ≠ 能启动) | 新增 Type 必须实际跑二进制 |
| 28 | 两份命令清单不自动同步 | 双清单登记 + 交叉注释 |
| 29 | 重定向/后台让验证命令本身不可信 | 前台对照实验 |
| 30 | `vue-draggable-plus``target` 是跨容器专用(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`(负载 `TransferProgress`200ms 节流)。
### 追加: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`
- 输出侧解码在 `useXterm``decoderFor(encoding)` 缓存 `TextDecoder`);**Rust 侧永不转码**,只发原始字节的 base64。
- 输入侧重编码在 `SshSession::write`(本地会话不需要 —— Windows 控制台收的是 UTF-8UTF-16 转换由 ConPTY 负责)。
- 历史面板 `Ctrl+Shift+H`;单击**填入**、双击 / `Enter` **填入并执行**(默认不执行是刻意的取舍,理由见 §10.12)。