@wu529778790/open-im 1.0.3 → 1.1.0
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/README.md +59 -3
- package/dist/commands/handler.d.ts +1 -1
- package/dist/config.d.ts +19 -1
- package/dist/config.js +69 -4
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/index.js +59 -12
- package/dist/setup.js +140 -29
- package/dist/shared/active-chats.d.ts +2 -2
- package/dist/wechat/auth/device-bind.d.ts +13 -0
- package/dist/wechat/auth/device-bind.js +76 -0
- package/dist/wechat/auth/device-guid.d.ts +5 -0
- package/dist/wechat/auth/device-guid.js +28 -0
- package/dist/wechat/auth/environments.d.ts +5 -0
- package/dist/wechat/auth/environments.js +21 -0
- package/dist/wechat/auth/index.d.ts +7 -0
- package/dist/wechat/auth/index.js +5 -0
- package/dist/wechat/auth/qclaw-api.d.ts +26 -0
- package/dist/wechat/auth/qclaw-api.js +100 -0
- package/dist/wechat/auth/types.d.ts +18 -0
- package/dist/wechat/auth/types.js +4 -0
- package/dist/wechat/auth/wechat-login.d.ts +17 -0
- package/dist/wechat/auth/wechat-login.js +172 -0
- package/dist/wechat/client.js +11 -2
- package/dist/wework/client.d.ts +46 -0
- package/dist/wework/client.js +356 -0
- package/dist/wework/event-handler.d.ts +12 -0
- package/dist/wework/event-handler.js +245 -0
- package/dist/wework/message-sender.d.ts +57 -0
- package/dist/wework/message-sender.js +258 -0
- package/dist/wework/types.d.ts +156 -0
- package/dist/wework/types.js +6 -0
- package/package.json +4 -1
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 微信扫码登录流程
|
|
3
|
+
* 1. 获取 state → 2. 显示二维码 → 3. 等待 code → 4. 换 token → 5. 设备绑定
|
|
4
|
+
*/
|
|
5
|
+
import { createInterface } from 'node:readline';
|
|
6
|
+
import { QClawAPI } from './qclaw-api.js';
|
|
7
|
+
import { getDeviceGuid } from './device-guid.js';
|
|
8
|
+
import { getEnvironment } from './environments.js';
|
|
9
|
+
import { performDeviceBinding } from './device-bind.js';
|
|
10
|
+
function nested(obj, ...keys) {
|
|
11
|
+
let cur = obj;
|
|
12
|
+
for (const k of keys) {
|
|
13
|
+
if (cur == null || typeof cur !== 'object')
|
|
14
|
+
return undefined;
|
|
15
|
+
cur = cur[k];
|
|
16
|
+
}
|
|
17
|
+
return cur;
|
|
18
|
+
}
|
|
19
|
+
function buildAuthUrl(state, env) {
|
|
20
|
+
const params = new URLSearchParams({
|
|
21
|
+
appid: env.wxAppId,
|
|
22
|
+
redirect_uri: env.wxLoginRedirectUri,
|
|
23
|
+
response_type: 'code',
|
|
24
|
+
scope: 'snsapi_login',
|
|
25
|
+
state,
|
|
26
|
+
});
|
|
27
|
+
return `https://open.weixin.qq.com/connect/qrconnect?${params.toString()}#wechat_redirect`;
|
|
28
|
+
}
|
|
29
|
+
async function displayQrCode(url) {
|
|
30
|
+
console.log('\n' + '='.repeat(64));
|
|
31
|
+
console.log('请用微信扫描下方二维码登录');
|
|
32
|
+
console.log('='.repeat(64));
|
|
33
|
+
try {
|
|
34
|
+
const { generate } = await import('qrcode-terminal');
|
|
35
|
+
if (generate)
|
|
36
|
+
generate(url, { small: true }, (qrcode) => console.log(qrcode));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
console.log('\n(未安装 qrcode-terminal,无法在终端显示二维码)');
|
|
40
|
+
console.log('可运行: npm install qrcode-terminal');
|
|
41
|
+
}
|
|
42
|
+
console.log('\n或在浏览器中打开以下链接:');
|
|
43
|
+
console.log(url);
|
|
44
|
+
console.log('='.repeat(64) + '\n');
|
|
45
|
+
}
|
|
46
|
+
function readLine(prompt) {
|
|
47
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
48
|
+
return new Promise((resolve) => {
|
|
49
|
+
rl.question(prompt, (answer) => {
|
|
50
|
+
rl.close();
|
|
51
|
+
resolve(answer.trim());
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async function waitForAuthCode() {
|
|
56
|
+
console.log('微信扫码授权后,浏览器会跳转到新页面,地址栏 URL 形如:');
|
|
57
|
+
console.log('https://security.guanjia.qq.com/login?code=0a1B2c...&state=xxx');
|
|
58
|
+
console.log('\n请复制 code= 后面的值(到 & 之前),或直接粘贴完整 URL。\n');
|
|
59
|
+
const raw = await readLine('请粘贴 code 值或完整 URL: ');
|
|
60
|
+
if (!raw)
|
|
61
|
+
return '';
|
|
62
|
+
const cleaned = raw.replace(/\\([?=&#])/g, '$1');
|
|
63
|
+
if (cleaned.includes('code=')) {
|
|
64
|
+
try {
|
|
65
|
+
const url = new URL(cleaned);
|
|
66
|
+
const code = url.searchParams.get('code');
|
|
67
|
+
if (code)
|
|
68
|
+
return code;
|
|
69
|
+
if (url.hash) {
|
|
70
|
+
const fragmentParams = new URLSearchParams(url.hash.replace(/^#/, ''));
|
|
71
|
+
const fCode = fragmentParams.get('code');
|
|
72
|
+
if (fCode)
|
|
73
|
+
return fCode;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
/* ignore */
|
|
78
|
+
}
|
|
79
|
+
const match = cleaned.match(/[?&#]code=([^&#]+)/);
|
|
80
|
+
if (match?.[1])
|
|
81
|
+
return match[1];
|
|
82
|
+
}
|
|
83
|
+
return cleaned;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 执行微信扫码登录,返回 token、guid、userId
|
|
87
|
+
*/
|
|
88
|
+
export async function performWeChatLogin(options = {}) {
|
|
89
|
+
const envName = options.envName ?? 'production';
|
|
90
|
+
const appId = options.appId;
|
|
91
|
+
if (!appId) {
|
|
92
|
+
throw new Error('appId is required. 请在配置中提供 wechatAppId 或通过环境变量 WECHAT_APP_ID 设置');
|
|
93
|
+
}
|
|
94
|
+
const env = getEnvironment(envName, appId);
|
|
95
|
+
const guid = getDeviceGuid();
|
|
96
|
+
const api = new QClawAPI(env, guid);
|
|
97
|
+
// 1. 获取 state
|
|
98
|
+
console.log('[微信登录] 步骤 1/5: 获取登录 state...');
|
|
99
|
+
let state = String(Math.floor(Math.random() * 10000));
|
|
100
|
+
const stateResult = await api.getWxLoginState();
|
|
101
|
+
if (stateResult.success) {
|
|
102
|
+
const s = nested(stateResult.data, 'state');
|
|
103
|
+
if (s)
|
|
104
|
+
state = s;
|
|
105
|
+
}
|
|
106
|
+
// 2. 显示二维码
|
|
107
|
+
console.log('[微信登录] 步骤 2/5: 生成微信登录二维码...');
|
|
108
|
+
const authUrl = buildAuthUrl(state, env);
|
|
109
|
+
await displayQrCode(authUrl);
|
|
110
|
+
// 3. 等待 code
|
|
111
|
+
console.log('[微信登录] 步骤 3/5: 等待微信扫码授权...');
|
|
112
|
+
const code = await waitForAuthCode();
|
|
113
|
+
if (!code) {
|
|
114
|
+
throw new Error('未获取到授权 code');
|
|
115
|
+
}
|
|
116
|
+
// 4. 用 code 换 token
|
|
117
|
+
console.log(`[微信登录] 步骤 4/5: 用授权码登录 (code=${code.substring(0, 10)}...)`);
|
|
118
|
+
const loginResult = await api.wxLogin(code, state);
|
|
119
|
+
if (!loginResult.success) {
|
|
120
|
+
throw new Error(`登录失败: ${loginResult.message ?? '未知错误'}`);
|
|
121
|
+
}
|
|
122
|
+
const loginData = loginResult.data;
|
|
123
|
+
const jwtToken = nested(loginData, 'token') || nested(loginData, 'data', 'token') || '';
|
|
124
|
+
const channelToken = nested(loginData, 'openclaw_channel_token') ||
|
|
125
|
+
nested(loginData, 'data', 'openclaw_channel_token') ||
|
|
126
|
+
'';
|
|
127
|
+
const userInfo = nested(loginData, 'user_info') ||
|
|
128
|
+
nested(loginData, 'data', 'user_info') ||
|
|
129
|
+
{};
|
|
130
|
+
const loginKey = userInfo.loginKey;
|
|
131
|
+
if (loginKey)
|
|
132
|
+
api.loginKey = loginKey;
|
|
133
|
+
api.jwtToken = jwtToken;
|
|
134
|
+
api.userId = String(userInfo.user_id ?? '');
|
|
135
|
+
const nickname = userInfo.nickname ?? 'unknown';
|
|
136
|
+
console.log(`[微信登录] 登录成功! 用户: ${nickname}`);
|
|
137
|
+
// 5. 设备绑定(服务端要求先绑定才接受 WebSocket,顺序不可颠倒)
|
|
138
|
+
console.log('[微信登录] 步骤 5/5: 设备绑定...');
|
|
139
|
+
const credentials = {
|
|
140
|
+
channelToken,
|
|
141
|
+
jwtToken,
|
|
142
|
+
userId: api.userId,
|
|
143
|
+
guid,
|
|
144
|
+
loginKey: api.loginKey,
|
|
145
|
+
userInfo,
|
|
146
|
+
};
|
|
147
|
+
const bindResult = await performDeviceBinding(api, {
|
|
148
|
+
showQr: async (url) => {
|
|
149
|
+
console.log('\n' + '='.repeat(64));
|
|
150
|
+
console.log('【设备绑定】请复制下方链接,在微信中发给「文件传输助手」后点击打开:');
|
|
151
|
+
console.log('='.repeat(64));
|
|
152
|
+
console.log(url);
|
|
153
|
+
console.log('='.repeat(64) + '\n');
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
if (bindResult.success) {
|
|
157
|
+
console.log(`[微信登录] ${bindResult.message}`);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
console.warn(`[微信登录] ${bindResult.message}`);
|
|
161
|
+
console.warn('[微信登录] 可稍后重新登录完成绑定。');
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
channelToken,
|
|
165
|
+
jwtToken,
|
|
166
|
+
userId: api.userId,
|
|
167
|
+
guid,
|
|
168
|
+
loginKey: api.loginKey,
|
|
169
|
+
userInfo,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
export { getEnvironment };
|
package/dist/wechat/client.js
CHANGED
|
@@ -35,8 +35,17 @@ export function getCurrentToken() {
|
|
|
35
35
|
* Initialize WeChat client with AGP WebSocket connection
|
|
36
36
|
*/
|
|
37
37
|
export async function initWeChat(config, eventHandler, onStateChange) {
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
// AGP 协议使用 token + guid,标准协议使用 appId + appSecret
|
|
39
|
+
const hasAGPCreds = config.wechatToken && config.wechatGuid;
|
|
40
|
+
const hasStandardCreds = config.wechatAppId && config.wechatAppSecret;
|
|
41
|
+
if (!hasAGPCreds && !hasStandardCreds) {
|
|
42
|
+
throw new Error('WeChat credentials required: AGP (token + guid) or standard (appId + appSecret)');
|
|
43
|
+
}
|
|
44
|
+
if (hasAGPCreds) {
|
|
45
|
+
log.info('Using AGP protocol for WeChat');
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
log.info('Using standard OAuth protocol for WeChat');
|
|
40
49
|
}
|
|
41
50
|
messageHandler = eventHandler;
|
|
42
51
|
stateChangeHandler = onStateChange ?? null;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeWork (企业微信/WeCom) Client
|
|
3
|
+
* 基于企业微信官方 AI_BOT WebSocket 协议
|
|
4
|
+
* WebSocket URL: wss://openws.work.weixin.qq.com
|
|
5
|
+
*
|
|
6
|
+
* 消息接收:通过 WebSocket (aibot_msg_callback)
|
|
7
|
+
* 消息发送:通过 WebSocket (aibot_respond_msg),必须透传 req_id
|
|
8
|
+
* 注意:长连接模式下不能用 HTTP response_url,会报 40008 invalid message type
|
|
9
|
+
*/
|
|
10
|
+
import type { Config } from '../config.js';
|
|
11
|
+
import { WeWorkConnectionState, WeWorkCallbackMessage, WeWorkResponseMessage, WeWorkHttpResponseBody } from './types.js';
|
|
12
|
+
/**
|
|
13
|
+
* Get current connection state
|
|
14
|
+
*/
|
|
15
|
+
export declare function getConnectionState(): WeWorkConnectionState;
|
|
16
|
+
/**
|
|
17
|
+
* 主动推送消息 (aibot_send_msg)
|
|
18
|
+
* 用于启动/关闭通知等场景,无需用户消息触发
|
|
19
|
+
* 注意:需用户曾与机器人对话后,才能向该会话主动推送
|
|
20
|
+
*/
|
|
21
|
+
export declare function sendProactiveMessage(chatId: string, content: string): void;
|
|
22
|
+
/**
|
|
23
|
+
* Send reply via WebSocket (aibot_respond_msg)
|
|
24
|
+
* 长连接模式下必须用此方式回复,透传 req_id
|
|
25
|
+
*/
|
|
26
|
+
export declare function sendWebSocketReply(reqId: string, body: WeWorkHttpResponseBody): void;
|
|
27
|
+
/**
|
|
28
|
+
* Initialize WeWork client with WebSocket connection
|
|
29
|
+
*/
|
|
30
|
+
export declare function initWeWork(cfg: Config, eventHandler: (data: WeWorkCallbackMessage) => Promise<void>, onStateChange?: (state: WeWorkConnectionState) => void): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Send message to WeWork
|
|
33
|
+
*/
|
|
34
|
+
export declare function sendMessage(message: WeWorkResponseMessage): void;
|
|
35
|
+
/**
|
|
36
|
+
* Send text message via WebSocket (requires req_id from callback)
|
|
37
|
+
*/
|
|
38
|
+
export declare function sendText(reqId: string, content: string): void;
|
|
39
|
+
/**
|
|
40
|
+
* Send stream message via WebSocket (requires req_id from callback)
|
|
41
|
+
*/
|
|
42
|
+
export declare function sendStream(reqId: string, streamId: string, content: string, finish: boolean): void;
|
|
43
|
+
/**
|
|
44
|
+
* Stop WeWork client
|
|
45
|
+
*/
|
|
46
|
+
export declare function stopWeWork(): void;
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeWork (企业微信/WeCom) Client
|
|
3
|
+
* 基于企业微信官方 AI_BOT WebSocket 协议
|
|
4
|
+
* WebSocket URL: wss://openws.work.weixin.qq.com
|
|
5
|
+
*
|
|
6
|
+
* 消息接收:通过 WebSocket (aibot_msg_callback)
|
|
7
|
+
* 消息发送:通过 WebSocket (aibot_respond_msg),必须透传 req_id
|
|
8
|
+
* 注意:长连接模式下不能用 HTTP response_url,会报 40008 invalid message type
|
|
9
|
+
*/
|
|
10
|
+
import { WebSocket } from 'ws';
|
|
11
|
+
import { randomBytes } from 'node:crypto';
|
|
12
|
+
import { createLogger } from '../logger.js';
|
|
13
|
+
const log = createLogger('WeWork');
|
|
14
|
+
const DEFAULT_WS_URL = 'wss://openws.work.weixin.qq.com';
|
|
15
|
+
const HEARTBEAT_INTERVAL = 30000; // 30秒
|
|
16
|
+
const MAX_RECONNECT_ATTEMPTS = 100;
|
|
17
|
+
// Global state
|
|
18
|
+
let ws = null;
|
|
19
|
+
let connectionState = 'disconnected';
|
|
20
|
+
let reconnectTimer = null;
|
|
21
|
+
let heartbeatTimer = null;
|
|
22
|
+
let reconnectAttempts = 0;
|
|
23
|
+
// Event handlers
|
|
24
|
+
let messageHandler = null;
|
|
25
|
+
let stateChangeHandler = null;
|
|
26
|
+
// Configuration
|
|
27
|
+
let config = null;
|
|
28
|
+
/**
|
|
29
|
+
* Generate unique request ID
|
|
30
|
+
*/
|
|
31
|
+
function generateReqId() {
|
|
32
|
+
return `${Date.now()}-${randomBytes(8).toString('hex')}`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Get current connection state
|
|
36
|
+
*/
|
|
37
|
+
export function getConnectionState() {
|
|
38
|
+
return connectionState;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 主动推送消息 (aibot_send_msg)
|
|
42
|
+
* 用于启动/关闭通知等场景,无需用户消息触发
|
|
43
|
+
* 注意:需用户曾与机器人对话后,才能向该会话主动推送
|
|
44
|
+
*/
|
|
45
|
+
export function sendProactiveMessage(chatId, content) {
|
|
46
|
+
if (!ws || connectionState !== 'connected') {
|
|
47
|
+
log.error('Cannot send proactive message: WebSocket not connected');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (!chatId) {
|
|
51
|
+
log.error('Cannot send proactive message: chatId is required');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const message = {
|
|
55
|
+
cmd: "aibot_send_msg" /* WeWorkCommand.AIBOT_SEND_MSG */,
|
|
56
|
+
headers: { req_id: generateReqId() },
|
|
57
|
+
body: {
|
|
58
|
+
chatid: chatId,
|
|
59
|
+
chat_type: 1, // 单聊
|
|
60
|
+
msgtype: 'markdown',
|
|
61
|
+
markdown: { content },
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
try {
|
|
65
|
+
ws.send(JSON.stringify(message));
|
|
66
|
+
log.info(`[WeWork] Sent aibot_send_msg to ${chatId}`);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
log.error('Error sending proactive message:', err);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Send reply via WebSocket (aibot_respond_msg)
|
|
74
|
+
* 长连接模式下必须用此方式回复,透传 req_id
|
|
75
|
+
*/
|
|
76
|
+
export function sendWebSocketReply(reqId, body) {
|
|
77
|
+
if (!ws || connectionState !== 'connected') {
|
|
78
|
+
log.error('Cannot send reply: WebSocket not connected');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (!reqId) {
|
|
82
|
+
log.error('Cannot send reply: req_id is required');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const message = {
|
|
86
|
+
cmd: "aibot_respond_msg" /* WeWorkCommand.AIBOT_RESPOND_MSG */,
|
|
87
|
+
headers: { req_id: reqId },
|
|
88
|
+
body,
|
|
89
|
+
};
|
|
90
|
+
try {
|
|
91
|
+
ws.send(JSON.stringify(message));
|
|
92
|
+
log.debug(`[WeWork] Sent aibot_respond_msg: msgtype=${body.msgtype}`);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
log.error('Error sending WebSocket reply:', err);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Initialize WeWork client with WebSocket connection
|
|
100
|
+
*/
|
|
101
|
+
export async function initWeWork(cfg, eventHandler, onStateChange) {
|
|
102
|
+
if (!cfg.weworkCorpId || !cfg.weworkSecret) {
|
|
103
|
+
throw new Error('WeWork botId and secret are required');
|
|
104
|
+
}
|
|
105
|
+
config = {
|
|
106
|
+
botId: cfg.weworkCorpId, // CorpId 实际上就是 botId
|
|
107
|
+
secret: cfg.weworkSecret,
|
|
108
|
+
websocketUrl: cfg.weworkWsUrl || DEFAULT_WS_URL,
|
|
109
|
+
};
|
|
110
|
+
messageHandler = eventHandler;
|
|
111
|
+
stateChangeHandler = onStateChange ?? null;
|
|
112
|
+
log.info(`Initializing WeWork client (botId: ${config.botId})`);
|
|
113
|
+
await connectWebSocket();
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Connect to WeWork WebSocket server
|
|
117
|
+
*/
|
|
118
|
+
async function connectWebSocket() {
|
|
119
|
+
if (connectionState === 'connecting') {
|
|
120
|
+
log.warn('WebSocket connection already in progress');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (!config) {
|
|
124
|
+
throw new Error('WeWork config not initialized');
|
|
125
|
+
}
|
|
126
|
+
updateState('connecting');
|
|
127
|
+
// Store config values locally to avoid null issues in Promise callback
|
|
128
|
+
const websocketUrl = config.websocketUrl || DEFAULT_WS_URL;
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
try {
|
|
131
|
+
ws = new WebSocket(websocketUrl);
|
|
132
|
+
ws.on('open', async () => {
|
|
133
|
+
log.info('WeWork WebSocket connected');
|
|
134
|
+
reconnectAttempts = 0;
|
|
135
|
+
updateState('connected');
|
|
136
|
+
startHeartbeat();
|
|
137
|
+
// 发送认证订阅消息,并等待服务端确认(否则 aibot_send_msg 会报 846609 not subscribed)
|
|
138
|
+
try {
|
|
139
|
+
await sendSubscribeAndWaitAck(resolve, reject);
|
|
140
|
+
log.info('WeWork authentication successful');
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
log.error('WeWork authentication failed:', err);
|
|
144
|
+
reject(err);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
ws.on('message', async (data) => {
|
|
148
|
+
try {
|
|
149
|
+
const message = JSON.parse(data.toString());
|
|
150
|
+
await handleMessage(message);
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
log.error('Error parsing WebSocket message:', err);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
ws.on('error', (err) => {
|
|
157
|
+
log.error('WeWork WebSocket error:', err);
|
|
158
|
+
updateState('error');
|
|
159
|
+
reject(err);
|
|
160
|
+
});
|
|
161
|
+
ws.on('close', () => {
|
|
162
|
+
log.info('WeWork WebSocket closed');
|
|
163
|
+
stopHeartbeat();
|
|
164
|
+
updateState('disconnected');
|
|
165
|
+
scheduleReconnect();
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
log.error('Error creating WebSocket connection:', err);
|
|
170
|
+
updateState('error');
|
|
171
|
+
reject(err);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/** 等待订阅确认的回调,收到服务端 errcode 响应后调用 */
|
|
176
|
+
let subscribeAckResolve = null;
|
|
177
|
+
let subscribeAckReject = null;
|
|
178
|
+
/**
|
|
179
|
+
* 发送认证订阅消息,并等待服务端 errcode: 0 确认
|
|
180
|
+
* 必须在收到确认后才能发送 aibot_send_msg,否则报 846609 not subscribed
|
|
181
|
+
*/
|
|
182
|
+
function sendSubscribeAndWaitAck(onSuccess, onError) {
|
|
183
|
+
if (!config || !ws) {
|
|
184
|
+
throw new Error('WebSocket not connected');
|
|
185
|
+
}
|
|
186
|
+
subscribeAckResolve = onSuccess;
|
|
187
|
+
subscribeAckReject = onError;
|
|
188
|
+
const subscribeMessage = {
|
|
189
|
+
cmd: "aibot_subscribe" /* WeWorkCommand.SUBSCRIBE */,
|
|
190
|
+
headers: { req_id: generateReqId() },
|
|
191
|
+
body: {
|
|
192
|
+
secret: config.secret,
|
|
193
|
+
bot_id: config.botId,
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
ws.send(JSON.stringify(subscribeMessage));
|
|
197
|
+
log.debug('Sent subscribe message, waiting for ack...');
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Handle incoming WebSocket message
|
|
201
|
+
*/
|
|
202
|
+
async function handleMessage(message) {
|
|
203
|
+
// 检查是否是响应消息(我们发送的消息的响应)
|
|
204
|
+
if ('errcode' in message) {
|
|
205
|
+
const response = message;
|
|
206
|
+
// 若在等待订阅确认,优先处理
|
|
207
|
+
if (subscribeAckResolve || subscribeAckReject) {
|
|
208
|
+
const resolve = subscribeAckResolve;
|
|
209
|
+
const reject = subscribeAckReject;
|
|
210
|
+
subscribeAckResolve = null;
|
|
211
|
+
subscribeAckReject = null;
|
|
212
|
+
if (response.errcode === 0) {
|
|
213
|
+
log.debug('Subscribe ack received');
|
|
214
|
+
resolve?.();
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
log.error(`WeWork subscribe failed: ${response.errcode} - ${response.errmsg}`);
|
|
218
|
+
reject?.(new Error(`Subscribe failed: ${response.errcode} ${response.errmsg}`));
|
|
219
|
+
}
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (response.errcode !== 0) {
|
|
223
|
+
log.error(`WeWork error response: ${response.errcode} - ${response.errmsg}`);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
log.debug('WeWork message sent successfully');
|
|
227
|
+
}
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
// 处理回调消息
|
|
231
|
+
if ('cmd' in message && message.cmd === "aibot_msg_callback" /* WeWorkCommand.AIBOT_CALLBACK */) {
|
|
232
|
+
const callback = message;
|
|
233
|
+
log.info(`[WeWork] Received message: msgtype=${callback.body.msgtype}, from=${callback.body.from.userid}, chatid=${callback.body.chatid}`);
|
|
234
|
+
if (messageHandler) {
|
|
235
|
+
try {
|
|
236
|
+
await messageHandler(callback);
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
log.error('Error in message handler:', err);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Send message to WeWork
|
|
246
|
+
*/
|
|
247
|
+
export function sendMessage(message) {
|
|
248
|
+
if (!ws || connectionState !== 'connected') {
|
|
249
|
+
log.warn('Cannot send message: WebSocket not connected');
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
ws.send(JSON.stringify(message));
|
|
254
|
+
log.info(`[WeWork] Sent message: ${message.cmd}, msgtype=${message.body.msgtype}`);
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
log.error('Error sending message:', err);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Send text message via WebSocket (requires req_id from callback)
|
|
262
|
+
*/
|
|
263
|
+
export function sendText(reqId, content) {
|
|
264
|
+
sendWebSocketReply(reqId, {
|
|
265
|
+
msgtype: 'text',
|
|
266
|
+
text: { content },
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Send stream message via WebSocket (requires req_id from callback)
|
|
271
|
+
*/
|
|
272
|
+
export function sendStream(reqId, streamId, content, finish) {
|
|
273
|
+
sendWebSocketReply(reqId, {
|
|
274
|
+
msgtype: 'stream',
|
|
275
|
+
stream: { id: streamId, finish, content },
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Update connection state and notify listeners
|
|
280
|
+
*/
|
|
281
|
+
function updateState(state) {
|
|
282
|
+
connectionState = state;
|
|
283
|
+
if (stateChangeHandler) {
|
|
284
|
+
stateChangeHandler(state);
|
|
285
|
+
}
|
|
286
|
+
log.debug('Connection state:', state);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Start heartbeat to keep connection alive
|
|
290
|
+
*/
|
|
291
|
+
function startHeartbeat() {
|
|
292
|
+
stopHeartbeat();
|
|
293
|
+
heartbeatTimer = setInterval(() => {
|
|
294
|
+
if (connectionState === 'connected' && ws) {
|
|
295
|
+
const pingMessage = {
|
|
296
|
+
cmd: "ping" /* WeWorkCommand.PING */,
|
|
297
|
+
headers: {
|
|
298
|
+
req_id: generateReqId(),
|
|
299
|
+
},
|
|
300
|
+
body: {},
|
|
301
|
+
};
|
|
302
|
+
try {
|
|
303
|
+
ws.send(JSON.stringify(pingMessage));
|
|
304
|
+
log.debug('Sent ping');
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
log.error('Error sending ping:', err);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}, HEARTBEAT_INTERVAL);
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Stop heartbeat
|
|
314
|
+
*/
|
|
315
|
+
function stopHeartbeat() {
|
|
316
|
+
if (heartbeatTimer) {
|
|
317
|
+
clearInterval(heartbeatTimer);
|
|
318
|
+
heartbeatTimer = null;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Schedule reconnection attempt
|
|
323
|
+
*/
|
|
324
|
+
function scheduleReconnect() {
|
|
325
|
+
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
|
326
|
+
log.error('Max reconnect attempts reached');
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const interval = 5000; // 5秒后重连
|
|
330
|
+
reconnectTimer = setTimeout(async () => {
|
|
331
|
+
reconnectAttempts++;
|
|
332
|
+
log.info(`Reconnecting... Attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS}`);
|
|
333
|
+
try {
|
|
334
|
+
await connectWebSocket();
|
|
335
|
+
}
|
|
336
|
+
catch (err) {
|
|
337
|
+
log.error('Reconnection failed:', err);
|
|
338
|
+
}
|
|
339
|
+
}, interval);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Stop WeWork client
|
|
343
|
+
*/
|
|
344
|
+
export function stopWeWork() {
|
|
345
|
+
stopHeartbeat();
|
|
346
|
+
if (reconnectTimer) {
|
|
347
|
+
clearTimeout(reconnectTimer);
|
|
348
|
+
reconnectTimer = null;
|
|
349
|
+
}
|
|
350
|
+
if (ws) {
|
|
351
|
+
ws.close();
|
|
352
|
+
ws = null;
|
|
353
|
+
}
|
|
354
|
+
updateState('disconnected');
|
|
355
|
+
log.info('WeWork client stopped');
|
|
356
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeWork (企业微信) Event Handler - Handle WeWork message events
|
|
3
|
+
*/
|
|
4
|
+
import type { Config } from '../config.js';
|
|
5
|
+
import type { SessionManager } from '../session/session-manager.js';
|
|
6
|
+
import type { WeWorkCallbackMessage } from './types.js';
|
|
7
|
+
export interface WeWorkEventHandlerHandle {
|
|
8
|
+
stop: () => void;
|
|
9
|
+
getRunningTaskCount: () => number;
|
|
10
|
+
handleEvent: (data: WeWorkCallbackMessage) => Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
export declare function setupWeWorkHandlers(config: Config, sessionManager: SessionManager): WeWorkEventHandlerHandle;
|