cursor-feedback 2.3.0 → 2.3.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/mcp-server.js +70 -19
- package/package.json +2 -2
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.3.1](https://github.com/jianger666/cursor-feedback-extension/compare/v2.3.0...v2.3.1) (2026-07-06)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **mcp:** 重复投递去重去掉 90s 时间窗,wire id 精确识别 + 迟到重投结果重放 ([3eb6be5](https://github.com/jianger666/cursor-feedback-extension/commit/3eb6be5771feb56f11e09f6f61f96fd0dbf627da))
|
|
11
|
+
|
|
5
12
|
## [2.3.0](https://github.com/jianger666/cursor-feedback-extension/compare/v2.2.0...v2.3.0) (2026-07-04)
|
|
6
13
|
|
|
7
14
|
|
package/dist/mcp-server.js
CHANGED
|
@@ -148,6 +148,11 @@ class McpFeedbackServer {
|
|
|
148
148
|
// stop() 防重入:server.close() 会触发 transport.onclose → 回调里又调 stop(),
|
|
149
149
|
// 无标志位会无限递归直至 "Maximum call stack size exceeded"(日志曾刷出数万条 Stopping server...)
|
|
150
150
|
this.stopping = false;
|
|
151
|
+
/**
|
|
152
|
+
* 已结束请求的结果缓存(wire id → 当时的结果):供「迟到的重复投递」精确重放。
|
|
153
|
+
* 容量与时效都收紧(feedback 结果可能含 base64 图片,不能无限囤积)。
|
|
154
|
+
*/
|
|
155
|
+
this.settledByWire = new Map();
|
|
151
156
|
this.port = port;
|
|
152
157
|
this.basePort = port;
|
|
153
158
|
this.server = new index_js_1.Server({
|
|
@@ -224,14 +229,17 @@ class McpFeedbackServer {
|
|
|
224
229
|
};
|
|
225
230
|
});
|
|
226
231
|
// 处理工具调用
|
|
227
|
-
this.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
232
|
+
this.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request, extra) => {
|
|
228
233
|
// AI 发起调用,刷新活动时间,避免被 watchdog 误判为闲置
|
|
229
234
|
this.lastActivityTime = Date.now();
|
|
230
235
|
const { name, arguments: args } = request.params;
|
|
236
|
+
// JSON-RPC 请求 id(同一连接内唯一):客户端把同一次 tool call 重复投递时若复用
|
|
237
|
+
// 同一 id,可据此做精确去重 / 结果重放(比 summary 内容启发式可靠)。
|
|
238
|
+
const wireKey = extra?.requestId !== undefined ? String(extra.requestId) : undefined;
|
|
231
239
|
try {
|
|
232
240
|
switch (name) {
|
|
233
241
|
case 'interactive_feedback':
|
|
234
|
-
return await this.handleInteractiveFeedback(args);
|
|
242
|
+
return await this.handleInteractiveFeedback(args, wireKey);
|
|
235
243
|
case 'get_system_info':
|
|
236
244
|
return this.handleGetSystemInfo();
|
|
237
245
|
default:
|
|
@@ -252,8 +260,9 @@ class McpFeedbackServer {
|
|
|
252
260
|
}
|
|
253
261
|
/**
|
|
254
262
|
* 处理交互式反馈请求
|
|
263
|
+
* @param wireKey 本次调用的 JSON-RPC 请求 id(用于重复投递的精确识别与结果重放)
|
|
255
264
|
*/
|
|
256
|
-
async handleInteractiveFeedback(args) {
|
|
265
|
+
async handleInteractiveFeedback(args, wireKey) {
|
|
257
266
|
// 参数校验:project_directory 是必填项
|
|
258
267
|
if (!args?.project_directory) {
|
|
259
268
|
const receivedParams = JSON.stringify(args || {});
|
|
@@ -276,15 +285,29 @@ class McpFeedbackServer {
|
|
|
276
285
|
const envTimeout = process.env.MCP_FEEDBACK_TIMEOUT ? parseInt(process.env.MCP_FEEDBACK_TIMEOUT, 10) : null;
|
|
277
286
|
const timeout = envTimeout || args?.timeout || 300;
|
|
278
287
|
const requestId = this.generateRequestId();
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
|
|
288
|
+
// 重复投递识别:客户端/传输层会把同一次 tool call 原样重复投递(实测某一时刻把进程内
|
|
289
|
+
// 所有 in-flight 调用各重投一遍,距原投递可晚至 3~8 分钟,无固定上限)。重复投递必须
|
|
290
|
+
// join 原等待共享结果,绝不能当新一轮:不发新卡、不顶旧请求——顶了会让原调用收到
|
|
291
|
+
// SUPERSEDED(AI 误以为被新会话取代而提前收尾),且重投开出的幽灵卡片没人认领,
|
|
292
|
+
// 用户回复它只会石沉大海。
|
|
293
|
+
const dup = this.findDuplicatePending(projectDir, summary, timeout, wireKey);
|
|
283
294
|
if (dup) {
|
|
284
|
-
debugLog(`Duplicate delivery detected for request ${dup.id}; joining its wait instead of superseding`);
|
|
295
|
+
debugLog(`Duplicate delivery detected (matched by ${dup.via}) for request ${dup.id}; joining its wait instead of superseding`);
|
|
285
296
|
const outcome = await new Promise((res) => dup.waiters.push(res));
|
|
297
|
+
this.rememberSettledWire(wireKey, outcome, dup.id);
|
|
286
298
|
return this.outcomeToResult(outcome, dup.id);
|
|
287
299
|
}
|
|
300
|
+
// 迟到的重复投递(原请求已结束):同 wire id 精确命中已结束请求 → 原样重放当时的结果。
|
|
301
|
+
// 绝不能开新一轮:原调用早已返回、没人会认领新卡,用户回复只会丢失。
|
|
302
|
+
// 只按 wire id 匹配、绝不按 summary——AI 超时续期的新一轮常复用同一句 summary,
|
|
303
|
+
// 按内容重放旧超时会造成「新调用秒收超时 → 立即重调 → 又秒收」的快速空转。
|
|
304
|
+
if (wireKey) {
|
|
305
|
+
const settled = this.settledByWire.get(wireKey);
|
|
306
|
+
if (settled) {
|
|
307
|
+
debugLog(`Late duplicate delivery (wire id ${wireKey}) for settled request ${settled.requestId}; replaying its outcome`);
|
|
308
|
+
return this.outcomeToResult(settled.outcome, settled.requestId);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
288
311
|
// 作废上一轮残留的「僵尸」请求:单实例单窗口同时只应有一个活跃反馈请求。
|
|
289
312
|
// 旧请求多半是对话被压缩 / 客户端取消后还卡在 await 的残留(要等 timeout 才自然结束),
|
|
290
313
|
// 不清理会让 pendingCount 虚高、全局视角误判「多个窗口在等」
|
|
@@ -324,7 +347,8 @@ class McpFeedbackServer {
|
|
|
324
347
|
}
|
|
325
348
|
try {
|
|
326
349
|
// 等待用户反馈
|
|
327
|
-
const outcome = await this.waitForFeedback(request, timeout * 1000);
|
|
350
|
+
const outcome = await this.waitForFeedback(request, timeout * 1000, wireKey);
|
|
351
|
+
this.rememberSettledWire(wireKey, outcome, requestId);
|
|
328
352
|
return this.outcomeToResult(outcome, requestId);
|
|
329
353
|
}
|
|
330
354
|
catch (error) {
|
|
@@ -606,19 +630,49 @@ class McpFeedbackServer {
|
|
|
606
630
|
this.pendingRequests.delete(reqId);
|
|
607
631
|
}
|
|
608
632
|
}
|
|
609
|
-
|
|
633
|
+
/**
|
|
634
|
+
* 查找可 join 的「重复投递」等待,两级匹配:
|
|
635
|
+
* 1) wire id 精确匹配:同一条 JSON-RPC 请求(同 id)还在等待 → 必是重复投递;
|
|
636
|
+
* 2) 内容启发式:同窗口 + 同 summary + 同 timeout,且原请求仍在 pending(覆盖客户端
|
|
637
|
+
* 重投时换了新 wire id 的情况)。
|
|
638
|
+
* 内容启发式不设时间窗——旧实现的 90s 窗口被实测击穿(重投可晚至 3~8 分钟,无固定上限)。
|
|
639
|
+
* 不设窗口是安全的:pending 仍存活 = 原调用尚未返回 = 同一会话不可能已合法开启新一轮;
|
|
640
|
+
* 不同会话在同一窗口撞出一字不差的 summary + timeout 概率可忽略,即便撞上,
|
|
641
|
+
* join(双方共享同一份回复)也远比 supersede(悄悄掐掉别人的等待)安全。
|
|
642
|
+
*/
|
|
643
|
+
findDuplicatePending(projectDir, summary, timeout, wireKey) {
|
|
610
644
|
const owner = this.normalizePath(projectDir);
|
|
611
645
|
for (const [id, pending] of this.pendingRequests) {
|
|
646
|
+
if (wireKey !== undefined && pending.wireId === wireKey) {
|
|
647
|
+
return { id, via: 'wire id', waiters: pending.waiters };
|
|
648
|
+
}
|
|
612
649
|
if (this.normalizePath(pending.projectDir) !== owner)
|
|
613
650
|
continue;
|
|
614
651
|
if (pending.request.summary !== summary)
|
|
615
652
|
continue;
|
|
616
|
-
if (
|
|
653
|
+
if (pending.request.timeout !== timeout)
|
|
617
654
|
continue;
|
|
618
|
-
return { id, waiters: pending.waiters };
|
|
655
|
+
return { id, via: 'content', waiters: pending.waiters };
|
|
619
656
|
}
|
|
620
657
|
return null;
|
|
621
658
|
}
|
|
659
|
+
rememberSettledWire(wireKey, outcome, requestId) {
|
|
660
|
+
if (!wireKey)
|
|
661
|
+
return;
|
|
662
|
+
const now = Date.now();
|
|
663
|
+
this.settledByWire.set(wireKey, { outcome, requestId, at: now });
|
|
664
|
+
for (const [k, v] of this.settledByWire) {
|
|
665
|
+
if (now - v.at > McpFeedbackServer.SETTLED_WIRE_TTL_MS)
|
|
666
|
+
this.settledByWire.delete(k);
|
|
667
|
+
}
|
|
668
|
+
// Map 按插入序迭代,超限时先淘汰最早的
|
|
669
|
+
while (this.settledByWire.size > McpFeedbackServer.SETTLED_WIRE_MAX) {
|
|
670
|
+
const oldest = this.settledByWire.keys().next().value;
|
|
671
|
+
if (oldest === undefined)
|
|
672
|
+
break;
|
|
673
|
+
this.settledByWire.delete(oldest);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
622
676
|
/**
|
|
623
677
|
* 把等待结果翻译成 MCP 工具响应(主等待与重复投递 join 的等待共用同一段收尾语义)。
|
|
624
678
|
*/
|
|
@@ -700,7 +754,7 @@ class McpFeedbackServer {
|
|
|
700
754
|
* 等待用户反馈:注册 pending 并挂起,结果通过 waiters 广播——
|
|
701
755
|
* 首个调用与后续 join 进来的重复投递调用都会收到同一份结果。
|
|
702
756
|
*/
|
|
703
|
-
waitForFeedback(request, timeoutMs) {
|
|
757
|
+
waitForFeedback(request, timeoutMs, wireKey) {
|
|
704
758
|
return new Promise((resolve) => {
|
|
705
759
|
const requestId = request.id;
|
|
706
760
|
const projectDir = request.projectDir;
|
|
@@ -738,6 +792,7 @@ class McpFeedbackServer {
|
|
|
738
792
|
remainingMs: timeoutMs,
|
|
739
793
|
request,
|
|
740
794
|
waiters,
|
|
795
|
+
wireId: wireKey,
|
|
741
796
|
});
|
|
742
797
|
// 抢跑兑现:空窗期的面板提交(优先,用户显式点了发送)与飞书暂存消息,
|
|
743
798
|
// 本轮 pending 一注册立即作为回复提交;最后是忙时队列里排队的追加消息
|
|
@@ -1930,12 +1985,8 @@ McpFeedbackServer.QUEUE_MAX = 100;
|
|
|
1930
1985
|
*(只剩别的窗口在扫端口)→ 视为我的窗口已关、本实例是僵尸:不再上报 pending,避免污染全局计数。
|
|
1931
1986
|
*/
|
|
1932
1987
|
McpFeedbackServer.OWNER_IDLE_MS = 12000;
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
* AI 每一轮的 summary 几乎不可能与上一轮一字不差,短窗口内完全相同基本可断定为
|
|
1936
|
-
* 客户端/传输层对同一次 tool call 的重复投递。
|
|
1937
|
-
*/
|
|
1938
|
-
McpFeedbackServer.DUP_JOIN_MS = 90000;
|
|
1988
|
+
McpFeedbackServer.SETTLED_WIRE_TTL_MS = 10 * 60 * 1000;
|
|
1989
|
+
McpFeedbackServer.SETTLED_WIRE_MAX = 20;
|
|
1939
1990
|
// 主函数
|
|
1940
1991
|
async function main() {
|
|
1941
1992
|
const port = 61927;
|
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.3.
|
|
5
|
+
"version": "2.3.1",
|
|
6
6
|
"icon": "icon.png",
|
|
7
7
|
"author": "jianger666",
|
|
8
8
|
"license": "MIT",
|
|
@@ -147,7 +147,7 @@
|
|
|
147
147
|
"watch": "tsc -watch -p ./",
|
|
148
148
|
"lint": "eslint src --ext ts",
|
|
149
149
|
"start:mcp": "node dist/mcp-server.js",
|
|
150
|
-
"test:smoke": "npm run compile && node scripts/smoke-queue.js",
|
|
150
|
+
"test:smoke": "npm run compile && node scripts/smoke-queue.js && node scripts/smoke-dup.js",
|
|
151
151
|
"prepublishOnly": "npm run check-changelog && npm run compile",
|
|
152
152
|
"check-changelog": "node scripts/check-changelog.js",
|
|
153
153
|
"release": "standard-version",
|