@wu529778790/open-im 1.11.12 → 1.11.13-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -77,6 +77,7 @@ open-im start
77
77
  | `/a` | 查看当前 AI 工具及可选列表 |
78
78
  | `/a <工具名>` | 切换当前平台的 AI 工具(claude/codex/codebuddy/opencode) |
79
79
  | `/cd <路径>` / `/pwd` | 切换/查看工作目录 |
80
+ | `/restart` | 重启服务(需 `/restart confirm` 确认) |
80
81
 
81
82
  ### 快捷命令
82
83
 
@@ -3,10 +3,11 @@
3
3
  */
4
4
  import type { Config } from '../config.js';
5
5
  import type { SessionManager } from '../session/session-manager.js';
6
+ import { type RequestRestartFn } from '../platform/create-event-context.js';
6
7
  export interface ClawBotEventHandlerHandle {
7
8
  stop: () => void;
8
9
  runningTasks: Map<string, import('../shared/ai-task.js').TaskRunState>;
9
10
  getRunningTaskCount: () => number;
10
11
  handleEvent: (chatId: string, msgId: string, content: string, imagePaths?: string[]) => Promise<void>;
11
12
  }
12
- export declare function setupClawbotHandlers(config: Config, sessionManager: SessionManager): ClawBotEventHandlerHandle;
13
+ export declare function setupClawbotHandlers(config: Config, sessionManager: SessionManager, requestRestart: RequestRestartFn): ClawBotEventHandlerHandle;
@@ -9,7 +9,7 @@ import { createPlatformEventContext } from '../platform/create-event-context.js'
9
9
  import { createPlatformAIRequestHandler } from '../platform/handle-ai-request.js';
10
10
  import { handleTextFlow } from '../platform/handle-text-flow.js';
11
11
  const log = createLogger('ClawBotHandler');
12
- export function setupClawbotHandlers(config, sessionManager) {
12
+ export function setupClawbotHandlers(config, sessionManager, requestRestart) {
13
13
  const ctx = createPlatformEventContext({
14
14
  platform: 'clawbot',
15
15
  allowedUserIds: config.clawbotAllowedUserIds,
@@ -20,6 +20,7 @@ export function setupClawbotHandlers(config, sessionManager) {
20
20
  await sendTextReply(chatId, text);
21
21
  },
22
22
  },
23
+ requestRestart,
23
24
  });
24
25
  const stopTaskCleanup = startTaskCleanup(ctx.runningTasks);
25
26
  const platformSender = {
@@ -13,6 +13,8 @@ export interface CommandHandlerDeps {
13
13
  requestQueue: RequestQueue;
14
14
  sender: MessageSender;
15
15
  getRunningTasksSize: () => number;
16
+ /** 触发 worker 重启(写重启标志 + 优雅关闭)。供 /restart 调用。 */
17
+ requestRestart: (reason: string) => Promise<void>;
16
18
  }
17
19
  export type ClaudeRequestHandler = (userId: string, chatId: string, prompt: string, workDir: string, convId?: string, threadCtx?: ThreadContext, replyToMessageId?: string) => Promise<void>;
18
20
  export declare function normalizeSlashCommandForDispatch(text: string): string;
@@ -23,6 +25,11 @@ export declare class CommandHandler {
23
25
  dispatch(text: string, chatId: string, userId: string, platform: Platform, handleClaudeRequest: ClaudeRequestHandler,
24
26
  /** 若提供,本条消息的斜杠命令回复走此 sender(须与 handleTextFlow 的 sendTextReply 一致,如带 msgId)。 */
25
27
  senderOverride?: MessageSender): Promise<boolean>;
28
+ /**
29
+ * 重启 worker(IM 桥):写重启标志文件 + 优雅关闭,由 manager 监督重生。
30
+ * 需二次确认(/restart confirm)以避免误触中断所有用户的进行中任务。
31
+ */
32
+ private handleRestart;
26
33
  private handleAutopilotStatus;
27
34
  private handleHelp;
28
35
  private handlePlugins;
@@ -108,6 +108,11 @@ export class CommandHandler {
108
108
  return this.handleSwitchAi(chatId, t.slice(2).trim(), platform);
109
109
  if (t === '/autopilot')
110
110
  return this.handleAutopilotStatus(chatId, userId);
111
+ // 重启 worker(需 /restart confirm 二次确认,避免误触中断所有任务)
112
+ if (t === '/restart')
113
+ return this.handleRestart(chatId, userId, false);
114
+ if (t === '/restart confirm')
115
+ return this.handleRestart(chatId, userId, true);
111
116
  // 快捷命令 — 直接发送预设 prompt 给 AI
112
117
  if (t === '/git commit')
113
118
  return this.handleQuickCommand(chatId, userId, 'git commit -m "AI generated commit"', platform);
@@ -138,6 +143,34 @@ export class CommandHandler {
138
143
  }
139
144
  return runBody();
140
145
  }
146
+ /**
147
+ * 重启 worker(IM 桥):写重启标志文件 + 优雅关闭,由 manager 监督重生。
148
+ * 需二次确认(/restart confirm)以避免误触中断所有用户的进行中任务。
149
+ */
150
+ async handleRestart(chatId, userId, confirmed) {
151
+ if (!confirmed) {
152
+ const running = this.deps.getRunningTasksSize();
153
+ const taskLine = running > 0
154
+ ? `⚠️ 当前有 ${running} 个进行中的任务,重启将全部中断。`
155
+ : '当前无进行中的任务。';
156
+ await this.replySender().sendTextReply(chatId, [
157
+ '🔄 重启服务',
158
+ '',
159
+ taskLine,
160
+ '重启会短暂断开 IM 连接(数秒后自动恢复),Web 面板保持在线。',
161
+ '',
162
+ '确认请发送:/restart confirm',
163
+ ].join('\n'));
164
+ return true;
165
+ }
166
+ // 必须先回复用户,否则 requestRestart 触发的关闭流程会让进程退出、回复发不出去。
167
+ await this.replySender().sendTextReply(chatId, '🔄 正在重启服务,IM 连接将短暂断开,稍后自动恢复…');
168
+ // 不 await:关闭流程会结束当前进程;await 反而可能因连接断开而抛错。
169
+ this.deps.requestRestart(`/restart by user ${userId}`).catch((err) => {
170
+ log.error('requestRestart failed:', err);
171
+ });
172
+ return true;
173
+ }
141
174
  async handleAutopilotStatus(chatId, userId) {
142
175
  const ap = this.deps.config.autopilot;
143
176
  const lines = [
@@ -188,6 +221,7 @@ export class CommandHandler {
188
221
  '/cd <路径> - 切换工作目录',
189
222
  '/pwd - 当前工作目录',
190
223
  '/autopilot - 查看限流自动恢复状态',
224
+ '/restart - 重启服务(需 /restart confirm 确认)',
191
225
  '',
192
226
  '⚡ 快捷命令:',
193
227
  '/git commit - 提交代码',
@@ -1,10 +1,11 @@
1
1
  import type { DWClientDownStream } from 'dingtalk-stream';
2
2
  import { type Config } from '../config.js';
3
3
  import type { SessionManager } from '../session/session-manager.js';
4
+ import { type RequestRestartFn } from '../platform/create-event-context.js';
4
5
  export interface DingTalkEventHandlerHandle {
5
6
  stop: () => void;
6
7
  runningTasks: Map<string, import('../shared/ai-task.js').TaskRunState>;
7
8
  getRunningTaskCount: () => number;
8
9
  handleEvent: (data: DWClientDownStream) => Promise<void>;
9
10
  }
10
- export declare function setupDingTalkHandlers(config: Config, sessionManager: SessionManager): DingTalkEventHandlerHandle;
11
+ export declare function setupDingTalkHandlers(config: Config, sessionManager: SessionManager, requestRestart: RequestRestartFn): DingTalkEventHandlerHandle;
@@ -142,7 +142,7 @@ async function buildMediaPrompt(message, kind, robotCodeFallback) {
142
142
  metadata: sanitized,
143
143
  });
144
144
  }
145
- export function setupDingTalkHandlers(config, sessionManager) {
145
+ export function setupDingTalkHandlers(config, sessionManager, requestRestart) {
146
146
  configureDingTalkMessageSender({
147
147
  cardTemplateId: config.dingtalkCardTemplateId,
148
148
  robotCodeFallback: config.dingtalkClientId,
@@ -162,6 +162,7 @@ export function setupDingTalkHandlers(config, sessionManager) {
162
162
  config,
163
163
  sessionManager,
164
164
  sender: { sendTextReply, sendDirectorySelection },
165
+ requestRestart,
165
166
  });
166
167
  const { accessControl, requestQueue, runningTasks } = ctx;
167
168
  // DingTalk-specific sender callbacks for the factory
@@ -1,9 +1,10 @@
1
1
  import { type Config } from '../config.js';
2
2
  import type { SessionManager } from '../session/session-manager.js';
3
+ import { type RequestRestartFn } from '../platform/create-event-context.js';
3
4
  export interface FeishuEventHandlerHandle {
4
5
  stop: () => void;
5
6
  runningTasks: Map<string, import('../shared/ai-task.js').TaskRunState>;
6
7
  getRunningTaskCount: () => number;
7
8
  handleEvent: (data: unknown) => Promise<void | Record<string, unknown>>;
8
9
  }
9
- export declare function setupFeishuHandlers(config: Config, sessionManager: SessionManager): FeishuEventHandlerHandle;
10
+ export declare function setupFeishuHandlers(config: Config, sessionManager: SessionManager, requestRestart: RequestRestartFn): FeishuEventHandlerHandle;
@@ -26,7 +26,7 @@ async function downloadFeishuMessageResource(client, messageId, fileKey, type, o
26
26
  await response.writeFile(targetPath);
27
27
  return targetPath;
28
28
  }
29
- export function setupFeishuHandlers(config, sessionManager) {
29
+ export function setupFeishuHandlers(config, sessionManager, requestRestart) {
30
30
  // Create shared platform event context
31
31
  const ctx = createPlatformEventContext({
32
32
  platform: 'feishu',
@@ -34,6 +34,7 @@ export function setupFeishuHandlers(config, sessionManager) {
34
34
  config,
35
35
  sessionManager,
36
36
  sender: { sendTextReply },
37
+ requestRestart,
37
38
  });
38
39
  // Feishu-specific streaming state for error recovery
39
40
  let consecutiveStreamErrors = 0;
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { dirname, join } from "node:path";
5
5
  import { getConfiguredAiCommands, loadConfig, needsSetup, resolvePlatformAiCommand } from "./config.js";
6
6
  import { runInteractiveSetup, runClaudeApiSetup } from "./setup.js";
7
7
  import { runWebConfigFlow } from "./config-web.js";
8
+ import { markRestartRequest } from "./service-control.js";
8
9
  // 导出供 cli.ts 使用
9
10
  export { needsSetup, runInteractiveSetup };
10
11
  import { initTelegram, stopTelegram } from "./telegram/client.js";
@@ -45,17 +46,17 @@ const { version: APP_VERSION } = require("../package.json");
45
46
  const log = createLogger("Main");
46
47
  const PLATFORM_MODULES = {
47
48
  telegram: {
48
- init: (config, sessionManager) => new Promise((resolve, reject) => {
49
+ init: (config, sessionManager, requestRestart) => new Promise((resolve, reject) => {
49
50
  initTelegram(config, (bot) => {
50
- resolve(setupTelegramHandlers(bot, config, sessionManager));
51
+ resolve(setupTelegramHandlers(bot, config, sessionManager, requestRestart));
51
52
  }).catch(reject);
52
53
  }),
53
54
  stop: () => stopTelegram(),
54
55
  sendNotification: (chatId, msg) => sendTelegramTextReply(chatId, msg),
55
56
  },
56
57
  feishu: {
57
- init: async (config, sessionManager) => {
58
- const handle = setupFeishuHandlers(config, sessionManager);
58
+ init: async (config, sessionManager, requestRestart) => {
59
+ const handle = setupFeishuHandlers(config, sessionManager, requestRestart);
59
60
  await initFeishu(config, handle.handleEvent);
60
61
  return handle;
61
62
  },
@@ -64,8 +65,8 @@ const PLATFORM_MODULES = {
64
65
  formatError: (err) => formatFeishuInitError(err),
65
66
  },
66
67
  qq: {
67
- init: async (config, sessionManager) => {
68
- const handle = setupQQHandlers(config, sessionManager);
68
+ init: async (config, sessionManager, requestRestart) => {
69
+ const handle = setupQQHandlers(config, sessionManager, requestRestart);
69
70
  await initQQ(config, handle.handleEvent);
70
71
  return handle;
71
72
  },
@@ -73,8 +74,8 @@ const PLATFORM_MODULES = {
73
74
  sendNotification: (chatId, msg) => sendQQTextReply(chatId, msg),
74
75
  },
75
76
  wework: {
76
- init: async (config, sessionManager) => {
77
- const handle = setupWeWorkHandlers(config, sessionManager);
77
+ init: async (config, sessionManager, requestRestart) => {
78
+ const handle = setupWeWorkHandlers(config, sessionManager, requestRestart);
78
79
  await initWeWork(config, handle.handleEvent);
79
80
  return handle;
80
81
  },
@@ -82,8 +83,8 @@ const PLATFORM_MODULES = {
82
83
  sendNotification: (chatId, msg) => sendWeWorkTextReply(chatId, msg),
83
84
  },
84
85
  dingtalk: {
85
- init: async (config, sessionManager) => {
86
- const handle = setupDingTalkHandlers(config, sessionManager);
86
+ init: async (config, sessionManager, requestRestart) => {
87
+ const handle = setupDingTalkHandlers(config, sessionManager, requestRestart);
87
88
  await initDingTalk(config, handle.handleEvent);
88
89
  return handle;
89
90
  },
@@ -92,8 +93,8 @@ const PLATFORM_MODULES = {
92
93
  // No sendNotification — DingTalk doesn't support proactive messaging
93
94
  },
94
95
  workbuddy: {
95
- init: async (config, sessionManager) => {
96
- const handle = setupWorkBuddyHandlers(config, sessionManager);
96
+ init: async (config, sessionManager, requestRestart) => {
97
+ const handle = setupWorkBuddyHandlers(config, sessionManager, requestRestart);
97
98
  await initWorkBuddy(config, handle.handleEvent);
98
99
  return handle;
99
100
  },
@@ -101,12 +102,12 @@ const PLATFORM_MODULES = {
101
102
  sendNotification: (chatId, msg) => sendWorkBuddyTextReply(null, chatId, msg, randomUUID()),
102
103
  },
103
104
  clawbot: {
104
- init: async (config, sessionManager) => {
105
+ init: async (config, sessionManager, requestRestart) => {
105
106
  const pc = config.platforms.clawbot;
106
107
  if (pc?.apiUrl && pc?.apiToken) {
107
108
  initClawBotSender(pc.apiUrl, pc.apiToken);
108
109
  }
109
- const handle = setupClawbotHandlers(config, sessionManager);
110
+ const handle = setupClawbotHandlers(config, sessionManager, requestRestart);
110
111
  await initClawbot(config, handle.handleEvent);
111
112
  return handle;
112
113
  },
@@ -310,6 +311,13 @@ export async function main() {
310
311
  // Track active platform handles and successfully initialized platforms
311
312
  const activeHandles = new Map();
312
313
  const successfulPlatforms = [];
314
+ // restart 触发 worker 重启:写重启标志 + 走优雅关闭(shutdown 见下方,运行时调用时已定义)。
315
+ // init 时把引用传给各平台,实际调用发生在用户触发 /restart 之后。
316
+ const restart = async (reason) => {
317
+ markRestartRequest(reason);
318
+ log.info(`Restart requested: ${reason}`);
319
+ await shutdown();
320
+ };
313
321
  for (const platform of config.enabledPlatforms) {
314
322
  const mod = PLATFORM_MODULES[platform];
315
323
  if (!mod) {
@@ -317,7 +325,7 @@ export async function main() {
317
325
  continue;
318
326
  }
319
327
  try {
320
- const handle = await mod.init(config, sessionManager);
328
+ const handle = await mod.init(config, sessionManager, restart);
321
329
  activeHandles.set(platform, handle);
322
330
  successfulPlatforms.push(platform);
323
331
  }
package/dist/manager.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { startWebConfigServer } from "./config-web.js";
2
2
  import { removeManagerPid, removeManagerReady, writeManagerReady } from "./manager-control.js";
3
- import { startBackgroundService, stopBackgroundService, waitForBackgroundServiceReady } from "./service-control.js";
3
+ import { startBackgroundService, stopBackgroundService, waitForBackgroundServiceReady, hasRestartRequested, clearRestartRequest, } from "./service-control.js";
4
4
  import { createLogger } from "./logger.js";
5
5
  import { loadFileConfig } from "./config.js";
6
6
  const log = createLogger("Manager");
@@ -8,10 +8,51 @@ async function main() {
8
8
  const file = loadFileConfig();
9
9
  const workDir = file.tools?.claude?.workDir ?? process.cwd();
10
10
  const web = await startWebConfigServer({ mode: "start", cwd: workDir, persistent: true });
11
- startBackgroundService(workDir);
11
+ // manager 是否正在主动关闭。置位后 worker 的 exit 回调不再重生,避免 stop 触发误拉起。
12
+ const state = { managerStopping: false };
13
+ /**
14
+ * 监督 worker 子进程:退出时若 worker 在退出前置写了重启请求标志文件(/restart),
15
+ * 则重新 spawn 一个 worker 并继续监督;否则不自动拉起(保持原"崩溃不重启"行为,
16
+ * 但已为此 supervisor 预留扩展点)。
17
+ *
18
+ * 用具名函数递归挂监听,避免回调嵌套;每个 child 只挂一次。
19
+ */
20
+ function supervise(child) {
21
+ if (!child)
22
+ return;
23
+ child.on("exit", (code, signal) => {
24
+ // manager 主动 stop:stopBackgroundService 会让 worker 退出,此时不应重生。
25
+ if (state.managerStopping) {
26
+ log.info(`Worker exited during manager shutdown (code=${code}, signal=${signal}).`);
27
+ return;
28
+ }
29
+ if (hasRestartRequested()) {
30
+ clearRestartRequest();
31
+ log.info("Restart requested, respawning worker...");
32
+ try {
33
+ const next = startBackgroundService(workDir);
34
+ supervise(next.child);
35
+ waitForBackgroundServiceReady().catch((err) => {
36
+ log.error("Respawned worker failed to become ready:", err);
37
+ });
38
+ }
39
+ catch (err) {
40
+ log.error("Failed to respawn worker:", err);
41
+ }
42
+ }
43
+ else {
44
+ // 非 restart 的退出:不自动拉起(保持现状),仅记录。
45
+ log.warn(`Worker exited (code=${code}, signal=${signal}), not respawning.`);
46
+ }
47
+ });
48
+ }
49
+ // 首次启动 worker 并监督:worker 退出时,若存在重启请求则重生,否则视为崩溃/正常退出。
50
+ const initial = startBackgroundService(workDir);
51
+ supervise(initial.child);
12
52
  await waitForBackgroundServiceReady();
13
53
  writeManagerReady();
14
54
  const shutdown = async () => {
55
+ state.managerStopping = true;
15
56
  await web.close().catch((err) => log.warn("Failed to close web server:", err));
16
57
  await stopBackgroundService().catch((err) => log.warn("Failed to stop background service:", err));
17
58
  removeManagerReady();
@@ -12,12 +12,16 @@ import { AccessControl } from '../access/access-control.js';
12
12
  import { RequestQueue } from '../queue/request-queue.js';
13
13
  import { CommandHandler, type MessageSender } from '../commands/handler.js';
14
14
  import type { TaskRunState } from '../shared/ai-task.js';
15
+ /** 触发 worker 重启(写重启标志 + 优雅关闭)。由 worker 注入,命令处理器调用。 */
16
+ export type RequestRestartFn = (reason: string) => Promise<void>;
15
17
  export interface CreateEventContextDeps {
16
18
  platform: Platform;
17
19
  allowedUserIds: string[];
18
20
  config: Config;
19
21
  sessionManager: SessionManager;
20
22
  sender: MessageSender;
23
+ /** 触发 /restart 的回调。 */
24
+ requestRestart: RequestRestartFn;
21
25
  }
22
26
  export interface PlatformEventContext {
23
27
  accessControl: AccessControl;
@@ -10,7 +10,7 @@ import { AccessControl } from '../access/access-control.js';
10
10
  import { RequestQueue } from '../queue/request-queue.js';
11
11
  import { CommandHandler } from '../commands/handler.js';
12
12
  export function createPlatformEventContext(deps) {
13
- const { allowedUserIds, config, sessionManager, sender } = deps;
13
+ const { allowedUserIds, config, sessionManager, sender, requestRestart } = deps;
14
14
  const accessControl = new AccessControl(allowedUserIds);
15
15
  const requestQueue = new RequestQueue();
16
16
  const runningTasks = new Map();
@@ -20,6 +20,7 @@ export function createPlatformEventContext(deps) {
20
20
  requestQueue,
21
21
  sender,
22
22
  getRunningTasksSize: () => runningTasks.size,
23
+ requestRestart,
23
24
  });
24
25
  return {
25
26
  accessControl,
@@ -1,10 +1,11 @@
1
1
  import { type Config } from "../config.js";
2
2
  import type { SessionManager } from "../session/session-manager.js";
3
3
  import type { QQMessageEvent } from "./types.js";
4
+ import { type RequestRestartFn } from "../platform/create-event-context.js";
4
5
  export interface QQEventHandlerHandle {
5
6
  stop: () => void;
6
7
  runningTasks: Map<string, import('../shared/ai-task.js').TaskRunState>;
7
8
  getRunningTaskCount: () => number;
8
9
  handleEvent: (event: QQMessageEvent) => Promise<void>;
9
10
  }
10
- export declare function setupQQHandlers(config: Config, sessionManager: SessionManager): QQEventHandlerHandle;
11
+ export declare function setupQQHandlers(config: Config, sessionManager: SessionManager, requestRestart: RequestRestartFn): QQEventHandlerHandle;
@@ -110,7 +110,7 @@ async function buildAttachmentPrompt(event) {
110
110
  guidance: "If direct attachment fetch is not available, explain the limitation and ask the user for a text summary or a resend via Telegram/Feishu/WeWork.",
111
111
  });
112
112
  }
113
- export function setupQQHandlers(config, sessionManager) {
113
+ export function setupQQHandlers(config, sessionManager, requestRestart) {
114
114
  // Use shared platform event context factory
115
115
  const platformContext = createPlatformEventContext({
116
116
  platform: "qq",
@@ -118,6 +118,7 @@ export function setupQQHandlers(config, sessionManager) {
118
118
  config,
119
119
  sessionManager,
120
120
  sender: { sendTextReply, sendDirectorySelection },
121
+ requestRestart,
121
122
  });
122
123
  const { accessControl, requestQueue, runningTasks } = platformContext;
123
124
  const recentEventIds = new Map();
@@ -1,3 +1,10 @@
1
+ import { type ChildProcess } from "node:child_process";
2
+ /** 写入重启请求标志。reason 与时间戳一并记录,便于日志追溯。供 worker 的 /restart 调用。 */
3
+ export declare function markRestartRequest(reason: string): void;
4
+ /** 是否存在待处理的重启请求。供 manager 的 worker exit 监听判断是否重生。 */
5
+ export declare function hasRestartRequested(): boolean;
6
+ /** 清除重启请求标志。manager 重生 worker 前调用,避免重复触发。 */
7
+ export declare function clearRestartRequest(): void;
1
8
  export declare function getPid(): number | null;
2
9
  export declare function writePid(pid: number): void;
3
10
  export declare function removePid(): void;
@@ -6,8 +13,15 @@ export declare function getServiceStatus(): {
6
13
  running: boolean;
7
14
  pid: number | null;
8
15
  };
16
+ /**
17
+ * Spawn the background service (worker).
18
+ * Returns the child handle so the manager can supervise it.
19
+ * If a worker is already running, returns its pid with a null child (manager supervision
20
+ * already has a child from the initial spawn; web UI callers only need the pid).
21
+ */
9
22
  export declare function startBackgroundService(cwd: string): {
10
23
  pid: number;
24
+ child: ChildProcess | null;
11
25
  };
12
26
  export declare function waitForBackgroundServiceReady(timeoutMs?: number, pollIntervalMs?: number): Promise<void>;
13
27
  export declare function stopBackgroundService(): Promise<{
@@ -7,6 +7,8 @@ import { resolveNodeExecutable } from "./node-exec.js";
7
7
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
8
  const PID_FILE = join(APP_HOME, "open-im-worker.pid");
9
9
  const PORT_FILE = join(APP_HOME, "open-im.port");
10
+ /** 重启请求标志文件:worker 优雅退出前置于此文件,manager 的 exit 监听据此判断是否重生 worker。 */
11
+ const RESTART_REQUESTED_FILE = join(APP_HOME, "open-im.restart-requested");
10
12
  function removePortFile() {
11
13
  try {
12
14
  if (existsSync(PORT_FILE))
@@ -16,6 +18,33 @@ function removePortFile() {
16
18
  /* ignore */
17
19
  }
18
20
  }
21
+ // ─── 重启请求标志文件(worker ↔ manager 协作) ───
22
+ /** 写入重启请求标志。reason 与时间戳一并记录,便于日志追溯。供 worker 的 /restart 调用。 */
23
+ export function markRestartRequest(reason) {
24
+ try {
25
+ const dir = dirname(RESTART_REQUESTED_FILE);
26
+ if (!existsSync(dir))
27
+ return; // APP_HOME 通常已存在;防御性
28
+ writeFileSync(RESTART_REQUESTED_FILE, JSON.stringify({ reason, at: new Date().toISOString() }, null, 2), "utf-8");
29
+ }
30
+ catch {
31
+ /* 写失败则 manager 不会重生,退化为普通关闭 */
32
+ }
33
+ }
34
+ /** 是否存在待处理的重启请求。供 manager 的 worker exit 监听判断是否重生。 */
35
+ export function hasRestartRequested() {
36
+ return existsSync(RESTART_REQUESTED_FILE);
37
+ }
38
+ /** 清除重启请求标志。manager 重生 worker 前调用,避免重复触发。 */
39
+ export function clearRestartRequest() {
40
+ try {
41
+ if (existsSync(RESTART_REQUESTED_FILE))
42
+ unlinkSync(RESTART_REQUESTED_FILE);
43
+ }
44
+ catch {
45
+ /* ignore */
46
+ }
47
+ }
19
48
  function getServiceEntry() {
20
49
  const node = resolveNodeExecutable();
21
50
  const extension = extname(fileURLToPath(import.meta.url));
@@ -80,10 +109,16 @@ export function getServiceStatus() {
80
109
  }
81
110
  return { running: true, pid };
82
111
  }
112
+ /**
113
+ * Spawn the background service (worker).
114
+ * Returns the child handle so the manager can supervise it.
115
+ * If a worker is already running, returns its pid with a null child (manager supervision
116
+ * already has a child from the initial spawn; web UI callers only need the pid).
117
+ */
83
118
  export function startBackgroundService(cwd) {
84
119
  const current = getServiceStatus();
85
120
  if (current.running && current.pid) {
86
- return { pid: current.pid };
121
+ return { pid: current.pid, child: null };
87
122
  }
88
123
  removePid();
89
124
  removePortFile();
@@ -104,7 +139,7 @@ export function startBackgroundService(cwd) {
104
139
  throw new Error("Failed to start background service.");
105
140
  }
106
141
  writePid(child.pid);
107
- return { pid: child.pid };
142
+ return { pid: child.pid, child };
108
143
  }
109
144
  export async function waitForBackgroundServiceReady(timeoutMs = SERVICE_READY_TIMEOUT_MS, pollIntervalMs = 100) {
110
145
  const startedAt = Date.now();
@@ -180,7 +180,7 @@ function buildCompletionNote(result, sessionManager, ctx) {
180
180
  parts.push(ctxWarning);
181
181
  return parts.join(' | ');
182
182
  }
183
- function buildRunOptions(config, sessionManager, ctx, aiCommand, toolAdapter) {
183
+ function buildRunOptions(config, sessionManager, ctx, aiCommand) {
184
184
  // 权限 hook 尚未接入 IM,暂时全局跳过
185
185
  const defaultSkipPermissions = true;
186
186
  return {
@@ -474,7 +474,7 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
474
474
  cleanup();
475
475
  resolve();
476
476
  },
477
- }, buildRunOptions(config, sessionManager, ctx, aiCommand, toolAdapter));
477
+ }, buildRunOptions(config, sessionManager, ctx, aiCommand));
478
478
  return activeHandle;
479
479
  };
480
480
  taskState = {
@@ -51,7 +51,7 @@ async function ping(target, workDir) {
51
51
  try {
52
52
  handle.abort();
53
53
  }
54
- catch (err) {
54
+ catch {
55
55
  // 中止失败,忽略错误
56
56
  }
57
57
  settle(false, `timeout after ${PING_TIMEOUT_MS}ms`);
@@ -3,15 +3,11 @@ import { createLogger } from "../logger.js";
3
3
  import { isFatalReconnectError, jitteredDelay } from "../shared/reconnect.js";
4
4
  const log = createLogger("Telegram");
5
5
  let bot;
6
- let botUsername;
7
6
  export function getBot() {
8
7
  if (!bot)
9
8
  throw new Error("Telegram bot not initialized");
10
9
  return bot;
11
10
  }
12
- function getBotUsername() {
13
- return botUsername;
14
- }
15
11
  export async function initTelegram(config, setupHandlers) {
16
12
  const token = config.telegramBotToken ?? "";
17
13
  if (!token) {
@@ -19,8 +15,7 @@ export async function initTelegram(config, setupHandlers) {
19
15
  }
20
16
  bot = new Telegraf(token);
21
17
  setupHandlers(bot);
22
- const me = (await bot.telegram.getMe());
23
- botUsername = me.username;
18
+ await bot.telegram.getMe();
24
19
  const launchWithRetry = async (attempt = 1) => {
25
20
  try {
26
21
  await bot.launch();
@@ -1,9 +1,10 @@
1
1
  import type { Telegraf } from "telegraf";
2
2
  import { type Config } from "../config.js";
3
3
  import type { SessionManager } from "../session/session-manager.js";
4
+ import { type RequestRestartFn } from "../platform/create-event-context.js";
4
5
  export interface TelegramEventHandlerHandle {
5
6
  stop: () => void;
6
7
  runningTasks: Map<string, import('../shared/ai-task.js').TaskRunState>;
7
8
  getRunningTaskCount: () => number;
8
9
  }
9
- export declare function setupTelegramHandlers(bot: Telegraf, config: Config, sessionManager: SessionManager): TelegramEventHandlerHandle;
10
+ export declare function setupTelegramHandlers(bot: Telegraf, config: Config, sessionManager: SessionManager, requestRestart: RequestRestartFn): TelegramEventHandlerHandle;
@@ -59,7 +59,7 @@ async function downloadTelegramFile(bot, fileId, basenameHint, fallbackExtension
59
59
  fallbackExtension,
60
60
  });
61
61
  }
62
- export function setupTelegramHandlers(bot, config, sessionManager) {
62
+ export function setupTelegramHandlers(bot, config, sessionManager, requestRestart) {
63
63
  // Create shared platform event context
64
64
  const platformCtx = createPlatformEventContext({
65
65
  platform: 'telegram',
@@ -67,6 +67,7 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
67
67
  config,
68
68
  sessionManager,
69
69
  sender: { sendTextReply, sendDirectorySelection },
70
+ requestRestart,
70
71
  });
71
72
  const { accessControl, requestQueue, runningTasks } = platformCtx;
72
73
  // Telegram-specific sender callbacks for the factory
@@ -4,6 +4,7 @@
4
4
  import type { Config } from '../config.js';
5
5
  import type { SessionManager } from '../session/session-manager.js';
6
6
  import type { WeWorkCallbackMessage } from './types.js';
7
+ import { type RequestRestartFn } from '../platform/create-event-context.js';
7
8
  type MediaKind = 'image' | 'file' | 'voice' | 'video';
8
9
  export interface WeWorkEventHandlerHandle {
9
10
  stop: () => void;
@@ -12,5 +13,5 @@ export interface WeWorkEventHandlerHandle {
12
13
  handleEvent: (data: WeWorkCallbackMessage) => Promise<void>;
13
14
  }
14
15
  export declare function buildMediaPrompt(data: WeWorkCallbackMessage, kind: MediaKind): Promise<string | null>;
15
- export declare function setupWeWorkHandlers(config: Config, sessionManager: SessionManager): WeWorkEventHandlerHandle;
16
+ export declare function setupWeWorkHandlers(config: Config, sessionManager: SessionManager, requestRestart: RequestRestartFn): WeWorkEventHandlerHandle;
16
17
  export {};
@@ -156,7 +156,7 @@ export async function buildMediaPrompt(data, kind) {
156
156
  guidance: 'If the media content is not directly accessible, explain that clearly and ask the user for a text summary, transcript, or a resend via a channel with native media support.',
157
157
  });
158
158
  }
159
- export function setupWeWorkHandlers(config, sessionManager) {
159
+ export function setupWeWorkHandlers(config, sessionManager, requestRestart) {
160
160
  // Mutable ref that captures the req_id of the message currently being handled.
161
161
  // WeWork requires req_id to reply; CommandHandler doesn't carry it, so we inject
162
162
  // it via a closure. WeWork delivers messages sequentially over WebSocket, so
@@ -168,6 +168,7 @@ export function setupWeWorkHandlers(config, sessionManager) {
168
168
  allowedUserIds: config.weworkAllowedUserIds,
169
169
  config,
170
170
  sessionManager,
171
+ requestRestart,
171
172
  sender: {
172
173
  sendTextReply: (chatId, text) => sendTextReply(chatId, text, senderCtx.reqId),
173
174
  sendDirectorySelection: (chatId, currentDir, userId) => sendDirectorySelection(chatId, currentDir, userId, senderCtx.reqId),
@@ -40,9 +40,6 @@ const reconnectManager = new ReconnectManager({
40
40
  }
41
41
  },
42
42
  });
43
- function getChannelState() {
44
- return stateManager.current;
45
- }
46
43
  export async function initWorkBuddy(config, eventHandler, onStateChange) {
47
44
  const pc = config.platforms?.workbuddy;
48
45
  if (!pc?.enabled) {
@@ -242,9 +239,6 @@ async function connect() {
242
239
  export function getCentrifugeClient() {
243
240
  return centrifugeClient;
244
241
  }
245
- function getOAuth() {
246
- return oauthClient;
247
- }
248
242
  /**
249
243
  * 单飞刷新 WorkBuddy access token:并发 401 只触发一次刷新,复用同一 Promise。
250
244
  * 刷新成功后 oauthClient.accessToken 原地更新,所有 HTTP 调用(含心跳)自动用新 token。
@@ -3,10 +3,11 @@
3
3
  */
4
4
  import type { Config } from '../config.js';
5
5
  import type { SessionManager } from '../session/session-manager.js';
6
+ import { type RequestRestartFn } from '../platform/create-event-context.js';
6
7
  export interface WorkBuddyEventHandlerHandle {
7
8
  stop: () => void;
8
9
  runningTasks: Map<string, import('../shared/ai-task.js').TaskRunState>;
9
10
  getRunningTaskCount: () => number;
10
11
  handleEvent: (chatId: string, msgId: string, content: string) => Promise<void>;
11
12
  }
12
- export declare function setupWorkBuddyHandlers(config: Config, sessionManager: SessionManager): WorkBuddyEventHandlerHandle;
13
+ export declare function setupWorkBuddyHandlers(config: Config, sessionManager: SessionManager, requestRestart: RequestRestartFn): WorkBuddyEventHandlerHandle;
@@ -9,7 +9,7 @@ import { createPlatformEventContext } from '../platform/create-event-context.js'
9
9
  import { createPlatformAIRequestHandler } from '../platform/handle-ai-request.js';
10
10
  import { handleTextFlow } from '../platform/handle-text-flow.js';
11
11
  const log = createLogger('WorkBuddyHandler');
12
- export function setupWorkBuddyHandlers(config, sessionManager) {
12
+ export function setupWorkBuddyHandlers(config, sessionManager, requestRestart) {
13
13
  // Create shared platform event context
14
14
  const ctx = createPlatformEventContext({
15
15
  platform: 'workbuddy',
@@ -21,6 +21,7 @@ export function setupWorkBuddyHandlers(config, sessionManager) {
21
21
  await sendTextReply(null, chatId, text, '');
22
22
  },
23
23
  },
24
+ requestRestart,
24
25
  });
25
26
  // Start task cleanup
26
27
  const stopTaskCleanup = startTaskCleanup(ctx.runningTasks);
@@ -41,14 +41,3 @@ export async function sendErrorReply(_client, chatId, error, msgId) {
41
41
  stop_reason: 'error',
42
42
  });
43
43
  }
44
- /**
45
- * Send streaming chunk to WeChat KF
46
- */
47
- function sendStreamingChunk(_client, chatId, text, msgId) {
48
- const client = _client ?? getCentrifugeClient();
49
- if (!client) {
50
- log.warn('WorkBuddy client not available, cannot send chunk');
51
- return;
52
- }
53
- client.sendMessageChunk(chatId, msgId, { type: 'text', text: toReplyPlainText(text) });
54
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.11.12",
3
+ "version": "1.11.13-beta.1",
4
4
  "description": "Your AI coding assistant, in every chat app. Multi-platform IM bridge for Claude Code, Codex, and CodeBuddy.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",