Files
opsxshell/docs/architecture-refactor.md
T

4.5 KiB
Raw Blame History

架构重构(渐进推进)

局部职责拆分已开始;下述 EventSink、契约生成和 infrastructure 端口仍是待实施方案。不要把已完成的局部改造当成全仓迁移,也不要顺带引入平行架构。

已落地:本地文件业务下沉至无 Tauri 依赖的 services/files.rs;AppError 部分接入;前端 IPC 拒绝错误统一映射;settings 功能模型、Hook 和控件提取;设置串行保存与失败回退;系统主题同步修复。实现位置与限制见 架构说明。

当前真实分层是 commands → services → domain DTO。目录里虽有 application/、infrastructure/,但:

  • infrastructure/ 只有 file_log,services 并不走它。
  • application/state.rs 是服务集合,不是端口组合根。
  • domain/error.rs 已有稳定 IPC 错误码 codes 和部分接入的 AppError;尚未覆盖所有业务错误。
  • SSH / 存储 / 桌面服务直接 use tauri::AppHandle;known_hosts 还用 bind_app_handle 全局 static。
  • 前后端契约靠手写 src/desktop.d.ts + shared/types/,没有 specta / ts-rs。

产品数据流保持不变:UI → Zustand → src/platform/* → Tauri command → Rust 服务。重构不得把 SSH、文件系统或加密拉回 React,也不得改成插件宿主。

为什么单独做

  1. EventSink 要改所有 app.emit 调用点,和功能修复冲突时极难回滚。
  2. specta 会动 command 签名和前端 invoke 形状,必须一次对齐 CI,不能「先生成一半」。
  3. 把 storage / SSH / 凭据收进 infrastructure trait,等于重写生命周期,和转发 retain、桌面 worker 取消语义绑在一起。

一次只开一条轨道,合入后再开下一条。每条轨道自己的 PR,跑 npm run test:patrol 和 npm run acceptance:full。

轨道 A:EventSink(先做,最小)

目标:services 不再拿 AppHandle,也不再 bind_app_handle。

  1. 在 application/ 定义:
pub trait EventSink: Send + Sync {
    fn emit_json(&self, channel: &str, payload: serde_json::Value);
    fn emit_empty(&self, channel: &str);
}
  1. TauriEventSink(AppHandle) 只放在 application / command 边界。
  2. SshService、known_hosts、RDP/VNC engine、license 等改为持有 Arc<dyn EventSink>。
  3. 删除 known_hosts 的 APP_HANDLE / PENDING / ONCE_TRUSTED 全局 static,收进 AppState。
  4. 单测用内存 EventSink 断言事件,不启 Tauri。

完成标准:rg "use tauri::AppHandle" src-tauri/src/services 为空(command 层可以留)。行为与现网事件名、payload 完全一致。

轨道 B:契约生成(specta 或 ts-rs)

目标:消灭「Rust DTO / desktop.d.ts / shared/types」三处手工对齐。

  1. 选一个:tauri-specta(和 command 绑定)或 ts-rs(只生成类型)。不要两个一起上。
  2. Rust 侧先把 serde_json::Value 逃逸收成强类型(ProxyConfig、JumpHostConfig、会话字段)。
  3. 生成文件放到例如 shared/types/*.generated.ts,CI 跑 git diff --exit-code。
  4. 手写 src/desktop.d.ts 只留窗口/opsxApi 外壳,领域类型改 re-export 生成结果。
  5. test/setup.ts 跟着生成类型改,不要继续复制一份松散 unknown。

完成标准:改一个 command 字段,忘跑生成则 CI 红。不要在这一轨改 EventSink 或换存储引擎。

轨道 C:infrastructure 端口(最后)

目标:I/O 可替换,services 只编排。

建议端口(按需,不要一次全铺):

Trait 先搬谁 不要顺便做
SessionStore services/storage.rs 的 JSON 会话 不要同时改成 SQLite
SecretStore credential_store.rs 不要同时改系统 keychain
SshTransport 仅当要换 russh 时再抽 默认保持 libssh2

infrastructure/mod.rs 里现在的「SQLite / keychain / PTY 适配器」是过期注释,做 C 轨时改成与真实 trait 一致,没做的删掉,避免再写成已存在。

IPC 错误继续扩展 domain/error.rs 的 AppError / codes,保留既有信封和运行态抛错契约,不另建重复错误类型。

明确不做(本轨道也不做)

  • 六边形/整仓重命名目录来「看起来像五层」。
  • 对外 CLI、MCP、插件宿主。
  • 把结账/签发搬回桌面进程。
  • 和签名、公证、自动更新、功能修复混在同一个 PR。