cursor-feedback 2.0.3 → 2.0.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 +19 -0
- package/README.md +29 -0
- package/README_CN.md +29 -0
- package/dist/mcp-server.js +58 -28
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
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.0.5](https://github.com/jianger666/cursor-feedback-extension/compare/v2.0.4...v2.0.5) (2026-06-23)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **mcp:** 根除多窗口取消风暴 + 无插件 host 不再因空闲自杀 ([5333b41](https://github.com/jianger666/cursor-feedback-extension/commit/5333b41a0903b8e16df2ad31cb033796426805fb))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Chores
|
|
14
|
+
|
|
15
|
+
* 补齐 release:patch 脚本,与 releasing skill 文档一致 ([ed8a292](https://github.com/jianger666/cursor-feedback-extension/commit/ed8a2927eff7155d2f1ac6c627d9fb9006f6c6f3))
|
|
16
|
+
|
|
17
|
+
### [2.0.4](https://github.com/jianger666/cursor-feedback-extension/compare/v2.0.3...v2.0.4) (2026-06-22)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* **mcp:** 反馈归属只取 AI 传入的 project_directory,不再被 WORKSPACE_FOLDER_PATHS 带偏 ([b43975f](https://github.com/jianger666/cursor-feedback-extension/commit/b43975f772651725569bb0e578937ef8a52683c9))
|
|
23
|
+
|
|
5
24
|
### [2.0.3](https://github.com/jianger666/cursor-feedback-extension/compare/v2.0.2...v2.0.3) (2026-06-22)
|
|
6
25
|
|
|
7
26
|
|
package/README.md
CHANGED
|
@@ -230,6 +230,35 @@ Either way works — but you first need to set up the bot in the Feishu console:
|
|
|
230
230
|
|
|
231
231
|
> Priority: **panel config (when credentials are filled) > env here > default**. The panel wins when App ID/Secret are filled; otherwise it falls back to env. You still need to send the bot one message in Feishu to complete binding.
|
|
232
232
|
|
|
233
|
+
### Feishu round-trip on any MCP host
|
|
234
|
+
|
|
235
|
+
`cursor-feedback` is **not tied to Cursor**. The whole Feishu loop — long connection, card push, and reply routing — lives inside the MCP server process, so it works on **any MCP-capable agent host**: Cursor, Claude Desktop, Cline, command-line agents, or your own harness. No sidebar / VS Code panel required.
|
|
236
|
+
|
|
237
|
+
Three steps to enable the round-trip:
|
|
238
|
+
|
|
239
|
+
1. **Register the MCP server** in the host's config with Feishu credentials in `env` (same `FEISHU_APP_ID` / `FEISHU_APP_SECRET` as above; `FEISHU_ENABLED` / `FEISHU_ACK` optional):
|
|
240
|
+
|
|
241
|
+
```json
|
|
242
|
+
{
|
|
243
|
+
"mcpServers": {
|
|
244
|
+
"cursor-feedback": {
|
|
245
|
+
"command": "npx",
|
|
246
|
+
"args": ["-y", "cursor-feedback@latest"],
|
|
247
|
+
"env": {
|
|
248
|
+
"FEISHU_APP_ID": "cli_xxxxxxxx",
|
|
249
|
+
"FEISHU_APP_SECRET": "your_app_secret"
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
2. **Bind the chat once** — send the bot any message in Feishu. The server records that chat as the push target (persisted to disk, shared across processes), so it knows where to deliver cards.
|
|
257
|
+
|
|
258
|
+
3. **Round-trip** — the agent calls `interactive_feedback` → a card is pushed to Feishu → you reply in Feishu → your reply is routed back to the agent as the tool result. Reply from your phone; no IDE needed.
|
|
259
|
+
|
|
260
|
+
> **No idle-kill outside Cursor.** Only the Cursor extension keeps the server warm by polling. On every other host the server stays alive purely on the stdio connection: it exits on stdin EOF or parent-process death, and **never** on idle — and it always stays up while a feedback request is still waiting. So a reply that takes several minutes still makes it back.
|
|
261
|
+
|
|
233
262
|
## 🏗️ Architecture
|
|
234
263
|
|
|
235
264
|
```
|
package/README_CN.md
CHANGED
|
@@ -234,6 +234,35 @@ npm install -g cursor-feedback
|
|
|
234
234
|
|
|
235
235
|
> 优先级:**插件面板(填了凭证)> 这里的 env > 默认**。面板里填了 App ID/Secret 就以面板为准;没填则回退到 env。首次仍需在飞书里给机器人发一条消息完成绑定。
|
|
236
236
|
|
|
237
|
+
### 在任意 MCP host 上跑飞书往复
|
|
238
|
+
|
|
239
|
+
`cursor-feedback` **不绑定 Cursor**。整条飞书链路——长连接、卡片推送、回复路由——都跑在 MCP server 进程内,所以它能在**任意支持 MCP 的 Agent host** 上工作:Cursor、Claude Desktop、Cline、命令行 Agent,或你自建的 harness,都不需要侧边栏 / VS Code 面板。
|
|
240
|
+
|
|
241
|
+
三步打通往复:
|
|
242
|
+
|
|
243
|
+
1. **注册 MCP server**:在 host 的配置里加上,并把飞书凭证写进 `env`(与上面相同的 `FEISHU_APP_ID` / `FEISHU_APP_SECRET`;`FEISHU_ENABLED` / `FEISHU_ACK` 可选):
|
|
244
|
+
|
|
245
|
+
```json
|
|
246
|
+
{
|
|
247
|
+
"mcpServers": {
|
|
248
|
+
"cursor-feedback": {
|
|
249
|
+
"command": "npx",
|
|
250
|
+
"args": ["-y", "cursor-feedback@latest"],
|
|
251
|
+
"env": {
|
|
252
|
+
"FEISHU_APP_ID": "cli_xxxxxxxx",
|
|
253
|
+
"FEISHU_APP_SECRET": "your_app_secret"
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
2. **首次绑定会话**:在飞书里给机器人发任意一条消息。server 会把这个会话记为推送目标(落盘、跨进程共享),之后就知道往哪推卡片。
|
|
261
|
+
|
|
262
|
+
3. **往复**:Agent 调 `interactive_feedback` → 卡片推到飞书 → 你在飞书回复 → 回复作为工具结果路由回 Agent。手机上就能回,不用守着 IDE。
|
|
263
|
+
|
|
264
|
+
> **非 Cursor 环境不会因空闲被杀。** 只有 Cursor 插件会靠轮询给 server「续命」。在其他所有 host 上,server 完全依赖 stdio 连接存活:只在 stdin EOF 或父进程死亡时退出,**绝不**因空闲退出;而且只要还有反馈在等用户,任何环境都不会退。所以你回复花了好几分钟,结果照样能回到 Agent。
|
|
265
|
+
|
|
237
266
|
## 🏗️ 架构
|
|
238
267
|
|
|
239
268
|
```
|
package/dist/mcp-server.js
CHANGED
|
@@ -104,10 +104,6 @@ class McpFeedbackServer {
|
|
|
104
104
|
// 所属工作区(只在 AI 调用 feedback 时设置)
|
|
105
105
|
// 只有来自同一工作区的轮询才会更新活动时间
|
|
106
106
|
this.ownerWorkspace = null;
|
|
107
|
-
// Cursor 在 spawn 本 MCP server 时注入的「真实所属工作区」,精确指向发起对话的那个窗口。
|
|
108
|
-
// 这是可靠的归属信号:AI 传的 project_directory 可能被填成对话里聊到的另一个项目,
|
|
109
|
-
// 用它路由会把反馈发到错误窗口、当前窗口收不到(用户反馈的 bug)。拿不到时回退到 AI 传参。
|
|
110
|
-
this.realWorkspace = (process.env.WORKSPACE_FOLDER_PATHS || '').split(',')[0].trim() || null;
|
|
111
107
|
// Server 启动时间
|
|
112
108
|
this.startTime = Date.now();
|
|
113
109
|
// 最近一次活动时间(任意 HTTP 轮询 / MCP 调用都会刷新)
|
|
@@ -239,9 +235,10 @@ class McpFeedbackServer {
|
|
|
239
235
|
isError: true,
|
|
240
236
|
};
|
|
241
237
|
}
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
|
|
238
|
+
// 归属直接用 AI 每轮传入的 project_directory(精确指向当前发起对话的窗口)。
|
|
239
|
+
// 不读 WORKSPACE_FOLDER_PATHS:那是进程级固定值,多窗口共享 / 复用同一个 MCP 进程时只反映
|
|
240
|
+
// 「首个拉起进程的窗口」,会把反馈发错窗口、当前窗口收不到(用户反馈的 bug)。
|
|
241
|
+
const projectDir = args.project_directory || '';
|
|
245
242
|
// summary 支持别名 message
|
|
246
243
|
const summary = args?.summary || args?.message || '我已完成您的请求。';
|
|
247
244
|
// 超时时间优先级:环境变量 > 工具参数 > 默认值(300秒)
|
|
@@ -253,7 +250,7 @@ class McpFeedbackServer {
|
|
|
253
250
|
// 旧请求多半是对话被压缩 / 客户端取消后还卡在 await 的残留(要等 timeout 才自然结束),
|
|
254
251
|
// 不清理会让 pendingCount 虚高、全局视角误判「多个窗口在等」
|
|
255
252
|
//(即你看到的两个一模一样的 cursor-feedback-extension)。
|
|
256
|
-
this.cancelStalePending();
|
|
253
|
+
this.cancelStalePending(projectDir);
|
|
257
254
|
// AI 调用 feedback 时设置 ownerWorkspace(这是唯一正确的时机)
|
|
258
255
|
this.ownerWorkspace = this.normalizePath(projectDir);
|
|
259
256
|
debugLog(`Owner workspace set to: ${this.ownerWorkspace}`);
|
|
@@ -276,8 +273,20 @@ class McpFeedbackServer {
|
|
|
276
273
|
}
|
|
277
274
|
try {
|
|
278
275
|
// 等待用户反馈
|
|
279
|
-
const
|
|
280
|
-
|
|
276
|
+
const outcome = await this.waitForFeedback(requestId, timeout * 1000, projectDir);
|
|
277
|
+
// 被同窗口/同进程的新一轮请求取代:安静结束,绝不能重试(否则多 agent 互相取消、忙等刷屏)。
|
|
278
|
+
if (outcome.kind === 'superseded') {
|
|
279
|
+
debugLog(`Request ${requestId} superseded by a newer request; ending this turn quietly`);
|
|
280
|
+
return {
|
|
281
|
+
content: [
|
|
282
|
+
{
|
|
283
|
+
type: 'text',
|
|
284
|
+
text: '[SUPERSEDED] This feedback request was replaced by a newer interactive_feedback call (the same workspace started a new round, or another agent shares this MCP process). Do NOT call interactive_feedback again for this turn — just end your turn quietly. 【本反馈请求已被同窗口/同进程的新请求取代:不要再次调用 interactive_feedback,安静结束本轮,切勿重试。】',
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
if (outcome.kind === 'timeout') {
|
|
281
290
|
debugLog('Feedback wait window elapsed without user input');
|
|
282
291
|
// 超时续期开关:MCP_AUTO_RETRY=false 时关闭(超时即结束),默认开启(超时返回续期提醒)。
|
|
283
292
|
// 关键:这里【绝不能】说成 "cancelled"——那会让 AI 误以为用户主动取消而结束对话,
|
|
@@ -298,6 +307,7 @@ class McpFeedbackServer {
|
|
|
298
307
|
],
|
|
299
308
|
};
|
|
300
309
|
}
|
|
310
|
+
const result = outcome.data;
|
|
301
311
|
debugLog(`Received feedback: ${result.interactive_feedback?.substring(0, 100)}...`);
|
|
302
312
|
const contentItems = [];
|
|
303
313
|
// 构建反馈文本
|
|
@@ -535,35 +545,45 @@ class McpFeedbackServer {
|
|
|
535
545
|
req.end();
|
|
536
546
|
}
|
|
537
547
|
/**
|
|
538
|
-
*
|
|
539
|
-
*
|
|
540
|
-
*
|
|
548
|
+
* 作废「同一窗口(projectDir)」此前残留的未决反馈请求(每次新请求进来时调用)。
|
|
549
|
+
* 关键:绝不能动其他窗口 / agent 的 pending——多窗口或 AgentWindow 会复用同一个 MCP 进程,
|
|
550
|
+
* 误清它们会让对方被当成超时而立即重试,多个 agent 互相取消,形成「取消风暴」(高频刷调用)。
|
|
551
|
+
* 被作废者 resolve(null) → waitForFeedback 映射为 superseded,旧 await 安静结束、不重试;
|
|
552
|
+
* 其 finally 因 id 不匹配也不会误清新请求。
|
|
541
553
|
*/
|
|
542
|
-
cancelStalePending() {
|
|
554
|
+
cancelStalePending(projectDir) {
|
|
543
555
|
if (this.pendingRequests.size === 0)
|
|
544
556
|
return;
|
|
557
|
+
const owner = this.normalizePath(projectDir);
|
|
545
558
|
for (const [reqId, pending] of this.pendingRequests) {
|
|
559
|
+
if (this.normalizePath(pending.projectDir) !== owner)
|
|
560
|
+
continue;
|
|
546
561
|
clearTimeout(pending.timeout);
|
|
547
|
-
pending.resolve(null);
|
|
562
|
+
pending.resolve(null); // → superseded:旧 await 安静结束,不触发重试
|
|
548
563
|
this.feishu.clearPending(reqId);
|
|
564
|
+
this.pendingRequests.delete(reqId);
|
|
549
565
|
}
|
|
550
|
-
this.pendingRequests.clear();
|
|
551
566
|
}
|
|
552
567
|
/**
|
|
553
568
|
* 等待用户反馈
|
|
554
569
|
*/
|
|
555
|
-
waitForFeedback(requestId, timeoutMs) {
|
|
570
|
+
waitForFeedback(requestId, timeoutMs, projectDir) {
|
|
556
571
|
return new Promise((resolve) => {
|
|
557
572
|
const timeout = setTimeout(() => {
|
|
558
573
|
debugLog(`Request ${requestId} timed out`);
|
|
559
574
|
this.pendingRequests.delete(requestId);
|
|
560
575
|
// 飞书侧的清理统一交给 handleInteractiveFeedback 的 finally(clearPending),这里不再重复。
|
|
561
|
-
resolve(
|
|
576
|
+
resolve({ kind: 'timeout' });
|
|
562
577
|
}, timeoutMs);
|
|
563
578
|
this.pendingRequests.set(requestId, {
|
|
564
|
-
resolve
|
|
565
|
-
|
|
566
|
-
|
|
579
|
+
// resolve 包一层:沿用「外部 resolve(feedback) / resolve(null)」旧约定,
|
|
580
|
+
// 但 null 一律映射为 superseded(被同进程新请求取代),绝不再走「超时重试」路径。
|
|
581
|
+
resolve: (value) => {
|
|
582
|
+
resolve(value === null ? { kind: 'superseded' } : { kind: 'feedback', data: value });
|
|
583
|
+
},
|
|
584
|
+
reject: () => resolve({ kind: 'superseded' }),
|
|
585
|
+
timeout,
|
|
586
|
+
projectDir,
|
|
567
587
|
});
|
|
568
588
|
// 抢跑兑现:用户在空窗期「无主」发来的消息已被暂存,本轮 pending 一注册立即作为回复提交
|
|
569
589
|
this.tryConsumeStash(requestId);
|
|
@@ -761,7 +781,7 @@ class McpFeedbackServer {
|
|
|
761
781
|
// 别的活跃窗口对全端口的扫描虽刷 lastActivityTime,但 workspace 不匹配、不刷此字段,
|
|
762
782
|
// 故已关窗口的残留 server 不会被别人续命,可被 watchdog / 僵尸自检识别。
|
|
763
783
|
const ws = this.normalizePath(u.searchParams.get('workspace') || '');
|
|
764
|
-
if (ws &&
|
|
784
|
+
if (ws && ws === this.ownerWorkspace) {
|
|
765
785
|
this.lastOwnerPollTime = Date.now();
|
|
766
786
|
this.everOwnerPolled = true;
|
|
767
787
|
}
|
|
@@ -1088,10 +1108,13 @@ class McpFeedbackServer {
|
|
|
1088
1108
|
* Cursor 关闭后,stdin 的 EOF 在该中间层下不可靠,node 易变成孤儿进程残留;
|
|
1089
1109
|
* 叠加 HTTP server 是常驻 active handle,进程无法自然退出。
|
|
1090
1110
|
*
|
|
1091
|
-
*
|
|
1092
|
-
* 1) 父进程死亡(被 init/launchd 收养,ppid 变为 1)→
|
|
1093
|
-
* 2) 超过 IDLE_TIMEOUT
|
|
1094
|
-
*
|
|
1111
|
+
* 防线(按环境自适应):
|
|
1112
|
+
* 1) 父进程死亡(被 init/launchd 收养,ppid 变为 1)→ 立即退出。**所有环境通用**。
|
|
1113
|
+
* 2) 超过 IDLE_TIMEOUT 无活动 → 判定所有 Cursor 窗口已关闭 → 退出。
|
|
1114
|
+
* **仅 Cursor 插件环境(everOwnerPolled)且无 pending 等待时才退**:插件每秒轮询 HTTP 刷新活动时间、
|
|
1115
|
+
* 停了就是关窗。无插件 host(Claude Desktop / CLI / 其他 MCP 客户端 / fe-ai-flow)没有轮询心跳、
|
|
1116
|
+
* idle 是常态、不靠它退出、只走防线 1 + stdin EOF(见 main 的 stdin close/end/error)。
|
|
1117
|
+
* 3) 曾被本窗口插件轮询、但久未再收到(僵尸实例、只剩别的窗口扫端口续命)→ 退出。
|
|
1095
1118
|
*/
|
|
1096
1119
|
startWatchdog() {
|
|
1097
1120
|
const IDLE_TIMEOUT = process.env.MCP_FEEDBACK_IDLE_TIMEOUT
|
|
@@ -1104,9 +1127,16 @@ class McpFeedbackServer {
|
|
|
1104
1127
|
this.stop();
|
|
1105
1128
|
process.exit(0);
|
|
1106
1129
|
}
|
|
1107
|
-
// 防线 2:长时间无任何活动 = Cursor
|
|
1130
|
+
// 防线 2:长时间无任何活动 = Cursor 已全部关闭。
|
|
1131
|
+
// 仅对「Cursor 插件环境」(everOwnerPolled:曾被自己窗口插件轮询过)生效——插件每秒轮询、
|
|
1132
|
+
// 停了就是关窗。无插件 host(Claude Desktop / CLI / 其他 MCP 客户端 / fe-ai-flow 等)没有轮询心跳、
|
|
1133
|
+
// idle 是常态、绝不能据此退出:否则 AI 调 interactive_feedback 等用户飞书回复的那几分钟里进程会自杀、
|
|
1134
|
+
// 回复永远回不来。这类 host 完全靠 stdin close/end/error(见 main)+ 防线 1(ppid===1)退出。
|
|
1135
|
+
// 另外:只要还有 pending 反馈在等用户、任何环境都不 idle 退出(正在等用户,绝不能死)。
|
|
1108
1136
|
const idle = Date.now() - this.lastActivityTime;
|
|
1109
|
-
if (
|
|
1137
|
+
if (this.everOwnerPolled &&
|
|
1138
|
+
this.pendingRequests.size === 0 &&
|
|
1139
|
+
idle > IDLE_TIMEOUT) {
|
|
1110
1140
|
debugLog(`No activity for ${idle}ms (> ${IDLE_TIMEOUT}ms), assuming Cursor closed, exiting...`);
|
|
1111
1141
|
this.stop();
|
|
1112
1142
|
process.exit(0);
|
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.0.
|
|
5
|
+
"version": "2.0.5",
|
|
6
6
|
"icon": "icon.png",
|
|
7
7
|
"author": "jianger666",
|
|
8
8
|
"license": "MIT",
|
|
@@ -150,6 +150,7 @@
|
|
|
150
150
|
"prepublishOnly": "npm run check-changelog && npm run compile",
|
|
151
151
|
"check-changelog": "node scripts/check-changelog.js",
|
|
152
152
|
"release": "standard-version",
|
|
153
|
+
"release:patch": "standard-version --release-as patch",
|
|
153
154
|
"release:minor": "standard-version --release-as minor",
|
|
154
155
|
"release:major": "standard-version --release-as major",
|
|
155
156
|
"prepare": "husky"
|