@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 +0 -3
- package/dist/adapters/claude-sdk-adapter.js +21 -1
- package/dist/adapters/registry.js +0 -5
- package/dist/adapters/tool-adapter.interface.d.ts +2 -0
- package/dist/commands/handler.d.ts +1 -1
- package/dist/commands/handler.js +3 -4
- package/dist/config.d.ts +0 -1
- package/dist/config.js +1 -53
- package/dist/session/session-manager.d.ts +2 -0
- package/dist/session/session-manager.js +10 -0
- package/dist/shared/ai-task.js +16 -2
- package/package.json +1 -1
- package/dist/adapters/cursor-adapter.d.ts +0 -11
- package/dist/adapters/cursor-adapter.js +0 -50
- package/dist/cursor/cli-runner.d.ts +0 -33
- package/dist/cursor/cli-runner.js +0 -265
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:
|
|
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;
|
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.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
|
-
|
|
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(
|
|
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
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. 校验
|
|
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
|
}
|
package/dist/shared/ai-task.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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,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
|
-
}
|