Files
cursor-byok/apps/docs/content/blog/2026-08-28-why-we-rebuilt.mdx
T
leookun 217f984112 feat(docs): enhance documentation and UI consistency
- Updated the index.html to dynamically set the theme color based on user selection, improving user experience during theme switching.
- Modified proxy.ts to include 'icon.png' in the matcher for better asset handling.
- Added a new icon.png file for enhanced branding.
- Improved metadata in layout.tsx and page.tsx files for better clarity and consistency in documentation.
- Updated various documentation files to reflect the new branding of 'Cursor Byok' and ensure consistent terminology throughout.
- Removed outdated blog entries and added new content to better align with the current product direction.
2026-08-28 01:33:51 +08:00

118 lines
12 KiB
Plaintext

---
title: 为什么我们重构代码?
description: 从 v0.0.49 的 Go 单体到 Rust 重写——harness、协议层、Agent 循环引擎、架构不变量与生态的深度解析。
---
在 [v1.0 Roadmap](https://github.com/leookun/cursor-byok/discussions/32) 里我们写过这样一段话:
> 不得不承认 vibe coding 带来了巨大的优势,但也带来了沉重的负担。现在我认为除了代码之外的一切内容都十分清晰,所以决定在近期重构代码。
产品方向、用户需要什么、协议怎么工作——这些都清楚,唯独代码本身成了继续前进的最大阻力。这篇文章把这次重构讲透:旧版到底哪里不行,新架构的每一层是怎么设计的,以及哪些不变量在支撑它。
## v0.0.49:一个走到产品上限的单体
重构前的版本(保留在 [archive/v0.0.49](https://github.com/leookun/cursor-byok/tree/archive/v0.0.49) 分支)是一个 Go 单体加 webview 前端:`internal/` 下 17 个包,`mitm`、`certs`、`cursoraccount`、`modelchannel`、`backend`、`bridge`……产品模式是**整体替换**——服务启动后接管 Cursor 的全部后端流量,用内置生成的 fake 账户顶替登录态。
这个前提决定了一切:需要「启动/停止服务」的开关,需要维护假账户体系,官方的插件、代码库索引在接管状态下全部失效。用户在「官方世界」和「本地世界」之间二选一。修补任何一处都动摇不了这个前提——想让官方模型和自己的模型自由混用,必须换前提。
同时,快速迭代长出来的代码没有独立的协议层:Cursor 客户端每次更新(新任务模式、multitask、checkpoint),都要在业务逻辑里到处开洞。模块边界靠约定、状态散落各处、回归靠人肉。这就是「沉重的负担」的具体含义。
## 重构后的架构
新版是一个 Rust workspace,边界即目录:
```text
cursor-byok
├── server/src/
│ ├── harness/ # 本地 CA 与 MITM 代理:流量的第一级路由
│ ├── cursor/ # Cursor 协议层:会话 actor、投影、提示词编译、工具调度
│ ├── run/ # Agent 循环引擎:模型周期 × 工具轮次
│ ├── provider/ # 模型适配:三种协议归一化为统一事件流
│ ├── store/ # SQLite:append-only 修订 DAG + 内容寻址存储
│ └── web/ # 网页检索能力
├── crates/
│ ├── semble-core/ # 代码索引与混合检索
│ └── semble-mcp/ # 以 MCP 服务形态暴露
└── apps/desktop/ # Tauri + React 桌面端
```
下面按请求的生命周期,逐层拆开。
## Harness:「并存」是一个两级路由决策
新版只接管一件事——模型调用。这句话在代码里是一个精确的两级决策:
**第一级在 MITM 代理层。** 本地 CA(3072 位 RSA 根证书,只存在于你的机器)让代理能解析 Cursor 发出的 HTTPS。但只有 `*.cursor.sh` 的流量会被 TLS 解析(`is_cursor_host`),其余一律原样隧道。解析后再看路径:只有一个固定白名单——Agent 运行(`AgentService/RunSSE`、`BidiService/BidiAppend`)、模型目录、账号信息展示等——会被改写到本地服务;登录、插件市场、代码库索引等其余请求原封不动发往官方服务器。Tab 补全按用户设置:选「直连」就直通官方,选公益或自建才路由出去。
**第二级在本地服务层。** 即使请求进了本地,服务端解码首个 `AgentClientMessage` 后按 `model_id` 查本地模型配置:查到,本地处理(BYOK);查不到——说明用户选的是官方模型——整个请求缓冲后转发回 `api2.cursor.sh`。Cursor 的运行由一对请求组成(上行 BidiAppend + 下行 RunSSE 流),两者必须走同一条路,所以有一个 `wait_route` 同步点:SSE 流会阻塞等待路由分类(`Local` 或 `Upstream`)后再跟随。
fake 账户、启停开关就是这样消失的:官方模型选 Auto 或官方型号照常走官方计费,选你配置的模型才进本地——同一个登录态,同一个客户端,逐请求分流。
## 协议层:把 Cursor 协议关进一个模块
`server/src/cursor/` 的职责是把 Cursor 的私有协议翻译成引擎能理解的中立事件,客户端更新时只有这一层需要动。几个值得展开的设计:
**每个请求一个 actor。** 每个 Cursor 运行请求对应一个 `CursorActor`,持有命令信箱、取消令牌和一个可重放的输出中枢(`OutputHub`)——SSE 订阅者迟到时,缓冲的帧会全部重放,客户端断线重连不丢内容。上行消息经过一个按序号重排的 `OrderedInbox`:乱序到达缓冲,重复到达丢弃,协议层天然容忍网络的不确定性。
**投影(projection)是协议层的核心。** 服务端内部只有一种「规范消息」(`CanonicalMessage`),发给 Cursor 的检查点、发给模型服务商的请求,都是从规范消息**投影**出来的两种视图。Cursor 侧的检查点还有一条硬性校验:稳定历史只能追加,如果重投影时发现某个已生成的根「变了」或「变少了」,直接报错——宁可失败也不静默破坏状态。
**提示词是编译出来的。** Agent、Ask、Plan、Debug、Multitask、Subagent、Compaction 七种模式,每种由 prompt 模板 + runtime 模板 + 工具清单构成,编译器负责占位符替换、按模型能力裁剪工具(不支持图片生成就移除对应工具)、以确定性顺序追加动态 MCP 工具。确定性不是风格偏好,后面讲缓存时会看到它是正确性要求。
**工具在客户端执行。** 模型发起的 shell、读写文件、搜索,服务端并不亲自执行,而是编码成指令下发给 Cursor 客户端,由客户端在用户环境里跑,结果再经 BidiAppend 回传。同路径的并发编辑有串行化调度,流式参数还会被投影成 UI 的实时预览。
## Loop 引擎:Agent 循环的最小内核
`server/src/run/` 是与 Cursor 协议无关的执行内核。一次运行就是一个循环:
```text
loop {
投影历史 → 调用模型(consume_model_cycle)
├── 无工具调用 → 追加最终回复,结束
└── 有工具调用 → 执行工具轮次(tool_round) → 提交结果 → 继续循环
}
```
**模型周期是严格的状态机。** `consume_model_cycle` 消费归一化事件流,校验协议不变量:必须先 Start、文本/思考/工具块必须成对开闭、`finish_reason` 与工具调用必须一致。取消发生时它会先干净地闭合未闭合的块再退出——否则 Cursor 端会把两次输出的增量拼在一起。
**提交屏障(commit barrier)。** 引擎每次提交状态(初始消息、工具结果落定、压缩、最终回复),都要等 Cursor 检查点工作线程确认写完才继续。检查点发布是引擎与协议层之间的硬同步点——引擎永远不会跑到已发布状态的前面。
**自动压缩是显式的前缀重置。** 引擎用最近一次真实用量校准 token 估算,当估算输入超过 `上下文窗口 − 10_000` 预留时触发压缩:保留**恰好一条**最新的 request-context 消息,其余历史交给模型摘要(上限 4096 token、关闭思考),然后以确定性顺序重建修订。压缩若被打断,退化为截取最近文本的兜底摘要,循环继续。
**取消与恢复。** 取消令牌从会话一路传到 provider 流;同一会话激活新运行会自动取消旧的。服务重启后,进行中的工具轮次能从 Cursor 检查点反解出来继续执行(`RunAction::Resume`)——这依赖投影是双向的。
## 不变量:前缀缓存稳定性
这是整次重构里最能体现「结构决定正确性」的部分。模型服务商的提示词缓存按前缀命中,收费差可达 10 倍。要吃到缓存,历史就必须是**只追加的日志**:上一轮发给模型的完整内容,必须是下一轮的字节级前缀。
约定守不住这件事,所以它被做进了每一层:
- **存储层**:会话历史是不可变的修订 DAG——每个修订是完整的有序消息列表,由 SHA-256 内容摘要标识;追加产生子修订,内容相同的追加会复用已有节点;唯一能「改写」的操作是压缩,而它是从根分支出新链。运行时事件有唯一标识,重试幂等,内容不同的复用直接报错。
- **投影层**:系统提示词与稳定工具前缀在输入不变时字节级稳定;规则/技能等请求上下文与当轮 runtime 消息分离,内容不变不重复追加,变了就在 runtime 消息前追加新的一条——从不改写旧的。上下文 A→B→A 会追加第三条 A,而不是复用第一条。
- **provider 层**:工具调用 ID 在发给服务商前统一归一化;思考块的签名(Anthropic 的 signature、OpenAI Responses 的加密推理项)被原样保存为回放状态,下一轮逐字回放。
新版 GPT 系列缓存命中率能稳定在高位,靠的不是运气,是这一整条链。仓库里甚至有一份专门的工程约束文档(`.agents/skills/cursor-prefix-stability`),任何触碰这条链的改动都要过它的检查单。
## Provider 层:三种协议,一种事件流
Anthropic Messages、OpenAI Chat Completions、OpenAI Responses 三个适配器,输出统一的归一化事件(`Start`、文本/思考/工具块的开闭与增量、`Usage`、`Done`)。引擎和协议层完全不知道上游是谁——这就是「其他模型无脑选 OpenAI Chat」在架构上成立的原因。
每次调用都有观测:请求头体(脱敏)、逐块流式响应(带缓冲与限额)、首字延迟、token 用量,详细模式下可完整回放。桌面端的调用详情页读的就是这些数据。
## 生态:重构不只是代码
**官方生态原样保留。** 并存设计最大的生态意义是「不减法」:插件、Skills、MCP、代码库索引、Tab 补全,登录自己的账号后全部照常。旧版做不到,是因为它把整个后端都换掉了。
**语义搜索是独立的、可度量的。** semble 作为独立 crate 实现代码索引与混合检索,以 MCP 服务形态接入。它有一套可复现的对比基准(React/Vue 真实提交、人工标注到实现行):自然语言查询 Recall@5 达 95%,字面与符号查询达 100%,并有公开的质量门槛文件——CI 里任何一次检索质量回退都会挡住合并。
**Tab 服务分层开放。** 公益服务(作者部署)、直连(自己账号的官方服务)、自建(`cursor-tab-server` 留在归档分支)三种模式,在系统设置里一键切换。
**工程体系也是生态。** 仓库里维护着一组领域约束文档(数据库 schema、前缀缓存稳定性、发布流程、i18n……),人和 AI 协作时共用同一套检查单;`make check` 一条命令跑完格式、静态检查、全量测试和前端类型检查。这个文档站本身——包括首页那个用真实桌面组件加 mock 数据构建的可交互 demo——也在同一个仓库里,与产品同步演进。
**方向由社区讨论驱动。** Roadmap 里的 Tab 支持、Gemini、图片生成、全部任务模式、代码库索引都已在重构后的版本里落地,更多 IDE 的支持在计划中。
## 值得吗
重构耗掉了数周本可以用来加功能的时间。换来的是:产品模式不再自我设限;协议更新有了固定的落点(只动 `cursor/`);正确性由类型、不变量和回归测试守护,而不是靠记忆;每个新能力都长在一个已经能工作的产品上。
「除了代码之外的一切都清晰」的问题,现在代码也清晰了。对这次重构有想法?欢迎到 [Roadmap 讨论区](https://github.com/leookun/cursor-byok/discussions/32) 聊聊。