This commit is contained in:
leokun
2026-06-30 10:38:52 +08:00
commit c083be5ec2
312 changed files with 146628 additions and 0 deletions
@@ -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 的语义历史 entriesprompt 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` 与同目录数字 chunkagent 执行侧、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`
@@ -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”,不要找磁盘 checkpointcheckpoint 是 live state,重启后的事实源是 `state.json + context.json`
- 如果问题是“为什么同一个 `modelID` 还能出现多个渠道”,先检查渠道 ID:规范化后 `baseURL + modelID + apiKey + displayName + openAIEndpoint` 的短 SHA-256resolver 仍兼容 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"