Files
opsxshell/docs/architecture.md
T
Logan.LiandCursor 08696896f9 feat: harden local shell, SFTP, and desktop session reliability
Finder-launched terminals now inherit a login PATH so tools like ollama resolve, destructive SFTP/editor actions require confirmation, and terminal search plus window dragging work again. SSH forwards and RDP sessions keep listeners and survive reconnect without FastPath crashes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-16 16:08:11 +08:00

175 lines
11 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.
# 架构说明
## 产品边界
OpsXShell 是简单好用的原生运维终端:短路径完成连接、终端、传文件、转发和远程桌面。审计、录像、审批和堡垒机能力不在客户端范围内。
## 目录与职责
```text
src/ React 界面、工作区和 Zustand 状态
src/i18n/ 界面文案(zh-CN / zh-TW / en / ja / ko / vi / ru)与 locale 检测
src/platform/ Tauri command/event 适配层
shared/types/ 渲染层 TypeScript 契约
src-tauri/src/commands/ 参数边界和统一响应
src-tauri/src/domain/ Rust 领域模型
src-tauri/src/services/ SSH、SFTP、隧道、PTY、串口、桌面、AI、存储和安全策略
```
React 不直接访问 SSH、文件系统或加密实现。Rust 服务持有连接、Shell、SFTP、串口和桌面会话资源,渲染层只保存资源 ID 与界面状态。新增原生接口时必须同步更新:
1. Rust command 和 `src-tauri/src/lib.rs` 注册
2. `src/desktop.d.ts` 类型契约
3. 对应 `src/platform/*.ts` 适配实现(通用能力走 `desktop.ts`,RDP / VNC 走 `rdp.ts` / `vnc.ts`)
4. `test/setup.ts` 测试替身
5. 若有用户可见文案:同步 `src/i18n/locales/{zh-CN,en}/` 并保证 key 对齐
Rust SSH 服务保留 `ssh.rs` 作为连接与 Shell 门面,已将独立边界拆出:
```text
services/
├── ssh.rs 连接池、Shell 与稳定公共 API
├── ssh/
│ ├── auth.rs 密码、私钥、Agent 与默认密钥认证
│ ├── known_hosts.rs OpenSSH 主机密钥策略
│ ├── proxy.rs HTTP/SOCKS5 连接代理与 ProxyJump
│ ├── path.rs 本机路径展开
│ └── sftp.rs 远程文件系统与断点传输
├── tunnel.rs TunnelManager 与启动/停止编排
├── tunnel/
│ ├── model.rs 运行实例、计数器和取消令牌
│ ├── socks5.rs SOCKS5 服务端协议
│ └── relay.rs TCP/libssh2 双向流转发
└── desktop/
├── rdp.rs / rdp_engine.rs IronRDP 会话与帧
├── vnc.rs / vnc_engine.rs RFB 会话与帧
├── cliprdr_backend.rs RDP 剪贴板
└── rdpdr_fs.rs RDP 共享盘
```
模块化只约束内部职责,不引入插件框架,也不把 libssh2 或桌面协议引擎资源暴露到 command 层。后续 connection/shell 拆分继续保持现有 Tauri command 契约不变。
## 原生服务
| 能力 | 实现 |
| -------------------- | ------------------------------------------------ |
| SSH/SFTP | `ssh2 0.9` / libssh2 |
| HTTP/SOCKS5 连接代理 | Rust TCP 握手后交给 libssh2 |
| ProxyJump | libssh2 direct-tcpip + loopback relay,最多 8 跳 |
| 本地/远程/动态转发 | libssh2 channel + Rust TCP relay |
| RDP | IronRDP(帧 / 输入 / Cliprdr / RDPDR) |
| VNC | `vnc-rs` RFB |
| 本地终端 | `portable-pty` |
| 串口 | `serialport` |
| 会话与设置 | 应用数据目录内原子写入 JSON |
| 凭据 | AES-256-GCM + `~/.opsxshell/master.key` |
| 主机信任 | OpenSSH `~/.ssh/known_hosts` |
| 终端 AI | Rust `reqwest` + OpenAI/Anthropic 兼容协议 |
| 界面语言 | i18next;zh-CN / zh-TW / en / ja / ko / vi / ru,首启按系统语言检测 |
## SSH Agent CLI 边界
AI 是可选的 SSH Agent。XTerm 只把当前焦点分屏中用户明确选中的内容交给 Rust;Rust 先脱敏常见密码、Token、Authorization Header、AWS Key 和私钥,再调用 Provider。模型文本不允许直接触发执行。
```text
XTerm 当前焦点分屏
-> 仅用户选区(可选)
-> Rust 本地脱敏
-> AiService (OpenAI Chat / Responses / Anthropic)
-> 下一条唯一单行命令 / FINAL 结论
-> 五级风险;critical 硬拒绝
-> 按本次运行锁定的安全模式决定是否确认
-> 当前 connectionId 的 ssh_exec
-> stdout/stderr/exit_code + 本地 analyze_output
-> 真实观察脱敏后交给 AI 判断下一步
-> 最多 8 步后收敛
```
- Provider 配置和模型路由保存在 `ai-providers-v1`,不包含 API Key。
- API Key 使用 `ai-provider:<provider-id>:api-key` 作用域写入现有 AES-256-GCM secrets。
- 模型可覆盖 Provider 协议和 Endpoint,以支持同一个上游模型的按量和 Plan 路由。
- models.dev 目录仅由 Rust 访问三个固定 HTTPS 地址,最多读取 24 MiB;它用于维护和扩展预设,不是应用启动依赖。
- new-api、One API 和其他聚合网关通过 OpenAI Compatible Provider 接入。
- AI 对话、提示词、终端上下文、命令和工具输出只存在当前面板内存中;关闭面板即清空。
- 新 Provider 的测试命令接收临时 API Key 并发出最小真实完成请求;测试过程不调用凭据存储。只有当前连接字段快照测试成功,界面才允许保存。
- 模型返回内容按不可信文本处理;终端输出也标记为不可信,其中的指令不能覆盖 system Prompt。
- Agent 只接受唯一单行命令,目标固定为当前活跃 SSH connection;本次运行开始后安全模式锁定,确认框必须显示目标、命令、风险和锁定模式。
- 命令风险为 safe/low/medium/high/critical 五级。严格模式从 medium 起确认;默认均衡模式从 high 起确认;宽松模式仅适合隔离测试环境。格式化磁盘、删除根目录等 critical 操作在任何模式下都硬拒绝。
- 相同命令第 3 次在执行前拦截;连续失败时要求更换排查路径。长输出先经本地 `analyze_output` 保留错误行和首尾。
- 不创建独立 Kubernetes 客户端或 podexec。集群运维命令沿当前 SSH 会话执行,避免引入本机 kubeconfig 与第二套目标/凭据边界。
完整取舍、记忆边界、演进架构和 Eval 见 [SSH Agent CLI 设计](./ai-agent.md)。
## 端口转发生命周期
转发规则和运行实例是两种不同状态:
- `ForwardRule` 由 React 管理并按会话 / 标签 / 连接维度写入设置,只保存类型、端口和目标。
- `ForwardStatus` 由 Rust `TunnelManager` 管理,只在当前进程有效,包含监听端口、开始时间、连接数和流量。
- 每个运行实例有唯一 `forwardId` 和取消标志。用户点停止 / 删除正在运行的规则、关闭标签、连接面板手动断开,或进程退出时,Rust 才关闭真实 Listener。终端 SSH 断开、空闲挂起或重连不得拆掉未停止的规则。
- 本地 / 动态转发:本机 TCP 监听长住,每条规则共用一条专用 SSH 会话(保活;会话挂了只重建传输,不拆本地端口)。远程转发:专用会话保活,断开后在进程内自动重听。
- SSH 重连(同一 `user@host:port`)通过 `adopt_orphans` 把仍在监听的运行实例挂到新 connectionId。运行态仍不落盘。
- 规则支持编辑和复制。复制只产生新的规则 ID,不复制运行态;运行中的规则必须先停止真实 Listener 才能编辑。
- 应用重启后不得根据上次 UI 状态显示“运行中”,必须以 `ssh_forward_list` 返回的原生状态为准。运行态只存在当前进程内存。
- 状态栏「转发」打开右侧抽屉:动态 SOCKS5 固定 `127.0.0.1:1080`(每台 SSH 主机一条),本地 / 远程规则可列表启停;右键状态栏项复制 `socks5://127.0.0.1:1080`。
当前动态 SOCKS5 默认仅绑定 `127.0.0.1`,不对局域网暴露。客户端仅支持 SOCKS5 CONNECT 和无认证方式。流量经当前 SSH 服务器出口 IP 访问外网。
## 远程桌面生命周期
- RDP / VNC 仅在 Tauri 原生应用中可用;浏览器预览不得显示为可连接。
- 会话运行态只存在原生内存;Canvas 帧与输入经 Tauri 事件传递;断开即取消 worker。
- RDP 可选剪贴板与共享盘(本机下载目录映射);VNC 剪贴板按 UTF-8 发送。
- 桌面会话不参与批量命令与终端广播。
## SFTP 写入策略
渲染层通过 `src/platform/sftp.ts` 固定路径方向:
- `localPath` 永远表示本机文件系统路径,`remotePath` 永远表示 SFTP 服务器路径。
- `uploadRemoteFile` 固定为本地源到远程目标;`downloadRemoteFile` 固定为远程源到本地目标。
- `readRemoteTextFile`/`writeRemoteTextFile` 只操作远程编辑器内存内容,不等同于下载/上传。
- SFTP ID 只能由已连接的 SSH connection 创建,连接失效后不得复用旧 SFTP ID。
上传和下载使用 `.opsx-part` 临时文件:
1. 检查临时文件已有长度。
2. 从相同偏移继续读取和写入。
3. 完成后校验源与目标字节数。
4. 校验通过后替换正式目标。
SSH 传输本身受协议 MAC 保护;当前应用额外执行大小校验,但尚未执行文件内容哈希。
## 终端输入输出管线
`TerminalPipeline` 是 XTerm 和 Shell API 之间的轻量边界,单终端与分屏共用:
- 输入:活动状态更新、Shell 路由、同步输入以及异步写入错误收敛。
- 输出:IPC 数据归一化、流式 UTF-8 解码、关键字变换、每帧限量写入和积压上限。
- 终端视图只处理布局、焦点与生命周期;Zmodem、OSC 和登录脚本后续作为显式管线阶段增加,不继续堆进工作区组件。
## ssh:// 深链接
系统协议由 Tauri deep-link 插件注册,single-instance 插件负责把后续启动参数交回现有进程。原生层只接受 `ssh://`,拒绝 URL 中的密码、路径、查询参数、片段、控制字符和异常用户名。
深链接不会静默连接任意服务器:Rust 将解析后的 `{ username, host, port }` 放入有界内存队列并通知前端;前端原子取走队列,打开预填充的快速连接窗口供用户确认。冷启动时通知可能早于 WebView,队列仍会保留;运行中唤起则聚焦现有主窗口。
## 安全规则
- 敏感凭据必须在 Rust 层加密后落盘。
- 不持久化终端命令历史或批量命令输出。
- 不持久化 AI 提示词、回答或终端上下文;API Key 不得返回 WebView 或写入普通 settings。
- 未实现能力不能在界面中显示为可用。
- 自动更新必须具备签名公钥、HTTPS 更新源和回滚策略,否则保持关闭。
- 应用不能静默绕过 changed host key 或 known_hosts 不匹配。
## 体积预算
| 产物 | 上限 |
| -------------- | ----------------------------- |
| macOS DMG | 25 MB |
| Windows 安装包 | 15 MB,不含 WebView2 引导程序 |
| Linux AppImage | 35 MB |
Release 使用 LTO、单 codegen unit、`panic = abort` 和符号剥离。平台功能必须在对应系统实机验收。