@wu529778790/open-im 1.6.6 → 1.6.7
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/dist/adapters/cursor-adapter.d.ts +1 -1
- package/dist/adapters/cursor-adapter.js +14 -4
- package/dist/config-web-page-i18n.d.ts +4 -0
- package/dist/config-web-page-i18n.js +4 -0
- package/dist/config-web-page-script.js +6 -0
- package/dist/config-web-page-template.js +9 -0
- package/dist/config-web.js +10 -3
- package/dist/config-web.test.js +30 -0
- package/dist/config.d.ts +12 -3
- package/dist/config.js +80 -18
- package/dist/config.test.d.ts +1 -0
- package/dist/config.test.js +70 -0
- package/dist/cursor/cli-runner.d.ts +3 -0
- package/dist/cursor/cli-runner.js +52 -3
- package/dist/cursor/cli-runner.test.d.ts +1 -0
- package/dist/cursor/cli-runner.test.js +94 -0
- package/dist/setup.js +2 -2
- package/dist/shared/ai-task.js +22 -17
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Cursor Adapter - 通过 Cursor
|
|
2
|
+
* Cursor Adapter - 通过 Cursor CLI 执行任务
|
|
3
3
|
* 需要预先安装: curl https://cursor.com/install -fsSL | bash
|
|
4
4
|
*/
|
|
5
5
|
import { runCursor } from '../cursor/cli-runner.js';
|
|
@@ -19,11 +19,13 @@ export class CursorAdapter {
|
|
|
19
19
|
model: options?.model,
|
|
20
20
|
chatId: options?.chatId,
|
|
21
21
|
hookPort: options?.hookPort,
|
|
22
|
+
proxy: options?.proxy,
|
|
22
23
|
};
|
|
23
24
|
return runCursor(this.cliPath, prompt, sessionId, workDir, {
|
|
24
25
|
onText: callbacks.onText,
|
|
25
26
|
onThinking: callbacks.onThinking,
|
|
26
27
|
onToolUse: callbacks.onToolUse,
|
|
28
|
+
onSessionInvalid: callbacks.onSessionInvalid,
|
|
27
29
|
onComplete: (raw) => {
|
|
28
30
|
const result = {
|
|
29
31
|
success: raw.success,
|
|
@@ -39,9 +41,17 @@ export class CursorAdapter {
|
|
|
39
41
|
},
|
|
40
42
|
onError: (err) => {
|
|
41
43
|
const msg = typeof err === 'string' ? err : String(err);
|
|
42
|
-
const friendly = msg.includes('
|
|
43
|
-
? 'Cursor
|
|
44
|
-
: msg
|
|
44
|
+
const friendly = msg.includes('Electron') || msg.includes('Chromium') || msg.includes('not in the list of known options')
|
|
45
|
+
? '当前使用的是 Cursor IDE 的 cursor.cmd,不支持 CLI 模式。请安装独立的 Cursor Agent CLI:在 PowerShell 运行 irm \'https://cursor.com/install?win32=true\' | iex,安装后把 tools.cursor.cliPath 改为 agent。'
|
|
46
|
+
: msg.includes('Authentication required') || msg.includes('agent login') || msg.includes('cursor agent login')
|
|
47
|
+
? 'Cursor 需要先登录。请在终端运行 agent login,或在 ~/.open-im/config.json 的 env 中添加 "CURSOR_API_KEY"。'
|
|
48
|
+
: msg.includes('No session found') || msg.includes('No conversation found') || msg.includes('Unable to find session') || msg.includes('session not found') || msg.includes('invalid session')
|
|
49
|
+
? 'Cursor 会话已失效,旧 session 已清理。请直接重试当前请求。'
|
|
50
|
+
: msg.includes('stream disconnected') || msg.includes('error sending request') || msg.includes('Connection refused') || msg.includes('ENOTFOUND') || msg.includes('ETIMEDOUT')
|
|
51
|
+
? 'Cursor 网络请求失败。如无法访问 Cursor API,可在 tools.cursor.proxy 或 CURSOR_PROXY 中配置代理。'
|
|
52
|
+
: msg.includes('usage limit') || msg.includes('You\'ve hit your usage limit')
|
|
53
|
+
? 'Cursor 模型用量已超限(如 Opus)。请在 config.json 的 tools.cursor.model 中改为 claude-4-sonnet 或其他非 Opus 模型,或运行 agent --list-models 查看可用模型。'
|
|
54
|
+
: msg;
|
|
45
55
|
callbacks.onError(friendly);
|
|
46
56
|
},
|
|
47
57
|
onSessionId: callbacks.onSessionId,
|
|
@@ -96,6 +96,8 @@ export declare const PAGE_TEXTS: {
|
|
|
96
96
|
readonly claudeModel: "ANTHROPIC_MODEL";
|
|
97
97
|
readonly claudeProxy: "Proxy (optional)";
|
|
98
98
|
readonly codexTimeout: "Codex timeout (ms)";
|
|
99
|
+
readonly cursorTimeout: "Cursor timeout (ms)";
|
|
100
|
+
readonly cursorModel: "Model (e.g. auto)";
|
|
99
101
|
readonly codebuddyTimeout: "CodeBuddy timeout (ms)";
|
|
100
102
|
readonly cursorProxy: "Proxy (optional)";
|
|
101
103
|
readonly hookPort: "Hook port";
|
|
@@ -215,6 +217,8 @@ export declare const PAGE_TEXTS: {
|
|
|
215
217
|
readonly claudeModel: "ANTHROPIC_MODEL";
|
|
216
218
|
readonly claudeProxy: "代理(可选)";
|
|
217
219
|
readonly codexTimeout: "Codex 超时(毫秒)";
|
|
220
|
+
readonly cursorTimeout: "Cursor 超时(毫秒)";
|
|
221
|
+
readonly cursorModel: "模型(如 auto)";
|
|
218
222
|
readonly codebuddyTimeout: "CodeBuddy 超时(毫秒)";
|
|
219
223
|
readonly cursorProxy: "代理(可选)";
|
|
220
224
|
readonly hookPort: "Hook 端口";
|
|
@@ -96,6 +96,8 @@ export const PAGE_TEXTS = {
|
|
|
96
96
|
claudeModel: "ANTHROPIC_MODEL",
|
|
97
97
|
claudeProxy: "Proxy (optional)",
|
|
98
98
|
codexTimeout: "Codex timeout (ms)",
|
|
99
|
+
cursorTimeout: "Cursor timeout (ms)",
|
|
100
|
+
cursorModel: "Model (e.g. auto)",
|
|
99
101
|
codebuddyTimeout: "CodeBuddy timeout (ms)",
|
|
100
102
|
cursorProxy: "Proxy (optional)",
|
|
101
103
|
hookPort: "Hook port",
|
|
@@ -215,6 +217,8 @@ export const PAGE_TEXTS = {
|
|
|
215
217
|
claudeModel: "ANTHROPIC_MODEL",
|
|
216
218
|
claudeProxy: "\u4ee3\u7406\uff08\u53ef\u9009\uff09",
|
|
217
219
|
codexTimeout: "Codex \u8d85\u65f6\uff08\u6beb\u79d2\uff09",
|
|
220
|
+
cursorTimeout: "Cursor \u8d85\u65f6\uff08\u6beb\u79d2\uff09",
|
|
221
|
+
cursorModel: "模型(如 auto)",
|
|
218
222
|
codebuddyTimeout: "CodeBuddy \u8d85\u65f6\uff08\u6beb\u79d2\uff09",
|
|
219
223
|
cursorProxy: "\u4ee3\u7406\uff08\u53ef\u9009\uff09",
|
|
220
224
|
hookPort: "Hook \u7aef\u53e3",
|
|
@@ -210,6 +210,8 @@ export const PAGE_SCRIPT = String.raw ` const platformDefinitions = [
|
|
|
210
210
|
{ id: "ai-codexTimeoutMs-label", key: "codexTimeout" },
|
|
211
211
|
{ id: "ai-codexProxy-label", key: "codexProxy" },
|
|
212
212
|
{ id: "ai-cursorCliPath-label", key: "cursorCli" },
|
|
213
|
+
{ id: "ai-cursorModel-label", key: "cursorModel" },
|
|
214
|
+
{ id: "ai-cursorTimeoutMs-label", key: "cursorTimeout" },
|
|
213
215
|
{ id: "ai-cursorProxy-label", key: "cursorProxy" },
|
|
214
216
|
{ id: "ai-codebuddyCliPath-label", key: "codebuddyCli" },
|
|
215
217
|
{ id: "ai-codebuddyTimeoutMs-label", key: "codebuddyTimeout" },
|
|
@@ -423,6 +425,8 @@ export const PAGE_SCRIPT = String.raw ` const platformDefinitions = [
|
|
|
423
425
|
{ id: "ai-codexTimeoutMs", key: "codexTimeoutMs" },
|
|
424
426
|
{ id: "ai-codexProxy", key: "codexProxy" },
|
|
425
427
|
{ id: "ai-cursorCliPath", key: "cursorCliPath" },
|
|
428
|
+
{ id: "ai-cursorModel", key: "cursorModel" },
|
|
429
|
+
{ id: "ai-cursorTimeoutMs", key: "cursorTimeoutMs" },
|
|
426
430
|
{ id: "ai-cursorProxy", key: "cursorProxy" },
|
|
427
431
|
{ id: "ai-codebuddyCliPath", key: "codebuddyCliPath" },
|
|
428
432
|
{ id: "ai-codebuddyTimeoutMs", key: "codebuddyTimeoutMs" },
|
|
@@ -669,6 +673,8 @@ export const PAGE_SCRIPT = String.raw ` const platformDefinitions = [
|
|
|
669
673
|
codexCliPath: getValue("ai-codexCliPath"),
|
|
670
674
|
codexProxy: getValue("ai-codexProxy"),
|
|
671
675
|
cursorCliPath: getValue("ai-cursorCliPath"),
|
|
676
|
+
cursorModel: getValue("ai-cursorModel"),
|
|
677
|
+
cursorTimeoutMs: getNumber("ai-cursorTimeoutMs"),
|
|
672
678
|
cursorProxy: getValue("ai-cursorProxy"),
|
|
673
679
|
codebuddyCliPath: getValue("ai-codebuddyCliPath"),
|
|
674
680
|
hookPort: getNumber("ai-hookPort"),
|
|
@@ -1175,6 +1175,15 @@ export const PAGE_HTML_PREFIX = String.raw `<!doctype html>
|
|
|
1175
1175
|
<label class="form-label" id="ai-cursorCliPath-label">CLI Path</label>
|
|
1176
1176
|
<input id="ai-cursorCliPath" class="form-input mono" type="text" />
|
|
1177
1177
|
</div>
|
|
1178
|
+
<div class="form-group">
|
|
1179
|
+
<label class="form-label" id="ai-cursorModel-label">Model</label>
|
|
1180
|
+
<input id="ai-cursorModel" class="form-input mono" type="text" placeholder="auto" />
|
|
1181
|
+
<div class="form-hint" id="ai-cursorModel-hint">如 auto、Claude 4 Sonnet 等,agent --list-models 查看</div>
|
|
1182
|
+
</div>
|
|
1183
|
+
<div class="form-group">
|
|
1184
|
+
<label class="form-label" id="ai-cursorTimeoutMs-label">Timeout (ms)</label>
|
|
1185
|
+
<input id="ai-cursorTimeoutMs" class="form-input" type="number" min="1" />
|
|
1186
|
+
</div>
|
|
1178
1187
|
<div class="form-group">
|
|
1179
1188
|
<label class="form-label" id="ai-cursorProxy-label">Proxy (optional)</label>
|
|
1180
1189
|
<input id="ai-cursorProxy" class="form-input mono" type="text" />
|
package/dist/config-web.js
CHANGED
|
@@ -143,8 +143,10 @@ function buildInitialPayload(file) {
|
|
|
143
143
|
claudeModel: file.tools?.claude?.model ?? claudeEnv.ANTHROPIC_MODEL ?? "",
|
|
144
144
|
claudeProxy: file.tools?.claude?.proxy ?? "",
|
|
145
145
|
codexTimeoutMs: file.tools?.codex?.timeoutMs ?? 600000,
|
|
146
|
+
cursorTimeoutMs: file.tools?.cursor?.timeoutMs ?? 600000,
|
|
146
147
|
codebuddyTimeoutMs: file.tools?.codebuddy?.timeoutMs ?? 600000,
|
|
147
|
-
cursorCliPath: file.tools?.cursor?.cliPath ?? "
|
|
148
|
+
cursorCliPath: file.tools?.cursor?.cliPath ?? "cursor",
|
|
149
|
+
cursorModel: file.tools?.cursor?.model ?? "auto",
|
|
148
150
|
codexCliPath: file.tools?.codex?.cliPath ?? "codex",
|
|
149
151
|
codebuddyCliPath: file.tools?.codebuddy?.cliPath ?? "codebuddy",
|
|
150
152
|
codexProxy: file.tools?.codex?.proxy ?? "",
|
|
@@ -186,6 +188,8 @@ function validatePayload(payload) {
|
|
|
186
188
|
errors.push("Claude timeout must be positive.");
|
|
187
189
|
if (!Number.isFinite(payload.ai.codexTimeoutMs) || payload.ai.codexTimeoutMs <= 0)
|
|
188
190
|
errors.push("Codex timeout must be positive.");
|
|
191
|
+
if (!Number.isFinite(payload.ai.cursorTimeoutMs) || payload.ai.cursorTimeoutMs <= 0)
|
|
192
|
+
errors.push("Cursor timeout must be positive.");
|
|
189
193
|
if (!Number.isFinite(payload.ai.codebuddyTimeoutMs) || payload.ai.codebuddyTimeoutMs <= 0)
|
|
190
194
|
errors.push("CodeBuddy timeout must be positive.");
|
|
191
195
|
if (!Number.isFinite(payload.ai.hookPort) || payload.ai.hookPort <= 0)
|
|
@@ -267,13 +271,14 @@ function createProbeConfig(values) {
|
|
|
267
271
|
dingtalkAllowedUserIds: [],
|
|
268
272
|
aiCommand: "claude",
|
|
269
273
|
claudeCliPath: "claude",
|
|
270
|
-
cursorCliPath: "
|
|
274
|
+
cursorCliPath: "cursor",
|
|
271
275
|
codexCliPath: "codex",
|
|
272
276
|
claudeWorkDir: process.cwd(),
|
|
273
277
|
claudeSkipPermissions: true,
|
|
274
278
|
defaultPermissionMode: "ask",
|
|
275
279
|
claudeTimeoutMs: 600000,
|
|
276
280
|
codexTimeoutMs: 600000,
|
|
281
|
+
cursorTimeoutMs: 600000,
|
|
277
282
|
codebuddyTimeoutMs: 600000,
|
|
278
283
|
hookPort: 35801,
|
|
279
284
|
logDir: "",
|
|
@@ -419,9 +424,11 @@ function toFileConfig(payload, existing) {
|
|
|
419
424
|
},
|
|
420
425
|
cursor: {
|
|
421
426
|
...existing.tools?.cursor,
|
|
422
|
-
cliPath: clean(payload.ai.cursorCliPath) ?? "
|
|
427
|
+
cliPath: clean(payload.ai.cursorCliPath) ?? "cursor",
|
|
423
428
|
skipPermissions: existing.tools?.cursor?.skipPermissions ?? payload.ai.claudeSkipPermissions,
|
|
424
429
|
proxy: clean(payload.ai.cursorProxy),
|
|
430
|
+
timeoutMs: payload.ai.cursorTimeoutMs,
|
|
431
|
+
model: clean(payload.ai.cursorModel),
|
|
425
432
|
},
|
|
426
433
|
codex: {
|
|
427
434
|
...existing.tools?.codex,
|
package/dist/config-web.test.js
CHANGED
|
@@ -55,3 +55,33 @@ describe("getHealthPlatformSnapshot", () => {
|
|
|
55
55
|
expect(snapshot.qq.message).toContain("configured");
|
|
56
56
|
});
|
|
57
57
|
});
|
|
58
|
+
describe("Cursor web config defaults", () => {
|
|
59
|
+
it("surfaces cursor as the default CLI path in initial config", async () => {
|
|
60
|
+
vi.resetModules();
|
|
61
|
+
const { startWebConfigServer } = await import("./config-web.js");
|
|
62
|
+
const server = await startWebConfigServer({ mode: "init", cwd: process.cwd(), persistent: true });
|
|
63
|
+
if (!server.url) {
|
|
64
|
+
await server.close();
|
|
65
|
+
throw new Error("Web config server failed to bind (url empty)");
|
|
66
|
+
}
|
|
67
|
+
// 使用原生 fetch(可能被 testPlatformConfig 的 vi.fn 覆盖),改用 http.get
|
|
68
|
+
const { get } = await import("node:http");
|
|
69
|
+
const body = await new Promise((resolve, reject) => {
|
|
70
|
+
get(`${server.url}/api/config`, (res) => {
|
|
71
|
+
let data = "";
|
|
72
|
+
res.on("data", (chunk) => { data += chunk; });
|
|
73
|
+
res.on("end", () => {
|
|
74
|
+
try {
|
|
75
|
+
resolve(JSON.parse(data));
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
reject(e);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}).on("error", reject);
|
|
82
|
+
});
|
|
83
|
+
await server.close();
|
|
84
|
+
// 无显式配置时为 cursor;Windows 下可能解析为安装路径
|
|
85
|
+
expect(body?.payload?.ai?.cursorCliPath === "cursor" || body?.payload?.ai?.cursorCliPath?.endsWith("cursor.cmd")).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
});
|
package/dist/config.d.ts
CHANGED
|
@@ -36,13 +36,18 @@ export interface Config {
|
|
|
36
36
|
codebuddyCliPath: string;
|
|
37
37
|
/** Codex 访问 chatgpt.com 的代理(如 http://127.0.0.1:7890) */
|
|
38
38
|
codexProxy?: string;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
defaultPermissionMode: 'ask' | 'accept-edits' | 'plan' | 'yolo';
|
|
39
|
+
/** Cursor 访问 API 的代理(如 http://127.0.0.1:7890,CLI 非官方支持) */
|
|
40
|
+
cursorProxy?: string;
|
|
42
41
|
claudeTimeoutMs: number;
|
|
43
42
|
codexTimeoutMs: number;
|
|
43
|
+
cursorTimeoutMs: number;
|
|
44
44
|
codebuddyTimeoutMs: number;
|
|
45
|
+
claudeWorkDir: string;
|
|
46
|
+
claudeSkipPermissions: boolean;
|
|
47
|
+
defaultPermissionMode: 'ask' | 'accept-edits' | 'plan' | 'yolo';
|
|
45
48
|
claudeModel?: string;
|
|
49
|
+
/** Cursor 专用模型,如 auto(自动选择)、Claude 4 Sonnet 等 */
|
|
50
|
+
cursorModel?: string;
|
|
46
51
|
hookPort: number;
|
|
47
52
|
logDir: string;
|
|
48
53
|
logLevel: LogLevel;
|
|
@@ -151,7 +156,11 @@ export interface FileToolCursor {
|
|
|
151
156
|
cliPath?: string;
|
|
152
157
|
/** 是否跳过权限确认(默认 true,与 tools.claude 共用权限服务器) */
|
|
153
158
|
skipPermissions?: boolean;
|
|
159
|
+
/** HTTP/HTTPS 代理(CLI 非官方支持,部分环境可能生效) */
|
|
154
160
|
proxy?: string;
|
|
161
|
+
timeoutMs?: number;
|
|
162
|
+
/** 模型名,如 auto、Claude 4 Sonnet、gpt-5.2 等,见 agent --list-models */
|
|
163
|
+
model?: string;
|
|
155
164
|
}
|
|
156
165
|
export interface FileToolCodex {
|
|
157
166
|
cliPath?: string;
|
package/dist/config.js
CHANGED
|
@@ -6,7 +6,7 @@ catch {
|
|
|
6
6
|
}
|
|
7
7
|
import { readFileSync, writeFileSync, accessSync, constants, existsSync, mkdirSync } from 'node:fs';
|
|
8
8
|
import { execFileSync } from 'node:child_process';
|
|
9
|
-
import { join, dirname, isAbsolute } from 'node:path';
|
|
9
|
+
import { join, dirname, isAbsolute, basename } from 'node:path';
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
11
|
import { APP_HOME } from './constants.js';
|
|
12
12
|
const AI_COMMANDS = ['claude', 'codex', 'cursor', 'codebuddy'];
|
|
@@ -60,8 +60,11 @@ function migrateToNewConfigFormat(raw) {
|
|
|
60
60
|
},
|
|
61
61
|
cursor: {
|
|
62
62
|
...tcur,
|
|
63
|
-
cliPath: tcur.cliPath ?? raw.cursorCliPath ?? '
|
|
63
|
+
cliPath: tcur.cliPath ?? raw.cursorCliPath ?? 'cursor',
|
|
64
64
|
skipPermissions: tcur.skipPermissions ?? raw.claudeSkipPermissions ?? true,
|
|
65
|
+
proxy: tcur.proxy,
|
|
66
|
+
timeoutMs: tcur.timeoutMs ?? raw.claudeTimeoutMs ?? 600000,
|
|
67
|
+
model: tcur.model ?? raw.cursorModel ?? 'auto',
|
|
65
68
|
},
|
|
66
69
|
codex: {
|
|
67
70
|
...tcod,
|
|
@@ -380,6 +383,7 @@ export function loadConfig() {
|
|
|
380
383
|
const tcb = file.tools?.codebuddy ?? {};
|
|
381
384
|
const claudeCliPath = process.env.CLAUDE_CLI_PATH ?? tc.cliPath ?? 'claude';
|
|
382
385
|
const codexProxy = process.env.CODEX_PROXY ?? tcod.proxy;
|
|
386
|
+
const cursorProxy = process.env.CURSOR_PROXY ?? tcur.proxy;
|
|
383
387
|
let codexCliPath = process.env.CODEX_CLI_PATH ?? tcod.cliPath ?? 'codex';
|
|
384
388
|
if (process.platform === 'win32' && codexCliPath === 'codex') {
|
|
385
389
|
const npmPaths = [
|
|
@@ -397,15 +401,68 @@ export function loadConfig() {
|
|
|
397
401
|
}
|
|
398
402
|
}
|
|
399
403
|
}
|
|
400
|
-
let cursorCliPath = process.env.CURSOR_CLI_PATH ?? tcur.cliPath ?? '
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
404
|
+
let cursorCliPath = process.env.CURSOR_CLI_PATH ?? tcur.cliPath ?? 'cursor';
|
|
405
|
+
const agentPaths = [
|
|
406
|
+
join(process.env.APPDATA || '', 'npm', 'agent.cmd'),
|
|
407
|
+
join(process.env.LOCALAPPDATA || '', 'npm', 'agent.cmd'),
|
|
408
|
+
join(process.env.LOCALAPPDATA || '', 'cursor-agent', 'agent.cmd'),
|
|
409
|
+
join(process.env.LOCALAPPDATA || '', 'Programs', 'cursor-agent', 'agent.cmd'),
|
|
410
|
+
join(process.env.USERPROFILE || '', '.cursor', 'bin', 'agent.cmd'),
|
|
411
|
+
];
|
|
412
|
+
if (process.platform === 'win32') {
|
|
413
|
+
// agent 需解析为完整路径,否则 spawn 报 ENOENT(Node 子进程 PATH 可能不包含 npm)
|
|
414
|
+
if (cursorCliPath === 'agent' || basename(cursorCliPath).toLowerCase() === 'agent') {
|
|
415
|
+
for (const p of agentPaths) {
|
|
416
|
+
try {
|
|
417
|
+
accessSync(p, constants.F_OK);
|
|
418
|
+
cursorCliPath = p;
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
/* 尝试下一个路径 */
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// 若已知路径均未找到,尝试 where agent 解析(用户 PATH 中的 agent)
|
|
426
|
+
if (cursorCliPath === 'agent') {
|
|
427
|
+
try {
|
|
428
|
+
const out = execFileSync('where', ['agent'], { encoding: 'utf-8', windowsHide: true });
|
|
429
|
+
const first = out.split(/\r?\n/)[0]?.trim();
|
|
430
|
+
if (first && existsSync(first))
|
|
431
|
+
cursorCliPath = first;
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
/* where 失败则保持 agent,后续校验会提示安装 */
|
|
435
|
+
}
|
|
436
|
+
}
|
|
406
437
|
}
|
|
407
|
-
|
|
408
|
-
|
|
438
|
+
if (cursorCliPath === 'cursor') {
|
|
439
|
+
for (const p of agentPaths) {
|
|
440
|
+
try {
|
|
441
|
+
accessSync(p, constants.F_OK);
|
|
442
|
+
cursorCliPath = p;
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
/* 尝试下一个路径 */
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (cursorCliPath === 'cursor') {
|
|
451
|
+
const cursorIdePaths = [
|
|
452
|
+
join(process.env.APPDATA || '', 'npm', 'cursor.cmd'),
|
|
453
|
+
join(process.env.LOCALAPPDATA || '', 'npm', 'cursor.cmd'),
|
|
454
|
+
join(process.env.ProgramFiles || 'C:\\Program Files', 'cursor', 'resources', 'app', 'bin', 'cursor.cmd'),
|
|
455
|
+
];
|
|
456
|
+
for (const p of cursorIdePaths) {
|
|
457
|
+
try {
|
|
458
|
+
accessSync(p, constants.F_OK);
|
|
459
|
+
cursorCliPath = p;
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
/* 尝试下一个路径 */
|
|
464
|
+
}
|
|
465
|
+
}
|
|
409
466
|
}
|
|
410
467
|
}
|
|
411
468
|
let codebuddyCliPath = process.env.CODEBUDDY_CLI_PATH ?? tcb.cliPath ?? 'codebuddy';
|
|
@@ -449,6 +506,9 @@ export function loadConfig() {
|
|
|
449
506
|
const codexTimeoutMs = process.env.CODEX_TIMEOUT_MS !== undefined
|
|
450
507
|
? parseInt(process.env.CODEX_TIMEOUT_MS, 10) || 600000
|
|
451
508
|
: tcod.timeoutMs ?? 600000;
|
|
509
|
+
const cursorTimeoutMs = process.env.CURSOR_TIMEOUT_MS !== undefined
|
|
510
|
+
? parseInt(process.env.CURSOR_TIMEOUT_MS, 10) || 600000
|
|
511
|
+
: tcur.timeoutMs ?? 600000;
|
|
452
512
|
const codebuddyTimeoutMs = process.env.CODEBUDDY_TIMEOUT_MS !== undefined
|
|
453
513
|
? parseInt(process.env.CODEBUDDY_TIMEOUT_MS, 10) || 600000
|
|
454
514
|
: tcb.timeoutMs ?? 600000;
|
|
@@ -590,24 +650,23 @@ export function loadConfig() {
|
|
|
590
650
|
catch {
|
|
591
651
|
const installGuide = [
|
|
592
652
|
'',
|
|
593
|
-
'━━━ Cursor CLI 未安装 ━━━',
|
|
653
|
+
'━━━ Cursor Agent CLI 未安装 ━━━',
|
|
594
654
|
'',
|
|
595
|
-
'
|
|
655
|
+
'open-im 需要独立的 Cursor Agent CLI(agent 命令),不是 Cursor IDE 自带的 cursor.cmd。',
|
|
596
656
|
'',
|
|
597
|
-
'
|
|
657
|
+
'安装方法(在 PowerShell 中执行):',
|
|
598
658
|
'',
|
|
599
|
-
'
|
|
600
|
-
' Windows: irm \'https://cursor.com/install?win32=true\' | iex',
|
|
659
|
+
' irm \'https://cursor.com/install?win32=true\' | iex',
|
|
601
660
|
'',
|
|
602
|
-
'安装后运行 agent
|
|
661
|
+
'安装后运行 agent -p "hello" 验证。',
|
|
603
662
|
'',
|
|
604
663
|
].join('\n');
|
|
605
664
|
throw new Error(installGuide);
|
|
606
665
|
}
|
|
607
666
|
}
|
|
608
|
-
// 提示 Cursor 认证:需 agent login 或 CURSOR_API_KEY
|
|
667
|
+
// 提示 Cursor 认证:需 cursor agent login 或 CURSOR_API_KEY
|
|
609
668
|
if (!process.env.CURSOR_API_KEY) {
|
|
610
|
-
console.warn('\n⚠ Cursor 模式:未检测到 CURSOR_API_KEY。首次使用请先运行 agent login,\n' +
|
|
669
|
+
console.warn('\n⚠ Cursor 模式:未检测到 CURSOR_API_KEY。首次使用请先运行 cursor agent login,\n' +
|
|
611
670
|
' 或在 ~/.open-im/config.json 的 env 中添加 "CURSOR_API_KEY": "你的 API Key"。\n');
|
|
612
671
|
}
|
|
613
672
|
}
|
|
@@ -775,13 +834,16 @@ export function loadConfig() {
|
|
|
775
834
|
codexCliPath,
|
|
776
835
|
codebuddyCliPath,
|
|
777
836
|
codexProxy,
|
|
837
|
+
cursorProxy,
|
|
778
838
|
claudeWorkDir,
|
|
779
839
|
claudeSkipPermissions,
|
|
780
840
|
defaultPermissionMode,
|
|
781
841
|
claudeTimeoutMs,
|
|
782
842
|
codexTimeoutMs,
|
|
843
|
+
cursorTimeoutMs,
|
|
783
844
|
codebuddyTimeoutMs,
|
|
784
845
|
claudeModel: process.env.CLAUDE_MODEL ?? tc.model,
|
|
846
|
+
cursorModel: process.env.CURSOR_MODEL ?? tcur.model ?? 'auto',
|
|
785
847
|
hookPort,
|
|
786
848
|
logDir,
|
|
787
849
|
logLevel,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
const { accessSyncMock, existsSyncMock, mkdirSyncMock, readFileSyncMock, writeFileSyncMock, execFileSyncMock, } = vi.hoisted(() => ({
|
|
3
|
+
accessSyncMock: vi.fn(),
|
|
4
|
+
existsSyncMock: vi.fn(),
|
|
5
|
+
mkdirSyncMock: vi.fn(),
|
|
6
|
+
readFileSyncMock: vi.fn(),
|
|
7
|
+
writeFileSyncMock: vi.fn(),
|
|
8
|
+
execFileSyncMock: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
vi.mock('node:fs', async () => {
|
|
11
|
+
const actual = await vi.importActual('node:fs');
|
|
12
|
+
return {
|
|
13
|
+
...actual,
|
|
14
|
+
accessSync: accessSyncMock,
|
|
15
|
+
existsSync: existsSyncMock,
|
|
16
|
+
mkdirSync: mkdirSyncMock,
|
|
17
|
+
readFileSync: readFileSyncMock,
|
|
18
|
+
writeFileSync: writeFileSyncMock,
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
vi.mock('node:child_process', () => ({
|
|
22
|
+
execFileSync: execFileSyncMock,
|
|
23
|
+
}));
|
|
24
|
+
import { loadConfig, loadFileConfig } from './config.js';
|
|
25
|
+
describe('Cursor config defaults', () => {
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
vi.clearAllMocks();
|
|
28
|
+
readFileSyncMock.mockImplementation(() => {
|
|
29
|
+
throw new Error('missing');
|
|
30
|
+
});
|
|
31
|
+
existsSyncMock.mockReturnValue(true);
|
|
32
|
+
accessSyncMock.mockImplementation(() => undefined);
|
|
33
|
+
execFileSyncMock.mockImplementation(() => Buffer.from('cursor'));
|
|
34
|
+
delete process.env.CURSOR_CLI_PATH;
|
|
35
|
+
delete process.env.AI_COMMAND;
|
|
36
|
+
});
|
|
37
|
+
it('defaults Cursor CLI path to cursor for runtime config', () => {
|
|
38
|
+
process.env.AI_COMMAND = 'cursor';
|
|
39
|
+
process.env.TELEGRAM_BOT_TOKEN = 'test-token';
|
|
40
|
+
const config = loadConfig();
|
|
41
|
+
// 无显式配置时使用 cursor;Windows 下可能解析为 npm 或安装路径
|
|
42
|
+
expect(config.cursorCliPath === 'cursor' || config.cursorCliPath.endsWith('cursor.cmd')).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
it('migrates old cursorCliPath-less configs to cursor', () => {
|
|
45
|
+
readFileSyncMock.mockReturnValue(JSON.stringify({
|
|
46
|
+
aiCommand: 'cursor',
|
|
47
|
+
claudeCliPath: 'claude',
|
|
48
|
+
claudeWorkDir: 'D:/coding/open-im',
|
|
49
|
+
claudeSkipPermissions: true,
|
|
50
|
+
}));
|
|
51
|
+
const file = loadFileConfig();
|
|
52
|
+
expect(file.tools?.cursor?.cliPath).toBe('cursor');
|
|
53
|
+
expect(writeFileSyncMock).toHaveBeenCalled();
|
|
54
|
+
});
|
|
55
|
+
it('preserves explicit legacy agent cursor configs', () => {
|
|
56
|
+
readFileSyncMock.mockReturnValue(JSON.stringify({
|
|
57
|
+
aiCommand: 'cursor',
|
|
58
|
+
platforms: { telegram: { enabled: true } },
|
|
59
|
+
telegramBotToken: 'test-token',
|
|
60
|
+
tools: {
|
|
61
|
+
cursor: {
|
|
62
|
+
cliPath: 'agent',
|
|
63
|
+
skipPermissions: true,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
}));
|
|
67
|
+
const config = loadConfig();
|
|
68
|
+
expect(config.cursorCliPath).toBe('agent');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -6,6 +6,7 @@ export interface CursorRunCallbacks {
|
|
|
6
6
|
onText: (accumulated: string) => void;
|
|
7
7
|
onThinking?: (accumulated: string) => void;
|
|
8
8
|
onToolUse?: (toolName: string, toolInput?: Record<string, unknown>) => void;
|
|
9
|
+
onSessionInvalid?: () => void;
|
|
9
10
|
onComplete: (result: {
|
|
10
11
|
success: boolean;
|
|
11
12
|
result: string;
|
|
@@ -26,6 +27,8 @@ export interface CursorRunOptions {
|
|
|
26
27
|
model?: string;
|
|
27
28
|
chatId?: string;
|
|
28
29
|
hookPort?: number;
|
|
30
|
+
/** HTTP/HTTPS 代理(CLI 非官方支持,部分环境可能生效) */
|
|
31
|
+
proxy?: string;
|
|
29
32
|
}
|
|
30
33
|
export interface CursorRunHandle {
|
|
31
34
|
abort: () => void;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* 参考: https://cursor.com/docs/cli/reference/output-format
|
|
4
4
|
*/
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
|
+
import { basename } from 'node:path';
|
|
6
7
|
import { createInterface } from 'node:readline';
|
|
7
8
|
import { createLogger } from '../logger.js';
|
|
8
9
|
const log = createLogger('CursorCli');
|
|
@@ -49,8 +50,16 @@ function extractToolFromCursorEvent(event) {
|
|
|
49
50
|
const args = val.args;
|
|
50
51
|
return { name, input: args };
|
|
51
52
|
}
|
|
53
|
+
function shouldPrependAgentSubcommand(cliPath) {
|
|
54
|
+
const command = basename(cliPath).toLowerCase();
|
|
55
|
+
return command === 'cursor' || command === 'cursor.cmd' || command === 'cursor.exe';
|
|
56
|
+
}
|
|
52
57
|
export function runCursor(cliPath, prompt, sessionId, workDir, callbacks, options) {
|
|
53
|
-
|
|
58
|
+
// 使用 json 格式:输出单行 JSON,stream-json 在 Windows 管道下可能无输出(lines=0)
|
|
59
|
+
const args = [
|
|
60
|
+
...(shouldPrependAgentSubcommand(cliPath) ? ['agent'] : []),
|
|
61
|
+
'-p', '--output-format', 'json',
|
|
62
|
+
'--trust', // headless 模式必须,否则会卡在 "Workspace Trust Required" 提示
|
|
54
63
|
'--sandbox', 'disabled', // 禁用 sandbox,避免 Windows 下 shell 命令极慢或卡死
|
|
55
64
|
];
|
|
56
65
|
// Cursor CLI 运行于 stream-json 非交互模式,stdin 设为 ignore。
|
|
@@ -78,6 +87,14 @@ export function runCursor(cliPath, prompt, sessionId, workDir, callbacks, option
|
|
|
78
87
|
env.CC_IM_CHAT_ID = options.chatId;
|
|
79
88
|
if (options?.hookPort)
|
|
80
89
|
env.CC_IM_HOOK_PORT = String(options.hookPort);
|
|
90
|
+
if (options?.proxy) {
|
|
91
|
+
env.HTTPS_PROXY = options.proxy;
|
|
92
|
+
env.HTTP_PROXY = options.proxy;
|
|
93
|
+
env.https_proxy = options.proxy;
|
|
94
|
+
env.http_proxy = options.proxy;
|
|
95
|
+
env.ALL_PROXY = options.proxy;
|
|
96
|
+
env.all_proxy = options.proxy;
|
|
97
|
+
}
|
|
81
98
|
const argsForLog = args.filter(a => a !== prompt).join(' ');
|
|
82
99
|
log.info(`Spawning Cursor CLI: path=${cliPath}, cwd=${workDir}, session=${sessionId ?? 'new'}, args=${argsForLog}`);
|
|
83
100
|
// Windows: .cmd 需通过 cmd.exe 执行,否则 spawn 报 ENOENT
|
|
@@ -94,6 +111,8 @@ export function runCursor(cliPath, prompt, sessionId, workDir, callbacks, option
|
|
|
94
111
|
let completed = false;
|
|
95
112
|
let model = '';
|
|
96
113
|
const toolStats = {};
|
|
114
|
+
let lineCount = 0;
|
|
115
|
+
let stdoutBytes = 0;
|
|
97
116
|
const MAX_TIMEOUT = 2_147_483_647;
|
|
98
117
|
const timeoutMs = options?.timeoutMs && options.timeoutMs > 0
|
|
99
118
|
? Math.min(options.timeoutMs, MAX_TIMEOUT)
|
|
@@ -133,25 +152,31 @@ export function runCursor(cliPath, prompt, sessionId, workDir, callbacks, option
|
|
|
133
152
|
// 实时打印 stderr,方便诊断 Cursor CLI 问题
|
|
134
153
|
log.debug(`[stderr] ${text.trimEnd()}`);
|
|
135
154
|
});
|
|
155
|
+
child.stdout?.on('data', (chunk) => {
|
|
156
|
+
stdoutBytes += chunk.length;
|
|
157
|
+
});
|
|
136
158
|
const rl = createInterface({ input: child.stdout });
|
|
137
159
|
rl.on('line', (line) => {
|
|
138
160
|
const trimmed = line.trim();
|
|
139
161
|
if (!trimmed)
|
|
140
162
|
return;
|
|
163
|
+
lineCount++;
|
|
141
164
|
let event;
|
|
142
165
|
try {
|
|
143
166
|
event = JSON.parse(trimmed);
|
|
144
167
|
}
|
|
145
168
|
catch {
|
|
169
|
+
log.warn(`[Cursor] Failed to parse line ${lineCount}: ${trimmed.slice(0, 80)}...`);
|
|
146
170
|
return;
|
|
147
171
|
}
|
|
148
|
-
log.debug(`[Cursor event] type=${event.type} subtype=${event.subtype}`);
|
|
149
172
|
const type = event.type;
|
|
173
|
+
log.debug(`[Cursor event] type=${type} subtype=${event.subtype}`);
|
|
150
174
|
if (type === 'system' && event.subtype === 'init') {
|
|
151
175
|
model = event.model ?? '';
|
|
152
176
|
const sid = event.session_id;
|
|
153
177
|
if (sid)
|
|
154
178
|
callbacks.onSessionId?.(sid);
|
|
179
|
+
log.info(`[Cursor] Session init: model=${model}, sessionId=${sid ?? 'none'}`);
|
|
155
180
|
return;
|
|
156
181
|
}
|
|
157
182
|
if (type === 'assistant') {
|
|
@@ -165,6 +190,15 @@ export function runCursor(cliPath, prompt, sessionId, workDir, callbacks, option
|
|
|
165
190
|
}
|
|
166
191
|
}
|
|
167
192
|
}
|
|
193
|
+
// 兼容 content 为字符串或结构不同的情况
|
|
194
|
+
const rawContent = msg?.content;
|
|
195
|
+
if (!Array.isArray(content) && typeof rawContent === 'string') {
|
|
196
|
+
accumulated += rawContent;
|
|
197
|
+
callbacks.onText(accumulated);
|
|
198
|
+
}
|
|
199
|
+
if (accumulated.length > 0) {
|
|
200
|
+
log.info(`[Cursor] Assistant text received: ${accumulated.length} chars total`);
|
|
201
|
+
}
|
|
168
202
|
return;
|
|
169
203
|
}
|
|
170
204
|
if (type === 'tool_call') {
|
|
@@ -190,8 +224,12 @@ export function runCursor(cliPath, prompt, sessionId, workDir, callbacks, option
|
|
|
190
224
|
if (timeoutHandle)
|
|
191
225
|
clearTimeout(timeoutHandle);
|
|
192
226
|
const result = event.result ?? '';
|
|
227
|
+
const sid = event.session_id;
|
|
228
|
+
if (sid)
|
|
229
|
+
callbacks.onSessionId?.(sid);
|
|
193
230
|
if (!accumulated && result)
|
|
194
231
|
accumulated = result;
|
|
232
|
+
log.info(`[Cursor] Result event: resultLen=${result.length}, accumulatedLen=${accumulated.length}, lines=${lineCount}`);
|
|
195
233
|
callbacks.onComplete({
|
|
196
234
|
success: true,
|
|
197
235
|
result,
|
|
@@ -229,9 +267,20 @@ export function runCursor(cliPath, prompt, sessionId, workDir, callbacks, option
|
|
|
229
267
|
stderrTail;
|
|
230
268
|
}
|
|
231
269
|
}
|
|
232
|
-
|
|
270
|
+
const fullErr = errMsg || `Cursor CLI exited with code ${exitCode}`;
|
|
271
|
+
const isSessionErr = /no session found|no conversation found|unable to find session|session not found|invalid session/i.test(fullErr);
|
|
272
|
+
if (isSessionErr)
|
|
273
|
+
callbacks.onSessionInvalid?.();
|
|
274
|
+
callbacks.onError(fullErr);
|
|
233
275
|
}
|
|
234
276
|
else {
|
|
277
|
+
const stderrPreview = stderrTotal > 0 ? stderrHead.slice(0, 500) : '(无)';
|
|
278
|
+
log.warn(`[Cursor] Process exited 0 without result event: accumulated=${accumulated.length} chars, lines=${lineCount}, stdoutBytes=${stdoutBytes}. stderr(${stderrTotal} chars): ${stderrPreview}`);
|
|
279
|
+
// 检测到 Cursor IDE(非 Agent CLI)时给出明确指引
|
|
280
|
+
if (stderrHead.includes('Electron') || stderrHead.includes('Chromium') || stderrHead.includes('not in the list of known options')) {
|
|
281
|
+
callbacks.onError('当前使用的是 Cursor IDE 的 cursor.cmd,不支持 CLI 模式。请安装独立的 Cursor Agent CLI:在 PowerShell 运行 irm \'https://cursor.com/install?win32=true\' | iex,安装后在配置中把 CLI Path 改为 agent。');
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
235
284
|
callbacks.onComplete({
|
|
236
285
|
success: true,
|
|
237
286
|
result: accumulated,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { PassThrough } from 'node:stream';
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
const { spawnMock } = vi.hoisted(() => ({
|
|
5
|
+
spawnMock: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
vi.mock('node:child_process', () => ({
|
|
8
|
+
spawn: spawnMock,
|
|
9
|
+
}));
|
|
10
|
+
import { runCursor } from './cli-runner.js';
|
|
11
|
+
class MockChildProcess extends EventEmitter {
|
|
12
|
+
stdout = new PassThrough();
|
|
13
|
+
stderr = new PassThrough();
|
|
14
|
+
pid = 1234;
|
|
15
|
+
killed = false;
|
|
16
|
+
kill() {
|
|
17
|
+
this.killed = true;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
describe('runCursor', () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vi.clearAllMocks();
|
|
23
|
+
spawnMock.mockImplementation(() => new MockChildProcess());
|
|
24
|
+
});
|
|
25
|
+
it('prepends the agent subcommand for the top-level cursor CLI', () => {
|
|
26
|
+
runCursor('cursor', 'hello', undefined, 'D:\\coding\\open-im', {
|
|
27
|
+
onText: vi.fn(),
|
|
28
|
+
onComplete: vi.fn(),
|
|
29
|
+
onError: vi.fn(),
|
|
30
|
+
});
|
|
31
|
+
expect(spawnMock).toHaveBeenCalledWith('cursor', [
|
|
32
|
+
'agent',
|
|
33
|
+
'-p',
|
|
34
|
+
'--output-format',
|
|
35
|
+
'json',
|
|
36
|
+
'--trust',
|
|
37
|
+
'--sandbox',
|
|
38
|
+
'disabled',
|
|
39
|
+
'--force',
|
|
40
|
+
'--workspace',
|
|
41
|
+
'D:\\coding\\open-im',
|
|
42
|
+
'--',
|
|
43
|
+
'hello',
|
|
44
|
+
], expect.objectContaining({ cwd: 'D:\\coding\\open-im' }));
|
|
45
|
+
});
|
|
46
|
+
it('uses cmd.exe for cursor.cmd and still prepends the agent subcommand', () => {
|
|
47
|
+
vi.stubGlobal('process', {
|
|
48
|
+
...process,
|
|
49
|
+
platform: 'win32',
|
|
50
|
+
env: process.env,
|
|
51
|
+
});
|
|
52
|
+
runCursor('D:\\Program Files\\cursor\\resources\\app\\bin\\cursor.cmd', 'hello', undefined, 'D:\\coding\\open-im', {
|
|
53
|
+
onText: vi.fn(),
|
|
54
|
+
onComplete: vi.fn(),
|
|
55
|
+
onError: vi.fn(),
|
|
56
|
+
});
|
|
57
|
+
expect(spawnMock).toHaveBeenCalledWith('cmd.exe', [
|
|
58
|
+
'/c',
|
|
59
|
+
'D:\\Program Files\\cursor\\resources\\app\\bin\\cursor.cmd',
|
|
60
|
+
'agent',
|
|
61
|
+
'-p',
|
|
62
|
+
'--output-format',
|
|
63
|
+
'json',
|
|
64
|
+
'--trust',
|
|
65
|
+
'--sandbox',
|
|
66
|
+
'disabled',
|
|
67
|
+
'--force',
|
|
68
|
+
'--workspace',
|
|
69
|
+
'D:\\coding\\open-im',
|
|
70
|
+
'--',
|
|
71
|
+
'hello',
|
|
72
|
+
], expect.objectContaining({ cwd: 'D:\\coding\\open-im', windowsHide: true }));
|
|
73
|
+
});
|
|
74
|
+
it('does not duplicate the agent subcommand for legacy agent executables', () => {
|
|
75
|
+
runCursor('agent', 'hello', undefined, 'D:\\coding\\open-im', {
|
|
76
|
+
onText: vi.fn(),
|
|
77
|
+
onComplete: vi.fn(),
|
|
78
|
+
onError: vi.fn(),
|
|
79
|
+
});
|
|
80
|
+
expect(spawnMock).toHaveBeenCalledWith('agent', [
|
|
81
|
+
'-p',
|
|
82
|
+
'--output-format',
|
|
83
|
+
'json',
|
|
84
|
+
'--trust',
|
|
85
|
+
'--sandbox',
|
|
86
|
+
'disabled',
|
|
87
|
+
'--force',
|
|
88
|
+
'--workspace',
|
|
89
|
+
'D:\\coding\\open-im',
|
|
90
|
+
'--',
|
|
91
|
+
'hello',
|
|
92
|
+
], expect.any(Object));
|
|
93
|
+
});
|
|
94
|
+
});
|
package/dist/setup.js
CHANGED
|
@@ -80,7 +80,7 @@ function printManualInstructions(configPath) {
|
|
|
80
80
|
"skipPermissions": true,
|
|
81
81
|
"timeoutMs": 600000
|
|
82
82
|
},
|
|
83
|
-
"cursor": { "cliPath": "
|
|
83
|
+
"cursor": { "cliPath": "cursor", "skipPermissions": true, "model": "auto" },
|
|
84
84
|
"codex": { "cliPath": "codex", "workDir": "${process.cwd().replace(/\\/g, "/")}", "skipPermissions": true, "proxy": "http://127.0.0.1:7890" },
|
|
85
85
|
"codebuddy": { "cliPath": "codebuddy", "skipPermissions": true, "timeoutMs": 600000 }
|
|
86
86
|
},
|
|
@@ -797,7 +797,7 @@ export async function runInteractiveSetup() {
|
|
|
797
797
|
},
|
|
798
798
|
cursor: {
|
|
799
799
|
...baseTools.cursor,
|
|
800
|
-
cliPath: baseTools.cursor?.cliPath ?? "
|
|
800
|
+
cliPath: baseTools.cursor?.cliPath ?? "cursor",
|
|
801
801
|
skipPermissions: baseTools.cursor?.skipPermissions ?? baseTools.claude?.skipPermissions ?? true,
|
|
802
802
|
},
|
|
803
803
|
codex: {
|
package/dist/shared/ai-task.js
CHANGED
|
@@ -100,23 +100,26 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
|
|
|
100
100
|
: undefined)
|
|
101
101
|
: undefined;
|
|
102
102
|
}
|
|
103
|
-
const
|
|
103
|
+
const toolId = toolAdapter.toolId;
|
|
104
|
+
const timeoutMs = toolId === 'codex'
|
|
104
105
|
? config.codexTimeoutMs
|
|
105
|
-
:
|
|
106
|
-
? config.
|
|
107
|
-
:
|
|
106
|
+
: toolId === 'cursor'
|
|
107
|
+
? config.cursorTimeoutMs
|
|
108
|
+
: toolId === 'codebuddy'
|
|
109
|
+
? config.codebuddyTimeoutMs
|
|
110
|
+
: config.claudeTimeoutMs;
|
|
108
111
|
const startRun = () => {
|
|
109
112
|
activeHandle = toolAdapter.run(prompt, currentSessionId, ctx.workDir, {
|
|
110
113
|
onSessionId: (id) => {
|
|
111
114
|
currentSessionId = id;
|
|
112
115
|
if (ctx.threadId)
|
|
113
|
-
sessionManager.setSessionIdForThread(ctx.userId, ctx.threadId,
|
|
116
|
+
sessionManager.setSessionIdForThread(ctx.userId, ctx.threadId, toolId, id);
|
|
114
117
|
else if (ctx.convId)
|
|
115
|
-
sessionManager.setSessionIdForConv(ctx.userId, ctx.convId,
|
|
118
|
+
sessionManager.setSessionIdForConv(ctx.userId, ctx.convId, toolId, id);
|
|
116
119
|
},
|
|
117
120
|
onSessionInvalid: () => {
|
|
118
121
|
if (ctx.convId)
|
|
119
|
-
sessionManager.clearSessionForConv(ctx.userId, ctx.convId,
|
|
122
|
+
sessionManager.clearSessionForConv(ctx.userId, ctx.convId, toolId);
|
|
120
123
|
},
|
|
121
124
|
onThinking: (t) => {
|
|
122
125
|
if (!firstContentLogged) {
|
|
@@ -125,7 +128,7 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
|
|
|
125
128
|
}
|
|
126
129
|
wasThinking = true;
|
|
127
130
|
thinkingText = t;
|
|
128
|
-
throttledUpdate(`💭 **${getAIToolDisplayName(
|
|
131
|
+
throttledUpdate(`💭 **${getAIToolDisplayName(toolId)} 思考中...**\n\n${t}`);
|
|
129
132
|
},
|
|
130
133
|
onText: (accumulated) => {
|
|
131
134
|
if (!firstContentLogged) {
|
|
@@ -190,14 +193,14 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
|
|
|
190
193
|
}
|
|
191
194
|
settled = true;
|
|
192
195
|
log.error(`Task error for user ${ctx.userId}: ${error}`);
|
|
193
|
-
if (
|
|
196
|
+
if (toolId !== 'claude' && !isUsageLimitError(error)) {
|
|
194
197
|
if (ctx.convId)
|
|
195
|
-
sessionManager.clearSessionForConv(ctx.userId, ctx.convId,
|
|
198
|
+
sessionManager.clearSessionForConv(ctx.userId, ctx.convId, toolId);
|
|
196
199
|
else
|
|
197
|
-
sessionManager.clearActiveToolSession(ctx.userId,
|
|
198
|
-
log.info(`Session reset for user ${ctx.userId} due to ${
|
|
200
|
+
sessionManager.clearActiveToolSession(ctx.userId, toolId);
|
|
201
|
+
log.info(`Session reset for user ${ctx.userId} due to ${toolId} task error`);
|
|
199
202
|
}
|
|
200
|
-
else if (
|
|
203
|
+
else if (toolId === 'codex' && isUsageLimitError(error)) {
|
|
201
204
|
log.info(`Keeping codex session for user ${ctx.userId} after usage limit error`);
|
|
202
205
|
}
|
|
203
206
|
try {
|
|
@@ -213,10 +216,12 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
|
|
|
213
216
|
skipPermissions,
|
|
214
217
|
permissionMode,
|
|
215
218
|
timeoutMs,
|
|
216
|
-
model: sessionManager.getModel(ctx.userId, ctx.threadId)
|
|
219
|
+
model: sessionManager.getModel(ctx.userId, ctx.threadId)
|
|
220
|
+
?? (toolId === 'cursor' ? config.cursorModel : config.claudeModel),
|
|
217
221
|
chatId: ctx.chatId,
|
|
218
|
-
...(config.useSdkMode ? {} : { hookPort: config.hookPort }),
|
|
219
|
-
...(
|
|
222
|
+
...(toolId === 'claude' && config.useSdkMode ? {} : { hookPort: config.hookPort }),
|
|
223
|
+
...(toolId === 'codex' && config.codexProxy ? { proxy: config.codexProxy } : {}),
|
|
224
|
+
...(toolId === 'cursor' && config.cursorProxy ? { proxy: config.cursorProxy } : {}),
|
|
220
225
|
});
|
|
221
226
|
return activeHandle;
|
|
222
227
|
};
|
|
@@ -231,7 +236,7 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
|
|
|
231
236
|
latestContent: '',
|
|
232
237
|
settle,
|
|
233
238
|
startedAt: Date.now(),
|
|
234
|
-
toolId
|
|
239
|
+
toolId,
|
|
235
240
|
};
|
|
236
241
|
startRun();
|
|
237
242
|
platformAdapter.onTaskReady(taskState);
|