@wu529778790/open-im 1.3.1 → 1.3.2-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 +16 -6
- package/dist/adapters/codex-adapter.d.ts +10 -0
- package/dist/adapters/codex-adapter.js +59 -0
- package/dist/adapters/cursor-adapter.d.ts +11 -0
- package/dist/adapters/cursor-adapter.js +50 -0
- package/dist/adapters/registry.js +10 -0
- package/dist/adapters/tool-adapter.interface.d.ts +2 -0
- package/dist/codex/cli-runner.d.ts +37 -0
- package/dist/codex/cli-runner.js +394 -0
- package/dist/commands/handler.d.ts +1 -1
- package/dist/commands/handler.js +8 -3
- package/dist/config.d.ts +4 -0
- package/dist/config.js +235 -12
- package/dist/cursor/cli-runner.d.ts +33 -0
- package/dist/cursor/cli-runner.js +290 -0
- package/dist/feishu/card-builder.d.ts +2 -0
- package/dist/feishu/card-builder.js +13 -10
- package/dist/feishu/event-handler.js +3 -3
- package/dist/feishu/message-sender.d.ts +1 -1
- package/dist/feishu/message-sender.js +8 -11
- package/dist/index.js +37 -9
- package/dist/session/session-manager.d.ts +7 -0
- package/dist/session/session-manager.js +30 -1
- package/dist/setup.js +101 -38
- package/dist/shared/ai-task.d.ts +2 -0
- package/dist/shared/ai-task.js +25 -13
- package/dist/shared/utils.d.ts +6 -1
- package/dist/shared/utils.js +24 -12
- package/dist/telegram/message-sender.js +5 -16
- package/dist/wechat/message-sender.js +2 -7
- package/dist/wework/message-sender.js +2 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,6 +55,8 @@ 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
|
+
| `CODEX_PROXY` | Codex 访问 chatgpt.com 的代理(如 http://127.0.0.1:7890) |
|
|
58
60
|
|
|
59
61
|
### Claude API 配置
|
|
60
62
|
|
|
@@ -69,13 +71,17 @@ open-im init
|
|
|
69
71
|
# 方式 2:编辑配置文件
|
|
70
72
|
cat > ~/.open-im/config.json << 'EOF'
|
|
71
73
|
{
|
|
72
|
-
# 工作目录 默认当前目录
|
|
73
|
-
"claudeWorkDir": "YOUR_WORK_DIR",
|
|
74
|
-
# 是否跳过权限确认 默认 true
|
|
75
|
-
"claudeSkipPermissions": true,
|
|
76
|
-
# 使用的 AI 命令 默认 claude
|
|
77
74
|
"aiCommand": "claude",
|
|
78
|
-
|
|
75
|
+
"tools": {
|
|
76
|
+
"claude": {
|
|
77
|
+
"cliPath": "claude",
|
|
78
|
+
"workDir": "YOUR_WORK_DIR",
|
|
79
|
+
"skipPermissions": true,
|
|
80
|
+
"timeoutMs": 600000
|
|
81
|
+
},
|
|
82
|
+
"cursor": { "cliPath": "agent", "skipPermissions": true },
|
|
83
|
+
"codex": { "cliPath": "codex", "workDir": "YOUR_WORK_DIR", "skipPermissions": true, "proxy": "http://127.0.0.1:7890" }
|
|
84
|
+
},
|
|
79
85
|
"platforms": {
|
|
80
86
|
# 企业微信配置
|
|
81
87
|
"wework": {
|
|
@@ -160,3 +166,7 @@ EOF
|
|
|
160
166
|
**飞书卡片报错**:未配置卡片回调,使用命令替代:`/mode ask`、`/mode yolo`
|
|
161
167
|
|
|
162
168
|
**企业微信收不到通知**:需先发一条消息给机器人,才能接收启动通知
|
|
169
|
+
|
|
170
|
+
**Cursor 报 Authentication required**:需先认证。方式 1:在终端运行 `agent login`;方式 2:在 `~/.open-im/config.json` 的 `env` 中添加 `"CURSOR_API_KEY": "你的 API Key"`
|
|
171
|
+
|
|
172
|
+
**Codex 报 stream disconnected / error sending request**:网络无法访问 chatgpt.com,需配置代理。在 `tools.codex` 中添加 `"proxy": "http://127.0.0.1:7890"`,或设置环境变量 `CODEX_PROXY`
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex Adapter - run tasks through OpenAI Codex CLI (`codex exec`)
|
|
3
|
+
*/
|
|
4
|
+
import type { RunCallbacks, RunHandle, RunOptions, ToolAdapter } from "./tool-adapter.interface.js";
|
|
5
|
+
export declare class CodexAdapter implements ToolAdapter {
|
|
6
|
+
private cliPath;
|
|
7
|
+
readonly toolId = "codex";
|
|
8
|
+
constructor(cliPath: string);
|
|
9
|
+
run(prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: RunOptions): RunHandle;
|
|
10
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex Adapter - run tasks through OpenAI Codex CLI (`codex exec`)
|
|
3
|
+
*/
|
|
4
|
+
import { runCodex } from "../codex/cli-runner.js";
|
|
5
|
+
export class CodexAdapter {
|
|
6
|
+
cliPath;
|
|
7
|
+
toolId = "codex";
|
|
8
|
+
constructor(cliPath) {
|
|
9
|
+
this.cliPath = cliPath;
|
|
10
|
+
}
|
|
11
|
+
run(prompt, sessionId, workDir, callbacks, options) {
|
|
12
|
+
const opts = {
|
|
13
|
+
skipPermissions: options?.skipPermissions,
|
|
14
|
+
permissionMode: options?.permissionMode,
|
|
15
|
+
timeoutMs: options?.timeoutMs,
|
|
16
|
+
model: options?.model,
|
|
17
|
+
chatId: options?.chatId,
|
|
18
|
+
hookPort: options?.hookPort,
|
|
19
|
+
proxy: options?.proxy,
|
|
20
|
+
};
|
|
21
|
+
return runCodex(this.cliPath, prompt, sessionId, workDir, {
|
|
22
|
+
onText: callbacks.onText,
|
|
23
|
+
onThinking: callbacks.onThinking,
|
|
24
|
+
onToolUse: callbacks.onToolUse,
|
|
25
|
+
onComplete: (raw) => {
|
|
26
|
+
const result = {
|
|
27
|
+
success: raw.success,
|
|
28
|
+
result: raw.result,
|
|
29
|
+
accumulated: raw.accumulated,
|
|
30
|
+
cost: raw.cost,
|
|
31
|
+
durationMs: raw.durationMs,
|
|
32
|
+
model: raw.model,
|
|
33
|
+
numTurns: raw.numTurns,
|
|
34
|
+
toolStats: raw.toolStats,
|
|
35
|
+
};
|
|
36
|
+
callbacks.onComplete(result);
|
|
37
|
+
},
|
|
38
|
+
onError: (err) => {
|
|
39
|
+
const msg = typeof err === "string" ? err : String(err);
|
|
40
|
+
const friendly = msg.includes("Authentication") || msg.includes("login")
|
|
41
|
+
? "Codex 需要先登录。请在终端运行 codex login,或在 ~/.open-im/config.json 的 env 中添加 OPENAI_API_KEY。"
|
|
42
|
+
: msg.includes("stream disconnected") ||
|
|
43
|
+
msg.includes("error sending request") ||
|
|
44
|
+
msg.includes("Connection refused") ||
|
|
45
|
+
msg.includes("ENOTFOUND") ||
|
|
46
|
+
msg.includes("ETIMEDOUT")
|
|
47
|
+
? "Codex 网络请求失败。如无法访问 chatgpt.com,请在 tools.codex.proxy 或 CODEX_PROXY 中配置代理。"
|
|
48
|
+
: msg.includes("No session found") ||
|
|
49
|
+
msg.includes("No conversation found") ||
|
|
50
|
+
msg.includes("Unable to find session")
|
|
51
|
+
? "Codex 会话已失效,旧 session 已清理。请直接重试当前请求。"
|
|
52
|
+
: msg;
|
|
53
|
+
callbacks.onError(friendly);
|
|
54
|
+
},
|
|
55
|
+
onSessionId: callbacks.onSessionId,
|
|
56
|
+
onSessionInvalid: callbacks.onSessionInvalid,
|
|
57
|
+
}, opts);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -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,7 @@
|
|
|
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
|
+
import { CodexAdapter } from './codex-adapter.js';
|
|
3
5
|
const adapters = new Map();
|
|
4
6
|
export function initAdapters(config) {
|
|
5
7
|
adapters.clear();
|
|
@@ -16,6 +18,14 @@ export function initAdapters(config) {
|
|
|
16
18
|
}));
|
|
17
19
|
}
|
|
18
20
|
}
|
|
21
|
+
else if (config.aiCommand === 'cursor') {
|
|
22
|
+
console.log('🖱️ 使用 Cursor Agent CLI 适配器');
|
|
23
|
+
adapters.set('cursor', new CursorAdapter(config.cursorCliPath));
|
|
24
|
+
}
|
|
25
|
+
else if (config.aiCommand === 'codex') {
|
|
26
|
+
console.log('📦 使用 Codex CLI 适配器');
|
|
27
|
+
adapters.set('codex', new CodexAdapter(config.codexCliPath));
|
|
28
|
+
}
|
|
19
29
|
}
|
|
20
30
|
export function getAdapter(aiCommand) {
|
|
21
31
|
return adapters.get(aiCommand);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex CLI Runner - 解析 codex exec --json 的 JSONL 输出
|
|
3
|
+
* 参考: https://developers.openai.com/codex/cli/reference/
|
|
4
|
+
* https://takopi.dev/reference/runners/codex/exec-json-cheatsheet/
|
|
5
|
+
*/
|
|
6
|
+
export interface CodexRunCallbacks {
|
|
7
|
+
onText: (accumulated: string) => void;
|
|
8
|
+
onThinking?: (accumulated: string) => void;
|
|
9
|
+
onToolUse?: (toolName: string, toolInput?: Record<string, unknown>) => void;
|
|
10
|
+
onComplete: (result: {
|
|
11
|
+
success: boolean;
|
|
12
|
+
result: string;
|
|
13
|
+
accumulated: string;
|
|
14
|
+
cost: number;
|
|
15
|
+
durationMs: number;
|
|
16
|
+
model?: string;
|
|
17
|
+
numTurns: number;
|
|
18
|
+
toolStats: Record<string, number>;
|
|
19
|
+
}) => void;
|
|
20
|
+
onError: (error: string) => void;
|
|
21
|
+
onSessionId?: (sessionId: string) => void;
|
|
22
|
+
onSessionInvalid?: () => void;
|
|
23
|
+
}
|
|
24
|
+
export interface CodexRunOptions {
|
|
25
|
+
skipPermissions?: boolean;
|
|
26
|
+
permissionMode?: 'default' | 'acceptEdits' | 'plan';
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
model?: string;
|
|
29
|
+
chatId?: string;
|
|
30
|
+
hookPort?: number;
|
|
31
|
+
/** HTTP/HTTPS 代理,用于访问 chatgpt.com */
|
|
32
|
+
proxy?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface CodexRunHandle {
|
|
35
|
+
abort: () => void;
|
|
36
|
+
}
|
|
37
|
+
export declare function runCodex(cliPath: string, prompt: string, sessionId: string | undefined, workDir: string, callbacks: CodexRunCallbacks, options?: CodexRunOptions): CodexRunHandle;
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex CLI Runner - 解析 codex exec --json 的 JSONL 输出
|
|
3
|
+
* 参考: https://developers.openai.com/codex/cli/reference/
|
|
4
|
+
* https://takopi.dev/reference/runners/codex/exec-json-cheatsheet/
|
|
5
|
+
*/
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
import { execFileSync } from 'node:child_process';
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
9
|
+
import { dirname, join } from 'node:path';
|
|
10
|
+
import { createInterface } from 'node:readline';
|
|
11
|
+
import { createLogger } from '../logger.js';
|
|
12
|
+
const log = createLogger('CodexCli');
|
|
13
|
+
function parseCodexEvent(line) {
|
|
14
|
+
const trimmed = line.trim();
|
|
15
|
+
if (!trimmed)
|
|
16
|
+
return null;
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(trimmed);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function buildCodexArgs(_prompt, sessionId, workDir, options) {
|
|
25
|
+
const commonOptions = ["--json", "--skip-git-repo-check"];
|
|
26
|
+
const newSessionOptions = [...commonOptions, "--cd", workDir];
|
|
27
|
+
const resumeOptions = [...commonOptions];
|
|
28
|
+
const canResume = Boolean(sessionId) && options?.permissionMode !== "plan";
|
|
29
|
+
if (options?.skipPermissions) {
|
|
30
|
+
newSessionOptions.push("--dangerously-bypass-approvals-and-sandbox");
|
|
31
|
+
resumeOptions.push("--dangerously-bypass-approvals-and-sandbox");
|
|
32
|
+
}
|
|
33
|
+
else if (options?.permissionMode === "plan") {
|
|
34
|
+
// `codex exec resume` 当前不支持 `--sandbox` / `--cd`,plan 模式统一新开只读会话。
|
|
35
|
+
newSessionOptions.push("--sandbox", "read-only");
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
newSessionOptions.push("--full-auto");
|
|
39
|
+
resumeOptions.push("--full-auto");
|
|
40
|
+
}
|
|
41
|
+
if (options?.model) {
|
|
42
|
+
newSessionOptions.push("--model", options.model);
|
|
43
|
+
resumeOptions.push("--model", options.model);
|
|
44
|
+
}
|
|
45
|
+
if (sessionId && !canResume) {
|
|
46
|
+
log.warn("Codex plan mode does not support resume; starting a new read-only session");
|
|
47
|
+
}
|
|
48
|
+
return canResume
|
|
49
|
+
? ["exec", "resume", ...resumeOptions, sessionId, "-"]
|
|
50
|
+
: ["exec", ...newSessionOptions, "-"];
|
|
51
|
+
}
|
|
52
|
+
function quoteForWindowsCmd(arg) {
|
|
53
|
+
// 普通 flag / sessionId / 无空格路径不需要加引号,否则引号可能被原样传给子进程。
|
|
54
|
+
if (/^[A-Za-z0-9_./:=+\\-]+$/.test(arg)) {
|
|
55
|
+
return arg;
|
|
56
|
+
}
|
|
57
|
+
const escaped = arg
|
|
58
|
+
.replace(/(\\*)"/g, '$1$1\\"')
|
|
59
|
+
.replace(/(\\+)$/g, '$1$1')
|
|
60
|
+
.replace(/%/g, '%%');
|
|
61
|
+
return `"${escaped}"`;
|
|
62
|
+
}
|
|
63
|
+
function formatWindowsCommandName(command) {
|
|
64
|
+
// 裸命令名(如 codex)依赖 PATH 查找,不能再包双引号,否则 cmd 会按字面量查找。
|
|
65
|
+
if (/^[A-Za-z0-9_.-]+$/.test(command)) {
|
|
66
|
+
return command;
|
|
67
|
+
}
|
|
68
|
+
return quoteForWindowsCmd(command);
|
|
69
|
+
}
|
|
70
|
+
function extractCodexJsFromCmdShim(cmdPath) {
|
|
71
|
+
try {
|
|
72
|
+
const content = readFileSync(cmdPath, 'utf-8');
|
|
73
|
+
const match = content.match(/"%~dp0\\([^"\r\n]*codex\\bin\\codex\.js)"/i);
|
|
74
|
+
if (!match)
|
|
75
|
+
return null;
|
|
76
|
+
const relativeJsPath = match[1].replace(/\\/g, '/');
|
|
77
|
+
return join(dirname(cmdPath), relativeJsPath);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function resolveWindowsCodexLaunch(cliPath, args) {
|
|
84
|
+
try {
|
|
85
|
+
const whereOutput = execFileSync('where', [cliPath], { stdio: 'pipe' })
|
|
86
|
+
.toString()
|
|
87
|
+
.split(/\r?\n/)
|
|
88
|
+
.map((line) => line.trim())
|
|
89
|
+
.filter(Boolean);
|
|
90
|
+
const cmdShimPath = whereOutput.find((line) => /\.cmd$/i.test(line)) ?? null;
|
|
91
|
+
if (!cmdShimPath)
|
|
92
|
+
return null;
|
|
93
|
+
const codexJsPath = extractCodexJsFromCmdShim(cmdShimPath);
|
|
94
|
+
if (!codexJsPath)
|
|
95
|
+
return null;
|
|
96
|
+
return {
|
|
97
|
+
command: process.execPath,
|
|
98
|
+
args: [codexJsPath, ...args],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options) {
|
|
106
|
+
// codex exec --json 非交互模式
|
|
107
|
+
const args = buildCodexArgs(prompt, sessionId, workDir, options);
|
|
108
|
+
const env = {};
|
|
109
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
110
|
+
if (v !== undefined)
|
|
111
|
+
env[k] = v;
|
|
112
|
+
}
|
|
113
|
+
if (options?.chatId)
|
|
114
|
+
env.CC_IM_CHAT_ID = options.chatId;
|
|
115
|
+
if (options?.hookPort)
|
|
116
|
+
env.CC_IM_HOOK_PORT = String(options.hookPort);
|
|
117
|
+
if (options?.proxy) {
|
|
118
|
+
env.HTTPS_PROXY = options.proxy;
|
|
119
|
+
env.HTTP_PROXY = options.proxy;
|
|
120
|
+
env.https_proxy = options.proxy;
|
|
121
|
+
env.http_proxy = options.proxy;
|
|
122
|
+
env.ALL_PROXY = options.proxy;
|
|
123
|
+
env.all_proxy = options.proxy;
|
|
124
|
+
}
|
|
125
|
+
if (process.platform === 'win32') {
|
|
126
|
+
// 强制子进程在 Windows 下使用 UTF-8,避免中文源码/命令输出乱码。
|
|
127
|
+
env.LANG = env.LANG || 'C.UTF-8';
|
|
128
|
+
env.LC_ALL = env.LC_ALL || 'C.UTF-8';
|
|
129
|
+
}
|
|
130
|
+
const argsForLog = args.join(' ');
|
|
131
|
+
log.info(`Spawning Codex CLI: path=${cliPath}, cwd=${workDir}, session=${sessionId ?? 'new'}, args=${argsForLog}`);
|
|
132
|
+
// Windows: .cmd/.bat 或简单命令名(如 codex)需通过 cmd.exe 执行,否则 spawn 报 ENOENT
|
|
133
|
+
const isWinCmd = process.platform === 'win32' &&
|
|
134
|
+
(/\.(cmd|bat)$/i.test(cliPath) || cliPath === 'codex');
|
|
135
|
+
const directWindowsLaunch = isWinCmd
|
|
136
|
+
? resolveWindowsCodexLaunch(cliPath, args)
|
|
137
|
+
: null;
|
|
138
|
+
const spawnCmd = directWindowsLaunch
|
|
139
|
+
? directWindowsLaunch.command
|
|
140
|
+
: isWinCmd
|
|
141
|
+
? 'cmd.exe'
|
|
142
|
+
: cliPath;
|
|
143
|
+
const spawnArgs = directWindowsLaunch
|
|
144
|
+
? directWindowsLaunch.args
|
|
145
|
+
: isWinCmd
|
|
146
|
+
? [
|
|
147
|
+
'/d',
|
|
148
|
+
'/s',
|
|
149
|
+
'/c',
|
|
150
|
+
`chcp 65001>nul && ${formatWindowsCommandName(cliPath)} ${args.map(quoteForWindowsCmd).join(' ')}`,
|
|
151
|
+
]
|
|
152
|
+
: args;
|
|
153
|
+
const child = spawn(spawnCmd, spawnArgs, {
|
|
154
|
+
cwd: workDir,
|
|
155
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
156
|
+
env,
|
|
157
|
+
windowsHide: process.platform === 'win32',
|
|
158
|
+
});
|
|
159
|
+
// 通过 stdin 传 prompt,避免 Windows 下命令行参数引用导致中文/路径/空格被拆分。
|
|
160
|
+
child.stdin?.write(prompt);
|
|
161
|
+
child.stdin?.end();
|
|
162
|
+
let accumulated = '';
|
|
163
|
+
let accumulatedThinking = '';
|
|
164
|
+
let completed = false;
|
|
165
|
+
let threadId = '';
|
|
166
|
+
const toolStats = {};
|
|
167
|
+
const startTime = Date.now();
|
|
168
|
+
const MAX_TIMEOUT = 2_147_483_647;
|
|
169
|
+
const timeoutMs = options?.timeoutMs && options.timeoutMs > 0
|
|
170
|
+
? Math.min(options.timeoutMs, MAX_TIMEOUT)
|
|
171
|
+
: 0;
|
|
172
|
+
let timeoutHandle = null;
|
|
173
|
+
if (timeoutMs > 0) {
|
|
174
|
+
timeoutHandle = setTimeout(() => {
|
|
175
|
+
if (!completed && !child.killed) {
|
|
176
|
+
completed = true;
|
|
177
|
+
log.warn(`Codex CLI timeout after ${timeoutMs}ms, killing pid=${child.pid}`);
|
|
178
|
+
child.kill('SIGTERM');
|
|
179
|
+
callbacks.onError(`执行超时(${timeoutMs}ms),已终止进程`);
|
|
180
|
+
}
|
|
181
|
+
}, timeoutMs);
|
|
182
|
+
}
|
|
183
|
+
const MAX_STDERR_HEAD = 4 * 1024;
|
|
184
|
+
const MAX_STDERR_TAIL = 6 * 1024;
|
|
185
|
+
let stderrHead = '';
|
|
186
|
+
let stderrTail = '';
|
|
187
|
+
let stderrTotal = 0;
|
|
188
|
+
let stderrHeadFull = false;
|
|
189
|
+
child.stderr?.on('data', (chunk) => {
|
|
190
|
+
const text = chunk.toString();
|
|
191
|
+
stderrTotal += text.length;
|
|
192
|
+
if (!stderrHeadFull) {
|
|
193
|
+
const room = MAX_STDERR_HEAD - stderrHead.length;
|
|
194
|
+
if (room > 0) {
|
|
195
|
+
stderrHead += text.slice(0, room);
|
|
196
|
+
if (stderrHead.length >= MAX_STDERR_HEAD)
|
|
197
|
+
stderrHeadFull = true;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
stderrTail += text;
|
|
201
|
+
if (stderrTail.length > MAX_STDERR_TAIL) {
|
|
202
|
+
stderrTail = stderrTail.slice(-MAX_STDERR_TAIL);
|
|
203
|
+
}
|
|
204
|
+
log.debug(`[stderr] ${text.trimEnd()}`);
|
|
205
|
+
});
|
|
206
|
+
const rl = createInterface({ input: child.stdout });
|
|
207
|
+
rl.on('line', (line) => {
|
|
208
|
+
const event = parseCodexEvent(line);
|
|
209
|
+
if (!event)
|
|
210
|
+
return;
|
|
211
|
+
const type = event.type;
|
|
212
|
+
log.debug(`[Codex event] type=${type}`);
|
|
213
|
+
if (type === 'thread.started') {
|
|
214
|
+
threadId = event.thread_id ?? '';
|
|
215
|
+
if (threadId)
|
|
216
|
+
callbacks.onSessionId?.(threadId);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (type === 'turn.failed') {
|
|
220
|
+
completed = true;
|
|
221
|
+
if (timeoutHandle)
|
|
222
|
+
clearTimeout(timeoutHandle);
|
|
223
|
+
const err = event.error;
|
|
224
|
+
callbacks.onError(err?.message ?? 'Codex turn failed');
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (type === 'error') {
|
|
228
|
+
const msg = event.message;
|
|
229
|
+
if (msg?.includes('Reconnecting')) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
completed = true;
|
|
233
|
+
if (timeoutHandle)
|
|
234
|
+
clearTimeout(timeoutHandle);
|
|
235
|
+
callbacks.onError(msg ?? 'Codex stream error');
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (type === 'item.started' || type === 'item.updated' || type === 'item.completed') {
|
|
239
|
+
const item = event.item;
|
|
240
|
+
if (!item)
|
|
241
|
+
return;
|
|
242
|
+
const itemType = item.type;
|
|
243
|
+
if (itemType === 'reasoning' && type === 'item.completed') {
|
|
244
|
+
const text = item.text;
|
|
245
|
+
if (text) {
|
|
246
|
+
accumulatedThinking += (accumulatedThinking ? '\n\n' : '') + text;
|
|
247
|
+
callbacks.onThinking?.(accumulatedThinking);
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (itemType === 'command_execution') {
|
|
252
|
+
const cmd = item.command;
|
|
253
|
+
const status = item.status;
|
|
254
|
+
if (cmd) {
|
|
255
|
+
if (type === 'item.started') {
|
|
256
|
+
const toolName = 'Bash';
|
|
257
|
+
toolStats[toolName] = (toolStats[toolName] || 0) + 1;
|
|
258
|
+
callbacks.onToolUse?.(toolName, { command: cmd });
|
|
259
|
+
}
|
|
260
|
+
if (type === 'item.completed' && status === 'completed') {
|
|
261
|
+
const out = item.aggregated_output;
|
|
262
|
+
const exitCode = item.exit_code;
|
|
263
|
+
if (out) {
|
|
264
|
+
accumulated += (accumulated ? '\n\n' : '') + '```\n' + out + '\n```';
|
|
265
|
+
callbacks.onText(accumulated);
|
|
266
|
+
}
|
|
267
|
+
const exitMsg = `\n\n✓ 命令执行完成 (exit ${exitCode ?? 0})`;
|
|
268
|
+
accumulated += exitMsg;
|
|
269
|
+
callbacks.onText(accumulated);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (itemType === 'file_change' && type === 'item.completed') {
|
|
275
|
+
const changes = item.changes;
|
|
276
|
+
const toolName = 'Edit';
|
|
277
|
+
toolStats[toolName] = (toolStats[toolName] || 0) + 1;
|
|
278
|
+
callbacks.onToolUse?.(toolName, { changes });
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (itemType === 'mcp_tool_call' && type === 'item.started') {
|
|
282
|
+
const tool = item.tool;
|
|
283
|
+
const server = item.server;
|
|
284
|
+
if (tool) {
|
|
285
|
+
const displayName = server ? `${server}/${tool}` : tool;
|
|
286
|
+
toolStats[displayName] = (toolStats[displayName] || 0) + 1;
|
|
287
|
+
callbacks.onToolUse?.(displayName, item.arguments);
|
|
288
|
+
}
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (itemType === 'agent_message' && type === 'item.completed') {
|
|
292
|
+
const text = item.text;
|
|
293
|
+
if (text) {
|
|
294
|
+
accumulated += (accumulated ? '\n\n' : '') + text;
|
|
295
|
+
callbacks.onText(accumulated);
|
|
296
|
+
}
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (type === 'turn.completed') {
|
|
301
|
+
completed = true;
|
|
302
|
+
if (timeoutHandle)
|
|
303
|
+
clearTimeout(timeoutHandle);
|
|
304
|
+
const usage = event.usage;
|
|
305
|
+
const durationMs = Date.now() - startTime;
|
|
306
|
+
callbacks.onComplete({
|
|
307
|
+
success: true,
|
|
308
|
+
result: accumulated,
|
|
309
|
+
accumulated,
|
|
310
|
+
cost: 0,
|
|
311
|
+
durationMs,
|
|
312
|
+
numTurns: 1,
|
|
313
|
+
toolStats,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
let exitCode = null;
|
|
318
|
+
let rlClosed = false;
|
|
319
|
+
let childClosed = false;
|
|
320
|
+
const finalize = () => {
|
|
321
|
+
if (!rlClosed || !childClosed)
|
|
322
|
+
return;
|
|
323
|
+
if (timeoutHandle)
|
|
324
|
+
clearTimeout(timeoutHandle);
|
|
325
|
+
if (!completed) {
|
|
326
|
+
if (exitCode !== null && exitCode !== 0) {
|
|
327
|
+
let errMsg = '';
|
|
328
|
+
if (stderrTotal > 0) {
|
|
329
|
+
if (!stderrHeadFull) {
|
|
330
|
+
errMsg = stderrHead;
|
|
331
|
+
}
|
|
332
|
+
else if (stderrTotal <= MAX_STDERR_HEAD + MAX_STDERR_TAIL) {
|
|
333
|
+
errMsg = stderrHead + stderrTail.slice(stderrTail.length - (stderrTotal - MAX_STDERR_HEAD));
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
errMsg =
|
|
337
|
+
stderrHead +
|
|
338
|
+
`\n\n... (省略 ${stderrTotal - MAX_STDERR_HEAD - MAX_STDERR_TAIL} 字节) ...\n\n` +
|
|
339
|
+
stderrTail;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (sessionId &&
|
|
343
|
+
(errMsg.includes("No session found") ||
|
|
344
|
+
errMsg.includes("No conversation found") ||
|
|
345
|
+
errMsg.includes("Unable to find session"))) {
|
|
346
|
+
callbacks.onSessionInvalid?.();
|
|
347
|
+
}
|
|
348
|
+
callbacks.onError(errMsg || `Codex CLI exited with code ${exitCode}`);
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
callbacks.onComplete({
|
|
352
|
+
success: true,
|
|
353
|
+
result: accumulated,
|
|
354
|
+
accumulated,
|
|
355
|
+
cost: 0,
|
|
356
|
+
durationMs: Date.now() - startTime,
|
|
357
|
+
numTurns: 0,
|
|
358
|
+
toolStats,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
child.on('close', (code) => {
|
|
364
|
+
log.info(`Codex CLI closed: exitCode=${code}, pid=${child.pid}`);
|
|
365
|
+
exitCode = code;
|
|
366
|
+
childClosed = true;
|
|
367
|
+
finalize();
|
|
368
|
+
});
|
|
369
|
+
rl.on('close', () => {
|
|
370
|
+
rlClosed = true;
|
|
371
|
+
finalize();
|
|
372
|
+
});
|
|
373
|
+
child.on('error', (err) => {
|
|
374
|
+
const errorCode = err.code;
|
|
375
|
+
log.error(`Codex CLI spawn error: ${err.message}, code=${errorCode}, path=${cliPath}`);
|
|
376
|
+
if (timeoutHandle)
|
|
377
|
+
clearTimeout(timeoutHandle);
|
|
378
|
+
if (!completed) {
|
|
379
|
+
completed = true;
|
|
380
|
+
callbacks.onError(`Failed to start Codex CLI: ${err.message}`);
|
|
381
|
+
}
|
|
382
|
+
childClosed = true;
|
|
383
|
+
finalize();
|
|
384
|
+
});
|
|
385
|
+
return {
|
|
386
|
+
abort: () => {
|
|
387
|
+
if (timeoutHandle)
|
|
388
|
+
clearTimeout(timeoutHandle);
|
|
389
|
+
rl.close();
|
|
390
|
+
if (!child.killed)
|
|
391
|
+
child.kill('SIGTERM');
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
}
|
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,14 @@ export class CommandHandler {
|
|
|
166
166
|
}
|
|
167
167
|
return true;
|
|
168
168
|
}
|
|
169
|
-
|
|
169
|
+
getAiVersion() {
|
|
170
|
+
const cmd = this.deps.config.aiCommand === 'cursor'
|
|
171
|
+
? this.deps.config.cursorCliPath
|
|
172
|
+
: this.deps.config.aiCommand === 'codex'
|
|
173
|
+
? this.deps.config.codexCliPath
|
|
174
|
+
: this.deps.config.claudeCliPath;
|
|
170
175
|
return new Promise((resolve) => {
|
|
171
|
-
execFile(
|
|
176
|
+
execFile(cmd, ['--version'], { timeout: 5000 }, (err, stdout) => {
|
|
172
177
|
resolve(err ? '未知' : (stdout?.toString().trim() || '未知'));
|
|
173
178
|
});
|
|
174
179
|
});
|