cursor-feedback 2.7.3 → 2.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [2.7.5](https://github.com/jianger666/cursor-feedback-extension/compare/v2.7.4...v2.7.5) (2026-07-07)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * **mcp:** headless 会话反馈链路修复——等待窗口压进 CLI 60s 硬超时 + 续期改绑卡片不刷屏 ([ca0a488](https://github.com/jianger666/cursor-feedback-extension/commit/ca0a488b413c8bc287668430199c4619e421a781))
11
+
12
+ ### [2.7.4](https://github.com/jianger666/cursor-feedback-extension/compare/v2.7.3...v2.7.4) (2026-07-07)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * **cli:** spawn 补 --force——headless 下 --approve-mcps 只解决 MCP 加载,工具调用仍被审批静默拒绝,agent 发不出飞书卡片(实测 stream-json 确认「用户拒绝了 interactive_feedback」) ([2f20524](https://github.com/jianger666/cursor-feedback-extension/commit/2f205244d065742c3a39e1a181fcd8911cab6426))
18
+
5
19
  ### [2.7.3](https://github.com/jianger666/cursor-feedback-extension/compare/v2.7.2...v2.7.3) (2026-07-07)
6
20
 
7
21
 
@@ -41,8 +41,9 @@ exports.CliLauncher = void 0;
41
41
  * - 必须用非交互 print 模式(-p):交互式 TUI 会持久改写 cli-config.json 的模型选择。
42
42
  * - CLI 不读 IDE 的全局 User Rules,用户的个人规则要显式注入到 prompt 里
43
43
  * (从 ~/.cursor-feedback/cli-rules.md 读取,没有则只注入 cursor-feedback 沟通协议)。
44
- * - 必须带 --approve-mcpsMCP 批准状态按工作目录落盘,headless 会话无法弹批准框,
45
- * 不带这个 flag 时 cursor-feedback 工具根本不加载,agent 会卡死。
44
+ * - 必须带 --approve-mcps + --force:前者让 MCP 加载(批准状态按工作目录落盘,
45
+ * headless 无法弹批准框);后者放行工具【调用】——只有 --approve-mcps
46
+ * interactive_feedback 的每次调用仍会被审批拦下并静默拒绝,agent 发不出飞书卡片。
46
47
  * - mcp.json 的 env 值必须全是字符串:出现 boolean/number 时 CLI 会静默忽略整个
47
48
  * mcp.json(IDE 却能容忍),spawn 前做一次归一化。
48
49
  * - 拉起的 agent 会通过全局注册的 cursor-feedback MCP 发飞书卡片,用户在手机上
@@ -422,7 +423,10 @@ class CliLauncher {
422
423
  // --approve-mcps 必带:headless 无法弹批准框,MCP 不批准就不加载,agent 会找不到
423
424
  // cursor-feedback 工具而卡死。批准状态按工作目录存(~/.cursor/projects/<dir>/
424
425
  // mcp-approvals.json),逐目录 enable 不现实,只有这个 flag 对任意 cwd 生效。
425
- const args = ['-p', '--trust', '--approve-mcps', '--model', this.model(), prompt];
426
+ // --force 必带(2026-07-07 实测):--approve-mcps 只解决「MCP 加载」,每次工具
427
+ // 【调用】在 headless 下仍要审批,没有 --force 时 interactive_feedback 会被静默
428
+ // 拒绝——agent 完全无法给用户发飞书卡片,只能把结果打到 stdout 等会话结束。
429
+ const args = ['-p', '--trust', '--force', '--approve-mcps', '--model', this.model(), prompt];
426
430
  const isWin = process.platform === 'win32';
427
431
  // Windows 上 .cmd/.ps1 不能直接 spawn(Node 18.20+ 禁止),需经 cmd.exe 转发
428
432
  const viaCmdShell = isWin && /\.(cmd|bat|ps1)$/i.test(bin);
package/dist/feishu.js CHANGED
@@ -673,6 +673,22 @@ class FeishuBridge {
673
673
  return null;
674
674
  }
675
675
  }
676
+ /**
677
+ * 超时续期改绑:把上一轮(刚超时)请求的卡片挪给新一轮请求,不发新卡。
678
+ * 用户对这张卡片的回复会经 resolveParent 直接路由到新请求。
679
+ * 返回 false 表示旧请求没有卡片(发送失败过 / 已被更早的续期改绑走),调用方应正常发新卡。
680
+ */
681
+ rebindCard(oldRequestId, newRequestId, summary, projectDir) {
682
+ const messageId = this.requestToCard.get(oldRequestId);
683
+ if (!messageId)
684
+ return false;
685
+ this.requestToCard.delete(oldRequestId);
686
+ this.cardToRequest.set(messageId, newRequestId);
687
+ this.requestToCard.set(newRequestId, messageId);
688
+ this.requestProjects.set(newRequestId, projectDir);
689
+ this.pendingSummaries.set(newRequestId, { summary, projectDir });
690
+ return true;
691
+ }
676
692
  /** 用 parent_id 反查 requestId(命中返回 requestId,否则 null) */
677
693
  resolveParent(parentId) {
678
694
  if (!parentId)
@@ -107,7 +107,8 @@ class McpFeedbackServer {
107
107
  * 暂存后等下一轮 pending 一注册立即兑现,避免回复石沉大海(修复用户反馈的竞态 bug)。
108
108
  */
109
109
  this.stashedInbound = null;
110
- /** 最近超时结束的请求(id → 归属窗口/时刻),供面板提交与飞书旧卡片回复「续接」下一轮 */
110
+ /** 最近超时结束的请求(id → 归属窗口/时刻/摘要/续期链信息),供面板提交与飞书旧卡片回复
111
+ * 「续接」下一轮,以及超时续期时新一轮「改绑旧卡片 + 继承续期链起点」(summary 判断内容未变) */
111
112
  this.recentlyTimedOut = new Map();
112
113
  /** 面板在超时空窗内提交的反馈:暂存到下一轮 pending 注册时立即兑现 */
113
114
  this.panelStash = null;
@@ -293,7 +294,16 @@ class McpFeedbackServer {
293
294
  // 超时时间优先级:环境变量 > 工具参数 > 默认值(300秒)
294
295
  // 这样用户配置的环境变量永远生效,不会被 AI 覆盖
295
296
  const envTimeout = process.env.MCP_FEEDBACK_TIMEOUT ? parseInt(process.env.MCP_FEEDBACK_TIMEOUT, 10) : null;
296
- const timeout = envTimeout || args?.timeout || 300;
297
+ const requestedTimeout = envTimeout || args?.timeout || 300;
298
+ // headless 主机(cursor-agent CLI 等非 IDE 宿主)对单次 MCP 工具调用有 60s 硬超时
299
+ //(MCP SDK 默认值;CLI 调 callTool 不传 options,无环境变量可改,也不认 progress 续命,
300
+ // 实测 2026-07 版本反编译确认)。等待窗口必须压进 60s,把「一次长等待」翻译成
301
+ // 「N 轮短等待」:requestedTimeout 是动态值(AI 每轮可传不同数),总时长语义由
302
+ // 续期链(chainStartAt)保住——见 outcomeToResult 的超时分支;
303
+ // 续期轮改绑旧卡片(rebindCard),用户飞书上不会被刷屏。
304
+ const timeout = McpFeedbackServer.HEADLESS_HOST
305
+ ? Math.min(requestedTimeout, McpFeedbackServer.HEADLESS_MAX_WAIT_S)
306
+ : requestedTimeout;
297
307
  const requestId = this.generateRequestId();
298
308
  // 重复投递识别:客户端/传输层会把同一次 tool call 原样重复投递(实测某一时刻把进程内
299
309
  // 所有 in-flight 调用各重投一遍,距原投递可晚至 3~8 分钟,无固定上限)。重复投递必须
@@ -335,6 +345,9 @@ class McpFeedbackServer {
335
345
  debugLog(`Owner workspace kept as ${this.ownerWorkspace} (confirmed by window polling); ` +
336
346
  `request project ${normalizedProjectDir} differs and does not rewrite owner`);
337
347
  }
348
+ // 续期识别:同窗口 + 同 summary 的上一轮刚超时 → 本轮是超时续期的下一跳,
349
+ // 继承续期链起点(保住动态 requestedTimeout 的总时长语义)并改绑旧卡片(不刷屏)。
350
+ const renewal = this.findRenewalSource(projectDir, summary);
338
351
  // 创建反馈请求
339
352
  const request = {
340
353
  id: requestId,
@@ -342,6 +355,8 @@ class McpFeedbackServer {
342
355
  projectDir,
343
356
  timeout,
344
357
  timestamp: Date.now(),
358
+ requestedTimeoutS: requestedTimeout,
359
+ chainStartAt: renewal?.chainStartAt ?? Date.now(),
345
360
  };
346
361
  this.currentRequest = request;
347
362
  debugLog(`Feedback request created: ${requestId}`);
@@ -350,10 +365,17 @@ class McpFeedbackServer {
350
365
  debugLog(`Timeout: ${timeout}s`);
351
366
  debugLog(`Waiting for VS Code extension to collect feedback...`);
352
367
  // 飞书:已配置则推送一张反馈请求卡片(失败不影响插件主流程)。
353
- // 例外:忙时队列里已有本项目的排队消息 → 本轮会在注册后立即被队列兑现,
368
+ // 例外 1:忙时队列里已有本项目的排队消息 → 本轮会在注册后立即被队列兑现,
354
369
  // 卡片发出去马上就过期(用户回复只会得到「已结束」),干脆不发。
370
+ // 例外 2:本轮是超时续期 → 改绑旧卡片而不发新卡,
371
+ // 避免续期循环每轮刷一张一模一样的卡(headless 主机 50s 一轮时会把用户刷疯)。
355
372
  if (this.feishu.isConfigured() && !this.hasQueuedFor(projectDir)) {
356
- this.feishu.sendFeedbackCard(requestId, summary, projectDir).catch(() => { });
373
+ if (renewal && this.feishu.rebindCard(renewal.id, requestId, summary, projectDir)) {
374
+ debugLog(`Renewal round: rebound Feishu card from ${renewal.id} to ${requestId} (no new card sent)`);
375
+ }
376
+ else {
377
+ this.feishu.sendFeedbackCard(requestId, summary, projectDir).catch(() => { });
378
+ }
357
379
  }
358
380
  try {
359
381
  // 等待用户反馈
@@ -1004,8 +1026,13 @@ class McpFeedbackServer {
1004
1026
  // 关键:这里【绝不能】说成 "cancelled"——那会让 AI 误以为用户主动取消而结束对话,
1005
1027
  // 这正是“超时后 AI 直接收尾、不再续命”的根因。改为明确区分“超时 ≠ 取消”。
1006
1028
  const autoRetry = this.effectiveAutoRetry();
1007
- const timeoutMessage = autoRetry
1008
- ? '[TIMEOUT_KEEP_WAITING] No feedback was submitted within the wait window. This is a TIMEOUT, NOT a cancellation — the user is most likely still away from the keyboard. You MUST immediately call the interactive_feedback tool again with the same project_directory to keep waiting. Do NOT end the conversation, do NOT summarize the task as finished. Only stop calling it when the user EXPLICITLY tells you to stop. 【这是超时续期,并非用户取消:请立即用相同 project_directory 再次调用 interactive_feedback 继续等待,不要结束对话、不要当作任务完成。】'
1029
+ // headless clamp 补偿:等待窗口被压短(<60s)时,AI 请求的动态 timeout(如 300s、
1030
+ // 每轮可传不同值)被翻译成 N 轮短等待。续期链累计还没盖过请求时长 本次「超时」
1031
+ // 只是内部分片,无条件续期(即便 autoRetry 关着,也必须等满用户要求的时长才算真超时)。
1032
+ const rt = this.recentlyTimedOut.get(requestId);
1033
+ const chainNotDone = rt !== undefined && rt.requestedTimeoutS * 1000 - (Date.now() - rt.chainStartAt) > 1000;
1034
+ const timeoutMessage = (autoRetry || chainNotDone)
1035
+ ? '[TIMEOUT_KEEP_WAITING] No feedback was submitted within the wait window. This is a TIMEOUT, NOT a cancellation — the user is most likely still away from the keyboard. You MUST immediately call the interactive_feedback tool again with the same project_directory AND the exact same summary (copy it verbatim — when the summary is unchanged the server silently reuses the existing notification card instead of pushing a duplicate to the user; any rewording spams them). Do NOT end the conversation, do NOT summarize the task as finished. Only stop calling it when the user EXPLICITLY tells you to stop. 【这是超时续期,并非用户取消:请立即用相同的 project_directory 和一字不差的 summary 再次调用 interactive_feedback 继续等待(summary 不变时服务端会复用已有通知卡片、不再重复打扰用户;改写 summary 会给用户刷屏)。不要结束对话、不要当作任务完成。】'
1009
1036
  : '[TIMEOUT_END] No feedback was submitted within the wait window and timeout auto-continue is disabled (MCP_AUTO_RETRY=false). You may end this turn now. 【超时未收到反馈,且已关闭超时续期(MCP_AUTO_RETRY=false),可以结束本轮。】';
1010
1037
  return {
1011
1038
  content: [
@@ -1076,8 +1103,16 @@ class McpFeedbackServer {
1076
1103
  const onTimeout = () => {
1077
1104
  debugLog(`Request ${requestId} timed out`);
1078
1105
  this.pendingRequests.delete(requestId);
1079
- // 记录「刚超时」:续期空窗内的面板提交 / 旧卡片回复可续接到下一轮(顺手清理过期记录)
1080
- this.recentlyTimedOut.set(requestId, { projectDir, at: Date.now() });
1106
+ // 记录「刚超时」:续期空窗内的面板提交 / 旧卡片回复可续接到下一轮;
1107
+ // summary 供续期轮「改绑旧卡片」判断内容未变;chainStartAt / requestedTimeoutS
1108
+ // 供 outcomeToResult 判断动态请求时长是否真的走完(顺手清理过期记录)
1109
+ this.recentlyTimedOut.set(requestId, {
1110
+ projectDir,
1111
+ at: Date.now(),
1112
+ summary: request.summary,
1113
+ chainStartAt: request.chainStartAt,
1114
+ requestedTimeoutS: request.requestedTimeoutS,
1115
+ });
1081
1116
  for (const [id, v] of this.recentlyTimedOut) {
1082
1117
  if (Date.now() - v.at > McpFeedbackServer.REJOIN_TTL_MS * 4)
1083
1118
  this.recentlyTimedOut.delete(id);
@@ -1205,6 +1240,27 @@ class McpFeedbackServer {
1205
1240
  }
1206
1241
  this.armStashExpiryNotice();
1207
1242
  }
1243
+ /**
1244
+ * 找到本轮的续期来源:同窗口 + 同 summary 的请求刚超时。
1245
+ * 命中说明本轮只是超时续期的下一跳(AI 按 TIMEOUT_KEEP_WAITING 指引原样重调),
1246
+ * 用户视角内容没变——改绑它的卡片(不发新卡)、继承它的续期链起点(保总时长语义)。
1247
+ * 多个候选取最近超时的那个。
1248
+ */
1249
+ findRenewalSource(projectDir, summary) {
1250
+ const owner = this.normalizePath(projectDir);
1251
+ let best = null;
1252
+ for (const [id, v] of this.recentlyTimedOut) {
1253
+ if (Date.now() - v.at > McpFeedbackServer.REJOIN_TTL_MS)
1254
+ continue;
1255
+ if (this.normalizePath(v.projectDir) !== owner)
1256
+ continue;
1257
+ if (v.summary !== summary)
1258
+ continue;
1259
+ if (!best || v.at > best.at)
1260
+ best = { id, chainStartAt: v.chainStartAt, at: v.at };
1261
+ }
1262
+ return best ? { id: best.id, chainStartAt: best.chainStartAt } : null;
1263
+ }
1208
1264
  /**
1209
1265
  * 用户回复了一张「刚超时」的卡片:超时续期开启时 AI 马上会重新发起下一轮,
1210
1266
  * 把回复暂存续接到下一轮,而不是回一句「已结束」把用户的内容丢掉。
@@ -2334,6 +2390,19 @@ class McpFeedbackServer {
2334
2390
  debugLog('Server stopped');
2335
2391
  }
2336
2392
  }
2393
+ /**
2394
+ * 是否运行在 headless 主机下(cursor-agent CLI 等非 IDE 宿主)。
2395
+ * IDE(Cursor/VS Code)拉起的 MCP 进程必带 VSCODE_PID / VSCODE_IPC_HOOK 等环境变量,
2396
+ * CLI 拉起的没有(2026-07 实测对比两类进程的完整 env 确认)。
2397
+ */
2398
+ McpFeedbackServer.HEADLESS_HOST = !process.env.VSCODE_PID && !process.env.VSCODE_IPC_HOOK;
2399
+ /**
2400
+ * headless 主机下单次等待窗口上限(秒)。cursor-agent 对每次 MCP 工具调用有 60s 硬超时
2401
+ *(内嵌 MCP SDK 的默认 timeout,callTool 不传 options、无配置可改、不认 progress 重置),
2402
+ * 等待必须压在 60s 内结束,留 10s 余量给传输与调度。长等待由续期循环补足。
2403
+ * 可用 MCP_FEEDBACK_HEADLESS_MAX_WAIT_S 覆盖(CLI 未来放宽超时 / 测试加速用)。
2404
+ */
2405
+ McpFeedbackServer.HEADLESS_MAX_WAIT_S = Number(process.env.MCP_FEEDBACK_HEADLESS_MAX_WAIT_S || '') || 50;
2337
2406
  McpFeedbackServer.PORT_SCAN_RANGE = 20; // 与插件端扫描范围保持一致
2338
2407
  /** 抢跑暂存有效期:覆盖「AI 刚结束、正要调下一轮 feedback」的竞态间隙(通常 1-3s)。
2339
2408
  * 取 5s——太长会把已脱离语境的旧消息硬塞进很久后才出现的新一轮,造成上下文错乱(像答非所问)。 */
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "cursor-feedback",
3
3
  "displayName": "Cursor Feedback",
4
4
  "description": "One Cursor conversation, unlimited AI interactions - Save your monthly request quota! Interactive feedback loop for AI chat via MCP",
5
- "version": "2.7.3",
5
+ "version": "2.7.5",
6
6
  "icon": "icon.png",
7
7
  "author": "jianger666",
8
8
  "license": "MIT",