CI / Lint & TypeCheck (push) Canceled after 0s
CI / Test (push) Canceled after 0s
CI / Tauri Foundation Acceptance (push) Canceled after 0s
CI / Build (macos-latest) (push) Canceled after 0s
CI / Build (ubuntu-latest) (push) Canceled after 0s
CI / Build (windows-latest) (push) Canceled after 0s
212 lines
16 KiB
Markdown
212 lines
16 KiB
Markdown
# 架构说明
|
||
|
||
## 产品边界
|
||
|
||
OpsXShell 是简单好用的原生运维终端:短路径完成连接、终端、传文件、转发和远程桌面。审计、录像、审批和堡垒机能力不在客户端范围内。
|
||
|
||
## 目录与职责
|
||
|
||
```text
|
||
src/ React 界面、工作区和 Zustand 状态
|
||
src/features/settings/ 设置模型、快捷键录入 Hook 和设置控件(渐进拆分)
|
||
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、存储和安全策略
|
||
src-tauri/target/…/bundle/ Tauri 2 标准产物(debug/release 的 .app / dmg / nsis)
|
||
```
|
||
|
||
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/` 七语种并保证 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 契约不变。
|
||
|
||
## 已落地的职责边界
|
||
|
||
- 本地文件读写、目录列表、元数据和增删改在 `src-tauri/src/services/files.rs`,DTO 在 `domain/files.rs`;该 service 不依赖 Tauri。Command 负责调度 `spawn_blocking` 和返回失败信封;原生文件对话框仍在主线程。
|
||
- `domain/error.rs` 提供 `AppError` 和稳定错误码,目前部分文件操作接入。SSH、存储、桌面等 service 仍有 Tauri 依赖,全量错误迁移和 EventSink 尚未完成。
|
||
- `src/platform/invoke.ts` 统一调用与拒绝错误映射;`NativeCommandError` 保留 command、detail、errorCode、originalError。它不把业务失败信封自动抛出,也不统一弹 toast 或为所有原生操作添加超时。
|
||
- `features/settings/model.ts` 管默认值和归一化,`useKeyRecording.ts` 管录入生命周期,`SettingControls.tsx` 提供设置控件。`components/settings/SettingsView.tsx` 仍负责页面组合与部分交互;其他领域尚未全量迁入 features。
|
||
- `settingsStore` 串行处理加载与保存:已确认快照叠加待保存修改形成即时 UI,保存成功才更新确认快照,失败只撤回对应项并提示,不把失败修改写入后续快照。错误由 store 消化并通知,不能仅凭 `Promise<void>` 完成就判断保存成功。
|
||
- 应用主题沿用 `themes/app-theme.ts` 颜色 token;`useTheme` 同步系统偏好、返回的有效主题、DOM 和边框变量。主题偏好随设置保存;尚未完成全界面样式 token 化,也未验证冷启动异步加载设置时完全无闪烁。
|
||
|
||
剩余重构见 [架构重构](architecture-refactor.md)。
|
||
|
||
## 原生服务
|
||
|
||
| 能力 | 实现 |
|
||
| -------------------- | ------------------------------------------------------------------- |
|
||
| 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 判断下一步
|
||
-> 最多 12 步后收敛
|
||
```
|
||
|
||
- 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 操作在前端与 Rust guardrail 都会硬拒绝。
|
||
- 相同命令第 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。运行态仍不落盘。原生 `ssh_disconnect` 必须带真实 reason(keepalive / idle suspend / 用户关闭);空缺时用 `connection closed`,不要再写 `closed by user`。
|
||
- 规则支持编辑和复制。复制只产生新的规则 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。标签还开着时意外断开会有限次自动重连(新会话);不发送假指针对抗空闲 GPO。
|
||
- 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 更新源和回滚策略,否则保持关闭。当前未接入 updater,做法见 [发布指南](./release.md#签名公证和自动更新)。
|
||
- 五层目录补全、specta、EventSink 是单独轨道,见 [架构重构](./architecture-refactor.md);未开始前继续按本文件的 commands → services → platform 扩展。
|
||
- 应用不能静默绕过 changed host key 或 known_hosts 不匹配。
|
||
|
||
## 体积预算
|
||
|
||
| 产物 | 上限 |
|
||
| -------------- | ----------------------------- |
|
||
| macOS DMG | 25 MB |
|
||
| Windows 安装包 | 15 MB,不含 WebView2 引导程序 |
|
||
| Linux AppImage | 35 MB |
|
||
|
||
Release 使用 LTO、单 codegen unit、`panic = abort` 和符号剥离。平台功能必须在对应系统实机验收。
|
||
|
||
## 开发态 Dock 图标(已知限制)
|
||
|
||
`./run.sh gui` 走 `tauri dev`,macOS 启动的是 `src-tauri/target/debug/` 下的裸二进制。Dock 对非 `.app` 进程显示默认齿轮图标——这是 Tauri 通病,不是 `bundle.icon` 没配。正式 `tauri build` 的 `.app` / DMG 三族图标一致。
|
||
|
||
安装包与校验和只放在 `src-tauri/target/<triple>/release/bundle/`,由 `build.sh` / `release.py` 直接读写,不再拷到仓库根 `release/`。opsxapi 开发态正确 Dock 图标来自临时 `.app` 壳(`src-tauri/target/gui-app/`,刷 `icon.icns`)。要对齐开发态 Dock 图标应复用该壳,而不是另建发版目录。
|
||
|
||
## 近期已落地(补记)
|
||
|
||
- **AI 会话**:对话、提示词、终端上下文和工具输出只在当前面板内存;关闭面板即清空,不落盘。本轮运行的安全模式在开始时锁定;需要确认的命令可「本会话允许」,该允许只活在当前面板,不跨重启。Provider / 模型路由仍走 `ai-providers-v1`,API Key 不进 WebView。详见上文「SSH Agent CLI 边界」。
|
||
- **关于页外链**:设置 → 关于里的官网、微信群、GitHub 走 `opsxApi.openExternal`(Tauri opener,系统浏览器)。桌面不可用时才回退 `window.open`。不在 WebView 内嵌打开,也不把链接当应用内路由。
|
||
|
||
## 最后核对(已落地 vs 目标)
|
||
|
||
核对日以本文件提交为准。产品行为以代码为准;重构目标仍以 [架构重构](architecture-refactor.md) 为准,不在这里再开第三份总览。
|
||
|
||
| 项 | 状态 |
|
||
| -------------------------------------------------------------- | -------------------------------- |
|
||
| 本地文件 service 无 Tauri 依赖、设置串行保存、IPC 拒绝错误映射 | 已落地(见「已落地的职责边界」) |
|
||
| AI 会话仅内存、关于外链走系统浏览器 | 已落地(见上节) |
|
||
| EventSink:services 不再持 `AppHandle` / `bind_app_handle` | 目标,未做(轨道 A) |
|
||
| specta / ts-rs 契约生成 | 目标,未做(轨道 B) |
|
||
| infrastructure 端口(SessionStore / SecretStore) | 目标,未做(轨道 C) |
|