桌面端架构
Spark Work 桌面端架构总览:Electron 主进程 / 渲染进程 / Preload / 类型化 IPC 桥、Agent Runtime 真实服务清单、本地 SQLite 与文件目录、凭据存储(keystore / 加密 vault)与三种执行引擎的载具差异。
本文按真实目录与真实模块名讲清 SparkWork 桌面端的结构:5 类进程、3 个执行内核、 主进程本地服务清单、SQLite/文件/凭据三处存储、459 个类型化 IPC 通道与 41 个 stream 推送通道, 以及一次画布文生图的完整链路。文中出现的服务名与文件名都可以在仓库里直接搜到; 旧版本页面里写过的 agent.service、workflow.service、audit.service、rules-engine、SQLCipher 在当前代码里不存在,已按实际实现改写。
1. 进程模型与代码位置
| 进程/角色 | 代码位置 | 职责 |
|---|---|---|
| Main 进程 | apps/desktop/src/main/(入口 index.ts) | 窗口管理、IPC handler、数据库、本地服务、更新、远程连接、媒体任务路由;构建产物还有两个 worker 入口(workers/background-maintenance.worker.ts、workers/depth-inference.worker.ts) |
| Preload | apps/desktop/src/preload/index.ts | 用 contextBridge 暴露受限的 window.spark:invoke、on、平台信息、文件拖放路径等;不暴露任何原始 Node/Electron API |
| Renderer | apps/desktop/src/renderer/(单入口 index.html) | React 19 + electron-vite 构建;主窗口、画布窗口(CanvasWindowApp.tsx)与内置浏览器窗口(BrowserWindowApp.tsx)共用同一份 bundle,靠 URL 参数区分,例如画布窗口是 ?window=canvas&projectId=... |
| Agent 子进程/运行时 | 见第 6 节 | Claude Code CLI 子进程(每次 SDK query 一个)、Codex 载具进程(可选常驻 App Server,空闲 2 分钟回收)、spark-engine 进程内 SDK |
| 受管 MCP 子进程 | apps/desktop/src/main/services/PlaywrightMcpRegistration.ts、BrowserAutomationMcpRuntime.ts | 例如 @playwright/mcp 通过独立 standalone Node 执行(打包时复制到真实文件系统,因为外部 Node 读不到 app.asar) |
窗口安全基线写死在主窗口创建处:contextIsolation: true、nodeIntegration: false、sandbox: true,preload 指向 out/preload/index.js。
仓库是 pnpm monorepo(nodeLinker: hoisted,为了 electron-builder 能完整收集生产依赖闭包):
apps/desktop:桌面端本体;apps/website:官网(含本文所在文档站)。packages/:protocol(IPC 契约与枚举的唯一来源)、agent-runtime(服务层与执行器)、storage(SQLite 与 migration SQL)、shared(keystore、 日志、工具函数)、tool-sdk、plugin-sdk、ui-kit。spark-engine:自研引擎(进程内@spark/agentSDK),独立于packages/*命名空间,作为第三个执行器被主进程 bundle 打包进去。
2. Agent Runtime 服务层(真实模块)
核心服务在 packages/agent-runtime/src/services/。会话相关的大文件已拆成 services/session/ 子目录(executor 选择、用量增量、检查点、事件定序等)。 下表是最常被问到的模块,路径均为真实文件:
| 模块 | 真实文件 | 职责 |
|---|---|---|
| SessionService | session.service.ts + session/* | 会话生命周期、turn 调度、系统提示词组装(项目上下文 + Runtime Rules + Skill + MCP) |
| 引擎注册表 | session/engine-registry.ts | 按 adapter 选择执行器并声明能力(原生 resume / 权限热切换 / checkpoint 回滚 / 子代理工具) |
| ProviderService | provider.service.ts | Provider CRUD、API Key 经 keystore 解析、健康检查 |
| ModelService | model.service.ts、model-router.service.ts | 模型清单与路由 |
| PermissionService | permission.service.ts | 动作类目映射、Profile 规则判定、审批等待(超时 30 分钟) |
| RulesService / RuleCompositionEngine | rules.service.ts、rule-composition.engine.ts | 规则 CRUD(system 规则只读)与 compose(仅经 rules:compose IPC 暴露) |
| Hook 服务 | hook.service.ts、services/hooks/* | Hooks V2:事件发射、绑定解析、派发、worker、补偿、旧配置迁移 |
| Skill 服务 | skill.service.ts、skill-registry/*、local-skill-importer.ts | 技能元信息、安装、加载与本地导入 |
| MCP 服务 | mcp-server.service.ts | MCP Server 注册表(managed / user) |
| 项目上下文 | project-context.service.ts | 读取工作区 AGENTS.md/CLAUDE.md 与上下文预算 |
| 运行时上下文组装 | runtime-composition.service.ts | Skill / 环境变量 / 工具等运行时上下文合成 |
| 用量账本 | usage-ledger.service.ts、session/session-usage-ledger.ts | per-turn token 增量入账与查询 |
| Team 分派 | team-dispatch.service.ts、team-*.ts | Team Mode 分派、成员执行生命周期、ledger / task graph / replay playbook 适配器 |
| 定时任务 | scheduled-task.service.ts | 任务调度与执行器对接 |
| 媒体 | media/media-router.service.ts、media/media-task-runtime.service.ts | 按 manifest 选适配器、异步媒体任务生命周期与恢复 |
| 记忆 | memory/* | 长期记忆抽取、写入与检索 |
| 工具与包 | unified-tools.ts、tool-packages/*、custom-tools/*、plugins/*、plugin-runtime/* | 统一工具目录、工具包与插件运行时 |
| Git / 工作区 | git-command.service.ts、git-worktree.service.ts、workspace.service.ts、checkpoint-git.service.ts | Git 状态、worktree、checkpoint 回滚 |
| SettingsService | settings.service.ts | app_settings 分类键值读写 |
没有独立的 agent.service / workflow.service / mcp-registry /usage.service / audit.service / hooks-engine 这些文件;Agent、Workflow、Team 定义由 packages/storage 的仓库类直接读写, 审计类数据分散在各模块自己的表里。
3. 主进程本地服务清单
apps/desktop/src/main/services/ 下是「只有桌面端才需要」的能力(约 120 个文件,含测试)。 常用的真实文件名:
- 窗口与浏览器:
CanvasWindowService.ts、BrowserPanelWindowService.ts、BrowserPanelDevtoolsService.ts、InternalBrowserService.ts、SystemBrowserService.ts、HtmlViewerService.ts、BrowserBridgeServer.ts、PlaywrightMcpRegistration.ts、PlaywrightEnvironment.ts。 - 协议与资源:
SafeFileProtocol.ts(safe-file://)、CapabilityAssetProtocol.ts、PrivilegedProtocolSchemes.ts、ExternalUrlPolicy.ts、FontAssetService.ts。 - 文件与工作区:
FilePatchService.ts、FileWatcherService.ts、WorkspaceSearchService.ts、WorkspaceSearchGitignore.ts、GitRuntimeService.ts、TerminalService.ts(node-pty)。 - 凭据与完整性:
CredentialVaultPersistence.ts、SdkIntegrityService.ts、CodexRuntimeIntegrityService.ts、PlaywrightIntegrityService.ts、FfmpegIntegrityService.ts、VoiceIntegrityService.ts、DepthModelIntegrityService.ts。 - 远程连接:
RemoteConnectionService.ts(桥接运行时与命令目录)、QqBotGateway.ts(QQ 官方 WebSocket 网关)、qqProtocol.ts、telegramInboundMedia.ts/telegramOutboundMedia.ts/telegramTextFormatting.ts/telegramTurnFeedback.ts、feishuImageMedia.ts、qqImageMedia.ts、remoteImageMedia.ts、remoteSessionIsolation.ts、remoteTurnReply.ts。 - 数据与维护:
DatabaseBackupService.ts(迁移前恢复点)、ProductionDbInheritService.ts(开发实例继承安装版数据)、TempMediaFilesMaintenance.ts、background-maintenance-worker.ts、AccountSync/(账号同步适配器与本地仓库)。 - 更新:
UpdateService.ts+updaterCache.ts。 - 可选能力与子应用:
optional-capabilities/、SubApp*.ts(SubAppServiceManager、SubAppJobManager、SubAppNetworkGateway、SubAppFileStore、SubAppPackageRuntime、SubAppShareService、SubAppBrowserService)、PlatformModel/(平台托管模型与凭据)、computer-use/、depth-video/、media/。
4. 本地存储:SQLite、文件、凭据
① SQLite(业务数据)
- 文件路径:
{userData}/spark.db,开发与安装版共用同一路径策略;打开时启用journal_mode = WAL、synchronous = NORMAL、foreign_keys = ON、temp_store = MEMORY、mmap_size = 256MB。 - schema 由
packages/storage/migrations/*.sql顺序执行(当前 101 个文件, 由schema_migrations表跟踪),迁移前会自动做恢复点备份。 - 表覆盖 78 个(
CREATE TABLE IF NOT EXISTS去重计数),代表性的有:sessions、agent_events(会话时间线)、app_settings、agents、agent_teams、workflows、workflow_runs、skills、skill_registries、mcp_servers、provider_profiles、usage_ledger、rules、permission_profiles/permission_rules/permission_decisions、hook_definitions/hook_bindings/hook_events/hook_runs、media_generation_tasks、canvas_projects/canvas_snapshots、scheduled_tasks、plugin_runtime_audit。
② 文件系统
| 路径 | 内容 |
|---|---|
{userData}/projects/ | 项目工作目录(存储用量页单列) |
{userData}/attachments/ | 会话附件;attachments/remote/telegram/ 存 Telegram 入站图片 |
{userData}/.spark-artifacts/media/ | 媒体产物默认根目录,按类型落 images/、audio/、videos/、text/ |
{userData}/logs/ | 运行时日志(设置 → 本地日志可读、导出、清空) |
{userData}/spark-agent-updater/<version>/ | 更新包缓存,启动时只保留最近 2 个版本目录 |
~/.spark-agent/board-attachments/ | 看板任务附件(safe-file:// 白名单内,其余 ~/.spark-agent 不放行) |
③ 凭据
- Provider API Key 走
packages/shared/src/keystore:唯一合法的 keytar 入口, service 名固定为spark-agent,引用形如<provider>-<profileId>。 - macOS 上不再是「一个 Provider 一条 Keychain 项」:敏感凭据集中进一个 vault, 由 Electron
safeStorage加密后存到{userData}/credential-vault-v1.enc(权限 0600);macOS Keychain 只作为首次导入来源。 Windows/Linux 仍走 keytar 的对应后端。 - 注意:远程连接(Telegram/飞书/QQ)的 bot token 等凭据不在 vault 里, 而是以明文 JSON 存在
app_settings的remote-connections分类下。
5. IPC 桥:类型化通道与事件流
- 契约只有一处真源:
packages/protocol/src/ipc/index.ts的IpcChannelMap(459 个请求/响应通道)与IpcStreamChannelMap(41 个 main → renderer 推送通道)。IpcChannel就是前者的 key 联合类型, preload 只能按这些名字调用。 - 主进程一律用
typedIpcHandle(apps/desktop/src/main/ipc/typed-ipc.ts)注册, 返回值统一包成IpcResult(ok/data或ok/error); preload 解包后成功返回 data、失败抛SparkIpcError,所以渲染端可以直接 try/catch。 - 命名约定:
session:*、agent:*、workflow:*、provider:*、mcp:*、skill:*、canvas:*、remote:*、update:*、permission:*、rules:*、hook:*(经典)与hookV2:*、usage:*、log:*、app:*、env:*。 - 主进程的 IPC 注册被拆成
apps/desktop/src/main/ipc/register*.ts一系列文件 (如registerHooksV2Ipc.ts、registerSubAppIpc.ts、registerToolPackagesIpc.ts、registerComputerUseIpc.ts), 剩下的集中注册在ipc/index.ts。 - 常用 stream 通道:
stream:session:agent-event(会话事件流,含 assistant 增量与工具事件)、stream:permission:approval-request/stream:permission:approval-resolved、stream:update:status(及available/progress/downloaded)、stream:canvas:media-task、stream:skill:install-progress、stream:remote:changed、stream:sdk:integrity。
6. 执行内核与载具
适配器(SessionAgentAdapter)有四个取值:claude、claude-sdk、codex、spark;统一抽象为三个引擎 EngineKind:claude-sdk(claude 与 claude-sdk 都归它)、codex、spark。契约是 EngineExecutor: 每个 turn 新建实例、终态只经事件流表达、实例身份即闸门。
| 引擎 | 真实实现 | 进程形态 | 能力声明 |
|---|---|---|---|
| claude-sdk | sdk/claude-sdk-executor.ts | 每个 turn 一次 query(),底层是 Claude Code CLI 子进程(可指定 pathToClaudeCodeExecutable);取消时 close query + abort | 原生 resume、权限热切换、checkpoint 回滚、子代理工具 |
| codex | sdk/codex-app-server/*、codex-cli-executor.ts、codex-openai-executor.ts、codex-sdk-executor.ts | 三种载具:本地 CLI(useLocalConfig)、OpenAI 兼容 chat 载具、App Server 常驻进程(可在执行器间复用,空闲 120 秒回收;握手失败或图片附件场景回退 SDK 载具) | 有 supervisor 时原生 resume;不支持权限热切换/回滚/子代理 |
| spark | sdk/spark-engine/* + spark-engine/(@spark/agent SDK) | 进程内 SDK,无子进程 | 原生 resume(事件重放续跑)、权限热切换 |
流式事件统一为 AgentEvent,主进程通过 stream:session:agent-event 推给渲染端(assistant 增量/完成、tool_use、tool_result、agent_status、agent_error、presented_files 等)。
原生模块(better-sqlite3、keytar、node-pty)必须编译到 Electron ABI: 打包脚本会先跑 pnpm run rebuild:native -- <arch>;该脚本内部在宿主架构与目标架构一致时,会用 Electron 真实加载一次(native:verify),不一致时跳过校验并给出警告。
7. 内置 MCP 与媒体运行时
内置 MCP server 的实现在 packages/agent-runtime/src/tools/*.mjs,注册名(也就是工具前缀)如下:
| 注册名 | 实现文件 | 用途 |
|---|---|---|
spark_image | image-generation-mcp-server.mjs | 旧图片生成链路(兼容保留) |
spark_media | media-generation-mcp-server.mjs | 统一多媒体:模型清单/描述、图片生成与编辑、语音合成、转写、视频生成与编辑、文件上传与任务查询 |
spark_search | web-search-mcp-server.mjs | 联网搜索 |
spark_browser | browser-automation-mcp-server.mjs | 内置浏览器自动化 |
spark_files | present-files-mcp-server.mjs | 把生成的文件作为卡片呈现给用户 |
spark_ui | quick-replies-mcp-server.mjs | 快捷回复按钮 |
spark_memory / spark_session | spark-memory-mcp-server.mjs / spark-session-mcp-server.mjs | 长期记忆与会话自省 |
spark_canvas | spark-canvas-mcp-server.mjs | 画布节点读写 |
spark_platform / spark_team / spark_plugins / spark_app | platform-management-mcp-server.mjs 等 | 平台管理、团队分派、插件运行时、子应用 |
spark_debug / spark_computer / spark_verify / spark_tool_results | debug-mode-mcp-server.mjs 等 | 调试、电脑操作、校验、超长工具结果回读 |
多媒体走 MediaRouterService:按 manifest 选择适配器,适配器 id 取自 MediaProviderKind(apimart、agnes、xai、openai-compatible、openai-images、google-generative-ai、bailian、volcengine-ark、volcengine-speech、kling、pixverse、minimax-hailuo、wan、happyhorse、omni),实现在 packages/agent-runtime/src/services/media/adapters/。 异步任务由 MediaTaskRuntimeService 持久化到 media_generation_tasks 表 (migration 029_media_generation_tasks.sql,另有 066_media_task_recovery.sql 支持恢复),产物写入 .spark-artifacts/media/<kind>。
渲染端要显示本地媒体/文档时走自定义协议 safe-file://(SafeFileProtocol.ts):绝对路径经 base64 编码进 URL, 主进程只放行白名单根目录——userData、系统临时目录、~/.spark-agent/board-attachments、已登记的 workspace 根目录、画布项目目录; 白名单外一律 403。
8. 一次画布文生图的真实链路
- 渲染端调用
canvas:task:create-media,带operation、prompt、waitForCompletion: false、可选的providerProfileId/manifestId/modelId/modelParams/outputDir。 - 主进程写入
media_generation_tasks一行并立即返回running响应(runtimeTaskId、providerProfileId、model)。 - 渲染端在画布上先画出「任务节点」,用户可继续平移/缩放/拖拽。
- 主进程经
MediaRouterService按 capability 选适配器与 manifest,走MediaRequestCompiler组装请求并调用 Provider API;长任务进入轮询。 - 产物下载/解码后落盘到
.spark-artifacts/media/images(或对应 kind 子目录), 并按需生成预览 data URL 或safe-file://链接。 - 完成、失败或取消时,主进程通过
stream:canvas:media-task推送任务状态, 载荷里的projectId/clientTaskId用于路由回对应画布。 - 渲染端写回画布(新增 image 节点与 lineage 边)。
9. 安全边界(可验证的部分)
- 渲染进程沙箱:主窗口
contextIsolation: true、nodeIntegration: false、sandbox: true;能力只经window.spark暴露。 - CSP 写在 renderer 的
index.htmlmeta 里(default-src 'self'、script-src 'self' capability-asset:、img-src ... safe-file: spark-snapshot: https: http: blob:、media-src ...、frame-src 'self' safe-file: capability-asset:), 不是由主进程动态下发。 - 内置浏览器窗口与主 UI 隔离:创建浏览器面板窗口时会删掉
webPreferences.preload/preloadURL,并强制nodeIntegration: false(含 subframe/worker)、contextIsolation: true, 远程页面拿不到 IPC bridge。 - 本地文件读取白名单:
safe-file://只放行上文列出的根目录, 越界返回 403;工作区路径边界另有isWithinWorkspace检查。 - 凭据:Provider Key 走 keystore(macOS 为 safeStorage 加密的 vault 文件); 远程连接凭据是
app_settings里的明文 JSON,界面用的是普通文本输入框,已保存的值会明文显示,不做掩码。 - 高风险操作走审批:由
PermissionService按动作类目与 Profile 规则判定, 详见「权限与治理」主题。 - 数据库不做透明加密:代码里没有 SQLCipher 或等价实现; 需要长期保密请依赖操作系统磁盘加密,或使用设置 → 存储与备份的迁移前恢复点做归档。
10. 排查与常见坑
- 想知道某个功能走哪条 IPC:先在
packages/protocol/src/ipc/index.ts搜通道名,再到apps/desktop/src/main/ipc/搜typedIpcHandle('通道名', 最后看渲染端的window.spark.invoke。这条路径比全局搜字符串快得多。 - 「设置页改了没用」:很多设置分两层持久化——localStorage(即时渲染)+
app_settings(权威值)。只看 localStorage 会误判;例如更新相关的值在updates/data与updates/lastChecked。 - 媒体任务卡住:先看
media_generation_tasks的行状态, 再看设置 → 本地日志里canvas范围的日志(画布任务日志有独立的 scope)。 - 原生模块报
Cannot find module或加载失败:几乎都是 ABI 不匹配, 跑pnpm --filter @spark/desktop rebuild:native后重试;打包相关请确认走的是build-mac-release.sh/build-win-release.sh(它们会在打包前重建原生模块)。 - 换机器后要重新下模型/运行时:Codex native runtime、Playwright、 ffmpeg、depth 推理与 voice pack 都是可选能力包,基础安装包不带,入口在设置 → 完整性 / 浏览器自动化 / 电脑操作等相关页面。
- 多窗口共用一个 bundle:画布窗口与浏览器窗口不是独立前端工程, 改
CanvasWindowApp.tsx就等于改主 bundle,注意别把主窗口逻辑耦合进去。
速查表
- 进程
- Main / Preload / Renderer(单 bundle 多窗口)/ 受管 MCP 子进程 / 执行引擎进程
- 执行内核
- claude-sdk / codex(cli、openai-chat、app-server 三种载具)/ spark(进程内)
- 应用配置
- app_settings(分类 + key 的 JSON 值)
- 数据库
- {userData}/spark.db(WAL,101 个 migration SQL)
- 凭据
- keystore(keytar service=spark-agent);macOS 为 safeStorage 加密的 credential-vault-v1.enc
- IPC 规模
- 459 个请求/响应通道 + 41 个 stream 推送通道(packages/protocol/src/ipc/index.ts)
- 媒体产物
- {userData}/.spark-artifacts/media/{images,audio,videos,text}
- 本地媒体协议
- safe-file://(白名单根目录,越界 403)
- 更新缓存
- {userData}/spark-agent-updater/<version>/,保留最近 2 个版本
常见问题
展开 6 条常见问题
- Renderer 能直接访问 Node 吗?
不能。主窗口以 contextIsolation: true、nodeIntegration: false、sandbox: true 创建,只有 preload 用 contextBridge 暴露的 window.spark(invoke / on / 平台信息)可用。
- 每个 turn 都会新起一个子进程吗?
Claude 路径是——每个 turn 一次 SDK query,底层就是 Claude Code CLI 子进程。Codex 不一定:App Server 载具是常驻进程,可在执行器之间复用,空闲 120 秒回收;本地 CLI / OpenAI chat 载具则是单次调用。Spark 引擎完全进程内。
- 状态放在哪里跨 turn 共享?
SQLite({userData}/spark.db,WAL 模式)存会话、事件、设置、用量、Hook 等业务数据;文件系统存项目、附件、媒体产物与日志;两者才是跨 turn 的共享状态,执行器实例本身不共享。
- 数据库加密了吗?
没有做透明加密,代码里不存在 SQLCipher 集成。{userData}/spark.db 是普通 SQLite 文件,依赖操作系统磁盘加密;应用只保证迁移前自动创建恢复点。
- 画布里的产物是怎么显示出来的?
media 产物默认落在 {userData}/.spark-artifacts/media/<kind>,渲染端用 safe-file://(绝对路径 base64 编码)加载,只放行白名单根目录(userData、临时目录、board-attachments、已登记 workspace、画布项目目录),白名单外返回 403。
- 内置 MCP 服务有哪些?
spark_image、spark_media、spark_search、spark_browser、spark_files、spark_ui、spark_memory、spark_session、spark_canvas、spark_platform、spark_team、spark_plugins、spark_app、spark_debug、spark_computer、spark_verify、spark_tool_results,实现都在 packages/agent-runtime/src/tools/*.mjs。