- 人工智能
- 大模型
- AI 应用
- 交互助手
- 本地部署
【免费下载链接】cherry-studio
🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端
Cherry Studio 是一个基于 Electron 的多 LLM 提供商桌面客户端,其源码划分为main(主进程)、renderer(渲染进程)、preload(预加载桥)与shared四个根目录。其中src/shared(别名@shared)是跨进程原语层——承载跨进程共享的类型、契约与纯逻辑,被主进程、渲染进程与预加载脚本共同导入。本文以 Shared Layer Architecture 为骨架,结合仓库真实源码,系统讲解@shared的成员准入规则(两条不变量)、封闭的顶层目录集合、types与utils的形态划分、放置决策流程与反模式清单,帮助你在 Cherry Studio 中正确判断"某段代码是否属于@shared、应放在哪个子目录"。
1.@shared在分层架构中的位置
在 Cherry Studio 的四层依赖模型中(详见 Renderer Architecture),@shared与packages/ui一起位于最底层的Primitives(第 4 层):
| 层 | 目录 | 角色 |
|---|---|---|
| 1. App / composition | windows/、routes/、顶层pages/ | 入口、路由、应用壳 |
| 2. Domain(目标态) | features/<domain>/ | 业务域纵向切片 |
| 3. Shared | components/→hooks//services/→utils//data//ipc//workers/ | 跨域可复用件 |
| 4. Primitives | packages/ui、@shared、@logger | 应用无关的基础层 |
@shared的特点在于它是跨进程的:Electron 的 main 与 renderer 运行在各自的 V8 隔离环境(realm)中,@shared的模块在每个进程内各被加载一次,因此它只允许导出类型、纯函数与不可变数据,不能携带任何进程相关的运行时状态。它与"面向单个进程的共享"(如 renderer 内部的components/、hooks/、services/)的根本区别,就是 Architecture Overview 中所说的:src/shared/是cross-process primitive layer,其准入门槛是"跨进程"而非"恰好被多处使用"。
从源码看,src/shared/当前实际只存在五个顶层目录,与文档声明完全一致:
src/shared/ ├── ai/ # 核心领域:AI 跨进程契约与纯逻辑 ├── data/ # 跨进程基础设施:API 类型、Cache/Preference/BootConfig schema、migration 映射、presets ├── ipc/ # 跨进程基础设施:IpcApi 框架(define helpers、请求/事件 schema、错误模型、共享类型) ├── types/ # 形态桶:无单一归属方的跨进程类型声明 ├── utils/ # 形态桶:跨进程纯逻辑及其常量、类蓝图 └── IpcChannel.ts # v1 遗留 channel 枚举(见 §6 待迁移项)2. 两条不变量(Invariants)
一切想进入@shared的模块必须同时满足以下两条,否则它就不属于这里。这两条是本文档的"准入宪法",也是理解@shared全部规则的前提。
2.1 不变量一:跨进程(Cross-process)
一个模块只有在main 与 renderer 两个进程都实际使用它时,才属于@shared——类型声明同样适用这条规则。
- 原因:
@shared是跨进程边界的唯一事实源(single source of truth);单进程代码本就有属于自己的进程层可以安放。 - 只被一个进程可达→ 应放在该进程自己的层(
src/main/*或src/renderer/{utils,hooks,services})。 - 禁止投机性放置(no speculative placement):如果某物只是"可能"会跨进程,就先写在
main/renderer,待它真正跨进程时再移入@shared。不要把代码"预存"在@shared里等将来用——最常见的失败模式就是为"以防万一"加了一个类型或工具函数,结果从未被跨进程使用,最终沦为冗余(cruft)。
2.1.1 唯一例外:Cache schema 注册表
Cache 子系统是 §2.1 的唯一豁免。所有 Cache 的key schema 与其 value 类型都必须放在@shared/data/cache/(cacheSchemas.ts+cacheValueTypes.ts),无论由哪个进程消费——包括仅被渲染进程使用的类型(如Tab、ChatScrollAnchor、AgentOpenExternalAppTarget等)。
源码中src/shared/data/cache/cacheSchemas.ts的头部注释印证了这一点:它定义了 key 命名规范(namespace.sub.key_name形式、模板 key${xxx}占位符、由 ESLint 规则data-schema-key/valid-key强制校验),并统一从./cacheValueTypes引用值类型。也就是说,一个"渲染进程专用的 cache value 类型"出现在这里属于合规行为,而非 §2.1 违规——不要将其标记或迁移。豁免仅限 Cache 子系统,其余一切位置仍适用 §2.1。
2.2 不变量二:无可变运行时状态(No mutable runtime state)
@shared只导出类型、纯函数与不可变数据,绝不导出类实例单例(services / managers / registries),也不导出任何持有运行时可变状态的模块级值。
- 原因:main 与 renderer 是相互隔离的 V8 realm,
@shared模块每个进程加载一次。所谓"共享单例"是一个假象——它实际上会退化为 N 个互不同步的进程内实例。可变状态没有一致的共享归属者,它应属于承载其生命周期与上下文的那个进程。 new不是判定标准——运行时可变性 + 身份(identity)才是。new只被允许用于构建随后被冻结并导出的不可变数据(例如由静态数据一次性构建、之后永不修改的Map/Set/RegExp查找表)。仓库中src/shared/utils/command/definitions.ts的私有commandMap(new Map<CommandId, RegisteredCommandDefinition<CommandId>>(...))正是文档点名的这类只读查找表实例。- 有状态类只从
@shared导出其"定义(蓝图)",实例则按进程创建。例如ContextKeyService的定义跨进程共享(位于src/shared/utils/command/contextExpr.ts,经src/shared/utils/command/index.ts导出),但new ContextKeyService()的实例化发生在渲染进程的src/renderer/components/command/CommandContextKeyProvider.tsx中——蓝图在@shared,实例在进程内。
准入对照表(Allowed vs Banned):
| 允许(Allowed) | 禁止(Banned) |
|---|---|
type/interface/enum、schema 派生类型 | export const x = new XService()(任何导出的实例单例) |
| 纯函数、谓词、转换器 | registry / manager / service 实例 |
不可变数据——常量、定义、通过new Map/Set构建的冻结查找表 | 任何持有运行时可变状态的模块级值 |
| 有状态类的定义(蓝图) | 此类的一个活着的实例 |
3. 封闭的顶层集合(The Closed Top-Level Set)
@shared的顶层是一组封闭集合(closed set)——这是 Naming Conventions §4.8"顶层默认封闭"原则在@shared上的应用。恰好五个目录,按三条有原则的类别划分:
| 目录 | 类别 | 为何能拥有顶层位置 |
|---|---|---|
ai | 核心领域(Core domain) | Cherry Studio 本质是 AI 产品;AI 的跨进程契约与纯逻辑是一等公民(镜像src/main/ai/)。只承载 AI 的跨进程切片——不含 AI UI 或按进程区分的服务 |
data | 跨进程基础设施(Cross-process infra) | 数据层的跨进程契约:API 实体/请求类型、cache/preference/bootConfig schema、migration 映射、presets。框架式、与领域无关 |
ipc | 跨进程基础设施(Cross-process infra) | IpcApi 框架:routedefinehelpers、请求 + 事件 schema、错误模型、共享类型(IpcContext、WindowId)。与领域无关 |
types | 形态桶(Shape bucket) | 无单一归属方的跨进程类型声明 |
utils | 形态桶(Shape bucket) | 跨进程纯逻辑及其配套常量与类蓝图 |
治理规则:一个新能力永远不能赢得一个新的顶层目录。它要么是(a)核心领域(只有ai),要么是(b)真正的跨进程基础设施,要么是(c)按形态(shape)分解进types/utils。其余一切 →types/utils。
命名遵循 Naming Conventions §4.9:ai/data/ipc是单数命名空间,types/utils是复数桶。
4. 形态划分:typesvsutils
@shared只有两个形态桶。由于没有 UI、没有 React、没有按进程区分的运行时,渲染进程丰富的形态(components/hooks/services/pages)在这里坍缩为"声明 vs 纯逻辑"两类:
types/ | utils/ |
|---|---|
| 类型别名、接口、枚举、schema 派生类型 | 纯函数、谓词、转换器 |
| (外加类型所需的小常量) | 外加这些函数所需的常量 / 静态数据,以及有状态类的蓝图 |
两者之间的路由遵循 Naming Conventions §5.2 的"按形态路由"表。从源码观察:src/shared/types/下是command.ts、mcp.ts、serializable.ts、miniAppManifest.ts等纯声明文件;src/shared/utils/下是keywordSearch.ts、dataUrl.ts、conversationTitle.ts、serializable.ts等纯函数文件,二者形态边界清晰。
4.1 文件 vs 子目录,以及 barrel
Barrel(index.ts聚合导出)规则以 Naming Conventions §6.4 为跨进程权威,本节只覆盖@shared特有细节:
- 默认是单个
.ts文件。大多数主题就是一个文件——types/<topic>.ts、utils/<topic>.ts,直接导入。只有当主题确实拥有多个文件时才升级为子目录(Naming Conventions §4.4);绝不预先创建。 - 主题子目录恰好有一个
index.ts作为其公共 API——types/<topic>/index.ts、utils/<topic>/index.ts,显式具名导出,禁止export *。这样无论主题是文件还是子目录,导入面都完全一致(@shared/utils/<topic>两种形式皆可),子目录内的其他文件保持私有。 - 桶根
types/与utils/没有index.ts。桶是类别(category)而非模块——一个把每个文件都重新导出的根 barrel 不会带来聚合 API,只会为每次新增带来 churn 和导入环风险。要导入具体文件或主题,绝不导入桶根。 types/没有任何运行时测试。声明桶没有运行时行为可测,因此types/下的行为测试(expect(fn(...))…)恰恰说明该文件含有逻辑——谓词、类型守卫、转换器、工厂或函数——应属于utils/(按 §4 的形态路由)。把逻辑移到utils/<topic>.ts(从types/导入所需类型,这是受祝福的utils → types方向),测试随之移动。类型守卫(x is T)同样属于运行时谓词,应与逻辑放在utils/,而不是与接口一起留在types/。由校验函数构建的 schema(z.custom(isFoo))跟随函数进入utils/;纯声明式 schema(z.object({…}))可以留在types/。types/中唯一应当存在的测试是类型级测试(expectTypeOf/assertType):它断言类型契约本身、没有运行时可供迁移,但它只是过渡性守护——仅当手写类型仍是事实源时才有价值;一旦运行时 schema(Zod / IpcApi)接管契约、类型变为z.infer派生,schema 自身的校验已涵盖它,类型级测试应随那次迁移退役。
4.2 常量与静态数据
- 默认:常量放在其领域/主题的单文件中,紧邻其服务的逻辑(AI 模型默认值 →
ai/;文件类型列表 →utils/file/)。 utils/constants.ts不是桶。它只承载真正全局、跨进程的残量(KB/MB/GB、APP_NAME)。只有当 100% 确定某个常量是应用全局且横切时才能加入;只要它属于任何领域,就该放进该领域的文件。——这正是旧config/constant.ts缺失的护栏,也是它长成"82 个导入方的杂物抽屉"(现已解散,见 §6)的原因。仓库现状src/shared/utils/constants.ts恰好只含KB、MB、GB、APP_NAME、LATEST_PRIVACY_POLICY_VERSION五个全局量,与文档描述完全吻合。- 单进程常量 → 离开
@shared(违反不变量一)。 - 没有
config/桶。常量是数据;一个放在其领域文件(或utils/)中的冻结值,能表达config/目录想做的一切,还不会招来无关的全局量。
4.3 有状态类的蓝图(Stateful-class blueprints)
有状态类的定义是纯代码,因此它搭乘utils/下的主题模块——先例是utils/blacklistMatchPattern.ts中的有状态类MatchPatternMap。@shared没有services/桶,因为服务是按进程的(不变量二)。
5. 放置决策(Placement Decision)
按顺序经过两道门,然后归类:
- 跨进程吗?是否被两个进程都可达——不是 → 进入进程层(
src/main/*或src/renderer/*)。(例外:Cache key 的 schema 条目与 value 类型即使单进程也留在@shared/data/cache/——§2.1.1。) - 无状态 / 不可变吗?是否导出实例、是否持有可变状态——不是 → 只有蓝图和静态数据留下;实例按进程放置。
- 归类:核心领域(
ai)/ 基础设施(data、ipc)/ 形态(types、utils)。不属于前两者 → 按形态分解进types/utils;绝不新开顶层目录。
6. 反模式清单(Anti-Patterns)
- 导出的实例单例——
export const x = new XService(),或任何 registry / manager / service 实例。违反不变量二。 - 单进程代码进入
@shared——仅为方便而把 main-only 或 renderer-only 的逻辑放在这里。违反不变量一。(前重灾区:现已解散的config/constant.ts——§7。Cache schema 注册表是唯一被认可的例外——§2.1.1。) - 杂物抽屉式文件或目录——一个
config/桶或constant.ts,跨领域、跨进程地堆积无关全局量。应按领域 + 进程分解,不要整块搬迁。 - 每个能力开一个顶层目录——每个能力都按形态分解;顶层是封闭的(§3)。
@shared中的有状态"service"——状态没有一致的共享归属者;它属于main或renderer。
7. 目标态 vs 当前态(Target vs Current State)
顶层分解已是当前事实:src/shared/只含ai/、data/、ipc/、types/、utils/五个目录。下表记录的是剩余已知差距,而非已完成迁移的历史:
| 区域 | 当前 | 目标 |
|---|---|---|
data/types/中的转换器/守卫——coerceSearchRole、deriveRootSpanId、readCherryMeta/withCherryMeta、knowledge.ts的字符串助手 | 逻辑住在data类型桶内;data/types/__tests__/下的行为测试暴露了它(§4.1 末段) | 悬而未决:按形态路由会把它们移到utils位置,但 schema 派生的守卫按惯例就近共置——决策已推迟 |
IpcChannel.ts | v1 channel 枚举位于根目录,仍被遗留领域与 data/IpcApi 传输通道使用 | 逐领域退役遗留条目,然后把剩余的基础设施通道移到ipc/下 |
8. 与周边文档的关系
- Architecture Overview——进程模型与
@shared的一句话总结。 - Renderer Architecture §2–§3——层模型及渲染进程如何依赖
@shared;其 §6 拥有 command 的renderer 侧单元格(本文拥有其@shared单元格)。 - Naming Conventions §4.8——顶层默认封闭(本文是其在
@shared上的应用);§4.9 单数 vs 复数;§5.2 按形态路由。
- 人工智能
- 大模型
- AI 应用
- 交互助手
- 本地部署
【免费下载链接】cherry-studio
🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端
相关推荐
Cherry Studio `@shared` 跨进程基础层架构:两大不变量、封闭顶层目录集与放置决策实战手册
Cherry Studio @shared 跨进程基础层架构:两大不变量、封闭顶层目录集与放置决策实战手册 本文基于 Cherry Studio 仓库的官方架构
AI 应用大模型桌面应用本地部署RAGCherry Studio 主进程架构解析:`src/main` 封闭顶层目录集合与依赖规则
Cherry Studio 主进程架构解析: src/main 封闭顶层目录集合与依赖规则 导读 本文是 Cherry Studio 桌面客户端主进程(Elec
人工智能大模型AI 应用交互助手本地部署Serial Studio 共享变量(Shared Variables)完整指南:用 Data Tables 实现跨数据集校准、滤波与状态共享
Serial Studio 共享变量(Shared Variables)完整指南:用 Data Tables 实现跨数据集校准、滤波与状态共享 导读 本文是 S
桌面应用数据可视化物联网
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考