Skip to content

Cosmosh 架构设计

1. 运行时拓扑

Cosmosh 采用 Electron 双进程模型,并嵌入后端服务:

  • Main 进程 (packages/main/src/index.ts):应用生命周期、BrowserWindow 创建、preload 注入、IPC 注册、后端进程编排。
  • Preload Bridge (packages/main/src/preload.ts):通过 contextBridge 暴露严格受控 API。
  • Renderer 进程 (packages/renderer/src):React UI、xterm UI、状态编排。
  • Backend 进程 (packages/backend/src/index.ts):Hono HTTP API + SSH/本地终端 WebSocket 会话服务。
flowchart LR
  R[Renderer React App] -->|window.electron.*| P[Preload Bridge]
  P -->|ipcRenderer.invoke/send| M[Electron Main]
  M -->|HTTP localhost + internal token| B[Backend Hono API]
  R -->|WebSocket token URL| WS1[SSH WS Service]
  R -->|WebSocket token URL| WS2[Local Terminal WS Service]
  B --> WS1
  B --> WS2
  B --> DB[(SQLite via Prisma)]

2. Main ↔ Renderer 职责划分

Main 进程 (packages/main/src/index.ts)

  • 应用启动阶段并行拉起 BrowserWindow 与 backend 预热流程。
  • 维护单例的后端启动中的 Promise,避免并发触发重复拉起。
  • Main 到 backend 的代理请求会在转发前确保 backend 已就绪。
  • 在开发启动路径中,Main 采用增量预检(packages/main/scripts/dev-preflight.cjs),当产物是最新时会跳过 @cosmosh/api-contract / @cosmosh/i18n 的重复构建。
  • Main 会以仅运行时且非 watch 的命令(dev:runtime)拉起 backend,避免嵌套 predev 重构建并降低笔记本持续风扇噪音。
  • 持有应用级能力:语言持久化(内存)、窗口/开发者工具/文件管理器操作。
  • 将渲染层请求代理到后端端点,并注入:
    • 作为内部鉴权头的 COSMOSH_INTERNAL_TOKEN
    • 用于后端 i18n 响应的 locale header。

Backend 进程 (packages/backend/src/index.ts)

  • 注册幂等的优雅关闭流程,覆盖运行时信号与致命进程事件。
  • 关闭顺序固定:先停 WS 会话服务,再关闭 HTTP 监听,最后断开 Prisma/SQLite 连接句柄。
  • Windows 终止信号(SIGBREAK)与 POSIX 信号共用同一路径,降低数据库文件锁残留概率。
  • 本地终端 profile 发现改为短时内存缓存 + 并行探测,降低 Home/Settings 首次加载时重复扫描带来的等待。
  • 启动阶段在 initializeDatabase(...) 内执行幂等 Prisma migration 文件同步,因此无论是安装后首次启动还是后续每次启动,都会在开放 HTTP 路由前将本地数据库结构收敛到当前后端契约。
  • Schema 同步采用快速失败策略:若运行时 migration 执行后仍无法满足必需表结构,backend 将中止启动,避免 API 进入部分可用/行为不确定状态。
  • migration 台账元数据采用与 Prisma 兼容的 _prisma_migrations 结构,便于后续平滑切换到原生 prisma migrate deploy/resolve 工作流。

Renderer 进程 (packages/renderer/src)

  • 仅通过 window.electron bridge 访问能力(不直接使用 Node API)。
  • 通过后端 API 创建 SSH/本地终端会话。
  • 通过 WebSocket 建立终端数据通道,并由 xterm.js 渲染。
  • 非 Home 的渲染页(含 SSH 与设置编辑器/Monaco)采用懒加载,避免重型资源进入默认启动路径。
  • Renderer 启动优先从本地缓存水合设置,再在后台向 backend 拉取权威值并同步覆盖。
  • 开发态 StrictMode 改为通过 VITE_ENABLE_STRICT_MODE=true 显式开启,降低本地性能排查时重复 effect 执行带来的干扰。
  • SSH 页面使用 tab 作用域的连接意图快照模型(不再依赖全局可变目标单例),重试与分屏互不串扰。
  • 隐藏 tab 保留渲染但不会触发新的 SSH 连接副作用,连接流程仅允许 active tab 发起。

3. IPC 生命周期(当前)

sequenceDiagram
  participant UI as Renderer UI
  participant PB as Preload Bridge
  participant MP as Main Process
  participant BE as Backend API
  participant WS as WS Session Service

  UI->>PB: window.electron.backendSshCreateSession(payload)
  PB->>MP: ipcRenderer.invoke('backend:ssh-create-session', payload)
  MP->>BE: POST /api/v1/ssh/sessions (+internal token)
  BE-->>MP: sessionId + websocketUrl + websocketToken
  MP-->>PB: API payload
  PB-->>UI: API payload

  UI->>WS: WebSocket connect (url + token)
  WS-->>UI: { type: 'ready' }
  UI->>WS: { type: 'input' | 'resize' | 'ping' }
  WS-->>UI: { type: 'output' | 'telemetry' | 'pong' | 'exit' }

  UI->>PB: close session
  PB->>MP: ipcRenderer.invoke('backend:ssh-close-session', sessionId)
  MP->>BE: DELETE /api/v1/ssh/sessions/{sessionId}

4. 安全模型

Electron 表面加固

  • nodeIntegration: false
  • contextIsolation: true
  • Renderer 仅获得显式 bridge API(contextBridge.exposeInMainWorld)。
  • 特权操作保留在 Main/Backend 进程。

Backend 访问边界

  • 后端仅监听 localhost,并在 electron-main 模式下由内部运行时 token(COSMOSH_INTERNAL_TOKEN)保护。
  • Main 进程注入头信息,不向 renderer 暴露内部 token。
  • 凭据加密 key 由 COSMOSH_SECRET_KEY / 内部 token 哈希在后端启动时推导。
  • HTTP i18n 采用请求级作用域:后端中间件优先从 x-cosmosh-locale(回退 accept-language)解析语言,并为每个请求注入翻译函数供路由统一生成响应消息。
  • WS 运行时 i18n 采用会话级作用域:会话创建时携带已解析语言到 SSH/本地终端运行时,使 WS error/exit 消息与关闭原因保持本地化一致。
  • i18n 运行时改为资源注入模型:各消费端在 createI18n(...) 注册阶段自行导入并注入语言 JSON,因此每个进程只打包所需作用域数据。

会话通道加固

  • WebSocket 路径包含 sessionId 与 query token。
  • token 不匹配或会话过期会立即关闭(1008)。
  • 30 秒 attach 超时用于避免资源孤儿化。

5. 当前缺口 / 规划工作

  • SFTP 运行时通道尚未实现;当前仅有 SSH 终端与本地终端会话通道。
  • Renderer 的 Home 右键菜单已有 SFTP 占位入口,实际页面/会话接线仍在规划中。

5.1 设置运行时(已实现)

  • 设置通过后端路由 GET/PUT /api/v1/settings 持久化。
  • 存储模型为按作用域单行 JSON(scopeAccountId + scopeDeviceId)的 AppSettings 表。
  • 默认作用域为本机(deviceId=local-device),并预留 account 作用域字段用于未来同步。
  • Renderer 启动阶段(packages/renderer/src/main.tsx)会优先使用缓存设置应用语言与主题,并在后台与 backend 同步。
  • 非视觉设置(如 SSH 运行时限制)当前仅做持久化与可发现,部分暂未绑定真实运行时行为。
  • 所有设置定义(类型、默认值、约束、枚举集、UI 元数据、分类)统一存放在单一注册表:packages/api-contract/src/settings-registry.ts。增删设置项仅需编辑此文件(加 i18n 语言文件)。
  • packages/api-contract/src/settings.ts 中的校验逻辑已改为通用的注册表驱动方式:每个 key 的规则(类型检查、枚举、范围、maxLength)在运行时从注册表派生,不再有手写的 switch/case。
  • OpenAPI 中的 SettingsValues schema 有意设为宽松模式(type: object);严格的 TypeScript 类型与约束仅存在于代码注册表中。
  • Settings API 响应类型(ApiSettingsGetResponseApiSettingsUpdateResponse)在 packages/api-contract/src/index.ts 中手工定义,使用注册表中的严格 SettingsValues,不依赖 OpenAPI 生成类型。
  • 已存储设置的读取解析采用前向兼容策略:对缺失/新增字段按字段回填默认值,而不是整份设置回退默认值。
  • PUT /api/v1/settings 仍保持严格全量校验,确保持久化 payload 的结构稳定可预期。
sequenceDiagram
  participant UI as Renderer
  participant PB as Preload
  participant MP as Main IPC
  participant BE as Backend Settings Route
  participant DB as SQLite(AppSettings)

  UI->>PB: window.electron.backendSettingsGet()
  PB->>MP: ipcRenderer.invoke('backend:settings-get')
  MP->>BE: GET /api/v1/settings
  BE->>DB: 按作用域读取 AppSettings
  DB-->>BE: payloadJson + revision
  BE-->>MP: SettingsGetSuccess
  MP-->>UI: settings payload
  UI->>UI: 应用 language + theme

6. 核心数据流视图

6.1 会话启动数据流

flowchart TD
  UI[Renderer UI] --> BRIDGE[window.electron bridge]
  BRIDGE --> MAIN[ipcMain handler]
  MAIN --> API[Backend route]
  API --> SERVICE[Session service]
  SERVICE --> DB[(Prisma / SQLite)]
  SERVICE --> REMOTE[SSH host or local PTY]
  SERVICE --> TOKEN[WS token + session registry]
  TOKEN --> UI

6.2 运行时流式数据流

flowchart LR
  XT[xterm.js] --> IN[input events]
  IN --> WS[WebSocket]
  WS --> SVC[Backend session runtime]
  SVC --> REM[Remote shell / PTY]
  REM --> OUT[stdout + stderr]
  OUT --> WS2[WebSocket output events]
  WS2 --> XT2[xterm.js write]

6.3 失败边界模型

  • Renderer 边界:负责视图状态与用户交互;失败应可通过 UI 重试恢复。
  • Main 边界:负责能力路由与内部鉴权注入;失败不应泄露任何特权 token。
  • Backend 边界:负责协议校验、会话生命周期与资源清理。
  • Remote 边界:SSH 主机 / 本地 shell 波动视为外部故障,映射为稳定 UI 错误码。

7. 架构决策动机

  • 保持 backend 为独立运行时进程,将协议与凭据处理与 renderer 攻击面隔离。
  • 保持 preload 为最小桥接面,减少 API 暴露并维持严格进程契约。
  • 终端高频 I/O 优先走 WS 数据面,避免 IPC 成为吞吐瓶颈。
  • Main 进程作为编排/代理,而非业务承载层,便于未来服务端解耦演进。

7.1 SSH 钥匙链凭据模型(2026-03)

  • SSH 凭据改为存储在 SshKeychain,并通过 SshServer.keychainId 关联。
  • SshServer 继续负责连接身份与主机策略(hostportusernamestrictHostKey),不再直接持有密码/私钥密文字段。
  • 钥匙链的组织信息(文件夹、标签)复用与服务器相同的 SshFolderSshTag 领域模型,不再维护独立的钥匙链专属文件夹/标签表。
  • 服务器编辑页保持原有简单流程:仍可直接填写认证信息,后端会自动落地为隐藏钥匙链。
  • 公用钥匙链支持多服务器复用;隐藏钥匙链用于单服务器私有凭据。
  • SSH 会话创建时统一通过 server → keychain 关系解析凭据后再建立 ssh2 连接。

8. 边界案例处理手册

8.1 启动时 Backend 未就绪

sequenceDiagram
  participant MAIN as Main Process
  participant BE as Backend Process
  participant UI as Renderer Window

  MAIN->>BE: start backend runtime
  MAIN->>UI: create BrowserWindow in parallel
  MAIN->>BE: poll /health
  BE-->>MAIN: not ready
  MAIN->>MAIN: retry with bounded wait
  BE-->>MAIN: healthy
  UI->>MAIN: first backend IPC request
  MAIN->>BE: await startup promise if needed

处理原则:

  • UI 优先尽早可见,backend 在后台并行预热。
  • 首个依赖 backend 的 IPC 在转发前必须确保 backend 已就绪。
  • 启动失败路径应清晰可观测。

8.2 WS Attach Token 不匹配

sequenceDiagram
  participant UI as Renderer
  participant WS as Backend WS Gateway

  UI->>WS: connect /ws/ssh/{sessionId}?token=invalid
  WS-->>UI: close code 1008
  UI->>UI: transition to failed state
  UI->>UI: allow explicit retry flow

处理原则:

  • token/session 不匹配属于安全敏感问题,必须失败即关闭。
  • 恢复路径应通过全新 session/token 重新建立。

8.3 活跃会话期间 Renderer 重载

sequenceDiagram
  participant UI1 as Renderer Instance A
  participant WS as Backend Session Runtime
  participant UI2 as Renderer Instance B

  UI1->>WS: active attach
  UI1-->>UI1: renderer reload
  UI2->>WS: re-attach with new token/session flow
  WS-->>UI2: ready or reject based on session state

处理原则:

  • 会话运行时必须防止陈旧 attach 状态污染。
  • Renderer 重载应视作新生命周期并显式重建状态。

8.4 启动时 SQLite 文件不可读

sequenceDiagram
  participant BE as Backend Bootstrap
  participant DB as SQLite File
  participant MAIN as Electron Main

  BE->>DB: 尝试 SQLCipher 启动校验
  DB-->>BE: file is not a database / unreadable
  BE-->>MAIN: 以 DB_PRAGMA_FAILED 失败退出

处理原则:

  • 生产环境采用严格策略:SQLCipher/Prisma 不兼容时快速失败,由运维修复。

8.5 启动时 Schema 升级路径

sequenceDiagram
  participant BE as Backend Bootstrap
  participant DB as SQLite

  BE->>DB: initializeDatabase(...)
  BE->>DB: 应用 PRAGMA + 执行待应用 Prisma migration.sql 文件
  DB-->>BE: schema 对齐完成(或返回错误)
  BE->>BE: 校验必需表集合
  BE-->>BE: 仅在校验通过后继续启动

处理原则:

  • 运行时 migration 同步是幂等操作,并在每次启动执行。
  • 在修复结构漂移时必须保持现有用户数据不被破坏。