cursor-feedback 2.3.1 → 2.4.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.
- package/CHANGELOG.md +7 -0
- package/README.md +31 -0
- package/README_CN.md +31 -0
- package/dist/cli-launcher.js +318 -0
- package/dist/daemon-install.js +292 -0
- package/dist/extension.js +60 -0
- package/dist/i18n/en.json +6 -0
- package/dist/i18n/index.js +6 -0
- package/dist/i18n/zh-CN.json +6 -0
- package/dist/keep-awake.js +101 -0
- package/dist/mcp-server.js +215 -20
- package/dist/webview/index.html +12 -1
- package/dist/webview/script.js +34 -0
- 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.4.0](https://github.com/jianger666/cursor-feedback-extension/compare/v2.3.1...v2.4.0) (2026-07-06)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* 飞书 /new 拉起 Cursor CLI 会话 + 常驻守护进程(IDE 关闭可用) ([1d4513e](https://github.com/jianger666/cursor-feedback-extension/commit/1d4513ed29b8050dc92dc1d2632bf9d61912ab71))
|
|
11
|
+
|
|
5
12
|
### [2.3.1](https://github.com/jianger666/cursor-feedback-extension/compare/v2.3.0...v2.3.1) (2026-07-06)
|
|
6
13
|
|
|
7
14
|
|
package/README.md
CHANGED
|
@@ -27,6 +27,8 @@ If you're on Cursor's 500 requests/month plan or another coding plan, every conv
|
|
|
27
27
|
- 📝 **Markdown Rendering** - Full Markdown support for AI summaries
|
|
28
28
|
- ⏱️ **Auto-retry on Timeout** - 5-minute default timeout, AI automatically re-requests
|
|
29
29
|
- 🔔 **Feishu (Lark) Bridge** - When AI requests feedback, the summary is pushed to Feishu so you can reply right from your phone
|
|
30
|
+
- 📱 **Launch sessions from your phone** - Send `/new task` in Feishu to spawn a Cursor CLI session, no computer access needed
|
|
31
|
+
- 🌙 **Background service** - Optional login daemon: Feishu keeps working with Cursor closed, auto keep-awake on AC power
|
|
30
32
|
- 🌍 **Multi-language** - Supports English, Simplified Chinese, Traditional Chinese
|
|
31
33
|
- 🔒 **Project Isolation** - Multiple windows work independently
|
|
32
34
|
|
|
@@ -260,6 +262,35 @@ Three steps to enable the round-trip:
|
|
|
260
262
|
|
|
261
263
|
> **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.
|
|
262
264
|
|
|
265
|
+
### Launch CLI sessions from Feishu (/new)
|
|
266
|
+
|
|
267
|
+
Start a brand-new AI session from your phone, away from the computer. Prerequisite: [Cursor CLI](https://cursor.com/cli) (`cursor-agent`) installed and logged in on the machine.
|
|
268
|
+
|
|
269
|
+
Send these directly in the bot chat:
|
|
270
|
+
|
|
271
|
+
| Command | What it does |
|
|
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 |
|
|
275
|
+
| `/stop` | Terminate the running CLI session |
|
|
276
|
+
| `/model [modelId]` | Show / set the session model (persisted) |
|
|
277
|
+
| `/cwd [dir]` | Show / set the default working directory (persisted) |
|
|
278
|
+
|
|
279
|
+
- 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
|
+
- 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.
|
|
281
|
+
- Put custom rules in `~/.cursor-feedback/cli-rules.md` (e.g. "always reply in Chinese"); they are injected into every `/new` session.
|
|
282
|
+
- When a session ends (done / error / `/stop` / 3-hour cap), a wrap-up message with the final output is sent to Feishu.
|
|
283
|
+
|
|
284
|
+
### Background service (works without the IDE)
|
|
285
|
+
|
|
286
|
+
Normally the server process is spawned by Cursor, so closing Cursor drops the Feishu link. Enable the **background service** and a standalone daemon auto-starts at login — messages and `/new` work whether Cursor is open or not:
|
|
287
|
+
|
|
288
|
+
- **Enable**: the "Background service" toggle in the extension's notification settings, or `npx cursor-feedback@latest install-daemon` (`uninstall-daemon` / `daemon-status` likewise).
|
|
289
|
+
- **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
|
+
- **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
|
+
- **Typical use**: leave the machine plugged in and locked after work; `/new` from your phone anytime.
|
|
292
|
+
- After upgrading the extension, toggle the switch off/on once to refresh the daemon's copy.
|
|
293
|
+
|
|
263
294
|
## 🏗️ Architecture
|
|
264
295
|
|
|
265
296
|
```
|
package/README_CN.md
CHANGED
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
- 📝 **Markdown 渲染** - AI 摘要支持完整的 Markdown 格式
|
|
30
30
|
- ⏱️ **超时自动重试** - 默认 5 分钟超时,超时后 AI 会自动重新请求反馈
|
|
31
31
|
- 🔔 **飞书打通** - AI 请求反馈时把摘要推送到飞书,你直接在手机上回复,不用守在电脑前
|
|
32
|
+
- 📱 **手机拉起会话** - 飞书发 `/new 任务` 直接拉起一轮 Cursor CLI 会话,人不在电脑前也能开工
|
|
33
|
+
- 🌙 **常驻服务** - 可选的开机自启守护进程:Cursor 不开也能收发飞书消息,接电源自动防睡眠
|
|
32
34
|
- 🌍 **多语言支持** - 支持简体中文、繁体中文和英文
|
|
33
35
|
- 🔒 **项目隔离** - 多窗口同时使用时,各项目互不干扰
|
|
34
36
|
|
|
@@ -264,6 +266,35 @@ npm install -g cursor-feedback
|
|
|
264
266
|
|
|
265
267
|
> **非 Cursor 环境不会因空闲被杀。** 只有 Cursor 插件会靠轮询给 server「续命」。在其他所有 host 上,server 完全依赖 stdio 连接存活:只在 stdin EOF 或父进程死亡时退出,**绝不**因空闲退出;而且只要还有反馈在等用户,任何环境都不会退。所以你回复花了好几分钟,结果照样能回到 Agent。
|
|
266
268
|
|
|
269
|
+
### 手机飞书拉起 CLI 会话(/new)
|
|
270
|
+
|
|
271
|
+
人不在电脑前,也能从手机飞书发起一轮全新的 AI 会话。前提:本机装好并登录过 [Cursor CLI](https://cursor.com/cli)(`cursor-agent`)。
|
|
272
|
+
|
|
273
|
+
在与机器人的会话里直接发:
|
|
274
|
+
|
|
275
|
+
| 命令 | 作用 |
|
|
276
|
+
|------|------|
|
|
277
|
+
| `/new 任务描述` | 拉起一个 headless CLI 会话跑这个任务(默认在活跃窗口工作区 / `/cwd` 设置的目录) |
|
|
278
|
+
| `/new /绝对路径 任务描述` | 指定工作目录拉起 |
|
|
279
|
+
| `/stop` | 终止运行中的 CLI 会话 |
|
|
280
|
+
| `/model [模型id]` | 查看 / 设置会话模型(持久化) |
|
|
281
|
+
| `/cwd [目录]` | 查看 / 设置默认工作目录(持久化) |
|
|
282
|
+
|
|
283
|
+
- 会话以**非交互模式**运行,拉起前强制写入 `maxMode=false`——按次计费套餐固定只扣 1 次请求,永远不会走 Max 计费。
|
|
284
|
+
- 拉起的 Agent 也通过本插件的反馈卡片与你沟通:需要确认、汇报进展都会推到飞书,直接回复卡片即可。
|
|
285
|
+
- 可在 `~/.cursor-feedback/cli-rules.md` 写自定义规则(如「永远用中文」),每次 `/new` 自动注入。
|
|
286
|
+
- 会话结束(完成 / 异常 / `/stop` / 超 3 小时兜底终止)都会发飞书收尾消息附最终输出。
|
|
287
|
+
|
|
288
|
+
### 常驻服务(IDE 关闭也可用)
|
|
289
|
+
|
|
290
|
+
默认情况下 server 进程由 Cursor 拉起,Cursor 一关飞书链路就断。打开**常驻服务**后,一个独立的守护进程会开机自启,Cursor 开不开都能收发飞书消息、响应 `/new`:
|
|
291
|
+
|
|
292
|
+
- **开启方式**:插件面板「通知设置」里打开「常驻服务」开关;或命令行 `npx cursor-feedback@latest install-daemon`(卸载对应 `uninstall-daemon`,状态 `daemon-status`)。
|
|
293
|
+
- **实现**:把当前包完整拷贝到 `~/.cursor-feedback/daemon/app`(自包含,不怕 npx 缓存被清),macOS 注册 launchd(崩溃自动拉起),Windows 注册登录计划任务。
|
|
294
|
+
- **防睡眠**:守护进程在接电源时自动阻止系统睡眠(macOS `caffeinate -s`;Windows 电源断言),电池供电不生效、不偷耗电。锁屏本来就不影响后台进程。
|
|
295
|
+
- **典型用法**:下班插着电源锁屏走人,手机飞书随时 `/new`。
|
|
296
|
+
- 升级插件版本后重新开关一次即可更新守护进程里的拷贝。
|
|
297
|
+
|
|
267
298
|
## 🏗️ 架构
|
|
268
299
|
|
|
269
300
|
```
|
|
@@ -0,0 +1,318 @@
|
|
|
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.CliLauncher = void 0;
|
|
37
|
+
/**
|
|
38
|
+
* CLI 会话拉起器:飞书 /new 命令 → 非交互模式 spawn cursor-agent。
|
|
39
|
+
*
|
|
40
|
+
* 关键设计(均来自实测结论,勿随意更改):
|
|
41
|
+
* - 必须用非交互 print 模式(-p):交互式 TUI 会话对 Fable 5 会强制回到 Max Mode,
|
|
42
|
+
* 次数计费的套餐一次对话被扣 10+ 次;非交互模式尊重 cli-config.json 的 maxMode=false,
|
|
43
|
+
* 固定只扣 1 次请求。
|
|
44
|
+
* - spawn 前每次都强制把 ~/.cursor/cli-config.json 写成 maxMode=false + 目标模型:
|
|
45
|
+
* 交互式会话会把这个文件改回 max,不能信任上次的残留状态。
|
|
46
|
+
* - CLI 不读 IDE 的全局 User Rules,用户的个人规则要显式注入到 prompt 里
|
|
47
|
+
* (从 ~/.cursor-feedback/cli-rules.md 读取,没有则只注入 cursor-feedback 沟通协议)。
|
|
48
|
+
* - 拉起的 agent 会通过全局注册的 cursor-feedback MCP 发飞书卡片,用户在手机上
|
|
49
|
+
* 直接和这个会话对话——本模块只负责拉起和收尾,过程中的交互走既有反馈链路。
|
|
50
|
+
*/
|
|
51
|
+
const child_process_1 = require("child_process");
|
|
52
|
+
const fs = __importStar(require("fs"));
|
|
53
|
+
const os = __importStar(require("os"));
|
|
54
|
+
const path = __importStar(require("path"));
|
|
55
|
+
function clog(message) {
|
|
56
|
+
console.error(`[${new Date().toISOString()}] [cli-launcher] ${message}`);
|
|
57
|
+
}
|
|
58
|
+
class CliLauncher {
|
|
59
|
+
constructor() {
|
|
60
|
+
this.child = null;
|
|
61
|
+
this.startedAt = 0;
|
|
62
|
+
this.taskBrief = '';
|
|
63
|
+
this.stopRequested = false;
|
|
64
|
+
}
|
|
65
|
+
isRunning() {
|
|
66
|
+
return this.child !== null;
|
|
67
|
+
}
|
|
68
|
+
/** 正在运行的会话描述(用于「已有会话在跑」的回执) */
|
|
69
|
+
describe() {
|
|
70
|
+
if (!this.child)
|
|
71
|
+
return '';
|
|
72
|
+
const mins = Math.round((Date.now() - this.startedAt) / 60000);
|
|
73
|
+
return `「${this.taskBrief}」(已运行 ${mins} 分钟)`;
|
|
74
|
+
}
|
|
75
|
+
settingsPath() {
|
|
76
|
+
return path.join(os.homedir(), '.cursor-feedback', 'cli.json');
|
|
77
|
+
}
|
|
78
|
+
readSettings() {
|
|
79
|
+
try {
|
|
80
|
+
const j = JSON.parse(fs.readFileSync(this.settingsPath(), 'utf-8'));
|
|
81
|
+
return typeof j === 'object' && j ? j : {};
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return {};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
writeSettings(patch) {
|
|
88
|
+
const merged = { ...this.readSettings(), ...patch };
|
|
89
|
+
const p = this.settingsPath();
|
|
90
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
91
|
+
fs.writeFileSync(p, JSON.stringify(merged, null, 2));
|
|
92
|
+
}
|
|
93
|
+
/** 当前生效模型:env 覆盖 > /model 持久化设置 > 默认。无论选什么模型,spawn 前都强制 maxMode=false */
|
|
94
|
+
model() {
|
|
95
|
+
return (process.env.CURSOR_FEEDBACK_CLI_MODEL ||
|
|
96
|
+
this.readSettings().model ||
|
|
97
|
+
CliLauncher.DEFAULT_MODEL);
|
|
98
|
+
}
|
|
99
|
+
/** /cwd 设置的默认工作目录(存在才返回) */
|
|
100
|
+
defaultCwd() {
|
|
101
|
+
const d = this.readSettings().defaultCwd;
|
|
102
|
+
if (d && fs.existsSync(d))
|
|
103
|
+
return d;
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 定位 cursor-agent 可执行文件。安装脚本默认放在 ~/.local/bin(mac/linux 是
|
|
108
|
+
* cursor-agent,Windows 是 cursor-agent.cmd/.exe),而 MCP server 进程的 PATH
|
|
109
|
+
* 通常不含它,不能只靠 PATH 兜底。
|
|
110
|
+
*/
|
|
111
|
+
findBinary() {
|
|
112
|
+
const explicit = process.env.CURSOR_AGENT_PATH;
|
|
113
|
+
if (explicit && fs.existsSync(explicit))
|
|
114
|
+
return explicit;
|
|
115
|
+
const names = process.platform === 'win32'
|
|
116
|
+
? ['cursor-agent.cmd', 'cursor-agent.exe', 'cursor-agent.ps1']
|
|
117
|
+
: ['cursor-agent'];
|
|
118
|
+
for (const dir of [
|
|
119
|
+
path.join(os.homedir(), '.local', 'bin'),
|
|
120
|
+
path.join(os.homedir(), 'AppData', 'Local', 'Programs', 'cursor-agent'),
|
|
121
|
+
]) {
|
|
122
|
+
for (const name of names) {
|
|
123
|
+
const p = path.join(dir, name);
|
|
124
|
+
if (fs.existsSync(p))
|
|
125
|
+
return p;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return 'cursor-agent'; // 最后交给 PATH 解析,spawn error 时报错提示
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* spawn 前强制写 cli-config.json:maxMode=false + 目标模型。
|
|
132
|
+
* 每次都写——交互式会话会把它改回 max,残留状态不可信。
|
|
133
|
+
*/
|
|
134
|
+
ensureMaxModeOff() {
|
|
135
|
+
const p = path.join(os.homedir(), '.cursor', 'cli-config.json');
|
|
136
|
+
let cfg = {};
|
|
137
|
+
try {
|
|
138
|
+
cfg = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// 文件不存在或损坏时从空对象重建(cursor-agent 会补齐其余字段)
|
|
142
|
+
}
|
|
143
|
+
cfg.maxMode = false;
|
|
144
|
+
cfg.model = { modelId: this.model(), maxMode: false };
|
|
145
|
+
cfg.hasChangedDefaultModel = true;
|
|
146
|
+
fs.writeFileSync(p, JSON.stringify(cfg, null, 2));
|
|
147
|
+
clog(`cli-config.json 已写入 maxMode=false, model=${this.model()}`);
|
|
148
|
+
}
|
|
149
|
+
/** 用户自定义注入规则:~/.cursor-feedback/cli-rules.md(可选) */
|
|
150
|
+
readUserRules() {
|
|
151
|
+
try {
|
|
152
|
+
const p = path.join(os.homedir(), '.cursor-feedback', 'cli-rules.md');
|
|
153
|
+
const text = fs.readFileSync(p, 'utf-8').trim();
|
|
154
|
+
return text;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return '';
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* 组装最终 prompt:cursor-feedback 沟通协议(必注入,否则 headless 会话无法与
|
|
162
|
+
* 手机上的用户交互)+ 用户自定义规则 + 任务本体。
|
|
163
|
+
*/
|
|
164
|
+
buildPrompt(task) {
|
|
165
|
+
const parts = [];
|
|
166
|
+
parts.push('【会话背景与必须遵守的沟通协议】\n' +
|
|
167
|
+
'- 用户不在电脑前,正通过手机飞书与你交流;你是被飞书 /new 命令拉起的 headless 会话。\n' +
|
|
168
|
+
'- 与用户的所有沟通必须通过 MCP 工具 cursor-feedback 的 interactive_feedback 进行' +
|
|
169
|
+
'(它的卡片会推送到用户飞书):需要确认、汇报进展、完成任务时都调用它。\n' +
|
|
170
|
+
'- interactive_feedback 返回以 [TIMEOUT_KEEP_WAITING] 开头时表示用户暂未回复,' +
|
|
171
|
+
'必须立即用相同 project_directory 再次调用继续等待;以 [TIMEOUT_END] 开头才可结束。\n' +
|
|
172
|
+
'- project_directory 参数固定传当前工作目录。');
|
|
173
|
+
const userRules = this.readUserRules();
|
|
174
|
+
if (userRules) {
|
|
175
|
+
parts.push('【用户附加规则(必须遵守)】\n' + userRules);
|
|
176
|
+
}
|
|
177
|
+
parts.push('【任务】\n' + task);
|
|
178
|
+
return parts.join('\n\n');
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* 拉起一个非交互 CLI 会话。调用方需先用 isRunning() 确认没有并发会话。
|
|
182
|
+
* @returns 启动失败时返回错误说明字符串;成功返回 null,结果经 onDone 回调
|
|
183
|
+
*/
|
|
184
|
+
start(task, cwd, onDone) {
|
|
185
|
+
if (this.child)
|
|
186
|
+
return '已有一个 CLI 会话在运行:' + this.describe();
|
|
187
|
+
try {
|
|
188
|
+
this.ensureMaxModeOff();
|
|
189
|
+
}
|
|
190
|
+
catch (e) {
|
|
191
|
+
return '写入 cli-config.json 失败:' + e;
|
|
192
|
+
}
|
|
193
|
+
const bin = this.findBinary();
|
|
194
|
+
if (!bin)
|
|
195
|
+
return '找不到 cursor-agent,可通过环境变量 CURSOR_AGENT_PATH 指定路径。';
|
|
196
|
+
const workDir = fs.existsSync(cwd) ? cwd : os.homedir();
|
|
197
|
+
const prompt = this.buildPrompt(task);
|
|
198
|
+
let stdout = '';
|
|
199
|
+
let stderr = '';
|
|
200
|
+
const append = (buf, chunk) => {
|
|
201
|
+
const next = buf + chunk.toString();
|
|
202
|
+
return next.length > CliLauncher.OUTPUT_TAIL_LIMIT
|
|
203
|
+
? next.slice(next.length - CliLauncher.OUTPUT_TAIL_LIMIT)
|
|
204
|
+
: next;
|
|
205
|
+
};
|
|
206
|
+
const args = ['-p', '--trust', '--model', this.model(), prompt];
|
|
207
|
+
const isWin = process.platform === 'win32';
|
|
208
|
+
// Windows 上 .cmd/.ps1 不能直接 spawn(Node 18.20+ 禁止),需经 cmd.exe 转发
|
|
209
|
+
const viaCmdShell = isWin && /\.(cmd|bat|ps1)$/i.test(bin);
|
|
210
|
+
const spawnBin = viaCmdShell ? 'cmd.exe' : bin;
|
|
211
|
+
const spawnArgs = viaCmdShell ? ['/d', '/s', '/c', bin, ...args] : args;
|
|
212
|
+
const pathSep = isWin ? ';' : ':';
|
|
213
|
+
let child;
|
|
214
|
+
try {
|
|
215
|
+
child = (0, child_process_1.spawn)(spawnBin, spawnArgs, {
|
|
216
|
+
cwd: workDir,
|
|
217
|
+
env: {
|
|
218
|
+
...process.env,
|
|
219
|
+
PATH: `${path.join(os.homedir(), '.local', 'bin')}${pathSep}${process.env.PATH || ''}`,
|
|
220
|
+
},
|
|
221
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
222
|
+
// POSIX:独立进程组,终止时连 cursor-agent 自己 spawn 的子进程一起杀干净;
|
|
223
|
+
// Windows 走 taskkill /T,不需要 detached
|
|
224
|
+
detached: !isWin,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
catch (e) {
|
|
228
|
+
return '拉起 cursor-agent 失败:' + e;
|
|
229
|
+
}
|
|
230
|
+
this.child = child;
|
|
231
|
+
this.startedAt = Date.now();
|
|
232
|
+
this.taskBrief = task.length > 40 ? task.slice(0, 40) + '…' : task;
|
|
233
|
+
this.stopRequested = false;
|
|
234
|
+
clog(`CLI 会话已拉起: pid=${child.pid} cwd=${workDir} model=${this.model()}`);
|
|
235
|
+
let timedOut = false;
|
|
236
|
+
const killTimer = setTimeout(() => {
|
|
237
|
+
timedOut = true;
|
|
238
|
+
clog('会话超过时长上限,强制终止');
|
|
239
|
+
CliLauncher.killTree(child, 'SIGKILL');
|
|
240
|
+
}, CliLauncher.SESSION_MAX_MS);
|
|
241
|
+
child.stdout?.on('data', (chunk) => { stdout = append(stdout, chunk); });
|
|
242
|
+
child.stderr?.on('data', (chunk) => { stderr = append(stderr, chunk); });
|
|
243
|
+
const finish = (code) => {
|
|
244
|
+
clearTimeout(killTimer);
|
|
245
|
+
const elapsedMs = Date.now() - this.startedAt;
|
|
246
|
+
const stopped = this.stopRequested;
|
|
247
|
+
this.child = null;
|
|
248
|
+
clog(`CLI 会话结束: code=${code} elapsed=${Math.round(elapsedMs / 1000)}s timedOut=${timedOut} stopped=${stopped}`);
|
|
249
|
+
onDone({
|
|
250
|
+
code,
|
|
251
|
+
output: stdout.trim(),
|
|
252
|
+
errorOutput: stderr.trim(),
|
|
253
|
+
timedOut,
|
|
254
|
+
stopped,
|
|
255
|
+
elapsedMs,
|
|
256
|
+
});
|
|
257
|
+
};
|
|
258
|
+
child.on('error', (err) => {
|
|
259
|
+
// spawn 失败(如二进制不存在)也走统一收尾,错误进 errorOutput
|
|
260
|
+
stderr = append(stderr, Buffer.from(String(err)));
|
|
261
|
+
finish(null);
|
|
262
|
+
});
|
|
263
|
+
child.on('close', (code) => finish(code));
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
/** 用户 /stop 主动终止会话。返回是否有会话被终止。 */
|
|
267
|
+
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');
|
|
277
|
+
}
|
|
278
|
+
}, 5000);
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* 终止整个进程树:POSIX 杀进程组(detached spawn 后 child.pid 即组长),
|
|
283
|
+
* Windows 用 taskkill /T 递归终止,连 agent 自己拉的子进程一起清理。
|
|
284
|
+
*/
|
|
285
|
+
static killTree(child, signal) {
|
|
286
|
+
if (!child.pid)
|
|
287
|
+
return;
|
|
288
|
+
if (process.platform === 'win32') {
|
|
289
|
+
try {
|
|
290
|
+
(0, child_process_1.spawn)('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore' });
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
try {
|
|
294
|
+
child.kill();
|
|
295
|
+
}
|
|
296
|
+
catch { /* 进程可能已退出 */ }
|
|
297
|
+
}
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
process.kill(-child.pid, signal);
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
try {
|
|
305
|
+
child.kill(signal);
|
|
306
|
+
}
|
|
307
|
+
catch { /* 进程可能已退出 */ }
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
exports.CliLauncher = CliLauncher;
|
|
312
|
+
/** 默认模型:实测非交互模式下该模型 + maxMode=false 固定扣 1 次请求 */
|
|
313
|
+
CliLauncher.DEFAULT_MODEL = 'claude-fable-5-thinking-max';
|
|
314
|
+
/** 会话时长兜底:防止无人回复的 headless 会话无限续期挂着 */
|
|
315
|
+
CliLauncher.SESSION_MAX_MS = 3 * 60 * 60 * 1000;
|
|
316
|
+
/** stdout/stderr 只保留尾部,防止长会话把内存吃爆 */
|
|
317
|
+
CliLauncher.OUTPUT_TAIL_LIMIT = 16000;
|
|
318
|
+
//# sourceMappingURL=cli-launcher.js.map
|