@wu529778790/open-im 1.11.11 → 1.11.13-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/clawbot/event-handler.d.ts +2 -1
- package/dist/clawbot/event-handler.js +2 -1
- package/dist/commands/handler.d.ts +7 -0
- package/dist/commands/handler.js +34 -0
- package/dist/dingtalk/event-handler.d.ts +2 -1
- package/dist/dingtalk/event-handler.js +2 -1
- package/dist/feishu/event-handler.d.ts +2 -1
- package/dist/feishu/event-handler.js +2 -1
- package/dist/index.js +23 -15
- package/dist/manager.js +43 -2
- package/dist/platform/create-event-context.d.ts +4 -0
- package/dist/platform/create-event-context.js +2 -1
- package/dist/qq/event-handler.d.ts +2 -1
- package/dist/qq/event-handler.js +2 -1
- package/dist/service-control.d.ts +14 -0
- package/dist/service-control.js +37 -2
- package/dist/telegram/event-handler.d.ts +2 -1
- package/dist/telegram/event-handler.js +2 -1
- package/dist/wework/event-handler.d.ts +2 -1
- package/dist/wework/event-handler.js +2 -1
- package/dist/workbuddy/event-handler.d.ts +2 -1
- package/dist/workbuddy/event-handler.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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;
|
package/dist/commands/handler.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/qq/event-handler.js
CHANGED
|
@@ -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<{
|
package/dist/service-control.js
CHANGED
|
@@ -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();
|
|
@@ -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),
|
|
@@ -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);
|
package/package.json
CHANGED