@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.
Files changed (68) hide show
  1. package/dist/channels/capabilities.d.ts +11 -0
  2. package/dist/channels/capabilities.js +55 -0
  3. package/dist/channels/capabilities.test.d.ts +1 -0
  4. package/dist/channels/capabilities.test.js +25 -0
  5. package/dist/cli.js +25 -0
  6. package/dist/codex/cli-runner.d.ts +1 -2
  7. package/dist/codex/cli-runner.js +96 -72
  8. package/dist/config-web-page-i18n.d.ts +193 -0
  9. package/dist/config-web-page-i18n.js +193 -0
  10. package/dist/config-web-page-script.d.ts +1 -0
  11. package/dist/config-web-page-script.js +370 -0
  12. package/dist/config-web-page-template.d.ts +2 -0
  13. package/dist/config-web-page-template.js +241 -0
  14. package/dist/config-web-page.d.ts +1 -0
  15. package/dist/config-web-page.js +5 -0
  16. package/dist/config-web-page.test.d.ts +1 -0
  17. package/dist/config-web-page.test.js +33 -0
  18. package/dist/config-web.d.ts +1 -0
  19. package/dist/config-web.js +149 -756
  20. package/dist/config-web.test.d.ts +1 -0
  21. package/dist/config-web.test.js +49 -0
  22. package/dist/dingtalk/event-handler.js +148 -34
  23. package/dist/dingtalk/message-sender.d.ts +1 -0
  24. package/dist/dingtalk/message-sender.js +4 -0
  25. package/dist/dingtalk/message-sender.test.js +5 -0
  26. package/dist/feishu/event-handler.js +87 -47
  27. package/dist/qq/client.js +21 -7
  28. package/dist/qq/event-handler.js +108 -12
  29. package/dist/qq/message-sender.d.ts +1 -0
  30. package/dist/qq/message-sender.js +4 -0
  31. package/dist/qq/message-sender.test.d.ts +1 -0
  32. package/dist/qq/message-sender.test.js +26 -0
  33. package/dist/qq/types.d.ts +18 -10
  34. package/dist/shared/media-analysis-prompt.d.ts +18 -0
  35. package/dist/shared/media-analysis-prompt.js +37 -0
  36. package/dist/shared/media-analysis-prompt.test.d.ts +1 -0
  37. package/dist/shared/media-analysis-prompt.test.js +40 -0
  38. package/dist/shared/media-context.d.ts +1 -0
  39. package/dist/shared/media-context.js +12 -0
  40. package/dist/shared/media-context.test.d.ts +1 -0
  41. package/dist/shared/media-context.test.js +13 -0
  42. package/dist/shared/media-prompt.d.ts +8 -0
  43. package/dist/shared/media-prompt.js +21 -0
  44. package/dist/shared/media-prompt.test.d.ts +1 -0
  45. package/dist/shared/media-prompt.test.js +26 -0
  46. package/dist/shared/media-storage.d.ts +7 -0
  47. package/dist/shared/media-storage.js +58 -0
  48. package/dist/shared/media-storage.test.d.ts +1 -0
  49. package/dist/shared/media-storage.test.js +21 -0
  50. package/dist/telegram/event-handler.js +153 -19
  51. package/dist/wechat/client.d.ts +2 -1
  52. package/dist/wechat/client.js +30 -30
  53. package/dist/wechat/client.test.d.ts +1 -0
  54. package/dist/wechat/client.test.js +30 -0
  55. package/dist/wechat/event-handler.js +143 -59
  56. package/dist/wechat/message-sender.d.ts +4 -0
  57. package/dist/wechat/message-sender.js +7 -0
  58. package/dist/wechat/message-sender.test.d.ts +1 -0
  59. package/dist/wechat/message-sender.test.js +21 -0
  60. package/dist/wechat/types.d.ts +7 -0
  61. package/dist/wework/client.d.ts +1 -0
  62. package/dist/wework/client.js +6 -0
  63. package/dist/wework/event-handler.d.ts +1 -1
  64. package/dist/wework/event-handler.js +171 -128
  65. package/dist/wework/message-sender.d.ts +8 -35
  66. package/dist/wework/message-sender.js +69 -91
  67. package/dist/wework/types.d.ts +7 -0
  68. 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 Runner - 解析 `codex exec --json` JSONL 输出。
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 {
@@ -1,14 +1,14 @@
1
1
  /**
2
- * Codex CLI Runner - 解析 `codex exec --json` JSONL 输出。
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 = ["--json", "--skip-git-repo-check"];
25
- const newSessionOptions = [...commonOptions, "--cd", workDir];
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 !== "plan";
27
+ const canResume = Boolean(sessionId) && options?.permissionMode !== 'plan';
28
28
  if (options?.skipPermissions) {
29
- newSessionOptions.push("--dangerously-bypass-approvals-and-sandbox");
30
- resumeOptions.push("--dangerously-bypass-approvals-and-sandbox");
29
+ newSessionOptions.push('--dangerously-bypass-approvals-and-sandbox');
30
+ resumeOptions.push('--dangerously-bypass-approvals-and-sandbox');
31
31
  }
32
- else if (options?.permissionMode === "plan") {
33
- // `codex exec resume` 当前不支持 `--sandbox` / `--cd`,plan 模式统一新开只读会话。
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("--full-auto");
38
- resumeOptions.push("--full-auto");
36
+ newSessionOptions.push('--full-auto');
37
+ resumeOptions.push('--full-auto');
39
38
  }
40
39
  if (options?.model) {
41
- newSessionOptions.push("--model", options.model);
42
- resumeOptions.push("--model", options.model);
40
+ newSessionOptions.push('--model', options.model);
41
+ resumeOptions.push('--model', options.model);
43
42
  }
44
43
  if (sessionId && !canResume) {
45
- log.warn("Codex plan mode does not support resume; starting a new read-only session");
44
+ log.warn('Codex plan mode does not support resume; starting a new read-only session');
46
45
  }
47
46
  return canResume
48
- ? ["exec", "resume", ...resumeOptions, sessionId, "-"]
49
- : ["exec", ...newSessionOptions, "-"];
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
- completed = true;
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
- if (timeoutHandle)
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
- if (timeoutHandle)
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
- if (timeoutHandle)
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
- if (timeoutHandle)
315
- clearTimeout(timeoutHandle);
316
- if (!completed) {
317
- if (exitCode !== null && exitCode !== 0) {
318
- let errMsg = '';
319
- if (stderrTotal > 0) {
320
- if (!stderrHeadFull) {
321
- errMsg = stderrHead;
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 (sessionId &&
334
- (errMsg.includes("No session found") ||
335
- errMsg.includes("No conversation found") ||
336
- errMsg.includes("Unable to find session"))) {
337
- callbacks.onSessionInvalid?.();
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
- else {
342
- callbacks.onComplete({
343
- success: true,
344
- result: accumulated,
345
- accumulated,
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
- if (timeoutHandle)
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
- if (timeoutHandle)
379
- clearTimeout(timeoutHandle);
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
+ };