@wu529778790/open-im 1.2.4-beta.3 → 1.3.1-beta.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/README.md +3 -0
- package/dist/adapters/cursor-adapter.d.ts +11 -0
- package/dist/adapters/cursor-adapter.js +50 -0
- package/dist/adapters/registry.js +5 -0
- package/dist/commands/handler.d.ts +1 -1
- package/dist/commands/handler.js +4 -3
- package/dist/config.d.ts +1 -0
- package/dist/config.js +53 -1
- package/dist/cursor/cli-runner.d.ts +33 -0
- package/dist/cursor/cli-runner.js +265 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,6 +55,7 @@ open-im start
|
|
|
55
55
|
| `ALLOWED_USER_IDS` | 白名单(逗号分隔,空=所有人) |
|
|
56
56
|
| `CLAUDE_WORK_DIR` | 工作目录,默认当前目录 |
|
|
57
57
|
| `ALLOWED_BASE_DIRS` | 允许访问的目录(逗号分隔) |
|
|
58
|
+
| `CURSOR_API_KEY` | Cursor Agent API Key(使用 cursor 时必填,或先运行 agent login) |
|
|
58
59
|
|
|
59
60
|
### Claude API 配置
|
|
60
61
|
|
|
@@ -160,3 +161,5 @@ EOF
|
|
|
160
161
|
**飞书卡片报错**:未配置卡片回调,使用命令替代:`/mode ask`、`/mode yolo`
|
|
161
162
|
|
|
162
163
|
**企业微信收不到通知**:需先发一条消息给机器人,才能接收启动通知
|
|
164
|
+
|
|
165
|
+
**Cursor 报 Authentication required**:需先认证。方式 1:在终端运行 `agent login`;方式 2:在 `~/.open-im/config.json` 的 `env` 中添加 `"CURSOR_API_KEY": "你的 API Key"`
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor Adapter - 通过 Cursor Agent CLI 执行任务
|
|
3
|
+
* 需要预先安装: curl https://cursor.com/install -fsSL | bash
|
|
4
|
+
*/
|
|
5
|
+
import type { ToolAdapter, RunCallbacks, RunOptions, RunHandle } from './tool-adapter.interface.js';
|
|
6
|
+
export declare class CursorAdapter implements ToolAdapter {
|
|
7
|
+
private cliPath;
|
|
8
|
+
readonly toolId = "cursor";
|
|
9
|
+
constructor(cliPath: string);
|
|
10
|
+
run(prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: RunOptions): RunHandle;
|
|
11
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor Adapter - 通过 Cursor Agent CLI 执行任务
|
|
3
|
+
* 需要预先安装: curl https://cursor.com/install -fsSL | bash
|
|
4
|
+
*/
|
|
5
|
+
import { runCursor } from '../cursor/cli-runner.js';
|
|
6
|
+
import { createLogger } from '../logger.js';
|
|
7
|
+
const log = createLogger('CursorAdapter');
|
|
8
|
+
export class CursorAdapter {
|
|
9
|
+
cliPath;
|
|
10
|
+
toolId = 'cursor';
|
|
11
|
+
constructor(cliPath) {
|
|
12
|
+
this.cliPath = cliPath;
|
|
13
|
+
}
|
|
14
|
+
run(prompt, sessionId, workDir, callbacks, options) {
|
|
15
|
+
const opts = {
|
|
16
|
+
skipPermissions: options?.skipPermissions,
|
|
17
|
+
permissionMode: options?.permissionMode,
|
|
18
|
+
timeoutMs: options?.timeoutMs,
|
|
19
|
+
model: options?.model,
|
|
20
|
+
chatId: options?.chatId,
|
|
21
|
+
hookPort: options?.hookPort,
|
|
22
|
+
};
|
|
23
|
+
return runCursor(this.cliPath, prompt, sessionId, workDir, {
|
|
24
|
+
onText: callbacks.onText,
|
|
25
|
+
onThinking: callbacks.onThinking,
|
|
26
|
+
onToolUse: callbacks.onToolUse,
|
|
27
|
+
onComplete: (raw) => {
|
|
28
|
+
const result = {
|
|
29
|
+
success: raw.success,
|
|
30
|
+
result: raw.result,
|
|
31
|
+
accumulated: raw.accumulated,
|
|
32
|
+
cost: raw.cost,
|
|
33
|
+
durationMs: raw.durationMs,
|
|
34
|
+
model: raw.model,
|
|
35
|
+
numTurns: raw.numTurns,
|
|
36
|
+
toolStats: raw.toolStats,
|
|
37
|
+
};
|
|
38
|
+
callbacks.onComplete(result);
|
|
39
|
+
},
|
|
40
|
+
onError: (err) => {
|
|
41
|
+
const msg = typeof err === 'string' ? err : String(err);
|
|
42
|
+
const friendly = msg.includes('Authentication required') || msg.includes('agent login')
|
|
43
|
+
? 'Cursor 需要先登录。请在终端运行 agent login,或在 ~/.open-im/config.json 的 env 中添加 "CURSOR_API_KEY"。'
|
|
44
|
+
: msg;
|
|
45
|
+
callbacks.onError(friendly);
|
|
46
|
+
},
|
|
47
|
+
onSessionId: callbacks.onSessionId,
|
|
48
|
+
}, opts);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ClaudeAdapter } from './claude-adapter.js';
|
|
2
2
|
import { ClaudeSDKAdapter } from './claude-sdk-adapter.js';
|
|
3
|
+
import { CursorAdapter } from './cursor-adapter.js';
|
|
3
4
|
const adapters = new Map();
|
|
4
5
|
export function initAdapters(config) {
|
|
5
6
|
adapters.clear();
|
|
@@ -16,6 +17,10 @@ export function initAdapters(config) {
|
|
|
16
17
|
}));
|
|
17
18
|
}
|
|
18
19
|
}
|
|
20
|
+
else if (config.aiCommand === 'cursor') {
|
|
21
|
+
console.log('🖱️ 使用 Cursor Agent CLI 适配器');
|
|
22
|
+
adapters.set('cursor', new CursorAdapter(config.cursorCliPath));
|
|
23
|
+
}
|
|
19
24
|
}
|
|
20
25
|
export function getAdapter(aiCommand) {
|
|
21
26
|
return adapters.get(aiCommand);
|
package/dist/commands/handler.js
CHANGED
|
@@ -107,7 +107,7 @@ export class CommandHandler {
|
|
|
107
107
|
return true;
|
|
108
108
|
}
|
|
109
109
|
async handleStatus(chatId, userId) {
|
|
110
|
-
const version = await this.
|
|
110
|
+
const version = await this.getAiVersion();
|
|
111
111
|
const workDir = this.deps.sessionManager.getWorkDir(userId);
|
|
112
112
|
const convId = this.deps.sessionManager.getConvId(userId);
|
|
113
113
|
const sessionId = this.deps.sessionManager.getSessionIdForConv(userId, convId);
|
|
@@ -166,9 +166,10 @@ export class CommandHandler {
|
|
|
166
166
|
}
|
|
167
167
|
return true;
|
|
168
168
|
}
|
|
169
|
-
|
|
169
|
+
getAiVersion() {
|
|
170
|
+
const cmd = this.deps.config.aiCommand === 'cursor' ? this.deps.config.cursorCliPath : this.deps.config.claudeCliPath;
|
|
170
171
|
return new Promise((resolve) => {
|
|
171
|
-
execFile(
|
|
172
|
+
execFile(cmd, ['--version'], { timeout: 5000 }, (err, stdout) => {
|
|
172
173
|
resolve(err ? '未知' : (stdout?.toString().trim() || '未知'));
|
|
173
174
|
});
|
|
174
175
|
});
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -183,6 +183,18 @@ export function loadConfig() {
|
|
|
183
183
|
// 5. AI / 工作目录 / 安全配置
|
|
184
184
|
const aiCommand = (process.env.AI_COMMAND ?? file.aiCommand ?? 'claude');
|
|
185
185
|
const claudeCliPath = process.env.CLAUDE_CLI_PATH ?? file.claudeCliPath ?? 'claude';
|
|
186
|
+
let cursorCliPath = process.env.CURSOR_CLI_PATH ?? file.cursorCliPath ?? 'agent';
|
|
187
|
+
// Windows: spawn 无法解析 .cmd,需使用完整路径(Cursor 默认安装在 %LOCALAPPDATA%\cursor-agent\agent.cmd)
|
|
188
|
+
if (process.platform === 'win32' && cursorCliPath === 'agent') {
|
|
189
|
+
const winAgentPath = join(process.env.LOCALAPPDATA || '', 'cursor-agent', 'agent.cmd');
|
|
190
|
+
try {
|
|
191
|
+
accessSync(winAgentPath, constants.F_OK);
|
|
192
|
+
cursorCliPath = winAgentPath;
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
/* 使用默认 agent,由后续 where 校验 */
|
|
196
|
+
}
|
|
197
|
+
}
|
|
186
198
|
const claudeWorkDir = process.env.CLAUDE_WORK_DIR ?? file.claudeWorkDir ?? process.cwd();
|
|
187
199
|
const allowedBaseDirs = process.env.ALLOWED_BASE_DIRS !== undefined
|
|
188
200
|
? parseCommaSeparated(process.env.ALLOWED_BASE_DIRS)
|
|
@@ -235,7 +247,46 @@ export function loadConfig() {
|
|
|
235
247
|
throw new Error(errorMsg);
|
|
236
248
|
}
|
|
237
249
|
}
|
|
238
|
-
// 7. 校验
|
|
250
|
+
// 7. 校验 Cursor CLI(使用 cursor 时)
|
|
251
|
+
if (aiCommand === 'cursor') {
|
|
252
|
+
if (isAbsolute(cursorCliPath) || cursorCliPath.includes('/') || cursorCliPath.includes('\\')) {
|
|
253
|
+
try {
|
|
254
|
+
accessSync(cursorCliPath, constants.F_OK);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
throw new Error(`Cursor CLI 不可执行: ${cursorCliPath}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
const checkCommand = process.platform === 'win32' ? 'where' : 'which';
|
|
262
|
+
try {
|
|
263
|
+
execFileSync(checkCommand, [cursorCliPath], { stdio: 'pipe' });
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
const installGuide = [
|
|
267
|
+
'',
|
|
268
|
+
'━━━ Cursor CLI 未安装 ━━━',
|
|
269
|
+
'',
|
|
270
|
+
'使用 Cursor 需要先安装 Cursor Agent CLI。',
|
|
271
|
+
'',
|
|
272
|
+
'安装方法:',
|
|
273
|
+
'',
|
|
274
|
+
' macOS/Linux: curl https://cursor.com/install -fsSL | bash',
|
|
275
|
+
' Windows: irm \'https://cursor.com/install?win32=true\' | iex',
|
|
276
|
+
'',
|
|
277
|
+
'安装后运行 agent --version 验证。',
|
|
278
|
+
'',
|
|
279
|
+
].join('\n');
|
|
280
|
+
throw new Error(installGuide);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// 提示 Cursor 认证:需 agent login 或 CURSOR_API_KEY
|
|
284
|
+
if (!process.env.CURSOR_API_KEY) {
|
|
285
|
+
console.warn('\n⚠ Cursor 模式:未检测到 CURSOR_API_KEY。首次使用请先运行 agent login,\n' +
|
|
286
|
+
' 或在 ~/.open-im/config.json 的 env 中添加 "CURSOR_API_KEY": "你的 API Key"。\n');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// 8. 校验 Claude CLI(SDK 模式不需要 CLI)
|
|
239
290
|
if (aiCommand === 'claude' && !useSdkMode) {
|
|
240
291
|
if (isAbsolute(claudeCliPath) || claudeCliPath.includes('/') || claudeCliPath.includes('\\')) {
|
|
241
292
|
try {
|
|
@@ -352,6 +403,7 @@ export function loadConfig() {
|
|
|
352
403
|
weworkAllowedUserIds,
|
|
353
404
|
aiCommand,
|
|
354
405
|
claudeCliPath,
|
|
406
|
+
cursorCliPath,
|
|
355
407
|
claudeWorkDir,
|
|
356
408
|
allowedBaseDirs,
|
|
357
409
|
claudeSkipPermissions,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor CLI Runner - 解析 Cursor Agent 的 stream-json 输出
|
|
3
|
+
* 参考: https://cursor.com/docs/cli/reference/output-format
|
|
4
|
+
*/
|
|
5
|
+
export interface CursorRunCallbacks {
|
|
6
|
+
onText: (accumulated: string) => void;
|
|
7
|
+
onThinking?: (accumulated: string) => void;
|
|
8
|
+
onToolUse?: (toolName: string, toolInput?: Record<string, unknown>) => void;
|
|
9
|
+
onComplete: (result: {
|
|
10
|
+
success: boolean;
|
|
11
|
+
result: string;
|
|
12
|
+
accumulated: string;
|
|
13
|
+
cost: number;
|
|
14
|
+
durationMs: number;
|
|
15
|
+
model?: string;
|
|
16
|
+
numTurns: number;
|
|
17
|
+
toolStats: Record<string, number>;
|
|
18
|
+
}) => void;
|
|
19
|
+
onError: (error: string) => void;
|
|
20
|
+
onSessionId?: (sessionId: string) => void;
|
|
21
|
+
}
|
|
22
|
+
export interface CursorRunOptions {
|
|
23
|
+
skipPermissions?: boolean;
|
|
24
|
+
permissionMode?: 'default' | 'acceptEdits' | 'plan';
|
|
25
|
+
timeoutMs?: number;
|
|
26
|
+
model?: string;
|
|
27
|
+
chatId?: string;
|
|
28
|
+
hookPort?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface CursorRunHandle {
|
|
31
|
+
abort: () => void;
|
|
32
|
+
}
|
|
33
|
+
export declare function runCursor(cliPath: string, prompt: string, sessionId: string | undefined, workDir: string, callbacks: CursorRunCallbacks, options?: CursorRunOptions): CursorRunHandle;
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor CLI Runner - 解析 Cursor Agent 的 stream-json 输出
|
|
3
|
+
* 参考: https://cursor.com/docs/cli/reference/output-format
|
|
4
|
+
*/
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { createInterface } from 'node:readline';
|
|
7
|
+
import { createLogger } from '../logger.js';
|
|
8
|
+
const log = createLogger('CursorCli');
|
|
9
|
+
/** 从 Cursor tool_call 事件提取工具名和参数 */
|
|
10
|
+
function extractToolFromCursorEvent(event) {
|
|
11
|
+
const toolCall = event.tool_call;
|
|
12
|
+
if (!toolCall || typeof toolCall !== 'object')
|
|
13
|
+
return null;
|
|
14
|
+
const keys = Object.keys(toolCall).filter((k) => k !== 'result');
|
|
15
|
+
if (keys.length === 0)
|
|
16
|
+
return null;
|
|
17
|
+
const key = keys[0];
|
|
18
|
+
const val = toolCall[key];
|
|
19
|
+
if (!val)
|
|
20
|
+
return null;
|
|
21
|
+
let name = key;
|
|
22
|
+
if (key === 'readToolCall')
|
|
23
|
+
name = 'Read';
|
|
24
|
+
else if (key === 'writeToolCall')
|
|
25
|
+
name = 'Write';
|
|
26
|
+
else if (key === 'editToolCall')
|
|
27
|
+
name = 'Edit';
|
|
28
|
+
else if (key === 'bashToolCall')
|
|
29
|
+
name = 'Bash';
|
|
30
|
+
else if (key === 'grepToolCall')
|
|
31
|
+
name = 'Grep';
|
|
32
|
+
else if (key === 'globToolCall')
|
|
33
|
+
name = 'Glob';
|
|
34
|
+
else if (key === 'webSearchToolCall')
|
|
35
|
+
name = 'WebSearch';
|
|
36
|
+
else if (key === 'webFetchToolCall')
|
|
37
|
+
name = 'WebFetch';
|
|
38
|
+
else if (key === 'function') {
|
|
39
|
+
const fn = val;
|
|
40
|
+
name = fn.name ?? 'unknown';
|
|
41
|
+
try {
|
|
42
|
+
const input = fn.arguments ? JSON.parse(fn.arguments) : undefined;
|
|
43
|
+
return { name, input };
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return { name };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const args = val.args;
|
|
50
|
+
return { name, input: args };
|
|
51
|
+
}
|
|
52
|
+
export function runCursor(cliPath, prompt, sessionId, workDir, callbacks, options) {
|
|
53
|
+
const args = ['-p', '--output-format', 'stream-json', '--stream-partial-output'];
|
|
54
|
+
if (options?.skipPermissions) {
|
|
55
|
+
args.push('--force');
|
|
56
|
+
}
|
|
57
|
+
else if (options?.permissionMode === 'plan') {
|
|
58
|
+
args.push('--plan');
|
|
59
|
+
}
|
|
60
|
+
else if (options?.permissionMode === 'acceptEdits') {
|
|
61
|
+
args.push('--trust');
|
|
62
|
+
}
|
|
63
|
+
if (options?.model)
|
|
64
|
+
args.push('--model', options.model);
|
|
65
|
+
if (sessionId)
|
|
66
|
+
args.push('--resume', sessionId);
|
|
67
|
+
args.push('--workspace', workDir);
|
|
68
|
+
args.push('--', prompt);
|
|
69
|
+
const env = {};
|
|
70
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
71
|
+
if (v !== undefined)
|
|
72
|
+
env[k] = v;
|
|
73
|
+
}
|
|
74
|
+
if (options?.chatId)
|
|
75
|
+
env.CC_IM_CHAT_ID = options.chatId;
|
|
76
|
+
if (options?.hookPort)
|
|
77
|
+
env.CC_IM_HOOK_PORT = String(options.hookPort);
|
|
78
|
+
log.info(`Spawning Cursor CLI: path=${cliPath}, cwd=${workDir}, session=${sessionId ?? 'new'}`);
|
|
79
|
+
// Windows: .cmd 需通过 cmd.exe 执行,否则 spawn 报 ENOENT
|
|
80
|
+
const isCmd = process.platform === 'win32' && /\.cmd$/i.test(cliPath);
|
|
81
|
+
const spawnCmd = isCmd ? 'cmd.exe' : cliPath;
|
|
82
|
+
const spawnArgs = isCmd ? ['/c', cliPath, ...args] : args;
|
|
83
|
+
const child = spawn(spawnCmd, spawnArgs, {
|
|
84
|
+
cwd: workDir,
|
|
85
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
86
|
+
env,
|
|
87
|
+
windowsHide: process.platform === 'win32',
|
|
88
|
+
});
|
|
89
|
+
let accumulated = '';
|
|
90
|
+
let completed = false;
|
|
91
|
+
let model = '';
|
|
92
|
+
const toolStats = {};
|
|
93
|
+
const MAX_TIMEOUT = 2_147_483_647;
|
|
94
|
+
const timeoutMs = options?.timeoutMs && options.timeoutMs > 0
|
|
95
|
+
? Math.min(options.timeoutMs, MAX_TIMEOUT)
|
|
96
|
+
: 0;
|
|
97
|
+
let timeoutHandle = null;
|
|
98
|
+
if (timeoutMs > 0) {
|
|
99
|
+
timeoutHandle = setTimeout(() => {
|
|
100
|
+
if (!completed && !child.killed) {
|
|
101
|
+
completed = true;
|
|
102
|
+
log.warn(`Cursor CLI timeout after ${timeoutMs}ms, killing pid=${child.pid}`);
|
|
103
|
+
child.kill('SIGTERM');
|
|
104
|
+
callbacks.onError(`执行超时(${timeoutMs}ms),已终止进程`);
|
|
105
|
+
}
|
|
106
|
+
}, timeoutMs);
|
|
107
|
+
}
|
|
108
|
+
const MAX_STDERR_HEAD = 4 * 1024;
|
|
109
|
+
const MAX_STDERR_TAIL = 6 * 1024;
|
|
110
|
+
let stderrHead = '';
|
|
111
|
+
let stderrTail = '';
|
|
112
|
+
let stderrTotal = 0;
|
|
113
|
+
let stderrHeadFull = false;
|
|
114
|
+
child.stderr?.on('data', (chunk) => {
|
|
115
|
+
const text = chunk.toString();
|
|
116
|
+
stderrTotal += text.length;
|
|
117
|
+
if (!stderrHeadFull) {
|
|
118
|
+
const room = MAX_STDERR_HEAD - stderrHead.length;
|
|
119
|
+
if (room > 0) {
|
|
120
|
+
stderrHead += text.slice(0, room);
|
|
121
|
+
if (stderrHead.length >= MAX_STDERR_HEAD)
|
|
122
|
+
stderrHeadFull = true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
stderrTail += text;
|
|
126
|
+
if (stderrTail.length > MAX_STDERR_TAIL) {
|
|
127
|
+
stderrTail = stderrTail.slice(-MAX_STDERR_TAIL);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
const rl = createInterface({ input: child.stdout });
|
|
131
|
+
rl.on('line', (line) => {
|
|
132
|
+
const trimmed = line.trim();
|
|
133
|
+
if (!trimmed)
|
|
134
|
+
return;
|
|
135
|
+
let event;
|
|
136
|
+
try {
|
|
137
|
+
event = JSON.parse(trimmed);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const type = event.type;
|
|
143
|
+
if (type === 'system' && event.subtype === 'init') {
|
|
144
|
+
model = event.model ?? '';
|
|
145
|
+
const sid = event.session_id;
|
|
146
|
+
if (sid)
|
|
147
|
+
callbacks.onSessionId?.(sid);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (type === 'assistant') {
|
|
151
|
+
const msg = event.message;
|
|
152
|
+
const content = msg?.content;
|
|
153
|
+
if (Array.isArray(content)) {
|
|
154
|
+
for (const block of content) {
|
|
155
|
+
if (block?.type === 'text' && block.text) {
|
|
156
|
+
accumulated += block.text;
|
|
157
|
+
callbacks.onText(accumulated);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (type === 'tool_call') {
|
|
164
|
+
const subtype = event.subtype;
|
|
165
|
+
if (subtype === 'started') {
|
|
166
|
+
const tool = extractToolFromCursorEvent(event);
|
|
167
|
+
if (tool) {
|
|
168
|
+
toolStats[tool.name] = (toolStats[tool.name] || 0) + 1;
|
|
169
|
+
callbacks.onToolUse?.(tool.name, tool.input);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (type === 'result' && event.subtype === 'success') {
|
|
175
|
+
completed = true;
|
|
176
|
+
if (timeoutHandle)
|
|
177
|
+
clearTimeout(timeoutHandle);
|
|
178
|
+
const result = event.result ?? '';
|
|
179
|
+
if (!accumulated && result)
|
|
180
|
+
accumulated = result;
|
|
181
|
+
callbacks.onComplete({
|
|
182
|
+
success: true,
|
|
183
|
+
result,
|
|
184
|
+
accumulated,
|
|
185
|
+
cost: 0,
|
|
186
|
+
durationMs: event.duration_ms ?? 0,
|
|
187
|
+
model,
|
|
188
|
+
numTurns: 0,
|
|
189
|
+
toolStats,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
let exitCode = null;
|
|
194
|
+
let rlClosed = false;
|
|
195
|
+
let childClosed = false;
|
|
196
|
+
const finalize = () => {
|
|
197
|
+
if (!rlClosed || !childClosed)
|
|
198
|
+
return;
|
|
199
|
+
if (timeoutHandle)
|
|
200
|
+
clearTimeout(timeoutHandle);
|
|
201
|
+
if (!completed) {
|
|
202
|
+
if (exitCode !== null && exitCode !== 0) {
|
|
203
|
+
let errMsg = '';
|
|
204
|
+
if (stderrTotal > 0) {
|
|
205
|
+
if (!stderrHeadFull) {
|
|
206
|
+
errMsg = stderrHead;
|
|
207
|
+
}
|
|
208
|
+
else if (stderrTotal <= MAX_STDERR_HEAD + MAX_STDERR_TAIL) {
|
|
209
|
+
errMsg = stderrHead + stderrTail.slice(stderrTail.length - (stderrTotal - MAX_STDERR_HEAD));
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
errMsg =
|
|
213
|
+
stderrHead +
|
|
214
|
+
`\n\n... (省略 ${stderrTotal - MAX_STDERR_HEAD - MAX_STDERR_TAIL} 字节) ...\n\n` +
|
|
215
|
+
stderrTail;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
callbacks.onError(errMsg || `Cursor CLI exited with code ${exitCode}`);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
callbacks.onComplete({
|
|
222
|
+
success: true,
|
|
223
|
+
result: accumulated,
|
|
224
|
+
accumulated,
|
|
225
|
+
cost: 0,
|
|
226
|
+
durationMs: 0,
|
|
227
|
+
model,
|
|
228
|
+
numTurns: 0,
|
|
229
|
+
toolStats,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
child.on('close', (code) => {
|
|
235
|
+
log.info(`Cursor CLI closed: exitCode=${code}, pid=${child.pid}`);
|
|
236
|
+
exitCode = code;
|
|
237
|
+
childClosed = true;
|
|
238
|
+
finalize();
|
|
239
|
+
});
|
|
240
|
+
rl.on('close', () => {
|
|
241
|
+
rlClosed = true;
|
|
242
|
+
finalize();
|
|
243
|
+
});
|
|
244
|
+
child.on('error', (err) => {
|
|
245
|
+
const errorCode = err.code;
|
|
246
|
+
log.error(`Cursor CLI spawn error: ${err.message}, code=${errorCode}, path=${cliPath}`);
|
|
247
|
+
if (timeoutHandle)
|
|
248
|
+
clearTimeout(timeoutHandle);
|
|
249
|
+
if (!completed) {
|
|
250
|
+
completed = true;
|
|
251
|
+
callbacks.onError(`Failed to start Cursor CLI: ${err.message}`);
|
|
252
|
+
}
|
|
253
|
+
childClosed = true;
|
|
254
|
+
finalize();
|
|
255
|
+
});
|
|
256
|
+
return {
|
|
257
|
+
abort: () => {
|
|
258
|
+
if (timeoutHandle)
|
|
259
|
+
clearTimeout(timeoutHandle);
|
|
260
|
+
rl.close();
|
|
261
|
+
if (!child.killed)
|
|
262
|
+
child.kill('SIGTERM');
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
}
|