news 2026/9/20 1:43:15

Cherry Studio 跨进程共享层(@shared)架构指南:五目录封闭集合、双不变量与放置决策

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Cherry Studio 跨进程共享层(@shared)架构指南:五目录封闭集合、双不变量与放置决策
  • 人工智能
  • 大模型
  • AI 应用
  • 交互助手
  • 本地部署

【免费下载链接】cherry-studio

🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端

项目地址:https://gitcode.com/CherryHQ/cherry-studio
点击查看免费下载

Cherry Studio 是一个基于 Electron 的多 LLM 提供商桌面客户端,其源码划分为main(主进程)、renderer(渲染进程)、preload(预加载桥)与shared四个根目录。其中src/shared(别名@shared)是跨进程原语层——承载跨进程共享的类型、契约与纯逻辑,被主进程、渲染进程与预加载脚本共同导入。本文以 Shared Layer Architecture 为骨架,结合仓库真实源码,系统讲解@shared的成员准入规则(两条不变量)、封闭的顶层目录集合、typesutils的形态划分、放置决策流程与反模式清单,帮助你在 Cherry Studio 中正确判断"某段代码是否属于@shared、应放在哪个子目录"。

1.@shared在分层架构中的位置

在 Cherry Studio 的四层依赖模型中(详见 Renderer Architecture),@sharedpackages/ui一起位于最底层的Primitives(第 4 层)

目录角色
1. App / compositionwindows/routes/、顶层pages/入口、路由、应用壳
2. Domain(目标态)features/<domain>/业务域纵向切片
3. Sharedcomponents/hooks//services/utils//data//ipc//workers/跨域可复用件
4. Primitivespackages/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),无论由哪个进程消费——包括仅被渲染进程使用的类型(如TabChatScrollAnchorAgentOpenExternalAppTarget等)。

源码中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的私有commandMapnew 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、错误模型、共享类型(IpcContextWindowId)。与领域无关
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.tsmcp.tsserializable.tsminiAppManifest.ts等纯声明文件;src/shared/utils/下是keywordSearch.tsdataUrl.tsconversationTitle.tsserializable.ts等纯函数文件,二者形态边界清晰。

4.1 文件 vs 子目录,以及 barrel

Barrel(index.ts聚合导出)规则以 Naming Conventions §6.4 为跨进程权威,本节只覆盖@shared特有细节:

  • 默认是单个.ts文件。大多数主题就是一个文件——types/<topic>.tsutils/<topic>.ts,直接导入。只有当主题确实拥有多个文件时才升级为子目录(Naming Conventions §4.4);绝不预先创建。
  • 主题子目录恰好有一个index.ts作为其公共 API——types/<topic>/index.tsutils/<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/GBAPP_NAME)。只有当 100% 确定某个常量是应用全局且横切时才能加入;只要它属于任何领域,就该放进该领域的文件。——这正是旧config/constant.ts缺失的护栏,也是它长成"82 个导入方的杂物抽屉"(现已解散,见 §6)的原因。仓库现状src/shared/utils/constants.ts恰好只含KBMBGBAPP_NAMELATEST_PRIVACY_POLICY_VERSION五个全局量,与文档描述完全吻合。
  • 单进程常量 → 离开@shared(违反不变量一)。
  • 没有config/。常量是数据;一个放在其领域文件(或utils/)中的冻结值,能表达config/目录想做的一切,还不会招来无关的全局量。

4.3 有状态类的蓝图(Stateful-class blueprints)

有状态类的定义是纯代码,因此它搭乘utils/下的主题模块——先例是utils/blacklistMatchPattern.ts中的有状态类MatchPatternMap@shared没有services/桶,因为服务是按进程的(不变量二)。

5. 放置决策(Placement Decision)

按顺序经过两道门,然后归类:

  1. 跨进程吗?是否被两个进程都可达——不是 → 进入进程层(src/main/*src/renderer/*)。(例外:Cache key 的 schema 条目与 value 类型即使单进程也留在@shared/data/cache/——§2.1.1。)
  2. 无状态 / 不可变吗?是否导出实例、是否持有可变状态——不是 → 只有蓝图和静态数据留下;实例按进程放置。
  3. 归类:核心领域(ai)/ 基础设施(dataipc)/ 形态(typesutils)。不属于前两者 → 按形态分解进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"——状态没有一致的共享归属者;它属于mainrenderer

7. 目标态 vs 当前态(Target vs Current State)

顶层分解已是当前事实:src/shared/只含ai/data/ipc/types/utils/五个目录。下表记录的是剩余已知差距,而非已完成迁移的历史:

区域当前目标
data/types/中的转换器/守卫——coerceSearchRolederiveRootSpanIdreadCherryMeta/withCherryMetaknowledge.ts的字符串助手逻辑住在data类型桶内;data/types/__tests__/下的行为测试暴露了它(§4.1 末段)悬而未决:按形态路由会把它们移到utils位置,但 schema 派生的守卫按惯例就近共置——决策已推迟
IpcChannel.tsv1 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 提供商的桌面客户端

项目地址:https://gitcode.com/CherryHQ/cherry-studio
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 1:40:26

AI辅助开发智慧厂房3D大屏:从CAD到上线的极速实践

接到智慧厂房3D大屏这个需求时&#xff0c;客户手里只有一张老旧的CAD平面图、一段产线监控视频&#xff0c;以及一堆散落在Excel里的设备台账。按我以往的干法&#xff0c;这种项目从现场调研到能演示的版本&#xff0c;至少要三周。但这次我换了一套打法&#xff1a;用GPT-Im…

作者头像 李华
网站建设 2026/9/20 1:40:04

Meteor 开源贡献完全指南:从 Bug 报告到核心 PR 的完整流程解析

后端前端开发工具移动开发 【免费下载链接】meteor Meteor, the JavaScript App Platform 项目地址&#xff1a; https://gitcode.com/gh_mirrors/me/meteor 点击查看 免费下载 本篇指南基于 Meteor 主仓库根目录下的 CONTRIBUTING.md 编写&#xff0c;系统梳理了向这个 JavaS…

作者头像 李华
网站建设 2026/9/20 1:36:48

MATLAB直接序列扩频DSSS仿真:处理增益与干扰容限分析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华