@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.
- package/dist/access/access-control.js +1 -1
- package/dist/adapters/claude-sdk-adapter.js +94 -36
- package/dist/channels/capabilities.js +5 -0
- package/dist/cli.js +5 -2
- package/dist/commands/handler.d.ts +1 -2
- package/dist/commands/handler.js +6 -18
- package/dist/config-web-page-i18n.d.ts +12 -0
- package/dist/config-web-page-i18n.js +12 -0
- package/dist/config-web-page-script.js +1 -0
- package/dist/config-web-page-template.js +48 -1
- package/dist/config-web.js +110 -7
- 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/dingtalk/client.js +11 -3
- package/dist/dingtalk/event-handler.js +18 -3
- package/dist/dingtalk/message-sender.js +13 -0
- package/dist/feishu/event-handler.js +144 -10
- package/dist/index.js +26 -2
- package/dist/manager-control.js +7 -0
- package/dist/qq/client.js +111 -88
- package/dist/qq/event-handler.js +16 -2
- package/dist/qq/message-sender.js +11 -0
- package/dist/service-control.js +4 -0
- package/dist/session/session-manager.js +11 -1
- package/dist/setup.js +2 -1
- package/dist/shared/active-chats.d.ts +2 -2
- package/dist/shared/ai-task.js +13 -1
- package/dist/shared/chat-user-map.js +11 -0
- package/dist/shared/media-storage.js +27 -0
- package/dist/telegram/client.js +25 -3
- package/dist/telegram/event-handler.js +44 -8
- package/dist/telegram/message-sender.js +13 -0
- package/dist/wechat/auth/qclaw-api.js +1 -1
- package/dist/wechat/client.js +81 -4
- package/dist/wechat/event-handler.js +10 -3
- package/dist/wework/client.js +36 -14
- package/dist/wework/event-handler.js +39 -4
- package/dist/wework/message-sender.js +53 -21
- 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 +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,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
|
+
}
|