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.electronbridge 访问能力(不直接使用 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: falsecontextIsolation: 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 中的
SettingsValuesschema 有意设为宽松模式(type: object);严格的 TypeScript 类型与约束仅存在于代码注册表中。 - Settings API 响应类型(
ApiSettingsGetResponse、ApiSettingsUpdateResponse)在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继续负责连接身份与主机策略(host、port、username、strictHostKey),不再直接持有密码/私钥密文字段。- 钥匙链的组织信息(文件夹、标签)复用与服务器相同的
SshFolder与SshTag领域模型,不再维护独立的钥匙链专属文件夹/标签表。 - 服务器编辑页保持原有简单流程:仍可直接填写认证信息,后端会自动落地为隐藏钥匙链。
- 公用钥匙链支持多服务器复用;隐藏钥匙链用于单服务器私有凭据。
- 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 同步是幂等操作,并在每次启动执行。
- 在修复结构漂移时必须保持现有用户数据不被破坏。