Files
Logan.Li 777cfa3593
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
docs: 索引 SDD 与架构轻抛光
2026-10-19 21:21:49 +08:00

212 lines
16 KiB
Markdown
Raw Permalink 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/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) |