@wu529778790/open-im 1.3.1-beta.0 → 1.3.1-beta.1

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 CHANGED
@@ -55,7 +55,6 @@ 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) |
59
58
 
60
59
  ### Claude API 配置
61
60
 
@@ -161,5 +160,3 @@ EOF
161
160
  **飞书卡片报错**:未配置卡片回调,使用命令替代:`/mode ask`、`/mode yolo`
162
161
 
163
162
  **企业微信收不到通知**:需先发一条消息给机器人,才能接收启动通知
164
-
165
- **Cursor 报 Authentication required**:需先认证。方式 1:在终端运行 `agent login`;方式 2:在 `~/.open-im/config.json` 的 `env` 中添加 `"CURSOR_API_KEY": "你的 API Key"`
@@ -117,9 +117,18 @@ export class ClaudeSDKAdapter {
117
117
  queryClosed = true;
118
118
  const m = msg;
119
119
  const success = m.subtype === 'success';
120
+ const errs = m.errors ?? [];
121
+ const noConvErr = errs.find((e) => e.includes('No conversation found with session ID'));
122
+ if (!success && noConvErr) {
123
+ log.warn(`SDK session invalid: ${noConvErr}`);
124
+ callbacks.onSessionInvalid?.();
125
+ callbacks.onError('会话已过期,请发送 /new 开始新会话');
126
+ return;
127
+ }
128
+ const resultText = m.result ?? '';
120
129
  const result = {
121
130
  success,
122
- result: m.result ?? '',
131
+ result: resultText,
123
132
  accumulated: success ? accumulated : '',
124
133
  cost: m.total_cost_usd ?? 0,
125
134
  durationMs: m.duration_ms ?? 0,
@@ -128,6 +137,17 @@ export class ClaudeSDKAdapter {
128
137
  };
129
138
  if (!result.accumulated && result.result)
130
139
  result.accumulated = result.result;
140
+ if (!result.accumulated && !result.result && accumulated) {
141
+ log.debug(`Result event had no text but accumulated=${accumulated.length} chars, using accumulated`);
142
+ result.accumulated = accumulated;
143
+ result.result = accumulated;
144
+ }
145
+ if (!result.accumulated && !result.result) {
146
+ const errMsg = errs[0] ?? '未知错误';
147
+ log.warn(`SDK result empty: subtype=${m.subtype}, errors=${JSON.stringify(errs)}`);
148
+ callbacks.onError(errMsg);
149
+ return;
150
+ }
131
151
  callbacks.onComplete(result);
132
152
  return;
133
153
  }
@@ -1,6 +1,5 @@
1
1
  import { ClaudeAdapter } from './claude-adapter.js';
2
2
  import { ClaudeSDKAdapter } from './claude-sdk-adapter.js';
3
- import { CursorAdapter } from './cursor-adapter.js';
4
3
  const adapters = new Map();
5
4
  export function initAdapters(config) {
6
5
  adapters.clear();
@@ -17,10 +16,6 @@ export function initAdapters(config) {
17
16
  }));
18
17
  }
19
18
  }
20
- else if (config.aiCommand === 'cursor') {
21
- console.log('🖱️ 使用 Cursor Agent CLI 适配器');
22
- adapters.set('cursor', new CursorAdapter(config.cursorCliPath));
23
- }
24
19
  }
25
20
  export function getAdapter(aiCommand) {
26
21
  return adapters.get(aiCommand);
@@ -18,6 +18,8 @@ export interface RunCallbacks {
18
18
  onComplete: (result: ParsedResult) => void;
19
19
  onError: (error: string) => void;
20
20
  onSessionId?: (sessionId: string) => void;
21
+ /** SDK 报 "No conversation found" 时调用,用于清除无效 session */
22
+ onSessionInvalid?: () => void;
21
23
  }
22
24
  export interface RunOptions {
23
25
  skipPermissions?: boolean;
@@ -31,7 +31,7 @@ export declare class CommandHandler {
31
31
  private handleCd;
32
32
  private handleAllow;
33
33
  private handleDeny;
34
- private getAiVersion;
34
+ private getClaudeVersion;
35
35
  }
36
36
  /**
37
37
  * 列出目录并返回目录信息
@@ -107,7 +107,7 @@ export class CommandHandler {
107
107
  return true;
108
108
  }
109
109
  async handleStatus(chatId, userId) {
110
- const version = await this.getAiVersion();
110
+ const version = await this.getClaudeVersion();
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,10 +166,9 @@ export class CommandHandler {
166
166
  }
167
167
  return true;
168
168
  }
169
- getAiVersion() {
170
- const cmd = this.deps.config.aiCommand === 'cursor' ? this.deps.config.cursorCliPath : this.deps.config.claudeCliPath;
169
+ getClaudeVersion() {
171
170
  return new Promise((resolve) => {
172
- execFile(cmd, ['--version'], { timeout: 5000 }, (err, stdout) => {
171
+ execFile(this.deps.config.claudeCliPath, ['--version'], { timeout: 5000 }, (err, stdout) => {
173
172
  resolve(err ? '未知' : (stdout?.toString().trim() || '未知'));
174
173
  });
175
174
  });
package/dist/config.d.ts CHANGED
@@ -24,7 +24,6 @@ export interface Config {
24
24
  weworkAllowedUserIds: string[];
25
25
  aiCommand: AiCommand;
26
26
  claudeCliPath: string;
27
- cursorCliPath: string;
28
27
  claudeWorkDir: string;
29
28
  allowedBaseDirs: string[];
30
29
  claudeSkipPermissions: boolean;
package/dist/config.js CHANGED
@@ -183,18 +183,6 @@ 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
- }
198
186
  const claudeWorkDir = process.env.CLAUDE_WORK_DIR ?? file.claudeWorkDir ?? process.cwd();
199
187
  const allowedBaseDirs = process.env.ALLOWED_BASE_DIRS !== undefined
200
188
  ? parseCommaSeparated(process.env.ALLOWED_BASE_DIRS)
@@ -247,46 +235,7 @@ export function loadConfig() {
247
235
  throw new Error(errorMsg);
248
236
  }
249
237
  }
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)
238
+ // 7. 校验 Claude CLI(SDK 模式不需要 CLI)
290
239
  if (aiCommand === 'claude' && !useSdkMode) {
291
240
  if (isAbsolute(claudeCliPath) || claudeCliPath.includes('/') || claudeCliPath.includes('\\')) {
292
241
  try {
@@ -403,7 +352,6 @@ export function loadConfig() {
403
352
  weworkAllowedUserIds,
404
353
  aiCommand,
405
354
  claudeCliPath,
406
- cursorCliPath,
407
355
  claudeWorkDir,
408
356
  allowedBaseDirs,
409
357
  claudeSkipPermissions,
@@ -7,6 +7,8 @@ export declare class SessionManager {
7
7
  constructor(defaultWorkDir: string, allowedBaseDirs: string[]);
8
8
  getSessionIdForConv(userId: string, convId: string): string | undefined;
9
9
  setSessionIdForConv(userId: string, convId: string, sessionId: string): void;
10
+ /** 清除指定会话的 sessionId(用于 SDK 报 "No conversation found" 时) */
11
+ clearSessionForConv(userId: string, convId: string): void;
10
12
  getSessionIdForThread(_userId: string, _threadId: string): string | undefined;
11
13
  setSessionIdForThread(userId: string, threadId: string, sessionId: string): void;
12
14
  getWorkDir(userId: string): string;
@@ -33,6 +33,16 @@ export class SessionManager {
33
33
  this.convSessionMap.set(`${userId}:${convId}`, sessionId);
34
34
  }
35
35
  }
36
+ /** 清除指定会话的 sessionId(用于 SDK 报 "No conversation found" 时) */
37
+ clearSessionForConv(userId, convId) {
38
+ const s = this.sessions.get(userId);
39
+ if (s?.activeConvId === convId) {
40
+ s.sessionId = undefined;
41
+ this.save();
42
+ }
43
+ this.convSessionMap.delete(`${userId}:${convId}`);
44
+ log.info(`Cleared session for user ${userId}, convId=${convId}`);
45
+ }
36
46
  getSessionIdForThread(_userId, _threadId) {
37
47
  return undefined;
38
48
  }
@@ -103,6 +103,10 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
103
103
  else if (ctx.convId)
104
104
  sessionManager.setSessionIdForConv(ctx.userId, ctx.convId, id);
105
105
  },
106
+ onSessionInvalid: () => {
107
+ if (ctx.convId)
108
+ sessionManager.clearSessionForConv(ctx.userId, ctx.convId);
109
+ },
106
110
  onThinking: (t) => {
107
111
  if (!firstContentLogged) {
108
112
  firstContentLogged = true;
@@ -147,9 +151,19 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
147
151
  pendingUpdate = null;
148
152
  }
149
153
  const note = buildCompletionNote(result, sessionManager, ctx, mode);
150
- const finalContent = result.accumulated || result.result || '(无输出)';
154
+ // 优先用 adapter 返回的 accumulated/result;若为空则用流式期间累积的 latestContent(SDK 有时 result 事件不携带文本)
155
+ const output = result.accumulated ||
156
+ result.result ||
157
+ taskState.latestContent ||
158
+ '(无输出)';
159
+ if (!result.accumulated && !result.result && taskState.latestContent) {
160
+ log.warn(`Empty AI output from adapter but had streamed content (${taskState.latestContent.length} chars), using latestContent. platform=${ctx.platform}, taskKey=${ctx.taskKey}`);
161
+ }
162
+ else if (!output || output === '(无输出)') {
163
+ log.warn(`Empty AI output for user ${ctx.userId}, platform=${ctx.platform}, taskKey=${ctx.taskKey}`);
164
+ }
151
165
  try {
152
- await platformAdapter.sendComplete(finalContent, note, thinkingText || undefined);
166
+ await platformAdapter.sendComplete(output, note, thinkingText || undefined);
153
167
  }
154
168
  catch (err) {
155
169
  log.error('Failed to send complete:', err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.3.1-beta.0",
3
+ "version": "1.3.1-beta.1",
4
4
  "description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, Cursor)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,11 +0,0 @@
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
- }
@@ -1,50 +0,0 @@
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,33 +0,0 @@
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;
@@ -1,265 +0,0 @@
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
- }