Cherry Studio 开发者协作与工程规范全解:从 CLAUDE.md 读懂多进程桌面应用的编码红线
【免费下载链接】cherry-studio🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio
CLAUDE.md 是 CherryHQ / cherry-studio 仓库为 AI 编码助手(Claude Code 等)编写的"总纲级"协作规范,它同时约束人与 Agent 的每一次代码改动:从编码心智、操作红线、开发命令、测试纪律,到进程架构、数据系统选型、窗口管理与生命周期服务,再到 v2 数据库迁移规则。读完本文,你将掌握该项目的完整工程约定、每一条规范对应的真实源码与配置落点,以及一套可直接照搬的多进程 Electron + React 应用的开发纪律清单。
一、CLAUDE.md 的定位与阅读顺序
CLAUDE.md位于仓库根目录,是一份覆盖整个研发流程的权威开发指令,其适用对象既包括人类开发者,也包括自动加载该文件的 AI 编码工具。全篇按如下顺序组织,构成一条从"动手前的心智"到"落库后的迁移"的完整链路:
- Guiding Principles(必须遵守)——思维方式与项目级操作规则;
- Development——命令、测试、补丁依赖管理;
- GitHub——PR、代码评审、Issue 的标准流程;
- Conventions——TypeScript、命名、日志、路径、i18n、UI 设计约定;
- Architecture——代码组织、数据系统、IPC、窗口管理、服务生命周期;
- Schema & Migration Rules——v2 数据层与数据库迁移纪律;
- Local Instructions——个人私有指令覆盖机制。
其中最后一条值得单独说明:如果仓库根目录存在被 gitignore 的CLAUDE.local.md,开发者在执行本文件任何内容前必须先完整阅读它——它承载开发者的私有指令,且在冲突时优先于本文件生效。这一机制保证了团队级规范与个人偏好可以共存而不互相污染。
二、编码心智:先思考、再最小化、外科手术式改动
2.1 Think Before Coding(先思考再编码)
CLAUDE.md 要求任何编码任务开始前先显式声明假设,不确定就提问,而不是默默替用户做决定:
- 存在多种合理解读时,必须把分歧摆上台面,禁止静默二选一;
- 有更简单的方案时要说出来,必要时对复杂方案提出反对;
- 遇到不清楚的地方立即停下,明确说出困惑点再问。
这四条的本质是把"AI 协作"从"猜需求"转变为"对齐需求",从源头降低返工成本。
2.2 Simplicity First(简单优先)
该节给出可量化的简化标准,值得逐条对照:
- 只写解决问题所需的最小代码,不做投机性设计;
- 不实现未被要求的特性;
- 不为一次性代码抽象;
- 不提供未被要求的"灵活性/可配置性";
- 不为不可能发生的场景写错误处理;
- "如果你写了 200 行而它本可以是 50 行,请重写";
- 内联注释上限为 2 行,超出意味着实现本身是补丁——应修复实现而非叙述实现;注释只解释why,不重述what,禁止 changelog、心得随笔、粘贴的聊天/评审回复。导出的公开 API 上的 TSDoc
@param/@returns/@deprecated属于文档而非叙述,不受此限。
2.3 Surgical Changes(外科手术式改动)
改动边界纪律:
- 只触碰任务要求的范围,不"顺手优化"相邻代码、注释或格式;
- 不重构没坏的东西;
- 即使自己风格不同也要匹配既有风格;
- 发现无关死代码时只提及、不删除;
- 清理自己改动所遗留的孤儿 import/变量/函数,但保留改动前就存在的死代码;
- 每一行改动都必须能追溯到用户请求。
2.4 Goal-Driven Execution(目标驱动执行)
把模糊任务转化为可验证目标,例如:
- "加校验" → "先为非法输入写测试,再让它们通过";
- "修 bug" → "先写一个能复现它的测试,再让它通过";
- "重构 X" → "确保重构前后测试均通过"。
多步骤任务必须给出带显式验证点的计划:
1. [Step] → verify: [check] 2. [Step] → verify: [check]三、操作红线:项目级 Operational Rules
CLAUDE.md 用一条条硬性规则定义了 Cherry Studio 特有的工程约束,每一条都能在仓库中找到对应落点:
| 规则 | 具体要求 | 仓库落点 |
|---|---|---|
| 先读本地 README | 编辑某目录前,先读该目录及其父目录的README.md,其记录了代码中看不出的约定与入口 | 如 src/main/core/paths/README.md |
| 修上游而非 hack 下游 | 新功能撞上既有模块限制时,先向上游提改进供决策,而非直接给下游打补丁 | 各 feature 服务层 |
| 库优先、自研殿后 | 优先查库/框架内置能力,只有无替代时才写自定义代码 | 依赖集中在 package.json |
| UI 统一用 Shadcn + Tailwind | 所有新 UI 组件一律取自@cherrystudio/ui(位于 packages/ui,Shadcn UI + Tailwind CSS) | packages/ui/src/components/ |
| 集中日志 | 所有日志走loggerService并携带正确 context,禁止console.log | src/main/core/logger/LoggerService.ts |
| 集中路径 | 主进程文件系统路径一律application.getPath('namespace.key', filename?),禁止app.getPath()、os.homedir()或临时拼接 | src/main/core/paths/README.md |
| 只验改动范围 | 代码改动跑pnpm lint+ 覆盖改动的测试;文档改动只需pnpm docs:check | package.json |
| Conventional Commits | 提交信息必须带具体模块 scope,如feat(data-api):、fix(lifecycle):,禁止main这类泛化 scope | 仓库提交历史 |
| 签名提交 | 每次提交必须git commit -S --signoff,并用git cat-file commit HEAD验证含gpgsig头 | Git 配置 |
| 分支策略 | main是唯一活跃开发默认分支,特性/重构/优化/修复都提交到这里 | docs/contrib/branching-strategy.md |
3.1 日志集中化的源码实现
"集中日志"并非一句口号,LoggerService.ts 的实现展示了它的完整机制:
- 全局单例
loggerService基于winston构建,主进程构造时抛出"不支持 worker 线程"以强制单进程日志; - 文件输出使用
winston-daily-rotate-file:通用日志app.%DATE%.log(单文件 10MB、保留 30 天),错误日志app-error.%DATE%.log(warn 起、保留 60 天),生产默认级别为info,开发/诊断模式为silly; - 支持通过
CSLOGGER_MAIN_LEVEL(级别过滤)与CSLOGGER_MAIN_SHOW_MODULES(模块白名单,逗号分隔)两个环境变量在开发/诊断模式下控制控制台输出; - 通过
withContext(module, context?)派生带模块上下文的子 logger(Object.create共享底层 logger 实例); error/warn级别会自动附加系统信息(OS、CPU、内存)与应用版本号,便于线上排障;- 渲染进程通过
IpcChannel.App_LogToMain通道把日志转发到主进程统一落盘。
3.2 路径集中化的源码实现
"禁止 ad-hoc 拼接路径"同样有完整实现支撑。根据 src/main/core/paths/README.md:
- 所有主进程路径统一注册在
pathRegistry.ts,通过application.getPath('feature.files.data', 'avatar.png')访问; - Key 命名受 ESLint 规则强制:
/^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/,至少两段、点号分隔、多词段用snake_case; - 命名空间分层:
cherry.*(通用基础设施)、sys.*(系统目录)、app.*(Electron 应用目录)、feature.*(功能数据,默认首选)、v1.*(仅清理用旧路径)、external.*(第三方路径,禁止删除); - 点号是语义而非物理路径:
feature.mcp.oauth实际位于~/.cherrystudio/config/mcp/oauth,绝不能从 key 嵌套推断磁盘嵌套; getPath()首次访问自动mkdirSync(目录 key),文件 key 只确保父目录;NO_ENSURE列表中的只读/外部/清理类路径跳过自动创建,并用satisfies做编译期类型校验;- 目录 key 禁止以
file结尾,因为自动 ensure 依赖后缀区分文件与目录; - 路径注册表在 preboot 阶段(
app.setPath('userData', ...)之后、app.whenReady()之前)构建一次,所有值只能依赖同步 Electron API、process.resourcesPath或 Node 内置模块;LoggerService与BootConfigService因运行更早,直接读取paths/constants.ts中的最早常量(CHERRY_HOME、BOOT_CONFIG_PATH、LOGS_DIR)。
四、开发命令全解
首次开发先执行pnpm install——Node 与 pnpm 版本已在 package.json 中锁定(当前为 Node>=24.11.1 <24.16.0、packageManager: pnpm@11.8.0),由 pnpm 强制校验。其余脚本一律以package.json为准,以下是必须掌握的六个:
| 命令 | 作用 | 底层构成(见 package.json) |
|---|---|---|
pnpm lint | 一键全量检查(会写文件) | oxlint --fix→eslint --fix→typecheck→i18n:check→format |
pnpm test | 运行全部 Vitest 测试 | 按 main / renderer / aiCore / ui / shared / provider-registry / scripts / preload 分项目串行执行 |
pnpm format | Biome 格式化 + lint(写入模式) | biome format --write && biome lint --write |
pnpm docs:check | 文档门禁 | docs:check-links+docs:check-structure+docs:check-frontmatter+docs:check-index |
pnpm build:check | 全量门禁,一条命令跑完lint+docs:check+ 全部test | 适合大而险的改动 |
pnpm test:lint | CI 等价 lint 门禁 | oxlint --deny-warnings+eslint,拒绝pnpm lint静默容忍的 warning |
4.1 精确选择检查范围
规范强调"只验改动,不验全仓":
- 代码改动:
pnpm lint+ 覆盖改动的测试;可用pnpm test:main <file>、pnpm test:renderer、pnpm test:aicore、pnpm test:shared、pnpm test:pkg:ui、pnpm test:scripts等分项目 wrapper,或对少量文件直接pnpm exec vitest run <file>; - 只有改动面很大或无法定位受影响测试时才跑全量
pnpm test; - 严禁
pnpm test <path>:test脚本内部用&&串联多次 vitest 调用,CLI 参数只会到达最后一次调用,前面的项目会以未过滤的完整套件执行; - 纯文档/Markdown 改动只需
pnpm docs:check; docs/references/**与docs/contrib/**下的文档带description/sourcesfrontmatter;docs/README.md是生成文件,改动 frontmatter 后运行pnpm docs:index,禁止手编索引;- CI 会跑全量门禁,本地任务只是"别明显搞坏它"。
五、测试纪律:断言契约,而非钉死现状
Cherry Studio 的测试体系运行在 Vitest 3 上(各项目配置见vitest.config.*),但真正独特的是一套反模式禁令:
禁止 behavior-pinning tests——只记录"代码当前行为"的测试(对任意输出的快照、对 mock 的
toHaveBeenCalled、用与实现相同方式反推的期望值)价值为零:它不会因真实原因失败、每次重构都会碎、还会把既有 bug 认证成"预期"。正确的做法是断言契约:真实输入 → 该功能承诺的输出,外加失败与边界用例。写测试前必须说得出"这个测试能抓住什么 bug",说不出来就不写。
CLAUDE.md 甚至允许(鼓励)在编辑某个文件时顺手删除该文件里已有的这类钉死行为测试,同时明确"仓库级大扫除是独立任务,不是某个无关 PR 的副作用"。
配套的测试设施约定:
- 前端测试必须阅读 docs/references/testing/frontend-testing.md;
- Mock 统一化:禁止为
application、服务或数据层临时自造 mock,统一使用 tests/mocks/README.md 中的 mock 体系; - 数据库测试:任何读写 SQLite 的 service/handler/seeder 必须用
@test-helpers/db的setupTestDatabase()——它提供带生产迁移的真实文件 DB;禁止手写CREATE TABLE、覆盖@application或 stub Drizzle 链,详见 docs/references/testing/database-testing.md。
六、GitHub 工作流
- PR:使用
gh-create-pr技能;兜底方案是直接阅读仓库内技能定义文件; - 代码评审:评审 GitHub PR 时不要在本地跑
pnpm lint/pnpm test/pnpm format——CI 已跑过,用gh检查即可; - Issue:使用
gh-create-issue技能,兜底方案同上。
七、编码约定(Conventions)
7.1 TypeScript 类型分层
跨进程类型归src/shared/,仅渲染进程使用的共享类型归src/renderer/types/,详见 docs/references/architecture/shared-layer.md。该分层的核心动机是:@shared只允许"跨进程 + 无可变运行时状态"的代码,从根上避免主/渲染进程间的隐式耦合。
7.2 命名规范
命名是"必须读"级别的强制文档:docs/references/architecture/naming-conventions.md 覆盖文件、目录、标识符与单复数规则。它对代码组织有两条直接影响:
- 每个进程根目录的顶层是一个closed set:新代码必须落入既有分类,禁止新建顶层目录(Naming §4.8);
- 目录的
index.ts是barrel:强制封装边界,只做 re-export(无逻辑、无嵌套、无export *),且仅当 lint 能封死深路径导入时才存在;index.tsx一律禁止(Naming §6.4)。
7.3 日志规范
规范给出的标准用法如下(完整实现见上文 3.1 节):
import { loggerService } from "@logger"; const logger = loggerService.withContext("moduleName"); // Renderer only: loggerService.initWindowSource('windowName') first logger.info("message", CONTEXT); logger.warn("message"); logger.error("message", error);7.4 路径规范
"必须读"级别文档为 src/main/core/paths/README.md,涵盖命名空间、命名规则、新增 key 的步骤与测试模式,详细内容见上文 3.2 节。
7.5 i18n 国际化
- 所有用户可见字符串必须走
i18next,禁止硬编码 UI 文案; - 语言目录位于
src/renderer/i18n/locales/与src/main/i18n/locales/,两者都以en-us.json为唯一事实源; - 新增/修改 key 的流程:只编辑
en-us.json→ 运行pnpm i18n:sync(用[to be translated]:占位符填充其他语言)→ 逐语言翻译完整; - 无需单独跑
i18n:check——pnpm lint已包含它,且会拒绝残留占位符、空值、插值/标签不匹配与未排序 key。
7.6 UI 设计
任何 UI 组件或页面样式工作,先读 DESIGN.md,严格遵守其中的颜色、字体、间距与组件规格。这也是上文"UI 统一用@cherrystudio/ui"规则的延伸。
八、架构约定:代码组织与四套数据系统
8.1 代码组织
在动手添加代码或打开某个目录前,先读对应进程的架构文档:
- 主进程 docs/references/architecture/main-process.md——
src/main/下的core/ipc/data/ai/features/services/utils/i18n目录及依赖方向; - 渲染进程 docs/references/architecture/renderer.md——
src/renderer/的"类型 × 领域"双轴布局与单向向下分层; - 共享层 docs/references/architecture/shared-layer.md——
@shared的准入条件与封闭顶层集合。
8.2 数据系统:按数据特征选型
docs/references/data/README.md 是数据系统的总入口,Cherry Studio 依据"数据特征 + 加载需求"划分出四个系统外加一张内部表:
| 系统 | 用途 | API 形态 | 丢失影响 |
|---|---|---|---|
| BootConfig | 早期启动设置(生命周期之前) | bootConfigService.get()、usePreference('BootConfig.*') | 低(可重建) |
| Cache | 临时数据(可丢失) | useCache、useSharedCache、useSharedCacheValue、usePersistCache | 无到极小 |
| Preference | 用户设置 | usePreference | 低(可重建) |
| DataApi | 业务数据(关键) | useQuery、useMutation | 严重(不可替代) |
app_state表 | 主进程内部连续性标记 | 各 owner 只读写自己的 key | 重复已完成工作 |
选型决策顺序(沿docs/references/data/README.md的决策流程):必须早于生命周期加载?→ BootConfig;可重建/可丢失?→ Cache;用户可配置且 key 结构稳定?→ Preference;用户活动产生的结构化业务数据?→ DataApi;防重复迁移/播种/对账的内部标记?→app_state表;否则回到第 2 步重新审视。
Cache 的三层持久化(同一份文档的定义):useCache纯内存、每次启动丢失、不跨窗口同步;useSharedCache经主进程跨窗口共享、重启丢失;usePersistCache跨重启存活,但渲染进程持久化到localStorage(渲染端权威),主进程持久化到自己的 JSON 文件{userData}/cache.json(主进程权威),两者是相互独立的存储,主进程只负责在窗口间中转渲染端持久化同步。
数据库技术栈:SQLite(better-sqlite3)+ Drizzle ORM,且驱动是同步的——getDb()查询与withWriteTx(fn)回调必须同步编写,不出现await。schema 位于src/main/data/db/schemas/,迁移用pnpm db:migrations:generate生成。
写原子性:多语句写入或"先读后写"必须使用application.get('DbService').withWriteTx(fn)包进一个同步的BEGIN IMMEDIATE事务(回调必须同步);单条写入不需要——better-sqlite3 在单连接上天然原子,详见 docs/references/data/database-patterns.md。
DataApi 边界规则:DataApi 只服务 SQLite 支撑的业务数据——没有数据库表就没有 DataApi 端点,改用 IPC,见 docs/references/data/api-design-guidelines.md。文档中的反模式表还给出了典型误用:AI 供应商配置存 Cache、对话历史存 Preference、主题列表存 Preference、主题/语言存 DataApi、API 响应存 DataApi、迁移状态存 Cache 等,均属错误选择。
8.3 IPC:IpcApi 作为第五子系统
非数据的命令型 IPC(窗口/系统/shell/通知/外部/文件)统一走IpcApi——它是与 BootConfig/Cache/Preference/DataApi 并列的第五个子系统,RPC-over-IPC,单一 schema 定义:schema + handler注册路由,ipcApi.request('namespace.action', input)调用,IpcApiService.broadcast/send+useIpcOn接收事件。旧式 command IPC 仍共存,因此两套都会遇到。选型裁决:SQLite 数据 → DataApi;用户设置 → Preference;可丢失/可共享 → Cache;其余命令式 → IpcApi。详见 docs/references/ipc/README.md。
8.4 Window Manager:三种生命周期模式
所有BrowserWindow必须经WindowManager创建,并声明三种模式之一:default/singleton/pooled。真实注册表在 src/main/core/window/windowRegistry.ts,当前已注册的类型包括:
- Main(singleton,主窗口,记住位置尺寸,macOS Dock 常驻);
- Print(default,隐藏的一次性打印面);
- McpBrowser(default,内置 MCP 浏览器的隐藏 CDP 面);
- SubWindow(pooled,拖出标签页的独立窗口,standbySize:1 + eager 预热);
- QuickAssistant(singleton,跟随光标的浮动面板);
- SelectionToolbar(singleton,选区工具栏);
- SelectionAction(pooled,动作结果窗口,standbySize:1、recycleMaxSize:3、5 分钟不活跃回收);
- Screenshot(pooled,每显示器一个的全屏截图遮罩,无 standby、10 分钟回收)。
注册表按"基础配置 + 平台覆盖"组织:mergeWindowOptions的合并优先级为 注册表基础windowOptions→ 基础platformOverrides[当前平台]→ 调用方overrides→ 调用方overrides.platformOverrides[当前平台],webPreferences按同序深合并,platformOverrides最终被剥离以免泄漏给new BrowserWindow(...)。
消费者侧三条铁律:
- 业务代码只允许
open()/close(),禁止create()/destroy(); - 事件监听必须在
onWindowCreated中挂接,而不是open()之后——复用窗口不会走后者; - 渲染进程通过
useWindowInitData读取初始化数据。
详见 docs/references/window-manager/README.md。
8.5 服务生命周期:IoC 容器 vs 直接导入单例
所有"持有长生命周期资源或注册持续副作用"的主进程服务必须纳入生命周期系统:
- 继承
BaseService,使用@Injectable、@ServicePhase、@DependsOn装饰器; - 在 src/main/core/application/serviceRegistry.ts 注册——每服务一行(该文件当前登记了 CacheService、DataApiService、DbService、PreferenceService、WindowManager、AiService、McpRuntimeService、JobManager、SchedulerService、MainWindowService、SubWindowService、QuickAssistantService、TrayService 等数十个服务);
@DependsOn只声明同阶段依赖:WhenReady 服务禁止依赖 BeforeReady 服务(PreferenceService、DbService、CacheService、DataApiService),因为容器已保证 BeforeReady 阶段先于 WhenReady 完成(见 docs/references/lifecycle/README.md 的"跨阶段依赖自动成立"规则);- 通过
application.get('Name')访问(@Conditional服务用getOptional()); - 生命周期基建方法:
this.ipcHandle()/this.ipcOn()(IPC,stop/destroy 时自动清理,返回Disposable)、this.registerInterval()(循环定时器,自动 unref、异常隔离、自动清理)、this.registerDisposable()(清理追踪,接受Disposable或() => void); - 服务间事件用
Emitter<T>/Event<T>,一次性完成信号用Signal<T>; - 拥有重型按需资源的服务实现
Activatable(IPC 保持注册,资源经onActivate()/onDeactivate()加载/释放); - 禁止
new或手工单例——容器统一管理实例化、顺序与关停。
配套文档:docs/references/lifecycle/lifecycle-usage.md(代码示例)、docs/references/lifecycle/lifecycle-migration-guide.md(旧服务迁移)。
反面情形——没有长生命周期资源或持续副作用的服务,用具名导出单例(export const x = new X()),不采用getInstance()模式。判据见 docs/references/lifecycle/lifecycle-decision-guide.md:典型反模式包括给ExportService(全方法作用域工作)上生命周期、给CacheService(持有需关停清理的 GC 定时器)用直接导入、在模块作用域调用application.get()(早于 bootstrap,服务未注册)等。
九、Schema 与迁移规则:v2 数据层不可动摇的底线
v2 重构已落地,规范对数据迁移划出三条硬性纪律:
- v1 数据只能经由
src/main/data/migration/v2/的 migrator 进入 v2,禁止为 v1 的保存/读取/丢失添加任何 fallback、双写或守卫; - 迁移链不再是可丢弃的:迁移链已合并为单一干净的初始迁移并随
v2.0.0-rc.1发布,migrations/sqlite-drizzle/现在运行在持有真实用户行的数据库上——绝不擦除或重写已发布的迁移,绝不建议用户删库;schema 变更一律以pnpm db:migrations:generate追加新迁移,且每个变更都必须能在有数据填充的库上 migrate-forward 存活; - 迁移合并冲突:重新生成,而非改名:上游迁移与本地产物冲突时,删除本地的
.sql及其meta/*_snapshot.json后重跑pnpm db:migrations:generate;改名/重编号会静默复用快照的随机id导致全链分叉,且drizzle-kit generate仍返回 0,只有pnpm db:migrations:check能抓住——CI 同时强制链检查和 schema↔迁移的 generate-and-diff。
数据分类工具链:v2-refactor-temp/tools/data-classify/是 v2 数据层的代码生成流水线,classification.json是唯一事实源。四个文件自动生成、严禁手编:src/shared/data/preference/preferenceSchemas.ts、src/shared/data/bootConfig/bootConfigSchemas.ts,以及src/main/data/migration/v2/migrators/mappings/下的PreferencesMappings.ts+BootConfigMappings.ts。修改方式是编辑data/下的classification.json或target-key-definitions.json,然后cd v2-refactor-temp/tools/data-classify && npm run generate。
Breaking Changes 日志:当 v2 变更对用户可感知并影响使用方式时,需在v2-refactor-temp/docs/breaking-changes/下新增条目,格式约定见 v2-refactor-temp/docs/breaking-changes/README.md。
十、给协作方的一句话总结
Cherry Studio 的 CLAUDE.md 本质上把"多人 + 多 Agent 共改一套多进程 Electron 代码库"这件事做了彻底的确定性约束:心智层要求先对齐需求、最小实现、外科手术式改动;操作层用集中日志、集中路径、统一 UI、签名提交和 Conventional Commits 收敛全局副作用;架构层用 closed set + barrel、四套数据系统、IpcApi、WindowManager 与生命周期容器把模块边界钉死;数据层则对 v2 迁移链立下"永不重写、只追加、冲突必重新生成"的底线。对于任何准备向本仓库提交代码的开发者或 Agent,遵循这份规范就是最低成本的正确路径。
【免费下载链接】cherry-studio🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考