@wu529778790/open-im 1.6.1-beta.0 → 1.6.1-beta.2
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/channels/capabilities.d.ts +11 -0
- package/dist/channels/capabilities.js +55 -0
- package/dist/channels/capabilities.test.d.ts +1 -0
- package/dist/channels/capabilities.test.js +25 -0
- package/dist/cli.js +25 -0
- package/dist/codex/cli-runner.d.ts +1 -2
- package/dist/codex/cli-runner.js +96 -72
- package/dist/config-web-page-i18n.d.ts +193 -0
- package/dist/config-web-page-i18n.js +193 -0
- package/dist/config-web-page-script.d.ts +1 -0
- package/dist/config-web-page-script.js +370 -0
- package/dist/config-web-page-template.d.ts +2 -0
- package/dist/config-web-page-template.js +241 -0
- package/dist/config-web-page.d.ts +1 -0
- package/dist/config-web-page.js +5 -0
- package/dist/config-web-page.test.d.ts +1 -0
- package/dist/config-web-page.test.js +33 -0
- package/dist/config-web.d.ts +1 -0
- package/dist/config-web.js +149 -756
- package/dist/config-web.test.d.ts +1 -0
- package/dist/config-web.test.js +49 -0
- package/dist/dingtalk/event-handler.js +148 -34
- package/dist/dingtalk/message-sender.d.ts +1 -0
- package/dist/dingtalk/message-sender.js +4 -0
- package/dist/dingtalk/message-sender.test.js +5 -0
- package/dist/feishu/event-handler.js +87 -47
- package/dist/qq/client.js +21 -7
- package/dist/qq/event-handler.js +108 -12
- package/dist/qq/message-sender.d.ts +1 -0
- package/dist/qq/message-sender.js +4 -0
- package/dist/qq/message-sender.test.d.ts +1 -0
- package/dist/qq/message-sender.test.js +26 -0
- package/dist/qq/types.d.ts +18 -10
- package/dist/shared/media-analysis-prompt.d.ts +18 -0
- package/dist/shared/media-analysis-prompt.js +37 -0
- package/dist/shared/media-analysis-prompt.test.d.ts +1 -0
- package/dist/shared/media-analysis-prompt.test.js +40 -0
- package/dist/shared/media-context.d.ts +1 -0
- package/dist/shared/media-context.js +12 -0
- package/dist/shared/media-context.test.d.ts +1 -0
- package/dist/shared/media-context.test.js +13 -0
- package/dist/shared/media-prompt.d.ts +8 -0
- package/dist/shared/media-prompt.js +21 -0
- package/dist/shared/media-prompt.test.d.ts +1 -0
- package/dist/shared/media-prompt.test.js +26 -0
- package/dist/shared/media-storage.d.ts +7 -0
- package/dist/shared/media-storage.js +58 -0
- package/dist/shared/media-storage.test.d.ts +1 -0
- package/dist/shared/media-storage.test.js +21 -0
- package/dist/telegram/event-handler.js +153 -19
- package/dist/wechat/client.d.ts +2 -1
- package/dist/wechat/client.js +30 -30
- package/dist/wechat/client.test.d.ts +1 -0
- package/dist/wechat/client.test.js +30 -0
- package/dist/wechat/event-handler.js +143 -59
- package/dist/wechat/message-sender.d.ts +4 -0
- package/dist/wechat/message-sender.js +7 -0
- package/dist/wechat/message-sender.test.d.ts +1 -0
- package/dist/wechat/message-sender.test.js +21 -0
- package/dist/wechat/types.d.ts +7 -0
- package/dist/wework/client.d.ts +1 -0
- package/dist/wework/client.js +6 -0
- package/dist/wework/event-handler.d.ts +1 -1
- package/dist/wework/event-handler.js +171 -128
- package/dist/wework/message-sender.d.ts +8 -35
- package/dist/wework/message-sender.js +69 -91
- package/dist/wework/types.d.ts +7 -0
- package/package.json +2 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Platform } from "../config.js";
|
|
2
|
+
export type CapabilityLevel = "native" | "fallback" | "none";
|
|
3
|
+
export type InboundMessageKind = "text" | "image" | "file" | "voice" | "video";
|
|
4
|
+
export type OutboundMessageKind = "streamEdit" | "streamPush" | "image" | "card" | "typing";
|
|
5
|
+
export interface ChannelCapabilities {
|
|
6
|
+
inbound: Record<InboundMessageKind, CapabilityLevel>;
|
|
7
|
+
outbound: Record<OutboundMessageKind, CapabilityLevel>;
|
|
8
|
+
}
|
|
9
|
+
export declare const CHANNEL_CAPABILITIES: Record<Platform, ChannelCapabilities>;
|
|
10
|
+
export declare function buildUnsupportedInboundMessage(platform: Platform, kind: Exclude<InboundMessageKind, "text">): string;
|
|
11
|
+
export declare function buildImageFallbackMessage(platform: Platform, path: string): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const PLATFORM_LABELS = {
|
|
2
|
+
telegram: "Telegram",
|
|
3
|
+
feishu: "Feishu",
|
|
4
|
+
qq: "QQ",
|
|
5
|
+
wechat: "微信",
|
|
6
|
+
wework: "企业微信",
|
|
7
|
+
dingtalk: "钉钉",
|
|
8
|
+
};
|
|
9
|
+
export const CHANNEL_CAPABILITIES = {
|
|
10
|
+
telegram: {
|
|
11
|
+
inbound: { text: "native", image: "native", file: "native", voice: "native", video: "native" },
|
|
12
|
+
outbound: { streamEdit: "native", streamPush: "fallback", image: "native", card: "native", typing: "native" },
|
|
13
|
+
},
|
|
14
|
+
feishu: {
|
|
15
|
+
inbound: { text: "native", image: "native", file: "native", voice: "fallback", video: "fallback" },
|
|
16
|
+
outbound: { streamEdit: "native", streamPush: "fallback", image: "native", card: "native", typing: "native" },
|
|
17
|
+
},
|
|
18
|
+
qq: {
|
|
19
|
+
inbound: { text: "native", image: "fallback", file: "fallback", voice: "fallback", video: "fallback" },
|
|
20
|
+
outbound: { streamEdit: "native", streamPush: "fallback", image: "fallback", card: "fallback", typing: "fallback" },
|
|
21
|
+
},
|
|
22
|
+
wechat: {
|
|
23
|
+
inbound: { text: "native", image: "fallback", file: "fallback", voice: "fallback", video: "fallback" },
|
|
24
|
+
outbound: { streamEdit: "native", streamPush: "fallback", image: "fallback", card: "native", typing: "native" },
|
|
25
|
+
},
|
|
26
|
+
wework: {
|
|
27
|
+
inbound: { text: "native", image: "fallback", file: "fallback", voice: "fallback", video: "fallback" },
|
|
28
|
+
outbound: { streamEdit: "native", streamPush: "fallback", image: "native", card: "native", typing: "native" },
|
|
29
|
+
},
|
|
30
|
+
dingtalk: {
|
|
31
|
+
inbound: { text: "native", image: "fallback", file: "fallback", voice: "fallback", video: "fallback" },
|
|
32
|
+
outbound: { streamEdit: "native", streamPush: "fallback", image: "fallback", card: "native", typing: "native" },
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
function listPreferredPlatforms(kind) {
|
|
36
|
+
return Object.entries(CHANNEL_CAPABILITIES)
|
|
37
|
+
.filter(([, capabilities]) => capabilities.inbound[kind] === "native")
|
|
38
|
+
.map(([platform]) => PLATFORM_LABELS[platform])
|
|
39
|
+
.join(" / ");
|
|
40
|
+
}
|
|
41
|
+
export function buildUnsupportedInboundMessage(platform, kind) {
|
|
42
|
+
const platformLabel = PLATFORM_LABELS[platform];
|
|
43
|
+
const preferred = listPreferredPlatforms(kind);
|
|
44
|
+
const kindLabel = kind === "image" ? "图片" :
|
|
45
|
+
kind === "file" ? "文件" :
|
|
46
|
+
kind === "voice" ? "语音" :
|
|
47
|
+
"视频";
|
|
48
|
+
if (preferred) {
|
|
49
|
+
return `${platformLabel} 当前还不支持直接处理${kindLabel}消息。可改用 ${preferred},或先发送文字说明/文件链接后继续。`;
|
|
50
|
+
}
|
|
51
|
+
return `${platformLabel} 当前还不支持直接处理${kindLabel}消息。请先发送文字说明或可访问的文件链接。`;
|
|
52
|
+
}
|
|
53
|
+
export function buildImageFallbackMessage(platform, path) {
|
|
54
|
+
return `${PLATFORM_LABELS[platform]} 当前没有原生图片回传,已改为文本提示。图片已保存到: ${path}`;
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { CHANNEL_CAPABILITIES, buildImageFallbackMessage, buildUnsupportedInboundMessage, } from "./capabilities.js";
|
|
3
|
+
describe("channel capabilities", () => {
|
|
4
|
+
it("defines core inbound and outbound capabilities for every channel", () => {
|
|
5
|
+
expect(CHANNEL_CAPABILITIES.telegram.inbound.image).toBe("native");
|
|
6
|
+
expect(CHANNEL_CAPABILITIES.telegram.inbound.file).toBe("native");
|
|
7
|
+
expect(CHANNEL_CAPABILITIES.feishu.outbound.card).toBe("native");
|
|
8
|
+
expect(CHANNEL_CAPABILITIES.qq.inbound.image).toBe("fallback");
|
|
9
|
+
expect(CHANNEL_CAPABILITIES.qq.inbound.voice).toBe("fallback");
|
|
10
|
+
expect(CHANNEL_CAPABILITIES.qq.inbound.video).toBe("fallback");
|
|
11
|
+
expect(CHANNEL_CAPABILITIES.wechat.inbound.image).toBe("fallback");
|
|
12
|
+
expect(CHANNEL_CAPABILITIES.wework.inbound.video).toBe("fallback");
|
|
13
|
+
expect(CHANNEL_CAPABILITIES.wework.outbound.image).toBe("native");
|
|
14
|
+
expect(CHANNEL_CAPABILITIES.dingtalk.inbound.file).toBe("fallback");
|
|
15
|
+
});
|
|
16
|
+
it("builds actionable fallback copy for unsupported inbound messages", () => {
|
|
17
|
+
expect(buildUnsupportedInboundMessage("dingtalk", "image")).toContain("Telegram");
|
|
18
|
+
expect(buildUnsupportedInboundMessage("dingtalk", "image")).toContain("Feishu");
|
|
19
|
+
expect(buildUnsupportedInboundMessage("dingtalk", "image")).toContain("文字说明");
|
|
20
|
+
});
|
|
21
|
+
it("builds a consistent image delivery fallback message", () => {
|
|
22
|
+
expect(buildImageFallbackMessage("qq", "/tmp/out.png")).toContain("/tmp/out.png");
|
|
23
|
+
expect(buildImageFallbackMessage("qq", "/tmp/out.png")).toContain("QQ");
|
|
24
|
+
});
|
|
25
|
+
});
|
package/dist/cli.js
CHANGED
|
@@ -75,6 +75,29 @@ async function cmdStop() {
|
|
|
75
75
|
console.log("\nopen-im stopped.");
|
|
76
76
|
console.log(` pid: ${result.pid}`);
|
|
77
77
|
}
|
|
78
|
+
async function cmdRestart() {
|
|
79
|
+
const status = getManagerStatus();
|
|
80
|
+
if (status.pid) {
|
|
81
|
+
await stopBackgroundService();
|
|
82
|
+
const stopped = await stopManagerProcess();
|
|
83
|
+
console.log("\nopen-im stopped.");
|
|
84
|
+
console.log(` pid: ${stopped.pid}`);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
console.log("open-im is not running in the background. Starting a new instance.");
|
|
88
|
+
}
|
|
89
|
+
if (!(await ensureConfigured("start"))) {
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
const { updated } = await checkAndUpdate();
|
|
93
|
+
if (updated) {
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
96
|
+
const child = await startManagerProcess(process.cwd());
|
|
97
|
+
console.log("\nopen-im restarted in the background.");
|
|
98
|
+
console.log(` pid: ${child.pid}`);
|
|
99
|
+
console.log(` config page: ${getWebConfigUrl()}`);
|
|
100
|
+
}
|
|
78
101
|
async function cmdInit() {
|
|
79
102
|
console.log("\nopen-im CLI setup\n");
|
|
80
103
|
const saved = await ensureConfigured("init");
|
|
@@ -101,6 +124,7 @@ Usage: open-im <command>
|
|
|
101
124
|
Commands:
|
|
102
125
|
start Run the full app in the background and serve the local config page
|
|
103
126
|
stop Stop the full app
|
|
127
|
+
restart Restart the full app in the background
|
|
104
128
|
init Run CLI setup
|
|
105
129
|
dev Run in the foreground for debugging
|
|
106
130
|
|
|
@@ -117,6 +141,7 @@ const cmd = process.argv[2];
|
|
|
117
141
|
const commands = {
|
|
118
142
|
start: cmdStart,
|
|
119
143
|
stop: cmdStop,
|
|
144
|
+
restart: cmdRestart,
|
|
120
145
|
init: cmdInit,
|
|
121
146
|
dev: cmdDev,
|
|
122
147
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Codex CLI
|
|
2
|
+
* Codex CLI runner for `codex exec --json` JSONL output.
|
|
3
3
|
*/
|
|
4
4
|
export interface CodexRunCallbacks {
|
|
5
5
|
onText: (accumulated: string) => void;
|
|
@@ -26,7 +26,6 @@ export interface CodexRunOptions {
|
|
|
26
26
|
model?: string;
|
|
27
27
|
chatId?: string;
|
|
28
28
|
hookPort?: number;
|
|
29
|
-
/** HTTP/HTTPS 代理,用于访问 chatgpt.com */
|
|
30
29
|
proxy?: string;
|
|
31
30
|
}
|
|
32
31
|
export interface CodexRunHandle {
|
package/dist/codex/cli-runner.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Codex CLI
|
|
2
|
+
* Codex CLI runner for `codex exec --json` JSONL output.
|
|
3
3
|
*/
|
|
4
|
-
import { spawn } from 'node:child_process';
|
|
5
|
-
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
6
5
|
import { readFileSync } from 'node:fs';
|
|
7
6
|
import { dirname, join } from 'node:path';
|
|
8
7
|
import { createInterface } from 'node:readline';
|
|
9
8
|
import { createLogger } from '../logger.js';
|
|
10
9
|
const log = createLogger('CodexCli');
|
|
11
10
|
const windowsCodexLaunchCache = new Map();
|
|
11
|
+
const DEFAULT_IDLE_TIMEOUT_MS = 90_000;
|
|
12
12
|
function parseCodexEvent(line) {
|
|
13
13
|
const trimmed = line.trim();
|
|
14
14
|
if (!trimmed)
|
|
@@ -21,32 +21,31 @@ function parseCodexEvent(line) {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
function buildCodexArgs(_prompt, sessionId, workDir, options) {
|
|
24
|
-
const commonOptions = [
|
|
25
|
-
const newSessionOptions = [...commonOptions,
|
|
24
|
+
const commonOptions = ['--json', '--skip-git-repo-check'];
|
|
25
|
+
const newSessionOptions = [...commonOptions, '--cd', workDir];
|
|
26
26
|
const resumeOptions = [...commonOptions];
|
|
27
|
-
const canResume = Boolean(sessionId) && options?.permissionMode !==
|
|
27
|
+
const canResume = Boolean(sessionId) && options?.permissionMode !== 'plan';
|
|
28
28
|
if (options?.skipPermissions) {
|
|
29
|
-
newSessionOptions.push(
|
|
30
|
-
resumeOptions.push(
|
|
29
|
+
newSessionOptions.push('--dangerously-bypass-approvals-and-sandbox');
|
|
30
|
+
resumeOptions.push('--dangerously-bypass-approvals-and-sandbox');
|
|
31
31
|
}
|
|
32
|
-
else if (options?.permissionMode ===
|
|
33
|
-
|
|
34
|
-
newSessionOptions.push("--sandbox", "read-only");
|
|
32
|
+
else if (options?.permissionMode === 'plan') {
|
|
33
|
+
newSessionOptions.push('--sandbox', 'read-only');
|
|
35
34
|
}
|
|
36
35
|
else {
|
|
37
|
-
newSessionOptions.push(
|
|
38
|
-
resumeOptions.push(
|
|
36
|
+
newSessionOptions.push('--full-auto');
|
|
37
|
+
resumeOptions.push('--full-auto');
|
|
39
38
|
}
|
|
40
39
|
if (options?.model) {
|
|
41
|
-
newSessionOptions.push(
|
|
42
|
-
resumeOptions.push(
|
|
40
|
+
newSessionOptions.push('--model', options.model);
|
|
41
|
+
resumeOptions.push('--model', options.model);
|
|
43
42
|
}
|
|
44
43
|
if (sessionId && !canResume) {
|
|
45
|
-
log.warn(
|
|
44
|
+
log.warn('Codex plan mode does not support resume; starting a new read-only session');
|
|
46
45
|
}
|
|
47
46
|
return canResume
|
|
48
|
-
? [
|
|
49
|
-
: [
|
|
47
|
+
? ['exec', 'resume', ...resumeOptions, sessionId, '-']
|
|
48
|
+
: ['exec', ...newSessionOptions, '-'];
|
|
50
49
|
}
|
|
51
50
|
function quoteForWindowsCmd(arg) {
|
|
52
51
|
if (/^[A-Za-z0-9_./:=+\\-]+$/.test(arg)) {
|
|
@@ -140,9 +139,7 @@ export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options
|
|
|
140
139
|
log.info(`Spawning Codex CLI: path=${cliPath}, cwd=${workDir}, session=${sessionId ?? 'new'}, args=${argsForLog}`);
|
|
141
140
|
const isWinCmd = process.platform === 'win32' &&
|
|
142
141
|
(/\.(cmd|bat)$/i.test(cliPath) || cliPath === 'codex');
|
|
143
|
-
const directWindowsLaunch = isWinCmd
|
|
144
|
-
? resolveWindowsCodexLaunch(cliPath, args)
|
|
145
|
-
: null;
|
|
142
|
+
const directWindowsLaunch = isWinCmd ? resolveWindowsCodexLaunch(cliPath, args) : null;
|
|
146
143
|
const spawnCmd = directWindowsLaunch
|
|
147
144
|
? directWindowsLaunch.command
|
|
148
145
|
: isWinCmd
|
|
@@ -175,17 +172,50 @@ export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options
|
|
|
175
172
|
const timeoutMs = options?.timeoutMs && options.timeoutMs > 0
|
|
176
173
|
? Math.min(options.timeoutMs, MAX_TIMEOUT)
|
|
177
174
|
: 0;
|
|
175
|
+
const idleTimeoutMs = timeoutMs > 0
|
|
176
|
+
? Math.min(DEFAULT_IDLE_TIMEOUT_MS, timeoutMs)
|
|
177
|
+
: DEFAULT_IDLE_TIMEOUT_MS;
|
|
178
178
|
let timeoutHandle = null;
|
|
179
|
+
let idleTimeoutHandle = null;
|
|
180
|
+
const rl = createInterface({ input: child.stdout });
|
|
181
|
+
const clearTimers = () => {
|
|
182
|
+
if (timeoutHandle) {
|
|
183
|
+
clearTimeout(timeoutHandle);
|
|
184
|
+
timeoutHandle = null;
|
|
185
|
+
}
|
|
186
|
+
if (idleTimeoutHandle) {
|
|
187
|
+
clearTimeout(idleTimeoutHandle);
|
|
188
|
+
idleTimeoutHandle = null;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
const failAndTerminate = (message, logMessage) => {
|
|
192
|
+
if (completed)
|
|
193
|
+
return;
|
|
194
|
+
completed = true;
|
|
195
|
+
clearTimers();
|
|
196
|
+
log.warn(logMessage);
|
|
197
|
+
rl.close();
|
|
198
|
+
if (!child.killed)
|
|
199
|
+
child.kill('SIGTERM');
|
|
200
|
+
callbacks.onError(message);
|
|
201
|
+
};
|
|
202
|
+
const resetIdleTimeout = () => {
|
|
203
|
+
if (idleTimeoutMs <= 0 || completed)
|
|
204
|
+
return;
|
|
205
|
+
if (idleTimeoutHandle)
|
|
206
|
+
clearTimeout(idleTimeoutHandle);
|
|
207
|
+
idleTimeoutHandle = setTimeout(() => {
|
|
208
|
+
failAndTerminate(`Codex 执行长时间无输出,已自动终止(${idleTimeoutMs}ms)`, `Codex CLI idle timeout after ${idleTimeoutMs}ms, killing pid=${child.pid}`);
|
|
209
|
+
}, idleTimeoutMs);
|
|
210
|
+
};
|
|
179
211
|
if (timeoutMs > 0) {
|
|
180
212
|
timeoutHandle = setTimeout(() => {
|
|
181
213
|
if (!completed && !child.killed) {
|
|
182
|
-
|
|
183
|
-
log.warn(`Codex CLI timeout after ${timeoutMs}ms, killing pid=${child.pid}`);
|
|
184
|
-
child.kill('SIGTERM');
|
|
185
|
-
callbacks.onError(`执行超时(${timeoutMs}ms),已终止进程`);
|
|
214
|
+
failAndTerminate(`执行超时(${timeoutMs}ms),已终止进程`, `Codex CLI timeout after ${timeoutMs}ms, killing pid=${child.pid}`);
|
|
186
215
|
}
|
|
187
216
|
}, timeoutMs);
|
|
188
217
|
}
|
|
218
|
+
resetIdleTimeout();
|
|
189
219
|
const MAX_STDERR_HEAD = 4 * 1024;
|
|
190
220
|
const MAX_STDERR_TAIL = 6 * 1024;
|
|
191
221
|
let stderrHead = '';
|
|
@@ -193,6 +223,7 @@ export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options
|
|
|
193
223
|
let stderrTotal = 0;
|
|
194
224
|
let stderrHeadFull = false;
|
|
195
225
|
child.stderr?.on('data', (chunk) => {
|
|
226
|
+
resetIdleTimeout();
|
|
196
227
|
const text = chunk.toString();
|
|
197
228
|
stderrTotal += text.length;
|
|
198
229
|
if (!stderrHeadFull) {
|
|
@@ -209,8 +240,8 @@ export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options
|
|
|
209
240
|
}
|
|
210
241
|
log.debug(`[stderr] ${text.trimEnd()}`);
|
|
211
242
|
});
|
|
212
|
-
const rl = createInterface({ input: child.stdout });
|
|
213
243
|
rl.on('line', (line) => {
|
|
244
|
+
resetIdleTimeout();
|
|
214
245
|
const event = parseCodexEvent(line);
|
|
215
246
|
if (!event)
|
|
216
247
|
return;
|
|
@@ -224,8 +255,7 @@ export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options
|
|
|
224
255
|
}
|
|
225
256
|
if (type === 'turn.failed') {
|
|
226
257
|
completed = true;
|
|
227
|
-
|
|
228
|
-
clearTimeout(timeoutHandle);
|
|
258
|
+
clearTimers();
|
|
229
259
|
const err = event.error;
|
|
230
260
|
callbacks.onError(err?.message ?? 'Codex turn failed');
|
|
231
261
|
return;
|
|
@@ -236,8 +266,7 @@ export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options
|
|
|
236
266
|
return;
|
|
237
267
|
}
|
|
238
268
|
completed = true;
|
|
239
|
-
|
|
240
|
-
clearTimeout(timeoutHandle);
|
|
269
|
+
clearTimers();
|
|
241
270
|
callbacks.onError(msg ?? 'Codex stream error');
|
|
242
271
|
return;
|
|
243
272
|
}
|
|
@@ -291,15 +320,13 @@ export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options
|
|
|
291
320
|
}
|
|
292
321
|
if (type === 'turn.completed') {
|
|
293
322
|
completed = true;
|
|
294
|
-
|
|
295
|
-
clearTimeout(timeoutHandle);
|
|
296
|
-
const durationMs = Date.now() - startTime;
|
|
323
|
+
clearTimers();
|
|
297
324
|
callbacks.onComplete({
|
|
298
325
|
success: true,
|
|
299
326
|
result: accumulated,
|
|
300
327
|
accumulated,
|
|
301
328
|
cost: 0,
|
|
302
|
-
durationMs,
|
|
329
|
+
durationMs: Date.now() - startTime,
|
|
303
330
|
numTurns: 1,
|
|
304
331
|
toolStats,
|
|
305
332
|
});
|
|
@@ -311,45 +338,43 @@ export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options
|
|
|
311
338
|
const finalize = () => {
|
|
312
339
|
if (!rlClosed || !childClosed)
|
|
313
340
|
return;
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
else if (stderrTotal <= MAX_STDERR_HEAD + MAX_STDERR_TAIL) {
|
|
324
|
-
errMsg = stderrHead + stderrTail.slice(stderrTail.length - (stderrTotal - MAX_STDERR_HEAD));
|
|
325
|
-
}
|
|
326
|
-
else {
|
|
327
|
-
errMsg =
|
|
328
|
-
stderrHead +
|
|
329
|
-
`\n\n... (省略 ${stderrTotal - MAX_STDERR_HEAD - MAX_STDERR_TAIL} 字节) ...\n\n` +
|
|
330
|
-
stderrTail;
|
|
331
|
-
}
|
|
341
|
+
clearTimers();
|
|
342
|
+
if (completed)
|
|
343
|
+
return;
|
|
344
|
+
if (exitCode !== null && exitCode !== 0) {
|
|
345
|
+
let errMsg = '';
|
|
346
|
+
if (stderrTotal > 0) {
|
|
347
|
+
if (!stderrHeadFull) {
|
|
348
|
+
errMsg = stderrHead;
|
|
332
349
|
}
|
|
333
|
-
if (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
350
|
+
else if (stderrTotal <= MAX_STDERR_HEAD + MAX_STDERR_TAIL) {
|
|
351
|
+
errMsg = stderrHead + stderrTail.slice(stderrTail.length - (stderrTotal - MAX_STDERR_HEAD));
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
errMsg =
|
|
355
|
+
stderrHead +
|
|
356
|
+
`\n\n... (omitted ${stderrTotal - MAX_STDERR_HEAD - MAX_STDERR_TAIL} bytes) ...\n\n` +
|
|
357
|
+
stderrTail;
|
|
338
358
|
}
|
|
339
|
-
callbacks.onError(errMsg || `Codex CLI exited with code ${exitCode}`);
|
|
340
359
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
cost: 0,
|
|
347
|
-
durationMs: Date.now() - startTime,
|
|
348
|
-
numTurns: 0,
|
|
349
|
-
toolStats,
|
|
350
|
-
});
|
|
360
|
+
if (sessionId &&
|
|
361
|
+
(errMsg.includes('No session found') ||
|
|
362
|
+
errMsg.includes('No conversation found') ||
|
|
363
|
+
errMsg.includes('Unable to find session'))) {
|
|
364
|
+
callbacks.onSessionInvalid?.();
|
|
351
365
|
}
|
|
366
|
+
callbacks.onError(errMsg || `Codex CLI exited with code ${exitCode}`);
|
|
367
|
+
return;
|
|
352
368
|
}
|
|
369
|
+
callbacks.onComplete({
|
|
370
|
+
success: true,
|
|
371
|
+
result: accumulated,
|
|
372
|
+
accumulated,
|
|
373
|
+
cost: 0,
|
|
374
|
+
durationMs: Date.now() - startTime,
|
|
375
|
+
numTurns: 0,
|
|
376
|
+
toolStats,
|
|
377
|
+
});
|
|
353
378
|
};
|
|
354
379
|
child.on('close', (code) => {
|
|
355
380
|
log.info(`Codex CLI closed: exitCode=${code}, pid=${child.pid}`);
|
|
@@ -364,8 +389,7 @@ export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options
|
|
|
364
389
|
child.on('error', (err) => {
|
|
365
390
|
const errorCode = err.code;
|
|
366
391
|
log.error(`Codex CLI spawn error: ${err.message}, code=${errorCode}, path=${cliPath}`);
|
|
367
|
-
|
|
368
|
-
clearTimeout(timeoutHandle);
|
|
392
|
+
clearTimers();
|
|
369
393
|
if (!completed) {
|
|
370
394
|
completed = true;
|
|
371
395
|
callbacks.onError(`Failed to start Codex CLI: ${err.message}`);
|
|
@@ -375,8 +399,8 @@ export function runCodex(cliPath, prompt, sessionId, workDir, callbacks, options
|
|
|
375
399
|
});
|
|
376
400
|
return {
|
|
377
401
|
abort: () => {
|
|
378
|
-
|
|
379
|
-
|
|
402
|
+
completed = true;
|
|
403
|
+
clearTimers();
|
|
380
404
|
rl.close();
|
|
381
405
|
if (!child.killed)
|
|
382
406
|
child.kill('SIGTERM');
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
export declare const PAGE_TEXTS: {
|
|
2
|
+
readonly en: {
|
|
3
|
+
readonly pageTitle: "open-im local control";
|
|
4
|
+
readonly heroBadge: "open-im local control";
|
|
5
|
+
readonly heroTitle: "Local bridge control.";
|
|
6
|
+
readonly heroBody: "";
|
|
7
|
+
readonly heroBodyFull: "One local bridge for Telegram, Feishu, QQ, WeWork, and DingTalk.";
|
|
8
|
+
readonly heroKicker: "Local AI bridge";
|
|
9
|
+
readonly langButton: "中文";
|
|
10
|
+
readonly controlCenter: "Control center";
|
|
11
|
+
readonly sidebarNoteTitle: "Local workflow";
|
|
12
|
+
readonly sidebarNoteBody: "Configure at least one platform, save the config, then start the bridge from Service.";
|
|
13
|
+
readonly mode: "Flow";
|
|
14
|
+
readonly dashboardTitle: "Dashboard";
|
|
15
|
+
readonly dashboardSubtitle: "Platform health status";
|
|
16
|
+
readonly dashboardSubtitleFull: "Platform status and setup progress";
|
|
17
|
+
readonly quickActionsTitle: "Quick Actions";
|
|
18
|
+
readonly serviceTitle: "Service Control";
|
|
19
|
+
readonly serviceHint: "Validate, save, start, and stop the local bridge from one place.";
|
|
20
|
+
readonly refreshHealth: "Refresh Health Status";
|
|
21
|
+
readonly viewConfig: "View Configuration";
|
|
22
|
+
readonly overviewTitle: "Start here";
|
|
23
|
+
readonly overviewBody: "Configure at least one platform, save the configuration, then start the bridge from the service section.";
|
|
24
|
+
readonly openPlatforms: "Open Platforms";
|
|
25
|
+
readonly openService: "Open Service";
|
|
26
|
+
readonly healthChecking: "Checking...";
|
|
27
|
+
readonly healthy: "Healthy";
|
|
28
|
+
readonly unhealthy: "Issues Found";
|
|
29
|
+
readonly notConfigured: "Not Configured";
|
|
30
|
+
readonly disabled: "Disabled";
|
|
31
|
+
readonly statConfiguredLabel: "Configured";
|
|
32
|
+
readonly statConfiguredMeta: "Platforms with saved credentials";
|
|
33
|
+
readonly statEnabledLabel: "Enabled";
|
|
34
|
+
readonly statEnabledMeta: "Platforms selected for startup";
|
|
35
|
+
readonly statServiceLabel: "Service";
|
|
36
|
+
readonly serviceRunningShort: "Running";
|
|
37
|
+
readonly serviceIdleShort: "Idle";
|
|
38
|
+
readonly serviceRunningMeta: "Local bridge process is active";
|
|
39
|
+
readonly serviceIdleMeta: "Bridge has not been started yet";
|
|
40
|
+
readonly listSeparator: ", ";
|
|
41
|
+
readonly platformsTitle: "Platforms";
|
|
42
|
+
readonly platformsHint: "Disabled platforms keep their saved values.";
|
|
43
|
+
readonly enabled: "Enabled";
|
|
44
|
+
readonly botToken: "Bot token";
|
|
45
|
+
readonly proxy: "Proxy";
|
|
46
|
+
readonly allowedUserIds: "Allowed user IDs";
|
|
47
|
+
readonly telegramHelp: "Get credentials: visit <a href=\"https://t.me/BotFather\" target=\"_blank\">@BotFather</a>, send /newbot, then copy the Bot Token";
|
|
48
|
+
readonly appId: "App ID";
|
|
49
|
+
readonly appSecret: "App Secret";
|
|
50
|
+
readonly feishuHelp: "Get credentials: visit <a href=\"https://open.feishu.cn/\" target=\"_blank\">Feishu Open Platform</a>, create an app, enable the bot, and copy the App ID / App Secret";
|
|
51
|
+
readonly qqAppId: "App ID";
|
|
52
|
+
readonly qqAppSecret: "App Secret";
|
|
53
|
+
readonly qqHelp: "Get credentials: visit <a href=\"https://bot.q.qq.com\" target=\"_blank\">QQ Open Platform</a>, create a bot, and copy the App ID / App Secret";
|
|
54
|
+
readonly corpId: "Corp ID / Bot ID";
|
|
55
|
+
readonly weworkHelp: "Get credentials: visit <a href=\"https://work.weixin.qq.com/\" target=\"_blank\">WeWork Admin Console</a>, create an app, and copy the Bot ID (Corp ID) / Secret";
|
|
56
|
+
readonly clientId: "Client ID / AppKey";
|
|
57
|
+
readonly clientSecret: "Client Secret / AppSecret";
|
|
58
|
+
readonly dingtalkHelp: "Get credentials: Create an enterprise internal app on DingTalk Open Platform, enable Stream Mode, and get Client ID / Client Secret";
|
|
59
|
+
readonly secret: "Secret";
|
|
60
|
+
readonly cardTemplateId: "Card template ID";
|
|
61
|
+
readonly optional: "Optional";
|
|
62
|
+
readonly commaSeparatedIds: "Comma-separated IDs";
|
|
63
|
+
readonly aiTitle: "AI Tooling";
|
|
64
|
+
readonly aiHint: "";
|
|
65
|
+
readonly claudeNote: "Claude credentials are still read from environment variables or ~/.claude/settings.json. This page manages local bridge config, not Claude account auth.";
|
|
66
|
+
readonly aiTool: "Default AI tool";
|
|
67
|
+
readonly workDir: "Default work directory";
|
|
68
|
+
readonly claudeCli: "Claude CLI path";
|
|
69
|
+
readonly cursorCli: "Cursor CLI path";
|
|
70
|
+
readonly codexCli: "Codex CLI path";
|
|
71
|
+
readonly codexProxy: "Codex proxy";
|
|
72
|
+
readonly claudeTimeout: "Claude timeout (ms)";
|
|
73
|
+
readonly claudeModel: "Claude model";
|
|
74
|
+
readonly hookPort: "Hook port";
|
|
75
|
+
readonly logLevel: "Log level";
|
|
76
|
+
readonly logLevelDefault: "default (app default)";
|
|
77
|
+
readonly autoApprove: "Auto-approve tool permissions";
|
|
78
|
+
readonly sdkMode: "Use Claude SDK mode";
|
|
79
|
+
readonly validate: "Validate";
|
|
80
|
+
readonly test: "Check config";
|
|
81
|
+
readonly testing: "Checking...";
|
|
82
|
+
readonly testSuccess: "Configuration looks valid.";
|
|
83
|
+
readonly testFailed: "Configuration issue: {error}";
|
|
84
|
+
readonly save: "Save config";
|
|
85
|
+
readonly start: "Start bridge";
|
|
86
|
+
readonly stop: "Stop bridge";
|
|
87
|
+
readonly bridgeRunning: "Bridge running (pid {pid})";
|
|
88
|
+
readonly bridgeStopped: "Bridge stopped";
|
|
89
|
+
readonly bridgeActive: "Bridge worker is active.";
|
|
90
|
+
readonly bridgeInactive: "Bridge worker is currently stopped.";
|
|
91
|
+
readonly summaryEnabled: "Enabled platforms: {platforms} | AI tool: {tool}";
|
|
92
|
+
readonly summaryEmpty: "No platform enabled yet | AI tool: {tool}";
|
|
93
|
+
readonly ready: "Control surface ready.";
|
|
94
|
+
readonly validationOk: "Configuration looks internally consistent.";
|
|
95
|
+
readonly saveOk: "Configuration saved.";
|
|
96
|
+
readonly startOk: "Bridge started.";
|
|
97
|
+
readonly stopOk: "Bridge stopped.";
|
|
98
|
+
};
|
|
99
|
+
readonly zh: {
|
|
100
|
+
readonly pageTitle: "open-im 本地控制台";
|
|
101
|
+
readonly heroBadge: "open-im";
|
|
102
|
+
readonly heroTitle: "本地桥接控制台";
|
|
103
|
+
readonly heroBody: "";
|
|
104
|
+
readonly heroBodyFull: "一个本地桥接入口,统一连接 Telegram、Feishu、QQ、WeWork 和 DingTalk。";
|
|
105
|
+
readonly heroKicker: "本地 AI 桥接";
|
|
106
|
+
readonly langButton: "EN";
|
|
107
|
+
readonly controlCenter: "控制中心";
|
|
108
|
+
readonly sidebarNoteTitle: "本地工作流";
|
|
109
|
+
readonly sidebarNoteBody: "至少配置一个平台,保存配置,然后在服务控制区启动桥接。";
|
|
110
|
+
readonly mode: "模式";
|
|
111
|
+
readonly dashboardTitle: "概览";
|
|
112
|
+
readonly dashboardSubtitle: "平台健康状态";
|
|
113
|
+
readonly dashboardSubtitleFull: "平台状态与接入进度";
|
|
114
|
+
readonly quickActionsTitle: "快捷操作";
|
|
115
|
+
readonly refreshHealth: "刷新健康状态";
|
|
116
|
+
readonly viewConfig: "查看配置";
|
|
117
|
+
readonly overviewTitle: "先完成基础配置";
|
|
118
|
+
readonly overviewBody: "先配置至少一个平台,保存本地配置,再到服务控制区启动桥接。";
|
|
119
|
+
readonly openPlatforms: "打开平台配置";
|
|
120
|
+
readonly openService: "打开服务控制";
|
|
121
|
+
readonly healthChecking: "检查中...";
|
|
122
|
+
readonly healthy: "正常";
|
|
123
|
+
readonly unhealthy: "有问题";
|
|
124
|
+
readonly notConfigured: "未配置";
|
|
125
|
+
readonly disabled: "已禁用";
|
|
126
|
+
readonly statConfiguredLabel: "已配置";
|
|
127
|
+
readonly statConfiguredMeta: "已填写凭证的平台数量";
|
|
128
|
+
readonly statEnabledLabel: "已启用";
|
|
129
|
+
readonly statEnabledMeta: "会随服务启动的平台数量";
|
|
130
|
+
readonly statServiceLabel: "服务";
|
|
131
|
+
readonly serviceRunningShort: "运行中";
|
|
132
|
+
readonly serviceIdleShort: "未启动";
|
|
133
|
+
readonly serviceRunningMeta: "本地桥接进程正在运行";
|
|
134
|
+
readonly serviceIdleMeta: "桥接服务尚未启动";
|
|
135
|
+
readonly listSeparator: "、";
|
|
136
|
+
readonly platformsTitle: "平台配置";
|
|
137
|
+
readonly platformsHint: "禁用的平台会保留已保存的值。";
|
|
138
|
+
readonly enabled: "启用";
|
|
139
|
+
readonly botToken: "Bot Token";
|
|
140
|
+
readonly proxy: "代理";
|
|
141
|
+
readonly allowedUserIds: "允许的用户 ID";
|
|
142
|
+
readonly telegramHelp: "获取凭证:访问 <a href=\"https://t.me/BotFather\" target=\"_blank\">@BotFather</a>,发送 /newbot 创建机器人并拿到 Bot Token";
|
|
143
|
+
readonly appId: "App ID";
|
|
144
|
+
readonly appSecret: "App Secret";
|
|
145
|
+
readonly feishuHelp: "获取凭证:访问 <a href=\"https://open.feishu.cn/\" target=\"_blank\">飞书开放平台</a>,创建应用、启用机器人并拿到 App ID / App Secret";
|
|
146
|
+
readonly qqAppId: "App ID";
|
|
147
|
+
readonly qqAppSecret: "App Secret";
|
|
148
|
+
readonly qqHelp: "获取凭证:访问 <a href=\"https://bot.q.qq.com\" target=\"_blank\">QQ 开放平台</a>,创建机器人并拿到 App ID / App Secret";
|
|
149
|
+
readonly corpId: "Corp ID / Bot ID";
|
|
150
|
+
readonly weworkHelp: "获取凭证:访问 <a href=\"https://work.weixin.qq.com/\" target=\"_blank\">企业微信管理后台</a>,创建应用并拿到 Bot ID(Corp ID)/ Secret";
|
|
151
|
+
readonly clientId: "Client ID / AppKey";
|
|
152
|
+
readonly clientSecret: "Client Secret / AppSecret";
|
|
153
|
+
readonly dingtalkHelp: "获取凭证:在钉钉开放平台创建企业内部应用,启用 Stream Mode,并拿到 Client ID / Client Secret";
|
|
154
|
+
readonly cardTemplateId: "卡片模板 ID";
|
|
155
|
+
readonly optional: "可选";
|
|
156
|
+
readonly commaSeparatedIds: "多个 ID 用逗号分隔";
|
|
157
|
+
readonly aiTitle: "AI 工具配置";
|
|
158
|
+
readonly aiHint: "";
|
|
159
|
+
readonly claudeNote: "Claude 凭证仍然从环境变量或 ~/.claude/settings.json 读取。这个页面只管理本地桥接配置,不负责 Claude 账号登录。";
|
|
160
|
+
readonly aiTool: "默认 AI 工具";
|
|
161
|
+
readonly workDir: "默认工作目录";
|
|
162
|
+
readonly claudeCli: "Claude CLI 路径";
|
|
163
|
+
readonly cursorCli: "Cursor CLI 路径";
|
|
164
|
+
readonly codexCli: "Codex CLI 路径";
|
|
165
|
+
readonly codexProxy: "Codex 代理";
|
|
166
|
+
readonly claudeTimeout: "Claude 超时(毫秒)";
|
|
167
|
+
readonly claudeModel: "Claude 模型";
|
|
168
|
+
readonly hookPort: "Hook 端口";
|
|
169
|
+
readonly logLevel: "日志级别";
|
|
170
|
+
readonly logLevelDefault: "default(程序默认)";
|
|
171
|
+
readonly autoApprove: "自动批准工具权限";
|
|
172
|
+
readonly sdkMode: "使用 Claude SDK 模式";
|
|
173
|
+
readonly validate: "校验配置";
|
|
174
|
+
readonly test: "校验配置";
|
|
175
|
+
readonly testing: "校验中...";
|
|
176
|
+
readonly testSuccess: "配置校验通过。";
|
|
177
|
+
readonly testFailed: "配置有问题:{error}";
|
|
178
|
+
readonly save: "保存配置";
|
|
179
|
+
readonly start: "启动桥接";
|
|
180
|
+
readonly stop: "停止桥接";
|
|
181
|
+
readonly bridgeRunning: "桥接运行中(pid {pid})";
|
|
182
|
+
readonly bridgeStopped: "桥接已停止";
|
|
183
|
+
readonly bridgeActive: "桥接 worker 正在运行。";
|
|
184
|
+
readonly bridgeInactive: "桥接 worker 当前已停止。";
|
|
185
|
+
readonly summaryEnabled: "已启用平台:{platforms} | AI 工具:{tool}";
|
|
186
|
+
readonly summaryEmpty: "暂未启用平台 | AI 工具:{tool}";
|
|
187
|
+
readonly ready: "控制台已就绪。";
|
|
188
|
+
readonly validationOk: "配置校验通过。";
|
|
189
|
+
readonly saveOk: "配置已保存。";
|
|
190
|
+
readonly startOk: "桥接已启动。";
|
|
191
|
+
readonly stopOk: "桥接已停止。";
|
|
192
|
+
};
|
|
193
|
+
};
|