cursor-feedback 2.4.0 → 2.5.0

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.
@@ -0,0 +1,9 @@
1
+ ---
2
+ description: 从对话反馈中积累的编码约定和偏好
3
+ alwaysApply: true
4
+ ---
5
+
6
+ # 编码约定
7
+
8
+ ## 发布流程
9
+ - 发版(npm / Open VSX / VS Code Marketplace)前必须经用户明确确认,绝不能改完代码直接发。改动完成后先提交 + 跑冒烟,向用户汇报并等确认后再走发版流程。
package/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
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.5.0](https://github.com/jianger666/cursor-feedback-extension/compare/v2.4.1...v2.5.0) (2026-07-06)
6
+
7
+
8
+ ### Features
9
+
10
+ * /help 完整用法说明;全角斜杠/归一化;持久化「发版需用户确认」规则 ([d345b55](https://github.com/jianger666/cursor-feedback-extension/commit/d345b55da22cad9e1a838999f0d58dd36ffa10b2))
11
+ * slash commands case-insensitive + /help command, malformed /model //cwd args no longer fall through to feedback routing ([a4bceeb](https://github.com/jianger666/cursor-feedback-extension/commit/a4bceebb588898c82acb797e8f5abefb7a457550))
12
+
13
+
14
+ ### Code Refactoring
15
+
16
+ * **cli:** /new 工作目录简化为「显式指定>主目录」与 IDE 窗口解耦;移除 /cwd;新增 /projects 查项目路径、/new 支持项目名匹配 ([0752a32](https://github.com/jianger666/cursor-feedback-extension/commit/0752a3232de0e712b977f9f01ba988613cd7597c))
17
+
18
+ ### [2.4.1](https://github.com/jianger666/cursor-feedback-extension/compare/v2.4.0...v2.4.1) (2026-07-06)
19
+
20
+
21
+ ### Bug Fixes
22
+
23
+ * **cli:** CLI 会话全局锁——多窗口/守护多实例并存时 /new /stop 跨实例一致 ([679c3ef](https://github.com/jianger666/cursor-feedback-extension/commit/679c3ef3ce43af768be939d4059a452cd3d20178))
24
+
5
25
  ## [2.4.0](https://github.com/jianger666/cursor-feedback-extension/compare/v2.3.1...v2.4.0) (2026-07-06)
6
26
 
7
27
 
package/README.md CHANGED
@@ -270,11 +270,13 @@ Send these directly in the bot chat:
270
270
 
271
271
  | Command | What it does |
272
272
  |---------|--------------|
273
- | `/new task description` | Launch a headless CLI session for the task (in the active window's workspace, or the `/cwd` default) |
274
- | `/new /abs/path task description` | Launch with an explicit working directory |
273
+ | `/new task description` | Launch a headless CLI session for the task (in your home directory by default) |
274
+ | `/new /abs/path task description` | Launch in an explicit working directory |
275
+ | `/new project-name task description` | Launch in a project Cursor has opened before (unique folder-name match) |
276
+ | `/projects` | List project paths Cursor has opened (to look up / copy into `/new`) |
275
277
  | `/stop` | Terminate the running CLI session |
276
278
  | `/model [modelId]` | Show / set the session model (persisted) |
277
- | `/cwd [dir]` | Show / set the default working directory (persisted) |
279
+ | `/help` | Show command usage |
278
280
 
279
281
  - Sessions run in **non-interactive mode** with `maxMode=false` force-written before spawn — on request-based plans each session costs exactly 1 request, never Max billing.
280
282
  - The launched agent talks to you through this extension's feedback cards: confirmations and progress reports are pushed to Feishu; just reply to the cards.
package/README_CN.md CHANGED
@@ -274,11 +274,13 @@ npm install -g cursor-feedback
274
274
 
275
275
  | 命令 | 作用 |
276
276
  |------|------|
277
- | `/new 任务描述` | 拉起一个 headless CLI 会话跑这个任务(默认在活跃窗口工作区 / `/cwd` 设置的目录) |
277
+ | `/new 任务描述` | 拉起一个 headless CLI 会话跑这个任务(默认在主目录) |
278
278
  | `/new /绝对路径 任务描述` | 指定工作目录拉起 |
279
+ | `/new 项目名 任务描述` | 在 Cursor 打开过的项目里拉起(目录名唯一匹配) |
280
+ | `/projects` | 列出 Cursor 打开过的项目路径(供查路径 / 复制给 `/new`) |
279
281
  | `/stop` | 终止运行中的 CLI 会话 |
280
282
  | `/model [模型id]` | 查看 / 设置会话模型(持久化) |
281
- | `/cwd [目录]` | 查看 / 设置默认工作目录(持久化) |
283
+ | `/help` | 查看命令用法 |
282
284
 
283
285
  - 会话以**非交互模式**运行,拉起前强制写入 `maxMode=false`——按次计费套餐固定只扣 1 次请求,永远不会走 Max 计费。
284
286
  - 拉起的 Agent 也通过本插件的反馈卡片与你沟通:需要确认、汇报进展都会推到飞书,直接回复卡片即可。
@@ -62,15 +62,113 @@ class CliLauncher {
62
62
  this.taskBrief = '';
63
63
  this.stopRequested = false;
64
64
  }
65
+ /* ---------- 磁盘全局会话锁 ----------
66
+ * 「同时只跑一个 CLI 会话」必须全局生效:多窗口 + 守护进程是多实例并存,
67
+ * 飞书把 /new、/stop 推给哪个实例是不确定的(长连接负载均衡)。只看本实例的
68
+ * child 会被击穿:A 拉了会话,下一条 /new 推给 B,B 以为空闲又拉一个。
69
+ * 锁文件记录会话进程 pid,任何实例都能据此判忙 / 跨实例终止;
70
+ * 持锁进程崩溃后锁自动失效(pid 探活),不会死锁。 */
71
+ lockPath() {
72
+ return path.join(os.homedir(), '.cursor-feedback', 'cli-session.lock');
73
+ }
74
+ readLock() {
75
+ try {
76
+ const j = JSON.parse(fs.readFileSync(this.lockPath(), 'utf-8'));
77
+ return typeof j.pid === 'number' ? j : null;
78
+ }
79
+ catch {
80
+ return null;
81
+ }
82
+ }
83
+ static pidAlive(pid) {
84
+ if (!pid || pid <= 0)
85
+ return false;
86
+ try {
87
+ process.kill(pid, 0);
88
+ return true;
89
+ }
90
+ catch {
91
+ return false;
92
+ }
93
+ }
94
+ /**
95
+ * pid 是否确实是 cursor-agent 会话进程。防 pid 复用:锁残留(极端崩溃)后系统把同号
96
+ * pid 分给了无关进程,跨实例 /stop 若不校验会误杀无辜。校验失败按「不是会话」处理。
97
+ */
98
+ static pidLooksLikeAgent(pid) {
99
+ try {
100
+ if (process.platform === 'win32') {
101
+ const out = (0, child_process_1.execFileSync)('tasklist', ['/FI', `PID eq ${pid}`, '/FO', 'CSV', '/NH'], {
102
+ encoding: 'utf-8',
103
+ });
104
+ return /cursor-agent|node|cmd\.exe/i.test(out);
105
+ }
106
+ const out = (0, child_process_1.execFileSync)('ps', ['-o', 'command=', '-p', String(pid)], { encoding: 'utf-8' });
107
+ return /cursor-agent/i.test(out);
108
+ }
109
+ catch {
110
+ return false;
111
+ }
112
+ }
113
+ /** 当前全局是否有活跃会话(锁存在且会话进程活着;死锁视为无会话) */
114
+ aliveLock() {
115
+ const lock = this.readLock();
116
+ if (!lock)
117
+ return null;
118
+ // pid=0 = 另一个实例刚抢到锁、spawn 还没完成:以它的实例进程判活,避免这瞬间被当死锁误清
119
+ const alive = lock.pid > 0
120
+ ? CliLauncher.pidAlive(lock.pid)
121
+ : CliLauncher.pidAlive(lock.ownerPid);
122
+ if (!alive) {
123
+ // 持锁会话已死(实例崩溃 / 强杀),清掉残锁
124
+ fs.rmSync(this.lockPath(), { force: true });
125
+ return null;
126
+ }
127
+ return lock;
128
+ }
129
+ /** 原子抢锁:wx 独占创建。返回是否抢到(false = 别的实例刚抢先) */
130
+ tryAcquireLock(task) {
131
+ // 先清死锁,再独占创建(两个实例同时 /new 时只有一个 wx 成功)
132
+ this.aliveLock();
133
+ const lock = { pid: 0, task, startedAt: Date.now(), ownerPid: process.pid };
134
+ try {
135
+ fs.mkdirSync(path.dirname(this.lockPath()), { recursive: true });
136
+ fs.writeFileSync(this.lockPath(), JSON.stringify(lock), { flag: 'wx' });
137
+ return true;
138
+ }
139
+ catch {
140
+ return false;
141
+ }
142
+ }
143
+ updateLockPid(pid) {
144
+ const lock = this.readLock();
145
+ if (lock && lock.ownerPid === process.pid) {
146
+ fs.writeFileSync(this.lockPath(), JSON.stringify({ ...lock, pid }));
147
+ }
148
+ }
149
+ releaseLock() {
150
+ const lock = this.readLock();
151
+ if (lock && lock.ownerPid === process.pid) {
152
+ fs.rmSync(this.lockPath(), { force: true });
153
+ }
154
+ }
155
+ /** 全局视角:是否有会话在跑(本实例或其他实例拉起的都算) */
65
156
  isRunning() {
66
- return this.child !== null;
157
+ return this.child !== null || this.aliveLock() !== null;
67
158
  }
68
- /** 正在运行的会话描述(用于「已有会话在跑」的回执) */
159
+ /** 正在运行的会话描述(用于「已有会话在跑」的回执;含其他实例拉起的会话) */
69
160
  describe() {
70
- if (!this.child)
71
- return '';
72
- const mins = Math.round((Date.now() - this.startedAt) / 60000);
73
- return `「${this.taskBrief}」(已运行 ${mins} 分钟)`;
161
+ if (this.child) {
162
+ const mins = Math.round((Date.now() - this.startedAt) / 60000);
163
+ return `「${this.taskBrief}」(已运行 ${mins} 分钟)`;
164
+ }
165
+ const lock = this.aliveLock();
166
+ if (lock) {
167
+ const brief = lock.task.length > 40 ? lock.task.slice(0, 40) + '…' : lock.task;
168
+ const mins = Math.round((Date.now() - lock.startedAt) / 60000);
169
+ return `「${brief}」(已运行 ${mins} 分钟,由另一个窗口实例托管)`;
170
+ }
171
+ return '';
74
172
  }
75
173
  settingsPath() {
76
174
  return path.join(os.homedir(), '.cursor-feedback', 'cli.json');
@@ -96,12 +194,61 @@ class CliLauncher {
96
194
  this.readSettings().model ||
97
195
  CliLauncher.DEFAULT_MODEL);
98
196
  }
99
- /** /cwd 设置的默认工作目录(存在才返回) */
100
- defaultCwd() {
101
- const d = this.readSettings().defaultCwd;
102
- if (d && fs.existsSync(d))
103
- return d;
104
- return null;
197
+ /** Cursor IDE 的 storage.json 路径(平台相关) */
198
+ static storageJsonPath() {
199
+ if (process.platform === 'darwin') {
200
+ return path.join(os.homedir(), 'Library', 'Application Support', 'Cursor', 'User', 'globalStorage', 'storage.json');
201
+ }
202
+ if (process.platform === 'win32') {
203
+ return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'Cursor', 'User', 'globalStorage', 'storage.json');
204
+ }
205
+ return path.join(os.homedir(), '.config', 'Cursor', 'User', 'globalStorage', 'storage.json');
206
+ }
207
+ /**
208
+ * 用户在 Cursor IDE 里打开过的项目目录(供手机上查路径 / 按项目名拉起会话)。
209
+ * 数据源是 IDE 的 storage.json:backupWorkspaces.folders 是当前/最近打开的窗口(排前面),
210
+ * profileAssociations.workspaces 是历史打开过的全部工作区。只返回仍然存在的目录。
211
+ */
212
+ listProjects() {
213
+ let raw;
214
+ try {
215
+ raw = JSON.parse(fs.readFileSync(CliLauncher.storageJsonPath(), 'utf-8'));
216
+ }
217
+ catch {
218
+ return [];
219
+ }
220
+ const uris = [
221
+ ...(raw.backupWorkspaces?.folders || []).map((f) => f.folderUri || ''),
222
+ ...Object.keys(raw.profileAssociations?.workspaces || {}),
223
+ ];
224
+ const seen = new Set();
225
+ const result = [];
226
+ for (const uri of uris) {
227
+ if (!uri.startsWith('file://'))
228
+ continue;
229
+ let dir;
230
+ try {
231
+ dir = decodeURIComponent(uri.replace(/^file:\/\//, ''));
232
+ }
233
+ catch {
234
+ continue;
235
+ }
236
+ // Windows 的 URI 形如 file:///c%3A/... → 去掉开头多余的 /
237
+ if (process.platform === 'win32' && /^\/[a-zA-Z]:/.test(dir))
238
+ dir = dir.slice(1);
239
+ const key = dir.toLowerCase();
240
+ if (seen.has(key))
241
+ continue;
242
+ seen.add(key);
243
+ try {
244
+ if (fs.statSync(dir).isDirectory())
245
+ result.push(dir);
246
+ }
247
+ catch {
248
+ // 已删除的目录不展示
249
+ }
250
+ }
251
+ return result;
105
252
  }
106
253
  /**
107
254
  * 定位 cursor-agent 可执行文件。安装脚本默认放在 ~/.local/bin(mac/linux 是
@@ -182,17 +329,24 @@ class CliLauncher {
182
329
  * @returns 启动失败时返回错误说明字符串;成功返回 null,结果经 onDone 回调
183
330
  */
184
331
  start(task, cwd, onDone) {
185
- if (this.child)
332
+ if (this.isRunning())
186
333
  return '已有一个 CLI 会话在运行:' + this.describe();
334
+ // 原子抢全局锁:两个实例同时收到 /new 时只有一个能抢到
335
+ if (!this.tryAcquireLock(task)) {
336
+ return '已有一个 CLI 会话在运行:' + (this.describe() || '(另一个窗口实例刚刚抢先拉起)');
337
+ }
187
338
  try {
188
339
  this.ensureMaxModeOff();
189
340
  }
190
341
  catch (e) {
342
+ this.releaseLock();
191
343
  return '写入 cli-config.json 失败:' + e;
192
344
  }
193
345
  const bin = this.findBinary();
194
- if (!bin)
346
+ if (!bin) {
347
+ this.releaseLock();
195
348
  return '找不到 cursor-agent,可通过环境变量 CURSOR_AGENT_PATH 指定路径。';
349
+ }
196
350
  const workDir = fs.existsSync(cwd) ? cwd : os.homedir();
197
351
  const prompt = this.buildPrompt(task);
198
352
  let stdout = '';
@@ -225,12 +379,14 @@ class CliLauncher {
225
379
  });
226
380
  }
227
381
  catch (e) {
382
+ this.releaseLock();
228
383
  return '拉起 cursor-agent 失败:' + e;
229
384
  }
230
385
  this.child = child;
231
386
  this.startedAt = Date.now();
232
387
  this.taskBrief = task.length > 40 ? task.slice(0, 40) + '…' : task;
233
388
  this.stopRequested = false;
389
+ this.updateLockPid(child.pid || 0);
234
390
  clog(`CLI 会话已拉起: pid=${child.pid} cwd=${workDir} model=${this.model()}`);
235
391
  let timedOut = false;
236
392
  const killTimer = setTimeout(() => {
@@ -243,8 +399,12 @@ class CliLauncher {
243
399
  const finish = (code) => {
244
400
  clearTimeout(killTimer);
245
401
  const elapsedMs = Date.now() - this.startedAt;
246
- const stopped = this.stopRequested;
402
+ // 跨实例 /stop 会在锁上打 stopRequested 标记再杀进程,这里一并算「主动终止」
403
+ const lockAtFinish = this.readLock();
404
+ const stopped = this.stopRequested ||
405
+ !!(lockAtFinish && lockAtFinish.ownerPid === process.pid && lockAtFinish.stopRequested);
247
406
  this.child = null;
407
+ this.releaseLock();
248
408
  clog(`CLI 会话结束: code=${code} elapsed=${Math.round(elapsedMs / 1000)}s timedOut=${timedOut} stopped=${stopped}`);
249
409
  onDone({
250
410
  code,
@@ -263,20 +423,60 @@ class CliLauncher {
263
423
  child.on('close', (code) => finish(code));
264
424
  return null;
265
425
  }
266
- /** 用户 /stop 主动终止会话。返回是否有会话被终止。 */
426
+ /**
427
+ * 用户 /stop 主动终止会话。返回是否有会话被终止。
428
+ * 跨实例:会话可能由另一个窗口实例托管(飞书把 /stop 推给了不同实例),
429
+ * 此时按锁里的 pid 直接终止会话进程,托管实例的 close 回调会自然收尾并回执。
430
+ */
267
431
  stop() {
268
- if (!this.child)
269
- return false;
270
- this.stopRequested = true;
271
- CliLauncher.killTree(this.child, 'SIGTERM');
272
- // SIGTERM 5s 内没退出则补 SIGKILL
273
- const child = this.child;
274
- setTimeout(() => {
275
- if (this.child === child) {
276
- CliLauncher.killTree(child, 'SIGKILL');
432
+ if (this.child) {
433
+ this.stopRequested = true;
434
+ CliLauncher.killTree(this.child, 'SIGTERM');
435
+ // SIGTERM 5s 内没退出则补 SIGKILL
436
+ const child = this.child;
437
+ setTimeout(() => {
438
+ if (this.child === child) {
439
+ CliLauncher.killTree(child, 'SIGKILL');
440
+ }
441
+ }, 5000);
442
+ return true;
443
+ }
444
+ const lock = this.aliveLock();
445
+ if (lock && lock.pid > 0) {
446
+ if (!CliLauncher.pidLooksLikeAgent(lock.pid)) {
447
+ // pid 已被无关进程复用(残锁):清锁但绝不误杀
448
+ clog(`锁 pid=${lock.pid} 不是 cursor-agent(已被复用),清除残锁`);
449
+ fs.rmSync(this.lockPath(), { force: true });
450
+ return false;
451
+ }
452
+ clog(`跨实例终止 CLI 会话: pid=${lock.pid}(由 ${lock.ownerPid} 托管)`);
453
+ try {
454
+ // 先在锁上打「主动终止」标记,托管实例收尾时据此报「已终止」而非「异常退出」
455
+ fs.writeFileSync(this.lockPath(), JSON.stringify({ ...lock, stopRequested: true }));
277
456
  }
278
- }, 5000);
279
- return true;
457
+ catch {
458
+ // 标记失败只影响收尾文案
459
+ }
460
+ try {
461
+ // 会话进程是托管实例 detached 出来的进程组组长,杀整组
462
+ if (process.platform === 'win32') {
463
+ (0, child_process_1.spawn)('taskkill', ['/pid', String(lock.pid), '/T', '/F'], { stdio: 'ignore' });
464
+ }
465
+ else {
466
+ try {
467
+ process.kill(-lock.pid, 'SIGTERM');
468
+ }
469
+ catch {
470
+ process.kill(lock.pid, 'SIGTERM');
471
+ }
472
+ }
473
+ }
474
+ catch {
475
+ // 进程刚好自己结束了
476
+ }
477
+ return true;
478
+ }
479
+ return false;
280
480
  }
281
481
  /**
282
482
  * 终止整个进程树:POSIX 杀进程组(detached spawn 后 child.pid 即组长),
@@ -396,7 +396,8 @@ class McpFeedbackServer {
396
396
  if (!text && images.length === 0 && files.length === 0)
397
397
  return;
398
398
  // 斜杠命令优先于反馈路由:用户显式输入命令时不应被当成对某张卡片的回复或排队消息。
399
- if (/^\/(new|stop|model|cwd)\b/.test(text) && this.handleCliCommand(text, chatId))
399
+ // 大小写不敏感:手机输入法常自动把句首字母大写(/New),必须容错。
400
+ if (/^\s*[//](new|stop|model|cwd|help|projects)\b/i.test(text) && this.handleCliCommand(text, chatId))
400
401
  return;
401
402
  if (parentId) {
402
403
  // 用户「回复」了某条卡片 → 用 parent_id 精确路由
@@ -494,13 +495,51 @@ class McpFeedbackServer {
494
495
  }
495
496
  /**
496
497
  * 处理飞书斜杠命令。返回是否已消费该消息。
497
- * - /new [工作目录] 任务描述:拉起一个 headless CLI 会话(非交互 + maxMode=false,扣 1 次请求)
498
+ * - /new [目录或项目名] 任务描述:拉起 headless CLI 会话(非交互 + maxMode=false,扣 1 次请求)。
499
+ * 工作目录刻意保持简单:显式指定 > 主目录,与当前开着哪些 IDE 窗口无关。
500
+ * - /projects:列出 Cursor 打开过的项目路径(供手机上查路径 / 复制给 /new)
498
501
  * - /stop:终止运行中的 CLI 会话
499
502
  * - /model [模型id]:查看 / 设置 CLI 会话模型(持久化;无论选什么模型都强制 maxMode=false)
500
- * - /cwd [目录]:查看 / 设置默认工作目录(持久化;IDE 不开时 /new 用它)
501
503
  */
502
- handleCliCommand(text, chatId) {
503
- if (/^\/stop\b/.test(text)) {
504
+ handleCliCommand(rawText, chatId) {
505
+ // 输入容错:首尾空格(飞书入站已 trim,这里兜底;trim 同样覆盖全角空格 U+3000);
506
+ // 全角斜杠「/」归一化(中文输入法常见);命令词大小写不敏感(手机输入法自动把句首
507
+ // 大写成 /New)。任务正文原样保留。
508
+ const text = rawText.trim().replace(/^//, '/');
509
+ if (/^\/help\b/i.test(text)) {
510
+ this.feishu.replyText(chatId, '📖 命令一览(大小写、首尾空格都不敏感)\n\n' +
511
+ '🚀 /new 任务描述\n' +
512
+ '拉起一个全新的 AI 会话跑任务,电脑不用开着 Cursor(需已开启常驻服务)。\n' +
513
+ '· 默认工作目录是主目录;可在任务前指定项目:\n' +
514
+ ' /new /Users/me/proj 帮我修下测试\n' +
515
+ ' /new crm-web 帮我修下测试(项目名唯一时自动匹配到完整路径)\n' +
516
+ '· 非交互模式 + 强制 maxMode=false,每次会话固定只扣 1 次请求\n' +
517
+ '· AI 需要沟通时会发反馈卡片,直接回复即可;同时只能跑一个会话\n\n' +
518
+ '📁 /projects\n' +
519
+ '列出 Cursor 打开过的项目路径(查路径 / 复制给 /new,也可直接用项目名)\n\n' +
520
+ '🛑 /stop\n' +
521
+ '终止运行中的 CLI 会话(任何窗口拉起的都能停)\n\n' +
522
+ '🧠 /model [模型id]\n' +
523
+ '查看 / 设置会话模型(持久化;无论选什么模型都不会走 Max 计费)\n\n' +
524
+ '❓ /help\n' +
525
+ '看这份帮助\n\n' +
526
+ '💬 不带斜杠的消息照常作为反馈:回复某张卡片就送达那个请求,直接发送则给当前等待中的 AI。');
527
+ return true;
528
+ }
529
+ if (/^\/projects\b/i.test(text)) {
530
+ const projects = this.cliLauncher.listProjects();
531
+ if (!projects.length) {
532
+ this.feishu.replyText(chatId, '没有找到 Cursor 打开过的项目记录。可以直接用绝对路径:/new /绝对路径 任务描述');
533
+ }
534
+ else {
535
+ const lines = projects.slice(0, 20).map((p) => `· ${path.basename(p)} — ${p}`);
536
+ this.feishu.replyText(chatId, `Cursor 打开过的项目(共 ${projects.length} 个${projects.length > 20 ? ',只列最近 20 个' : ''}):\n` +
537
+ lines.join('\n') +
538
+ '\n\n用法:/new 项目名 任务描述(项目名唯一时直接匹配),或 /new 完整路径 任务描述');
539
+ }
540
+ return true;
541
+ }
542
+ if (/^\/stop\b/i.test(text)) {
504
543
  if (this.cliLauncher.stop()) {
505
544
  this.feishu.replyText(chatId, '🛑 正在终止 CLI 会话…结束后我会再发一条收尾消息。');
506
545
  }
@@ -509,7 +548,7 @@ class McpFeedbackServer {
509
548
  }
510
549
  return true;
511
550
  }
512
- const mModel = text.match(/^\/model(?:\s+(\S+))?\s*$/);
551
+ const mModel = text.match(/^\/model(?:\s+(\S+))?\s*$/i);
513
552
  if (mModel) {
514
553
  if (mModel[1]) {
515
554
  this.cliLauncher.writeSettings({ model: mModel[1] });
@@ -520,52 +559,59 @@ class McpFeedbackServer {
520
559
  }
521
560
  return true;
522
561
  }
523
- const mCwd = text.match(/^\/cwd(?:\s+(\S+))?\s*$/);
524
- if (mCwd) {
525
- if (mCwd[1]) {
526
- const dir = this.expandDirToken(mCwd[1]);
527
- if (!dir) {
528
- this.feishu.replyText(chatId, `❌ 目录不存在或不是有效路径:${mCwd[1]}`);
529
- }
530
- else {
531
- this.cliLauncher.writeSettings({ defaultCwd: dir });
532
- this.feishu.replyText(chatId, `✅ 默认工作目录已设为 ${dir}\n之后 /new 不带路径时都用它。`);
533
- }
534
- }
535
- else {
536
- const cur = this.cliLauncher.defaultCwd();
537
- this.feishu.replyText(chatId, `当前默认工作目录:${cur || '(未设置,回退到活跃窗口工作区或主目录)'}\n设置:/cwd 绝对路径`);
538
- }
562
+ // /cwd 已移除(工作目录简化为「/new 里显式指定,否则主目录」),给老用户指个路
563
+ if (/^\/cwd\b/i.test(text)) {
564
+ this.feishu.replyText(chatId, '/cwd 已移除。现在直接在 /new 里指定:/new 项目名或/绝对路径 任务描述;不指定就在主目录跑。\n用 /projects 可以查项目路径。');
565
+ return true;
566
+ }
567
+ // /model 带了多余参数(如 /model a b):不落入反馈路由(用户明显在敲命令),回用法提示
568
+ if (/^\/model\b/i.test(text)) {
569
+ this.feishu.replyText(chatId, '参数不对。用法:/model 模型id(只接受一个参数,留空为查看当前模型)');
539
570
  return true;
540
571
  }
541
- const m = text.match(/^\/new\s*([\s\S]*)$/);
572
+ const m = text.match(/^\/new\b\s*([\s\S]*)$/i);
542
573
  if (!m)
543
574
  return false;
544
575
  let task = (m[1] || '').trim();
545
576
  if (!task) {
546
- this.feishu.replyText(chatId, '用法:/new 任务描述\n' +
547
- '可选在任务前带一个已存在的目录作为工作目录:\n' +
577
+ this.feishu.replyText(chatId, '用法:/new 任务描述(默认在主目录跑)\n' +
578
+ '可选在任务前带工作目录(绝对路径或项目名):\n' +
548
579
  '/new /Users/me/proj 帮我看下测试为什么挂了\n' +
549
- '相关命令:/cwd 设默认目录、/model 设模型、/stop 终止会话。');
580
+ '/new my-blog 帮我看下测试为什么挂了\n' +
581
+ '相关命令:/projects 查项目路径、/model 设模型、/stop 终止会话。');
550
582
  return true;
551
583
  }
552
584
  if (this.cliLauncher.isRunning()) {
553
585
  this.feishu.replyText(chatId, '已有一个 CLI 会话在运行:' + this.cliLauncher.describe() + '\n发 /stop 可先终止它。');
554
586
  return true;
555
587
  }
556
- // 工作目录优先级:命令里显式路径 > /cwd 持久化默认目录 > 本实例归属窗口 > 主目录。
557
- // 注意 ownerWorkspace 是小写归一化路径,macOS 默认大小写不敏感文件系统下可直接使用。
558
- let cwd = this.cliLauncher.defaultCwd() ||
559
- (this.ownerWorkspace && fs.existsSync(this.ownerWorkspace)
560
- ? this.ownerWorkspace
561
- : os.homedir());
562
- const explicitDir = this.expandDirToken(task.split(/\s+/)[0]);
588
+ // 工作目录刻意保持简单、与 IDE 窗口无关:命令里显式指定(路径或项目名)> 主目录。
589
+ let cwd = os.homedir();
590
+ const firstToken = task.split(/\s+/)[0];
591
+ const explicitDir = this.expandDirToken(firstToken);
563
592
  if (explicitDir) {
564
593
  cwd = explicitDir;
565
- task = task.slice(task.split(/\s+/)[0].length).trim();
594
+ task = task.slice(firstToken.length).trim();
595
+ }
596
+ else {
597
+ // 首词不是路径 → 试着当项目名匹配(Cursor 打开过的项目,按目录名精确匹配、忽略大小写)。
598
+ // 唯一命中才采用;多个重名让用户用完整路径消歧;没命中就当作任务正文的第一个词。
599
+ const matches = this.cliLauncher
600
+ .listProjects()
601
+ .filter((p) => path.basename(p).toLowerCase() === firstToken.toLowerCase());
602
+ if (matches.length === 1) {
603
+ cwd = matches[0];
604
+ task = task.slice(firstToken.length).trim();
605
+ }
606
+ else if (matches.length > 1) {
607
+ this.feishu.replyText(chatId, `有 ${matches.length} 个同名项目「${firstToken}」:\n` +
608
+ matches.map((p) => `· ${p}`).join('\n') +
609
+ '\n请用完整路径:/new 完整路径 任务描述');
610
+ return true;
611
+ }
566
612
  }
567
613
  if (!task) {
568
- this.feishu.replyText(chatId, '只给了目录没给任务。用法:/new [工作目录] 任务描述');
614
+ this.feishu.replyText(chatId, '只给了目录没给任务。用法:/new [目录或项目名] 任务描述');
569
615
  return true;
570
616
  }
571
617
  const err = this.cliLauncher.start(task, cwd, (result) => this.onCliSessionDone(result, chatId));
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.4.0",
5
+ "version": "2.5.0",
6
6
  "icon": "icon.png",
7
7
  "author": "jianger666",
8
8
  "license": "MIT",