Cherry Studio 主进程媒体协议:`cherry-media://` 内存二进制媒体分发架构与实战指南 人工智能大模型AI 应用交互助手本地部署【免费下载链接】cherry-studio Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址https://gitcode.com/CherryHQ/cherry-studio点击查看免费下载本篇指南围绕 Cherry Studio 主进程中的src/main/services/mediaProtocol模块展开讲解它如何通过自定义cherry-media://scheme 把主进程内存中的大体积二进制媒体如截图直接分发给渲染进程从而绕开 IPC 结构化克隆与磁盘临时文件的成本。读完本文你将掌握该协议的设计动机、URL 与 kind 模型、Electron 两阶段注册时序、特权声明、内存生命周期契约、与cherrystudio://深链协议的区别以及如何在真实业务截图浮层中安全地store/getUrl/remove。为什么需要一个内存媒体协议在 Electron 应用中渲染进程经常需要展示主进程已经持有的二进制数据。常规方案有两种各有明显短板通过 IPC 传递Electron 的 IPC 默认使用结构化克隆structured clone序列化数据大 Buffer 拷贝成本高、耗内存写临时文件再读取把数据落盘再让渲染进程读文件一次全屏截图几十 MB 的往返开销比截图本身还贵。Cherry Studio 的截图浮层screenshot overlay正是首个吃下这类痛点的消费者每个显示器的全屏截图像素数据高达几十 MB浮层既要整图绘制、又要反复读取局部区域。因此主进程侧实现了mediaProtocol模块把二进制媒体留在主进程内存中通过自定义cherry-media://scheme 让渲染进程按 URL 拉取字节不经过 IPC 拷贝、不落临时文件。其设计思想与完整实现记录在 mediaProtocol 模块 README。URL 形状单 scheme kind 作为 host协议 URL 的固定形状为cherry-media://kind/id └ host └ pathkind是 URL 的 host 段id是 path 段。一个 scheme 承载所有媒体类型类型通过 host 区分。当前代码中 kind 常量定义于 types.tsexport const CHERRY_MEDIA_SCHEME cherry-media export const MediaKind { Image: image } as const export type MediaKind (typeof MediaKind)[keyof typeof MediaKind] export const MEDIA_KINDS new Setstring(Object.values(MediaKind)) export interface MediaEntry { data: Buffer mimeType: string }也就是说cherry-media://image/uuid是当前唯一合法的 URL 形态MEDIA_KINDS集合用于在请求处理时校验 kind 合法性。为什么不做 per-kind 的独立 schemeElectron 的protocol.registerSchemesAsPrivileged每个进程只能调用一次且必须在 app ready 之前执行。如果为每种媒体各注册一个 scheme那么每新增一种媒体类型都是一次 preboot 阶段的改动。采用单 scheme host 区分 kind 后新增类型只需在MediaKind中增加一个条目完全不动 preboot 时序。未知 kind 必须返回 400id 是按 kind 隔离的image类型下存的 id 与video类型毫无关系。如果收到cherry-media://video/image-id绝不能 fallthrough 到 image 的存储里去取数据——那会返回错误字节。实现上未知 kind 直接以 400 拒绝见下文请求处理一节测试用例rejects an unknown kind instead of falling through to another kind store也专门锁定了这一行为见 MediaProtocolService.test.ts。模块地图四个文件各司其职文件职责types.ts定义CHERRY_MEDIA_SCHEME、MediaKindMEDIA_KINDS、内部MediaEntry结构registerSchemes.ts导出CHERRY_MEDIA_SCHEME_DECLARATION—— app ready 之前的特权声明由 main.ts 与 mini-app scheme 一起放进进程唯一的registerSchemesAsPrivileged调用MediaProtocolService.ts存储store与protocol.handle响应器本体index.tsbarrel 出口 —— 目录外代码唯一允许的导入面从源码看index.ts 只导出MediaProtocolService、CHERRY_MEDIA_SCHEME_DECLARATION与CHERRY_MEDIA_SCHEME/MediaKind其余实现细节被完全封装在目录内部。Electron 两阶段注册时序约束是设计的分界线Electron 把自定义协议拆成两个半场且二者的时序约束正好相反步骤API时机调用方声明特权protocol.registerSchemesAsPrivilegedapp ready 之前之后调用会抛错main.tspreboot 序列安装响应器protocol.handle仅限 app ready 之后MediaProtocolService.onInitPhase.WhenReady这一点在 main.ts 中体现得淋漓尽致消费CHERRY_MEDIA_SCHEME_DECLARATION的registerSchemesAsPrivileged调用位于main.ts 的顶层同步段而不是startApp()内部——因为runV2MigrationGate()会await app.whenReady()放在它后面的任何代码都已经太迟// main.ts 顶层同步段节选 protocol.registerSchemesAsPrivileged([CHERRY_MEDIA_SCHEME_DECLARATION, MINI_APP_SCHEME_DECLARATION])而安装响应器则完全相反MediaProtocolService.ts 用ServicePhase(Phase.WhenReady)声明自身在Phase.WhenReady阶段初始化onInit内部才调用protocol.handle(CHERRY_MEDIA_SCHEME, ...)。protocol.handle要求 app-ready 进程Phase.WhenReady恰好表达这一约束生命周期相位机制见 core/lifecycle 目录。值得注意的是按 core/preboot/README.md 的成员资格标准第二条本模块刻意不在core/preboot/目录中截图是可移除能力removable capability因此模块落在它天然的归属目录services/下由main.ts在正确时机调用。这一点与main.ts顶部注释“新服务应归入生命周期系统preboot 只放不可移除步骤”的分层原则完全一致。特权声明逐项解读registerSchemes.ts 中的声明如下export const CHERRY_MEDIA_SCHEME_DECLARATION { scheme: CHERRY_MEDIA_SCHEME, privileges: { standard: true, secure: true, supportFetchAPI: true, corsEnabled: true } } as const特权作用standardURL 能按 host path 解析这正是 handler 拿到 kind 段用于分发的前提secure被当作安全源secure origin处理浮层不会被拦截加载supportFetchAPIcorsEnabled消费者用fetch拉取字节为 Blob再通过 object URL 渲染若跨源img被绘制进 canvas会污染画布导致导出时抛SecurityError。而corsEnabledscheme 经由protocol.handle服务时无需返回Access-Control-Allow-Origin头因此 handler 不发送任何 CORS 头刻意不开启stream那是给支持 Range 请求的音视频用的应当等出现第一个真正需要的 kind 时再按需添加而不是提前臆测。生命周期契约每一次 store() 都要配对 remove()这是本模块最核心的内存纪律没有任何机制会自动回收条目——刻意不做 TTL、不做定时清扫器sweeper。理由很实在基于时间的清扫必须猜测“一次会话持续多久”而截图会话的真实时长无上界——用户可能慢慢标注或让保存对话框无限期开着猜短了图片会在活动的浮层底下凭空消失要豁免这种场景就得给条目打上 owned / unowned 标签届时 TTL 分支实际上没有任何真实调用方——因为当前每个消费者都是 owned 的。因此防泄漏靠两条确定性的检查而非定时器单元测试断言会话结束_doDestroy后每个 id 的has()都为false内存回归检查重复执行会话后内存回到基线。当未来真的出现“存了就不再管”的 fire-and-forget 消费者时再为那个消费者专门设计回收方案而不是现在为它加通用 TTL。此外onInit把protocol.unhandle与清空整个 store注册为 disposable即使某个调用方漏掉了自己的remove()进程关闭时也不会让截图驻留内存。对应源码在 MediaProtocolService.tsprotected async onInit(): Promisevoid { protocol.handle(CHERRY_MEDIA_SCHEME, (request) this.handleRequest(request)) this.registerDisposable(() protocol.unhandle(CHERRY_MEDIA_SCHEME)) this.registerDisposable(() this.stores.clear()) logger.info(Media protocol handler registered, { scheme: CHERRY_MEDIA_SCHEME }) }与 services/protocol/ProtocolService 不是一回事仓库里有两个名字里带 protocol 的模块但毫无重叠维度services/protocol/ProtocolService本模块mediaProtocol方向外部 → 应用OS 把 URL 交给应用进程内渲染进程向主进程要字节Schemecherrystudio://深链cherry-media://Electron APIapp.setAsDefaultProtocolClientopen-url/second-instanceprotocol.registerSchemesAsPrivilegedprotocol.handle注册时机app ready 之后注册没问题特权必须在 app ready之前声明状态等待就绪渲染进程的待处理 URL等待渲染进程请求的二进制条目两者只共享 “protocol” 这个词。把它们合并等于把一个要求 preboot 时序的 Chromium scheme 注册塞进一个无法表达该相位的服务里架构上是错误的。源码注释也明确警告 “NOT to be merged into services/protocol/ProtocolService”见 MediaProtocolService.ts。API 与使用示例模块对外暴露的 API 非常克制方法契约store(kind, data, mimeType)存入 Buffer返回其 id生命周期由调用方负责remove(kind, id)删除条目返回该条目原先是否存在has(kind, id)该条目是否仍在存储中getUrl(kind, id)渲染进程加载该条目所用的 URLREADME 中的标准用法如下可直接照搬到业务代码import { MediaKind } from main/services/mediaProtocol const media application.get(MediaProtocolService) const id media.store(MediaKind.Image, pngBuffer, image/png) const url media.getUrl(MediaKind.Image, id) // cherry-media://image/uuid // ... 当拥有它的会话结束时 media.remove(MediaKind.Image, id)请求处理与错误语义源码级MediaProtocolService.ts 中的handleRequest完整呈现了分发逻辑private handleRequest(request: Request): Response { const url new URL(request.url) const kind url.hostname if (!MEDIA_KINDS.has(kind)) { return new Response(Unknown media kind, { status: 400 }) } // pathname 是 /{id} —— 去掉开头的斜杠 const id url.pathname.slice(1) if (!id) { return new Response(Missing media id, { status: 400 }) } const entry this.stores.get(kind as MediaKind)?.get(id) if (!entry) { return new Response(Not found, { status: 404 }) } return new Response(new Uint8Array(entry.data), { headers: { Content-Type: entry.mimeType } }) }错误语义一览情况状态码kind 不在MEDIA_KINDS中400URL 缺少 id400kind 合法但 id 不存在404命中200Content-Type为 store 时登记的 mimeTypebody 为原始字节store内部使用node:crypto的randomUUID()生成 id并按 kind 维护二级MapMediaKind, Mapstring, MediaEntry结构见 MediaProtocolService.ts。测试如何锁定契约MediaProtocolService.test.ts 通过vi.mock(electron)捕获protocol.handle注册的 handler 后直接以Request调用覆盖四条关键契约存取闭环store后访问getUrl返回cherry-media://image/idhandler 返回 200、正确 Content-Type 与字节remove后再访问返回 404未知 kind 拒绝请求cherry-media://video/image-id返回 400绝不 fallthrough缺 id 拒绝请求cherry-media://image/返回 400销毁即清空_doDestroy后任何捕获都返回 404 —— 进程关闭不能留下驻留截图。真实消费者截图浮层的完整链路ScreenshotOverlayService.ts 是当前唯一消费者其服务声明了DependsOn([WindowManager, MediaProtocolService, OcrInferenceService])保证MediaProtocolService先行初始化。完整调用链为store第 278 行对每块显示器捕获后立即mediaProtocol.store(MediaKind.Image, captureResult.buffer, image/png)并把返回的mediaId记入this.mediaIds与this.sessionCapturesgetUrl 下发第 300 行mediaProtocol.getUrl(MediaKind.Image, mediaId)生成 URL通过windowManager.open的initData.imageUrl传给截图窗口渲染进程——注意 initData 必须走open()路径才能推给回收复用的窗口渲染进程消费渲染进程fetch(imageUrl)得到 Blob经 object URL 绘制/读取remove第 727-730 行会话结束时遍历this.mediaIds逐一mediaProtocol.remove(MediaKind.Image, mediaId)。这一链路完整演示了 README 强调的纪律每次store()都配对remove()并且store必须发生在open()之前确保窗口创建时 initData 已携带媒体 id。如何扩展一种新的媒体类型基于上述设计新增媒体类型只需四步全程不触碰 preboot 时序在 types.ts 的MediaKind中追加条目如Video: videoMEDIA_KINDS会自动包含新值调用方按需store(kind, data, mimeType)渲染进程通过getUrl生成的cherry-media://newkind/id拉取数据会话结束记得remove若新类型确实需要 Range 流式播放再在 registerSchemes.ts 的privileges中按需补stream: true。整个模块的价值在于用一次不可逆的 preboot 特权声明换来任意多种内存媒体类型的进程内分发同时把内存安全的最终责任清晰地交给调用方——这正是 Cherry Studio 在“大字节跨进程传输”这一经典 Electron 难题上给出的工程答案。赞分享人工智能大模型AI 应用交互助手本地部署【免费下载链接】cherry-studio Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址https://gitcode.com/CherryHQ/cherry-studio点击查看免费下载相关推荐Cherry Studio 的 cherry-media:// 自定义协议向渲染进程提供内存二进制媒体的两阶段注册方案Cherry Studio 的 cherry media:// 自定义协议向渲染进程提供内存二进制媒体的两阶段注册方案 本文围绕 src/main/serviAI 应用大模型桌面应用本地部署RAGCherry Studio架构深度解析主进程与渲染进程如何构建高效AI桌面应用Cherry Studio架构深度解析主进程与渲染进程如何构建高效AI桌面应用 Cherry Studio是一款支持多个LLM提供商的桌面客户端采用Elec人工智能大模型AI 应用交互助手本地部署如何在不同操作系统上安装和配置pdftotextLinux、macOS、Windows全攻略如何在不同操作系统上安装和配置pdftotextLinux、macOS、Windows全攻略 pdftotext是一款简单高效的PDF文本提取工具能够帮助用NLP数据工程上一篇IsaacLab项目中RSL-RL蒸馏训练预加载教师模型的实现方法下一篇Parabolic项目视频下载格式异常问题解析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考