1. 一次好奇心驱动的抓包:Cherry Studio 背后到底发生了什么
先说清楚我为什么要写这篇东西。Cherry Studio 在 AI 客户端圈子里热度一直不低,很多人把它当成日常 Chat 的主力工具。但我属于那种“用了东西就想拆开看看”的人,光知道它好用不够,我更想知道它每次发出消息时,底层到底走了哪些请求、带上了什么参数、服务端又是怎么回应的。所以我花了一个晚上,用 HTTP 抓包的方式把它完整地“解剖”了一遍。
这篇博文不是官方文档的复述,也不是源码级别的逐行解读,而是基于实际抓包数据的逆向观察和推理。我会把整个分析过程、抓包环境怎么搭、请求结构长什么样、鉴权是怎么做的、配置同步的机制、以及我在这个过程中踩过的坑,全部整理出来。如果你也在做同类客户端的开发,或者单纯对这类 AI 工具的网络实现原理感兴趣,这篇内容应该能帮你省下不少自己摸索的时间。
需要提前说明的是,抓包这件事本身并不复杂,复杂的是你怎么从一堆看似杂乱无章的 HTTPS 请求里提炼出规律。这篇文章的核心目的,就是带你走一遍完整的分析链路,让你以后面对任何“黑盒”客户端,都知道从哪里下手。有一个前提必须强调:整个抓包和分析过程针对的是你自己设备上、你自己账号名下的流量,而且仅用于技术学习,别拿这套方法去碰别人的数据,也不要去逆向破解付费功能或绕过服务端限制。我在本文中涉及的所有分析都止步于“理解原理”这个层面,不涉及任何绕过、篡改或非法获取他人数据的行为。
2. 抓包环境搭建:让 HTTPS 流量“透明化”的三个关键步骤
2.1 为什么直接抓包看不到东西
第一次尝试抓 Cherry Studio 流量的人,十有八九会碰壁。你用 Wireshark 或者 Fiddler 挂在那边,发现能抓到 TCP 连接,但应用层的数据全是密文,什么都读不出来。原因很简单:Cherry Studio 的 API 通信走的是 HTTPS,TLS 层把 HTTP 明文整个加密了。想看到里面的内容,就必须让客户端信任你的抓包代理证书,让流量经过代理时能被解密再转发。
这一步说起来一句话,做起来有一堆细节。我用的方案是 mitmproxy + Windows 系统证书导入,没有用 Fiddler,因为 mitmproxy 的脚本化过滤能力更适合我后续做请求归类。你完全可以用任意你顺手的抓包工具,核心思路是一致的。
2.2 三步让 HTTPS 流量可见
第一步,启动 mitmproxy,默认监听 8080 端口。第二步,把 Windows 的系统代理指向 127.0.0.1:8080,这一步 Cherry Studio 不需要额外配置,因为它默认走系统代理(绝大多数 Electron 应用都是这个行为)。第三步,访问 mitm.it 下载并安装 mitmproxy 的 CA 证书,安装时必须选择“受信任的根证书颁发机构”,而不是“个人”存储区,否则 Chrome 内核不认。
这里有一个最容易忽略的坑:Cherry Studio 是 Electron 应用,它的网络栈来自 Chromium。Chromium 在某些情况下不读 Windows 系统证书库,而是用自己的证书库。Electron 应用默认是读系统证书的,但如果打包时设置了--ignore-certificate-errors之类的开关,或者应用内内置了证书固定(Certificate Pinning),你导入系统证书也没用。我这次运气比较好,Cherry Studio 没有做证书固定,证书导入后流量立刻就能解密。如果你的目标应用做了证书固定,那就需要更进阶的手段了,本文不展开,也不建议碰那条线。
2.3 抓包环境的完整配置清单
| 项目 | 我的环境 | 说明 |
|---|---|---|
| 抓包工具 | mitmproxy 10.x | 支持脚本过滤和导出 |
| 系统代理 | 127.0.0.1:8080 | 手动设置,抓完后记得关 |
| 证书安装位置 | 受信任的根证书颁发机构 | 装错位置会导致解密失败 |
| 目标应用 | Cherry Studio 最新版 | Electron 架构 |
| 分析辅助 | Chrome DevTools 的 Network 面板 | 用于对比纯网页版的请求结构 |
顺着这个过程走完,你的 mitmproxy 界面上就会开始刷刷地出现一堆带 Host、路径和状态码的请求条目,这说明解密已经成功了。下一步才是真正的重头戏:从这些请求里找出 Cherry Studio 的核心通信链路。
3. 消息发送全链路拆解:从按下回车到流式回复落地的完整请求序列
3.1 一次对话涉及哪些请求
我先清空了 mitmproxy 的流量列表,然后在 Cherry Studio 里随便选中一个模型,发了一句“你好,请简单介绍一下自己”。按下回车之后,我盯着流量面板看了大概五秒钟,等流式输出结束,再把请求按时间排序,整个过程一共产生了 6 类关键请求。
| 序号 | 请求目标 | 方法 | 主要作用 |
|---|---|---|---|
| 1 | api.siliconflow.cn 或类似模型网关 | POST | 核心的对话补全请求 |
| 2 | 本地 localhost 端口 | POST | 把当前会话写入本地存储 |
| 3 | 本地 localhost 端口 | GET | 拉取会话列表或相关元数据 |
| 4 | 文件或对象存储域名 | GET | 如果消息里带了图片/附件,这里会拉取 |
| 5 | 遥测/统计域名 | POST | 上报使用数据 |
| 6 | 配置同步相关域名 | GET/POST | 拉取或同步云端配置 |
这里我要特别说明一下第 1 条。大家在 Cherry Studio 里配置模型的时候,填的都是模型提供商自己的 API 地址,比如你填的是某个兼容 OpenAI 格式的网关地址,那请求就会直接打到那个网关,Cherry Studio 本身不做什么中转。也就是说,Cherry Studio 更像是一个“前端调度器”,它把你的请求原样组装成 OpenAI 兼容格式,发到你配置的地址上。这一点决定了它和那种“套壳中转站”有本质区别,官方本身并不经手你的模型流量。
3.2 核心对话请求的报文解剖
下面是我抓到的核心 POST 请求的简化结构,Host 部分我已经打码,但路径和请求体结构保留得很完整:
POST /chat/completions HTTP/1.1 Host: [你配置的模型网关域名] Authorization: Bearer sk-xxxxxxxxxxxxx Content-Type: application/json User-Agent: CherryStudio/1.x (Windows; ...) Accept: text/event-stream { "model": "gpt-4o-mini", "messages": [ {"role": "system", "content": "..."}, {"role": "user", "content": "你好,请简单介绍一下自己"} ], "stream": true, "temperature": 0.7, "max_tokens": 2048 }有几个细节非常有价值。第一,请求头里的Authorization就是你自己的 API Key,Cherry Studio 不会替你换一个 Key,也不会做二次签名。也就是说,你配置的 Key 相当于直接暴露在本地网络流量里(TLS 解密后可见),如果这台机器上有恶意软件,Key 是能被读走的。所以建议在共享电脑上使用这类工具时要格外注意。
第二,Accept: text/event-stream是流式请求的标志。Cherry Studio 默认开启流式输出,服务端会按 SSE(Server-Sent Events)协议分片返回内容,每片是一个data:开头的 JSON,最后以data: [DONE]结束。客户端每收到一个 chunk,就会把里面的delta.content追加到当前消息上,这样你看到的回复就是逐字蹦出来的。
第三,messages数组里包含了你这次会话的完整上下文。也就是说,你每次发一条新消息,Cherry Studio 会把当前会话里的历史消息全部打包发给模型,而不是只发最新一句。这解释了为什么长对话会明显变慢、token 消耗会越来越大——它每次都带着全量上下文在跑。模型上下文窗口的限制也是这么来的,你聊到接近窗口上限时,要么手动清空上下文,要么依赖服务端的自动裁剪策略。
3.3 SSE 流式响应的实际抓包观察
服务端返回的响应体是这个样子的(我截取了中间几帧):
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]} data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"你好"},"finish_reason":null}]} data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":",我是"},"finish_reason":null}]} data: [DONE]Cherry Studio 拿到data: [DONE]之后才会把这条消息标记为完成状态,并触发本地数据库写入和 UI 更新。如果你抓包时发现请求发出去了但界面上一直没有字,大概率是 SSE 流解析出了问题,或者代理对 chunked 传输的缓冲策略导致数据没有即时推送。mitmproxy 默认会实时转发响应,但如果你的代理工具开了响应缓冲,就可能看到“请求完成但消息空白”的假象。
这里我额外做了一次对比实验:把stream改为false再发一次同样的请求。响应体变成了一个完整的 JSON,里面包含message.content和usage字段。usage里明确标了prompt_tokens、completion_tokens和total_tokens,这个数据就是你在 Cherry Studio 界面上看到的 token 计数的来源。所以界面上的 token 数字不是客户端自己算的,是服务端返回的。
4. 鉴权体系与配置结构:它是怎么管理那么多模型服务的
4.1 API Key 的去向与存储方式
Cherry Studio 支持同时配置多家模型服务,OpenAI、Anthropic、各家国内网关等都能加。这意味着它内部必须有一套统一的“服务商 + 模型 + 密钥”管理机制。通过抓包和翻本地存储,我发现这套机制的实现方式非常直观:
- 每个模型服务商对应一个“提供方”配置,包含
id、name、api_key、base_url。 - 当你在界面上切换模型时,请求的目标 Host 会同步切换成对应提供方的
base_url。 api_key不会随请求头之外的任何地方重复出现,也没有额外的签名逻辑,就是裸的 Bearer Token。
也就是说,Cherry Studio 本质上是一个“多服务商聚合客户端”,它做的事情就是把你的配置翻译成标准的 OpenAI 兼容请求。这个设计取舍很明显:它牺牲了一定程度的安全性(Key 在本地明文或可逆加密存储),换来了极高的兼容性和灵活性。任何服务商,只要暴露的是 OpenAI 兼容接口,就能被 Cherry Studio 接管。
4.2 配置同步的拉取链路
除了模型请求本身,我还观察到 Cherry Studio 在启动时会向自己的服务端发一些配置类请求。具体会拉取一些模型市场数据、provider 列表和更新信息。这些请求的响应体里包含的是模型 ID 的清单、显示名称、上下文长度等元数据。你不需要手动去配置每个模型支持多大的上下文,很多信息是直接从这些远端配置里拉下来的。
这部分流量里有一个值得留意的点:配置更新和模型流量是分离的。Cherry Studio 的服务端只负责下发“模型有哪些”这种元信息,不负责中转实际的对话内容。这种架构和很多商业 AI 客户端不一样,后者会把所有流量都汇到自家服务器,而 Cherry Studio 在模型调用上是“直连”的。
4.3 本地存储里能看到什么
顺着抓包发现的 localhost 请求,我去翻了 Cherry Studio 的本地数据目录,位置在用户目录下的AppData/Roaming/CherryStudio(Windows)。里面有一个 SQLite 数据库,消息记录、会话结构、模型配置都存这里。之前我提到 Host 为 localhost 的 POST 请求,就是应用在把消息写入这个 SQLite 库。
有一点需要提醒:SQLite 里的 API Key 并不是明文存储的,它经过了一层加密处理,不会像文本文件那样直接暴露。但我必须说清楚,这层加密更多是“防君子不防小人”,因为解密密钥就在本地,拿到数据库文件的人只要愿意花时间,还是有办法还原。所以你自己要有一个判断:工作电脑上不要存放重要的、有额度的生产环境 Key,最好用独立的小额 Key,或者定期轮换。
5. 埋点、遥测与后台任务:那些不显眼的流量里藏了什么
5.1 应用启动时的“悄悄话”
大部分用户不会注意到,Cherry Studio 启动之后,除了拉取配置,还会往若干遥测域名发送上报请求。这些请求里包含了应用版本号、操作系统信息、设备 ID、启动耗时、页面切换行为等数据。
| 上报类型 | 触发时机 | 主要字段 |
|---|---|---|
| 应用启动 | 每次冷启动 | app_version, os, device_id |
| 功能使用 | 切换模型、新建会话、发送消息 | event_name, session_id |
| 错误日志 | 请求失败、崩溃 | error_code, stack_trace |
| 性能数据 | 长耗时操作 | duration_ms, api_latency |
我知道有些人对遥测比较敏感,但平心而论,这个行业里几乎所有的客户端都会做类似的数据采集,重点区别在于采集的数据是否包含消息正文和 API Key。从我抓包的结果看,遥测请求体里没有出现对话内容,也没有 API Key,主要是事件名和设备信息。所以它在隐私层面属于“常规操作”范围,不算越界。
但如果你就是不想被采集,也有办法。Cherry Studio 的设置里应该有和“隐私”“数据上报”相关的选项,可以尝试关掉。关掉之后再次抓包,遥测请求的数量会明显减少。我实测是减少了绝大部分,但不敢保证完全为零,因为有些请求可能是配置更新的一部分,不好彻底分开。
5.2 自动更新与静态资源分发
另一个容易被忽略的流量是自动更新检查。Cherry Studio 会在启动时向更新服务器发一个版本检查请求,拿到最新的版本号和下载地址。这个下载地址通常指向 CDN,更新包体积不小。如果你在抓包时看到某个请求的响应特别大、而且 Content-Type 是application/zip或application/octet-stream,那基本就是更新包。
这部分的实现原理和其他 Electron 应用没有本质区别:本地版本号与服务端下发的版本号比较,不一致就提示更新。抓这个包的意义在于,你可以通过拦截这个请求看到服务端返回的最新版本信息,不需要去官网自己翻。
5.3 网络代理的判定逻辑
还有一个比较有意思的细节。Cherry Studio 在启动时会尝试探测当前网络是否“正常”。它是通过向自己的服务端发一个轻量请求来判断的,如果这个请求超时或返回异常,界面上可能就会提示网络错误。这个机制在抓包里表现为:启动初期有一个短时间内发出的探测请求,紧接着才是配置拉取。
这背后是一个非常重要的实现原则:客户端必须区分“网络不通”和“服务端不可用”这两种情况。Cherry Studio 把探测目标设为自己的服务端,而不是某个大厂的公共 DNS,这样一旦检测失败,它能确认是“自己依赖的服务不可达”,而不是笼统的网络问题。这个设计思路在你做自己的应用时也值得借鉴,探测点一定不要选无关的第三方,否则你分不清是第三方挂了还是自己的服务挂了。
6. 我踩过的坑和排查方法:给同样在做抓包分析的人提个醒
6.1 证书导入了但还是解密失败
我一开始在 mitmproxy 里能看到连接,但所有 HTTPS 请求都显示为“无法解密”。排查了半天,发现问题出在证书安装位置上。Windows 的证书导入向导默认是“当前用户”存储区,而且证书类型默认可能是“个人”。我把它改成“本地计算机” + “受信任的根证书颁发机构”之后,问题立刻消失。
这个坑很典型,因为很多图文教程里根本没强调证书存储区的位置。Electron 应用读取证书时走的是 Chromium 的网络栈,它对“个人”存储区的证书信任度很低,只有放在根证书颁发机构里才认。如果你也遇到类似问题,优先检查这一点。
6.2 系统代理和 TUN 模式的冲突
我的机器上还开着别的代理工具,它们一开就是 TUN 模式,直接在网络层接管所有流量,导致 mitmproxy 的 8080 端口根本收不到包,因为流量在更底层就被转发走了。后来我把这些工具的 TUN 模式关掉,只保留系统代理指向 mitmproxy,才恢复正常。
这类冲突的排查思路是:在 mitmproxy 里看有没有任何连接进来。如果一个连接都没有,说明流量根本没走到这一层,问题出在更底层的网络配置上,而不是证书或 TLS 上。
6.3 流式响应在代理下“卡住”的假象
前面提到,mitmproxy 默认对流式响应是即时转发的,但如果你用的是图形化的抓包工具,或者代理链路上有什么缓冲组件,SSE 流就可能被缓冲,导致你看到“请求已经返回 200,但界面上一个字都没有”。这个问题的本质是 SSE 依赖即时 flushing,任何中间层的缓冲行为都会破坏它的实时性。排查时优先看响应头里有没有Content-Type: text/event-stream,以及响应体是不是分片到达的。
6.4 如何快速归类大量请求
我整理了一个非常简单的请求归类思路,供你参考:
- 先按域名分组,模型的 Host 通常是你在配置里填写的那个,一眼就能认出来。
- 再按路径分组,
/chat/completions或/v1/messages这类路径基本就是核心对话接口。 - 最后按响应大小分组,响应体特别大的,要么是模型输出,要么是配置文件。
这个方法虽然朴素,但效率极高,能帮你在几十条请求里迅速定位到关键链路。
7. 从这次抓包里得到的启发:这类应用的设计套路是共通的
7.1 三层结构的复现
把 Cherry Studio 的网络实现拆开看,它其实就是一个非常标准的三层结构:
- 表现层:Electron 渲染进程,负责聊天 UI 的渲染和交互。
- 桥接层:主进程里的网络模块,负责组装请求、管理 Key、解析 SSE。
- 数据层:SQLite 本地库 + 远端配置服务,负责存消息和拉模型元数据。
这个结构不是 Cherry Studio 独有的,几乎所有同类的 AI 桌面客户端都是这么设计的。区别只在于桥接层的实现细节,比如 Key 的加密方式、请求超时的配置、错误处理的重试策略。如果你能完整理解 Cherry Studio 的这一套,以后看任何类似工具都会非常轻松。
7.2 对开发者最有价值的几点借鉴
第一,统一请求格式。Cherry Studio 把所有服务商的接口都归一化成 OpenAI 兼容格式,这个思路极大降低了客户端代码的复杂度。就算你只做一个私有的小工具,也建议把“上游差异”和“核心逻辑”隔离,不要让你业务代码里散落着各种服务商的特有分支。
第二,日志记录请求链路。抓包这种手段只能用于开发调试,真正落到产品里,你应该在客户端内部保留完善的请求日志,包括 URL、耗时、状态码、错误信息。Cherry Studio 在这方面的日志是可查的,这也是我能快速定位它行为的原因。
第三,配置与流量分离。它把“模型配置元数据”和“实际对话流量”完全拆开,配置走自己的服务端,对话直连模型服务商。这个模式的好处是,即便配置服务临时不可用,你已有的模型配置也不会受影响,对话仍然能正常发起。
7.3 安全层面的三个提醒
- 第一,任何桌面客户端里的 API Key 都不可能绝对安全,因为客户端拿到的所有东西,用户都能拿到。开发者要做的不是追求绝对不可破解,而是降低泄露后的影响面。
- 第二,如果你能通过抓包看到自己的流量,就要假设恶意软件也能看到。别在主力工作机上存放高权限的 Key。
- 第三,遥测上报是普遍存在的,它不一定是恶意的,但你作为用户有权了解并关闭它。
这次抓包分析下来,我对 Cherry Studio 的整体评价是:它做了一个聪明的架构取舍,把复杂度收敛在本地,模型远调用保持直连。这个思路让它的部署非常轻,用户完全掌控自己的 Key 和流量路径,同时灵活度又很高——任何兼容 OpenAI 格式的服务都能接进来。
回想整个分析过程,最有价值的不是搞清楚了 Cherry Studio 某一个具体接口,而是掌握了一套通用方法论:从 UI 操作反推请求触发点,从请求结构反推数据设计,从响应行为反推服务端逻辑。这套方法论可以复用到任何客户端工具上。下次你再遇到一个“黑盒”软件,不妨也搭一套抓包环境,从流量层面去理解它的运作方式——那种从混沌中理出规律的感觉,确实值得体验一次。