@wu529778790/open-im 1.10.2-beta.0 → 1.10.2-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/codebuddy/cli-runner.js +1 -1
- package/dist/codex/cli-runner.js +4 -2
- package/dist/commands/handler.js +4 -2
- package/dist/config-web-page-script.js +16 -6
- package/dist/config-web-page-template.js +1 -2
- package/dist/config-web.js +1 -61
- package/dist/dingtalk/event-handler.js +25 -25
- package/dist/feishu/client.js +1 -1
- package/dist/feishu/event-handler.js +14 -22
- package/dist/feishu/message-sender.js +1 -1
- package/dist/index.js +1 -1
- package/dist/platform/handle-ai-request.js +14 -30
- package/dist/platform/handle-text-flow.js +3 -16
- package/dist/qq/event-handler.js +2 -6
- package/dist/queue/request-queue.d.ts +0 -3
- package/dist/queue/request-queue.js +8 -25
- package/dist/queue/request-queue.test.js +0 -32
- package/dist/shared/active-chats.js +8 -5
- package/dist/shared/ai-task.js +6 -4
- package/dist/shared/utils.d.ts +13 -0
- package/dist/shared/utils.js +16 -0
- package/dist/telegram/event-handler.js +55 -120
- package/dist/wework/event-handler.js +2 -6
- package/dist/workbuddy/client.js +8 -1
- package/dist/workbuddy/event-handler.js +39 -135
- package/package.json +1 -1
|
@@ -294,7 +294,7 @@ export function runCodeBuddy(cliPath, prompt, sessionId, workDir, callbacks, opt
|
|
|
294
294
|
handlePayload(JSON.parse(payload));
|
|
295
295
|
}
|
|
296
296
|
catch {
|
|
297
|
-
|
|
297
|
+
log.debug(`Ignoring trailing partial CodeBuddy payload on close`);
|
|
298
298
|
}
|
|
299
299
|
}
|
|
300
300
|
if (completed)
|
package/dist/codex/cli-runner.js
CHANGED
|
@@ -117,7 +117,8 @@ function extractCodexJsFromCmdShim(cmdPath) {
|
|
|
117
117
|
const relativeJsPath = match[1].replace(/\\/g, '/');
|
|
118
118
|
return join(dirname(cmdPath), relativeJsPath);
|
|
119
119
|
}
|
|
120
|
-
catch {
|
|
120
|
+
catch (err) {
|
|
121
|
+
log.debug(`Failed to extract Codex JS path from cmd shim ${cmdPath}:`, err);
|
|
121
122
|
return null;
|
|
122
123
|
}
|
|
123
124
|
}
|
|
@@ -152,7 +153,8 @@ function resolveWindowsCodexLaunch(cliPath, args) {
|
|
|
152
153
|
windowsCodexLaunchCache.set(cliPath, resolved);
|
|
153
154
|
return { command: resolved.command, args: [...resolved.args, ...args] };
|
|
154
155
|
}
|
|
155
|
-
catch {
|
|
156
|
+
catch (err) {
|
|
157
|
+
log.debug(`Failed to resolve Windows Codex launch for ${cliPath}:`, err);
|
|
156
158
|
windowsCodexLaunchCache.set(cliPath, null);
|
|
157
159
|
return null;
|
|
158
160
|
}
|
package/dist/commands/handler.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { resolvePlatformAiCommand } from '../config.js';
|
|
2
2
|
import { escapePathForMarkdown } from '../shared/utils.js';
|
|
3
3
|
import { TERMINAL_ONLY_COMMANDS } from '../constants.js';
|
|
4
|
+
import { createLogger } from '../logger.js';
|
|
5
|
+
const log = createLogger('Commands');
|
|
4
6
|
import { execFile } from 'node:child_process';
|
|
5
7
|
import { readdirSync } from 'node:fs';
|
|
6
8
|
import { dirname, join } from 'node:path';
|
|
@@ -136,8 +138,8 @@ export function listDirectories(basePath) {
|
|
|
136
138
|
.sort((a, b) => a.name.localeCompare(b.name)); // 按名称排序
|
|
137
139
|
dirs.push(...subDirs);
|
|
138
140
|
}
|
|
139
|
-
catch {
|
|
140
|
-
|
|
141
|
+
catch (err) {
|
|
142
|
+
log.debug('Failed to list subdirectories:', err);
|
|
141
143
|
}
|
|
142
144
|
return dirs;
|
|
143
145
|
}
|
|
@@ -108,8 +108,7 @@ export const PAGE_SCRIPT = String.raw ` const platformDefinitions = [
|
|
|
108
108
|
[
|
|
109
109
|
"headerValidateButton",
|
|
110
110
|
"headerSaveButton",
|
|
111
|
-
"
|
|
112
|
-
"headerStopButton",
|
|
111
|
+
"headerToggleServiceButton",
|
|
113
112
|
"langButton",
|
|
114
113
|
].forEach((id) => {
|
|
115
114
|
const node = el(id);
|
|
@@ -233,8 +232,7 @@ export const PAGE_SCRIPT = String.raw ` const platformDefinitions = [
|
|
|
233
232
|
buttons: [
|
|
234
233
|
{ id: "headerValidateButton", key: "validate" },
|
|
235
234
|
{ id: "headerSaveButton", key: "save" },
|
|
236
|
-
{ id: "
|
|
237
|
-
{ id: "headerStopButton", key: "stop" },
|
|
235
|
+
{ id: "headerToggleServiceButton", key: "start" },
|
|
238
236
|
],
|
|
239
237
|
testButtons: [
|
|
240
238
|
{ prefix: "test-", key: "test" },
|
|
@@ -623,6 +621,12 @@ export const PAGE_SCRIPT = String.raw ` const platformDefinitions = [
|
|
|
623
621
|
serviceState.textContent = serviceStateText;
|
|
624
622
|
serviceState.className = "badge " + (data.running ? "badge-success" : "badge-default");
|
|
625
623
|
}
|
|
624
|
+
// Update toggle button text and style
|
|
625
|
+
const toggleBtn = el("headerToggleServiceButton");
|
|
626
|
+
if (toggleBtn) {
|
|
627
|
+
toggleBtn.textContent = data.running ? t("stop") : t("start");
|
|
628
|
+
toggleBtn.className = "btn btn-sm " + (data.running ? "btn-danger" : "btn-primary");
|
|
629
|
+
}
|
|
626
630
|
return data;
|
|
627
631
|
}
|
|
628
632
|
|
|
@@ -750,8 +754,14 @@ export const PAGE_SCRIPT = String.raw ` const platformDefinitions = [
|
|
|
750
754
|
};
|
|
751
755
|
el("headerValidateButton").onclick = validate;
|
|
752
756
|
el("headerSaveButton").onclick = onSaveClick;
|
|
753
|
-
el("
|
|
754
|
-
|
|
757
|
+
el("headerToggleServiceButton").onclick = async () => {
|
|
758
|
+
const status = await refreshStatus();
|
|
759
|
+
if (status.running) {
|
|
760
|
+
await stopService();
|
|
761
|
+
} else {
|
|
762
|
+
await onStartClick();
|
|
763
|
+
}
|
|
764
|
+
};
|
|
755
765
|
|
|
756
766
|
// Platform test buttons
|
|
757
767
|
platformKeys.forEach((platform) => {
|
|
@@ -866,8 +866,7 @@ export const PAGE_HTML_PREFIX = String.raw `<!doctype html>
|
|
|
866
866
|
<div class="main-header-toolbar" id="headerToolbar" role="toolbar" aria-label="Bridge controls">
|
|
867
867
|
<button type="button" id="headerValidateButton" class="btn btn-warning btn-sm">Validate</button>
|
|
868
868
|
<button type="button" id="headerSaveButton" class="btn btn-secondary btn-sm">Save config</button>
|
|
869
|
-
<button type="button" id="
|
|
870
|
-
<button type="button" id="headerStopButton" class="btn btn-danger btn-sm">Stop bridge</button>
|
|
869
|
+
<button type="button" id="headerToggleServiceButton" class="btn btn-primary btn-sm">Start bridge</button>
|
|
871
870
|
</div>
|
|
872
871
|
</header>
|
|
873
872
|
|
package/dist/config-web.js
CHANGED
|
@@ -894,67 +894,7 @@ export async function startWebConfigServer(options) {
|
|
|
894
894
|
}
|
|
895
895
|
if (request.method === "GET" && requestUrl.pathname === "/api/health") {
|
|
896
896
|
const file = loadFileConfig();
|
|
897
|
-
const
|
|
898
|
-
const fileFeishu = file.platforms?.feishu;
|
|
899
|
-
const fileQQ = file.platforms?.qq;
|
|
900
|
-
const fileWework = file.platforms?.wework;
|
|
901
|
-
const fileDingtalk = file.platforms?.dingtalk;
|
|
902
|
-
const fileWorkbuddy = file.platforms?.workbuddy;
|
|
903
|
-
const telegramBotToken = process.env.TELEGRAM_BOT_TOKEN ?? fileTelegram?.botToken ?? file.telegramBotToken;
|
|
904
|
-
const feishuAppId = process.env.FEISHU_APP_ID ?? fileFeishu?.appId ?? file.feishuAppId;
|
|
905
|
-
const feishuAppSecret = process.env.FEISHU_APP_SECRET ?? fileFeishu?.appSecret ?? file.feishuAppSecret;
|
|
906
|
-
const qqAppId = process.env.QQ_BOT_APPID ?? process.env.QQ_APP_ID ?? fileQQ?.appId;
|
|
907
|
-
const qqSecret = process.env.QQ_BOT_SECRET ?? process.env.QQ_SECRET ?? fileQQ?.secret;
|
|
908
|
-
const weworkCorpId = process.env.WEWORK_CORP_ID ?? fileWework?.corpId;
|
|
909
|
-
const weworkSecret = process.env.WEWORK_SECRET ?? fileWework?.secret;
|
|
910
|
-
const dingtalkClientId = process.env.DINGTALK_CLIENT_ID ?? fileDingtalk?.clientId;
|
|
911
|
-
const dingtalkClientSecret = process.env.DINGTALK_CLIENT_SECRET ?? fileDingtalk?.clientSecret;
|
|
912
|
-
const workbuddyAccessToken = fileWorkbuddy?.accessToken;
|
|
913
|
-
const workbuddyRefreshToken = fileWorkbuddy?.refreshToken;
|
|
914
|
-
const workbuddyUserId = fileWorkbuddy?.userId;
|
|
915
|
-
const platforms = {};
|
|
916
|
-
// 检查 Telegram
|
|
917
|
-
platforms.telegram = {
|
|
918
|
-
configured: !!telegramBotToken,
|
|
919
|
-
enabled: !!telegramBotToken && fileTelegram?.enabled !== false,
|
|
920
|
-
healthy: !!telegramBotToken,
|
|
921
|
-
message: telegramBotToken ? "Token configured" : "Token not configured"
|
|
922
|
-
};
|
|
923
|
-
// 检查 Feishu
|
|
924
|
-
platforms.feishu = {
|
|
925
|
-
configured: !!(feishuAppId && feishuAppSecret),
|
|
926
|
-
enabled: !!(feishuAppId && feishuAppSecret) && fileFeishu?.enabled !== false,
|
|
927
|
-
healthy: !!(feishuAppId && feishuAppSecret),
|
|
928
|
-
message: (feishuAppId && feishuAppSecret) ? "App ID and Secret configured" : "Missing credentials"
|
|
929
|
-
};
|
|
930
|
-
// 检查 QQ
|
|
931
|
-
platforms.qq = {
|
|
932
|
-
configured: !!(qqAppId && qqSecret),
|
|
933
|
-
enabled: !!(qqAppId && qqSecret) && fileQQ?.enabled !== false,
|
|
934
|
-
healthy: !!(qqAppId && qqSecret),
|
|
935
|
-
message: (qqAppId && qqSecret) ? "App ID and Secret configured" : "Missing credentials"
|
|
936
|
-
};
|
|
937
|
-
// 检查 WeWork
|
|
938
|
-
platforms.wework = {
|
|
939
|
-
configured: !!(weworkCorpId && weworkSecret),
|
|
940
|
-
enabled: !!(weworkCorpId && weworkSecret) && fileWework?.enabled !== false,
|
|
941
|
-
healthy: !!(weworkCorpId && weworkSecret),
|
|
942
|
-
message: (weworkCorpId && weworkSecret) ? "Corp ID and Secret configured" : "Missing credentials"
|
|
943
|
-
};
|
|
944
|
-
// 检查 DingTalk
|
|
945
|
-
platforms.dingtalk = {
|
|
946
|
-
configured: !!(dingtalkClientId && dingtalkClientSecret),
|
|
947
|
-
enabled: !!(dingtalkClientId && dingtalkClientSecret) && fileDingtalk?.enabled !== false,
|
|
948
|
-
healthy: !!(dingtalkClientId && dingtalkClientSecret),
|
|
949
|
-
message: (dingtalkClientId && dingtalkClientSecret) ? "Client ID and Secret configured" : "Missing credentials"
|
|
950
|
-
};
|
|
951
|
-
// 检查 WorkBuddy
|
|
952
|
-
platforms.workbuddy = {
|
|
953
|
-
configured: !!(workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId),
|
|
954
|
-
enabled: !!(workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId) && fileWorkbuddy?.enabled !== false,
|
|
955
|
-
healthy: !!(workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId),
|
|
956
|
-
message: (workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId) ? "OAuth credentials configured" : "Missing credentials"
|
|
957
|
-
};
|
|
897
|
+
const platforms = getHealthPlatformSnapshot(file);
|
|
958
898
|
json(response, 200, { platforms, serviceStatus: getServiceStatus() });
|
|
959
899
|
return;
|
|
960
900
|
}
|
|
@@ -2,6 +2,8 @@ import { configureDingTalkMessageSender, sendThinkingMessage, updateMessage, sen
|
|
|
2
2
|
import { ackMessage, downloadRobotMessageFile, registerSessionWebhook } from './client.js';
|
|
3
3
|
import { createPlatformEventContext } from '../platform/create-event-context.js';
|
|
4
4
|
import { createPlatformAIRequestHandler } from '../platform/handle-ai-request.js';
|
|
5
|
+
import { handleTextFlow } from '../platform/handle-text-flow.js';
|
|
6
|
+
import { handleEnqueueResult } from '../shared/utils.js';
|
|
5
7
|
import { setActiveChatId, setDingTalkActiveTarget } from '../shared/active-chats.js';
|
|
6
8
|
import { setChatUser } from '../shared/chat-user-map.js';
|
|
7
9
|
import { createLogger } from '../logger.js';
|
|
@@ -246,12 +248,7 @@ export function setupDingTalkHandlers(config, sessionManager) {
|
|
|
246
248
|
return;
|
|
247
249
|
}
|
|
248
250
|
const enqueueResult = await enqueuePrompt(userId, chatId, prompt, dingtalkTarget);
|
|
249
|
-
|
|
250
|
-
await sendTextReply(chatId, '请求队列已满,请稍后再试。');
|
|
251
|
-
}
|
|
252
|
-
else if (enqueueResult === 'queued') {
|
|
253
|
-
await sendTextReply(chatId, '您的请求已排队等待。');
|
|
254
|
-
}
|
|
251
|
+
await handleEnqueueResult(enqueueResult, (text) => sendTextReply(chatId, text));
|
|
255
252
|
ackMessage(callbackId, { queued: enqueueResult, kind });
|
|
256
253
|
return;
|
|
257
254
|
}
|
|
@@ -259,26 +256,29 @@ export function setupDingTalkHandlers(config, sessionManager) {
|
|
|
259
256
|
ackMessage(callbackId, { ignored: 'empty text' });
|
|
260
257
|
return;
|
|
261
258
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
259
|
+
// Use shared text flow with customEnqueue to carry dingtalkTarget
|
|
260
|
+
const processed = await handleTextFlow({
|
|
261
|
+
platform: 'dingtalk',
|
|
262
|
+
userId,
|
|
263
|
+
chatId,
|
|
264
|
+
text,
|
|
265
|
+
ctx,
|
|
266
|
+
handleAIRequest,
|
|
267
|
+
sendTextReply,
|
|
268
|
+
workDir: sessionManager.getWorkDir(userId),
|
|
269
|
+
convId: sessionManager.getConvId(userId),
|
|
270
|
+
queueFullMessage: '请求队列已满,请稍后再试。',
|
|
271
|
+
queuedMessage: '您的请求已排队等待。',
|
|
272
|
+
customEnqueue: (prompt) => {
|
|
273
|
+
return enqueuePrompt(userId, chatId, prompt, dingtalkTarget);
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
if (!processed) {
|
|
277
|
+
// Access denied
|
|
278
|
+
ackMessage(callbackId, { denied: true });
|
|
279
|
+
return;
|
|
280
280
|
}
|
|
281
|
-
ackMessage(callbackId, {
|
|
281
|
+
ackMessage(callbackId, { handled: true });
|
|
282
282
|
}
|
|
283
283
|
return {
|
|
284
284
|
stop: () => { },
|
package/dist/feishu/client.js
CHANGED
|
@@ -103,7 +103,7 @@ export async function initFeishu(config, eventHandler) {
|
|
|
103
103
|
app_secret: config.feishuAppSecret,
|
|
104
104
|
},
|
|
105
105
|
});
|
|
106
|
-
if (tokenResp.code !== 0
|
|
106
|
+
if (tokenResp.code !== 0) {
|
|
107
107
|
throw new Error(`Feishu credentials invalid: ${tokenResp.msg} (code: ${tokenResp.code})`);
|
|
108
108
|
}
|
|
109
109
|
log.info('Feishu credentials validated successfully');
|
|
@@ -11,6 +11,7 @@ import { buildProgressNote } from '../shared/message-note.js';
|
|
|
11
11
|
import { createPlatformEventContext } from '../platform/create-event-context.js';
|
|
12
12
|
import { createPlatformAIRequestHandler } from '../platform/handle-ai-request.js';
|
|
13
13
|
import { handleTextFlow } from '../platform/handle-text-flow.js';
|
|
14
|
+
import { handleEnqueueResult } from '../shared/utils.js';
|
|
14
15
|
import { isPermissionError, handlePermissionError } from './permission.js';
|
|
15
16
|
const log = createLogger('FeishuHandler');
|
|
16
17
|
async function downloadFeishuMessageResource(client, messageId, fileKey, type, options) {
|
|
@@ -188,13 +189,12 @@ export function setupFeishuHandlers(config, sessionManager) {
|
|
|
188
189
|
const messageId = message.message_id ?? '';
|
|
189
190
|
const msgType = message.message_type;
|
|
190
191
|
const contentStr = message.content ?? '{}';
|
|
191
|
-
log.info(`[handleEvent] Parsed: chatId=${chatId}, msgType=${msgType}`);
|
|
192
192
|
log.info(`Message: chatId=${chatId}, messageId=${messageId}, msgType=${msgType}`);
|
|
193
193
|
// Parse message content
|
|
194
194
|
let content;
|
|
195
195
|
try {
|
|
196
196
|
content = JSON.parse(contentStr);
|
|
197
|
-
log.
|
|
197
|
+
log.debug(`Parsed content:`, JSON.stringify(content).slice(0, 200));
|
|
198
198
|
}
|
|
199
199
|
catch (err) {
|
|
200
200
|
log.error('Failed to parse message content:', err);
|
|
@@ -223,7 +223,7 @@ export function setupFeishuHandlers(config, sessionManager) {
|
|
|
223
223
|
text = text.replace(/ /gi, ' ');
|
|
224
224
|
// 最后做一次首尾 trim,但不动中间的空格
|
|
225
225
|
text = text.trim();
|
|
226
|
-
log.
|
|
226
|
+
log.debug(`[MSG] Type=text, User=${senderId}, Length=${text.length}, Content="${text}"`);
|
|
227
227
|
}
|
|
228
228
|
else if (msgType === 'post') {
|
|
229
229
|
// Feishu rich text/post messages - extract text content
|
|
@@ -252,7 +252,7 @@ export function setupFeishuHandlers(config, sessionManager) {
|
|
|
252
252
|
return '';
|
|
253
253
|
}
|
|
254
254
|
if (rawContent && Array.isArray(rawContent)) {
|
|
255
|
-
log.
|
|
255
|
+
log.debug(`[MSG] Post content structure:`, JSON.stringify(rawContent).slice(0, 500));
|
|
256
256
|
for (const section of rawContent) {
|
|
257
257
|
if (Array.isArray(section)) {
|
|
258
258
|
// 二维数组:段落内多个元素
|
|
@@ -266,7 +266,7 @@ export function setupFeishuHandlers(config, sessionManager) {
|
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
268
|
text = text.trim();
|
|
269
|
-
log.
|
|
269
|
+
log.debug(`[MSG] Type=post, User=${senderId}, Length=${text.length}, Content="${text}"`);
|
|
270
270
|
}
|
|
271
271
|
if (!text) {
|
|
272
272
|
log.warn(`[MSG] ${msgType} message has no extractable text content`);
|
|
@@ -321,15 +321,11 @@ export function setupFeishuHandlers(config, sessionManager) {
|
|
|
321
321
|
const enqueueResult = ctx.requestQueue.enqueue(senderId, convId, prompt, async (p, signal) => {
|
|
322
322
|
await handleAIRequest({ userId: senderId, chatId, prompt: p, workDir: work, convId, replyToMessageId: messageId, signal });
|
|
323
323
|
});
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
log.warn('[feishu] Failed to send queue full message for image:', sendErr);
|
|
327
|
-
});
|
|
324
|
+
try {
|
|
325
|
+
await handleEnqueueResult(enqueueResult, (text) => sendTextReply(chatId, text));
|
|
328
326
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
log.warn('[feishu] Failed to send queued message for image:', sendErr);
|
|
332
|
-
});
|
|
327
|
+
catch (sendErr) {
|
|
328
|
+
log.warn('[feishu] Failed to send enqueue result message for image:', sendErr);
|
|
333
329
|
}
|
|
334
330
|
}
|
|
335
331
|
catch (err) {
|
|
@@ -369,15 +365,11 @@ export function setupFeishuHandlers(config, sessionManager) {
|
|
|
369
365
|
const enqueueResult = ctx.requestQueue.enqueue(senderId, convId, prompt, async (p, signal) => {
|
|
370
366
|
await handleAIRequest({ userId: senderId, chatId, prompt: p, workDir, convId, replyToMessageId: messageId, signal });
|
|
371
367
|
});
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
log.warn(`[feishu] Failed to send queue full message for ${msgType}:`, sendErr);
|
|
375
|
-
});
|
|
368
|
+
try {
|
|
369
|
+
await handleEnqueueResult(enqueueResult, (text) => sendTextReply(chatId, text));
|
|
376
370
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
log.warn(`[feishu] Failed to send queued message for ${msgType}:`, sendErr);
|
|
380
|
-
});
|
|
371
|
+
catch (sendErr) {
|
|
372
|
+
log.warn(`[feishu] Failed to send enqueue result message for ${msgType}:`, sendErr);
|
|
381
373
|
}
|
|
382
374
|
}
|
|
383
375
|
catch (err) {
|
|
@@ -389,7 +381,7 @@ export function setupFeishuHandlers(config, sessionManager) {
|
|
|
389
381
|
}
|
|
390
382
|
else {
|
|
391
383
|
log.warn(`[MSG] Unsupported message type: ${msgType}, senderId=${senderId}`);
|
|
392
|
-
log.
|
|
384
|
+
log.debug(`[MSG] Content structure:`, JSON.stringify(content).slice(0, 500));
|
|
393
385
|
}
|
|
394
386
|
}
|
|
395
387
|
}
|
|
@@ -76,7 +76,7 @@ async function getTenantAccessToken() {
|
|
|
76
76
|
app_secret: client.appSecret,
|
|
77
77
|
},
|
|
78
78
|
});
|
|
79
|
-
if (resp.code !== 0
|
|
79
|
+
if (resp.code !== 0) {
|
|
80
80
|
throw new Error(`Failed to get tenant access token: ${resp.msg}`);
|
|
81
81
|
}
|
|
82
82
|
return resp.data.tenant_access_token;
|
package/dist/index.js
CHANGED
|
@@ -296,7 +296,7 @@ export async function main() {
|
|
|
296
296
|
// WebSocket "not open" errors are transient — the connection will auto-reconnect.
|
|
297
297
|
// Exiting would take down all platforms, so just log and continue.
|
|
298
298
|
if (msg.includes("WebSocket is not open") || msg.includes("readyState")) {
|
|
299
|
-
log.
|
|
299
|
+
log.debug("Transient WebSocket error (ignored, will reconnect):", err);
|
|
300
300
|
return;
|
|
301
301
|
}
|
|
302
302
|
log.error("Uncaught exception (process will exit):", err);
|
|
@@ -97,42 +97,26 @@ export function createPlatformAIRequestHandler(deps) {
|
|
|
97
97
|
const mergedCallbacks = { ...defaultCallbacks };
|
|
98
98
|
// Use taskCallbacksFactory if provided (has full context access)
|
|
99
99
|
let factoryCallbacks;
|
|
100
|
-
|
|
101
|
-
factoryCallbacks = taskCallbacksFactory({
|
|
100
|
+
const platformOverrides = taskCallbacksFactory
|
|
101
|
+
? (factoryCallbacks = taskCallbacksFactory({
|
|
102
102
|
chatId,
|
|
103
103
|
msgId,
|
|
104
104
|
taskKey,
|
|
105
105
|
userId,
|
|
106
106
|
toolId,
|
|
107
107
|
replyToMessageId,
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
if (
|
|
113
|
-
mergedCallbacks.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
mergedCallbacks.onFirstContent =
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
else if (taskCallbacks) {
|
|
123
|
-
// Fall back to static taskCallbacks
|
|
124
|
-
if (taskCallbacks.streamUpdate) {
|
|
125
|
-
mergedCallbacks.streamUpdate = taskCallbacks.streamUpdate;
|
|
126
|
-
}
|
|
127
|
-
if (taskCallbacks.sendComplete) {
|
|
128
|
-
mergedCallbacks.sendComplete = taskCallbacks.sendComplete;
|
|
129
|
-
}
|
|
130
|
-
if (taskCallbacks.sendError) {
|
|
131
|
-
mergedCallbacks.sendError = taskCallbacks.sendError;
|
|
132
|
-
}
|
|
133
|
-
if (taskCallbacks.onFirstContent) {
|
|
134
|
-
mergedCallbacks.onFirstContent = taskCallbacks.onFirstContent;
|
|
135
|
-
}
|
|
108
|
+
}))
|
|
109
|
+
: taskCallbacks;
|
|
110
|
+
if (platformOverrides) {
|
|
111
|
+
const { streamUpdate, sendComplete, sendError, onFirstContent } = platformOverrides;
|
|
112
|
+
if (streamUpdate)
|
|
113
|
+
mergedCallbacks.streamUpdate = streamUpdate;
|
|
114
|
+
if (sendComplete)
|
|
115
|
+
mergedCallbacks.sendComplete = sendComplete;
|
|
116
|
+
if (sendError)
|
|
117
|
+
mergedCallbacks.sendError = sendError;
|
|
118
|
+
if (onFirstContent)
|
|
119
|
+
mergedCallbacks.onFirstContent = onFirstContent;
|
|
136
120
|
}
|
|
137
121
|
if (onThinkingToText) {
|
|
138
122
|
mergedCallbacks.onThinkingToText = onThinkingToText;
|
|
@@ -17,15 +17,12 @@
|
|
|
17
17
|
import { setActiveChatId } from '../shared/active-chats.js';
|
|
18
18
|
import { setChatUser } from '../shared/chat-user-map.js';
|
|
19
19
|
import { createLogger } from '../logger.js';
|
|
20
|
+
import { handleEnqueueResult, DEFAULT_QUEUE_FULL_MESSAGE, DEFAULT_QUEUED_MESSAGE } from '../shared/utils.js';
|
|
20
21
|
const log = createLogger('TextFlow');
|
|
21
22
|
/** Default access-denied message. */
|
|
22
23
|
function defaultAccessDeniedMessage(userId) {
|
|
23
24
|
return `抱歉,您没有访问权限。\n您的 ID: ${userId}`;
|
|
24
25
|
}
|
|
25
|
-
/** Default queue-full message. */
|
|
26
|
-
const DEFAULT_QUEUE_FULL_MESSAGE = '请求队列已满,请稍后再试。';
|
|
27
|
-
/** Default queued message. */
|
|
28
|
-
const DEFAULT_QUEUED_MESSAGE = '您的请求已排队等待。';
|
|
29
26
|
/**
|
|
30
27
|
* Handles the full text message flow shared across all platforms.
|
|
31
28
|
*
|
|
@@ -80,12 +77,7 @@ export async function handleTextFlow(params) {
|
|
|
80
77
|
if (customEnqueue) {
|
|
81
78
|
// Platform-specific enqueue (e.g., WorkBuddy with extra context)
|
|
82
79
|
const enqueueResult = await customEnqueue(text);
|
|
83
|
-
|
|
84
|
-
await sendTextReply(chatId, queueFullMessage);
|
|
85
|
-
}
|
|
86
|
-
else if (enqueueResult === 'queued') {
|
|
87
|
-
await sendTextReply(chatId, queuedMessage);
|
|
88
|
-
}
|
|
80
|
+
await handleEnqueueResult(enqueueResult, (text) => sendTextReply(chatId, text), { queueFull: queueFullMessage, queued: queuedMessage });
|
|
89
81
|
}
|
|
90
82
|
else {
|
|
91
83
|
// Standard enqueue flow
|
|
@@ -103,12 +95,7 @@ export async function handleTextFlow(params) {
|
|
|
103
95
|
signal,
|
|
104
96
|
});
|
|
105
97
|
});
|
|
106
|
-
|
|
107
|
-
await sendTextReply(chatId, queueFullMessage);
|
|
108
|
-
}
|
|
109
|
-
else if (enqueueResult === 'queued') {
|
|
110
|
-
await sendTextReply(chatId, queuedMessage);
|
|
111
|
-
}
|
|
98
|
+
await handleEnqueueResult(enqueueResult, (text) => sendTextReply(chatId, text), { queueFull: queueFullMessage, queued: queuedMessage });
|
|
112
99
|
}
|
|
113
100
|
return true;
|
|
114
101
|
}
|
package/dist/qq/event-handler.js
CHANGED
|
@@ -9,6 +9,7 @@ import { setChatUser } from "../shared/chat-user-map.js";
|
|
|
9
9
|
import { createPlatformEventContext } from "../platform/create-event-context.js";
|
|
10
10
|
import { createPlatformAIRequestHandler } from "../platform/handle-ai-request.js";
|
|
11
11
|
import { handleTextFlow } from "../platform/handle-text-flow.js";
|
|
12
|
+
import { handleEnqueueResult } from "../shared/utils.js";
|
|
12
13
|
const log = createLogger("QQHandler");
|
|
13
14
|
const QQ_THROTTLE_MS = 1200;
|
|
14
15
|
const QQ_MIN_STREAM_DELTA_CHARS = 80;
|
|
@@ -266,12 +267,7 @@ export function setupQQHandlers(config, sessionManager) {
|
|
|
266
267
|
signal,
|
|
267
268
|
});
|
|
268
269
|
});
|
|
269
|
-
|
|
270
|
-
await sendTextReply(chatId, "请求队列已满,请稍后再试。");
|
|
271
|
-
}
|
|
272
|
-
else if (enqueueResult === "queued") {
|
|
273
|
-
await sendTextReply(chatId, "您的请求已排队等待。");
|
|
274
|
-
}
|
|
270
|
+
await handleEnqueueResult(enqueueResult, (text) => sendTextReply(chatId, text));
|
|
275
271
|
log.info(`QQ message handled: user=${userId}, chat=${chatId}, attachments=${event.attachments?.length ?? 0}`);
|
|
276
272
|
}
|
|
277
273
|
}
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import { createLogger } from '../logger.js';
|
|
2
2
|
const log = createLogger('Queue');
|
|
3
|
-
export class QueueTimeoutError extends Error {
|
|
4
|
-
constructor(timeoutMs) {
|
|
5
|
-
super(`Task timed out after ${timeoutMs / 1000}s`);
|
|
6
|
-
this.name = 'QueueTimeoutError';
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
3
|
const MAX_QUEUE_SIZE = 3;
|
|
10
|
-
const TASK_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
|
11
4
|
export class RequestQueue {
|
|
12
5
|
queues = new Map();
|
|
13
6
|
enqueue(userId, convId, prompt, execute) {
|
|
@@ -25,7 +18,9 @@ export class RequestQueue {
|
|
|
25
18
|
return 'queued';
|
|
26
19
|
}
|
|
27
20
|
q.running = true;
|
|
28
|
-
this.run(key, prompt, execute).catch(() => {
|
|
21
|
+
this.run(key, prompt, execute).catch((err) => {
|
|
22
|
+
log.error(`Unhandled error in task execution for ${key}:`, err);
|
|
23
|
+
});
|
|
29
24
|
return 'running';
|
|
30
25
|
}
|
|
31
26
|
/** 清除指定用户会话的所有排队任务(不中止正在运行的任务) */
|
|
@@ -42,34 +37,22 @@ export class RequestQueue {
|
|
|
42
37
|
}
|
|
43
38
|
async run(key, prompt, execute) {
|
|
44
39
|
const controller = new AbortController();
|
|
45
|
-
let timer;
|
|
46
40
|
try {
|
|
47
|
-
|
|
48
|
-
timer = setTimeout(() => {
|
|
49
|
-
controller.abort();
|
|
50
|
-
reject(new QueueTimeoutError(TASK_TIMEOUT_MS));
|
|
51
|
-
}, TASK_TIMEOUT_MS);
|
|
52
|
-
});
|
|
53
|
-
await Promise.race([execute(prompt, controller.signal), timeoutPromise]);
|
|
41
|
+
await execute(prompt, controller.signal);
|
|
54
42
|
}
|
|
55
43
|
catch (err) {
|
|
56
|
-
|
|
57
|
-
log.error(`Timeout executing task for ${key}: ${err.message}`);
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
log.error(`Error executing task for ${key}:`, err);
|
|
61
|
-
}
|
|
44
|
+
log.error(`Error executing task for ${key}:`, err);
|
|
62
45
|
throw err;
|
|
63
46
|
}
|
|
64
47
|
finally {
|
|
65
|
-
if (timer)
|
|
66
|
-
clearTimeout(timer);
|
|
67
48
|
const q = this.queues.get(key);
|
|
68
49
|
if (!q)
|
|
69
50
|
return;
|
|
70
51
|
const next = q.tasks.shift();
|
|
71
52
|
if (next) {
|
|
72
|
-
setImmediate(() => this.run(key, next.prompt, next.execute).catch(() => {
|
|
53
|
+
setImmediate(() => this.run(key, next.prompt, next.execute).catch((err) => {
|
|
54
|
+
log.error(`Unhandled error in next task execution for ${key}:`, err);
|
|
55
|
+
}));
|
|
73
56
|
}
|
|
74
57
|
else {
|
|
75
58
|
q.running = false;
|
|
@@ -67,20 +67,6 @@ describe('RequestQueue', () => {
|
|
|
67
67
|
const queue = new RequestQueue();
|
|
68
68
|
expect(queue.clear('nobody', 'noconv')).toBe(0);
|
|
69
69
|
});
|
|
70
|
-
it('times out long-running tasks after 10 minutes', async () => {
|
|
71
|
-
vi.useFakeTimers();
|
|
72
|
-
const queue = new RequestQueue();
|
|
73
|
-
const execute = vi.fn().mockReturnValue(new Promise(() => { })); // never resolves
|
|
74
|
-
queue.enqueue('user1', 'conv1', 'hello', execute);
|
|
75
|
-
expect(execute).toHaveBeenCalledTimes(1);
|
|
76
|
-
// Advance past 10-minute timeout
|
|
77
|
-
vi.advanceTimersByTime(10 * 60 * 1000 + 1);
|
|
78
|
-
// Let microtasks settle
|
|
79
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
80
|
-
// The timed-out task should be done, queue should be cleared
|
|
81
|
-
expect(execute).toHaveBeenCalledTimes(1);
|
|
82
|
-
vi.useRealTimers();
|
|
83
|
-
});
|
|
84
70
|
it('handles task execution error gracefully and processes next', async () => {
|
|
85
71
|
const queue = new RequestQueue();
|
|
86
72
|
const execute = vi.fn()
|
|
@@ -93,22 +79,4 @@ describe('RequestQueue', () => {
|
|
|
93
79
|
expect(execute).toHaveBeenCalledWith('first', expect.any(AbortSignal));
|
|
94
80
|
expect(execute).toHaveBeenCalledWith('second', expect.any(AbortSignal));
|
|
95
81
|
});
|
|
96
|
-
it('aborts the AbortSignal on timeout', async () => {
|
|
97
|
-
vi.useFakeTimers();
|
|
98
|
-
const queue = new RequestQueue();
|
|
99
|
-
let receivedSignal;
|
|
100
|
-
const execute = vi.fn().mockImplementation(async (_prompt, signal) => {
|
|
101
|
-
receivedSignal = signal;
|
|
102
|
-
// Never resolve — simulates a stuck task
|
|
103
|
-
await new Promise(() => { });
|
|
104
|
-
});
|
|
105
|
-
queue.enqueue('user1', 'conv1', 'hello', execute);
|
|
106
|
-
await vi.advanceTimersByTimeAsync(10);
|
|
107
|
-
expect(receivedSignal?.aborted).toBe(false);
|
|
108
|
-
// Advance past timeout
|
|
109
|
-
vi.advanceTimersByTime(10 * 60 * 1000 + 1);
|
|
110
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
111
|
-
expect(receivedSignal?.aborted).toBe(true);
|
|
112
|
-
vi.useRealTimers();
|
|
113
|
-
});
|
|
114
82
|
});
|
|
@@ -2,6 +2,8 @@ import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { writeFile, mkdir } from 'node:fs/promises';
|
|
3
3
|
import { join, dirname } from 'node:path';
|
|
4
4
|
import { APP_HOME } from '../constants.js';
|
|
5
|
+
import { createLogger } from '../logger.js';
|
|
6
|
+
const log = createLogger('ActiveChats');
|
|
5
7
|
const ACTIVE_CHATS_FILE = join(APP_HOME, 'data', 'active-chats.json');
|
|
6
8
|
let data = {};
|
|
7
9
|
let saveTimer = null;
|
|
@@ -25,8 +27,8 @@ function scheduleSave() {
|
|
|
25
27
|
await mkdir(dirname(ACTIVE_CHATS_FILE), { recursive: true });
|
|
26
28
|
await writeFile(ACTIVE_CHATS_FILE, JSON.stringify(data, null, 2), 'utf-8');
|
|
27
29
|
}
|
|
28
|
-
catch {
|
|
29
|
-
|
|
30
|
+
catch (err) {
|
|
31
|
+
log.warn('Failed to save active chats:', err);
|
|
30
32
|
}
|
|
31
33
|
}, 500);
|
|
32
34
|
}
|
|
@@ -39,7 +41,8 @@ export function loadActiveChats() {
|
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
}
|
|
42
|
-
catch {
|
|
44
|
+
catch (err) {
|
|
45
|
+
log.warn('Failed to load active chats, starting with empty state:', err);
|
|
43
46
|
data = {};
|
|
44
47
|
}
|
|
45
48
|
}
|
|
@@ -84,7 +87,7 @@ export function flushActiveChats() {
|
|
|
84
87
|
mkdirSync(dir, { recursive: true });
|
|
85
88
|
writeFileSync(ACTIVE_CHATS_FILE, JSON.stringify(data, null, 2), 'utf-8');
|
|
86
89
|
}
|
|
87
|
-
catch {
|
|
88
|
-
|
|
90
|
+
catch (err) {
|
|
91
|
+
log.warn('Failed to flush active chats:', err);
|
|
89
92
|
}
|
|
90
93
|
}
|
package/dist/shared/ai-task.js
CHANGED
|
@@ -198,16 +198,18 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
|
|
|
198
198
|
}
|
|
199
199
|
settled = true;
|
|
200
200
|
log.error(`Task error for user ${ctx.userId}: ${error}`);
|
|
201
|
-
if (
|
|
201
|
+
if (isUsageLimitError(error)) {
|
|
202
|
+
// Usage limit errors: keep session for all tools (user can retry later)
|
|
203
|
+
log.warn(`Keeping ${aiCommand} session for user ${ctx.userId} after usage limit error`);
|
|
204
|
+
}
|
|
205
|
+
else if (aiCommand !== 'claude') {
|
|
206
|
+
// Non-CLI errors for codex/codebuddy: reset session to avoid stale state
|
|
202
207
|
if (ctx.convId)
|
|
203
208
|
sessionManager.clearSessionForConv(ctx.userId, ctx.convId, aiCommand);
|
|
204
209
|
else
|
|
205
210
|
sessionManager.clearActiveToolSession(ctx.userId, aiCommand);
|
|
206
211
|
log.warn(`Session reset for user ${ctx.userId} due to ${aiCommand} task error`);
|
|
207
212
|
}
|
|
208
|
-
else if (aiCommand === 'codex' && isUsageLimitError(error)) {
|
|
209
|
-
log.warn(`Keeping codex session for user ${ctx.userId} after usage limit error`);
|
|
210
|
-
}
|
|
211
213
|
const friendlyError = hadSessionInvalid
|
|
212
214
|
? '当前 Claude 会话已失效,已自动执行 /new 重置会话,请重新发送刚才的问题。'
|
|
213
215
|
: error;
|
package/dist/shared/utils.d.ts
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
|
+
import type { EnqueueResult } from '../queue/request-queue.js';
|
|
1
2
|
/** 消息头部品牌后缀,用于飞书等平台展示 */
|
|
2
3
|
export declare const OPEN_IM_BRAND_SUFFIX = " \u00B7 \u901A\u8FC7 open-im \u63A7\u5236";
|
|
4
|
+
/** Default queue-full message. */
|
|
5
|
+
export declare const DEFAULT_QUEUE_FULL_MESSAGE = "\u8BF7\u6C42\u961F\u5217\u5DF2\u6EE1\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002";
|
|
6
|
+
/** Default queued message. */
|
|
7
|
+
export declare const DEFAULT_QUEUED_MESSAGE = "\u60A8\u7684\u8BF7\u6C42\u5DF2\u6392\u961F\u7B49\u5F85\u3002";
|
|
8
|
+
/**
|
|
9
|
+
* Handle enqueue result by sending the appropriate notification message.
|
|
10
|
+
* Centralizes the repeated rejected/queued notification pattern across all platforms.
|
|
11
|
+
*/
|
|
12
|
+
export declare function handleEnqueueResult(enqueueResult: EnqueueResult, sendTextReply: (text: string) => Promise<void>, messages?: {
|
|
13
|
+
queueFull?: string;
|
|
14
|
+
queued?: string;
|
|
15
|
+
}): Promise<void>;
|
|
3
16
|
/** 转义路径供 Markdown 显示,防止 xxx.yyy.com 被解析为链接 */
|
|
4
17
|
export declare function escapePathForMarkdown(path: string): string;
|
|
5
18
|
/** AI 工具显示名称映射(aiCommand -> 用户友好名称) */
|
package/dist/shared/utils.js
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
/** 消息头部品牌后缀,用于飞书等平台展示 */
|
|
2
2
|
export const OPEN_IM_BRAND_SUFFIX = ' · 通过 open-im 控制';
|
|
3
|
+
/** Default queue-full message. */
|
|
4
|
+
export const DEFAULT_QUEUE_FULL_MESSAGE = '请求队列已满,请稍后再试。';
|
|
5
|
+
/** Default queued message. */
|
|
6
|
+
export const DEFAULT_QUEUED_MESSAGE = '您的请求已排队等待。';
|
|
7
|
+
/**
|
|
8
|
+
* Handle enqueue result by sending the appropriate notification message.
|
|
9
|
+
* Centralizes the repeated rejected/queued notification pattern across all platforms.
|
|
10
|
+
*/
|
|
11
|
+
export async function handleEnqueueResult(enqueueResult, sendTextReply, messages) {
|
|
12
|
+
if (enqueueResult === 'rejected') {
|
|
13
|
+
await sendTextReply(messages?.queueFull ?? DEFAULT_QUEUE_FULL_MESSAGE);
|
|
14
|
+
}
|
|
15
|
+
else if (enqueueResult === 'queued') {
|
|
16
|
+
await sendTextReply(messages?.queued ?? DEFAULT_QUEUED_MESSAGE);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
3
19
|
/** 转义路径供 Markdown 显示,防止 xxx.yyy.com 被解析为链接 */
|
|
4
20
|
export function escapePathForMarkdown(path) {
|
|
5
21
|
return `\`${path.replace(/`/g, '\\`')}\``;
|
|
@@ -9,6 +9,7 @@ import { buildErrorNote, buildProgressNote } from "../shared/message-note.js";
|
|
|
9
9
|
import { createPlatformEventContext } from "../platform/create-event-context.js";
|
|
10
10
|
import { createPlatformAIRequestHandler } from "../platform/handle-ai-request.js";
|
|
11
11
|
import { handleTextFlow } from "../platform/handle-text-flow.js";
|
|
12
|
+
import { handleEnqueueResult } from "../shared/utils.js";
|
|
12
13
|
import { setActiveChatId } from "../shared/active-chats.js";
|
|
13
14
|
import { setChatUser } from "../shared/chat-user-map.js";
|
|
14
15
|
const log = createLogger("TgHandler");
|
|
@@ -245,6 +246,35 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
|
|
|
245
246
|
runningTasks,
|
|
246
247
|
taskCallbacksFactory: telegramTaskCallbacksFactory,
|
|
247
248
|
});
|
|
249
|
+
/**
|
|
250
|
+
* Shared preamble for all media handlers: access check + chat registration.
|
|
251
|
+
* Returns null (with early return) if user is not allowed.
|
|
252
|
+
*/
|
|
253
|
+
function registerMediaChat(tgCtx) {
|
|
254
|
+
const chatId = String(tgCtx.chat.id);
|
|
255
|
+
const userId = String(tgCtx.from?.id ?? "");
|
|
256
|
+
if (!accessControl.isAllowed(userId))
|
|
257
|
+
return null;
|
|
258
|
+
setActiveChatId("telegram", chatId);
|
|
259
|
+
setChatUser(chatId, userId, "telegram");
|
|
260
|
+
return { chatId, userId };
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Generic handler for file-type media (document, audio, voice, video).
|
|
264
|
+
* Downloads the file, builds a prompt with media context, and enqueues.
|
|
265
|
+
*/
|
|
266
|
+
async function handleFileMedia(ids, downloadFn, kind, metadata, caption, errorMsg) {
|
|
267
|
+
try {
|
|
268
|
+
const contextText = buildMediaContext(metadata, caption ? `Caption: ${caption}` : undefined);
|
|
269
|
+
const path = await downloadFn();
|
|
270
|
+
const enqueueResult = await enqueueSavedMedia(ids.userId, ids.chatId, kind, path, contextText);
|
|
271
|
+
await handleEnqueueResult(enqueueResult, (text) => sendTextReply(ids.chatId, text));
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
log.error(`Failed to download ${kind}:`, err);
|
|
275
|
+
await sendTextReply(ids.chatId, errorMsg);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
248
278
|
async function enqueueSavedMedia(userId, chatId, kind, localPath, text) {
|
|
249
279
|
const prompt = buildSavedMediaPrompt({
|
|
250
280
|
source: "Telegram",
|
|
@@ -309,13 +339,10 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
|
|
|
309
339
|
}
|
|
310
340
|
});
|
|
311
341
|
bot.on(message("photo"), async (ctx) => {
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
const caption = ctx.message.caption?.trim() || "";
|
|
315
|
-
if (!accessControl.isAllowed(userId))
|
|
342
|
+
const ids = registerMediaChat(ctx);
|
|
343
|
+
if (!ids)
|
|
316
344
|
return;
|
|
317
|
-
|
|
318
|
-
setChatUser(chatId, userId, "telegram");
|
|
345
|
+
const caption = ctx.message.caption?.trim() || "";
|
|
319
346
|
const photos = ctx.message.photo;
|
|
320
347
|
const largest = photos[photos.length - 1];
|
|
321
348
|
const contextText = buildMediaContext({
|
|
@@ -328,134 +355,42 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
|
|
|
328
355
|
}
|
|
329
356
|
catch (err) {
|
|
330
357
|
log.error("Failed to download photo:", err);
|
|
331
|
-
await sendTextReply(chatId, "图片下载失败。");
|
|
358
|
+
await sendTextReply(ids.chatId, "图片下载失败。");
|
|
332
359
|
return;
|
|
333
360
|
}
|
|
334
|
-
const enqueueResult = await enqueueSavedMedia(userId, chatId, "image", imagePath, contextText);
|
|
335
|
-
|
|
336
|
-
await sendTextReply(chatId, "请求队列已满,请稍后再试。");
|
|
337
|
-
}
|
|
338
|
-
else if (enqueueResult === "queued") {
|
|
339
|
-
await sendTextReply(chatId, "您的请求已排队等待。");
|
|
340
|
-
}
|
|
361
|
+
const enqueueResult = await enqueueSavedMedia(ids.userId, ids.chatId, "image", imagePath, contextText);
|
|
362
|
+
await handleEnqueueResult(enqueueResult, (text) => sendTextReply(ids.chatId, text));
|
|
341
363
|
});
|
|
342
364
|
bot.on(message("document"), async (ctx) => {
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
const caption = ctx.message.caption?.trim() || "";
|
|
346
|
-
if (!accessControl.isAllowed(userId))
|
|
365
|
+
const ids = registerMediaChat(ctx);
|
|
366
|
+
if (!ids)
|
|
347
367
|
return;
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const document = ctx.message.document;
|
|
352
|
-
const contextText = buildMediaContext({
|
|
353
|
-
Filename: document.file_name,
|
|
354
|
-
MimeType: document.mime_type,
|
|
355
|
-
Size: document.file_size,
|
|
356
|
-
}, caption ? `Caption: ${caption}` : undefined);
|
|
357
|
-
const path = await downloadTelegramFile(bot, document.file_id, document.file_name ?? document.file_id, "bin");
|
|
358
|
-
const enqueueResult = await enqueueSavedMedia(userId, chatId, "document", path, contextText);
|
|
359
|
-
if (enqueueResult === "rejected") {
|
|
360
|
-
await sendTextReply(chatId, "请求队列已满,请稍后再试。");
|
|
361
|
-
}
|
|
362
|
-
else if (enqueueResult === "queued") {
|
|
363
|
-
await sendTextReply(chatId, "您的请求已排队等待。");
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
catch (err) {
|
|
367
|
-
log.error("Failed to download document:", err);
|
|
368
|
-
await sendTextReply(chatId, "文档下载失败。");
|
|
369
|
-
}
|
|
368
|
+
const caption = ctx.message.caption?.trim() || "";
|
|
369
|
+
const document = ctx.message.document;
|
|
370
|
+
await handleFileMedia(ids, () => downloadTelegramFile(bot, document.file_id, document.file_name ?? document.file_id, "bin"), "document", { Filename: document.file_name, MimeType: document.mime_type, Size: document.file_size }, caption || undefined, "文档下载失败。");
|
|
370
371
|
});
|
|
371
372
|
bot.on(message("audio"), async (ctx) => {
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
const caption = ctx.message.caption?.trim() || "";
|
|
375
|
-
if (!accessControl.isAllowed(userId))
|
|
373
|
+
const ids = registerMediaChat(ctx);
|
|
374
|
+
if (!ids)
|
|
376
375
|
return;
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
const audio = ctx.message.audio;
|
|
381
|
-
const contextText = buildMediaContext({
|
|
382
|
-
Filename: audio.file_name,
|
|
383
|
-
Title: audio.title,
|
|
384
|
-
Performer: audio.performer,
|
|
385
|
-
DurationSeconds: audio.duration,
|
|
386
|
-
MimeType: audio.mime_type,
|
|
387
|
-
}, caption ? `Caption: ${caption}` : undefined);
|
|
388
|
-
const path = await downloadTelegramFile(bot, audio.file_id, audio.file_name ?? audio.file_id, "mp3");
|
|
389
|
-
const enqueueResult = await enqueueSavedMedia(userId, chatId, "audio", path, contextText);
|
|
390
|
-
if (enqueueResult === "rejected") {
|
|
391
|
-
await sendTextReply(chatId, "请求队列已满,请稍后再试。");
|
|
392
|
-
}
|
|
393
|
-
else if (enqueueResult === "queued") {
|
|
394
|
-
await sendTextReply(chatId, "您的请求已排队等待。");
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
catch (err) {
|
|
398
|
-
log.error("Failed to download audio:", err);
|
|
399
|
-
await sendTextReply(chatId, "音频下载失败。");
|
|
400
|
-
}
|
|
376
|
+
const caption = ctx.message.caption?.trim() || "";
|
|
377
|
+
const audio = ctx.message.audio;
|
|
378
|
+
await handleFileMedia(ids, () => downloadTelegramFile(bot, audio.file_id, audio.file_name ?? audio.file_id, "mp3"), "audio", { Filename: audio.file_name, Title: audio.title, Performer: audio.performer, DurationSeconds: audio.duration, MimeType: audio.mime_type }, caption || undefined, "音频下载失败。");
|
|
401
379
|
});
|
|
402
380
|
bot.on(message("voice"), async (ctx) => {
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
if (!accessControl.isAllowed(userId))
|
|
381
|
+
const ids = registerMediaChat(ctx);
|
|
382
|
+
if (!ids)
|
|
406
383
|
return;
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
try {
|
|
410
|
-
const voice = ctx.message.voice;
|
|
411
|
-
const contextText = buildMediaContext({
|
|
412
|
-
DurationSeconds: voice.duration,
|
|
413
|
-
MimeType: voice.mime_type,
|
|
414
|
-
});
|
|
415
|
-
const path = await downloadTelegramFile(bot, voice.file_id, voice.file_unique_id ?? voice.file_id, "ogg");
|
|
416
|
-
const enqueueResult = await enqueueSavedMedia(userId, chatId, "voice", path, contextText);
|
|
417
|
-
if (enqueueResult === "rejected") {
|
|
418
|
-
await sendTextReply(chatId, "请求队列已满,请稍后再试。");
|
|
419
|
-
}
|
|
420
|
-
else if (enqueueResult === "queued") {
|
|
421
|
-
await sendTextReply(chatId, "您的请求已排队等待。");
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
catch (err) {
|
|
425
|
-
log.error("Failed to download voice message:", err);
|
|
426
|
-
await sendTextReply(chatId, "语音下载失败。");
|
|
427
|
-
}
|
|
384
|
+
const voice = ctx.message.voice;
|
|
385
|
+
await handleFileMedia(ids, () => downloadTelegramFile(bot, voice.file_id, voice.file_unique_id ?? voice.file_id, "ogg"), "voice", { DurationSeconds: voice.duration, MimeType: voice.mime_type }, undefined, "语音下载失败。");
|
|
428
386
|
});
|
|
429
387
|
bot.on(message("video"), async (ctx) => {
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
const caption = ctx.message.caption?.trim() || "";
|
|
433
|
-
if (!accessControl.isAllowed(userId))
|
|
388
|
+
const ids = registerMediaChat(ctx);
|
|
389
|
+
if (!ids)
|
|
434
390
|
return;
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
const video = ctx.message.video;
|
|
439
|
-
const contextText = buildMediaContext({
|
|
440
|
-
Filename: video.file_name,
|
|
441
|
-
DurationSeconds: video.duration,
|
|
442
|
-
Width: video.width,
|
|
443
|
-
Height: video.height,
|
|
444
|
-
MimeType: video.mime_type,
|
|
445
|
-
}, caption ? `Caption: ${caption}` : undefined);
|
|
446
|
-
const path = await downloadTelegramFile(bot, video.file_id, video.file_name ?? video.file_unique_id ?? video.file_id, "mp4");
|
|
447
|
-
const enqueueResult = await enqueueSavedMedia(userId, chatId, "video", path, contextText);
|
|
448
|
-
if (enqueueResult === "rejected") {
|
|
449
|
-
await sendTextReply(chatId, "请求队列已满,请稍后再试。");
|
|
450
|
-
}
|
|
451
|
-
else if (enqueueResult === "queued") {
|
|
452
|
-
await sendTextReply(chatId, "您的请求已排队等待。");
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
catch (err) {
|
|
456
|
-
log.error("Failed to download video:", err);
|
|
457
|
-
await sendTextReply(chatId, "视频下载失败。");
|
|
458
|
-
}
|
|
391
|
+
const caption = ctx.message.caption?.trim() || "";
|
|
392
|
+
const video = ctx.message.video;
|
|
393
|
+
await handleFileMedia(ids, () => downloadTelegramFile(bot, video.file_id, video.file_name ?? video.file_unique_id ?? video.file_id, "mp4"), "video", { Filename: video.file_name, DurationSeconds: video.duration, Width: video.width, Height: video.height, MimeType: video.mime_type }, caption || undefined, "视频下载失败。");
|
|
459
394
|
});
|
|
460
395
|
return {
|
|
461
396
|
stop: () => { },
|
|
@@ -14,6 +14,7 @@ import { buildErrorNote, buildProgressNote } from '../shared/message-note.js';
|
|
|
14
14
|
import { createPlatformEventContext } from '../platform/create-event-context.js';
|
|
15
15
|
import { createPlatformAIRequestHandler } from '../platform/handle-ai-request.js';
|
|
16
16
|
import { handleTextFlow } from '../platform/handle-text-flow.js';
|
|
17
|
+
import { handleEnqueueResult } from '../shared/utils.js';
|
|
17
18
|
import { decryptAes256CbcMedia, downloadMediaFromUrl, inferExtensionFromBuffer, inferExtensionFromContentType, saveBase64Media, saveBufferMedia, } from '../shared/media-storage.js';
|
|
18
19
|
const log = createLogger('WeWorkHandler');
|
|
19
20
|
const WEWORK_MEDIA_TIMEOUT_MS = 60_000;
|
|
@@ -243,12 +244,7 @@ export function setupWeWorkHandlers(config, sessionManager) {
|
|
|
243
244
|
const enqueueResult = ctx.requestQueue.enqueue(userId, convId, prompt, async (nextPrompt, signal) => {
|
|
244
245
|
await handleAIRequest({ userId, chatId, prompt: nextPrompt, workDir, convId, replyToMessageId: undefined, signal });
|
|
245
246
|
});
|
|
246
|
-
|
|
247
|
-
await sendTextReply(chatId, '请求队列已满,请稍后再试。', reqId);
|
|
248
|
-
}
|
|
249
|
-
else if (enqueueResult === 'queued') {
|
|
250
|
-
await sendTextReply(chatId, '您的请求已排队等待。', reqId);
|
|
251
|
-
}
|
|
247
|
+
await handleEnqueueResult(enqueueResult, (text) => sendTextReply(chatId, text, reqId));
|
|
252
248
|
}
|
|
253
249
|
async function handleEvent(data) {
|
|
254
250
|
log.debug('[handleEvent] Called with data:', JSON.stringify(data).slice(0, 800));
|
package/dist/workbuddy/client.js
CHANGED
|
@@ -195,7 +195,14 @@ async function connect() {
|
|
|
195
195
|
scheduleReconnect();
|
|
196
196
|
},
|
|
197
197
|
onError: (error) => {
|
|
198
|
-
|
|
198
|
+
const msg = error instanceof Error ? error.message : JSON.stringify(error);
|
|
199
|
+
// "transport closed" is a transient WebSocket disconnect, not a real error
|
|
200
|
+
if (msg.includes('transport closed')) {
|
|
201
|
+
log.debug(`WorkBuddy Centrifuge transient error: ${msg}`);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
log.error(`WorkBuddy Centrifuge error: ${msg}`);
|
|
205
|
+
}
|
|
199
206
|
updateState('error');
|
|
200
207
|
},
|
|
201
208
|
onPersistentFailure: () => {
|
|
@@ -4,15 +4,12 @@
|
|
|
4
4
|
import { sendTextReply, sendErrorReply } from './message-sender.js';
|
|
5
5
|
import { startTaskCleanup } from '../shared/task-cleanup.js';
|
|
6
6
|
import { WORKBUDDY_THROTTLE_MS } from '../constants.js';
|
|
7
|
-
import { setActiveChatId } from '../shared/active-chats.js';
|
|
8
|
-
import { setChatUser } from '../shared/chat-user-map.js';
|
|
9
7
|
import { createLogger } from '../logger.js';
|
|
10
8
|
import { createPlatformEventContext } from '../platform/create-event-context.js';
|
|
11
9
|
import { createPlatformAIRequestHandler } from '../platform/handle-ai-request.js';
|
|
10
|
+
import { handleTextFlow } from '../platform/handle-text-flow.js';
|
|
12
11
|
const log = createLogger('WorkBuddyHandler');
|
|
13
12
|
export function setupWorkBuddyHandlers(config, sessionManager) {
|
|
14
|
-
// WorkBuddy-specific: taskKeyByChatId map for tracking
|
|
15
|
-
const taskKeyByChatId = new Map();
|
|
16
13
|
// Create shared platform event context
|
|
17
14
|
const ctx = createPlatformEventContext({
|
|
18
15
|
platform: 'workbuddy',
|
|
@@ -21,7 +18,6 @@ export function setupWorkBuddyHandlers(config, sessionManager) {
|
|
|
21
18
|
sessionManager,
|
|
22
19
|
sender: {
|
|
23
20
|
sendTextReply: async (chatId, text) => {
|
|
24
|
-
// WorkBuddy needs msgId for all replies, we'll handle this per-event
|
|
25
21
|
await sendTextReply(null, chatId, text, '');
|
|
26
22
|
},
|
|
27
23
|
},
|
|
@@ -31,157 +27,65 @@ export function setupWorkBuddyHandlers(config, sessionManager) {
|
|
|
31
27
|
// WorkBuddy-specific sender callbacks (no thinking message needed)
|
|
32
28
|
const platformSender = {
|
|
33
29
|
sendThinkingMessage: async (_chatId, _replyToMessageId, _toolId) => {
|
|
34
|
-
// WorkBuddy uses incoming msgId
|
|
35
|
-
// This is a no-op since we'll use the incoming msgId
|
|
30
|
+
// WorkBuddy uses incoming msgId directly; no separate thinking message
|
|
36
31
|
return 'workbuddy_no_thinking';
|
|
37
32
|
},
|
|
38
|
-
sendTextReply: async (
|
|
39
|
-
|
|
40
|
-
await sendTextReply(null, _chatId, text, '');
|
|
33
|
+
sendTextReply: async (chatId, text) => {
|
|
34
|
+
await sendTextReply(null, chatId, text, '');
|
|
41
35
|
},
|
|
42
36
|
startTyping: (_chatId) => {
|
|
43
37
|
// WorkBuddy doesn't support typing indicators
|
|
44
38
|
return () => { };
|
|
45
39
|
},
|
|
46
40
|
};
|
|
47
|
-
// WorkBuddy-specific callbacks (log-only streaming, no real updates)
|
|
48
|
-
const workBuddyTaskCallbacks = {
|
|
49
|
-
streamUpdate: async (content) => {
|
|
50
|
-
// WorkBuddy doesn't support streaming updates via Centrifuge
|
|
51
|
-
log.debug(`Stream update (not sent): ${content.substring(0, 50)}...`);
|
|
52
|
-
},
|
|
53
|
-
sendComplete: async (_content) => {
|
|
54
|
-
// Will be handled per-event with correct msgId
|
|
55
|
-
},
|
|
56
|
-
sendError: async (_error) => {
|
|
57
|
-
// Will be handled per-event with correct msgId
|
|
58
|
-
},
|
|
59
|
-
extraCleanup: () => {
|
|
60
|
-
// Clean up taskKeyByChatId on completion
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
// WorkBuddy-specific init to capture msgId for task tracking
|
|
64
|
-
const extraInit = ({ chatId, taskKey }) => {
|
|
65
|
-
taskKeyByChatId.set(chatId, taskKey);
|
|
66
|
-
return () => {
|
|
67
|
-
if (taskKeyByChatId.get(chatId) === taskKey) {
|
|
68
|
-
taskKeyByChatId.delete(chatId);
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
};
|
|
72
|
-
// Create platform-specific AI request handler
|
|
73
|
-
// Note: WorkBuddy uses a different handleAIRequest signature with msgId
|
|
74
|
-
// We'll need to wrap the standard handler
|
|
75
|
-
createPlatformAIRequestHandler({
|
|
76
|
-
platform: 'workbuddy',
|
|
77
|
-
config,
|
|
78
|
-
sessionManager,
|
|
79
|
-
sender: platformSender,
|
|
80
|
-
throttleMs: WORKBUDDY_THROTTLE_MS,
|
|
81
|
-
runningTasks: ctx.runningTasks,
|
|
82
|
-
taskCallbacks: workBuddyTaskCallbacks,
|
|
83
|
-
extraInit,
|
|
84
|
-
});
|
|
85
|
-
// WorkBuddy-specific wrapper that captures msgId
|
|
86
|
-
async function handleAIRequest(userId, chatId, msgId, prompt, workDir, convId, signal) {
|
|
87
|
-
log.info(`[AI_REQUEST] userId=${userId}, chatId=${chatId}, msgId=${msgId}, promptLength=${prompt.length}`);
|
|
88
|
-
// WorkBuddy uses incoming msgId as taskKey (no thinking message needed)
|
|
89
|
-
const taskKey = `${userId}:${msgId}`;
|
|
90
|
-
// Directly run AI task (WorkBuddy doesn't use the standard flow)
|
|
91
|
-
const { resolvePlatformAiCommand } = await import('../config.js');
|
|
92
|
-
const { getAdapter } = await import('../adapters/registry.js');
|
|
93
|
-
const { runAITask } = await import('../shared/ai-task.js');
|
|
94
|
-
const aiCommand = resolvePlatformAiCommand(config, 'workbuddy');
|
|
95
|
-
const toolAdapter = getAdapter(aiCommand);
|
|
96
|
-
if (!toolAdapter) {
|
|
97
|
-
log.error(`[handleAIRequest] No adapter found for: ${aiCommand}`);
|
|
98
|
-
await sendErrorReply(null, chatId, `AI tool is not configured: ${aiCommand}`, msgId);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
const sessionId = convId
|
|
102
|
-
? sessionManager.getSessionIdForConv(userId, convId, aiCommand)
|
|
103
|
-
: undefined;
|
|
104
|
-
log.info(`[handleAIRequest] Running ${aiCommand} for user ${userId}, sessionId=${sessionId ?? 'new'}`);
|
|
105
|
-
// Set up task tracking key mapping
|
|
106
|
-
taskKeyByChatId.set(chatId, taskKey);
|
|
107
|
-
await runAITask({ config, sessionManager }, { userId, chatId, workDir, sessionId, convId, platform: 'workbuddy', taskKey, signal }, prompt, toolAdapter, {
|
|
108
|
-
throttleMs: WORKBUDDY_THROTTLE_MS,
|
|
109
|
-
streamUpdate: async (content) => {
|
|
110
|
-
log.debug(`Stream update (not sent): ${content.substring(0, 50)}...`);
|
|
111
|
-
},
|
|
112
|
-
sendComplete: async (content) => {
|
|
113
|
-
await sendTextReply(null, chatId, content, msgId);
|
|
114
|
-
},
|
|
115
|
-
sendError: async (error) => {
|
|
116
|
-
await sendErrorReply(null, chatId, error, msgId);
|
|
117
|
-
},
|
|
118
|
-
extraCleanup: () => {
|
|
119
|
-
ctx.runningTasks.delete(taskKey);
|
|
120
|
-
if (taskKeyByChatId.get(chatId) === taskKey) {
|
|
121
|
-
taskKeyByChatId.delete(chatId);
|
|
122
|
-
}
|
|
123
|
-
},
|
|
124
|
-
onTaskReady: (state) => {
|
|
125
|
-
ctx.runningTasks.set(taskKey, state);
|
|
126
|
-
taskKeyByChatId.set(chatId, taskKey);
|
|
127
|
-
},
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
41
|
async function handleEvent(chatId, msgId, content) {
|
|
131
42
|
log.info(`[handleEvent] chatId=${chatId}, msgId=${msgId}, content="${content.substring(0, 100)}"`);
|
|
132
43
|
// Use chatId as userId for WorkBuddy (WeChat KF doesn't have separate userId)
|
|
133
44
|
const userId = chatId;
|
|
134
45
|
const text = content.trim();
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
await sendErrorReply(null, chatId, `抱歉,您没有访问权限。\n您的 ID: ${userId}`, msgId);
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
setActiveChatId('workbuddy', chatId);
|
|
142
|
-
setChatUser(chatId, userId, 'workbuddy');
|
|
143
|
-
const workDir = sessionManager.getWorkDir(userId);
|
|
144
|
-
const convId = sessionManager.getConvId(userId);
|
|
145
|
-
// Create a per-event sender that captures msgId
|
|
146
|
-
const eventSender = {
|
|
46
|
+
// Create a per-event sender that captures msgId for all replies
|
|
47
|
+
const msgIdSender = {
|
|
48
|
+
...platformSender,
|
|
147
49
|
sendTextReply: async (c, t) => {
|
|
148
50
|
await sendTextReply(null, c, t, msgId);
|
|
149
51
|
},
|
|
150
52
|
};
|
|
151
|
-
// Create
|
|
152
|
-
const
|
|
153
|
-
|
|
53
|
+
// Create per-event handleAIRequest that captures msgId for task callbacks
|
|
54
|
+
const handleAIRequest = createPlatformAIRequestHandler({
|
|
55
|
+
platform: 'workbuddy',
|
|
154
56
|
config,
|
|
155
57
|
sessionManager,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
58
|
+
sender: msgIdSender,
|
|
59
|
+
throttleMs: WORKBUDDY_THROTTLE_MS,
|
|
60
|
+
runningTasks: ctx.runningTasks,
|
|
61
|
+
taskKeyBuilder: (userId, _msgId) => `${userId}:${msgId}`,
|
|
62
|
+
taskCallbacksFactory: ({ chatId: c }) => ({
|
|
63
|
+
streamUpdate: async () => {
|
|
64
|
+
// WorkBuddy doesn't support streaming updates via Centrifuge
|
|
65
|
+
},
|
|
66
|
+
sendComplete: async (content) => {
|
|
67
|
+
await sendTextReply(null, c, content, msgId);
|
|
68
|
+
},
|
|
69
|
+
sendError: async (error) => {
|
|
70
|
+
await sendErrorReply(null, c, error, msgId);
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
159
73
|
});
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
const enqueueResult = ctx.requestQueue.enqueue(userId, convId, text, async (nextPrompt, signal) => {
|
|
176
|
-
log.info(`Executing AI request for: ${nextPrompt}`);
|
|
177
|
-
await handleAIRequest(userId, chatId, msgId, nextPrompt, workDir, convId, signal);
|
|
74
|
+
// Use shared text flow with customEnqueue to carry msgId through
|
|
75
|
+
await handleTextFlow({
|
|
76
|
+
platform: 'workbuddy',
|
|
77
|
+
userId,
|
|
78
|
+
chatId,
|
|
79
|
+
text,
|
|
80
|
+
ctx,
|
|
81
|
+
handleAIRequest,
|
|
82
|
+
sendTextReply: (c, t) => sendTextReply(null, c, t, msgId),
|
|
83
|
+
workDir: sessionManager.getWorkDir(userId),
|
|
84
|
+
convId: sessionManager.getConvId(userId),
|
|
85
|
+
accessDeniedMessage: (uid) => `抱歉,您没有访问权限。\n您的 ID: ${uid}`,
|
|
86
|
+
queueFullMessage: '请求队列已满,请稍后再试。',
|
|
87
|
+
queuedMessage: '您的请求已排队等待。',
|
|
178
88
|
});
|
|
179
|
-
if (enqueueResult === 'rejected') {
|
|
180
|
-
await sendErrorReply(null, chatId, '请求队列已满,请稍后再试。', msgId);
|
|
181
|
-
}
|
|
182
|
-
else if (enqueueResult === 'queued') {
|
|
183
|
-
await sendTextReply(null, chatId, '您的请求已排队等待。', msgId);
|
|
184
|
-
}
|
|
185
89
|
}
|
|
186
90
|
return {
|
|
187
91
|
stop: () => stopTaskCleanup(),
|