feat(console): add initial console for Cursor BYOK with provider management and LLM call tracking

This commit is contained in:
leookun
2026-08-17 10:14:10 +08:00
parent 4db2061611
commit c96cb0a110
220 changed files with 29466 additions and 10769 deletions
+70 -153
View File
@@ -1,170 +1,87 @@
# Cursor Rust 服务端实施与验收计划
# cursor-byok
## 项目说明
`cursor-byok` 是一个基于真实 Cursor Agent 流量与 protobuf 实现的自托管服务端,用于把 Cursor 客户端接入用户指定的 LLM Provider。
`cursor-byok` 是一个兼容 Cursor Agent 客户端协议的自托管服务端项目,用于把 Cursor 客户端接入用户指定的 LLM Provider。项目根据真实客户端流量和提取出的 protobuf 协议实现,不依赖 Cursor 原服务保存对话状态。
当前 Rust 服务 `cursor-server` 已实现:
当前 Rust 服务 `cursor-server` 实现以下完整链路:
- Cursor `RunSSE + BidiAppend` 双向协议与 Connect envelope。
- OpenAI Chat、OpenAI Responses、Anthropic 三种无状态流式端点。
- `LLM → 客户端工具 → 结果提交 → 下一轮 LLM` 的通用 Loop。
- append-only canonical messages、不可变 revision 分支和同对话 Run 抢占。
- Cursor typed tool UI、Exec/Interaction、多阶段编辑、MCP 与子代理。
- Blob CAS、KV GET/SET ACK、两阶段 checkpoint 和 pending ToolRound 恢复。
- 未匹配 Cursor backend 路由原样流式转发到上游。
- React + Vite + TypeScript + Tailwind 管理台,支持 Provider 配置、模型发现和调用明细。
- 每次 Provider 调用的时间、模型快照、状态和 authoritative usage;详细模式保存脱敏请求与原始流响应。
- 通过 `RunSSE + BidiAppend` 组成的双向协议与 Cursor 客户端通信
- 将 OpenAI Chat、OpenAI Responses 和 Anthropic 的流式响应统一为内部 `ResponseEvent`
- 运行无状态 LLM Loop`LLM → 客户端工具执行 → 结果追加 → 下一轮 LLM`,直到 Turn 完成或被新 Run 打断。
- 以 append-only messages 作为上下文唯一事实源,保证相邻 LLM 请求的稳定前缀和可重复投射。
- 支持文本、thinking、tool start、参数增量、tool result、usage、model、rules、commands、skills、MCP 和 subagent 上下文。
- 使用 SQLite 持久化 messages、Run 状态、Blob CAS、引用边和 outbox。
- 使用不可变 Blob 对象图表达 Conversation、Turn、UserMessage 和 StepsBlobID 为原始内容的 `SHA-256`
- 在客户端确认 Blob 已存储后发布 checkpoint,并提供单工具粒度的历史回滚和未确认操作恢复。
启动方式见 [cursor-server/README.md](./cursor-server/README.md)。协议证据见 [Cursor上下文与状态同步抓包分析.md](./Cursor上下文与状态同步抓包分析.md),当前实现约束见 [一次性重构计划计划.md](./docs/一次性重构计划计划.md)
运行时职责划分如下:Cursor 客户端负责真正执行本地工具并保存服务端同步的 Blob;`cursor-server` 负责 Loop 决策、上下文投射、Provider 调用、状态持久化和 checkpoint 构造。Todo/Plan 等业务状态不单独维护,而是从 messages 确定性推导。
协议与状态模型的抓包结论见 [Cursor上下文与状态同步抓包分析.md](./Cursor上下文与状态同步抓包分析.md)。Rust 服务的启动方式和运行配置见 [cursor-server/README.md](./cursor-server/README.md)。
## 目录硬约束
下列目录、文件名和职责是实现验收条件,不是建议。首版只允许一个 `cursor-server` crate;代码必须落在对应文件,不得用 `core.rs``service.rs` 等总入口替代,也不得提前创建 MCP/subagent 空模块。新增文件必须说明为何现有职责无法容纳;删除、改名或移动下列文件必须先同步修改本计划。
## 核心数据流
```text
cursor-byok/
├── cursor-server/ # 新 Rust 服务
│ ├── Cargo.toml
│ ├── build.rs # 从 cursor-proto/proto 生成 prost 类型
│ ├── README.md # 启动方式、架构和核心不变量
│ │
│ ├── migrations/
│ │ └── 0001_initial.sql # Blob、messages、runs、outbox
│ │
│ ├── src/
│ │ ├── main.rs # 进程入口
│ │ ├── lib.rs # 模块出口
│ │ ├── app.rs # 依赖组装、启动和关闭
│ │ ├── config.rs # 地址、数据库、provider 配置
│ │ ├── error.rs # 服务统一错误
│ │ │
│ │ ├── model/ # 纯领域类型,不依赖 Cursor/provider
│ │ │ ├── mod.rs
│ │ │ ├── message.rs # CanonicalMessage、Role、Origin
│ │ │ ├── runtime_tag.rs # RuntimeEvent、exactly-once 约束
│ │ │ ├── conversation.rs # Conversation、Turn、revision
│ │ │ ├── tool.rs # ToolCall、ToolResult
│ │ │ └── usage.rs # provider usage 与 Turn usage
│ │ │
│ │ ├── run/ # Loop 引擎和一次 request 的状态机
│ │ │ ├── mod.rs
│ │ │ ├── registry.rs # request_id → RunHandle
│ │ │ ├── actor.rs # 每个 Run 一个 actor
│ │ │ ├── command.rs # run_request、exec/KV result、abort
│ │ │ ├── inbox.rs # append_seqno 排序、去重
│ │ │ ├── loop_engine.rs # LLM → Tool → LLM 主循环
│ │ │ └── lifecycle.rs # turn_ended/checkpoint/EndStream
│ │ │
│ │ ├── cursor/ # Cursor 协议适配器
│ │ │ ├── mod.rs
│ │ │ ├── proto.rs # include prost 生成代码
│ │ │ ├── connect.rs # 5-byte Connect envelope
│ │ │ ├── handlers.rs # Axum 路由入口
│ │ │ ├── bidi_append.rs # 上行 AgentClientMessage
│ │ │ ├── run_sse.rs # 下行 AgentServerMessage
│ │ │ ├── interaction.rs # 交互事件、Tool args 和 usage 投射
│ │ │ ├── exec.rs # Exec 上行/下行解析
│ │ │ ├── pending.rs # Exec/Interaction 的运行期 ID 关联
│ │ │ ├── tools.rs # 唯一工具路由、本地工具和 Cursor step index
│ │ │ ├── tool_result.rs # typed result、UI completion 和结果通道
│ │ │ ├── blob_sync.rs # KV GET/SET、ACK、重试
│ │ │ └── checkpoint.rs # Blob 图和 checkpoint 构造
│ │ │
│ │ ├── provider/ # LLM 端点适配器
│ │ │ ├── mod.rs # Provider trait
│ │ │ ├── event.rs # Canonical ResponseEvent
│ │ │ ├── openai_chat.rs # 第一条可运行链路
│ │ │ ├── openai_responses.rs
│ │ │ └── anthropic.rs
│ │ │
│ │ ├── prompting/ # 模型请求编译
│ │ │ ├── mod.rs
│ │ │ ├── assets.rs # 校验并嵌入根目录 prompt/
│ │ │ ├── compiler.rs # messages + mode + tools
│ │ │ ├── projector.rs # CanonicalMessage → provider 格式
│ │ │ └── derived_state.rs # 从 messages fold Todo/Plan
│ │ │
│ │ └── store/ # SQLite 持久化
│ │ ├── mod.rs
│ │ ├── sqlite.rs # pool、事务、PRAGMA
│ │ ├── messages.rs # append-only messages
│ │ ├── blobs.rs # CAS 与引用边
│ │ ├── conversations.rs # conversation head/revision
│ │ ├── runs.rs # 活动 Run 和恢复信息
│ │ └── outbox.rs # KV/checkpoint 待确认操作
│ │
│ └── tests/
│ ├── support/
│ │ ├── fake_provider.rs
│ │ ├── fake_cursor.rs
│ │ └── fixtures.rs
│ ├── text_turn.rs # 纯文本完整 Turn
│ ├── tool_loop.rs # LLM → Tool → LLM
│ ├── runtime_tag_once.rs # Runtime tag 不重复追加
│ ├── prefix_stability.rs # M(n) 是 M(n+1) 前缀
│ ├── checkpoint_recovery.rs # 单 Tool 回滚
│ ├── interrupt.rs # 新 Run 打断旧 Run
│ └── connect_wire.rs # Connect 二进制兼容性
├── cursor-proto/ # 现有 protobuf 提取和源文件
├── cursor-backend/ # 现有 Go 抓包调试器
├── prompt/ # 已复制的完整模式资产
└── docs/
HTTP / Connect
Cursor adapter
↓ ClientCommand / ClientEvent
RunEngine
canonical messages + selected revision
typed ModelRequest
Provider adapter → HTTP/SSE → ModelEvent
```
## 按文件实施顺序与通过条件
Loop 不依赖 Cursor protobuf、Blob、checkpoint、数字 wire id 或具体 Provider JSON。Cursor adapter 和 Provider adapter 只在各自边界做协议投射。
1. `Cargo.toml``build.rs``src/{main,lib,app,config,error}.rs`:服务能加载配置、迁移数据库、生成 Cursor protobuf 并启动/优雅关闭。
2. `src/model/*.rs``src/store/*.rs``migrations/0001_initial.sql`:实现纯领域消息、runtime tag、tool/usage,以及 append-only messages、Blob CAS/引用边、revision、run 恢复与 outbox`tests/runtime_tag_once.rs``tests/prefix_stability.rs` 必须通过。
3. `src/cursor/{proto,connect,handlers,bidi_append,run_sse}.rs`:实现抓包一致的 5-byte Connect envelope、二进制 RunSSE、BidiAppend 解码和 `append_seqno` 排序去重;`tests/connect_wire.rs` 必须通过。
4. `src/provider/{event,openai_chat,openai_responses,anthropic}.rs``src/prompting/*.rs`:三个端点统一为 canonical `ResponseEvent`;所有 mode 的 prompt/tool 资产可加载;messages 投射幂等且保持严格前缀。
5. `src/run/*.rs``src/cursor/{tools,interaction,exec,pending,tool_result}.rs`:一个 request 一个 RunActorLoop 只处理统一 `ToolCompletion`,工具名称到 Exec/Interaction/Local 的唯一映射只存在于 `cursor/tools.rs`;完成 toolstart 占位、参数增量、客户端执行、打断与 usage。每个完成的工具必须原子追加 `assistant(tool_call) → tool(result)`,整批完成前不得进入下一次 LLM。
6. `src/cursor/{blob_sync,checkpoint}.rs``src/store/{blobs,outbox}.rs`:构造不可变 Blob 对象图,KV SET 未 ACK 前不得发布引用它的 checkpointcheckpoint 达到单工具粒度,最终状态重复发布后再 EndStream;`tests/checkpoint_recovery.rs` 必须通过。
7. `tests/{text_turn,tool_loop,interrupt}.rs``tests/support/*.rs`:覆盖纯文本 Turn、完整工具循环、新 Run 打断旧 Run和恢复路径。最终验收命令固定为 `cargo fmt --check``cargo clippy --all-targets -- -D warnings``cargo test --all-targets`
## 状态与 checkpoint
- SQLite 中的 immutable messages 与 revision 父链是对话事实源;回滚只选择旧 revision 并建立新分支。
- ToolRound 保存完整 assistant、原始 call 顺序和真实 result 完成顺序;结果未齐时不会把悬空 tool call 投给下一轮模型。
- BlobID 是 `SHA-256(data)`;Blob 类型来自引用字段,不编码在 ID 中。
- checkpoint 引用的新 Blob 必须先收到对应 KV SET ACK。协议中不存在 checkpoint ACK,也不保存跨流 outbox。
- staged checkpoint 内联完整 pending assistantToolRound 全部结果提交后才折叠进 stable roots。抓包没有单 ToolResult checkpoint,因此实现也不制造该状态。
- settled checkpoint 必须先于下一轮 LLM 调用。最终文本轮严格发送 `turn_ended → staged → settled → settled 重发 → EndStream`
- Cursor.app 只把 `turn_ended` 前的 checkpoint 作为自动恢复候选;恢复 pending assistant 时先继续工具,不重复调用 LLM。
模块依赖方向固定为:
## 目录边界
HTTP/Connect
cursor adapter
run actor
model + prompting
provider / client tools
store + checkpoint
```text
cursor-server/src/
├── control/ # 客户端无关的 Provider、模型、调用观测 HTTP API
├── client/ # 所有客户端共用的最小 command/event port
├── model/ # canonical message、revision、ModelSpec、typed history
├── run/ # 协议无关 Loop、ModelCycle、ToolRound 和 RunRegistry
├── provider/ # 各 LLM 端点请求与流事件适配
├── cursor/
│ ├── request/ # RunRequest → PreparedRun/CursorRunContext
│ ├── prompting/ # Cursor prompt、工具 catalog 和 mode manifest
│ ├── projection/ # Cursor AI-SDK stable/pending JSON 编解码
│ ├── interaction/ # UI 更新、InteractionQuery、typed ToolCall 渲染
│ ├── tools/ # Cursor 工具 transport、runtime、dispatch 和 result
│ └── checkpoint/ # root/Turn/derived/recovery 与串行 worker
└── store/ # SQLite messages、revision、ToolRound、Run 和 Blob CAS
几个关键决定:
model/ 不引用 Cursor protobuf,也不引用具体 provider。
cursor/ 只负责协议转换,不能包含 Loop 业务决策。
provider/ 只把不同端点转换为统一 ResponseEvent。
prompting/derived_state.rs 只 fold messages,不持久化 Todo/Plan。
store/messages.rs 是上下文唯一事实源。
store/outbox.rs 保存尚未确认的 Blob/checkpoint 操作。
MCP 和 subagent 暂时不建空目录:MCP 先作为动态 Tool 接入;subagent 复用 RunActor,需求落地时再加入 run/subagent.rs。
依赖建议:
```
tokio 异步运行时
axum + hyper Connect HTTP 服务
prost + prost-build protobuf
protoc-bin-vendored 避免系统 protoc 依赖
sqlx/sqlite 持久化和事务
reqwest provider HTTP
eventsource-stream provider SSE
serde/serde_json 模型和工具 JSON
sha2 + base64 BlobID
bytes 二进制载荷
tokio-util CancellationToken
thiserror + tracing 错误和日志
include_dir 编译期嵌入 prompt/ 资产
console/ # React 管理台;只依赖 control API,不依赖 Cursor protobuf
```
Cursor上下文与状态同步抓包分析.md 来告诉你很多信息,你需要一次性读他
详细到文件的目标目录和验收项只在重构计划中维护,README 不复制第二份易漂移的完整文件清单。
Users/leokun/Library/Application Support/cursor-byok/cursor-proxy-debugger.db 是cursor的原服务抓包信息,内容由/Users/leokun/Documents/cursor-byok/cursor-backend产生
## 工程原则
- 不保留旧路径、兼容层或失败后的隐式 fallback。
- 同一状态只有一个所有者;协议层不做 Loop 决策。
- PromptSpec、ModelSpec 和 selected revision 决定可重放的 ModelRequestrequest id、时间和 model call id 不进入模型输入。
- 前缀稳定限定在相同 PromptSpec/ModelSpec/Provider route;新 Run 切换模型或模式时只替换 Cursor system root,其他历史 message roots 继续复用。
- Provider replay state 只回传给产生它的端点;可展示 thinking 不是跨端点 reasoning 字段。
- Provider usage 只采用端点报告的单轮最终值,不自行估算。
- 同一个取消信号覆盖等待 HTTP 响应头和读取 SSE 两段。
## 验证
```bash
cd cursor-server
cargo fmt --check
cargo clippy --all-targets -- -D warnings
cargo test --all-targets
```