- 人工智能
- 大模型
- AI 应用
- 交互助手
- 本地部署
【免费下载链接】cherry-studio
🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端
导读
本文是 Cherry Studio 桌面客户端主进程(Electron Main Process)架构的权威参考,围绕src/main/的顶层目录组织展开:为什么顶层目录是一个封闭且锁定的分类集合、每个目录承担何种唯一职责、features/与services/如何按规模分档、依赖方向如何流向业务无关的基座层,以及新增能力时如何在不新增顶层目录的前提下被正确归位。读完本文,你将掌握一套可直接落地的进程级代码组织规范——既能判断一个新模块该放进ai/、data/、services/还是features/<domain>/,也能理解 Cherry Studio 当前主进程代码结构背后的治理逻辑与尚存的迁移缺口。
1. 定位:主进程架构在 Cherry Studio 文档体系中的位置
src/main/是 Cherry Studio 的 Electron 主进程代码根。本文档是它的规范级(canonical)参考,回答三个问题:
src/main/下每个顶层目录是干什么的;- 哪些规则阻止这些目录"无序蔓延";
- 目录之间如何相互依赖。
它是渲染进程架构与共享层架构的主进程对等文档;跨进程的完整视图(进程模型、monorepo 目录树)见架构总览。
与项目整体目录规则的关系:主进程的顶层封闭规则是命名规范 §4.8「顶层目录默认封闭」在src/main/下的最严格落地,且按 §4.9(单复数)、§4.10(feature 与类型桶的区分)、§5.2(按形态路由)进一步细化。三者配合构成完整的治理闭环。
核心命题:顶层是一组有原则的封闭分类集合,而不是一份开放式模块清单。每个顶层目录只装一种东西,并且因为不同的理由而占据顶层位置。该集合被锁定——任何新能力都必须按其本质归入已有类别,永远不为其新增顶层目录(详见 §4)。
2. 封闭的顶层集合:九个目录,各自一份章程
src/main/下恰好存在以下顶层条目,每个都有且只有一份职责:
| 目录 | 类别 | 占据顶层位置的理由 |
|---|---|---|
core | 应用运行时 | 与业务无关、只关心"如何把应用跑起来"的基础设施。检验标准:把core/搬到另一个 Electron 应用上,加上别的业务代码,你就得到了一个不同的应用。「与业务无关」是必要但不充分条件:core/只容纳不可移除的底座——应用离开它就无法运行;可移除的能力即使必须在启动早期执行,也应归入services/(见 §4)。它只装一种东西——应用底座:生命周期/DI 容器、路径注册表、日志器、窗口管理器、调度器与任务、preboot、诊断、安全原语(IPC 发送方信任)。 |
ipc | 跨进程边界 | Electron 最具定义性的进程间机制——足够特殊和重要,因此独立成目录。IpcApi(schema + router + handler)是已迁移域的类型化边界;遗留的根级ipc.ts仍共存,作为 §7 追踪的迁移缺口。 |
data | 数据层 | 通用业务数据存储——一等公民数据层,故独立。持有 DbService / CacheService / PreferenceService / DataApiService / BootConfig、DB schema,以及 v1→v2 迁移器(它们按设计读取领域数据——一次性迁移代码)。详见数据系统参考。 |
ai | 核心领域 | Cherry Studio 本质上是一个 AI 客户端,因此 AI 拥有自己的顶层归属:一切与 AI 本质绑定的事物都在这里(providers、middleware、MCP、agents、stream manager),与@shared/ai镜像。 |
features | 领域模块 | 业务领域,一个领域一个目录。复杂领域将自身相关的 services/utils 等捆绑在features/<domain>/之下。 |
services | 业务服务 | 业务功能服务。简单服务是单个文件;较大的服务组织成自己的子目录。 |
utils | 无状态助手 | 跨领域、与领域无关的无状态函数,无单一属主。「无状态」是门槛而非「纯函数」:助手可以通过环境化的@application/@logger触及基础设施(见 §3);它只是不拥有状态、不产生外向副作用(见 §2 反模式)。 |
i18n | 主进程本地化 | 主进程自己的locales/目录及其t()/getI18n()解析器。这是封闭集合上一次有意的、受治理的扩充(§4),与src/renderer/i18n/镜像,使每个进程各自拥有独立目录;utils/i18n/方案因破坏跨进程对称性而被否决。 |
入口文件:
- main.ts——进程入口:先跑 preboot,再
application.bootstrap()。使用具名文件而非index,因为index按命名规范 §6.4 保留给 barrel(重导出聚合文件)。 ipc.ts——遗留 IPC 注册,正在被逐步退役进ipc/。
命名遵循命名规范 §4.9:core/data/ai/ipc/i18n是单数命名空间;features/services/utils是复数桶(collection bucket)。判断规则是「这个目录是否装着许多同类东西」——是则复数,否则单数。
从源码可以完整验证这份目录清单。实际仓库根目录与文档一致,共九个顶层条目:
src/main/ ├── main.ts # 进程入口:preboot → application.bootstrap() ├── ipc.ts # 遗留 IPC 注册(正在被退役进 ipc/) ├── core/ # 业务无关的应用运行时(lifecycle/DI、paths、logger、window、scheduler/job、preboot、security) ├── ipc/ # IpcApi —— 类型化的 main↔renderer 边界 ├── data/ # 数据层(DB/Cache/Preference/DataApi/BootConfig、schemas、migration) ├── ai/ # AI 子系统 —— 产品的核心领域 ├── features/ # 业务领域,一个目录一个(各自捆绑自己的 services/utils) ├── services/ # 业务功能服务(单文件,或一个子目录) ├── utils/ # 跨领域无状态助手 └── i18n/ # 主进程本地化目录与解析器2.1 从入口源码看分层落地
main.ts 是理解这套分层的最佳起点。文件头部注释明确写着:"DO NOT add new code here"——新服务应进入生命周期系统(core/lifecycle/),不可移除的 preboot 步骤进入core/preboot/,可移除但必须在 preboot 期执行的能力归入其本性之家(如services/)并从入口调用。该文件只是胶水,只应收缩。
入口启动序列与文档所述完全吻合:
// Preboot 阶段——顺序重要,见 core/preboot/README.md resolveUserDataLocation() // 用户数据目录解析(必须先执行) requireSingleInstance() // 单实例锁 configureChromiumFlags() // Chromium 启动开关 initCrashTelemetry() // 崩溃遥测 protocol.registerSchemesAsPrivileged([...]) // 特权 scheme 声明 application.initPathRegistry() // 冻结路径注册表——bootstrap() 会断言其完成 // ... application.registerAll(serviceList) // 注册全部生命周期服务 const bootstrapPromise = application.bootstrap() // 生命周期引导 await app.whenReady() await bootstrapPromise // ... await registerIpc() // 遗留 IPC 注册(v2 待分解)其中application.registerAll(serviceList)与application.bootstrap()分别来自core/application/与core/lifecycle/,serviceList来自 serviceRegistry.ts,这正是文档 §3 所说「handler 通过application.get('XxxService')解析生命周期服务」的容器底座。
2.2core/目录的边界判定
core/README.md 给出了清晰的判定规则:
经验法则:如果移除某个模块会让应用无论功能如何都无法运行,它属于
core/;如果移除它只会破坏某个具体功能,它属于别处(如services/、data/)。
当前core/模块清单(含各自的参考文档)包括:
| 模块 | 描述 | 参考文档 |
|---|---|---|
application/ | 应用单例、服务注册表、bootstrap 编排 | 生命周期参考 |
lifecycle/ | IoC 容器、服务生命周期管理、分阶段引导 | 生命周期参考 |
logger/ | 基于 Winston 的日志服务(preboot 单例,经@logger别名消费) | 日志参考 |
paths/ | 路径注册表:所有主进程文件系统路径的唯一真源 | paths/README |
preboot/ | 引导前的同步设置(userData 解析等) | preboot/README |
window/ | 窗口管理器 | 窗口管理器参考 |
utilityProcess/ | 崩溃隔离的 Electron utility 进程:注册、类型化客户端、线协议、子运行时 | Utility Process 参考 |
scheduler/+job/ | 调度器与任务系统 | 任务与调度参考 |
concurrency/ | createLatestReconciler——通用 latest-wins 异步副作用协调器 | concurrency/README |
security/ | IPC 发送方信任等安全原语 | IpcApi Overview §Security |
注意core/的启动分阶段词汇是全代码库的标准用语:preboot(core/preboot/负责,调用bootstrap()前必须完成的同步设置,无 DI、无生命周期服务)→bootstrap(core/application/+core/lifecycle/负责,冻结路径注册表、构建 IoC 容器、运行 Background / BeforeReady / WhenReady 各生命周期阶段)→running(bootstrap()返回后的稳态)。core/README.md特别提醒:这三个生命周期阶段运行在 bootstrap 阶段内部,不是三个独立的顶层阶段。
3.features与services:同一个东西的两种尺寸
services/与features/是同一种东西——业务逻辑——的两种规模形态。分档遵循命名规范 §4.10 的跨进程规则:
提升而非默认——而且要分步走。一个小的、自包含的服务,最初以单文件形式放在桶根:
services/<Topic>Service.ts(一个持有状态的Service/Manager类用与其类名一致的PascalCase,见命名规范 §5.2;而通用助手是utils/<topic>.ts——主题专用的助手在进入子目录步骤之前保持内联)。当单个文件容纳不下时,先在原处扩展为camelCase主题子目录——services/<topic>/容纳<Topic>Service.ts及其助手——而不是直接升级为 feature。注意形态:目录是主题名,不带Service后缀(命名规范 §4.5);只有类文件保留后缀(例如services/webSearch/WebSearchService.ts)。只有当它成长为大型、多文件的领域、捆绑自身 services/utils 与助手时,才赢得features/<domain>/的归属(典型如 knowledge、apiGateway、fileProcessing)。不要为一个预期中的模块预先创建子目录或 feature。ai/不是普通 feature。它是产品的核心领域,拥有自己的顶层归属(§2);它是奠基性的,不是众多领域之一。按角色路由(命名规范 §5.2):持有长期资源或有持久副作用的有状态类→ 生命周期
Service(见生命周期参考);无状态模块→ 默认utils/,仅当出现外向副作用或被迫向上依赖时才提升到services/(见 §5.2 路由表);大型领域→features/<domain>/。只读永远不构成提升理由——为查询而触碰基础设施的助手依然是助手。
源码佐证:src/main/features/下当前恰好四个领域目录——apiGateway/、fileProcessing/、knowledge/、miniApp/,与文档列举的「knowledge、apiGateway、fileProcessing」示例一致且规模相当;而src/main/services/则是单文件服务(如AppService.ts、TrayService.ts、VersionService.ts等数十个*Service.ts)与主题子目录(webSearch/、file/、oauth/、proxy/、cherryCloud/等)并存的混合桶——正是「单个文件是默认,主题多文件时提升为子目录」的直观体现。
3.1 子目录与 Barrel 规则
单个.ts文件是默认形态;只有当主题确实拥有多个文件时才提升为子目录。Barrel(index.ts聚合导出)遵循命名规范 §6.4(跨进程的单一权威),应用到services/与utils/:
- 桶根
services/与utils/没有index.ts。桶是类别而非模块——要导入就导入具体文件或主题,绝不导入整个桶。 services/<topic>/子目录恰好有一个index.ts作为其公开 API(显式具名导出,禁止export *);其余文件保持私有。- 复杂的
utils/<topic>/子目录同样只有一个index.ts。 - 为什么:每个主题通过单一公开入口被导入——与单文件模块完全一致——内部文件保持私有,消费者永不深导入。对
utils/而言,当文件与目录共享主题名时,说明符(@main/utils/<topic>)在文件长成文件夹后甚至无需改变。
(features/<domain>/是同一「单入口」思想的高一层:消费者通过其一个公开入口导入领域,而非其内部文件。)
这一规则与命名规范 §6.4 的 barrel 铁律完全一致:barrel 只做重导出、必须真正可封闭、不得嵌套 barrel。桶根types/、utils/、services/因此不设根index.ts。
4. 依赖方向:流向业务无关的基座层
各目录的章程蕴含了方向:依赖流向业务无关的基座层:
- 基座层(Foundation)——
core/与utils/不携带业务知识;没有任何业务代码位于它们之下。 - 数据层(Data layer)——
data/是基座层之上的存储层。 - 业务层(Business)——
ai/、features/、services/是业务层;它们向下依赖data/、core/、utils/。 ai/在业务层内部具有基座地位:features/与services/可以依赖它;它不得导入任何 feature。- feature 领域互相隔离:
features/<domain>/不得导入同级feature——通过services/、ai/、data/或@shared共享。 ipc/是边界适配器:handler 保持薄(边界策略 +IpcError映射 + 委托),并通过两种方式触达业务代码——生命周期服务(注册于serviceRegistry.ts)通过application.get('XxxService')解析,绝不直接导入;非生命周期模块(无状态主题 barrel 或直接导入的单例)通过其精选入口导入,而仅为获取 DI 句柄而虚构一个生命周期服务是反模式。详见 Handler: Pure Function vs Service Delegate。- 禁止导入渲染进程:
src/main与src/preload不得导入渲染进程代码。跨进程类型放在@shared,主进程独有类型留在src/main——放置规则见共享层架构。此规则由 ESLintno-restricted-imports规则强制:在src/main+src/preload中封禁@renderer;§7 追踪仅存的一个例外。
4.1 两条贯穿性环境依赖:@logger与@application
有两条依赖横穿每个目录,且不是分层边——它们是环境化的基础设施访问:@logger(日志)与@application(DI 容器 / 服务定位器)。对源码导入做一次原始扫描会发现几乎一切都"依赖core",而这仅仅是因为这两条别名;上述规则关注的是领域之间的直接模块导入。
4.2 依赖规则的两级强制执行现状
- 内部方向边暂未自动化(不同于渲染进程文档 §5 提出的
import/no-restricted-paths分区方案):方向靠约定与评审维持。 - 外部 main↔renderer 边界已被强制:eslint 规则
BAN_RENDERER_FROM_MAIN是硬约束。从 eslint.config.mjs 可以看到其完整定义:
const BAN_RENDERER_FROM_MAIN = { group: ['@renderer', '@renderer/**', '**/renderer/**'], message: 'Main/preload must not import renderer code. Use `@shared` for cross-process types, or `src/main` for main-only types. See docs/references/architecture/shared-layer.md.' }该禁令在配置中针对src/main+src/preload以'error'级别生效(eslint.config.mjs)。另有一条BAN_DRIZZLE_MIGRATOR禁令:禁止直接调用 drizzle 的migrate(),必须改用@data/db/applyMigrations的applyMigrations(),否则表重建式迁移会因事务让PRAGMA foreign_keys=OFF失效而静默级联删除子行——这正是 §1 中「v1→v2 迁移器属于 data/」的工程动机之一。
5. 封闭顶层治理:新能力永不新增目录
顶层集合封闭且锁定——把在
src/main/下新增目录视为不可行。这是命名规范 §4.8(顶层默认封闭)的最严格形态:§4.8 仅在同时满足必要性(没有现有类别能在无语义损失的前提下容纳这些文件)与完备性(新目录有清晰范围、符合 §4.3 复数桶或 §4.5 单数领域模块形态、不与现有桶重叠)时才允许新增顶层目录,而主进程的类别已覆盖全部空间——所以新能力被归入现有类别,永不拥有自己的目录。唯一有意的扩充是i18n/(§2),它的加入让主进程拥有了与src/renderer/i18n/对称的本地化目录;这是一次有记录的受治理例外,而非规则松动。渲染进程(§6)与@shared(§2)的顶层同样受此治理约束。
新能力永不获得新的顶层目录,按其本质归位:
| 该能力是…… | 归属 |
|---|---|
| 与 AI 本质绑定 | ai/ |
| 业务数据 / 存储 | data/ |
| 一条 IPC 路由 | ipc/(IpcApi) |
| 业务无关、不可移除的应用运行时基础设施 | core/ |
| 一个业务服务 | services/——若是大型多文件领域则为features/<domain>/ |
| 纯粹、领域无关的逻辑 | utils/ |
结合命名规范 §4.8 的完整判定流程,新目录的准入标准是两条都必须成立:①必要性——现有顶层桶无法无语义损失地容纳新文件;②完备性——新目录范围清晰、形态合规、与现有桶无重叠。任何一条存疑,就把文件放进现有桶;现有桶下的子目录不受限制。
6. 反模式清单
下列模式被明确禁止:
- 把业务代码(任何 Cherry Studio所做之事的专属逻辑)放进
core/——core/必须保持纯应用运行时。 features/<domain>/导入同级feature(跨领域耦合)。ai/导入features/(核心领域向上依赖 feature)。- 为单一能力开设新的顶层目录(§4)。
- 将业务数据散落到临时存储,而不是走
data/子系统;或将命令式操作散落到临时通道,而不是走ipc/(IpcApi)。
这与命名规范 §6.7 的「桶反模式」相互呼应:单数命名却装了许多同类项、桶内混入与声明类别不符的文件、长期只装 0–2 个文件的瘦桶、两个顶层桶范围重叠——出现任何信号都值得一次整合评审。
7. 子系统参考索引
各子系统的纵深细节位于各自的专项文档;本文只负责目录布局本身,不在此重复子系统细节:
| 子系统 | 位置 | 参考文档 |
|---|---|---|
| 服务生命周期(IoC、分阶段引导) | core/lifecycle/、core/application/ | 生命周期参考 |
| 启动阶段(preboot / bootstrap / running) | core/preboot/、core/application/ | core/README |
| 窗口管理器 | core/window/ | 窗口管理器参考 |
| Utility 进程(崩溃隔离的工作进程) | core/utilityProcess/ | Utility Process 参考 |
| 调度器与任务 | core/scheduler/、core/job/ | 任务与调度参考 |
| 路径注册表 | core/paths/ | paths/README |
IPC 发送方信任门(validateSender) | core/security/ | IpcApi Overview §Security |
| 数据系统(DB/Cache/Preference/DataApi/BootConfig) | data/ | 数据系统参考 |
| IPC(IpcApi) | ipc/ | IPC 参考 |
| AI 子系统 | ai/ | AI 参考 |
8. 当前偏差(目标 vs 现状)
本文描述的是目标态。当前代码尚未完全对齐之处记录如下——本轮治理只记录、不修改代码。这里只列出结构性偏差(封闭顶层集合 §4、桶 barrel §2.1、放置规则 §2);按文件的命名后缀审计(§5.2)不在本文范围:
| 区域 | 当前状态 | 目标态 |
|---|---|---|
遗留ipc.ts | v1 IPC 注册位于进程根,与 IpcApi 共存 | 各领域逐步迁移进ipc/(IpcApi),直至ipc.ts被退役(§1) |
从 main.ts 的源码可以看到这一偏差的实况:registerIpc()的注释明确写道——遗留的单体 IPC 注册造成了 bootstrap 与 IPC 就绪之间的时序耦合,TODO(v2) 计划将其分解为生命周期服务内部的逐服务ipcHandle/ipcOn。
设计使然的边界不是偏差,因此被有意省略:data/migration/v2/迁移器读取领域数据(§1),以及@logger/@application从任何层级的环境化访问(§3)。
9. 相关文档
- 架构总览——进程模型、数据流、monorepo 目录树(本文档的跨进程上级)。
- 渲染进程架构 / 共享层架构——各进程目录参考的对等文档。
- 命名规范——§4.8 封闭顶层、§4.9 单复数、§4.10 feature 与类型桶、§5.2 按形态路由。
- 人工智能
- 大模型
- AI 应用
- 交互助手
- 本地部署
【免费下载链接】cherry-studio
🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端
相关推荐
Cherry Studio 主进程架构详解:src/main 的封闭目录体系、依赖方向与治理规则
Cherry Studio 主进程架构详解:src/main 的封闭目录体系、依赖方向与治理规则 Cherry Studio 的 Electron 主进程目录
AI 应用大模型桌面应用本地部署RAGCherry Studio `@shared` 跨进程基础层架构:两大不变量、封闭顶层目录集与放置决策实战手册
Cherry Studio @shared 跨进程基础层架构:两大不变量、封闭顶层目录集与放置决策实战手册 本文基于 Cherry Studio 仓库的官方架构
AI 应用大模型桌面应用本地部署RAGCherry Studio 文档治理与规格驱动流程:封闭集目录树、Frontmatter 门禁与 Agent 决策记录
Cherry Studio 文档治理与规格驱动流程:封闭集目录树、Frontmatter 门禁与 Agent 决策记录 本文基于 Cherry Studio 仓
AI 应用大模型桌面应用本地部署RAG
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考