mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-17 11:37:20 +08:00
v0.3.8
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
---
|
||||
name: cursor-client-e2e-debugging
|
||||
description: Use when debugging Cursor client agent/local-mode/tool/backend-store/provider-replay failures in this repo, especially after the state/context history-store refactor, when triaging installed app bundles read-only, correlating installed-client behavior with repo code, mapping a user-provided id to conversation/request/model-call evidence, replaying provider requests from debug logs, or locating the current client/backend/protocol/log files quickly.
|
||||
---
|
||||
|
||||
当用户反馈 Cursor agent、本地模式、工具调用、协议桥接、客户端 bundle 行为异常,或需要只读核对已安装客户端与仓库实现/日志差异时,使用此技能。
|
||||
|
||||
当用户只给一个 UUID / id,希望反查它是 `conversationId`、`requestId`、`modelCallId`、`toolCallId` 还是其它运行期 id,并继续定位对应的会话 history、provider 调用状态或协议日志时,也使用此技能。
|
||||
|
||||
当用户遇到 provider 400/参数错误、SSE `event: error`、需要从 `debug/provider.jsonl` 抽取最终 provider body 并用 curl 独立复现时,也使用此技能。
|
||||
|
||||
## 首要约束
|
||||
|
||||
- 不要修改已安装的 Cursor 客户端代码、bundle、签名或 app 副本。
|
||||
- 允许且推荐读取、搜索、比对和分析客户端 bundle、日志、协议事件与本仓库实现。
|
||||
- 如果本仓库存在 `.cursor-app-formatted/`,优先读取这个格式化快照来搜索和引用客户端 bundle;只有在快照缺失、过期或需要 hash/真实性核对时,才只读读取 `/Applications/Cursor.app`。
|
||||
- 如果用户要求提取、格式化、刷新或规范化 Cursor.app 快照流程,使用 `cursor-app-formatted` skill;调查时可以读格式化代码,但不要 patch 格式化快照里的 bundle 代码。
|
||||
- 如果用户要求 patch 客户端做 e2e,要改成只读证据采集与差异定位,不执行客户端修改。
|
||||
- 当前本仓库已经重构为 `state.json + context.json` history-store;不要沿用旧 `data.sqlite`、`conversation.json`、`turns/<n>/request.json|sse.jsonl|summary.json` 排查路径。
|
||||
|
||||
## 先做路由判断
|
||||
|
||||
- `history + logs` 反查层
|
||||
- 现象:用户发来一个 id,要判断它是 `conversationId`、`requestId`、`modelCallId`、`toolCallId`;需要从 `history/<conversationId>/state.json` 与 `history/<conversationId>/context.json` 追运行状态和语义历史。
|
||||
- 先读 [references/backend-store-log-tracing.md](references/backend-store-log-tracing.md)
|
||||
- `provider replay / debug` 层
|
||||
- 现象:provider 返回 400/参数错误、SSE `event: error`、需要验证最终出站 provider body 是否能被独立 curl 复现。
|
||||
- 先读 [references/provider-replay-debugging.md](references/provider-replay-debugging.md),必要时使用 [scripts/provider-replay.sh](scripts/provider-replay.sh)
|
||||
- `cursor-agent` 层
|
||||
- 现象:`CursorAgentProvider`、`ClaudeSDKClient`、`AnthropicProxy`、`registerAgentProvider`、`InteractionUpdate` 映射、模型桥接异常。
|
||||
- 先读 [references/file-map.md](references/file-map.md) 和 [references/search-patterns.md](references/search-patterns.md)
|
||||
- `cursor-always-local` / 本地模式协议层
|
||||
- 现象:`BidiAppend`、`RunSSE`、`AgentServerMessage`、`ExecClientMessage`、`InteractionResponse`、live checkpoint / pending 收口异常。
|
||||
- 先读 [references/file-map.md](references/file-map.md) 和 [references/search-patterns.md](references/search-patterns.md)
|
||||
- 客户端 bundle 只读定位层
|
||||
- 现象:需要核对已安装 app bundle、确认实际运行副本、只读验证行为是否命中,并判断差异来自客户端还是本仓库。
|
||||
- 先读 [references/installed-client-readonly-validation.md](references/installed-client-readonly-validation.md)
|
||||
|
||||
如果问题同时涉及多层,优先从最靠近故障表象的一层开始,不要一开始就同时追所有链路。
|
||||
|
||||
## 当前工作流
|
||||
|
||||
1. 如果用户给了一个 id,先用 `history/` 目录、`context.json.items`、`state.json` 和 `logs/app.log` 判断它属于哪类 id;不要假设它一定是 `requestId`。
|
||||
2. 一旦拿到 `conversationId`,同时看两份事实源:
|
||||
- `history/<conversationId>/state.json`:会话元数据和当前状态,例如 loop、token、current todo/plan、`latest_request_prefix`、`last_provider_call`。
|
||||
- `history/<conversationId>/context.json`:append-only 的语义历史 entries;prompt replay 由 `ProjectPromptReplay()` 从这里投影。
|
||||
3. 不要去找旧 provider 调用工件:当前 `RecordLLMRequest` 不再落 `request.json`,`AppendLLMResponseChunk` 是 no-op,`RecordLLMSummary` 只补齐内存态并更新 `state.latest_request_prefix` / usage。
|
||||
4. 再确认故障主要落在 `cursor-agent`、`cursor-always-local`,还是本仓库 `internal/backend` 的协议兼容层。
|
||||
5. 用 references 里的固定搜索词快速找到入口函数、协议消息和桥接点。
|
||||
6. 如果 provider 返回 400/参数错误、SSE `event: error`,或需要验证最终出站 provider body:
|
||||
- 先读 [references/provider-replay-debugging.md](references/provider-replay-debugging.md)。
|
||||
- 通过 id 反查拿到 `conversationId`、`requestId`、`modelCallId`,再定位 `history/<conversationId>/debug/provider.jsonl`。
|
||||
- 必要时运行 [scripts/provider-replay.sh](scripts/provider-replay.sh),只保存 replay 产物,不把 API key 或完整 request body 写进技能/回复。
|
||||
7. 如果用户要核对 prefix cache / cache hit:
|
||||
- 优先运行 `go run ./scripts/historymetrics [conversationId|path]`
|
||||
- 它读取当前 `history/<conversationId>/state.json` 与 `history/<conversationId>/context.json`,并结合 `history/usage.json` 统计。
|
||||
- 关注 `cache_read_tokens / prompt_tokens_total`,并检查 `context.json.items` 是否缺失、重复或顺序异常。
|
||||
8. 如果需要对照已安装 app 与仓库行为:
|
||||
- 只做只读核对与证据采集,不修改客户端 bundle / app 副本 / 签名。
|
||||
- 优先用 `.cursor-app-formatted/` 中的格式化副本定位符号、行号和控制流;再按需只读核对 `/Applications/Cursor.app` 原始文件 hash 或运行副本。
|
||||
- 先确认实际运行的 app 副本和目标 bundle 路径。
|
||||
- 再读取 bundle 内容、日志、端口与 history 状态,并与本仓库实现对照。
|
||||
9. 如果证据显示问题更像是客户端 bundle 行为差异:
|
||||
- 记录具体文件、符号、日志和协议证据链。
|
||||
- 继续判断本仓库是否可以兼容、绕过,或直接输出分析结论。
|
||||
- 不要对已安装 Cursor 客户端做 patch、重签名、替换文件或写入式验证。
|
||||
|
||||
## 约束
|
||||
|
||||
- 不要修改已安装的 Cursor 客户端代码、bundle、签名或 app 副本。
|
||||
- 不要默认复刻整套 Cursor backend;先确认是不是只需要改模型桥接层。
|
||||
- 不要先假设用户给的是 `requestId`;必须同时考虑 `conversationId`、`requestId`、`modelCallId`、`toolCallId`。
|
||||
- 不要把 `history/<conversationId>/state.json` 和 `history/<conversationId>/context.json` 混为一谈:前者是元数据与当前状态,后者是 replayable 语义历史。
|
||||
- 不要再依赖 `agent_request_runs`、`agent_conversations`、`agent_history_entries`、`protocol_traces`、`data.sqlite`;当前实现已经不支持 DB-backed store / trace debug UI。
|
||||
- 不要把当前排查进度、临时结论、一次性的 request_id / 端口 / token 写进技能。
|
||||
- 技能里只保留稳定流程、固定入口、可复用搜索词和只读验证规则。
|
||||
|
||||
## 模型渠道规则
|
||||
|
||||
- 模型渠道唯一性不再由 `modelID` 决定。
|
||||
- 当前规范化渠道 ID 是 `baseURL + modelID + apiKey + displayName + openAIEndpoint` 的短 `SHA-256` hash(前 16 个十六进制字符)。
|
||||
- resolver 仍兼容 legacy 渠道 ID:`baseURL + modelID + apiKey + displayName`。
|
||||
- `modelID` 只表示 provider model;排查选择器、默认模型和命中渠道时,要优先看渠道 ID 和 `openAIEndpoint`。
|
||||
|
||||
## 参考加载规则
|
||||
|
||||
- `history + logs` 路径、id 反查、state/context 生成链路:读 [references/backend-store-log-tracing.md](references/backend-store-log-tracing.md)
|
||||
- provider 400/参数错误、SSE `event: error`、最终出站 provider body curl 重放:读 [references/provider-replay-debugging.md](references/provider-replay-debugging.md)
|
||||
- 文件地图:读 [references/file-map.md](references/file-map.md)
|
||||
- 搜索词与判断树:读 [references/search-patterns.md](references/search-patterns.md)
|
||||
- 已安装客户端的只读核对、进程确认、行为验证:读 [references/installed-client-readonly-validation.md](references/installed-client-readonly-validation.md)
|
||||
|
||||
## 自带脚本
|
||||
|
||||
- 统计 prefix cache / cache hit:运行 `go run ./scripts/historymetrics [conversationId|path]`
|
||||
- 兼容壳脚本:运行 [scripts/cache-hit-rate.mjs](scripts/cache-hit-rate.mjs)
|
||||
- provider curl 重放:运行 [scripts/provider-replay.sh](scripts/provider-replay.sh),必填 `REQUEST_LOG`、`REQUEST_ID`、`MODEL_CALL_ID`
|
||||
@@ -0,0 +1,177 @@
|
||||
# backend/store 日志反查
|
||||
|
||||
当用户只给一个 id,或明确让你从本地记录里反查一次请求、会话、模型调用、工具调用或 provider 错误时,优先读这份参考。
|
||||
|
||||
## 当前固定路径
|
||||
|
||||
当前用户机器上的固定助手根目录:
|
||||
|
||||
- `~/.cursor-local-assistant-v2`
|
||||
|
||||
当前最关键的是三类内容:
|
||||
|
||||
- `history/<conversationId>/state.json`
|
||||
- 会话元数据与当前状态。
|
||||
- 重点字段:`request_id` `conversation_id`、`root_conversation_id`、`parent_conversation_id`、`parent_tool_call_id`、`mode`、`current_loop_status`、`current_request_id`、`current_turn_seq`、`context_version`、`next_turn_seq`、`next_entry_seq`、`latest_request_prefix`、`last_provider_call`、`current_todos`、`current_plans`、token / compaction 字段。
|
||||
- `history/<conversationId>/context.json`
|
||||
- append-only 语义历史。
|
||||
- 重点字段:`version`、`items[]`。
|
||||
- `items[]` 中每个 entry 通常有 `seq`、`turn_seq`、`request_id`、`role`、`kind`、`tool_call_id`、`parent_tool_call_id`、`payload`、`created_at`。
|
||||
- `history/usage.json`
|
||||
- provider call 与 turn usage 聚合。
|
||||
- 重点字段:`totals`、`daily`、`recent_events`、`event_index`。
|
||||
|
||||
`logs/app.log` 是运行日志;它只用于补充运行时证据,不是会话事实源。
|
||||
|
||||
当前实现不再支持:
|
||||
|
||||
- DB-backed store / searchable conversation memory
|
||||
- HTTP/protocol trace debug UI
|
||||
- `data/data.sqlite` / `protocol_traces`
|
||||
- `history/<conversationId>/conversation.json`
|
||||
- `history/<conversationId>/turns/<n>/request.json|sse.jsonl|summary.json`
|
||||
- 根目录或会话目录下的旧 `latest.json`、`summary.json`、`replay.json`、`runtime.json`、`request.json`、`recovery.json`、`entries.jsonl`、数字 turn 目录
|
||||
|
||||
这些旧产物会被 `internal/backend/forwarder/history_maintenance.go` 清理。不要把它们当成当前事实源。
|
||||
|
||||
## 用户发来一个 id 时的固定步骤
|
||||
|
||||
### 1. 先判断 id 类型
|
||||
|
||||
不要先假设它是 `requestId`。按这个顺序缩小范围:
|
||||
|
||||
1. 是否是 `conversationId`
|
||||
- 检查 `history/<id>/state.json` 和 `history/<id>/context.json` 是否存在。
|
||||
2. 是否是 `requestId`
|
||||
- 在 `history/*/state.json` 中查 `current_request_id`、`latest_request_prefix.request_id`、`last_provider_call.request_id`。
|
||||
- 在 `history/*/context.json` 的 `items[].request_id` 中查。
|
||||
- 在 `logs/app.log` 中查。
|
||||
3. 是否是 `modelCallId`
|
||||
- 在 `state.json` 中查 `latest_request_prefix.model_call_id`、`last_provider_call.model_call_id`。
|
||||
- 在 `context.json.items[].payload` 中查 `model_call_id`。
|
||||
- 在 `logs/app.log` 中查 `model_call_id=<id>`。
|
||||
4. 是否是 `toolCallId` / `exec_id`
|
||||
- 在 `context.json.items[].tool_call_id`、`items[].payload` 中查。
|
||||
- 在协议/工具相关日志中查。
|
||||
|
||||
可以用本地脚本或 `rg` 做只读反查。不要再用 SQLite 查询模板。
|
||||
|
||||
### 2. 拿到 `conversationId` 后看两份事实源
|
||||
|
||||
```bash
|
||||
HISTORY_ROOT="$HOME/.cursor-local-assistant-v2/history"
|
||||
CONV_ID="<conversation-id>"
|
||||
|
||||
ls -la "$HISTORY_ROOT/$CONV_ID"
|
||||
```
|
||||
|
||||
重点检查:
|
||||
|
||||
- `state.json`
|
||||
- `current_loop_status`:`idle`、`running`、`waiting_tool`、`completed`、`canceled`、`provider_error`、`failed`
|
||||
- `current_request_id`、`current_turn_seq`
|
||||
- `latest_request_prefix`:最近一次 provider 请求的 provider/model/openai_endpoint/model_call_id/prompt token 摘要
|
||||
- `last_provider_call`:最近 provider 状态与错误文本
|
||||
- `next_entry_seq`、`next_turn_seq`、`context_version`
|
||||
- `current_todos`、`current_plans`
|
||||
- `context.json`
|
||||
- `version` 是否与 `state.context_version` 对齐
|
||||
- `items[]` 是否按 `seq` 稳定递增
|
||||
- 同一 `turn_seq` 下是否有预期的 user/request_context/prompt_context/assistant/tool_result/metadata entries
|
||||
- 是否有重复、缺失或顺序异常
|
||||
- `usage.json`
|
||||
- 通过 `event_index` 或 `recent_events` 查 request/model-call 相关 usage
|
||||
- `totals.cache_read_tokens / (totals.cache_read_tokens + totals.input_tokens)` 可粗略看 cache hit
|
||||
|
||||
### 3. Provider 调用证据现在在哪里
|
||||
|
||||
当前 provider artifact recorder 的行为:
|
||||
|
||||
- `RecordLLMRequest(...)`
|
||||
- 只缓存当前 provider call 的请求摘要。
|
||||
- 如果 payload 可解析 provider/model/openai_endpoint,会更新 `state.latest_request_prefix`。
|
||||
- 不再写 `request.json`。
|
||||
- `AppendLLMResponseChunk(...)`
|
||||
- 当前是 no-op。
|
||||
- 不再写 `sse.jsonl`。
|
||||
- `RecordLLMSummary(...)`
|
||||
- 只补齐当前 provider call summary,并更新 `state.latest_request_prefix.prompt_tokens_total`。
|
||||
- usage 聚合写入 `history/usage.json`。
|
||||
- 不再写 `summary.json`。
|
||||
|
||||
所以 provider 错误排查应优先看:
|
||||
|
||||
- `state.last_provider_call`
|
||||
- `state.latest_request_prefix`
|
||||
- `context.json.items` 里的 `metadata/provider_error/turn_completed` 等 payload
|
||||
- `history/usage.json`
|
||||
- `logs/app.log`
|
||||
- `internal/backend/agent/model/openai.go` / `anthropic.go` 的请求构造和错误解析
|
||||
|
||||
## 这些文件是怎么生成的
|
||||
|
||||
### 根路径
|
||||
|
||||
- `internal/appdata/paths.go`
|
||||
- `RootDir()` 固定为 `~/.cursor-local-assistant-v2`
|
||||
- `HistoryRootPath()` 为 `~/.cursor-local-assistant-v2/history`
|
||||
- `UsageFilePath()` 为 `~/.cursor-local-assistant-v2/history/usage.json`
|
||||
- `LogsRootPath()` 为 `~/.cursor-local-assistant-v2/logs`
|
||||
|
||||
### `state.json + context.json`
|
||||
|
||||
来源链路:
|
||||
|
||||
- `internal/backend/forwarder/file_store.go`
|
||||
- `CreateConversation`
|
||||
- `LoadConversation`
|
||||
- `AppendEntries`
|
||||
- `SaveConversationWithEntries`
|
||||
- `UpdateConversationMeta`
|
||||
- `ReplaceEntries`
|
||||
- `internal/backend/forwarder/service.go`
|
||||
- `handleRunIntent` 开始新 loop / turn
|
||||
- `appendConversationEntries` 追加语义事件
|
||||
- `internal/backend/forwarder/projector.go`
|
||||
- `ProjectPromptReplay()` 把 `context.json.items` 投影为 provider messages
|
||||
|
||||
稳定结论:
|
||||
|
||||
- `state.json` 是当前状态和可变元数据。
|
||||
- `context.json.items` 是 replayable 语义历史。
|
||||
- 发给 LLM 的历史由 projector 从 `context.json.items` 投影,不是从 provider artifacts 重放。
|
||||
- `state.json.entries` 只是内存结构 `ConversationFile` 的字段;落盘时可投影历史在 `context.json.items`。
|
||||
|
||||
### `usage.json`
|
||||
|
||||
来源链路:
|
||||
|
||||
- `internal/backend/forwarder/usage_store.go`
|
||||
- `UsageFileStore.UpsertEvent`
|
||||
- `UsageFileStore.LookupEvent`
|
||||
- `internal/backend/forwarder/token_usage.go`
|
||||
- `internal/historymetrics/`
|
||||
|
||||
稳定结论:
|
||||
|
||||
- `usage.json` 是全局 usage 聚合,不属于单个 conversation 的语义历史。
|
||||
- `recent_events` 只保留最近有限数量事件;长期总量看 `totals` / `daily`。
|
||||
|
||||
### legacy 清理
|
||||
|
||||
来源链路:
|
||||
|
||||
- `internal/backend/forwarder/history_maintenance.go`
|
||||
|
||||
稳定结论:
|
||||
|
||||
- `turns/`、`conversation.json`、`entries.jsonl`、`request.json`、`summary.json` 等都是 legacy artifact。
|
||||
- 启动后的 history maintenance 会清理这些旧产物。
|
||||
|
||||
## 快速判断规则
|
||||
|
||||
- 用户只发一个 id 时,先查 `history/<id>/state.json` 是否存在;不存在再扫 `state.json/context.json/logs`。
|
||||
- 请求失败时,先看 `state.last_provider_call`、`context.json.items` 的错误 metadata、`logs/app.log`;不要找 `turns/<n>/summary.json`。
|
||||
- pending / 工具不收口时,先看同一 `turn_seq` 的 tool call 和 tool result entries,再对照协议上行 `exec_client_message` / `exec_client_control_message` / `interaction_response`。
|
||||
- prefix cache 异常时,先看 `context.json.items` 的稳定追加顺序和 `usage.json` 的 cache token 字段。
|
||||
- 如果 history 与日志冲突,优先相信当前仍在更新的 `state.json/context.json`,再用日志解释运行时经过了哪条路径。
|
||||
@@ -0,0 +1,147 @@
|
||||
# 文件地图
|
||||
|
||||
## 已安装客户端 bundle
|
||||
|
||||
优先核对这些实际运行中的客户端文件:
|
||||
|
||||
- `/Applications/Cursor.app/Contents/Resources/app/out/vs/workbench/workbench.desktop.main.js`
|
||||
- `/Applications/Cursor.app/Contents/Resources/app/out/vs/workbench/api/node/extensionHostProcess.js`
|
||||
- `/Applications/Cursor.app/Contents/Resources/app/extensions/cursor-always-local/dist/main.js`
|
||||
- `/Applications/Cursor.app/Contents/Resources/app/extensions/cursor-always-local/dist/gitWorker.js`
|
||||
- `/Applications/Cursor.app/Contents/Resources/app/extensions/cursor-agent-exec/dist/main.js`
|
||||
- `/Applications/Cursor.app/Contents/Resources/app/extensions/cursor-agent-exec/dist/*.js`
|
||||
- `/Applications/Cursor.app/Contents/Resources/app/extensions/cursor-agent-worker/dist/main.js`
|
||||
|
||||
当前安装包里 `cursor-agent` 已拆成 split bundle;旧路径 `/Applications/Cursor.app/Contents/Resources/app/extensions/cursor-agent/dist/main.js` 通常不存在。不要按旧路径下结论;需要先列出 `extensions/`,再确认实际存在的 `cursor-agent-exec`、`cursor-agent-worker`、`cursor-always-local` 及其 `dist/` 文件。
|
||||
|
||||
大致归属:
|
||||
|
||||
- `out/vs/workbench/workbench.desktop.main.js`:主 UI、agent window / titlebar、feature flag、用户可点击入口和只读/禁用态。
|
||||
- `cursor-always-local/dist/main.js`:本地模式协议、`BidiAppend`、`RunSSE`、`AgentServerMessage` / `AgentClientMessage` 桥接。
|
||||
- `cursor-agent-exec/dist/main.js` 与同目录数字 chunk:agent 执行侧、SDK/canvas runtime、工具执行、proto 消息定义与拆分 chunk。当前构建中 `411.js` 可能包含 agent 执行链路关键片段,但 chunk 编号不是稳定接口,先用 `dist/*.js` 搜索。
|
||||
- `cursor-agent-worker/dist/main.js`:agent worker 侧后台逻辑。
|
||||
|
||||
用户机器上可能还存在其它 app 副本,例如:
|
||||
|
||||
- `~/Applications/Cursor Hooked.app`
|
||||
- `/Applications/Cursor Patched.app`
|
||||
|
||||
不要假设哪一个在跑,先看进程路径。
|
||||
|
||||
## 当前 backend/store 与 history
|
||||
|
||||
当前用户机器上的固定助手目录:
|
||||
|
||||
- `~/.cursor-local-assistant-v2/`
|
||||
|
||||
重点看:
|
||||
|
||||
- `~/.cursor-local-assistant-v2/config.yaml`
|
||||
- `~/.cursor-local-assistant-v2/data/ca.crt`
|
||||
- `~/.cursor-local-assistant-v2/data/ads/`
|
||||
- `~/.cursor-local-assistant-v2/history/usage.json`
|
||||
- `~/.cursor-local-assistant-v2/history/<conversationId>/state.json`
|
||||
- `~/.cursor-local-assistant-v2/history/<conversationId>/context.json`
|
||||
- `~/.cursor-local-assistant-v2/history/<conversationId>/conversation.lock`
|
||||
- `~/.cursor-local-assistant-v2/logs/app.log`
|
||||
|
||||
其中:
|
||||
|
||||
- `state.json` 是会话元数据、loop 状态、latest provider/request prefix、当前 todos/plans、token/compaction 状态。
|
||||
- `context.json.items` 是 append-only 语义历史,也是 prompt replay 的事实源。
|
||||
- `usage.json` 是全局 provider call / turn usage 聚合。
|
||||
- `conversation.lock` 是会话级文件锁。
|
||||
- checkpoint 只表示同一 backend 进程内的 live state,不是持久化恢复事实源。
|
||||
- legacy artifacts:`conversation.json`、`entries.jsonl`、`turns/`、`request.json`、`summary.json`、`sse.jsonl`、`replay.json`、`runtime.json`、`latest.json`、数字 turn 目录,当前会被 history maintenance 清理。
|
||||
|
||||
这些内容的生成入口主要在:
|
||||
|
||||
- `internal/appdata/paths.go`
|
||||
- `internal/backend/host.go`
|
||||
- `internal/backend/README.md`
|
||||
- `internal/backend/forwarder/file_store.go`
|
||||
- `internal/backend/forwarder/history_maintenance.go`
|
||||
- `internal/backend/forwarder/usage_store.go`
|
||||
- `internal/backend/forwarder/token_usage.go`
|
||||
- `internal/backend/forwarder/artifacts.go`
|
||||
|
||||
## 本仓库协议与本地模式实现
|
||||
|
||||
协议定义:
|
||||
|
||||
- `proto/agent_v1.proto`
|
||||
- `proto/aiserver_v1.proto`
|
||||
- `proto/from_extensions/agent_v1.proto`
|
||||
- `proto/from_extensions/aiserver_v1.proto`
|
||||
|
||||
扩展快照与提取:
|
||||
|
||||
- `proto/extensions-cursor-app/cursor-always-local/package.json`
|
||||
- `proto/extract_extensions_proto.sh`
|
||||
- `proto/ext_tool/main.go`
|
||||
|
||||
本地后端入口:
|
||||
|
||||
- `internal/backend/host.go`
|
||||
- `internal/backend/server/route.go`
|
||||
- `internal/backend/server/policy.go`
|
||||
- `internal/backend/server/local.go`
|
||||
- `internal/backend/server/config/types.go`
|
||||
- `internal/backend/server/config/manager.go`
|
||||
- `internal/backend/server/config/resolver.go`
|
||||
|
||||
forwarder 主链路:
|
||||
|
||||
- `internal/backend/forwarder/module.go`
|
||||
- `internal/backend/forwarder/service.go`
|
||||
- `internal/backend/forwarder/actor.go`
|
||||
- `internal/backend/forwarder/broker.go`
|
||||
- `internal/backend/forwarder/events.go`
|
||||
- `internal/backend/forwarder/compiler.go`
|
||||
- `internal/backend/forwarder/projector.go`
|
||||
- `internal/backend/forwarder/provider.go`
|
||||
- `internal/backend/forwarder/checkpoint_memory.go`
|
||||
- `internal/backend/forwarder/runtime_summary.go`
|
||||
|
||||
协议解码:
|
||||
|
||||
- `internal/backend/agent/protocol/inbound.go`
|
||||
|
||||
执行桥 / 交互桥:
|
||||
|
||||
- `internal/backend/agent/bridge/exec/bridge.go`
|
||||
- `internal/backend/agent/bridge/interaction/bridge.go`
|
||||
|
||||
模型适配:
|
||||
|
||||
- `internal/backend/agent/model/router.go`
|
||||
- `internal/backend/agent/model/openai.go`
|
||||
- `internal/backend/agent/model/anthropic.go`
|
||||
- `internal/backend/agent/model/artifacts.go`
|
||||
- `internal/backend/agent/model/http_error.go`
|
||||
- `internal/backend/agent/model/tool_call_id.go`
|
||||
- `internal/modelchannel/identity.go`
|
||||
- `internal/runtime/local_runtime.go`
|
||||
|
||||
Prompt / replay:
|
||||
|
||||
- `internal/backend/agent/prompt/engine.go`
|
||||
- `internal/backend/agent/prompt/replay.go`
|
||||
- `internal/backend/agent/prompt/content_parts.go`
|
||||
- `internal/backend/forwarder/prompt_context.go`
|
||||
- `internal/backend/forwarder/request_context.go`
|
||||
- `internal/backend/forwarder/reminders.go`
|
||||
- `internal/backend/forwarder/prompt_guard.go`
|
||||
|
||||
## 构建相关参考(只读)
|
||||
|
||||
仓库内已有 macOS 构建与签名相关文件,可用于理解产物结构或历史处理方式,但不要把它们当成修改已安装 Cursor 客户端的操作指南:
|
||||
|
||||
- `Taskfile.yml`
|
||||
- `build/darwin/Taskfile.yml`
|
||||
- `build/dmg-extras/提示损坏?点我.command`
|
||||
|
||||
重点看:
|
||||
|
||||
- `build/darwin/Taskfile.yml` 中的 `codesign:adhoc`
|
||||
- `build/dmg-extras/提示损坏?点我.command` 中的 `xattr -cr`
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
# 已安装客户端只读核对与验证
|
||||
|
||||
首要原则:
|
||||
|
||||
- 不要修改已安装的 Cursor 客户端代码、bundle、签名或 app 副本。
|
||||
- 允许且推荐读取、搜索、比对和分析客户端 bundle、日志、端口和 history 状态。
|
||||
- 目标是定位差异、收集证据、判断问题归属,而不是 patch 客户端。
|
||||
|
||||
## 1. 先确认实际运行的 app 副本
|
||||
|
||||
优先用非交互命令核对:
|
||||
|
||||
```bash
|
||||
pgrep -fal 'Cursor Hooked|Cursor Patched|/Contents/MacOS/Cursor'
|
||||
ps -axo pid,ppid,command | rg 'Cursor(.app)?/Contents/MacOS/Cursor|extension-host'
|
||||
```
|
||||
|
||||
不要在没确认实际运行副本前就下结论,也不要修改客户端文件。
|
||||
|
||||
## 2. 只读定位目标 bundle 与关键文件
|
||||
|
||||
优先定位并读取这些文件,而不是改写它们:
|
||||
|
||||
```bash
|
||||
ls -l "/absolute/path/Target.app/Contents/Resources/app/extensions"
|
||||
shasum -a 256 "/absolute/path/Target.app/Contents/Resources/app/extensions/cursor-always-local/dist/main.js"
|
||||
```
|
||||
|
||||
重点关注:
|
||||
|
||||
- `out/vs/workbench/workbench.desktop.main.js`
|
||||
- `out/vs/workbench/api/node/extensionHostProcess.js`
|
||||
- `cursor-always-local/dist/main.js`
|
||||
- `cursor-always-local/dist/gitWorker.js`
|
||||
- `cursor-agent-exec/dist/main.js`
|
||||
- `cursor-agent-exec/dist/*.js`
|
||||
- `cursor-agent-worker/dist/main.js`
|
||||
|
||||
当前安装包里旧路径 `cursor-agent/dist/main.js` 通常不存在;先确认 `extensions/` 里的实际扩展名和 `dist/` 文件,再选择 `workbench` / `cursor-agent-exec` / `cursor-agent-worker` / `cursor-always-local` 对应排查。
|
||||
|
||||
## 3. 只读读取 bundle 内容
|
||||
|
||||
常用定位关键词:
|
||||
|
||||
```bash
|
||||
rg -n 'BidiTransport|ExecClientMessage|InteractionResponse|conversation_checkpoint_update' "/absolute/path/Target.app/Contents/Resources/app/extensions/cursor-always-local/dist/main.js"
|
||||
rg -n 'CursorAgentProvider|AnthropicProxy|ANTHROPIC_BASE_URL|InteractionUpdate|checkpoint|agent window' "/absolute/path/Target.app/Contents/Resources/app/extensions/cursor-agent-exec/dist/main.js" "/absolute/path/Target.app/Contents/Resources/app/extensions/cursor-agent-exec/dist"/*.js "/absolute/path/Target.app/Contents/Resources/app/extensions/cursor-agent-worker/dist/main.js"
|
||||
rg -n 'agent window|open_agent_window|NameAgent|UpdateConversationMetadata|shouldShowAgentWindowTitleHelperText' "/absolute/path/Target.app/Contents/Resources/app/out/vs/workbench/workbench.desktop.main.js" "/absolute/path/Target.app/Contents/Resources/app/extensions/cursor-agent-exec/dist"/*.js
|
||||
```
|
||||
|
||||
读取具体文件内容时,优先用读取工具按需查看相关片段,不要修改 bundle。
|
||||
|
||||
如果需要和仓库实现对照,优先同时打开:
|
||||
|
||||
- `proto/agent_v1.proto`
|
||||
- `proto/aiserver_v1.proto`
|
||||
- `internal/backend/...`
|
||||
- `internal/runtime/local_runtime.go`
|
||||
|
||||
## 4. 验证行为是否命中目标副本
|
||||
|
||||
至少做其中两项:
|
||||
|
||||
- 进程路径是否是目标 app
|
||||
- 目标扩展 host 是否起来
|
||||
- 本地监听端口是否存在
|
||||
- `~/.cursor-local-assistant-v2/logs/app.log` 是否更新
|
||||
- `~/.cursor-local-assistant-v2/history/<conversationId>/state.json` / `context.json` 是否更新
|
||||
- 请求/协议事件是否真的经过你正在分析的 bundle 文件
|
||||
|
||||
常用验证:
|
||||
|
||||
```bash
|
||||
pgrep -fal '/absolute/path/Target.app/Contents/MacOS/Cursor'
|
||||
lsof -nP -iTCP -sTCP:LISTEN | rg 'Cursor|127.0.0.1'
|
||||
```
|
||||
|
||||
## 5. 记录证据并输出归因
|
||||
|
||||
如果确认“已安装 app 行为”和“仓库代码理解”存在差异,优先记录:
|
||||
|
||||
1. 实际运行的 app 路径
|
||||
2. 命中的 bundle 文件路径与关键符号
|
||||
3. 对应日志、端口、`history/state.json`、`history/context.json`、`usage.json` 证据
|
||||
4. 仓库里对应实现的位置
|
||||
|
||||
如果结论指向客户端侧,也停留在分析和归因,不要继续 patch、重签名、替换文件或做写入式验证。
|
||||
@@ -0,0 +1,153 @@
|
||||
# Provider replay / debug
|
||||
|
||||
当现象是 provider 返回错误、SSE 里只有 `event: error`、需要确认最终出站 provider body 是否能被独立复现时,优先读这份参考。
|
||||
|
||||
这套流程只用于还原“后端最终发给 provider 的请求形状”和“provider 对该请求的真实响应”。它不是语义 history,也不是客户端输入事实源。
|
||||
|
||||
## 证据边界
|
||||
|
||||
- `history/<conversationId>/debug/provider.jsonl`
|
||||
- 最接近 provider 出站边界。
|
||||
- `event=llm_request` 的 `payload.body` 是最终 provider request body。
|
||||
- 用它做 curl replay,判断问题是否已经出现在出站请求形状。
|
||||
- `history/<conversationId>/state.json`
|
||||
- 当前状态和最近 provider 摘要。
|
||||
- 重点看 `latest_request_prefix`、`last_provider_call`。
|
||||
- `history/<conversationId>/context.json`
|
||||
- replayable 语义历史。
|
||||
- 用来解释为什么会形成这次 prompt,不用来直接重放 provider HTTP 请求。
|
||||
- `history/<conversationId>/debug/bidi.raw.jsonl`
|
||||
- 客户端原始上行字节证据。
|
||||
- `history/<conversationId>/debug/bidi.decoded.jsonl`
|
||||
- 当前 known-schema 解码后的客户端上行证据。
|
||||
- `history/<conversationId>/debug/runtime.jsonl`
|
||||
- 后端把哪些字段挂到 active request / stream 上。
|
||||
- `history/<conversationId>/debug/runsse.jsonl`
|
||||
- 后端尝试发回客户端的消息。
|
||||
|
||||
不要把这些证据混用:provider replay 只能证明最终 provider HTTP 请求与响应,不能单独证明客户端原始上传了什么,也不能替代 `context.json` 的语义历史。
|
||||
|
||||
## 前置条件
|
||||
|
||||
- 请求发生时 `config.yaml` 中 `log: true`,或已有 `history/<conversationId>/debug/provider.jsonl`。
|
||||
- 已经通过 id 反查拿到:
|
||||
- `conversationId`
|
||||
- `requestId`
|
||||
- `modelCallId`
|
||||
- 已经确认要测的是 Anthropic-compatible `/v1/messages` 请求。
|
||||
|
||||
如果没有 debug 文件,先回到 `state.json`、`context.json`、`usage.json`、`logs/app.log` 做推断,并明确“没有直接 provider body 证据”。
|
||||
|
||||
## 最小流程
|
||||
|
||||
1. 定位 provider debug 文件:
|
||||
|
||||
```bash
|
||||
ROOT="$HOME/.cursor-local-assistant-v2"
|
||||
CONV="<conversationId>"
|
||||
REQ="<requestId>"
|
||||
MODEL_CALL="<modelCallId>"
|
||||
REQUEST_LOG="$ROOT/history/$CONV/debug/provider.jsonl"
|
||||
```
|
||||
|
||||
2. 确认 `llm_request` 存在:
|
||||
|
||||
```bash
|
||||
jq -c --arg req "$REQ" --arg mc "$MODEL_CALL" '
|
||||
select(.event == "llm_request" and .request_id == $req and .model_call_id == $mc)
|
||||
| {at, conversation_id, request_id, model_call_id, provider: .payload.provider, model: .payload.body.model}
|
||||
' "$REQUEST_LOG"
|
||||
```
|
||||
|
||||
3. 执行通用重放脚本:
|
||||
|
||||
```bash
|
||||
REQUEST_LOG="$REQUEST_LOG" \
|
||||
REQUEST_ID="$REQ" \
|
||||
MODEL_CALL_ID="$MODEL_CALL" \
|
||||
CHANNEL_NAME="GLM" \
|
||||
OUT_DIR="/tmp/cursor-provider-replay-$REQ" \
|
||||
.agents/skills/cursor-client-e2e-debugging/scripts/provider-replay.sh
|
||||
```
|
||||
|
||||
也可以直接传入 provider 配置,避免读取 `config.yaml`:
|
||||
|
||||
```bash
|
||||
REQUEST_LOG="$REQUEST_LOG" \
|
||||
REQUEST_ID="$REQ" \
|
||||
MODEL_CALL_ID="$MODEL_CALL" \
|
||||
BASE_URL="<provider-base-url>" \
|
||||
API_KEY="<provider-api-key>" \
|
||||
OUT_DIR="/tmp/cursor-provider-replay-$REQ" \
|
||||
.agents/skills/cursor-client-e2e-debugging/scripts/provider-replay.sh
|
||||
```
|
||||
|
||||
4. 查看产物:
|
||||
|
||||
- `request.body.json`:抽取出的最终 provider body。
|
||||
- `response.headers`:HTTP 响应头。
|
||||
- `response.sse`:SSE 响应体。
|
||||
- `replay.meta.json`:本次重放引用的 id 和 provider log 路径。
|
||||
|
||||
## 结果判断
|
||||
|
||||
- `curl_exit_code != 0`
|
||||
- 网络、TLS、超时、连接或本机 curl 问题。
|
||||
- 先看 stderr、`response.headers` 是否存在,再判断是否真的到达 provider。
|
||||
- HTTP 非 2xx
|
||||
- provider 网关或鉴权层拒绝。
|
||||
- 优先看 `response.headers` 和 provider 错误体。
|
||||
- HTTP 2xx 但 SSE 中有 `event: error`
|
||||
- provider 已接受连接,但认为请求参数不合法或模型侧拒绝。
|
||||
- 这种情况下重点比对 `request.body.json` 的消息结构、tool schema、thinking/reasoning 参数、model 名称和 endpoint 兼容性。
|
||||
- SSE 正常流式输出
|
||||
- 原始 provider 请求形状基本可用。
|
||||
- 如果客户端仍失败,回到 `runsse.jsonl`、forwarder 状态机或客户端协议层继续查。
|
||||
|
||||
## 常见收敛方向
|
||||
|
||||
provider 参数错误时,优先检查:
|
||||
|
||||
- `model` 是否是目标 endpoint 支持的名称。
|
||||
- `messages` 是否符合 Anthropic-compatible 形态。
|
||||
- `system` 是否被目标 provider 支持,或需要改成 message。
|
||||
- `tools` / `tool_choice` 是否符合目标 provider 方言。
|
||||
- `thinking` / `reasoning` 字段是否被目标 provider 支持。
|
||||
- 图片、文件、cache_control、metadata 等扩展字段是否超出 provider 兼容范围。
|
||||
- `max_tokens`、`temperature`、`top_p`、`stop_sequences` 是否落在 provider 允许范围内。
|
||||
|
||||
## 敏感信息规则
|
||||
|
||||
- 不把 API key 写进 skill、reference、脚本默认值或提交内容。
|
||||
- 回复用户时不要粘贴完整 API key;最多说明“已使用用户提供的 key / config 中的 key”。
|
||||
- 不把完整 `request.body.json` 大段贴给用户;只摘和结论相关的字段形状。
|
||||
- 不把一次性 `conversationId/requestId/modelCallId` 写进 skill 文档。
|
||||
- 临时 replay 产物默认放 `/tmp`;如果需要保留,明确说明路径和原因。
|
||||
|
||||
## 脚本参数
|
||||
|
||||
`scripts/provider-replay.sh` 使用环境变量控制:
|
||||
|
||||
- 必填:
|
||||
- `REQUEST_LOG`
|
||||
- `REQUEST_ID`
|
||||
- `MODEL_CALL_ID`
|
||||
- 可选:
|
||||
- `BASE_URL`
|
||||
- `API_KEY`
|
||||
- `GLM_BASE_URL`
|
||||
- `GLM_API_KEY`
|
||||
- `ANTHROPIC_BASE_URL`
|
||||
- `ANTHROPIC_API_KEY`
|
||||
- `CONFIG_FILE`,默认 `~/.cursor-local-assistant-v2/config.yaml`
|
||||
- `CHANNEL_NAME`,默认 `GLM`
|
||||
- `OUT_DIR`,默认 `/tmp/cursor-provider-replay-<requestId>`
|
||||
- `MAX_TIME`,默认 `240`
|
||||
- `ENDPOINT_PATH`,默认 `/v1/messages`
|
||||
|
||||
脚本输出四个稳定产物:
|
||||
|
||||
- `request.body.json`
|
||||
- `response.headers`
|
||||
- `response.sse`
|
||||
- `replay.meta.json`
|
||||
@@ -0,0 +1,223 @@
|
||||
# 搜索词与判断树
|
||||
|
||||
## 先判断层级
|
||||
|
||||
### `history + logs` / id 反查层
|
||||
|
||||
当现象是“用户只给了一个 id”“需要判断它是 `conversationId`、`requestId`、`modelCallId`、`toolCallId`”“要从本地 history 和日志追状态”:
|
||||
|
||||
优先搜索:
|
||||
|
||||
- `state.json`
|
||||
- `context.json`
|
||||
- `usage.json`
|
||||
- `logs/app.log`
|
||||
- `conversation_id`
|
||||
- `current_request_id`
|
||||
- `request_id`
|
||||
- `model_call_id`
|
||||
- `tool_call_id`
|
||||
- `latest_request_prefix`
|
||||
- `last_provider_call`
|
||||
- `current_loop_status`
|
||||
- `context_version`
|
||||
- `next_entry_seq`
|
||||
- `next_turn_seq`
|
||||
- `LoadConversation`
|
||||
- `CreateConversation`
|
||||
- `SaveConversationWithEntries`
|
||||
- `AppendEntries`
|
||||
- `UpdateConversationMeta`
|
||||
- `ReplaceEntries`
|
||||
- `ProjectPromptReplay`
|
||||
- `UsageFileStore`
|
||||
- `UpsertEvent`
|
||||
- `LookupEvent`
|
||||
|
||||
不要再优先搜索或依赖:
|
||||
|
||||
- `data.sqlite`
|
||||
- `protocol_traces`
|
||||
- `agent_request_runs`
|
||||
- `conversation.json`
|
||||
- `entries.jsonl`
|
||||
- `turns/<n>`
|
||||
- `request.json`
|
||||
- `sse.jsonl`
|
||||
- `summary.json`
|
||||
|
||||
这些是旧实现或 legacy artifact 相关线索,只在排查迁移/清理逻辑时作为历史背景。
|
||||
|
||||
### `cursor-agent-exec` / `cursor-agent-worker` 层
|
||||
|
||||
当现象涉及 agent 主循环、模型桥接、`InteractionUpdate` 映射、工具 started/completed、session/provider 状态:
|
||||
|
||||
优先在已安装客户端 split bundle 中搜索:
|
||||
|
||||
- `/Applications/Cursor.app/Contents/Resources/app/extensions/cursor-agent-exec/dist/main.js`
|
||||
- `/Applications/Cursor.app/Contents/Resources/app/extensions/cursor-agent-exec/dist/*.js`
|
||||
- `/Applications/Cursor.app/Contents/Resources/app/extensions/cursor-agent-worker/dist/main.js`
|
||||
|
||||
优先搜索:
|
||||
|
||||
- `registerAgentProvider`
|
||||
- `CursorAgentProvider`
|
||||
- `CursorAgentProviderHandle`
|
||||
- `ClaudeSDKClient`
|
||||
- `streamInteractionUpdates`
|
||||
- `handlePartialMessage`
|
||||
- `AnthropicProxy`
|
||||
- `getAnthropicProxyPort`
|
||||
- `getAnthropicProxyAuthToken`
|
||||
- `ANTHROPIC_BASE_URL`
|
||||
- `ANTHROPIC_API_KEY`
|
||||
- `InteractionUpdate`
|
||||
- `checkpoint`
|
||||
|
||||
旧路径 `/Applications/Cursor.app/Contents/Resources/app/extensions/cursor-agent/dist/main.js` 可能不存在。先确认实际 `extensions/` 结构,再按 `cursor-agent-exec` / `cursor-agent-worker` / `cursor-always-local` 分层排查。
|
||||
|
||||
### agent window / conversation metadata UI 层
|
||||
|
||||
当现象涉及 agent window 标题、窗口信息、titlebar 按钮、是否可点击修改、会话名/metadata 更新:
|
||||
|
||||
优先在已安装客户端主 UI 和 split bundle 中搜索:
|
||||
|
||||
- `/Applications/Cursor.app/Contents/Resources/app/out/vs/workbench/workbench.desktop.main.js`
|
||||
- `/Applications/Cursor.app/Contents/Resources/app/extensions/cursor-agent-exec/dist/main.js`
|
||||
- `/Applications/Cursor.app/Contents/Resources/app/extensions/cursor-agent-exec/dist/*.js`
|
||||
- `/Applications/Cursor.app/Contents/Resources/app/extensions/cursor-always-local/dist/main.js`
|
||||
|
||||
优先搜索:
|
||||
|
||||
- `shouldShowAgentWindowTitleHelperText`
|
||||
- `glass_open_agents_titlebar_button`
|
||||
- `open_agent_window_top`
|
||||
- `open_agent_window_bottom_convo`
|
||||
- `glass.enable_open_agent_in_window`
|
||||
- `NameAgentRequest`
|
||||
- `NameAgentResponse`
|
||||
- `UpdateConversationMetadataRequest`
|
||||
- `UpdateConversationMetadataResponse`
|
||||
- `CreateTranscriptOverviewRequest`
|
||||
- `createTranscriptOverview`
|
||||
- `updateConversationMetadata`
|
||||
- `conversation_checkpoint_update`
|
||||
|
||||
判断规则:
|
||||
|
||||
- `ConversationStateStructure` / `conversation_checkpoint_update` 是 UI 同步快照,不应作为持久化修改入口。
|
||||
- 如果客户端调用 `UpdateConversationMetadata` / `NameAgent`,要继续确认本地后端是否显式注册对应 `/agent.v1.AgentService/*` 路由;不能只看 proto message 存在。
|
||||
- 本地模式修改会话名/metadata 时,应落到 `history/<conversationId>/state.json` 或等价持久化会话元数据,再 publish checkpoint 同步 UI。
|
||||
|
||||
### `cursor-always-local` / 协议层
|
||||
|
||||
当现象涉及本地模式、客户端没有回包、pending 不收口、同一 backend 进程内的 live checkpoint 重连错乱:
|
||||
|
||||
优先搜索:
|
||||
|
||||
- `BidiTransport`
|
||||
- `startYieldingInputsToTheServer`
|
||||
- `BidiAppend`
|
||||
- `RunSSE`
|
||||
- `AgentServerMessage`
|
||||
- `AgentClientMessage`
|
||||
- `ExecServerMessage`
|
||||
- `ExecClientMessage`
|
||||
- `ExecClientControlMessage`
|
||||
- `InteractionQuery`
|
||||
- `InteractionResponse`
|
||||
- `conversation_checkpoint_update`
|
||||
|
||||
### 本仓库 forwarder 层
|
||||
|
||||
当现象涉及本地后端收发、provider 继续/暂停、exec/interaction 桥接、history 投影:
|
||||
|
||||
优先搜索:
|
||||
|
||||
- `handleRunIntent`
|
||||
- `driveProvider`
|
||||
- `startStreamActor`
|
||||
- `streamCommandEnvelope`
|
||||
- `handleToolInvocation`
|
||||
- `handleExecResult`
|
||||
- `handleExecControl`
|
||||
- `publishCheckpoint`
|
||||
- `CheckpointConversation`
|
||||
- `snapshotCheckpointConversation`
|
||||
- `appendConversationEntries`
|
||||
- `OpenExec`
|
||||
- `OpenQuery`
|
||||
- `StartStream`
|
||||
- `deriveConversationLoopState`
|
||||
- `historyEntryToolCallID`
|
||||
- `recordProviderUsage`
|
||||
- `recordTurnUsage`
|
||||
|
||||
### provider / 模型适配层
|
||||
|
||||
当现象是 provider 400/500、thinking/reasoning、tool_call_id、OpenAI/Anthropic 请求形状、usage/cache 不对:
|
||||
|
||||
优先搜索:
|
||||
|
||||
- `StartStream`
|
||||
- `StreamRequest`
|
||||
- `ResolvedChannelID`
|
||||
- `ResolvedChannelName`
|
||||
- `ProviderModelID`
|
||||
- `ThinkingEnabled`
|
||||
- `buildAnthropicThinkingConfig`
|
||||
- `normalizeAnthropicProviderMessages`
|
||||
- `normalizeOpenAIProviderMessages`
|
||||
- `normalizeOpenAIResponsesInput`
|
||||
- `reasoning_content`
|
||||
- `ReasoningContent`
|
||||
- `ReasoningSignature`
|
||||
- `RecordLLMRequest`
|
||||
- `RecordLLMSummary`
|
||||
- `http_error`
|
||||
- `namespaceToolCallID`
|
||||
|
||||
## 快速判断规则
|
||||
|
||||
- 如果问题是“给你一个 id,让你先判断是什么 ID,再找日志”,先看 `history/<id>/state.json` 是否存在,再扫 `history/*/state.json`、`history/*/context.json` 和 `logs/app.log`。
|
||||
- 如果问题是“模型输出语义不对”,先看 `context.json.items` 到 `ProjectPromptReplay()` 的投影,再看 provider request normalization。
|
||||
- 如果问题是“provider 报 400/参数错误”,先看模型适配层请求构造、`state.latest_request_prefix`、`state.last_provider_call`、`logs/app.log`。
|
||||
- 如果问题是“客户端没回某个工具结果 / pending 不收口”,先看 `cursor-always-local` 与 forwarder,同时核对同一 `turn_seq` 是否有 `tool_result` 或控制面错误 entry。
|
||||
- 如果问题是“backend 重启后为什么 checkpoint 没法继续恢复 pending”,不要找磁盘 checkpoint;checkpoint 是 live state,重启后的事实源是 `state.json + context.json`。
|
||||
- 如果问题是“为什么同一个 `modelID` 还能出现多个渠道”,先检查渠道 ID:规范化后 `baseURL + modelID + apiKey + displayName + openAIEndpoint` 的短 SHA-256;resolver 仍兼容 legacy `baseURL + modelID + apiKey + displayName`。
|
||||
- 如果问题是“只想桥接到其他 LLM”,优先看模型桥接层,不要默认深入整套 local runtime。
|
||||
- 如果问题是“已安装 app 行为和仓库代码不一致”,先核对实际运行 bundle,再做只读比对;不要 patch 客户端。
|
||||
|
||||
## 协议关键词
|
||||
|
||||
上行:
|
||||
|
||||
- `run_request`
|
||||
- `exec_client_message`
|
||||
- `exec_client_control_message`
|
||||
- `interaction_response`
|
||||
|
||||
下行:
|
||||
|
||||
- `interaction_update`
|
||||
- `exec_server_message`
|
||||
- `exec_server_control_message`
|
||||
- `interaction_query`
|
||||
- `conversation_checkpoint_update`
|
||||
|
||||
如果只看到下行请求,没有对应上行结果或控制消息,优先排查:
|
||||
|
||||
- `exec_id`
|
||||
- `id`
|
||||
- `tool_call_id`
|
||||
- `request_id`
|
||||
- `model_call_id`
|
||||
- pending 收口逻辑
|
||||
|
||||
如果用户给的是一个裸 id,不要直接把它当成 `request_id`。先同时查:
|
||||
|
||||
- `history/<id>/state.json`
|
||||
- `history/*/state.json` 的 `current_request_id`、`latest_request_prefix`、`last_provider_call`
|
||||
- `history/*/context.json` 的 `items[].request_id`、`items[].tool_call_id`、`items[].payload`
|
||||
- `history/usage.json` 的 `event_index` / `recent_events`
|
||||
- `logs/app.log`
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const scriptPath = fileURLToPath(import.meta.url);
|
||||
const skillRoot = path.resolve(path.dirname(scriptPath), "..");
|
||||
const repoRoot = path.resolve(skillRoot, "../../..");
|
||||
const args = ["run", "./scripts/historymetrics", ...process.argv.slice(2)];
|
||||
|
||||
const child = spawn("go", args, {
|
||||
cwd: repoRoot,
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
child.on("error", (error) => {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error(`cache-hit-rate.mjs failed: ${message}`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
||||
child.on("exit", (code, signal) => {
|
||||
if (typeof code === "number") {
|
||||
process.exitCode = code;
|
||||
return;
|
||||
}
|
||||
if (signal) {
|
||||
console.error(`cache-hit-rate.mjs terminated by signal: ${signal}`);
|
||||
}
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -0,0 +1,194 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
REQUEST_LOG=<path/to/provider.jsonl> REQUEST_ID=<request-id> MODEL_CALL_ID=<model-call-id> \
|
||||
[BASE_URL=<provider-base-url>] [API_KEY=<provider-api-key>] [CONFIG_FILE=<config.yaml>] \
|
||||
[OUT_DIR=<output-dir>] [MAX_TIME=240] provider-replay.sh
|
||||
|
||||
Required:
|
||||
REQUEST_LOG Path to history/<conversationId>/debug/provider.jsonl
|
||||
REQUEST_ID Provider request_id to replay
|
||||
MODEL_CALL_ID Provider model_call_id to replay
|
||||
|
||||
Optional:
|
||||
BASE_URL Provider base URL. Falls back to GLM_BASE_URL or ANTHROPIC_BASE_URL.
|
||||
API_KEY Provider API key. Falls back to ANTHROPIC_API_KEY or GLM_API_KEY.
|
||||
CONFIG_FILE Defaults to ~/.cursor-local-assistant-v2/config.yaml.
|
||||
CHANNEL_NAME Display name to read from config.yaml when BASE_URL/API_KEY is missing. Defaults to GLM.
|
||||
OUT_DIR Output directory. Defaults to /tmp/cursor-provider-replay-<request-id>.
|
||||
MAX_TIME curl max-time seconds. Defaults to 240.
|
||||
ENDPOINT_PATH Provider path. Defaults to /v1/messages.
|
||||
EOF
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
REQUEST_LOG="${REQUEST_LOG:-}"
|
||||
REQUEST_ID="${REQUEST_ID:-}"
|
||||
MODEL_CALL_ID="${MODEL_CALL_ID:-}"
|
||||
CONFIG_FILE="${CONFIG_FILE:-$HOME/.cursor-local-assistant-v2/config.yaml}"
|
||||
CHANNEL_NAME="${CHANNEL_NAME:-GLM}"
|
||||
BASE_URL="${BASE_URL:-${GLM_BASE_URL:-${ANTHROPIC_BASE_URL:-}}}"
|
||||
API_KEY="${API_KEY:-${ANTHROPIC_API_KEY:-${GLM_API_KEY:-}}}"
|
||||
ENDPOINT_PATH="${ENDPOINT_PATH:-/v1/messages}"
|
||||
MAX_TIME="${MAX_TIME:-240}"
|
||||
OUT_DIR="${OUT_DIR:-/tmp/cursor-provider-replay-${REQUEST_ID:-unknown}}"
|
||||
BODY_FILE="$OUT_DIR/request.body.json"
|
||||
RESP_FILE="$OUT_DIR/response.sse"
|
||||
HEADER_FILE="$OUT_DIR/response.headers"
|
||||
META_FILE="$OUT_DIR/replay.meta.json"
|
||||
|
||||
require_value() {
|
||||
local name="$1"
|
||||
local value="$2"
|
||||
if [[ -z "$value" ]]; then
|
||||
echo "缺少 $name。运行 --help 查看用法。" >&2
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
read_channel_config() {
|
||||
local field="$1"
|
||||
python3 - "$CONFIG_FILE" "$CHANNEL_NAME" "$field" <<'PY'
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
config_path = Path(sys.argv[1]).expanduser()
|
||||
channel_name = sys.argv[2]
|
||||
field = sys.argv[3]
|
||||
if not config_path.exists():
|
||||
raise SystemExit
|
||||
|
||||
lines = config_path.read_text(encoding="utf-8").splitlines()
|
||||
in_channel = False
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("- displayName:"):
|
||||
in_channel = stripped.split(":", 1)[1].strip().strip('"') == channel_name
|
||||
continue
|
||||
if in_channel and stripped.startswith(field + ":"):
|
||||
print(stripped.split(":", 1)[1].strip().strip('"'))
|
||||
raise SystemExit
|
||||
PY
|
||||
}
|
||||
|
||||
require_value "REQUEST_LOG" "$REQUEST_LOG"
|
||||
require_value "REQUEST_ID" "$REQUEST_ID"
|
||||
require_value "MODEL_CALL_ID" "$MODEL_CALL_ID"
|
||||
|
||||
if [[ ! -f "$REQUEST_LOG" ]]; then
|
||||
echo "REQUEST_LOG 不存在: $REQUEST_LOG" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ -z "$BASE_URL" ]]; then
|
||||
BASE_URL="$(read_channel_config baseURL || true)"
|
||||
fi
|
||||
|
||||
if [[ -z "$API_KEY" ]]; then
|
||||
API_KEY="$(read_channel_config apiKey || true)"
|
||||
fi
|
||||
|
||||
require_value "BASE_URL/GLM_BASE_URL/ANTHROPIC_BASE_URL 或 config[$CHANNEL_NAME].baseURL" "$BASE_URL"
|
||||
require_value "API_KEY/ANTHROPIC_API_KEY/GLM_API_KEY 或 config[$CHANNEL_NAME].apiKey" "$API_KEY"
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
: > "$HEADER_FILE"
|
||||
: > "$RESP_FILE"
|
||||
|
||||
python3 - "$REQUEST_LOG" "$REQUEST_ID" "$MODEL_CALL_ID" "$BODY_FILE" "$META_FILE" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
log_path = Path(sys.argv[1]).expanduser()
|
||||
request_id = sys.argv[2]
|
||||
model_call_id = sys.argv[3]
|
||||
body_path = Path(sys.argv[4])
|
||||
meta_path = Path(sys.argv[5])
|
||||
|
||||
body = None
|
||||
meta = None
|
||||
with log_path.open(encoding="utf-8") as f:
|
||||
for raw in f:
|
||||
if not raw.strip():
|
||||
continue
|
||||
row = json.loads(raw)
|
||||
if row.get("event") != "llm_request":
|
||||
continue
|
||||
if row.get("request_id") != request_id:
|
||||
continue
|
||||
if row.get("model_call_id") != model_call_id:
|
||||
continue
|
||||
payload = row.get("payload") or {}
|
||||
body = payload.get("body")
|
||||
meta = {
|
||||
"at": row.get("at"),
|
||||
"event": row.get("event"),
|
||||
"conversation_id": row.get("conversation_id"),
|
||||
"request_id": row.get("request_id"),
|
||||
"model_call_id": row.get("model_call_id"),
|
||||
"provider_log": str(log_path),
|
||||
}
|
||||
break
|
||||
|
||||
if body is None:
|
||||
raise SystemExit(f"未找到 llm_request: request_id={request_id} model_call_id={model_call_id}")
|
||||
|
||||
body_path.write_text(json.dumps(body, ensure_ascii=False, separators=(",", ":")), encoding="utf-8")
|
||||
meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
PY
|
||||
|
||||
url="${BASE_URL%/}${ENDPOINT_PATH}"
|
||||
|
||||
set +e
|
||||
curl --no-buffer --silent --show-error \
|
||||
--connect-timeout 30 \
|
||||
--max-time "$MAX_TIME" \
|
||||
--request POST "$url" \
|
||||
--header "content-type: application/json" \
|
||||
--header "anthropic-version: 2023-06-01" \
|
||||
--header "User-Agent: claude-cli/1.0.25" \
|
||||
--header "x-api-key: $API_KEY" \
|
||||
--header "Authorization: Bearer $API_KEY" \
|
||||
--data-binary "@$BODY_FILE" \
|
||||
--dump-header "$HEADER_FILE" \
|
||||
--output "$RESP_FILE"
|
||||
code=$?
|
||||
set -e
|
||||
|
||||
echo "curl_exit_code=$code"
|
||||
echo "body=$BODY_FILE"
|
||||
echo "headers=$HEADER_FILE"
|
||||
echo "response=$RESP_FILE"
|
||||
echo "meta=$META_FILE"
|
||||
|
||||
echo "--- response headers ---"
|
||||
if [[ -f "$HEADER_FILE" ]]; then
|
||||
python3 - "$HEADER_FILE" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
for line in Path(sys.argv[1]).read_text(encoding="utf-8", errors="replace").splitlines()[:40]:
|
||||
print(line)
|
||||
PY
|
||||
fi
|
||||
|
||||
echo "--- response first 120 lines ---"
|
||||
if [[ -f "$RESP_FILE" ]]; then
|
||||
python3 - "$RESP_FILE" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
for line in Path(sys.argv[1]).read_text(encoding="utf-8", errors="replace").splitlines()[:120]:
|
||||
print(line)
|
||||
PY
|
||||
else
|
||||
echo "响应文件不存在。"
|
||||
fi
|
||||
|
||||
exit "$code"
|
||||
Reference in New Issue
Block a user