cursor-feedback 2.1.0 → 2.1.1

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,13 @@
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.1.1](https://github.com/jianger666/cursor-feedback-extension/compare/v2.1.0...v2.1.1) (2026-07-02)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * **mcp:** 等待反馈期间不再被僵尸判定误杀——Connection closed 断连根因修复 ([39cc9d5](https://github.com/jianger666/cursor-feedback-extension/commit/39cc9d5fe5730023069014c8a749841993117efa))
11
+
5
12
  ## [2.1.0](https://github.com/jianger666/cursor-feedback-extension/compare/v2.0.5...v2.1.0) (2026-07-02)
6
13
 
7
14
 
package/dist/extension.js CHANGED
@@ -46,6 +46,12 @@ let feedbackViewProvider = null;
46
46
  function normalizePath(p) {
47
47
  return (p || '').replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
48
48
  }
49
+ /** 两个已归一化的路径互为前缀(相等 / 一方是另一方的子目录)即视为同一窗口语境 */
50
+ function pathsRelated(a, b) {
51
+ if (!a || !b)
52
+ return false;
53
+ return a === b || a.startsWith(b + '/') || b.startsWith(a + '/');
54
+ }
49
55
  function activate(context) {
50
56
  // 注册侧边栏 WebView(端口从 61927 开始自动扫描)
51
57
  feedbackViewProvider = new FeedbackViewProvider(context.extensionUri, 61927, context.globalState);
@@ -109,8 +115,9 @@ function getWorkspacePaths() {
109
115
  return folders.map(f => f.uri.fsPath);
110
116
  }
111
117
  /**
112
- * 检查路径是否匹配当前工作区(精确匹配)
113
- * - 有工作区的窗口:只接收匹配工作区路径的消息
118
+ * 检查路径是否匹配当前工作区
119
+ * - 有工作区的窗口:接收匹配工作区路径的消息(相等或互为子目录——AI 传的
120
+ * project_directory 常是工作区的子目录,精确匹配会漏收,面板收不到、心跳也失配)
114
121
  * - 没有工作区的窗口:只接收没有指定项目路径的消息
115
122
  */
116
123
  function isPathInWorkspace(targetPath) {
@@ -128,9 +135,7 @@ function isPathInWorkspace(targetPath) {
128
135
  return false;
129
136
  }
130
137
  for (const wsPath of workspacePaths) {
131
- const normalizedWs = normalizePath(wsPath);
132
- // 精确匹配:只匹配完全相同的路径
133
- if (normalizedTarget === normalizedWs) {
138
+ if (pathsRelated(normalizedTarget, normalizePath(wsPath))) {
134
139
  return true;
135
140
  }
136
141
  }
@@ -331,10 +336,10 @@ class FeedbackViewProvider {
331
336
  // 如果有活跃端口,先尝试只轮询该端口
332
337
  if (this._activePort) {
333
338
  const result = await this._checkPortForRequest(this._activePort);
334
- // 检查是否仍然是我们的 Server
339
+ // 检查是否仍然是我们的 Server(owner 可能是本工作区的子目录:AI 传的 project_directory)
335
340
  if (result.connected) {
336
341
  const serverOwner = result.ownerWorkspace ? normalizePath(result.ownerWorkspace) : '';
337
- const isMyServer = !serverOwner || serverOwner === normalizedCurrentWorkspace;
342
+ const isMyServer = !serverOwner || pathsRelated(serverOwner, normalizedCurrentWorkspace);
338
343
  if (isMyServer) {
339
344
  // 端口仍然有效,保持使用
340
345
  this._debugInfo.connectedPorts = [this._activePort];
@@ -376,7 +381,7 @@ class FeedbackViewProvider {
376
381
  return false;
377
382
  }
378
383
  const serverOwner = r.ownerWorkspace ? normalizePath(r.ownerWorkspace) : '';
379
- return !serverOwner || serverOwner === normalizedCurrentWorkspace;
384
+ return !serverOwner || pathsRelated(serverOwner, normalizedCurrentWorkspace);
380
385
  }).sort((a, b) => b.request.timestamp - a.request.timestamp);
381
386
  // 处理最新的请求
382
387
  if (myRequests.length > 0) {
@@ -108,6 +108,10 @@ class McpFeedbackServer {
108
108
  // 所属工作区(只在 AI 调用 feedback 时设置)
109
109
  // 只有来自同一工作区的轮询才会更新活动时间
110
110
  this.ownerWorkspace = null;
111
+ // owner 身份是否已被真实窗口的轮询验证过。验证后 ownerWorkspace 不再被后续调用改写:
112
+ // AI 传的 project_directory 是「正在操作的项目」,不一定等于窗口工作区(同窗口操作兄弟/子目录很常见),
113
+ // 每次改写会让插件心跳失配 → 实例被防线 3 当僵尸误杀(等待反馈中直接 Connection closed)。
114
+ this.ownerConfirmed = false;
111
115
  // Server 启动时间
112
116
  this.startTime = Date.now();
113
117
  // 最近一次活动时间(任意 HTTP 轮询 / MCP 调用都会刷新)
@@ -122,6 +126,9 @@ class McpFeedbackServer {
122
126
  this.everOwnerPolled = false;
123
127
  // 看门狗定时器(兜底退出,防止进程残留 / CPU 占满)
124
128
  this.watchdogTimer = null;
129
+ // stop() 防重入:server.close() 会触发 transport.onclose → 回调里又调 stop(),
130
+ // 无标志位会无限递归直至 "Maximum call stack size exceeded"(日志曾刷出数万条 Stopping server...)
131
+ this.stopping = false;
125
132
  this.port = port;
126
133
  this.basePort = port;
127
134
  this.server = new index_js_1.Server({
@@ -255,9 +262,18 @@ class McpFeedbackServer {
255
262
  // 不清理会让 pendingCount 虚高、全局视角误判「多个窗口在等」
256
263
  //(即你看到的两个一模一样的 cursor-feedback-extension)。
257
264
  this.cancelStalePending(projectDir);
258
- // AI 调用 feedback 时设置 ownerWorkspace(这是唯一正确的时机)
259
- this.ownerWorkspace = this.normalizePath(projectDir);
260
- debugLog(`Owner workspace set to: ${this.ownerWorkspace}`);
265
+ // AI 调用 feedback 时设置 ownerWorkspace;但 owner 一旦被真实窗口的轮询验证过(ownerConfirmed)
266
+ // 就锁定不再改写——project_directory 只代表 AI 正在操作的项目,改写已验证的窗口身份
267
+ // 会让心跳失配、实例被防线 3 误杀。
268
+ const normalizedProjectDir = this.normalizePath(projectDir);
269
+ if (!this.ownerConfirmed) {
270
+ this.ownerWorkspace = normalizedProjectDir;
271
+ debugLog(`Owner workspace set to: ${this.ownerWorkspace}`);
272
+ }
273
+ else if (normalizedProjectDir !== this.ownerWorkspace) {
274
+ debugLog(`Owner workspace kept as ${this.ownerWorkspace} (confirmed by window polling); ` +
275
+ `request project ${normalizedProjectDir} differs and does not rewrite owner`);
276
+ }
261
277
  // 创建反馈请求
262
278
  this.currentRequest = {
263
279
  id: requestId,
@@ -459,6 +475,16 @@ class McpFeedbackServer {
459
475
  normalizePath(p) {
460
476
  return (p || '').replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
461
477
  }
478
+ /**
479
+ * 两个已归一化的路径是否属于同一窗口语境:相等,或一方是另一方的子目录。
480
+ * 用于 owner 心跳匹配——AI 传的 project_directory 常是窗口工作区的子目录(monorepo 子包等),
481
+ * 精确相等会漏判、心跳失配导致实例被防线 3 误杀。
482
+ */
483
+ pathsRelated(a, b) {
484
+ if (!a || !b)
485
+ return false;
486
+ return a === b || a.startsWith(b + '/') || b.startsWith(a + '/');
487
+ }
462
488
  /** 飞书回复命中某 requestId → resolve 该 pending,并回执用户 */
463
489
  submitFromFeishu(requestId, text, chatId, images = [], attachedFiles = [], ackMessageId) {
464
490
  const pending = this.pendingRequests.get(requestId);
@@ -942,10 +968,13 @@ class McpFeedbackServer {
942
968
  // 「我的窗口」心跳:仅当轮询带的 workspace 匹配本实例归属时刷新。
943
969
  // 别的活跃窗口对全端口的扫描虽刷 lastActivityTime,但 workspace 不匹配、不刷此字段,
944
970
  // 故已关窗口的残留 server 不会被别人续命,可被 watchdog / 僵尸自检识别。
971
+ // 匹配放宽为「路径互为前缀」:AI 传的 project_directory 可能是窗口工作区的子目录。
972
+ // 命中即确认 owner 身份(ownerConfirmed),此后 owner 不再被后续调用改写。
945
973
  const ws = this.normalizePath(u.searchParams.get('workspace') || '');
946
- if (ws && ws === this.ownerWorkspace) {
974
+ if (ws && this.ownerWorkspace && this.pathsRelated(ws, this.ownerWorkspace)) {
947
975
  this.lastOwnerPollTime = Date.now();
948
976
  this.everOwnerPolled = true;
977
+ this.ownerConfirmed = true;
949
978
  }
950
979
  }
951
980
  catch {
@@ -1352,8 +1381,13 @@ class McpFeedbackServer {
1352
1381
  }
1353
1382
  // 防线 3:本实例曾被自己窗口插件轮询,但久未再收到(只剩别的活跃窗口在扫端口刷 lastActivityTime)
1354
1383
  // → 我的窗口已关、本实例是僵尸 → 退出,避免持续被全局 pending 计数误算成「另一个在等的窗口」。
1384
+ // ⚠️ 与防线 2 同款约束:还有 pending 反馈在等用户时绝不退——曾因缺这一条,AI 在等待反馈期间
1385
+ // 传了与窗口工作区不同的 project_directory(改写 owner 后心跳失配),30 秒后实例被当僵尸杀掉,
1386
+ // 正在等待的 interactive_feedback 直接报 "MCP error -32000: Connection closed"。
1355
1387
  const ownerIdle = Date.now() - this.lastOwnerPollTime;
1356
- if (this.everOwnerPolled && ownerIdle > IDLE_TIMEOUT) {
1388
+ if (this.everOwnerPolled &&
1389
+ this.pendingRequests.size === 0 &&
1390
+ ownerIdle > IDLE_TIMEOUT) {
1357
1391
  debugLog(`Owner window idle for ${ownerIdle}ms, stale instance, exiting...`);
1358
1392
  this.stop();
1359
1393
  process.exit(0);
@@ -1363,9 +1397,13 @@ class McpFeedbackServer {
1363
1397
  this.watchdogTimer.unref();
1364
1398
  }
1365
1399
  /**
1366
- * 停止服务器
1400
+ * 停止服务器(幂等:重复调用 / server.close() 触发 onclose 再次进入时直接返回)
1367
1401
  */
1368
1402
  stop() {
1403
+ if (this.stopping) {
1404
+ return;
1405
+ }
1406
+ this.stopping = true;
1369
1407
  debugLog('Stopping server...');
1370
1408
  // 关闭看门狗定时器
1371
1409
  if (this.watchdogTimer) {
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.1.0",
5
+ "version": "2.1.1",
6
6
  "icon": "icon.png",
7
7
  "author": "jianger666",
8
8
  "license": "MIT",