cursor-feedback 2.4.1 → 2.6.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.6.0](https://github.com/jianger666/cursor-feedback-extension/compare/v2.5.0...v2.6.0) (2026-07-06)
6
+
7
+
8
+ ### Features
9
+
10
+ * **diag:** server 日志落盘(按天切割/7天保留)+ 诊断包导出(端点/面板按钮/密钥脱敏/无server降级);/help 去掉计费说明 ([91a0d67](https://github.com/jianger666/cursor-feedback-extension/commit/91a0d67346a207afdab8d2df02278c40674bfbb5))
11
+
12
+ ## [2.5.0](https://github.com/jianger666/cursor-feedback-extension/compare/v2.4.1...v2.5.0) (2026-07-06)
13
+
14
+
15
+ ### Features
16
+
17
+ * /help 完整用法说明;全角斜杠/归一化;持久化「发版需用户确认」规则 ([d345b55](https://github.com/jianger666/cursor-feedback-extension/commit/d345b55da22cad9e1a838999f0d58dd36ffa10b2))
18
+ * 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))
19
+
20
+
21
+ ### Code Refactoring
22
+
23
+ * **cli:** /new 工作目录简化为「显式指定>主目录」与 IDE 窗口解耦;移除 /cwd;新增 /projects 查项目路径、/new 支持项目名匹配 ([0752a32](https://github.com/jianger666/cursor-feedback-extension/commit/0752a3232de0e712b977f9f01ba988613cd7597c))
24
+
5
25
  ### [2.4.1](https://github.com/jianger666/cursor-feedback-extension/compare/v2.4.0...v2.4.1) (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.
@@ -289,6 +291,10 @@ Normally the server process is spawned by Cursor, so closing Cursor drops the Fe
289
291
  - **How**: the current package is copied to `~/.cursor-feedback/daemon/app` (self-contained, immune to npx cache cleanup); registered as a launchd agent on macOS (auto-restart on crash) or a logon scheduled task on Windows.
290
292
  - **Keep-awake**: while on AC power the daemon prevents system sleep (macOS `caffeinate -s`; Windows power assertion). On battery it does nothing. Lock screen never affects background processes.
291
293
  - **Typical use**: leave the machine plugged in and locked after work; `/new` from your phone anytime.
294
+
295
+ ### Diagnostics export
296
+
297
+ Server logs are also written to `~/.cursor-feedback/logs/` (daily files, 7-day retention). Hit **Export diagnostics** in the extension's notification settings to save a single report — recent logs, environment and sanitized config (secrets are masked) — to attach to bug reports. Works even when no server is running (falls back to reading log files directly).
292
298
  - After upgrading the extension, toggle the switch off/on once to refresh the daemon's copy.
293
299
 
294
300
  ## 🏗️ Architecture
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 也通过本插件的反馈卡片与你沟通:需要确认、汇报进展都会推到飞书,直接回复卡片即可。
@@ -293,6 +295,10 @@ npm install -g cursor-feedback
293
295
  - **实现**:把当前包完整拷贝到 `~/.cursor-feedback/daemon/app`(自包含,不怕 npx 缓存被清),macOS 注册 launchd(崩溃自动拉起),Windows 注册登录计划任务。
294
296
  - **防睡眠**:守护进程在接电源时自动阻止系统睡眠(macOS `caffeinate -s`;Windows 电源断言),电池供电不生效、不偷耗电。锁屏本来就不影响后台进程。
295
297
  - **典型用法**:下班插着电源锁屏走人,手机飞书随时 `/new`。
298
+
299
+ ### 诊断包导出
300
+
301
+ server 日志会同时落盘到 `~/.cursor-feedback/logs/`(按天切割,保留 7 天)。在插件「通知设置」里点**导出诊断包**,一键保存包含最近日志、环境信息和脱敏配置(密钥打码)的报告文件,报 bug 时直接附上。即使 server 全挂了也能导(降级为直接读日志文件)。
296
302
  - 升级插件版本后重新开关一次即可更新守护进程里的拷贝。
297
303
 
298
304
  ## 🏗️ 架构
@@ -52,8 +52,10 @@ const child_process_1 = require("child_process");
52
52
  const fs = __importStar(require("fs"));
53
53
  const os = __importStar(require("os"));
54
54
  const path = __importStar(require("path"));
55
+ const logger_js_1 = require("./logger.js");
55
56
  function clog(message) {
56
57
  console.error(`[${new Date().toISOString()}] [cli-launcher] ${message}`);
58
+ (0, logger_js_1.fileLog)('cli-launcher', message);
57
59
  }
58
60
  class CliLauncher {
59
61
  constructor() {
@@ -194,12 +196,61 @@ class CliLauncher {
194
196
  this.readSettings().model ||
195
197
  CliLauncher.DEFAULT_MODEL);
196
198
  }
197
- /** /cwd 设置的默认工作目录(存在才返回) */
198
- defaultCwd() {
199
- const d = this.readSettings().defaultCwd;
200
- if (d && fs.existsSync(d))
201
- return d;
202
- return null;
199
+ /** Cursor IDE 的 storage.json 路径(平台相关) */
200
+ static storageJsonPath() {
201
+ if (process.platform === 'darwin') {
202
+ return path.join(os.homedir(), 'Library', 'Application Support', 'Cursor', 'User', 'globalStorage', 'storage.json');
203
+ }
204
+ if (process.platform === 'win32') {
205
+ return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'Cursor', 'User', 'globalStorage', 'storage.json');
206
+ }
207
+ return path.join(os.homedir(), '.config', 'Cursor', 'User', 'globalStorage', 'storage.json');
208
+ }
209
+ /**
210
+ * 用户在 Cursor IDE 里打开过的项目目录(供手机上查路径 / 按项目名拉起会话)。
211
+ * 数据源是 IDE 的 storage.json:backupWorkspaces.folders 是当前/最近打开的窗口(排前面),
212
+ * profileAssociations.workspaces 是历史打开过的全部工作区。只返回仍然存在的目录。
213
+ */
214
+ listProjects() {
215
+ let raw;
216
+ try {
217
+ raw = JSON.parse(fs.readFileSync(CliLauncher.storageJsonPath(), 'utf-8'));
218
+ }
219
+ catch {
220
+ return [];
221
+ }
222
+ const uris = [
223
+ ...(raw.backupWorkspaces?.folders || []).map((f) => f.folderUri || ''),
224
+ ...Object.keys(raw.profileAssociations?.workspaces || {}),
225
+ ];
226
+ const seen = new Set();
227
+ const result = [];
228
+ for (const uri of uris) {
229
+ if (!uri.startsWith('file://'))
230
+ continue;
231
+ let dir;
232
+ try {
233
+ dir = decodeURIComponent(uri.replace(/^file:\/\//, ''));
234
+ }
235
+ catch {
236
+ continue;
237
+ }
238
+ // Windows 的 URI 形如 file:///c%3A/... → 去掉开头多余的 /
239
+ if (process.platform === 'win32' && /^\/[a-zA-Z]:/.test(dir))
240
+ dir = dir.slice(1);
241
+ const key = dir.toLowerCase();
242
+ if (seen.has(key))
243
+ continue;
244
+ seen.add(key);
245
+ try {
246
+ if (fs.statSync(dir).isDirectory())
247
+ result.push(dir);
248
+ }
249
+ catch {
250
+ // 已删除的目录不展示
251
+ }
252
+ }
253
+ return result;
203
254
  }
204
255
  /**
205
256
  * 定位 cursor-agent 可执行文件。安装脚本默认放在 ~/.local/bin(mac/linux 是
package/dist/extension.js CHANGED
@@ -38,6 +38,7 @@ exports.deactivate = deactivate;
38
38
  const vscode = __importStar(require("vscode"));
39
39
  const http = __importStar(require("http"));
40
40
  const fs = __importStar(require("fs"));
41
+ const os = __importStar(require("os"));
41
42
  const path = __importStar(require("path"));
42
43
  const child_process_1 = require("child_process");
43
44
  const i18n_1 = require("./i18n");
@@ -303,6 +304,9 @@ class FeedbackViewProvider {
303
304
  case 'toggleDaemon':
304
305
  await this._handleToggleDaemon(!!data.payload?.enabled);
305
306
  break;
307
+ case 'exportDiagnostics':
308
+ await this._handleExportDiagnostics();
309
+ break;
306
310
  case 'feishuRegisterStart':
307
311
  await this._handleFeishuRegisterStart();
308
312
  break;
@@ -1392,6 +1396,63 @@ class FeedbackViewProvider {
1392
1396
  });
1393
1397
  }
1394
1398
  }
1399
+ /**
1400
+ * 导出诊断包:优先从活跃 server 拿完整报告(含运行时状态);server 全挂时降级为
1401
+ * 插件本地拼装(正好覆盖「server 起不来」这种最需要日志的场景)。存为 txt 并打开。
1402
+ */
1403
+ async _handleExportDiagnostics() {
1404
+ let report = null;
1405
+ const port = this._anyServerPort();
1406
+ if (port) {
1407
+ try {
1408
+ report = await this._httpGet(`http://127.0.0.1:${port}/api/diagnostics`);
1409
+ }
1410
+ catch {
1411
+ report = null;
1412
+ }
1413
+ }
1414
+ if (!report) {
1415
+ report = this._buildLocalDiagnostics();
1416
+ }
1417
+ const def = path.join(os.homedir(), 'Downloads', `cursor-feedback-diag-${new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-')}.txt`);
1418
+ const uri = await vscode.window.showSaveDialog({
1419
+ defaultUri: vscode.Uri.file(def),
1420
+ filters: { Text: ['txt'] },
1421
+ });
1422
+ if (!uri)
1423
+ return;
1424
+ await vscode.workspace.fs.writeFile(uri, Buffer.from(report, 'utf-8'));
1425
+ const open = this._t('diagOpenAction');
1426
+ const picked = await vscode.window.showInformationMessage(this._t('diagSaved'), open);
1427
+ if (picked === open) {
1428
+ await vscode.window.showTextDocument(uri);
1429
+ }
1430
+ }
1431
+ /** server 全挂时的降级诊断:插件自己读日志文件 + 基本环境信息(不含 server 运行时状态) */
1432
+ _buildLocalDiagnostics() {
1433
+ const parts = [
1434
+ `cursor-feedback 诊断报告(降级模式:没有活跃的 server,运行时状态缺失) 生成于 ${new Date().toISOString()}`,
1435
+ `===== 环境 =====\nextension version: ${vscode.extensions.getExtension('jianger666.cursor-feedback')?.packageJSON?.version || '?'}\nplatform: ${process.platform} (${process.arch})\nvscode: ${vscode.version}`,
1436
+ ];
1437
+ try {
1438
+ const dir = path.join(os.homedir(), '.cursor-feedback', 'logs');
1439
+ const files = fs
1440
+ .readdirSync(dir)
1441
+ .filter((f) => /^\d{4}-\d{2}-\d{2}\.log$/.test(f))
1442
+ .sort()
1443
+ .slice(-2);
1444
+ let text = '';
1445
+ for (const f of files) {
1446
+ text += `===== ${f} =====\n` + fs.readFileSync(path.join(dir, f), 'utf-8');
1447
+ }
1448
+ const MAX = 256 * 1024;
1449
+ parts.push(text.length > MAX ? '…(前文截断)\n' + text.slice(text.length - MAX) : text || '(没有日志文件)');
1450
+ }
1451
+ catch {
1452
+ parts.push('(没有日志文件)');
1453
+ }
1454
+ return parts.join('\n\n') + '\n';
1455
+ }
1395
1456
  /**
1396
1457
  * HTTP GET 请求
1397
1458
  */
package/dist/feishu.js CHANGED
@@ -50,8 +50,10 @@ exports.FeishuBridge = void 0;
50
50
  const fs = __importStar(require("fs"));
51
51
  const os = __importStar(require("os"));
52
52
  const path = __importStar(require("path"));
53
+ const logger_js_1 = require("./logger.js");
53
54
  function flog(message) {
54
55
  console.error(`[${new Date().toISOString()}] [feishu] ${message}`);
56
+ (0, logger_js_1.fileLog)('feishu', message);
55
57
  }
56
58
  class FeishuBridge {
57
59
  constructor() {
package/dist/i18n/en.json CHANGED
@@ -80,6 +80,11 @@
80
80
  "daemonNotSupported": "Not supported on this platform",
81
81
  "daemonFailed": "Operation failed: {error}",
82
82
  "daemonWorking": "Working…",
83
+ "diagLabel": "Diagnostics",
84
+ "diagDesc": "Export a diagnostic report (recent logs, environment and sanitized config — secrets are masked) to attach to bug reports.",
85
+ "diagExportBtn": "Export diagnostics",
86
+ "diagSaved": "Diagnostic report saved.",
87
+ "diagOpenAction": "Open",
83
88
  "queueSend": "Queue Message",
84
89
  "queuedTitle": "Queued messages",
85
90
  "queueModeHint": "AI is busy — messages sent now are queued and delivered when it next asks for feedback",
@@ -171,6 +171,11 @@ function getDefaultMessages() {
171
171
  daemonNotSupported: "Not supported on this platform",
172
172
  daemonFailed: "Operation failed: {error}",
173
173
  daemonWorking: "Working…",
174
+ diagLabel: "Diagnostics",
175
+ diagDesc: "Export a diagnostic report (recent logs, environment and sanitized config — secrets are masked) to attach to bug reports.",
176
+ diagExportBtn: "Export diagnostics",
177
+ diagSaved: "Diagnostic report saved.",
178
+ diagOpenAction: "Open",
174
179
  queueSend: "Queue Message",
175
180
  queuedTitle: "Queued messages",
176
181
  queueModeHint: "AI is busy — messages sent now are queued and delivered when it next asks for feedback",
@@ -80,6 +80,11 @@
80
80
  "daemonNotSupported": "当前平台暂不支持",
81
81
  "daemonFailed": "操作失败:{error}",
82
82
  "daemonWorking": "处理中…",
83
+ "diagLabel": "诊断信息",
84
+ "diagDesc": "导出诊断报告(最近日志、环境信息、脱敏后的配置——密钥不会包含在内),报 bug 时附上它。",
85
+ "diagExportBtn": "导出诊断包",
86
+ "diagSaved": "诊断报告已保存。",
87
+ "diagOpenAction": "打开",
83
88
  "queueSend": "排队发送",
84
89
  "queuedTitle": "排队中的消息",
85
90
  "queueModeHint": "AI 正忙,现在发送的消息会排队,等它下一轮询问时自动送达",
package/dist/logger.js ADDED
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.fileLog = fileLog;
37
+ exports.readRecentLogs = readRecentLogs;
38
+ /**
39
+ * 落盘日志:server 侧所有日志除了打到 stderr(Cursor 的 MCP 日志),同时写入
40
+ * ~/.cursor-feedback/logs/YYYY-MM-DD.log,供「导出诊断包」排查用户现场问题。
41
+ * 设计约束:绝不能因为日志失败影响主流程(全部吞错);日志量小,直接同步追加(崩溃不丢行)。
42
+ */
43
+ const fs = __importStar(require("fs"));
44
+ const os = __importStar(require("os"));
45
+ const path = __importStar(require("path"));
46
+ /** 日志保留天数:超过的按文件名日期清理 */
47
+ const RETENTION_DAYS = 7;
48
+ function logsDir() {
49
+ return path.join(os.homedir(), '.cursor-feedback', 'logs');
50
+ }
51
+ /** 本地日期 YYYY-MM-DD(日志按用户本地时区的自然日切割) */
52
+ function localDay(d = new Date()) {
53
+ const pad = (n) => String(n).padStart(2, '0');
54
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
55
+ }
56
+ let cleanedForDay = '';
57
+ /** 清理过期日志:每个进程每天最多执行一次 */
58
+ function cleanupOldLogs(today) {
59
+ if (cleanedForDay === today)
60
+ return;
61
+ cleanedForDay = today;
62
+ try {
63
+ const cutoff = new Date();
64
+ cutoff.setDate(cutoff.getDate() - RETENTION_DAYS);
65
+ const cutoffDay = localDay(cutoff);
66
+ for (const f of fs.readdirSync(logsDir())) {
67
+ const m = f.match(/^(\d{4}-\d{2}-\d{2})\.log$/);
68
+ // 文件名日期字典序即时间序;不匹配命名规则的文件不动
69
+ if (m && m[1] < cutoffDay) {
70
+ fs.rmSync(path.join(logsDir(), f), { force: true });
71
+ }
72
+ }
73
+ }
74
+ catch {
75
+ // 目录不存在或不可读,忽略
76
+ }
77
+ }
78
+ /** 追加一行日志到当天的文件。tag 用于区分模块(mcp / feishu / cli-launcher)。 */
79
+ function fileLog(tag, message) {
80
+ try {
81
+ const today = localDay();
82
+ fs.mkdirSync(logsDir(), { recursive: true });
83
+ cleanupOldLogs(today);
84
+ const line = `[${new Date().toISOString()}] [${tag}] ${message}\n`;
85
+ fs.appendFileSync(path.join(logsDir(), `${today}.log`), line);
86
+ }
87
+ catch {
88
+ // 磁盘满/权限问题都不能影响主流程
89
+ }
90
+ }
91
+ /**
92
+ * 读取最近的日志尾部(今天优先,不够再补前一天),用于诊断包。
93
+ * 返回不超过 maxBytes 的文本。
94
+ */
95
+ function readRecentLogs(maxBytes = 256 * 1024) {
96
+ try {
97
+ const files = fs
98
+ .readdirSync(logsDir())
99
+ .filter((f) => /^\d{4}-\d{2}-\d{2}\.log$/.test(f))
100
+ .sort()
101
+ .slice(-2);
102
+ let text = '';
103
+ for (const f of files) {
104
+ text += `===== ${f} =====\n`;
105
+ text += fs.readFileSync(path.join(logsDir(), f), 'utf-8');
106
+ }
107
+ return text.length > maxBytes ? '…(前文截断)\n' + text.slice(text.length - maxBytes) : text;
108
+ }
109
+ catch {
110
+ return '(没有日志文件)';
111
+ }
112
+ }
113
+ //# sourceMappingURL=logger.js.map
@@ -62,6 +62,7 @@ const feishu_js_1 = require("./feishu.js");
62
62
  const cli_launcher_js_1 = require("./cli-launcher.js");
63
63
  const keep_awake_js_1 = require("./keep-awake.js");
64
64
  const daemon_install_js_1 = require("./daemon-install.js");
65
+ const logger_js_1 = require("./logger.js");
65
66
  // ⚠️ MCP stdio 协议要求 stdout 只承载 JSON-RPC 消息。第三方库(尤其飞书 SDK 的内置 logger,
66
67
  // 输出形如 "[info]: [...]")会用 console.log/info/debug 往 stdout 打日志,一旦混入就会让
67
68
  // Cursor 端 JSON 解析失败、连接进入 failed(表现为 "Not connected")。这里在进程最早期把这三个
@@ -74,6 +75,7 @@ console.debug = (...args) => console.error(...args);
74
75
  function debugLog(message) {
75
76
  const timestamp = new Date().toISOString();
76
77
  console.error(`[${timestamp}] ${message}`);
78
+ (0, logger_js_1.fileLog)('mcp', message);
77
79
  }
78
80
  // 包版本真相源:package.json(dist/mcp-server.js 的上一级目录)。读不到时兜底 0.0.0。
79
81
  const PKG_VERSION = (() => {
@@ -396,7 +398,8 @@ class McpFeedbackServer {
396
398
  if (!text && images.length === 0 && files.length === 0)
397
399
  return;
398
400
  // 斜杠命令优先于反馈路由:用户显式输入命令时不应被当成对某张卡片的回复或排队消息。
399
- if (/^\/(new|stop|model|cwd)\b/.test(text) && this.handleCliCommand(text, chatId))
401
+ // 大小写不敏感:手机输入法常自动把句首字母大写(/New),必须容错。
402
+ if (/^\s*[//](new|stop|model|cwd|help|projects)\b/i.test(text) && this.handleCliCommand(text, chatId))
400
403
  return;
401
404
  if (parentId) {
402
405
  // 用户「回复」了某条卡片 → 用 parent_id 精确路由
@@ -494,13 +497,50 @@ class McpFeedbackServer {
494
497
  }
495
498
  /**
496
499
  * 处理飞书斜杠命令。返回是否已消费该消息。
497
- * - /new [工作目录] 任务描述:拉起一个 headless CLI 会话(非交互 + maxMode=false,扣 1 次请求)
500
+ * - /new [目录或项目名] 任务描述:拉起 headless CLI 会话(非交互 + maxMode=false,扣 1 次请求)。
501
+ * 工作目录刻意保持简单:显式指定 > 主目录,与当前开着哪些 IDE 窗口无关。
502
+ * - /projects:列出 Cursor 打开过的项目路径(供手机上查路径 / 复制给 /new)
498
503
  * - /stop:终止运行中的 CLI 会话
499
504
  * - /model [模型id]:查看 / 设置 CLI 会话模型(持久化;无论选什么模型都强制 maxMode=false)
500
- * - /cwd [目录]:查看 / 设置默认工作目录(持久化;IDE 不开时 /new 用它)
501
505
  */
502
- handleCliCommand(text, chatId) {
503
- if (/^\/stop\b/.test(text)) {
506
+ handleCliCommand(rawText, chatId) {
507
+ // 输入容错:首尾空格(飞书入站已 trim,这里兜底;trim 同样覆盖全角空格 U+3000);
508
+ // 全角斜杠「/」归一化(中文输入法常见);命令词大小写不敏感(手机输入法自动把句首
509
+ // 大写成 /New)。任务正文原样保留。
510
+ const text = rawText.trim().replace(/^//, '/');
511
+ if (/^\/help\b/i.test(text)) {
512
+ this.feishu.replyText(chatId, '📖 命令一览(大小写、首尾空格都不敏感)\n\n' +
513
+ '🚀 /new 任务描述\n' +
514
+ '拉起一个全新的 AI 会话跑任务,电脑不用开着 Cursor(需已开启常驻服务)。\n' +
515
+ '· 默认工作目录是主目录;可在任务前指定项目:\n' +
516
+ ' /new /Users/me/proj 帮我修下测试\n' +
517
+ ' /new crm-web 帮我修下测试(项目名唯一时自动匹配到完整路径)\n' +
518
+ '· AI 需要沟通时会发反馈卡片,直接回复即可;同时只能跑一个会话\n\n' +
519
+ '📁 /projects\n' +
520
+ '列出 Cursor 打开过的项目路径(查路径 / 复制给 /new,也可直接用项目名)\n\n' +
521
+ '🛑 /stop\n' +
522
+ '终止运行中的 CLI 会话(任何窗口拉起的都能停)\n\n' +
523
+ '🧠 /model [模型id]\n' +
524
+ '查看 / 设置会话模型(持久化)\n\n' +
525
+ '❓ /help\n' +
526
+ '看这份帮助\n\n' +
527
+ '💬 不带斜杠的消息照常作为反馈:回复某张卡片就送达那个请求,直接发送则给当前等待中的 AI。');
528
+ return true;
529
+ }
530
+ if (/^\/projects\b/i.test(text)) {
531
+ const projects = this.cliLauncher.listProjects();
532
+ if (!projects.length) {
533
+ this.feishu.replyText(chatId, '没有找到 Cursor 打开过的项目记录。可以直接用绝对路径:/new /绝对路径 任务描述');
534
+ }
535
+ else {
536
+ const lines = projects.slice(0, 20).map((p) => `· ${path.basename(p)} — ${p}`);
537
+ this.feishu.replyText(chatId, `Cursor 打开过的项目(共 ${projects.length} 个${projects.length > 20 ? ',只列最近 20 个' : ''}):\n` +
538
+ lines.join('\n') +
539
+ '\n\n用法:/new 项目名 任务描述(项目名唯一时直接匹配),或 /new 完整路径 任务描述');
540
+ }
541
+ return true;
542
+ }
543
+ if (/^\/stop\b/i.test(text)) {
504
544
  if (this.cliLauncher.stop()) {
505
545
  this.feishu.replyText(chatId, '🛑 正在终止 CLI 会话…结束后我会再发一条收尾消息。');
506
546
  }
@@ -509,7 +549,7 @@ class McpFeedbackServer {
509
549
  }
510
550
  return true;
511
551
  }
512
- const mModel = text.match(/^\/model(?:\s+(\S+))?\s*$/);
552
+ const mModel = text.match(/^\/model(?:\s+(\S+))?\s*$/i);
513
553
  if (mModel) {
514
554
  if (mModel[1]) {
515
555
  this.cliLauncher.writeSettings({ model: mModel[1] });
@@ -520,52 +560,59 @@ class McpFeedbackServer {
520
560
  }
521
561
  return true;
522
562
  }
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
- }
563
+ // /cwd 已移除(工作目录简化为「/new 里显式指定,否则主目录」),给老用户指个路
564
+ if (/^\/cwd\b/i.test(text)) {
565
+ this.feishu.replyText(chatId, '/cwd 已移除。现在直接在 /new 里指定:/new 项目名或/绝对路径 任务描述;不指定就在主目录跑。\n用 /projects 可以查项目路径。');
539
566
  return true;
540
567
  }
541
- const m = text.match(/^\/new\s*([\s\S]*)$/);
568
+ // /model 带了多余参数(如 /model a b):不落入反馈路由(用户明显在敲命令),回用法提示
569
+ if (/^\/model\b/i.test(text)) {
570
+ this.feishu.replyText(chatId, '参数不对。用法:/model 模型id(只接受一个参数,留空为查看当前模型)');
571
+ return true;
572
+ }
573
+ const m = text.match(/^\/new\b\s*([\s\S]*)$/i);
542
574
  if (!m)
543
575
  return false;
544
576
  let task = (m[1] || '').trim();
545
577
  if (!task) {
546
- this.feishu.replyText(chatId, '用法:/new 任务描述\n' +
547
- '可选在任务前带一个已存在的目录作为工作目录:\n' +
578
+ this.feishu.replyText(chatId, '用法:/new 任务描述(默认在主目录跑)\n' +
579
+ '可选在任务前带工作目录(绝对路径或项目名):\n' +
548
580
  '/new /Users/me/proj 帮我看下测试为什么挂了\n' +
549
- '相关命令:/cwd 设默认目录、/model 设模型、/stop 终止会话。');
581
+ '/new my-blog 帮我看下测试为什么挂了\n' +
582
+ '相关命令:/projects 查项目路径、/model 设模型、/stop 终止会话。');
550
583
  return true;
551
584
  }
552
585
  if (this.cliLauncher.isRunning()) {
553
586
  this.feishu.replyText(chatId, '已有一个 CLI 会话在运行:' + this.cliLauncher.describe() + '\n发 /stop 可先终止它。');
554
587
  return true;
555
588
  }
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]);
589
+ // 工作目录刻意保持简单、与 IDE 窗口无关:命令里显式指定(路径或项目名)> 主目录。
590
+ let cwd = os.homedir();
591
+ const firstToken = task.split(/\s+/)[0];
592
+ const explicitDir = this.expandDirToken(firstToken);
563
593
  if (explicitDir) {
564
594
  cwd = explicitDir;
565
- task = task.slice(task.split(/\s+/)[0].length).trim();
595
+ task = task.slice(firstToken.length).trim();
596
+ }
597
+ else {
598
+ // 首词不是路径 → 试着当项目名匹配(Cursor 打开过的项目,按目录名精确匹配、忽略大小写)。
599
+ // 唯一命中才采用;多个重名让用户用完整路径消歧;没命中就当作任务正文的第一个词。
600
+ const matches = this.cliLauncher
601
+ .listProjects()
602
+ .filter((p) => path.basename(p).toLowerCase() === firstToken.toLowerCase());
603
+ if (matches.length === 1) {
604
+ cwd = matches[0];
605
+ task = task.slice(firstToken.length).trim();
606
+ }
607
+ else if (matches.length > 1) {
608
+ this.feishu.replyText(chatId, `有 ${matches.length} 个同名项目「${firstToken}」:\n` +
609
+ matches.map((p) => `· ${p}`).join('\n') +
610
+ '\n请用完整路径:/new 完整路径 任务描述');
611
+ return true;
612
+ }
566
613
  }
567
614
  if (!task) {
568
- this.feishu.replyText(chatId, '只给了目录没给任务。用法:/new [工作目录] 任务描述');
615
+ this.feishu.replyText(chatId, '只给了目录没给任务。用法:/new [目录或项目名] 任务描述');
569
616
  return true;
570
617
  }
571
618
  const err = this.cliLauncher.start(task, cwd, (result) => this.onCliSessionDone(result, chatId));
@@ -581,6 +628,64 @@ class McpFeedbackServer {
581
628
  }
582
629
  return true;
583
630
  }
631
+ /**
632
+ * 组装诊断报告(纯文本)。密钥严格脱敏:appSecret 绝不输出,appId 只留前 8 位。
633
+ * 每个区块独立 try/catch,单块失败不影响整包导出。
634
+ */
635
+ buildDiagnostics() {
636
+ const sections = [];
637
+ const add = (title, fn) => {
638
+ let body;
639
+ try {
640
+ body = fn();
641
+ }
642
+ catch (e) {
643
+ body = `(读取失败:${e})`;
644
+ }
645
+ sections.push(`===== ${title} =====\n${body}`);
646
+ };
647
+ const maskId = (id) => (id.length > 8 ? id.slice(0, 8) + '***' : '***');
648
+ const readJson = (file) => {
649
+ try {
650
+ return JSON.parse(fs.readFileSync(path.join(os.homedir(), '.cursor-feedback', file), 'utf-8'));
651
+ }
652
+ catch {
653
+ return null;
654
+ }
655
+ };
656
+ add('环境', () => [
657
+ `version: ${PKG_VERSION}`,
658
+ `pid: ${process.pid} uptimeSec: ${Math.round(process.uptime())}`,
659
+ `platform: ${process.platform} ${os.release()} (${process.arch})`,
660
+ `node: ${process.version}`,
661
+ `daemonMode: ${this.daemonMode}`,
662
+ `ownerWorkspace: ${this.ownerWorkspace || '(none)'}`,
663
+ `hasCurrentRequest: ${this.currentRequest !== null}`,
664
+ ].join('\n'));
665
+ add('飞书配置(脱敏)', () => {
666
+ const cfg = readJson('feishu-config.json');
667
+ if (!cfg)
668
+ return '(未配置)';
669
+ const safe = { ...cfg };
670
+ if (typeof safe.appId === 'string')
671
+ safe.appId = maskId(safe.appId);
672
+ if ('appSecret' in safe)
673
+ safe.appSecret = '***';
674
+ return JSON.stringify(safe, null, 2);
675
+ });
676
+ add('飞书绑定(appId 脱敏)', () => {
677
+ const bind = readJson('feishu-bind.json');
678
+ if (!bind)
679
+ return '(无)';
680
+ return JSON.stringify(Object.fromEntries(Object.entries(bind).map(([k, v]) => [maskId(k), v])), null, 2);
681
+ });
682
+ add('通用设置 settings.json', () => JSON.stringify(readJson('settings.json') ?? '(无)'));
683
+ add('CLI 设置 cli.json', () => JSON.stringify(readJson('cli.json') ?? '(无)'));
684
+ add('CLI 会话锁', () => JSON.stringify(readJson('cli-session.lock') ?? '(无活跃会话)'));
685
+ add('常驻服务状态', () => JSON.stringify((0, daemon_install_js_1.daemonStatus)(), null, 2));
686
+ add('最近日志(今天 + 昨天,尾部截断)', () => (0, logger_js_1.readRecentLogs)());
687
+ return `cursor-feedback 诊断报告 生成于 ${new Date().toISOString()}\n\n` + sections.join('\n\n') + '\n';
688
+ }
584
689
  /** CLI 会话结束的收尾回执:把 agent 的最终输出(尾部)发回飞书 */
585
690
  onCliSessionDone(result, chatId) {
586
691
  const mins = Math.max(1, Math.round(result.elapsedMs / 60000));
@@ -1890,6 +1995,13 @@ class McpFeedbackServer {
1890
1995
  });
1891
1996
  return;
1892
1997
  }
1998
+ // 诊断包:版本/环境/配置(脱敏)/守护状态/会话锁 + 最近日志尾部,纯文本一把梭。
1999
+ // 插件面板「导出诊断包」按钮请求这里;轻量起见不打 zip,一个 txt 就够排查。
2000
+ if (req.method === 'GET' && req.url === '/api/diagnostics') {
2001
+ res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
2002
+ res.end(this.buildDiagnostics());
2003
+ return;
2004
+ }
1893
2005
  // 健康检查
1894
2006
  if (req.method === 'GET' && req.url === '/api/health') {
1895
2007
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -221,6 +221,15 @@
221
221
  <p id="daemonStatusText" class="feishu-modal__desc feishu-modal__desc--sub"></p>
222
222
  </div>
223
223
 
224
+ <!-- 区块③b:诊断信息(导出日志 + 环境 + 脱敏配置,报 bug 用) -->
225
+ <div class="feishu-section">
226
+ <span class="feishu-switch__label feishu-switch__label--strong">{{i18n.diagLabel}}</span>
227
+ <p class="feishu-modal__desc">{{i18n.diagDesc}}</p>
228
+ <div class="feishu-modal__actions">
229
+ <button id="diagExportBtn" class="feishu-link" type="button">{{i18n.diagExportBtn}}</button>
230
+ </div>
231
+ </div>
232
+
224
233
  <!-- 区块④:飞书通知(推送开关 + 凭证 + 绑定状态) -->
225
234
  <div class="feishu-section">
226
235
  <label class="feishu-switch">
@@ -243,6 +243,11 @@
243
243
  daemonStatusText.textContent = i18n.daemonWorking || 'Working…';
244
244
  vscode.postMessage({ type: 'toggleDaemon', payload: { enabled: daemonToggle.checked } });
245
245
  });
246
+ // ---------- 诊断包导出 ----------
247
+ document.getElementById('diagExportBtn').addEventListener('click', () => {
248
+ vscode.postMessage({ type: 'exportDiagnostics' });
249
+ });
250
+
246
251
  function updateDaemonUI(s) {
247
252
  if (!s) return;
248
253
  daemonToggle.disabled = !s.supported;
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.1",
5
+ "version": "2.6.0",
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 && node scripts/smoke-dup.js && node scripts/smoke-cli-launcher.js && node scripts/smoke-daemon.js",
150
+ "test:smoke": "npm run compile && node scripts/smoke-queue.js && node scripts/smoke-dup.js && node scripts/smoke-cli-launcher.js && node scripts/smoke-daemon.js && node scripts/smoke-logger.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",