Cherry Studio API 设计解析:桌面端 RESTful 接口规范是如何落地的
Cherry Studio API 设计解析:桌面端 RESTful 接口规范是如何落地的
【免费下载链接】cherry-studio🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio
Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端。要讲清它的 API 设计与 RESTful 接口规范,得先认清一个约束:它不跑 HTTP 服务,"服务器"就是本机的 Electron 主进程。桌面客户端如何把知识库、文件、MCP(模型上下文协议,让 AI 调用外部工具的协议)这类本地能力安全地暴露给渲染进程,是它的接口架构要回答的核心问题。
桌面端接口架构:一次调用的完整链路
渲染进程只持有 UI,任何碰磁盘、进程、外部服务的请求都要跨进程边界进入主进程。
图中分工清晰:useChat() 发出消息,经 Electron IPC transport 进入主进程,由补全服务处理,结果流分块回推,最后由消息服务落 SQLite。
更关键的是,项目在同一套 IPC(进程间通信)传输上维护着两条独立的 API 通道:
| 维度 | DataApi | IpcApi |
|---|---|---|
| 范式 | REST,资源导向 | RPC,能力导向 |
| 寻址方式 | /knowledge-bases 加 HTTP 方法 | knowledge.search 式点分路由 |
| 副作用 | 禁止,仅 SQLite 读写 | 窗口、文件系统、进程、外部服务 |
| 错误形态 | HTTP 状态码 | 字符串错误码 |
DataApi 只碰 SQLite 里的持久化业务数据,形态保证未来能"换一台真远程服务器";IpcApi 绑定本地主进程,永不远程。两条通道的路由表分别在 shared/data/api/schemas/ 与 shared/ipc/schemas/,主进程侧实现在 src/main/ipc/ 与 src/main/data/。
接口契约三要素:路径、方法、状态码
DataApi 的 URL 规则写在 schema 文件里:集合用复数名词加 kebab-case,层级用嵌套,CRUD 路径不带动词。/knowledge-bases/:id/items 是标准形态,POST /getTopics 不允许存在;路径参数本身含 / 时用贪婪参数 :id*,整张路由表仍受类型检查。
方法与默认状态码由服务端推断,handler 不用写:
| 方法 | 幂等 | 默认状态码 | 语义边界 |
|---|---|---|---|
| GET | 是 | 200 | 纯读,可重试可缓存 |
| POST | 否 | 201 | 创建;接受异步任务时返回 202 |
| PUT | 是 | 200 | 整体替换 |
| PATCH | 是 | 200 | 部分更新;带动效的操作应走专属端点 |
| DELETE | 是 | 204(无体) | 返回数据时降为 200 |
错误处理是"码 + 状态"两层契约:每个错误码固定一个 HTTP 状态,能否自动重试一次声明:
| 错误码 | HTTP 状态 | 自动重试 |
|---|---|---|
| NOT_FOUND | 404 | 否 |
| VALIDATION_ERROR | 422 | 否 |
| CONCURRENT_MODIFICATION | 409 | 否 |
| RESOURCE_LOCKED | 423 | 是 |
| SERVICE_UNAVAILABLE / TIMEOUT | 503 / 504 | 是 |
可重试错误只在白名单内,渲染层按 isRetryable 标志决定是否重试,而不是猜状态码。
核心接口拆解:五组场景怎么跑
应用管理:链路自检的入口
app.get_info 一次返回版本、打包状态与 logsPath 等真实目录路径,配合 app.cache_cleanup.inspect/run 两段式清缓存、app.relaunch 重启。这组路由不碰数据库,是接入前的链路自检探针。
知识库:读写分离是主线
场景是"建一次库、高频检索"。读走 DataApi:GET /knowledge-bases 支持 cursor + limit(上限 100),GET /knowledge-bases/:id/items 列文档,PATCH /knowledge-bases/:id/order 调排序;建库、检索、重建走 IpcApi,如 knowledge.search:
'knowledge.search': defineRoute({ input: z.strictObject({ baseId: z.string().trim().min(1), query: z.string().trim().min(1).max(1000) }), output: z.array(KnowledgeSearchResultSchema) })检索是运行时计算而非持久化读取,不进 DataApi——进了会被自动重试与缓存,毫无意义。
文件:只读 SQL 视图加独立 FS 通道
GET /files/entries 一族端点是纯 SQL、固定响应形状,游标分页,ref-counts 批量聚合一次上限 500 个 id。碰真实文件系统的操作(stat、绝对路径解析、写文件)全部走独立的 file IPC 通道——这个切分保证 DataApi 的重试与缓存不触发副作用。
MCP:删服务器为什么走 RPC
/mcp-servers 增改查走 DataApi:GET 支持 id/isActive/type 过滤,PATCH 整体重排顺序。删除刻意不上 DataApi——移除服务器要同时杀掉运行时 stdio 进程,这是副作用,所以走 mcp.server.remove,由主进程统一清进程、删行。工具调用进度经 mcp.tool.call_progress 事件推回渲染层。
记忆:三种机制各走各的通道
Cherry Studio 的记忆不是单一 API,而是三种并行机制:Agent 文件记忆(SOUL.md 等四个文件,按 Agent 隔离,Agent 用文件工具自主读写)、知识库(Assistant 与 Agent 共用)、MCP 记忆(内置 @cherry/memory 服务器,知识图谱形态)。接口层不发明"记忆协议",每种机制各走既有通道,没有新传输。
统一信封:请求、响应、错误共用一套形状
DataApi 的请求信封是扁平对象,桌面链路里的"头"由 id 与 metadata 承担:
export interface DataRequest { id: string // 请求 ID,响应回显 method: HttpMethod // GET | POST | PUT | PATCH | DELETE path: string // 如 '/knowledge-bases' params?: Record<string, any> body?: unknown headers?: Record<string, string> metadata?: { timestamp: number; spanContext?: unknown } }响应与之对称:id 回显请求 ID,status 是 HTTP 状态,data 与 error 取其一,metadata 带时间戳与缓存信息。错误是序列化结构而非抛出的堆栈——堆栈不过 IPC,靠主进程日志:
{ "code": "NOT_FOUND", "message": "Topic with id 'abc123' not found", "status": 404, "details": { "resource": "Topic", "id": "abc123" }, "requestContext": { "requestId": "req_1", "path": "/topics/abc123", "method": "GET" } }两处细节:VALIDATION_ERROR 的 details.fieldErrors 是"字段→消息数组"映射,前端可直接定位报错位置;IpcApi 则返回 { ok, data } / { ok: false, error } 而非 rejected Promise——Electron 的 invoke 在 reject 时只保留 message,code 与 details 会丢,所以这条通道永不 reject。
认证与安全防护:信任边界在进程,不在 Token
桌面应用没有用户体系,"认证"转译为"信任源校验"。IpcApi 单入口有两道门:来源校验——只信顶层 frame,webview 内容直接拒绝,生产环境 frame URL 必须是应用包内的 file: 路径,一份能摸到 ipcRenderer 的下载 HTML 过不去;输入校验——渲染到主进程的入参全部先经 zod parse,主到渲染的事件是纯类型、不做运行时校验,因为事件由主进程自己构造,校验换不来安全。
handler 看到的调用方身份只有 IpcContext.senderId 窗口 ID,由主进程从真实 sender 推导,禁止从 input 携带——否则渲染层可伪造窗口 ID 操作别的窗口。DTO 是第二道防线:写入体由实体 schema 白名单 .pick() 派生,z.strictObject 拒绝多余字段,mass assignment 在边界被挡。这里没有 Bearer Token 与 RBAC,权限模型退化为"通道收窄":路由表里声明的路由才存在,服务不能随手注册新通道。
稳定性工程:分页、版本策略与可观测
分页提供 offset(page + limit)与 cursor(cursor + limit)两种,知识库与文件列表都走 cursor——keyset 保证并发插入不错行不丢行。过滤用字段直名,排序用 sortBy + sortOrder,批量上限显式(ref-counts 500 个 id)。字段选择不靠 fields 参数,靠"固定形状 + 独立通道":派生数据各有专属端点。
版本不进 /v1 路径,进 schema 层:路由表是 TS 类型,编译器强制"schema 声明的每个路由必须有 handler",增删路由是可见的类型变更;错误码是稳定字符串契约,前端按 code 等值分支,不按 message 文本。
可观测复用请求信封:metadata.spanContext 携带 OpenTelemetry 上下文,AI 调用在 topic 的 traceId 下生成 span 树(chat.turn 为根),日志本地 JSONL 投影、不远程上报。跨窗口一致性走 contenteditable="false">【免费下载链接】cherry-studio🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考