@wu529778790/open-im 1.8.1-beta.2 → 1.8.1-beta.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/access/access-control.js +1 -1
  2. package/dist/adapters/claude-sdk-adapter.js +94 -36
  3. package/dist/channels/capabilities.js +5 -0
  4. package/dist/cli.js +5 -2
  5. package/dist/commands/handler.d.ts +1 -2
  6. package/dist/commands/handler.js +6 -18
  7. package/dist/config-web-page-i18n.d.ts +12 -0
  8. package/dist/config-web-page-i18n.js +12 -0
  9. package/dist/config-web-page-script.js +1 -0
  10. package/dist/config-web-page-template.js +48 -1
  11. package/dist/config-web.js +110 -7
  12. package/dist/config.d.ts +25 -1
  13. package/dist/config.js +46 -0
  14. package/dist/constants.d.ts +2 -0
  15. package/dist/constants.js +2 -0
  16. package/dist/dingtalk/client.js +11 -3
  17. package/dist/dingtalk/event-handler.js +18 -3
  18. package/dist/dingtalk/message-sender.js +13 -0
  19. package/dist/feishu/event-handler.js +144 -10
  20. package/dist/index.js +26 -2
  21. package/dist/manager-control.js +7 -0
  22. package/dist/qq/client.js +111 -88
  23. package/dist/qq/event-handler.js +16 -2
  24. package/dist/qq/message-sender.js +11 -0
  25. package/dist/service-control.js +4 -0
  26. package/dist/session/session-manager.js +11 -1
  27. package/dist/setup.js +2 -1
  28. package/dist/shared/active-chats.d.ts +2 -2
  29. package/dist/shared/ai-task.js +13 -1
  30. package/dist/shared/chat-user-map.js +11 -0
  31. package/dist/shared/media-storage.js +27 -0
  32. package/dist/telegram/client.js +25 -3
  33. package/dist/telegram/event-handler.js +44 -8
  34. package/dist/telegram/message-sender.js +13 -0
  35. package/dist/wechat/auth/qclaw-api.js +1 -1
  36. package/dist/wechat/client.js +81 -4
  37. package/dist/wechat/event-handler.js +10 -3
  38. package/dist/wework/client.js +36 -14
  39. package/dist/wework/event-handler.js +39 -4
  40. package/dist/wework/message-sender.js +53 -21
  41. package/dist/workbuddy/centrifuge-client.d.ts +74 -0
  42. package/dist/workbuddy/centrifuge-client.js +272 -0
  43. package/dist/workbuddy/client.d.ts +27 -0
  44. package/dist/workbuddy/client.js +162 -0
  45. package/dist/workbuddy/event-handler.d.ts +11 -0
  46. package/dist/workbuddy/event-handler.js +118 -0
  47. package/dist/workbuddy/index.d.ts +8 -0
  48. package/dist/workbuddy/index.js +8 -0
  49. package/dist/workbuddy/message-sender.d.ts +16 -0
  50. package/dist/workbuddy/message-sender.js +51 -0
  51. package/dist/workbuddy/oauth.d.ts +114 -0
  52. package/dist/workbuddy/oauth.js +310 -0
  53. package/dist/workbuddy/types.d.ts +86 -0
  54. package/dist/workbuddy/types.js +4 -0
  55. package/package.json +4 -2
@@ -101,6 +101,18 @@ function formatWeWorkMessage(title, content, status, note) {
101
101
  return message;
102
102
  }
103
103
  const streamStates = new Map();
104
+ // Periodic cleanup of expired/orphaned stream states
105
+ const STREAM_CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
106
+ setInterval(() => {
107
+ const now = Date.now();
108
+ for (const [id, state] of streamStates) {
109
+ if (now - state.createdAt >= STREAM_SAFE_TTL_MS) {
110
+ state.closed = true;
111
+ streamStates.delete(id);
112
+ log.info(`Cleaned up expired stream state: ${id}`);
113
+ }
114
+ }
115
+ }, STREAM_CLEANUP_INTERVAL_MS);
104
116
  function sleep(ms) {
105
117
  return new Promise((resolve) => setTimeout(resolve, ms));
106
118
  }
@@ -187,6 +199,12 @@ export async function updateMessage(chatId, streamId, content, status, note, too
187
199
  return;
188
200
  if (Date.now() - state.createdAt >= STREAM_SAFE_TTL_MS) {
189
201
  markExpired(state, streamId);
202
+ // Stream expired - fall back to text delivery for errors and final states
203
+ if (status === 'error' || status === 'done') {
204
+ const reqIdUsed = getReqId(reqId);
205
+ sendText(reqIdUsed, message);
206
+ log.info(`Stream expired, sent ${status} via text fallback: streamId=${streamId}`);
207
+ }
190
208
  return;
191
209
  }
192
210
  state.pendingUpdate = { message, status, reqId };
@@ -207,17 +225,24 @@ export async function sendFinalMessages(chatId, streamId, fullContent, note, too
207
225
  const title = getToolTitle(toolId, 'done');
208
226
  const parts = splitLongContent(contentToSend, MAX_WEWORK_MESSAGE_LENGTH);
209
227
  const finalMessage = formatWeWorkMessage(title, parts[0], 'done', parts.length > 1 ? `内容较长,已分段发送 (1/${parts.length})` : note);
210
- try {
211
- const state = streamStates.get(streamId);
212
- const shouldFallbackToText = !!state && (state.expired || Date.now() - state.createdAt >= STREAM_SAFE_TTL_MS);
213
- if (!shouldFallbackToText && state && contentToSend.length > 0) {
214
- // 先发一条「输出中」带正文,再发 finish 的最终条,避免企微端一直停在「思考中」不刷新
228
+ const state = streamStates.get(streamId);
229
+ const shouldFallbackToText = !!state && (state.expired || Date.now() - state.createdAt >= STREAM_SAFE_TTL_MS);
230
+ // 先发一条「输出中」带正文,再发 finish 的最终条,避免企微端一直停在「思考中」不刷新
231
+ // 独立 try-catch:即使此步失败,仍须发送 finish=true,否则企微永远卡在「正在思考」
232
+ if (!shouldFallbackToText && state && contentToSend.length > 0) {
233
+ try {
215
234
  await updateMessage(chatId, streamId, contentToSend, 'streaming', note, toolId, reqId);
216
235
  }
217
- if (state) {
218
- state.closed = true;
219
- state.pendingUpdate = undefined;
236
+ catch (err) {
237
+ log.warn('Pre-finish streaming update failed, will still send finish=true:', err);
220
238
  }
239
+ }
240
+ if (state) {
241
+ state.closed = true;
242
+ state.pendingUpdate = undefined;
243
+ }
244
+ // finish=true 是关键:必须保证发出,否则企微 UI 永远停留在「正在思考」
245
+ try {
221
246
  if (!shouldFallbackToText) {
222
247
  if (state) {
223
248
  const elapsed = Date.now() - state.lastSentAt;
@@ -232,21 +257,28 @@ export async function sendFinalMessages(chatId, streamId, fullContent, note, too
232
257
  sendText(getReqId(reqId), finalMessage);
233
258
  log.info(`Final stream expired, sent text fallback instead: streamId=${streamId}`);
234
259
  }
235
- streamStates.delete(streamId);
236
- for (let i = 1; i < parts.length; i++) {
237
- try {
238
- const partContent = `${parts[i]}\n\n_*(续 ${i + 1}/${parts.length})*_`;
239
- const partMessage = formatWeWorkMessage(title, partContent, 'done', i === parts.length - 1 ? note : undefined);
240
- sendText(getReqId(reqId), partMessage);
241
- log.info(`Final message part ${i + 1}/${parts.length} sent`);
242
- }
243
- catch (err) {
244
- log.error(`Failed to send part ${i + 1}:`, err);
245
- }
246
- }
247
260
  }
248
261
  catch (err) {
249
- log.error('Failed to send final messages:', err);
262
+ log.warn('Primary finish send failed, trying text fallback:', err);
263
+ try {
264
+ sendText(getReqId(reqId), finalMessage);
265
+ log.info(`Fallback text sent after primary finish failure, streamId=${streamId}`);
266
+ }
267
+ catch (fallbackErr) {
268
+ log.error('Both primary and fallback finish sends failed:', fallbackErr);
269
+ }
270
+ }
271
+ streamStates.delete(streamId);
272
+ for (let i = 1; i < parts.length; i++) {
273
+ try {
274
+ const partContent = `${parts[i]}\n\n_*(续 ${i + 1}/${parts.length})*_`;
275
+ const partMessage = formatWeWorkMessage(title, partContent, 'done', i === parts.length - 1 ? note : undefined);
276
+ sendText(getReqId(reqId), partMessage);
277
+ log.info(`Final message part ${i + 1}/${parts.length} sent`);
278
+ }
279
+ catch (err) {
280
+ log.error(`Failed to send part ${i + 1}:`, err);
281
+ }
250
282
  }
251
283
  }
252
284
  /**
@@ -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
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * WorkBuddy Client - Main client for WorkBuddy WeChat integration
3
+ */
4
+ import type { Config } from '../config.js';
5
+ import { WorkBuddyOAuth } from './oauth.js';
6
+ import { WorkBuddyCentrifugeClient } from './centrifuge-client.js';
7
+ import type { WorkBuddyState } from './types.js';
8
+ /**
9
+ * Get current channel state
10
+ */
11
+ export declare function getChannelState(): WorkBuddyState;
12
+ /**
13
+ * Initialize WorkBuddy client with CodeBuddy OAuth and Centrifuge WebSocket
14
+ */
15
+ export declare function initWorkBuddy(config: Config, eventHandler: (chatId: string, msgId: string, content: string) => Promise<void>, onStateChange?: (state: WorkBuddyState) => void): Promise<void>;
16
+ /**
17
+ * Get Centrifuge client for sending messages
18
+ */
19
+ export declare function getCentrifugeClient(): WorkBuddyCentrifugeClient | null;
20
+ /**
21
+ * Get OAuth client
22
+ */
23
+ export declare function getOAuth(): WorkBuddyOAuth | null;
24
+ /**
25
+ * Stop WorkBuddy client
26
+ */
27
+ export declare function stopWorkBuddy(): void;
@@ -0,0 +1,162 @@
1
+ /**
2
+ * WorkBuddy Client - Main client for WorkBuddy WeChat integration
3
+ */
4
+ import { randomUUID } from 'node:crypto';
5
+ import { existsSync, mkdirSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { createLogger } from '../logger.js';
8
+ import { WorkBuddyOAuth } from './oauth.js';
9
+ import { WorkBuddyCentrifugeClient } from './centrifuge-client.js';
10
+ const log = createLogger('WorkBuddy');
11
+ const CREDENTIALS_FILE = 'workbuddy-credentials.json';
12
+ const DEFAULT_BASE_URL = 'https://copilot.tencent.com';
13
+ const DEFAULT_WORKSPACE_ID = 'open-im-workspace';
14
+ const DEFAULT_WORKSPACE_NAME = 'OpenIM Workspace';
15
+ // Global state
16
+ let oauth = null;
17
+ let centrifugeClient = null;
18
+ let channelState = 'disconnected';
19
+ let credentialsPath = null;
20
+ let currentSessionId = null;
21
+ // Event handlers
22
+ let messageHandler = null;
23
+ let stateChangeHandler = null;
24
+ /**
25
+ * Get current channel state
26
+ */
27
+ export function getChannelState() {
28
+ return channelState;
29
+ }
30
+ /**
31
+ * Initialize WorkBuddy client with CodeBuddy OAuth and Centrifuge WebSocket
32
+ */
33
+ export async function initWorkBuddy(config, eventHandler, onStateChange) {
34
+ const platformConfig = config.platforms?.workbuddy;
35
+ if (!platformConfig?.enabled) {
36
+ throw new Error('WorkBuddy platform not enabled');
37
+ }
38
+ // Check credentials
39
+ const hasCredentials = platformConfig.accessToken && platformConfig.refreshToken && platformConfig.userId;
40
+ if (!hasCredentials) {
41
+ throw new Error('WorkBuddy credentials required: accessToken, refreshToken, userId');
42
+ }
43
+ log.info('Initializing WorkBuddy client...');
44
+ messageHandler = eventHandler;
45
+ stateChangeHandler = onStateChange ?? null;
46
+ // Set up credentials storage path
47
+ const baseDir = config.logDir ?? join(process.env.HOME ?? '', '.open-im');
48
+ credentialsPath = join(baseDir, 'data');
49
+ if (!existsSync(credentialsPath)) {
50
+ mkdirSync(credentialsPath, { recursive: true });
51
+ }
52
+ // Initialize OAuth client
53
+ const baseUrl = platformConfig.baseUrl || DEFAULT_BASE_URL;
54
+ oauth = new WorkBuddyOAuth(baseUrl);
55
+ oauth.loadCredentials({
56
+ accessToken: platformConfig.accessToken,
57
+ refreshToken: platformConfig.refreshToken,
58
+ userId: platformConfig.userId,
59
+ });
60
+ // Build session ID
61
+ currentSessionId = oauth.buildSessionId(platformConfig.workspacePath);
62
+ log.info(`WorkBuddy sessionId: ${currentSessionId ?? ''}`);
63
+ // Register workspace to get Centrifuge tokens
64
+ let centrifugeTokens;
65
+ try {
66
+ centrifugeTokens = await oauth.registerWorkspace({
67
+ userId: platformConfig.userId || '',
68
+ hostId: hostname(),
69
+ workspaceId: DEFAULT_WORKSPACE_ID,
70
+ workspaceName: DEFAULT_WORKSPACE_NAME,
71
+ });
72
+ log.info(`Registered workspace: channel=${centrifugeTokens.channel}`);
73
+ }
74
+ catch (err) {
75
+ log.error('Failed to register workspace:', err);
76
+ throw new Error(`WorkBuddy workspace registration failed: ${err}`);
77
+ }
78
+ // Generate GUID for this instance
79
+ const guid = platformConfig.guid || randomUUID();
80
+ const workspaceSessionId = currentSessionId || '';
81
+ // Create Centrifuge client
82
+ const callbacks = {
83
+ onConnected: () => {
84
+ log.info('WorkBuddy Centrifuge connected');
85
+ updateState('connected');
86
+ },
87
+ onDisconnected: (reason) => {
88
+ log.info(`WorkBuddy Centrifuge disconnected: ${reason}`);
89
+ updateState('disconnected');
90
+ },
91
+ onError: (error) => {
92
+ log.error('WorkBuddy Centrifuge error:', error);
93
+ updateState('error');
94
+ },
95
+ onMessage: async (chatId, msgId, content) => {
96
+ if (messageHandler) {
97
+ try {
98
+ await messageHandler(chatId, msgId, content);
99
+ }
100
+ catch (err) {
101
+ log.error('Error in message handler:', err);
102
+ }
103
+ }
104
+ },
105
+ };
106
+ centrifugeClient = new WorkBuddyCentrifugeClient({
107
+ url: centrifugeTokens.url,
108
+ connectionToken: centrifugeTokens.connectionToken,
109
+ subscriptionToken: centrifugeTokens.subscriptionToken,
110
+ channel: centrifugeTokens.channel,
111
+ guid,
112
+ userId: platformConfig.userId || '',
113
+ httpBaseUrl: baseUrl,
114
+ httpAccessToken: platformConfig.accessToken || '',
115
+ workspaceSessionId,
116
+ }, callbacks);
117
+ // Start Centrifuge client
118
+ centrifugeClient.start();
119
+ log.info('WorkBuddy client initialized');
120
+ }
121
+ /**
122
+ * Get Centrifuge client for sending messages
123
+ */
124
+ export function getCentrifugeClient() {
125
+ return centrifugeClient;
126
+ }
127
+ /**
128
+ * Get OAuth client
129
+ */
130
+ export function getOAuth() {
131
+ return oauth;
132
+ }
133
+ /**
134
+ * Update channel state and notify listeners
135
+ */
136
+ function updateState(state) {
137
+ channelState = state;
138
+ if (stateChangeHandler) {
139
+ stateChangeHandler(state);
140
+ }
141
+ log.debug(`Channel state: ${state}`);
142
+ }
143
+ /**
144
+ * Stop WorkBuddy client
145
+ */
146
+ export function stopWorkBuddy() {
147
+ log.info('Stopping WorkBuddy client...');
148
+ if (centrifugeClient) {
149
+ centrifugeClient.stop();
150
+ centrifugeClient = null;
151
+ }
152
+ oauth = null;
153
+ currentSessionId = null;
154
+ updateState('disconnected');
155
+ log.info('WorkBuddy client stopped');
156
+ }
157
+ /**
158
+ * Helper to get hostname
159
+ */
160
+ function hostname() {
161
+ return process.env.HOSTNAME || process.env.COMPUTERNAME || 'unknown';
162
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * WorkBuddy Event Handler - Handle WeChat KF message events from Centrifuge
3
+ */
4
+ import { type Config } from '../config.js';
5
+ import type { SessionManager } from '../session/session-manager.js';
6
+ export interface WorkBuddyEventHandlerHandle {
7
+ stop: () => void;
8
+ getRunningTaskCount: () => number;
9
+ handleEvent: (chatId: string, msgId: string, content: string) => Promise<void>;
10
+ }
11
+ export declare function setupWorkBuddyHandlers(config: Config, sessionManager: SessionManager): WorkBuddyEventHandlerHandle;