Files
cursor-byok/docs/架构文档.md
T
2026-08-13 22:01:11 +08:00

22 KiB
Raw Blame History

cursor-byok 当前架构文档

基线:当前工作树,而不是历史提交或产品宣传文档。

更新时间:2026-08-10

1. 结论先行

当前项目是一个单进程、本地优先的 Wails 桌面应用。Go 进程同时承担三类职责:

  1. 通过本机 App Host 向 Vue WebView 提供产品控制面;
  2. 启动一个 Cursor Runtime,包含 Cursor Host、MITM 代理和 Cursor IDE 系统设置注入;
  3. 把 Cursor 的本地协议请求转换为用户配置的 OpenAI/Anthropic 兼容模型请求。

当前主进程只注册一个 cursor-default Runtime。旧架构中的 Cursor 账号控制面、广告服务和广告资源路由已经从当前工作树移除,不应再作为现状组件绘制。cursor-tab-server 仍是独立的命令行程序,不由桌面进程创建。

2. 系统上下文

flowchart LR
    User["用户"] --> UI["Vue 3 WebView"]
    Cursor["Cursor IDE"] --> Proxy["本机 MITM Proxy"]
    UI -->|同源 Connect-Web| Host["App Host\n127.0.0.1:随机端口"]
    Proxy -->|四条白名单接口| CursorHost["Cursor Host\n本机 BackendListenAddr"]
    Proxy -->|非白名单请求原样回源| CursorCloud["Cursor 官方服务"]
    CursorHost -->|统一消息与流事件| Provider["Provider Router"]
    Provider -->|兼容 HTTP/SSE| ModelAPI["用户配置的模型 API"]
    Host --> AppService["AppService ConnectRPC"]
    AppService --> Runtime["Supervisor / Runtime"]
    AppService --> Test["Model Test Manager"]
    AppService --> Metrics["Usage JSON 读取"]
    AppService --> Desktop["Wails Desktop Controller"]

2.1 进程内边界

main.go
  └─ startup.Run(组合根)
       ├─ platform/desktop       Wails 窗口、托盘和系统动作
       ├─ backend/app             产品控制面、快照和 Watch
       ├─ backend/cursor          Cursor Runtime、协议循环和模型链路
       ├─ proxy                   MITM、证书和请求转发
       ├─ modeltest               模型列表与连通性测试
       ├─ historymetrics           usage.json 统计读取
       ├─ updater                 更新检查、下载和安装
       └─ platform/*              文件系统、系统代理、证书与平台适配

backend/app 只依赖产品级端口(配置、Runtime、模型测试、指标、更新、桌面动作);具体实现由 internal/startup 的适配器注入。Cursor 协议实现位于 backend/cursor,不能让前端或 AppService 直接依赖其 Protobuf/Provider DTO。

3. 组合根与启动顺序

internal/startup/bootstrap.go 是唯一组合根,拥有长期资源并决定释放顺序。

sequenceDiagram
    autonumber
    participant M as main.go
    participant S as startup.Run
    participant FS as filesystem
    participant CR as Cursor Runtime
    participant AP as AppService
    participant H as App Host
    participant D as Wails Controller
    participant SV as Supervisor
    participant U as Updater

    M->>S: Run(嵌入的前端资源和图标)
    S->>FS: EnsureDataRoot()
    S->>S: 初始化统一 HTTP Transport 和 CA Manager
    S->>CR: newCursorRuntime()
    Note over CR: config.yaml + Config Manager\nagent-state.db + Runner + Provider Factory\nCursor Host + Proxy 工厂
    S->>SV: Register(cursor-default)
    S->>AP: NewService(配置/Runtime/模型/指标/更新/桌面端口)
    S->>H: NewHost(前端资源 + AppService Handler)
    S->>H: Start() 监听回环随机端口
    S->>D: Run(BootstrapURL, ModelConfigURL, 托盘动作)
    D-->>S: ApplicationStarted
    S->>U: Start()
    S->>SV: Start(cursor-default)
    SV->>CR: StartProxy()
    CR->>CR: 启动 Cursor Host
    CR->>CR: 健康检查,最长 15 秒
    CR->>CR: 创建/启动 MITM Proxy
    CR->>CR: 安装 CA 并写入 Cursor IDE 代理设置
    CR-->>SV: 返回代理 endpoint

    D-->>S: OnShutdown
    S->>U: Shutdown()
    S->>AP: Close(),取消产品 Watcher
    S->>SV: Shutdown(),逆序停止 Runtime
    SV->>CR: StopProxy()
    CR->>CR: 停止 Proxy、清理 IDE 设置、停止 Cursor Host
    S->>CR: Close(),关闭 Agent Module 和 SQLite
    S->>H: Stop()

启动过程失败时,Runtime 按已完成步骤逆序回滚。Supervisor 对同一个 Runtime 使用串行操作锁,重复 Start/Stop 不会并发交错。桌面事件循环退出后,Run 仍会再次调用幂等 shutdown,确保命令行异常退出和窗口退出都能释放资源。

4. 产品控制面

4.1 App Host

internal/backend/app/host.go 把静态前端和 AppService 放在同一个回环 HTTP 服务中:

  • 默认监听 127.0.0.1:0,实际端口由系统分配;
  • /bootstrap?token=... 只允许一次 GET,用一次性 token 换取会话 Cookie
  • Cookie 为 HttpOnlySameSite=Strict,后续请求必须携带;
  • 请求还要通过 Host Origin 校验,只允许空 Origin 或当前回环 Host
  • SPA 未命中静态文件时回退到 /
  • 当前只挂载 /app.v1.AppService/

4.2 AppService API 分组

定义文件为 internal/backend/app/proto/app_v1.proto,当前 API 可按职责分为:

分组 RPC
首屏与状态 BootstrapWatch
配置 LoadConfigSaveConfig
Runtime ListRuntimesGetRuntimeStartRuntimeStopRuntimeRestartRuntime
模型 TestModelAdapterGetModelAdapterTestResultsFetchModelAdapterModels
指标与应用信息 GetHomeMetricsGetAppInfo
桌面动作 OpenPathOpenExternalSetLocaleControlWindow
更新 CheckForUpdatesInstallReadyUpdate

Service 层只做请求校验、端口调用、领域错误到 Connect 错误的映射和 DTO 转换。配置、Runtime、测速和更新的并发/持久化责任分别留在对应实现中。

4.3 快照与 Watch

服务端 eventHub 为事件分配全局递增 revisionWatch 每次连接先发送完整 BootstrapResponse 快照,随后发送配置、Runtime、模型测试和更新事件。慢订阅者无法及时消费时会被断开,客户端通过重连重新获取快照,而不是在服务端保留无限事件队列。

sequenceDiagram
    participant V as Vue 状态层
    participant W as watchCore
    participant T as Connect Transport
    participant H as App Host
    participant A as AppService
    participant E as eventHub

    V->>W: subscribeAppEvents(listener)
    W->>T: Watch(afterRevision=0)
    T->>H: 同源二进制 Connect 请求
    H->>A: Watch()
    A-->>W: Snapshot(revision=N)
    W->>V: 应用完整快照
    A->>E: 配置/Runtime/测速/更新变化
    E-->>W: AppEvent(revision>N)
    W->>V: 应用增量事件
    Note over W: 断流后按 250ms~5s 指数退避重连;\nBigInt revision 去重,重连首包强制视为快照

前端另外保留 localStorage 作为启动缓存,但后端配置和 Runtime 快照才是运行时事实来源。当前 bootstrapAppState 仍会分别调用配置、测速、版本、Runtime 和指标 RPC;Watch 流用于持续同步变化。

5. 前端结构

frontend/src/
  main.js                 Vue、路由、i18n 和初始状态启动
  layouts/MainLayout.vue  桌面主布局
  views/Home.vue          服务状态、首页指标和更新入口
  views/Config.vue        通用应用配置
  views/ModelConfig.vue   模型渠道管理
  components/             模型编辑、测速卡片、指标卡和 UI 基础组件
  rpc/                    Connect-Web Transport、App Client、Watch 重连
  services/clientApi.js  页面语义到 RPC 的薄封装
  state/                  响应式状态、配置规范化、用户动作和派生视图
  i18n/                   zh-CN/en-US/ja-JP/ru-RU 运行时国际化

模型配置编辑器支持 OpenAI/Anthropic 类型、端点、密钥、模型 ID、推理/思考参数、额外 JSON 参数、自定义请求头、排序、复制、删除、批量测速和供应商模型列表拉取。保存前由前端校验,再通过 SaveConfig 完整提交;后端 configAdapter 只合并 AppService 定义的字段。

6. Runtime 与 MITM

6.1 Supervisor 状态

通用 Runtime 状态为 stopped → starting → running → stopping → stopped,失败进入 failed。当前注册项为:

id:           cursor-default
kind:         cursor
capabilities: agent, models, proxy

RuntimeDescriptor.Endpoint 对前端只暴露代理 endpoint。前端的 runtimeToLegacyState 把它投影为 backendRunning/proxyRunning/serviceRunning 等旧页面字段,因此 UI 看到的是兼容视图,而不是 Cursor Runtime 的全部内部状态。

6.2 Cursor Runtime 内部步骤

stateDiagram-v2
    [*] --> stopped
    stopped --> starting: Supervisor.Start
    starting --> host_ready: Cursor Host Start + HealthCheck
    host_ready --> proxy_ready: 创建并启动 MITM Proxy
    proxy_ready --> running: CA/IDE 设置 Apply 成功
    starting --> failed: 配置、监听或健康检查失败
    host_ready --> failed: Proxy 创建/启动失败
    proxy_ready --> failed: IDE 设置失败
    running --> stopping: Supervisor.Stop
    stopping --> stopped: 清理 Proxy + IDE + Host 成功
    stopping --> failed: 清理失败

Host 使用配置中的 BackendListenAddrProxy 使用 ProxyListenAddr。代理目标固定为本机 Cursor Host;Runtime 明确拒绝通过外部接口绕过 Host 修改 baseURL。若 Proxy 监听地址发生变化,必须先停止运行中的服务,再创建新代理实例。

6.3 请求分流

internal/backend/cursor/routes/routes.go 是当前路由事实源。仅当请求同时满足以下条件时,MITM 才把请求转发到本机 Cursor Host

  • CONNECT 目标是 cursor.sh 或其子域名;
  • 请求方法是 POST
  • 路径属于四个白名单接口:
/aiserver.v1.AiService/AvailableModels
/aiserver.v1.AiService/GetUsableModels
/aiserver.v1.BidiService/BidiAppend
/agent.v1.AgentService/RunSSE

其余请求保持原请求回源,代理不读取 body、不改写 URL/headers。MITM 使用内置 CA 动态签发目标站点证书,并缓存按主机生成的证书;HTTP 客户端经过 netproxy.NewTransport,统一遵守环境变量和系统代理设置,同时绕过 localhost/127.0.0.1/::1。

7. Cursor Agent 执行链

sequenceDiagram
    autonumber
    participant C as Cursor IDE
    participant P as MITM Proxy
    participant H as Cursor Host
    participant D as CursorDialect
    participant R as Agent Runner
    participant L as loop 状态机
    participant DB as SQLite Store
    participant F as Provider Factory
    participant A as Provider Adapter
    participant API as 模型 API
    participant B as Broker

    C->>P: POST BidiAppend(request_id, append_seqno)
    P->>H: 转发本机白名单接口
    H->>D: DecodeBidi
    D-->>R: InputEnvelope(Start / Cancel)
    R->>DB: FindInputCommit + Load(conversation)
    R->>L: TransitionState(Start)
    L-->>R: CommandCallLLM + 新消息历史
    R->>DB: CommitTransition(CAS + planned llm_call)
    R->>F: ForModel(model, conversation, request)
    F-->>R: 统一 LLM 客户端
    R->>A: Call(RequestMessages)
    A->>API: OpenAI/Anthropic 流式请求
    API-->>A: SSE/chunk
    A-->>R: ResponseEvent(当前桥接主要是文本/usage/Done/Error
    R->>L: TransitionState(LLMEvent)
    R->>B: Publish(request_id, 编码前的事件)
    C->>P: POST RunSSE(request_id)
    P->>H: 转发本机白名单接口
    H->>B: Next(request_id, index)
    B-->>C: AgentServerMessage 流
    R->>DB: CommitTransition(最终助手消息 + llm_call)
    B-->>C: end=true

7.1 协议入口实际支持范围

  • BidiAppend 解码 AgentClientMessage;当前 Dialect 接受用户消息启动回合和取消动作,但取消消息没有携带 ConversationID,进入 Runner 后会落到缺少会话标识的错误路径。
  • RunSSErequest_id 从 Broker 顺序消费,直到结束事件或错误。
  • AvailableModels/GetUsableModels 从配置渠道生成 Cursor 需要的模型目录。
  • Runner 对同一 conversationID 加互斥锁;输入按 conversationID + inputID 去重。
  • 状态提交使用 SQLite version CAS,模型调用的 planned/final 事实与会话提交放在同一事务边界。

7.2 当前工具链边界

当前代码已经定义了工具、thinking、图片、usage 和供应商 tool-call 事件的数据模型,OpenAI/Anthropic 适配器也包含 thinking/工具调用流解析与参数累积逻辑。但桌面主链路仍有两个明确限制:

  1. internal/backend/cursor/provider/provider.go 创建 StreamRequest 时把 Tools 固定为 nil,并且只把文本、思考内容和工具结果文本投影到 Provider Message,助手 ToolCall 不会进入上游请求;
  2. Provider bridge 当前只消费 ModelEventKindTextDeltaModelEventKindTurnFinished,没有把 thinking/tool 事件转换成 ResponseEventCursorDialect 虽定义了对应编码分支,实际主链路拿不到这些事件,Runner.runCommand 也没有执行 CommandCallClient

因此当前“有效运行闭环”是用户文本生成、usage、完成和错误收口;thinking、工具调用和取消属于已建模或已接入口但尚未形成可靠端到端闭环的扩展面,不能在架构图中标成已完成能力。

8. 模型 Provider 层

Runner
  → provider.Factory.ForModel
  → llm/adapter.Router
  → store/config.Manager.SelectChannelForModel
  → OpenAIAdapter 或 AnthropicAdapter
  → 用户配置的 BaseURL + API Key

Router 根据 Cursor 请求中的模型 ID 选择渠道,并注入:

  • provider 类型、BaseURL、API Key、真实上游模型 ID
  • OpenAI Responses/Chat Completions 端点和推理强度;
  • Anthropic thinking/max_tokens/额外参数;
  • 自定义请求头、provider 流空闲超时、上下文窗口和输出限制。

适配器负责请求体构造、SSE 解码、thinking 标签/签名处理、工具调用参数增量、usage 归一化、重试和空闲超时。modeltest.Manager 是独立的测试通道,不复用 Agent Runner 的会话状态。

9. 配置、存储和数据流

9.1 配置边界

配置文件为 ~/.cursor-local-assistant-v2/config.yamlstore/config.Manager 负责规范化、原子快照、保存通知和热加载;startup/config_adapter.go 把内部配置投影为 AppService 的 UserConfig

AppService 公开的配置包括日志开关、provider 流空闲超时、模型渠道和首页缓存命中率口径。监听地址、运行时私有字段由 Cursor 配置管理器保留;前端仍有少量旧字段缓存,但它们不属于当前 AppService Protobuf 合同。

9.2 SQLite 事实模型

数据库路径为 ~/.cursor-local-assistant-v2/history/agent-state.db,当前迁移创建:

作用
conversations 版本化会话状态、消息历史和等待客户端信息
input_commits 输入幂等提交和可重放结果
llm_calls 精确请求、请求哈希、消息哈希和调用状态
client_operations 工具客户端操作事实模型(当前执行链尚未完整使用)
stream_diagnostics 可选流诊断事件,不回写模型历史

模型流期间的 delta 保存在内存中的 PendingResponse 和 Broker;只有回合收口后的助手消息才追加到 ConversationState.Messages。这是保持历史 append-only 和 provider prompt cache 稳定性的关键约束。

9.3 文件与平台资源

路径 内容
config.yaml 产品/模型配置
history/agent-state.db Agent 会话和调用事实
history/usage.json 首页用量摘要
logs/app-YYYY-MM-DD.log 按本地日期切换的结构化日志文件,权限 0600
data/ca.crt 注入系统和 Cursor 的 CA 文件
Cursor state.vscdb Runtime 启动时同步本地模拟用户信息并关闭绕过代理的实验开关

10. 桌面外壳、更新与观测

Wails Controller 只负责窗口、托盘、外部浏览器、白名单目录和语言同步,不承载模型业务。主窗口默认加载 App Host 的 bootstrap URL;模型配置窗口加载同一 Host 的 /model-config 路由。

更新管理器在 Wails 应用启动后开始后台检查,状态通过 AppService Watch 的 UpdateChanged 事件到达前端;安装动作委托平台 Installer,完成后通过桌面控制器退出应用。

日志层当前使用 slog + charm.land/log/v2:控制台输出彩色信息日志,文件输出 logfmt;标准库 log 被转接到统一门面。代理连接/TLS/转发错误带有按错误特征的时间窗口限流,避免大量重复错误淹没日志。

11. 生命周期和失败处理

stateDiagram-v2
    [*] --> stopped
    stopped --> starting: StartRuntime
    starting --> running: Host ready + Proxy ready + IDE Apply
    starting --> failed: 任一步失败
    running --> stopping: StopRuntime / App shutdown
    stopping --> stopped: Proxy stop + IDE Clear + Host stop
    stopping --> failed: 清理失败
    failed --> starting: 重试启动
    failed --> stopped: 后续停止完成清理

启动预算和回滚策略:

  • Cursor Host 单次健康检查超时 1 秒,整体等待预算 15 秒;
  • Runtime 启动失败时停止已经启动的 Proxy 和 Host;
  • 停止顺序为 Proxy → 清理 Cursor IDE/system proxy → Cursor Host
  • 进程退出时再关闭 Transport Module、SQLite、Supervisor、Updater 和 App Host
  • Provider 流、Agent 任务和 Broker 在 Module.Close 时通过运行域 context 统一取消。

12. 当前工作树中必须关注的事实与风险

已确认的现状

  • 当前主进程只有 AppService 控制面;账号、广告相关 Go 包、Proto、前端组件和 RPC 客户端均已删除。
  • 当前 Runtime 注册表仍为可扩展的多 Runtime 抽象,但实际只注册 Cursor 一个实例。
  • Provider 适配器覆盖 OpenAI Responses/Chat Completions 和 Anthropic Messages,支持 thinking、usage、部分工具事件解析。
  • Agent Dialect/Runner 仍是受限 MVP:主要闭环为用户消息 → 模型流 → 文本下行 → usage/完成或错误;thinking、工具结果和取消仍有桥接缺口。
  • 首页指标适配器只读取 history/usage.json;当前仓库没有对应写入器,新安装环境会得到空指标,除非该文件由外部或尚未合入的链路生成。
  • cursor-tab-server 是独立 Go module,使用固定 Cursor Tab 上游路径和 YAML token,不共享桌面进程的 Host、Cookie 或 Runtime。

当前验证阻塞

本次分析执行了后端相关测试,但当前工作树的 go.mod 已移除 charm.land/log/v2 直接依赖,而 internal/logger/logger.go 仍导入该包,因此 go test 在编译 cursor/proxy/startup 相关包时失败。该依赖不一致属于工作树现状,文档没有擅自修改。

13. 演进建议

  1. 先打通 Provider → Runner → Dialect 的工具闭环:传递 Tools、保留助手 ToolCall、发布 Cursor 工具事件、接受工具结果,再驱动下一轮 CommandCallLLM
  2. AppService 错误边界引入稳定领域错误类型,减少当前基于错误文本的 Connect code 判断。
  3. 将前端旧的监听地址缓存字段从状态合同中清理,避免用户误以为可以通过产品配置修改 Runtime 拓扑。
  4. 为 Runtime、Proxy、Runner、Provider 调用统一注入 request/conversation/call trace ID,打通结构化日志、SQLite 和性能测试结果。
  5. 修复依赖锁定后再执行 go test ./...、前端 RPC 测试和跨平台构建;协议生成任务继续以 build/Taskfile.yml 为唯一入口。

14. 关键源码索引

主题 入口
进程入口 main.go
组合根 internal/startup/bootstrap.go
Runtime 管理 internal/startup/supervisor.go
AppService 合同 internal/backend/app/proto/app_v1.proto
AppService 实现 internal/backend/app/service.go
App Host 认证与 SPA internal/backend/app/host.go
App 事件与快照 internal/backend/app/events.gointernal/backend/app/snapshot.go
Cursor Host internal/backend/cursor/host.go
Cursor Runtime 生命周期 internal/backend/cursor/runtime_lifecycle.go
MITM 分流 internal/proxy/router.gointernal/proxy/passthrough.go
路由事实源 internal/backend/cursor/routes/routes.go
Agent 传输 internal/backend/cursor/transport/handler.gointernal/backend/cursor/transport/cursor_dialect.go
Agent 状态机 internal/backend/cursor/loop/transition.go
Agent 协调器 internal/backend/cursor/agentrun/runner.go
Provider 路由 internal/backend/cursor/llm/adapter/router.go
Provider 桥接 internal/backend/cursor/provider/provider.go
SQLite 持久化 internal/backend/cursor/store/sqlite.gointernal/backend/cursor/store/conversations.go
前端 RPC 与重连 frontend/src/rpc/watchCore.jsfrontend/src/services/clientApi.js
前端状态 frontend/src/state/appState.jsfrontend/src/state/appActions.js
日志与按日文件 internal/logger/logger.gointernal/logger/daily_file.go
构建与协议生成 Taskfile.ymlbuild/Taskfile.yml