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 +7 -0
- package/dist/extension.js +13 -8
- package/dist/mcp-server.js +44 -6
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
|
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) {
|
package/dist/mcp-server.js
CHANGED
|
@@ -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
|
-
|
|
260
|
-
|
|
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 &&
|
|
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 &&
|
|
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.
|
|
5
|
+
"version": "2.1.1",
|
|
6
6
|
"icon": "icon.png",
|
|
7
7
|
"author": "jianger666",
|
|
8
8
|
"license": "MIT",
|