@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
@@ -0,0 +1,118 @@
1
+ /**
2
+ * WorkBuddy Event Handler - Handle WeChat KF message events from Centrifuge
3
+ */
4
+ import { resolvePlatformAiCommand } from '../config.js';
5
+ import { AccessControl } from '../access/access-control.js';
6
+ import { RequestQueue } from '../queue/request-queue.js';
7
+ import { sendTextReply, sendErrorReply } from './message-sender.js';
8
+ import { CommandHandler } from '../commands/handler.js';
9
+ import { getAdapter } from '../adapters/registry.js';
10
+ import { runAITask } from '../shared/ai-task.js';
11
+ import { startTaskCleanup } from '../shared/task-cleanup.js';
12
+ import { WORKBUDDY_THROTTLE_MS } from '../constants.js';
13
+ import { setActiveChatId } from '../shared/active-chats.js';
14
+ import { setChatUser } from '../shared/chat-user-map.js';
15
+ import { createLogger } from '../logger.js';
16
+ const log = createLogger('WorkBuddyHandler');
17
+ export function setupWorkBuddyHandlers(config, sessionManager) {
18
+ const accessControl = new AccessControl(config.workbuddyAllowedUserIds);
19
+ const requestQueue = new RequestQueue();
20
+ const runningTasks = new Map();
21
+ const taskKeyByChatId = new Map();
22
+ const stopTaskCleanup = startTaskCleanup(runningTasks);
23
+ const commandHandler = new CommandHandler({
24
+ config,
25
+ sessionManager,
26
+ requestQueue,
27
+ sender: {
28
+ sendTextReply: async (chatId, text) => {
29
+ // We'll handle this in the AI request
30
+ },
31
+ },
32
+ getRunningTasksSize: () => runningTasks.size,
33
+ });
34
+ async function handleAIRequest(userId, chatId, msgId, prompt, workDir, convId) {
35
+ log.info(`[AI_REQUEST] userId=${userId}, chatId=${chatId}, promptLength=${prompt.length}`);
36
+ const aiCommand = resolvePlatformAiCommand(config, 'workbuddy');
37
+ const toolAdapter = getAdapter(aiCommand);
38
+ if (!toolAdapter) {
39
+ log.error(`[handleAIRequest] No adapter found for: ${aiCommand}`);
40
+ await sendErrorReply(null, chatId, `AI tool is not configured: ${aiCommand}`, msgId);
41
+ return;
42
+ }
43
+ const sessionId = convId
44
+ ? sessionManager.getSessionIdForConv(userId, convId, aiCommand)
45
+ : undefined;
46
+ log.info(`[handleAIRequest] Running ${aiCommand} for user ${userId}, sessionId=${sessionId ?? 'new'}`);
47
+ const toolId = aiCommand;
48
+ const taskKey = `${userId}:${msgId}`;
49
+ await runAITask({ config, sessionManager }, { userId, chatId, workDir, sessionId, convId, platform: 'workbuddy', taskKey }, prompt, toolAdapter, {
50
+ throttleMs: WORKBUDDY_THROTTLE_MS,
51
+ streamUpdate: async (content) => {
52
+ // WorkBuddy doesn't support streaming updates via Centrifuge
53
+ log.debug(`Stream update (not sent): ${content.substring(0, 50)}...`);
54
+ },
55
+ sendComplete: async (content) => {
56
+ await sendTextReply(null, chatId, content, msgId);
57
+ },
58
+ sendError: async (error) => {
59
+ await sendErrorReply(null, chatId, error, msgId);
60
+ },
61
+ extraCleanup: () => {
62
+ runningTasks.delete(taskKey);
63
+ if (taskKeyByChatId.get(chatId) === taskKey) {
64
+ taskKeyByChatId.delete(chatId);
65
+ }
66
+ },
67
+ onTaskReady: (state) => {
68
+ runningTasks.set(taskKey, state);
69
+ taskKeyByChatId.set(chatId, taskKey);
70
+ },
71
+ });
72
+ }
73
+ async function handleEvent(chatId, msgId, content) {
74
+ log.info(`[handleEvent] chatId=${chatId}, msgId=${msgId}, content="${content.substring(0, 100)}"`);
75
+ // Use chatId as userId for WorkBuddy (WeChat KF doesn't have separate userId)
76
+ const userId = chatId;
77
+ const text = content.trim();
78
+ if (!accessControl.isAllowed(userId)) {
79
+ log.warn(`Access denied for sender: ${userId}`);
80
+ await sendErrorReply(null, chatId, `Access denied. Your chat ID: ${userId}`, msgId);
81
+ return;
82
+ }
83
+ setActiveChatId('workbuddy', chatId);
84
+ setChatUser(chatId, userId, 'workbuddy');
85
+ const workDir = sessionManager.getWorkDir(userId);
86
+ const convId = sessionManager.getConvId(userId);
87
+ // Try command handler first
88
+ try {
89
+ const handled = await commandHandler.dispatch(text, chatId, userId, 'workbuddy', (u, c, p, w, conv, _r, m) => handleAIRequest(u, c, msgId, p, w, conv));
90
+ if (handled) {
91
+ log.info(`Command handled for message: ${text}`);
92
+ return;
93
+ }
94
+ }
95
+ catch (err) {
96
+ log.error('Error in commandHandler.dispatch:', err);
97
+ }
98
+ // No command, proceed with AI request
99
+ if (!text) {
100
+ return;
101
+ }
102
+ const enqueueResult = requestQueue.enqueue(userId, convId, text, async (nextPrompt) => {
103
+ log.info(`Executing AI request for: ${text}`);
104
+ await handleAIRequest(userId, chatId, msgId, nextPrompt, workDir, convId);
105
+ });
106
+ if (enqueueResult === 'rejected') {
107
+ await sendErrorReply(null, chatId, 'Request queue is full. Please try again later.', msgId);
108
+ }
109
+ else if (enqueueResult === 'queued') {
110
+ await sendTextReply(null, chatId, 'Your request is queued.', msgId);
111
+ }
112
+ }
113
+ return {
114
+ stop: () => stopTaskCleanup(),
115
+ getRunningTaskCount: () => runningTasks.size,
116
+ handleEvent,
117
+ };
118
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * WorkBuddy Module - CodeBuddy OAuth + Centrifuge WebSocket for WeChat
3
+ */
4
+ export * from './types.js';
5
+ export * from './oauth.js';
6
+ export * from './centrifuge-client.js';
7
+ export * from './message-sender.js';
8
+ export * from './client.js';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * WorkBuddy Module - CodeBuddy OAuth + Centrifuge WebSocket for WeChat
3
+ */
4
+ export * from './types.js';
5
+ export * from './oauth.js';
6
+ export * from './centrifuge-client.js';
7
+ export * from './message-sender.js';
8
+ export * from './client.js';
@@ -0,0 +1,16 @@
1
+ /**
2
+ * WorkBuddy Message Sender - Send responses to WeChat KF
3
+ */
4
+ import type { WorkBuddyCentrifugeClient } from './centrifuge-client.js';
5
+ /**
6
+ * Send text reply to WeChat KF
7
+ */
8
+ export declare function sendTextReply(_client: WorkBuddyCentrifugeClient | null, chatId: string, text: string, msgId: string): Promise<void>;
9
+ /**
10
+ * Send error response to WeChat KF
11
+ */
12
+ export declare function sendErrorReply(_client: WorkBuddyCentrifugeClient | null, chatId: string, error: string, msgId: string): Promise<void>;
13
+ /**
14
+ * Send streaming chunk to WeChat KF
15
+ */
16
+ export declare function sendStreamingChunk(_client: WorkBuddyCentrifugeClient | null, chatId: string, text: string, msgId: string): void;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * WorkBuddy Message Sender - Send responses to WeChat KF
3
+ */
4
+ import { createLogger } from '../logger.js';
5
+ import { getCentrifugeClient } from './client.js';
6
+ const log = createLogger('WorkBuddySender');
7
+ /**
8
+ * Send text reply to WeChat KF
9
+ */
10
+ export async function sendTextReply(_client, chatId, text, msgId) {
11
+ const client = _client ?? getCentrifugeClient();
12
+ if (!client) {
13
+ log.warn('WorkBuddy client not available, cannot send reply');
14
+ return;
15
+ }
16
+ log.info(`Sending WorkBuddy reply to chatId=${chatId}, msgId=${msgId}`);
17
+ client.sendPromptResponse({
18
+ session_id: chatId,
19
+ prompt_id: msgId,
20
+ content: [{ type: 'text', text }],
21
+ stop_reason: 'end_turn',
22
+ });
23
+ }
24
+ /**
25
+ * Send error response to WeChat KF
26
+ */
27
+ export async function sendErrorReply(_client, chatId, error, msgId) {
28
+ const client = _client ?? getCentrifugeClient();
29
+ if (!client) {
30
+ log.warn('WorkBuddy client not available, cannot send error');
31
+ return;
32
+ }
33
+ log.info(`Sending WorkBuddy error to chatId=${chatId}, msgId=${msgId}`);
34
+ client.sendPromptResponse({
35
+ session_id: chatId,
36
+ prompt_id: msgId,
37
+ error,
38
+ stop_reason: 'error',
39
+ });
40
+ }
41
+ /**
42
+ * Send streaming chunk to WeChat KF
43
+ */
44
+ export function sendStreamingChunk(_client, chatId, text, msgId) {
45
+ const client = _client ?? getCentrifugeClient();
46
+ if (!client) {
47
+ log.warn('WorkBuddy client not available, cannot send chunk');
48
+ return;
49
+ }
50
+ client.sendMessageChunk(chatId, msgId, { type: 'text', text });
51
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * WorkBuddy OAuth - CodeBuddy authentication for WeChat KF integration
3
+ */
4
+ import type { WorkBuddyCredentials, CentrifugeTokens } from './types.js';
5
+ export declare class WorkBuddyOAuth {
6
+ private baseUrl;
7
+ private hostId;
8
+ accessToken: string;
9
+ refreshToken: string;
10
+ userId: string;
11
+ constructor(baseUrl?: string);
12
+ private getHeaders;
13
+ /**
14
+ * Get login URL and state for OAuth flow
15
+ */
16
+ fetchAuthState(): Promise<{
17
+ authUrl: string;
18
+ state: string;
19
+ }>;
20
+ /**
21
+ * Poll for OAuth token after user completes login
22
+ */
23
+ pollToken(state: string, signal?: AbortSignal, timeoutMs?: number): Promise<{
24
+ accessToken: string;
25
+ refreshToken: string;
26
+ userId?: string;
27
+ }>;
28
+ /**
29
+ * Get account info
30
+ */
31
+ getAccount(state: string, signal?: AbortSignal): Promise<Record<string, unknown>>;
32
+ /**
33
+ * Refresh access token
34
+ */
35
+ refreshTokenAuth(): Promise<{
36
+ accessToken: string;
37
+ refreshToken: string;
38
+ }>;
39
+ /**
40
+ * Build sessionId for WorkBuddy workspace
41
+ */
42
+ buildSessionId(workspacePath?: string): string;
43
+ /**
44
+ * Get WeChat KF binding link
45
+ */
46
+ getWeChatKfLink(sessionId: string, userId?: string): Promise<{
47
+ success: boolean;
48
+ url?: string;
49
+ expiresIn?: number;
50
+ message?: string;
51
+ }>;
52
+ /**
53
+ * Check WeChat KF binding status
54
+ */
55
+ getWeChatKfBindStatus(sessionId: string): Promise<{
56
+ success: boolean;
57
+ bound: boolean;
58
+ externalUserId?: string;
59
+ boundAt?: string;
60
+ nickname?: string;
61
+ avatar?: string;
62
+ message?: string;
63
+ }>;
64
+ /**
65
+ * Poll binding status until bound
66
+ */
67
+ pollBindStatus(sessionId: string, intervalMs?: number, timeoutMs?: number): Promise<{
68
+ bound: boolean;
69
+ nickname?: string;
70
+ avatar?: string;
71
+ externalUserId?: string;
72
+ }>;
73
+ /**
74
+ * Register workspace to get Centrifuge connection tokens
75
+ */
76
+ registerWorkspace(params: {
77
+ userId: string;
78
+ hostId: string;
79
+ workspaceId: string;
80
+ workspaceName: string;
81
+ }): Promise<CentrifugeTokens>;
82
+ /**
83
+ * Register channel for WeChat KF
84
+ */
85
+ registerChannel(params: {
86
+ type: string;
87
+ sessionId: string;
88
+ channelId?: string;
89
+ [key: string]: unknown;
90
+ }): Promise<Record<string, unknown>>;
91
+ /**
92
+ * Send response to WeChat KF
93
+ */
94
+ sendResponse(payload: {
95
+ type: string;
96
+ msgId: string;
97
+ chatId: string;
98
+ success: boolean;
99
+ message: string;
100
+ metadata: Record<string, unknown>;
101
+ }): Promise<void>;
102
+ /**
103
+ * Load credentials from object
104
+ */
105
+ loadCredentials(creds: Partial<WorkBuddyCredentials>): void;
106
+ /**
107
+ * Export credentials
108
+ */
109
+ exportCredentials(): WorkBuddyCredentials;
110
+ /**
111
+ * Check if authenticated
112
+ */
113
+ isAuthenticated(): boolean;
114
+ }
@@ -0,0 +1,310 @@
1
+ /**
2
+ * WorkBuddy OAuth - CodeBuddy authentication for WeChat KF integration
3
+ */
4
+ import { hostname } from 'node:os';
5
+ import { createLogger } from '../logger.js';
6
+ const log = createLogger('WorkBuddyOAuth');
7
+ const DEFAULT_BASE_URL = 'https://copilot.tencent.com';
8
+ const PLATFORM = 'ide';
9
+ export class WorkBuddyOAuth {
10
+ baseUrl;
11
+ hostId;
12
+ // Credentials
13
+ accessToken = '';
14
+ refreshToken = '';
15
+ userId = '';
16
+ constructor(baseUrl = DEFAULT_BASE_URL) {
17
+ this.baseUrl = baseUrl.replace(/\/+$/, '');
18
+ this.hostId = hostname();
19
+ }
20
+ getHeaders(auth = true) {
21
+ const h = {
22
+ 'Content-Type': 'application/json',
23
+ };
24
+ if (auth && this.accessToken) {
25
+ h['Authorization'] = `Bearer ${this.accessToken}`;
26
+ }
27
+ return h;
28
+ }
29
+ /**
30
+ * Get login URL and state for OAuth flow
31
+ */
32
+ async fetchAuthState() {
33
+ const url = `${this.baseUrl}/v2/plugin/auth/state?platform=${PLATFORM}`;
34
+ const res = await fetch(url, {
35
+ method: 'POST',
36
+ headers: {
37
+ ...this.getHeaders(false),
38
+ 'X-No-Authorization': 'true',
39
+ 'X-No-User-Id': 'true',
40
+ 'X-No-Enterprise-Id': 'true',
41
+ },
42
+ signal: AbortSignal.timeout(30_000),
43
+ });
44
+ if (!res.ok) {
45
+ throw new Error(`fetchAuthState failed: ${res.status} ${res.statusText}`);
46
+ }
47
+ const data = (await res.json());
48
+ const result = data?.data;
49
+ if (!result?.authUrl || !result?.state) {
50
+ throw new Error('fetchAuthState: missing authUrl or state in response');
51
+ }
52
+ return { authUrl: result.authUrl, state: result.state };
53
+ }
54
+ /**
55
+ * Poll for OAuth token after user completes login
56
+ */
57
+ async pollToken(state, signal, timeoutMs = 5 * 60 * 1000) {
58
+ const start = Date.now();
59
+ while (Date.now() - start < timeoutMs) {
60
+ if (signal?.aborted)
61
+ throw new Error('登录已取消');
62
+ await new Promise((r) => setTimeout(r, 1000));
63
+ try {
64
+ const url = `${this.baseUrl}/v2/plugin/auth/token?state=${state}`;
65
+ const res = await fetch(url, {
66
+ method: 'GET',
67
+ headers: {
68
+ ...this.getHeaders(false),
69
+ 'X-No-Authorization': 'true',
70
+ },
71
+ signal: AbortSignal.timeout(10_000),
72
+ });
73
+ if (!res.ok) {
74
+ const body = await res.text().catch(() => '');
75
+ // code 11217 = still waiting
76
+ if (body.includes('11217'))
77
+ continue;
78
+ throw new Error(`pollToken: ${res.status} ${body}`);
79
+ }
80
+ const data = (await res.json());
81
+ const token = data?.data;
82
+ if (token?.accessToken) {
83
+ this.accessToken = token.accessToken;
84
+ this.refreshToken = token.refreshToken || '';
85
+ return {
86
+ accessToken: token.accessToken,
87
+ refreshToken: token.refreshToken || '',
88
+ userId: token.userId,
89
+ };
90
+ }
91
+ }
92
+ catch (e) {
93
+ // code 11217 = still waiting, continue polling
94
+ if (e?.message?.includes('11217'))
95
+ continue;
96
+ // network errors: retry
97
+ if (e?.code === 'UND_ERR_CONNECT_TIMEOUT' || e?.code === 'ECONNREFUSED')
98
+ continue;
99
+ throw e;
100
+ }
101
+ }
102
+ throw new Error('登录超时(5 分钟)');
103
+ }
104
+ /**
105
+ * Get account info
106
+ */
107
+ async getAccount(state, signal) {
108
+ const start = Date.now();
109
+ const timeoutMs = 5 * 60 * 1000;
110
+ while (Date.now() - start < timeoutMs) {
111
+ if (signal?.aborted)
112
+ throw new Error('操作已取消');
113
+ await new Promise((r) => setTimeout(r, 1000));
114
+ try {
115
+ const url = `${this.baseUrl}/v2/plugin/login/account?state=${state}`;
116
+ const res = await fetch(url, {
117
+ method: 'GET',
118
+ headers: {
119
+ ...this.getHeaders(),
120
+ 'X-No-User-Id': 'true',
121
+ 'X-No-Enterprise-Id': 'true',
122
+ },
123
+ signal: AbortSignal.timeout(10_000),
124
+ });
125
+ if (!res.ok) {
126
+ const body = await res.text().catch(() => '');
127
+ if (body.includes('12151'))
128
+ continue;
129
+ throw new Error(`getAccount: ${res.status} ${body}`);
130
+ }
131
+ const data = (await res.json());
132
+ if (data?.data)
133
+ return data.data;
134
+ }
135
+ catch (e) {
136
+ if (e?.message?.includes('12151'))
137
+ continue;
138
+ throw e;
139
+ }
140
+ }
141
+ throw new Error('获取账号信息超时');
142
+ }
143
+ /**
144
+ * Refresh access token
145
+ */
146
+ async refreshTokenAuth() {
147
+ const url = `${this.baseUrl}/v2/plugin/auth/token/refresh`;
148
+ const res = await fetch(url, {
149
+ method: 'POST',
150
+ headers: {
151
+ ...this.getHeaders(),
152
+ 'X-Refresh-Token': this.refreshToken,
153
+ 'X-Auth-Refresh-Source': 'ide-main',
154
+ },
155
+ signal: AbortSignal.timeout(30_000),
156
+ });
157
+ if (!res.ok) {
158
+ const status = res.status;
159
+ if (status === 401 || status === 403) {
160
+ throw new Error('Token 已过期,请重新登录');
161
+ }
162
+ throw new Error(`refreshToken failed: ${status} ${res.statusText}`);
163
+ }
164
+ const data = (await res.json());
165
+ const token = data?.data;
166
+ if (token?.accessToken) {
167
+ this.accessToken = token.accessToken;
168
+ if (token.refreshToken)
169
+ this.refreshToken = token.refreshToken;
170
+ return { accessToken: token.accessToken, refreshToken: token.refreshToken || this.refreshToken };
171
+ }
172
+ throw new Error('refreshToken: missing accessToken in response');
173
+ }
174
+ /**
175
+ * Build sessionId for WorkBuddy workspace
176
+ */
177
+ buildSessionId(workspacePath) {
178
+ const wp = workspacePath || `${process.env.HOME || process.env.USERPROFILE}/WorkBuddy/Claw`;
179
+ return `${this.userId}_${this.hostId}_${wp}`;
180
+ }
181
+ /**
182
+ * Get WeChat KF binding link
183
+ */
184
+ async getWeChatKfLink(sessionId, userId) {
185
+ const url = `${this.baseUrl}/v2/backgroundagent/wechatkfProxy/link`;
186
+ const res = await fetch(url, {
187
+ method: 'POST',
188
+ headers: this.getHeaders(),
189
+ body: JSON.stringify({ sessionId, userId: userId || this.userId }),
190
+ signal: AbortSignal.timeout(30_000),
191
+ });
192
+ if (!res.ok) {
193
+ const body = await res.text().catch(() => '');
194
+ return { success: false, message: `获取链接失败: ${res.status} ${body}` };
195
+ }
196
+ return (await res.json());
197
+ }
198
+ /**
199
+ * Check WeChat KF binding status
200
+ */
201
+ async getWeChatKfBindStatus(sessionId) {
202
+ const url = `${this.baseUrl}/v2/backgroundagent/wechatkfProxy/bindStatus?sessionId=${encodeURIComponent(sessionId)}`;
203
+ const res = await fetch(url, {
204
+ method: 'GET',
205
+ headers: this.getHeaders(),
206
+ signal: AbortSignal.timeout(30_000),
207
+ });
208
+ if (!res.ok) {
209
+ return { success: false, bound: false, message: `查询状态失败: ${res.status}` };
210
+ }
211
+ return (await res.json());
212
+ }
213
+ /**
214
+ * Poll binding status until bound
215
+ */
216
+ async pollBindStatus(sessionId, intervalMs = 10_000, timeoutMs = 5 * 60 * 1000) {
217
+ const start = Date.now();
218
+ while (Date.now() - start < timeoutMs) {
219
+ await new Promise((r) => setTimeout(r, intervalMs));
220
+ const result = await this.getWeChatKfBindStatus(sessionId);
221
+ if (result.success && result.bound) {
222
+ return {
223
+ bound: true,
224
+ nickname: result.nickname,
225
+ avatar: result.avatar,
226
+ externalUserId: result.externalUserId,
227
+ };
228
+ }
229
+ }
230
+ return { bound: false };
231
+ }
232
+ /**
233
+ * Register workspace to get Centrifuge connection tokens
234
+ */
235
+ async registerWorkspace(params) {
236
+ const url = `${this.baseUrl}/v2/agentos/localagent/registerWorkspace`;
237
+ const res = await fetch(url, {
238
+ method: 'POST',
239
+ headers: this.getHeaders(),
240
+ body: JSON.stringify({
241
+ ...params,
242
+ localAgentType: 'ide',
243
+ }),
244
+ signal: AbortSignal.timeout(30_000),
245
+ });
246
+ if (!res.ok)
247
+ throw new Error(`registerWorkspace failed: ${res.status} ${res.statusText}`);
248
+ const data = (await res.json());
249
+ if (!data?.data)
250
+ throw new Error('registerWorkspace: missing data field');
251
+ return data.data;
252
+ }
253
+ /**
254
+ * Register channel for WeChat KF
255
+ */
256
+ async registerChannel(params) {
257
+ const url = `${this.baseUrl}/v2/backgroundagent/localProxy/register`;
258
+ const res = await fetch(url, {
259
+ method: 'POST',
260
+ headers: this.getHeaders(),
261
+ body: JSON.stringify(params),
262
+ signal: AbortSignal.timeout(30_000),
263
+ });
264
+ if (!res.ok) {
265
+ const body = await res.text().catch(() => '');
266
+ throw new Error(`registerChannel failed: ${res.status} ${body}`);
267
+ }
268
+ return (await res.json());
269
+ }
270
+ /**
271
+ * Send response to WeChat KF
272
+ */
273
+ async sendResponse(payload) {
274
+ const url = `${this.baseUrl}/v2/backgroundagent/wecom/local-proxy/receive`;
275
+ const res = await fetch(url, {
276
+ method: 'POST',
277
+ headers: this.getHeaders(),
278
+ body: JSON.stringify(payload),
279
+ signal: AbortSignal.timeout(30_000),
280
+ });
281
+ if (!res.ok)
282
+ throw new Error(`sendResponse failed: ${res.status} ${res.statusText}`);
283
+ }
284
+ /**
285
+ * Load credentials from object
286
+ */
287
+ loadCredentials(creds) {
288
+ this.accessToken = creds.accessToken || '';
289
+ this.refreshToken = creds.refreshToken || '';
290
+ this.userId = creds.userId || '';
291
+ }
292
+ /**
293
+ * Export credentials
294
+ */
295
+ exportCredentials() {
296
+ return {
297
+ accessToken: this.accessToken,
298
+ refreshToken: this.refreshToken,
299
+ userId: this.userId,
300
+ hostId: this.hostId,
301
+ baseUrl: this.baseUrl,
302
+ };
303
+ }
304
+ /**
305
+ * Check if authenticated
306
+ */
307
+ isAuthenticated() {
308
+ return !!this.accessToken && !!this.userId;
309
+ }
310
+ }