@wu529778790/open-im 1.8.1-beta.5 → 1.8.1-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/channels/capabilities.js +5 -0
- package/dist/commands/handler.d.ts +1 -1
- package/dist/commands/handler.js +3 -1
- package/dist/config-web.js +1 -0
- package/dist/config.d.ts +25 -1
- package/dist/config.js +46 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/index.js +17 -2
- package/dist/setup.js +2 -1
- package/dist/shared/active-chats.d.ts +2 -2
- package/dist/workbuddy/centrifuge-client.d.ts +74 -0
- package/dist/workbuddy/centrifuge-client.js +272 -0
- package/dist/workbuddy/client.d.ts +27 -0
- package/dist/workbuddy/client.js +162 -0
- package/dist/workbuddy/event-handler.d.ts +11 -0
- package/dist/workbuddy/event-handler.js +118 -0
- package/dist/workbuddy/index.d.ts +8 -0
- package/dist/workbuddy/index.js +8 -0
- package/dist/workbuddy/message-sender.d.ts +16 -0
- package/dist/workbuddy/message-sender.js +51 -0
- package/dist/workbuddy/oauth.d.ts +114 -0
- package/dist/workbuddy/oauth.js +310 -0
- package/dist/workbuddy/types.d.ts +86 -0
- package/dist/workbuddy/types.js +4 -0
- package/package.json +2 -1
|
@@ -5,6 +5,7 @@ const PLATFORM_LABELS = {
|
|
|
5
5
|
wechat: "微信",
|
|
6
6
|
wework: "企业微信",
|
|
7
7
|
dingtalk: "钉钉",
|
|
8
|
+
workbuddy: "WorkBuddy",
|
|
8
9
|
};
|
|
9
10
|
export const CHANNEL_CAPABILITIES = {
|
|
10
11
|
telegram: {
|
|
@@ -31,6 +32,10 @@ export const CHANNEL_CAPABILITIES = {
|
|
|
31
32
|
inbound: { text: "native", image: "fallback", file: "fallback", voice: "fallback", video: "fallback" },
|
|
32
33
|
outbound: { streamEdit: "native", streamPush: "fallback", image: "fallback", card: "native", typing: "native" },
|
|
33
34
|
},
|
|
35
|
+
workbuddy: {
|
|
36
|
+
inbound: { text: "native", image: "none", file: "none", voice: "none", video: "none" },
|
|
37
|
+
outbound: { streamEdit: "none", streamPush: "none", image: "none", card: "none", typing: "none" },
|
|
38
|
+
},
|
|
34
39
|
};
|
|
35
40
|
function listPreferredPlatforms(kind) {
|
|
36
41
|
return Object.entries(CHANNEL_CAPABILITIES)
|
|
@@ -18,7 +18,7 @@ export type ClaudeRequestHandler = (userId: string, chatId: string, prompt: stri
|
|
|
18
18
|
export declare class CommandHandler {
|
|
19
19
|
private deps;
|
|
20
20
|
constructor(deps: CommandHandlerDeps);
|
|
21
|
-
dispatch(text: string, chatId: string, userId: string, platform: 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wechat' | 'wework', handleClaudeRequest: ClaudeRequestHandler): Promise<boolean>;
|
|
21
|
+
dispatch(text: string, chatId: string, userId: string, platform: 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wechat' | 'wework' | 'workbuddy', handleClaudeRequest: ClaudeRequestHandler): Promise<boolean>;
|
|
22
22
|
private getClearHistoryHint;
|
|
23
23
|
private handleHelp;
|
|
24
24
|
private handleNew;
|
package/dist/commands/handler.js
CHANGED
|
@@ -40,7 +40,9 @@ export class CommandHandler {
|
|
|
40
40
|
? '💡 提示:如需清除本对话的历史消息,请清除聊天记录'
|
|
41
41
|
: platform === 'dingtalk'
|
|
42
42
|
? '💡 提示:如需清除本对话的历史消息,请在钉钉中清空聊天记录'
|
|
43
|
-
:
|
|
43
|
+
: platform === 'workbuddy'
|
|
44
|
+
? '💡 提示:如需清除本对话的历史消息,请清除聊天记录'
|
|
45
|
+
: '💡 提示:如需清除本对话的历史消息,请点击 Telegram 聊天右上角 ⋮ → 清除历史';
|
|
44
46
|
}
|
|
45
47
|
async handleHelp(chatId, platform) {
|
|
46
48
|
const help = [
|
package/dist/config-web.js
CHANGED
package/dist/config.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type LogLevel } from './logger.js';
|
|
2
|
-
export type Platform = 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wechat' | 'wework';
|
|
2
|
+
export type Platform = 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wechat' | 'wework' | 'workbuddy';
|
|
3
3
|
export type AiCommand = 'claude' | 'codex' | 'codebuddy';
|
|
4
4
|
export interface Config {
|
|
5
5
|
enabledPlatforms: Platform[];
|
|
@@ -29,6 +29,7 @@ export interface Config {
|
|
|
29
29
|
wechatAllowedUserIds: string[];
|
|
30
30
|
weworkAllowedUserIds: string[];
|
|
31
31
|
dingtalkAllowedUserIds: string[];
|
|
32
|
+
workbuddyAllowedUserIds: string[];
|
|
32
33
|
aiCommand: AiCommand;
|
|
33
34
|
codexCliPath: string;
|
|
34
35
|
codebuddyCliPath: string;
|
|
@@ -82,6 +83,17 @@ export interface Config {
|
|
|
82
83
|
allowedUserIds: string[];
|
|
83
84
|
cardTemplateId?: string;
|
|
84
85
|
};
|
|
86
|
+
workbuddy?: {
|
|
87
|
+
enabled: boolean;
|
|
88
|
+
aiCommand?: AiCommand;
|
|
89
|
+
allowedUserIds: string[];
|
|
90
|
+
accessToken?: string;
|
|
91
|
+
refreshToken?: string;
|
|
92
|
+
userId?: string;
|
|
93
|
+
baseUrl?: string;
|
|
94
|
+
guid?: string;
|
|
95
|
+
workspacePath?: string;
|
|
96
|
+
};
|
|
85
97
|
};
|
|
86
98
|
}
|
|
87
99
|
export interface FilePlatformTelegram {
|
|
@@ -134,6 +146,17 @@ export interface FilePlatformDingtalk {
|
|
|
134
146
|
allowedUserIds?: string[];
|
|
135
147
|
cardTemplateId?: string;
|
|
136
148
|
}
|
|
149
|
+
interface FilePlatformWorkBuddy {
|
|
150
|
+
enabled?: boolean;
|
|
151
|
+
aiCommand?: AiCommand;
|
|
152
|
+
allowedUserIds?: string[];
|
|
153
|
+
accessToken?: string;
|
|
154
|
+
refreshToken?: string;
|
|
155
|
+
userId?: string;
|
|
156
|
+
baseUrl?: string;
|
|
157
|
+
guid?: string;
|
|
158
|
+
workspacePath?: string;
|
|
159
|
+
}
|
|
137
160
|
export interface FileToolClaude {
|
|
138
161
|
cliPath?: string;
|
|
139
162
|
workDir?: string;
|
|
@@ -167,6 +190,7 @@ export interface FileConfig {
|
|
|
167
190
|
wechat?: FilePlatformWechat;
|
|
168
191
|
wework?: FilePlatformWework;
|
|
169
192
|
dingtalk?: FilePlatformDingtalk;
|
|
193
|
+
workbuddy?: FilePlatformWorkBuddy;
|
|
170
194
|
};
|
|
171
195
|
env?: Record<string, string>;
|
|
172
196
|
aiCommand?: string;
|
package/dist/config.js
CHANGED
|
@@ -233,6 +233,7 @@ export function loadConfig() {
|
|
|
233
233
|
const fileWechat = file.platforms?.wechat;
|
|
234
234
|
const fileWework = file.platforms?.wework;
|
|
235
235
|
const fileDingtalk = file.platforms?.dingtalk;
|
|
236
|
+
const fileWorkBuddy = file.platforms?.workbuddy;
|
|
236
237
|
// 1. 加载各平台凭证(env 优先,其次新结构,最后旧字段)
|
|
237
238
|
const telegramBotToken = process.env.TELEGRAM_BOT_TOKEN ??
|
|
238
239
|
fileTelegram?.botToken ??
|
|
@@ -276,6 +277,19 @@ export function loadConfig() {
|
|
|
276
277
|
fileDingtalk?.clientSecret;
|
|
277
278
|
const dingtalkCardTemplateId = process.env.DINGTALK_CARD_TEMPLATE_ID ??
|
|
278
279
|
fileDingtalk?.cardTemplateId;
|
|
280
|
+
// WorkBuddy credentials
|
|
281
|
+
const workbuddyAccessToken = process.env.WORKBUDDY_ACCESS_TOKEN ??
|
|
282
|
+
fileWorkBuddy?.accessToken;
|
|
283
|
+
const workbuddyRefreshToken = process.env.WORKBUDDY_REFRESH_TOKEN ??
|
|
284
|
+
fileWorkBuddy?.refreshToken;
|
|
285
|
+
const workbuddyUserId = process.env.WORKBUDDY_USER_ID ??
|
|
286
|
+
fileWorkBuddy?.userId;
|
|
287
|
+
const workbuddyBaseUrl = process.env.WORKBUDDY_BASE_URL ??
|
|
288
|
+
fileWorkBuddy?.baseUrl;
|
|
289
|
+
const workbuddyGuid = process.env.WORKBUDDY_GUID ??
|
|
290
|
+
fileWorkBuddy?.guid;
|
|
291
|
+
const workbuddyWorkspacePath = process.env.WORKBUDDY_WORKSPACE_PATH ??
|
|
292
|
+
fileWorkBuddy?.workspacePath;
|
|
279
293
|
// 2. 计算启用平台
|
|
280
294
|
const enabledPlatforms = [];
|
|
281
295
|
const telegramEnabledFlag = fileTelegram?.enabled;
|
|
@@ -284,6 +298,7 @@ export function loadConfig() {
|
|
|
284
298
|
const wechatEnabledFlag = fileWechat?.enabled;
|
|
285
299
|
const weworkEnabledFlag = fileWework?.enabled;
|
|
286
300
|
const dingtalkEnabledFlag = fileDingtalk?.enabled;
|
|
301
|
+
const workbuddyEnabledFlag = fileWorkBuddy?.enabled;
|
|
287
302
|
const telegramEnabled = !!telegramBotToken && (telegramEnabledFlag !== false);
|
|
288
303
|
const feishuEnabled = !!(feishuAppId && feishuAppSecret) && (feishuEnabledFlag !== false);
|
|
289
304
|
const qqEnabled = !!(qqAppId && qqSecret) && (qqEnabledFlag !== false);
|
|
@@ -294,6 +309,8 @@ export function loadConfig() {
|
|
|
294
309
|
// 企业微信只需要 corpId (botId) 和 secret
|
|
295
310
|
const weworkEnabled = !!(weworkCorpId && weworkSecret) && (weworkEnabledFlag !== false);
|
|
296
311
|
const dingtalkEnabled = !!(dingtalkClientId && dingtalkClientSecret) && (dingtalkEnabledFlag !== false);
|
|
312
|
+
// WorkBuddy 需要 OAuth 凭证
|
|
313
|
+
const workbuddyEnabled = !!(workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId) && (workbuddyEnabledFlag !== false);
|
|
297
314
|
if (telegramEnabled)
|
|
298
315
|
enabledPlatforms.push('telegram');
|
|
299
316
|
if (feishuEnabled)
|
|
@@ -306,6 +323,8 @@ export function loadConfig() {
|
|
|
306
323
|
enabledPlatforms.push('wework');
|
|
307
324
|
if (dingtalkEnabled)
|
|
308
325
|
enabledPlatforms.push('dingtalk');
|
|
326
|
+
if (workbuddyEnabled)
|
|
327
|
+
enabledPlatforms.push('workbuddy');
|
|
309
328
|
if (enabledPlatforms.length === 0) {
|
|
310
329
|
throw new Error('至少需要配置 Telegram、Feishu、WeChat、WeWork 或 DingTalk 其中一个平台(可以通过环境变量或 config.json)');
|
|
311
330
|
}
|
|
@@ -332,6 +351,9 @@ export function loadConfig() {
|
|
|
332
351
|
const dingtalkAllowedUserIds = process.env.DINGTALK_ALLOWED_USER_IDS !== undefined
|
|
333
352
|
? parseCommaSeparated(process.env.DINGTALK_ALLOWED_USER_IDS)
|
|
334
353
|
: fileDingtalk?.allowedUserIds ?? allowedUserIds;
|
|
354
|
+
const workbuddyAllowedUserIds = process.env.WORKBUDDY_ALLOWED_USER_IDS !== undefined
|
|
355
|
+
? parseCommaSeparated(process.env.WORKBUDDY_ALLOWED_USER_IDS)
|
|
356
|
+
: fileWorkBuddy?.allowedUserIds ?? allowedUserIds;
|
|
335
357
|
// 5. AI / 工作目录 / 安全配置(从 tools 读取)
|
|
336
358
|
const aiCommand = normalizeAiCommand(process.env.AI_COMMAND ?? file.aiCommand, 'claude');
|
|
337
359
|
const tc = file.tools?.claude ?? {};
|
|
@@ -579,6 +601,29 @@ export function loadConfig() {
|
|
|
579
601
|
allowedUserIds: dingtalkAllowedUserIds,
|
|
580
602
|
cardTemplateId: dingtalkCardTemplateId,
|
|
581
603
|
},
|
|
604
|
+
workbuddy: workbuddyEnabled
|
|
605
|
+
? {
|
|
606
|
+
enabled: true,
|
|
607
|
+
aiCommand: normalizeAiCommand(file.platforms?.workbuddy?.aiCommand, aiCommand),
|
|
608
|
+
allowedUserIds: workbuddyAllowedUserIds,
|
|
609
|
+
accessToken: workbuddyAccessToken,
|
|
610
|
+
refreshToken: workbuddyRefreshToken,
|
|
611
|
+
userId: workbuddyUserId,
|
|
612
|
+
baseUrl: workbuddyBaseUrl,
|
|
613
|
+
guid: workbuddyGuid,
|
|
614
|
+
workspacePath: workbuddyWorkspacePath,
|
|
615
|
+
}
|
|
616
|
+
: {
|
|
617
|
+
enabled: false,
|
|
618
|
+
aiCommand: normalizeAiCommand(file.platforms?.workbuddy?.aiCommand, aiCommand),
|
|
619
|
+
allowedUserIds: workbuddyAllowedUserIds,
|
|
620
|
+
accessToken: workbuddyAccessToken,
|
|
621
|
+
refreshToken: workbuddyRefreshToken,
|
|
622
|
+
userId: workbuddyUserId,
|
|
623
|
+
baseUrl: workbuddyBaseUrl,
|
|
624
|
+
guid: workbuddyGuid,
|
|
625
|
+
workspacePath: workbuddyWorkspacePath,
|
|
626
|
+
},
|
|
582
627
|
};
|
|
583
628
|
return {
|
|
584
629
|
enabledPlatforms,
|
|
@@ -608,6 +653,7 @@ export function loadConfig() {
|
|
|
608
653
|
wechatAllowedUserIds,
|
|
609
654
|
weworkAllowedUserIds,
|
|
610
655
|
dingtalkAllowedUserIds,
|
|
656
|
+
workbuddyAllowedUserIds,
|
|
611
657
|
aiCommand,
|
|
612
658
|
codexCliPath,
|
|
613
659
|
codebuddyCliPath,
|
package/dist/constants.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export declare const CARDKIT_THROTTLE_MS = 80;
|
|
|
11
11
|
export declare const TELEGRAM_THROTTLE_MS = 200;
|
|
12
12
|
/** WeChat 流式更新节流:1000ms(AGP 协议建议值) */
|
|
13
13
|
export declare const WECHAT_THROTTLE_MS = 1000;
|
|
14
|
+
/** WorkBuddy 流式更新节流:1000ms(Centrifuge 协议建议值) */
|
|
15
|
+
export declare const WORKBUDDY_THROTTLE_MS = 1000;
|
|
14
16
|
export declare const WEWORK_THROTTLE_MS = 500;
|
|
15
17
|
export declare const MAX_TELEGRAM_MESSAGE_LENGTH = 4000;
|
|
16
18
|
export declare const MAX_FEISHU_MESSAGE_LENGTH = 4000;
|
package/dist/constants.js
CHANGED
|
@@ -32,6 +32,8 @@ export const CARDKIT_THROTTLE_MS = 80;
|
|
|
32
32
|
export const TELEGRAM_THROTTLE_MS = 200;
|
|
33
33
|
/** WeChat 流式更新节流:1000ms(AGP 协议建议值) */
|
|
34
34
|
export const WECHAT_THROTTLE_MS = 1000;
|
|
35
|
+
/** WorkBuddy 流式更新节流:1000ms(Centrifuge 协议建议值) */
|
|
36
|
+
export const WORKBUDDY_THROTTLE_MS = 1000;
|
|
35
37
|
export const WEWORK_THROTTLE_MS = 500;
|
|
36
38
|
export const MAX_TELEGRAM_MESSAGE_LENGTH = 4000;
|
|
37
39
|
export const MAX_FEISHU_MESSAGE_LENGTH = 4000;
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,8 @@ import { setupWeWorkHandlers } from "./wework/event-handler.js";
|
|
|
23
23
|
import { sendProactiveTextReply as sendWeWorkTextReply } from "./wework/message-sender.js";
|
|
24
24
|
import { initDingTalk, stopDingTalk, formatDingTalkInitError } from "./dingtalk/client.js";
|
|
25
25
|
import { setupDingTalkHandlers } from "./dingtalk/event-handler.js";
|
|
26
|
+
import { initWorkBuddy, stopWorkBuddy } from "./workbuddy/client.js";
|
|
27
|
+
import { setupWorkBuddyHandlers } from "./workbuddy/event-handler.js";
|
|
26
28
|
import { initAdapters, cleanupAdapters } from "./adapters/registry.js";
|
|
27
29
|
import { SessionManager } from "./session/session-manager.js";
|
|
28
30
|
import { loadActiveChats, getActiveChatId, flushActiveChats, } from "./shared/active-chats.js";
|
|
@@ -34,8 +36,8 @@ const require = createRequire(import.meta.url);
|
|
|
34
36
|
const { version: APP_VERSION } = require("../package.json");
|
|
35
37
|
const log = createLogger("Main");
|
|
36
38
|
async function sendLifecycleNotification(platform, message) {
|
|
37
|
-
// DingTalk 不支持主动发消息(OpenAPI 需 robotCode 等,易报 robot 不存在),跳过启动/关闭通知
|
|
38
|
-
if (platform === "dingtalk")
|
|
39
|
+
// DingTalk 和 WorkBuddy 不支持主动发消息(OpenAPI 需 robotCode 等,易报 robot 不存在),跳过启动/关闭通知
|
|
40
|
+
if (platform === "dingtalk" || platform === "workbuddy")
|
|
39
41
|
return;
|
|
40
42
|
const telegramChatId = getActiveChatId("telegram");
|
|
41
43
|
const feishuChatId = getActiveChatId("feishu");
|
|
@@ -174,6 +176,7 @@ export async function main() {
|
|
|
174
176
|
let wechatHandle = null;
|
|
175
177
|
let weworkHandle = null;
|
|
176
178
|
let dingtalkHandle = null;
|
|
179
|
+
let workbuddyHandle = null;
|
|
177
180
|
// Track successfully initialized platforms
|
|
178
181
|
const successfulPlatforms = [];
|
|
179
182
|
if (config.enabledPlatforms.includes("telegram")) {
|
|
@@ -237,6 +240,16 @@ export async function main() {
|
|
|
237
240
|
log.error("Failed to initialize DingTalk:", formatDingTalkInitError(err));
|
|
238
241
|
}
|
|
239
242
|
}
|
|
243
|
+
if (config.enabledPlatforms.includes("workbuddy")) {
|
|
244
|
+
try {
|
|
245
|
+
workbuddyHandle = setupWorkBuddyHandlers(config, sessionManager);
|
|
246
|
+
await initWorkBuddy(config, workbuddyHandle.handleEvent);
|
|
247
|
+
successfulPlatforms.push("workbuddy");
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
log.error("Failed to initialize WorkBuddy:", err);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
240
253
|
// Require at least one platform to start successfully
|
|
241
254
|
if (successfulPlatforms.length === 0) {
|
|
242
255
|
throw new Error("No platforms initialized successfully. Service cannot start.");
|
|
@@ -283,6 +296,8 @@ export async function main() {
|
|
|
283
296
|
stopWeWork();
|
|
284
297
|
dingtalkHandle?.stop();
|
|
285
298
|
stopDingTalk();
|
|
299
|
+
workbuddyHandle?.stop();
|
|
300
|
+
stopWorkBuddy();
|
|
286
301
|
sessionManager.destroy();
|
|
287
302
|
cleanupAdapters();
|
|
288
303
|
flushActiveChats();
|
package/dist/setup.js
CHANGED
|
@@ -940,8 +940,9 @@ const PLATFORM_LABELS = {
|
|
|
940
940
|
wework: "企业微信",
|
|
941
941
|
dingtalk: "钉钉",
|
|
942
942
|
wechat: "微信(测试中)",
|
|
943
|
+
workbuddy: "WorkBuddy",
|
|
943
944
|
};
|
|
944
|
-
const ALL_PLATFORMS = ["telegram", "feishu", "qq", "wework", "dingtalk", "wechat"];
|
|
945
|
+
const ALL_PLATFORMS = ["telegram", "feishu", "qq", "wework", "dingtalk", "wechat", "workbuddy"];
|
|
945
946
|
/**
|
|
946
947
|
* 启动时让用户选择要启用的平台(无论单通道还是多通道)
|
|
947
948
|
* 显示全部 4 个平台,已配置的预选;若用户选择未配置的,引导运行 init
|
|
@@ -6,8 +6,8 @@ export interface DingTalkActiveTarget {
|
|
|
6
6
|
updatedAt: number;
|
|
7
7
|
}
|
|
8
8
|
export declare function loadActiveChats(): void;
|
|
9
|
-
export declare function getActiveChatId(platform: 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wechat' | 'wework'): string | undefined;
|
|
10
|
-
export declare function setActiveChatId(platform: 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wechat' | 'wework', chatId: string): void;
|
|
9
|
+
export declare function getActiveChatId(platform: 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wechat' | 'wework' | 'workbuddy'): string | undefined;
|
|
10
|
+
export declare function setActiveChatId(platform: 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wechat' | 'wework' | 'workbuddy', chatId: string): void;
|
|
11
11
|
export declare function getDingTalkActiveTarget(): DingTalkActiveTarget | undefined;
|
|
12
12
|
export declare function setDingTalkActiveTarget(target: Omit<DingTalkActiveTarget, 'updatedAt'>): void;
|
|
13
13
|
export declare function flushActiveChats(): void;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkBuddy Centrifuge Client - WebSocket connection for WeChat KF messages
|
|
3
|
+
*/
|
|
4
|
+
import type { WorkBuddyState, PromptResponsePayload } from './types.js';
|
|
5
|
+
/** Centrifuge client configuration */
|
|
6
|
+
export interface CentrifugeClientConfig {
|
|
7
|
+
url: string;
|
|
8
|
+
connectionToken: string;
|
|
9
|
+
subscriptionToken: string;
|
|
10
|
+
channel: string;
|
|
11
|
+
guid: string;
|
|
12
|
+
userId: string;
|
|
13
|
+
httpBaseUrl?: string;
|
|
14
|
+
httpAccessToken?: string;
|
|
15
|
+
workspaceSessionId?: string;
|
|
16
|
+
}
|
|
17
|
+
/** Client callbacks */
|
|
18
|
+
export interface CentrifugeCallbacks {
|
|
19
|
+
onConnected?: () => void;
|
|
20
|
+
onDisconnected?: (reason?: string) => void;
|
|
21
|
+
onError?: (error: Error) => void;
|
|
22
|
+
onMessage?: (chatId: string, msgId: string, content: string) => void;
|
|
23
|
+
}
|
|
24
|
+
export declare class WorkBuddyCentrifugeClient {
|
|
25
|
+
private config;
|
|
26
|
+
private callbacks;
|
|
27
|
+
private client;
|
|
28
|
+
private sub;
|
|
29
|
+
private extraSubs;
|
|
30
|
+
private state;
|
|
31
|
+
private processedMsgIds;
|
|
32
|
+
private static readonly MAX_MSG_ID_CACHE;
|
|
33
|
+
constructor(config: CentrifugeClientConfig, callbacks?: CentrifugeCallbacks);
|
|
34
|
+
get logPrefix(): string;
|
|
35
|
+
getState(): WorkBuddyState;
|
|
36
|
+
start(): void;
|
|
37
|
+
stop(): void;
|
|
38
|
+
setCallbacks(callbacks: Partial<CentrifugeCallbacks>): void;
|
|
39
|
+
/**
|
|
40
|
+
* Subscribe to additional channel
|
|
41
|
+
*/
|
|
42
|
+
subscribeChannel(channel: string, subscriptionToken: string): void;
|
|
43
|
+
/**
|
|
44
|
+
* Send message chunk through Centrifuge
|
|
45
|
+
*/
|
|
46
|
+
sendMessageChunk(sessionId: string, promptId: string, content: {
|
|
47
|
+
type: string;
|
|
48
|
+
text?: string;
|
|
49
|
+
}, guid?: string, userId?: string): void;
|
|
50
|
+
/**
|
|
51
|
+
* Send tool call through Centrifuge
|
|
52
|
+
*/
|
|
53
|
+
sendToolCall(sessionId: string, promptId: string, toolCall: {
|
|
54
|
+
id: string;
|
|
55
|
+
name: string;
|
|
56
|
+
input?: Record<string, unknown>;
|
|
57
|
+
}, guid?: string, userId?: string): void;
|
|
58
|
+
/**
|
|
59
|
+
* Send prompt response (for WeChat KF, use HTTP instead)
|
|
60
|
+
*/
|
|
61
|
+
sendPromptResponse(payload: PromptResponsePayload, _guid?: string, _userId?: string): void;
|
|
62
|
+
/**
|
|
63
|
+
* Handle incoming publication from Centrifuge
|
|
64
|
+
*/
|
|
65
|
+
private handlePublication;
|
|
66
|
+
/**
|
|
67
|
+
* Send AGP envelope through Centrifuge
|
|
68
|
+
*/
|
|
69
|
+
private sendEnvelope;
|
|
70
|
+
/**
|
|
71
|
+
* Clean up old message IDs from cache
|
|
72
|
+
*/
|
|
73
|
+
private cleanMsgIdCache;
|
|
74
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkBuddy Centrifuge Client - WebSocket connection for WeChat KF messages
|
|
3
|
+
*/
|
|
4
|
+
import { Centrifuge } from 'centrifuge';
|
|
5
|
+
import { WebSocket } from 'ws';
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import { createLogger } from '../logger.js';
|
|
8
|
+
const log = createLogger('WorkBuddyCentrifuge');
|
|
9
|
+
export class WorkBuddyCentrifugeClient {
|
|
10
|
+
config;
|
|
11
|
+
callbacks;
|
|
12
|
+
client = null;
|
|
13
|
+
sub = null;
|
|
14
|
+
extraSubs = [];
|
|
15
|
+
state = 'disconnected';
|
|
16
|
+
processedMsgIds = new Set();
|
|
17
|
+
static MAX_MSG_ID_CACHE = 1000;
|
|
18
|
+
constructor(config, callbacks = {}) {
|
|
19
|
+
this.config = config;
|
|
20
|
+
this.callbacks = callbacks;
|
|
21
|
+
}
|
|
22
|
+
get logPrefix() {
|
|
23
|
+
return `[workbuddy:${this.config.userId}]`;
|
|
24
|
+
}
|
|
25
|
+
getState() {
|
|
26
|
+
return this.state;
|
|
27
|
+
}
|
|
28
|
+
start() {
|
|
29
|
+
if (this.state === 'connected' || this.state === 'connecting') {
|
|
30
|
+
log.info(`${this.logPrefix} Already connected or connecting`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
this.state = 'connecting';
|
|
34
|
+
log.info(`${this.logPrefix} Connecting to: ${this.config.url}, channel=${this.config.channel}`);
|
|
35
|
+
this.client = new Centrifuge(this.config.url, {
|
|
36
|
+
token: this.config.connectionToken,
|
|
37
|
+
websocket: WebSocket,
|
|
38
|
+
});
|
|
39
|
+
this.client.on('connected', (ctx) => {
|
|
40
|
+
log.info(`${this.logPrefix} Connected (transport=${ctx.transport})`);
|
|
41
|
+
this.state = 'connected';
|
|
42
|
+
this.callbacks.onConnected?.();
|
|
43
|
+
});
|
|
44
|
+
this.client.on('disconnected', (ctx) => {
|
|
45
|
+
log.info(`${this.logPrefix} Disconnected: code=${ctx.code}, reason=${ctx.reason}`);
|
|
46
|
+
if (this.state !== 'disconnected') {
|
|
47
|
+
this.state = 'disconnected';
|
|
48
|
+
this.callbacks.onDisconnected?.(ctx.reason || `code=${ctx.code}`);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
this.client.on('connecting', (ctx) => {
|
|
52
|
+
log.info(`${this.logPrefix} Reconnecting: code=${ctx.code}, reason=${ctx.reason}`);
|
|
53
|
+
if (this.state === 'connected') {
|
|
54
|
+
this.state = 'reconnecting';
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
this.client.on('error', (ctx) => {
|
|
58
|
+
log.error(`${this.logPrefix} Error: ${ctx.error.message}`);
|
|
59
|
+
this.callbacks.onError?.(new Error(ctx.error.message));
|
|
60
|
+
});
|
|
61
|
+
// Create channel subscription
|
|
62
|
+
this.sub = this.client.newSubscription(this.config.channel, {
|
|
63
|
+
token: this.config.subscriptionToken,
|
|
64
|
+
});
|
|
65
|
+
this.sub.on('publication', (ctx) => {
|
|
66
|
+
this.handlePublication(ctx.data);
|
|
67
|
+
});
|
|
68
|
+
this.sub.on('error', (ctx) => {
|
|
69
|
+
log.error(`${this.logPrefix} Subscription error: ${ctx.error.message}`);
|
|
70
|
+
});
|
|
71
|
+
this.sub.subscribe();
|
|
72
|
+
this.client.connect();
|
|
73
|
+
}
|
|
74
|
+
stop() {
|
|
75
|
+
log.info(`${this.logPrefix} Stopping...`);
|
|
76
|
+
this.state = 'disconnected';
|
|
77
|
+
this.processedMsgIds.clear();
|
|
78
|
+
for (const sub of this.extraSubs) {
|
|
79
|
+
sub.unsubscribe();
|
|
80
|
+
}
|
|
81
|
+
this.extraSubs = [];
|
|
82
|
+
if (this.sub) {
|
|
83
|
+
this.sub.unsubscribe();
|
|
84
|
+
this.sub = null;
|
|
85
|
+
}
|
|
86
|
+
if (this.client) {
|
|
87
|
+
this.client.disconnect();
|
|
88
|
+
this.client = null;
|
|
89
|
+
}
|
|
90
|
+
log.info(`${this.logPrefix} Stopped`);
|
|
91
|
+
}
|
|
92
|
+
setCallbacks(callbacks) {
|
|
93
|
+
this.callbacks = { ...this.callbacks, ...callbacks };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Subscribe to additional channel
|
|
97
|
+
*/
|
|
98
|
+
subscribeChannel(channel, subscriptionToken) {
|
|
99
|
+
if (!this.client) {
|
|
100
|
+
log.warn(`${this.logPrefix} Cannot subscribe: client not initialized`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
log.info(`${this.logPrefix} Subscribing to additional channel: ${channel}`);
|
|
104
|
+
const sub = this.client.newSubscription(channel, { token: subscriptionToken });
|
|
105
|
+
sub.on('publication', (ctx) => {
|
|
106
|
+
this.handlePublication(ctx.data);
|
|
107
|
+
});
|
|
108
|
+
sub.on('error', (ctx) => {
|
|
109
|
+
log.error(`${this.logPrefix} Extra subscription error (${channel}): ${ctx.error.message}`);
|
|
110
|
+
});
|
|
111
|
+
sub.on('subscribed', () => {
|
|
112
|
+
log.info(`${this.logPrefix} Extra channel subscribed: ${channel}`);
|
|
113
|
+
});
|
|
114
|
+
this.extraSubs.push(sub);
|
|
115
|
+
sub.subscribe();
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Send message chunk through Centrifuge
|
|
119
|
+
*/
|
|
120
|
+
sendMessageChunk(sessionId, promptId, content, guid, userId) {
|
|
121
|
+
const payload = {
|
|
122
|
+
session_id: sessionId,
|
|
123
|
+
prompt_id: promptId,
|
|
124
|
+
update_type: 'message_chunk',
|
|
125
|
+
content: [content],
|
|
126
|
+
};
|
|
127
|
+
this.sendEnvelope('session.update', payload, guid, userId);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Send tool call through Centrifuge
|
|
131
|
+
*/
|
|
132
|
+
sendToolCall(sessionId, promptId, toolCall, guid, userId) {
|
|
133
|
+
const payload = {
|
|
134
|
+
session_id: sessionId,
|
|
135
|
+
prompt_id: promptId,
|
|
136
|
+
update_type: 'tool_call',
|
|
137
|
+
tool_call: toolCall,
|
|
138
|
+
};
|
|
139
|
+
this.sendEnvelope('session.update', payload, guid, userId);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Send prompt response (for WeChat KF, use HTTP instead)
|
|
143
|
+
*/
|
|
144
|
+
sendPromptResponse(payload, _guid, _userId) {
|
|
145
|
+
// WeChat KF messages: send via HTTP COPILOT_RESPONSE
|
|
146
|
+
if (this.config.httpBaseUrl && this.config.httpAccessToken) {
|
|
147
|
+
const message = payload.content?.map((c) => c.text).join('') || payload.error || '';
|
|
148
|
+
const httpPayload = {
|
|
149
|
+
type: 'COPILOT_RESPONSE',
|
|
150
|
+
msgId: payload.prompt_id,
|
|
151
|
+
chatId: payload.session_id,
|
|
152
|
+
success: payload.stop_reason === 'end_turn',
|
|
153
|
+
message,
|
|
154
|
+
metadata: {
|
|
155
|
+
sessionId: this.config.workspaceSessionId || payload.session_id,
|
|
156
|
+
requestId: payload.prompt_id,
|
|
157
|
+
state: payload.stop_reason === 'end_turn' ? 'completed' : payload.stop_reason,
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
const url = `${this.config.httpBaseUrl}/v2/backgroundagent/wecom/local-proxy/receive`;
|
|
161
|
+
fetch(url, {
|
|
162
|
+
method: 'POST',
|
|
163
|
+
headers: {
|
|
164
|
+
'Content-Type': 'application/json',
|
|
165
|
+
Authorization: `Bearer ${this.config.httpAccessToken}`,
|
|
166
|
+
},
|
|
167
|
+
body: JSON.stringify(httpPayload),
|
|
168
|
+
signal: AbortSignal.timeout(30_000),
|
|
169
|
+
})
|
|
170
|
+
.then(async (res) => {
|
|
171
|
+
if (!res.ok) {
|
|
172
|
+
const body = await res.text().catch(() => '');
|
|
173
|
+
log.error(`${this.logPrefix} HTTP COPILOT_RESPONSE failed: ${res.status} ${body.substring(0, 200)}`);
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
.catch((err) => {
|
|
177
|
+
log.error(`${this.logPrefix} HTTP COPILOT_RESPONSE error:`, err);
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
this.sendEnvelope('session.promptResponse', payload, _guid, _userId);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Handle incoming publication from Centrifuge
|
|
185
|
+
*/
|
|
186
|
+
handlePublication(data) {
|
|
187
|
+
try {
|
|
188
|
+
const raw = data;
|
|
189
|
+
// AGP format message (from QClaw gateway)
|
|
190
|
+
if (raw?.method && raw?.msg_id) {
|
|
191
|
+
const envelope = raw;
|
|
192
|
+
if (this.processedMsgIds.has(envelope.msg_id)) {
|
|
193
|
+
log.debug(`${this.logPrefix} Duplicate message, skipping: ${envelope.msg_id}`);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
this.processedMsgIds.add(envelope.msg_id);
|
|
197
|
+
this.cleanMsgIdCache();
|
|
198
|
+
log.debug(`${this.logPrefix} Received AGP message: method=${envelope.method}, msg_id=${envelope.msg_id}`);
|
|
199
|
+
if (envelope.method === 'session.prompt') {
|
|
200
|
+
const payload = envelope.payload;
|
|
201
|
+
const content = payload.content?.find((c) => c.type === 'text')?.text || '';
|
|
202
|
+
this.callbacks.onMessage?.(payload.session_id, envelope.msg_id, content);
|
|
203
|
+
}
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// WeChat KF format message (from WorkBuddy Centrifuge)
|
|
207
|
+
if (raw?.chatId && raw?.msgId) {
|
|
208
|
+
const msgId = String(raw.msgId);
|
|
209
|
+
if (this.processedMsgIds.has(msgId)) {
|
|
210
|
+
log.debug(`${this.logPrefix} Duplicate message, skipping: ${msgId}`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
this.processedMsgIds.add(msgId);
|
|
214
|
+
this.cleanMsgIdCache();
|
|
215
|
+
const content = String(raw.content ?? '');
|
|
216
|
+
const chatId = String(raw.chatId);
|
|
217
|
+
log.info(`${this.logPrefix} Received WeChat KF message: msgId=${msgId}, chatId=${chatId}, content=${content.substring(0, 50)}`);
|
|
218
|
+
this.callbacks.onMessage?.(chatId, msgId, content);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const preview = JSON.stringify(data).substring(0, 500);
|
|
222
|
+
log.warn(`${this.logPrefix} Unknown message format: ${preview}`);
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
log.error(`${this.logPrefix} Message handling failed:`, error);
|
|
226
|
+
this.callbacks.onError?.(error instanceof Error ? error : new Error(`Message handling failed: ${String(error)}`));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Send AGP envelope through Centrifuge
|
|
231
|
+
*/
|
|
232
|
+
sendEnvelope(method, payload, guid, userId) {
|
|
233
|
+
if (!this.client || this.state !== 'connected') {
|
|
234
|
+
log.warn(`${this.logPrefix} Cannot send message, state: ${this.state}`);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const envelope = {
|
|
238
|
+
msg_id: randomUUID(),
|
|
239
|
+
guid: guid ?? this.config.guid,
|
|
240
|
+
user_id: userId ?? this.config.userId,
|
|
241
|
+
method: method,
|
|
242
|
+
payload,
|
|
243
|
+
};
|
|
244
|
+
try {
|
|
245
|
+
this.client.publish(this.config.channel, envelope).catch((err) => {
|
|
246
|
+
log.error(`${this.logPrefix} Message send failed:`, err);
|
|
247
|
+
this.callbacks.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
248
|
+
});
|
|
249
|
+
const json = JSON.stringify(envelope);
|
|
250
|
+
const preview = json.length > 500 ? json.substring(0, 500) + `...(truncated)` : json;
|
|
251
|
+
log.debug(`${this.logPrefix} Sent message: method=${method}, msg_id=${envelope.msg_id}`);
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
log.error(`${this.logPrefix} Message send failed:`, error);
|
|
255
|
+
this.callbacks.onError?.(error instanceof Error ? error : new Error(`Message send failed: ${String(error)}`));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Clean up old message IDs from cache
|
|
260
|
+
*/
|
|
261
|
+
cleanMsgIdCache() {
|
|
262
|
+
if (this.processedMsgIds.size > WorkBuddyCentrifugeClient.MAX_MSG_ID_CACHE) {
|
|
263
|
+
const entries = [...this.processedMsgIds];
|
|
264
|
+
this.processedMsgIds.clear();
|
|
265
|
+
entries
|
|
266
|
+
.slice(-WorkBuddyCentrifugeClient.MAX_MSG_ID_CACHE / 2)
|
|
267
|
+
.forEach((id) => {
|
|
268
|
+
this.processedMsgIds.add(id);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|