@jiexiaoyin/wecom-api 0.0.2
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 +228 -0
- package/config.example.json +7 -0
- package/config.js +76 -0
- package/docs/approval-templates.example.json +11 -0
- package/docs/nginx-mirror.md +193 -0
- package/openclaw.plugin.json +15 -0
- package/package.json +34 -0
- package/plugin.cjs +172 -0
- package/plugin.ts +136 -0
- package/skills/wecom-api/SKILL.md +40 -0
- package/skills/wecom-api/index.js +288 -0
- package/skills/wecom-api/openclaw.plugin.json +10 -0
- package/src/callback-helper.js +198 -0
- package/src/config.cjs +286 -0
- package/src/core/permission.js +479 -0
- package/src/crypto.js +130 -0
- package/src/index.js +199 -0
- package/src/modules/addressbook/index.js +413 -0
- package/src/modules/addressbook_cache/index.js +365 -0
- package/src/modules/advanced/index.js +159 -0
- package/src/modules/app/index.js +102 -0
- package/src/modules/approval/index.js +146 -0
- package/src/modules/auth/index.js +103 -0
- package/src/modules/callback/index.js +1180 -0
- package/src/modules/chain/index.js +193 -0
- package/src/modules/checkin/index.js +142 -0
- package/src/modules/checkin_rules/index.js +251 -0
- package/src/modules/contact/index.js +481 -0
- package/src/modules/contact_stats/index.js +349 -0
- package/src/modules/custom/index.js +140 -0
- package/src/modules/customer/index.js +51 -0
- package/src/modules/disk/index.js +245 -0
- package/src/modules/document/index.js +282 -0
- package/src/modules/hr/index.js +93 -0
- package/src/modules/intelligence/index.js +346 -0
- package/src/modules/kf/index.js +74 -0
- package/src/modules/live/index.js +122 -0
- package/src/modules/media/index.js +183 -0
- package/src/modules/meeting/index.js +665 -0
- package/src/modules/message/index.js +402 -0
- package/src/modules/messenger/index.js +208 -0
- package/src/modules/moments/index.js +161 -0
- package/src/modules/msgaudit/index.js +24 -0
- package/src/modules/notify/index.js +81 -0
- package/src/modules/oceanengine/index.js +199 -0
- package/src/modules/openchat/index.js +197 -0
- package/src/modules/phone/index.js +45 -0
- package/src/modules/room/index.js +178 -0
- package/src/modules/schedule/index.js +246 -0
- package/src/modules/school/index.js +199 -0
- package/src/modules/security/index.js +223 -0
- package/src/modules/sensitive/index.js +170 -0
- package/src/modules/thirdparty/index.js +145 -0
- package/src/sdk/index.js +269 -0
- package/src/utils/callback-helper.js +198 -0
- package/test/callback-crypto.test.js +55 -0
- package/test/crypto.test.js +85 -0
- package/test/permission.test.js +115 -0
|
@@ -0,0 +1,1180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 回调处理模块
|
|
3
|
+
* 企业微信回调消息处理框架
|
|
4
|
+
*
|
|
5
|
+
* 功能:
|
|
6
|
+
* 1. 统一回调入口 - 处理所有企业微信回调事件
|
|
7
|
+
* 2. 事件记录 - 自动记录所有回调事件
|
|
8
|
+
* 3. 事件分发 - 根据事件类型自动调用对应处理器
|
|
9
|
+
* 4. 响应生成 - 自动生成响应消息
|
|
10
|
+
*
|
|
11
|
+
* 使用方式:
|
|
12
|
+
* const callback = new Callback(config);
|
|
13
|
+
* callback.on('change_contact', async (event) => { ... });
|
|
14
|
+
* callback.handle(req, res);
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const crypto = require('crypto');
|
|
18
|
+
const xml2js = require('xml2js');
|
|
19
|
+
const EventEmitter = require('events');
|
|
20
|
+
const { verifyWecomSignature, decryptWecomEncrypted, encryptWecomPlaintext } = require('../../crypto');
|
|
21
|
+
const Approval = require('../approval');
|
|
22
|
+
|
|
23
|
+
class Callback extends EventEmitter {
|
|
24
|
+
constructor(config) {
|
|
25
|
+
super();
|
|
26
|
+
this.token = config.token || '';
|
|
27
|
+
this.encodingAESKey = config.encodingAESKey || '';
|
|
28
|
+
this.corpId = config.corpId || '';
|
|
29
|
+
this.agentId = config.agentId || '';
|
|
30
|
+
|
|
31
|
+
// 事件记录存储
|
|
32
|
+
this.eventHistory = [];
|
|
33
|
+
this.maxHistorySize = config.maxHistorySize || 1000;
|
|
34
|
+
|
|
35
|
+
// 事件处理器映射
|
|
36
|
+
this.handlers = {};
|
|
37
|
+
|
|
38
|
+
// 审批模块(用于获取审批详情)
|
|
39
|
+
this.approval = new Approval(config);
|
|
40
|
+
|
|
41
|
+
// 初始化默认处理器
|
|
42
|
+
this._initDefaultHandlers();
|
|
43
|
+
|
|
44
|
+
// 加载已有事件历史
|
|
45
|
+
this.loadEventHistory();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ========== 初始化默认处理器 ==========
|
|
49
|
+
|
|
50
|
+
_initDefaultHandlers() {
|
|
51
|
+
// 通讯录事件
|
|
52
|
+
this.handlers['change_contact'] = 'handleContactChange';
|
|
53
|
+
this.handlers['change_external_contact'] = 'handleExternalContactChange';
|
|
54
|
+
|
|
55
|
+
// 客户联系事件
|
|
56
|
+
this.handlers['add_external_contact'] = 'handleAddExternalContact';
|
|
57
|
+
this.handlers['del_external_contact'] = 'handleDelExternalContact';
|
|
58
|
+
this.handlers['edit_external_contact'] = 'handleEditExternalContact';
|
|
59
|
+
this.handlers['add_half_external_contact'] = 'handleAddHalfExternalContact';
|
|
60
|
+
this.handlers['del_follow_user'] = 'handleDelFollowUser';
|
|
61
|
+
this.handlers['transfer_fail'] = 'handleTransferFail';
|
|
62
|
+
|
|
63
|
+
// 客户群事件
|
|
64
|
+
this.handlers['create_chat'] = 'handleCreateChat';
|
|
65
|
+
this.handlers['update_chat'] = 'handleUpdateChat';
|
|
66
|
+
this.handlers['dismiss_chat'] = 'handleDismissChat';
|
|
67
|
+
|
|
68
|
+
// 消息事件
|
|
69
|
+
this.handlers['user_click'] = 'handleUserClick';
|
|
70
|
+
this.handlers['view'] = 'handleUserView';
|
|
71
|
+
this.handlers['scancode_push'] = 'handleScanCodePush';
|
|
72
|
+
this.handlers['scancode_waitmsg'] = 'handleScanCodeWaitMsg';
|
|
73
|
+
this.handlers['pic_sysphoto'] = 'handlePicSysPhoto';
|
|
74
|
+
this.handlers['pic_photo_or_album'] = 'handlePicPhotoOrAlbum';
|
|
75
|
+
this.handlers['pic_weixin'] = 'handlePicWeixin';
|
|
76
|
+
this.handlers['location_select'] = 'handleLocationSelect';
|
|
77
|
+
this.handlers['enter_agent'] = 'handleEnterAgent';
|
|
78
|
+
this.handlers['message'] = 'handleMessage';
|
|
79
|
+
|
|
80
|
+
// 审批事件
|
|
81
|
+
this.handlers['submit_approval'] = 'handleSubmitApproval';
|
|
82
|
+
this.handlers['sys_approval_change'] = 'handleSysApprovalChange';
|
|
83
|
+
this.handlers['Approval'] = 'handleApproval';
|
|
84
|
+
|
|
85
|
+
// 打卡事件
|
|
86
|
+
this.handlers['checkin'] = 'handleCheckin';
|
|
87
|
+
this.handlers['report_checkin'] = 'handleReportCheckin';
|
|
88
|
+
|
|
89
|
+
// 会议事件
|
|
90
|
+
this.handlers['meeting_start'] = 'handleMeetingStart';
|
|
91
|
+
this.handlers['meeting_end'] = 'handleMeetingEnd';
|
|
92
|
+
this.handlers['meeting_created'] = 'handleMeetingCreated';
|
|
93
|
+
this.handlers['meeting_cancelled'] = 'handleMeetingCancelled';
|
|
94
|
+
|
|
95
|
+
// 回调验证事件
|
|
96
|
+
this.handlers['url_verification'] = 'handleUrlVerification';
|
|
97
|
+
this.handlers['callback_verification'] = 'handleCallbackVerification';
|
|
98
|
+
|
|
99
|
+
// ========== 客户联系回调(新增)==========
|
|
100
|
+
// 联系我相关
|
|
101
|
+
this.handlers['add_contact_way'] = 'handleAddContactWay';
|
|
102
|
+
this.handlers['del_contact_way'] = 'handleDelContactWay';
|
|
103
|
+
|
|
104
|
+
// 入群方式相关
|
|
105
|
+
this.handlers['add_join_way'] = 'handleAddJoinWay';
|
|
106
|
+
this.handlers['del_join_way'] = 'handleDelJoinWay';
|
|
107
|
+
|
|
108
|
+
// 客服消息相关
|
|
109
|
+
this.handlers['kf_msg_push'] = 'handleKfMsgPush';
|
|
110
|
+
this.handlers['kf_msg_send'] = 'handleKfMsgSend';
|
|
111
|
+
this.handlers['msg_dialogice_send'] = 'handleMsgDialogiceSend';
|
|
112
|
+
|
|
113
|
+
// ========== 通讯录变更回调(新增)==========
|
|
114
|
+
this.handlers['change_member'] = 'handleChangeMember';
|
|
115
|
+
this.handlers['change_department'] = 'handleChangeDepartment';
|
|
116
|
+
this.handlers['change_tag'] = 'handleChangeTag';
|
|
117
|
+
|
|
118
|
+
// ========== 会议回调(补充)==========
|
|
119
|
+
this.handlers['meeting_ended'] = 'handleMeetingEnded';
|
|
120
|
+
this.handlers['meetingParticipantJoin'] = 'handleMeetingParticipantJoin';
|
|
121
|
+
this.handlers['meetingParticipantLeave'] = 'handleMeetingParticipantLeave';
|
|
122
|
+
|
|
123
|
+
// ========== 直播回调 ==========
|
|
124
|
+
this.handlers['living_status'] = 'handleLivingStatus';
|
|
125
|
+
|
|
126
|
+
// ========== 微盘回调 ==========
|
|
127
|
+
this.handlers['change_psm'] = 'handleChangePsm';
|
|
128
|
+
this.handlers['change_disk'] = 'handleChangeDisk';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ========== 消息验证 ==========
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 验证 URL(用于首次配置回调)
|
|
135
|
+
* @param {string} msgSignature 签名
|
|
136
|
+
* @param {string} timestamp 时间戳
|
|
137
|
+
* @param {string} nonce 随机字符串
|
|
138
|
+
* @param {string} echostr 加密的随机字符串
|
|
139
|
+
*/
|
|
140
|
+
verifyURL(msgSignature, timestamp, nonce, echostr) {
|
|
141
|
+
const signature = this.getSignature(timestamp, nonce, echostr);
|
|
142
|
+
if (signature !== msgSignature) {
|
|
143
|
+
return { success: false, message: '签名验证失败' };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const decrypted = this.decrypt(echostr);
|
|
147
|
+
return { success: true, echostr: decrypted };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 验证消息签名
|
|
152
|
+
* @param {string} msgSignature 签名
|
|
153
|
+
* @param {string} timestamp 时间戳
|
|
154
|
+
* @param {string} nonce 随机字符串
|
|
155
|
+
* @param {string} encrypt 加密内容
|
|
156
|
+
*/
|
|
157
|
+
verifyMessage(msgSignature, timestamp, nonce, encrypt) {
|
|
158
|
+
console.log('[wecom-api] verifyMessage params:', {
|
|
159
|
+
token: this.token,
|
|
160
|
+
timestamp,
|
|
161
|
+
nonce,
|
|
162
|
+
encrypt: encrypt ? encrypt.substring(0, 50) + '...' : null,
|
|
163
|
+
signature: msgSignature ? msgSignature.substring(0, 50) + '...' : null
|
|
164
|
+
});
|
|
165
|
+
const result = verifyWecomSignature({
|
|
166
|
+
token: this.token,
|
|
167
|
+
timestamp,
|
|
168
|
+
nonce,
|
|
169
|
+
encrypt,
|
|
170
|
+
signature: msgSignature
|
|
171
|
+
});
|
|
172
|
+
console.log('[wecom-api] verifyMessage result:', result);
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ========== 消息解密 ==========
|
|
177
|
+
|
|
178
|
+
decrypt(encrypt) {
|
|
179
|
+
try {
|
|
180
|
+
return decryptWecomEncrypted({
|
|
181
|
+
encodingAESKey: this.encodingAESKey,
|
|
182
|
+
encrypt
|
|
183
|
+
});
|
|
184
|
+
} catch (e) {
|
|
185
|
+
throw new Error('解密失败: ' + e.message);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 加密消息
|
|
191
|
+
* @param {string} content 消息内容
|
|
192
|
+
* @param {string} replyNonce 回复随机字符串
|
|
193
|
+
* @param {number} timestamp 时间戳
|
|
194
|
+
*/
|
|
195
|
+
encrypt(content, replyNonce, timestamp) {
|
|
196
|
+
try {
|
|
197
|
+
const encrypted = encryptWecomPlaintext({
|
|
198
|
+
encodingAESKey: this.encodingAESKey,
|
|
199
|
+
receiveId: this.corpId,
|
|
200
|
+
plaintext: content
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const signature = this.getSignature(timestamp, replyNonce, encrypted);
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
encrypt: encrypted,
|
|
207
|
+
signature,
|
|
208
|
+
timestamp,
|
|
209
|
+
nonce: replyNonce
|
|
210
|
+
};
|
|
211
|
+
} catch (e) {
|
|
212
|
+
throw new Error('加密失败: ' + e.message);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ========== 消息解析 ==========
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* 解析 XML 消息
|
|
220
|
+
* @param {string} xmlContent XML 内容
|
|
221
|
+
*/
|
|
222
|
+
async parseXML(xmlContent) {
|
|
223
|
+
const parser = new xml2js.Parser({ explicitArray: false });
|
|
224
|
+
const result = await parser.parseStringPromise(xmlContent);
|
|
225
|
+
return result.xml;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 解析消息事件
|
|
230
|
+
* @param {string} xmlContent XML 内容
|
|
231
|
+
*/
|
|
232
|
+
async parseMessage(xmlContent) {
|
|
233
|
+
const xml = await this.parseXML(xmlContent);
|
|
234
|
+
console.log('[wecom-api] parseMessage xml keys:', Object.keys(xml));
|
|
235
|
+
console.log('[wecom-api] parseMessage xml:', JSON.stringify(xml).substring(0, 500));
|
|
236
|
+
|
|
237
|
+
const message = {
|
|
238
|
+
ToUserName: xml.ToUserName,
|
|
239
|
+
FromUserName: xml.FromUserName,
|
|
240
|
+
CreateTime: parseInt(xml.CreateTime),
|
|
241
|
+
MsgType: xml.MsgType,
|
|
242
|
+
Content: xml.Content,
|
|
243
|
+
MsgId: xml.MsgId,
|
|
244
|
+
Event: xml.Event,
|
|
245
|
+
EventKey: xml.EventKey,
|
|
246
|
+
AgentID: xml.AgentID,
|
|
247
|
+
// 媒体相关
|
|
248
|
+
MediaId: xml.MediaId,
|
|
249
|
+
PicUrl: xml.PicUrl,
|
|
250
|
+
Format: xml.Format,
|
|
251
|
+
ThumbMediaId: xml.ThumbMediaId,
|
|
252
|
+
// 位置相关
|
|
253
|
+
Location_X: xml.Location_X,
|
|
254
|
+
Location_Y: xml.Location_Y,
|
|
255
|
+
Scale: xml.Scale,
|
|
256
|
+
Label: xml.Label,
|
|
257
|
+
// 链接相关
|
|
258
|
+
Title: xml.Title,
|
|
259
|
+
Description: xml.Description,
|
|
260
|
+
Url: xml.Url,
|
|
261
|
+
// 加密消息
|
|
262
|
+
Encrypt: xml.Encrypt,
|
|
263
|
+
// 外部联系人变更事件关键字段
|
|
264
|
+
ChangeType: xml.ChangeType,
|
|
265
|
+
ExternalUserId: xml.ExternalUserId,
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
return message;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ========== 统一回调入口 ==========
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* 处理企业微信回调请求(统一入口)
|
|
275
|
+
* @param {object} params 请求参数
|
|
276
|
+
* @param {string} params.msgSignature 签名
|
|
277
|
+
* @param {string} params.timestamp 时间戳
|
|
278
|
+
* @param {string} params.nonce 随机字符串
|
|
279
|
+
* @param {string} params.echostr 加密字符串(验证URL时)
|
|
280
|
+
* @param {string} params.xmlBody XML请求体
|
|
281
|
+
* @returns {Promise<object>} 处理结果
|
|
282
|
+
*/
|
|
283
|
+
async handle(params) {
|
|
284
|
+
const { msgSignature, timestamp, nonce, echostr, xmlBody } = params;
|
|
285
|
+
|
|
286
|
+
// URL验证模式(首次配置回调)
|
|
287
|
+
if (echostr) {
|
|
288
|
+
return this._handleUrlVerification(msgSignature, timestamp, nonce, echostr);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 消息处理模式
|
|
292
|
+
if (xmlBody) {
|
|
293
|
+
return this._handleMessage(msgSignature, timestamp, nonce, xmlBody);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
throw new Error('缺少必要参数');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* 处理 URL 验证
|
|
301
|
+
*/
|
|
302
|
+
async _handleUrlVerification(msgSignature, timestamp, nonce, echostr) {
|
|
303
|
+
const result = this.verifyURL(msgSignature, timestamp, nonce, echostr);
|
|
304
|
+
|
|
305
|
+
if (result.success) {
|
|
306
|
+
// 记录事件
|
|
307
|
+
this._recordEvent({
|
|
308
|
+
type: 'url_verification',
|
|
309
|
+
success: true,
|
|
310
|
+
timestamp: Date.now()
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
type: 'success',
|
|
315
|
+
body: result.echostr
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
type: 'error',
|
|
321
|
+
message: result.message
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* 处理消息事件
|
|
327
|
+
*/
|
|
328
|
+
async _handleMessage(msgSignature, timestamp, nonce, xmlBody) {
|
|
329
|
+
console.log('[wecom-api] _handleMessage called');
|
|
330
|
+
console.log('[wecom-api] xmlBody preview:', xmlBody ? xmlBody.substring(0, 300) : 'missing');
|
|
331
|
+
const xml = await this.parseXML(xmlBody);
|
|
332
|
+
console.log('[wecom-api] parsed xml Encrypt length:', xml.Encrypt ? xml.Encrypt.length : 'null');
|
|
333
|
+
const encrypt = xml.Encrypt;
|
|
334
|
+
|
|
335
|
+
// 验证签名
|
|
336
|
+
console.log('[wecom-api] 验证签名');
|
|
337
|
+
if (!this.verifyMessage(msgSignature, timestamp, nonce, encrypt)) {
|
|
338
|
+
throw new Error('签名验证失败');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// 解密消息
|
|
342
|
+
console.log('[wecom-api] decrypting...');
|
|
343
|
+
const decryptedXml = this.decrypt(encrypt);
|
|
344
|
+
const message = await this.parseMessage(decryptedXml);
|
|
345
|
+
|
|
346
|
+
// 记录事件
|
|
347
|
+
const eventRecord = this._recordEvent({
|
|
348
|
+
type: message.Event || message.MsgType,
|
|
349
|
+
msgType: message.MsgType,
|
|
350
|
+
fromUserName: message.FromUserName,
|
|
351
|
+
createTime: message.CreateTime,
|
|
352
|
+
event: message.Event,
|
|
353
|
+
eventKey: message.EventKey,
|
|
354
|
+
agentID: message.AgentID,
|
|
355
|
+
raw: message,
|
|
356
|
+
timestamp: Date.now()
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// 触发事件
|
|
360
|
+
this.emit(message.Event || message.MsgType, message, eventRecord);
|
|
361
|
+
|
|
362
|
+
// 调用对应处理器
|
|
363
|
+
const handlerName = this.handlers[message.Event || message.MsgType];
|
|
364
|
+
if (handlerName && typeof this[handlerName] === 'function') {
|
|
365
|
+
await this[handlerName](message, eventRecord);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// 返回成功响应
|
|
369
|
+
return {
|
|
370
|
+
type: 'success',
|
|
371
|
+
body: 'success',
|
|
372
|
+
message: message // 返回解析后的消息对象
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ========== 事件记录 ==========
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* 记录回调事件
|
|
380
|
+
* @param {object} event 事件数据
|
|
381
|
+
*/
|
|
382
|
+
_recordEvent(event) {
|
|
383
|
+
const record = {
|
|
384
|
+
id: this._generateId(),
|
|
385
|
+
...event,
|
|
386
|
+
timestamp: event.timestamp || Date.now()
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
this.eventHistory.unshift(record);
|
|
390
|
+
|
|
391
|
+
// 限制历史记录数量
|
|
392
|
+
if (this.eventHistory.length > this.maxHistorySize) {
|
|
393
|
+
this.eventHistory.pop();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// 根据类型分别保存到不同文件
|
|
397
|
+
if (record.type === 'text' || record.msgType === 'text') {
|
|
398
|
+
this._appendToFile('message_history.jsonl', record);
|
|
399
|
+
} else {
|
|
400
|
+
this._appendToFile('event_history.jsonl', record);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return record;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* 获取事件历史记录
|
|
408
|
+
* @param {object} options 查询选项
|
|
409
|
+
* @param {string} options.type 事件类型
|
|
410
|
+
* @param {number} options.limit 返回数量
|
|
411
|
+
* @param {number} options.offset 偏移量
|
|
412
|
+
*/
|
|
413
|
+
getEventHistory(options = {}) {
|
|
414
|
+
let { type, limit = 100, offset = 0 } = options;
|
|
415
|
+
|
|
416
|
+
let history = this.eventHistory;
|
|
417
|
+
|
|
418
|
+
if (type) {
|
|
419
|
+
history = history.filter(e => e.type === type);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return history.slice(offset, offset + limit);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* 获取事件详情
|
|
427
|
+
* @param {string} eventId 事件ID
|
|
428
|
+
*/
|
|
429
|
+
getEventById(eventId) {
|
|
430
|
+
return this.eventHistory.find(e => e.id === eventId);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* 清空事件历史
|
|
435
|
+
*/
|
|
436
|
+
clearEventHistory() {
|
|
437
|
+
this.eventHistory = [];
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* 导出事件历史到文件
|
|
442
|
+
* @param {string} filePath 文件路径
|
|
443
|
+
*/
|
|
444
|
+
async exportEventHistory(filePath) {
|
|
445
|
+
const fs = require('fs');
|
|
446
|
+
const data = JSON.stringify(this.eventHistory, null, 2);
|
|
447
|
+
fs.writeFileSync(filePath, data, 'utf8');
|
|
448
|
+
return { success: true, count: this.eventHistory.length, filePath };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* 追加记录到指定文件
|
|
453
|
+
* @param {string} filename 文件名
|
|
454
|
+
* @param {object} record 单条记录
|
|
455
|
+
*/
|
|
456
|
+
_appendToFile(filename, record) {
|
|
457
|
+
try {
|
|
458
|
+
const fs = require('fs');
|
|
459
|
+
const path = require('path');
|
|
460
|
+
const dir = '/root/.openclaw/extensions/wecom-api/data';
|
|
461
|
+
if (!fs.existsSync(dir)) {
|
|
462
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
463
|
+
}
|
|
464
|
+
const filePath = path.join(dir, filename);
|
|
465
|
+
fs.appendFileSync(filePath, JSON.stringify(record) + '\n', 'utf8');
|
|
466
|
+
} catch (e) {
|
|
467
|
+
console.log('[wecom-api] 写入文件失败:', e.message);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* 加载事件历史(兼容旧数组格式 + 新增追加格式)
|
|
473
|
+
* 文本消息从 message_history.jsonl 加载,事件从 event_history.jsonl 加载
|
|
474
|
+
*/
|
|
475
|
+
loadEventHistory() {
|
|
476
|
+
try {
|
|
477
|
+
const fs = require('fs');
|
|
478
|
+
const path = require('path');
|
|
479
|
+
const dataDir = '/root/.openclaw/extensions/wecom-api/data';
|
|
480
|
+
const records = [];
|
|
481
|
+
|
|
482
|
+
const files = ['event_history.jsonl', 'message_history.jsonl'];
|
|
483
|
+
|
|
484
|
+
for (const filename of files) {
|
|
485
|
+
const filePath = path.join(dataDir, filename);
|
|
486
|
+
|
|
487
|
+
if (!fs.existsSync(filePath)) {
|
|
488
|
+
// 尝试旧格式数组文件
|
|
489
|
+
const oldPath = path.join(dataDir, 'event_history.jsonl');
|
|
490
|
+
if (fs.existsSync(oldPath)) {
|
|
491
|
+
const raw = fs.readFileSync(oldPath, 'utf8').trim();
|
|
492
|
+
if (raw.startsWith('[')) {
|
|
493
|
+
const oldRecords = JSON.parse(raw);
|
|
494
|
+
records.push(...oldRecords);
|
|
495
|
+
console.log('[wecom-api] 兼容加载旧格式:', oldRecords.length, '条 from', filename);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const raw = fs.readFileSync(filePath, 'utf8').trim();
|
|
502
|
+
if (!raw) continue;
|
|
503
|
+
|
|
504
|
+
const fileRecords = raw.split('\n')
|
|
505
|
+
.filter(line => line.trim())
|
|
506
|
+
.map(line => JSON.parse(line));
|
|
507
|
+
records.push(...fileRecords);
|
|
508
|
+
console.log('[wecom-api] 加载', filename, ':', fileRecords.length, '条');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// 按时间倒序
|
|
512
|
+
records.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
|
|
513
|
+
this.eventHistory = records.slice(0, this.maxHistorySize);
|
|
514
|
+
console.log('[wecom-api] 事件历史合计:', this.eventHistory.length, '条');
|
|
515
|
+
} catch (e) {
|
|
516
|
+
console.log('[wecom-api] 加载事件历史失败:', e.message);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* 加载事件历史从文件(兼容旧数组格式 + 新追加格式)
|
|
522
|
+
*/
|
|
523
|
+
loadEventHistory() {
|
|
524
|
+
try {
|
|
525
|
+
const fs = require('fs');
|
|
526
|
+
const path = require('path');
|
|
527
|
+
const filePath = '/root/.openclaw/extensions/wecom-api/data/event_history.jsonl';
|
|
528
|
+
|
|
529
|
+
if (!fs.existsSync(filePath)) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const raw = fs.readFileSync(filePath, 'utf8').trim();
|
|
534
|
+
if (!raw) {
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// 兼容旧格式(JSON 数组)
|
|
539
|
+
if (raw.startsWith('[')) {
|
|
540
|
+
this.eventHistory = JSON.parse(raw);
|
|
541
|
+
} else {
|
|
542
|
+
// 新格式:每行一个 JSON 对象
|
|
543
|
+
this.eventHistory = raw.split('\n')
|
|
544
|
+
.filter(line => line.trim())
|
|
545
|
+
.map(line => JSON.parse(line))
|
|
546
|
+
.reverse(); // reverse 让最新的在前面(与 unshift 一致)
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
console.log('[wecom-api] 已加载事件历史:', this.eventHistory.length, '条');
|
|
550
|
+
} catch (e) {
|
|
551
|
+
console.log('[wecom-api] 加载事件历史失败:', e.message);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ========== 事件处理器 ==========
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* 通讯录变更事件
|
|
559
|
+
*/
|
|
560
|
+
/**
|
|
561
|
+
* 通讯录变更事件(通用)
|
|
562
|
+
* 关键字段: ChangeType(1=新增 2=更新 3=删除), UserID, Name, Department
|
|
563
|
+
*/
|
|
564
|
+
async handleContactChange(event, record) {
|
|
565
|
+
const ct = event.ChangeType || event.change_type || 0;
|
|
566
|
+
const ctMap = {1:'新增', 2:'更新', 3:'删除'};
|
|
567
|
+
record.changeType = ct;
|
|
568
|
+
record.userId = event.UserID || event.userid || '';
|
|
569
|
+
record.name = event.Name || event.name || '';
|
|
570
|
+
record.department = event.Department || event.department || [];
|
|
571
|
+
record.changeTypeText = ctMap[ct] || '变更(' + ct + ')';
|
|
572
|
+
console.log('[Callback] 通讯录变更: ' + record.changeTypeText + ' UserId=' + record.userId);
|
|
573
|
+
return { handled: true, changeType: record.changeTypeText, userId: record.userId };
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* 外部联系人变更事件
|
|
578
|
+
*/
|
|
579
|
+
/**
|
|
580
|
+
* 外部联系人变更事件
|
|
581
|
+
* 关键字段: ChangeType(1=新增 4=删除), UserID, ExternalUserId
|
|
582
|
+
*/
|
|
583
|
+
async handleExternalContactChange(event, record) {
|
|
584
|
+
const changeType = event.ChangeType || event.change_type || 0;
|
|
585
|
+
const userId = event.UserID || event.userid || '';
|
|
586
|
+
const externalUserId = event.ExternalUserId || event.external_userid || '';
|
|
587
|
+
record.changeType = changeType;
|
|
588
|
+
record.userId = userId;
|
|
589
|
+
record.externalUserId = externalUserId;
|
|
590
|
+
const actionMap = {1:'新增', 4:'删除'};
|
|
591
|
+
record.action = actionMap[changeType] || '变更(' + changeType + ')';
|
|
592
|
+
console.log('[Callback] 外部联系人变更: ' + record.action + ' UserId=' + userId + ' ExternalUserId=' + externalUserId);
|
|
593
|
+
return { handled: true, changeType: record.action, userId, externalUserId };
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* 添加外部联系人
|
|
598
|
+
* 关键字段: UserID, ExternalUserId, WelcomeCode
|
|
599
|
+
*/
|
|
600
|
+
async handleAddExternalContact(event, record) {
|
|
601
|
+
record.userId = event.UserID || event.userid || '';
|
|
602
|
+
record.externalUserId = event.ExternalUserId || event.external_userid || '';
|
|
603
|
+
record.welcomeCode = event.WelcomeCode || event.welcome_code || '';
|
|
604
|
+
console.log('[Callback] 添加外部联系人: UserId=' + record.userId + ' ExternalUserId=' + record.externalUserId);
|
|
605
|
+
this.emit('external_contact:add', event, record);
|
|
606
|
+
return { handled: true, userId: record.userId, externalUserId: record.externalUserId };
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* 删除外部联系人
|
|
611
|
+
* 关键字段: UserID, ExternalUserId
|
|
612
|
+
*/
|
|
613
|
+
async handleDelExternalContact(event, record) {
|
|
614
|
+
record.userId = event.UserID || event.userid || '';
|
|
615
|
+
record.externalUserId = event.ExternalUserId || event.external_userid || '';
|
|
616
|
+
console.log('[Callback] 删除外部联系人: UserId=' + record.userId + ' ExternalUserId=' + record.externalUserId);
|
|
617
|
+
this.emit('external_contact:del', event, record);
|
|
618
|
+
return { handled: true, userId: record.userId, externalUserId: record.externalUserId };
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* 编辑外部联系人
|
|
623
|
+
*/
|
|
624
|
+
/**
|
|
625
|
+
* 编辑外部联系人
|
|
626
|
+
* 关键字段: UserID, ExternalUserId
|
|
627
|
+
*/
|
|
628
|
+
async handleEditExternalContact(event, record) {
|
|
629
|
+
record.userId = event.UserID || event.userid || '';
|
|
630
|
+
record.externalUserId = event.ExternalUserId || event.external_userid || '';
|
|
631
|
+
console.log('[Callback] 编辑外部联系人: UserId=' + record.userId + ' ExternalUserId=' + record.externalUserId);
|
|
632
|
+
return { handled: true, userId: record.userId, externalUserId: record.externalUserId };
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* 添加半程外部联系人
|
|
637
|
+
*/
|
|
638
|
+
/**
|
|
639
|
+
* 添加半程外部联系人
|
|
640
|
+
* 关键字段: UserID, ExternalUserId, HalfAuthChangeType
|
|
641
|
+
*/
|
|
642
|
+
async handleAddHalfExternalContact(event, record) {
|
|
643
|
+
record.userId = event.UserID || event.userid || '';
|
|
644
|
+
record.externalUserId = event.ExternalUserId || event.external_userid || '';
|
|
645
|
+
record.halfAuthChangeType = event.HalfAuthChangeType || event.half_auth_change_type || 0;
|
|
646
|
+
console.log('[Callback] 添加半程外部联系人: UserId=' + record.userId + ' ExternalUserId=' + record.externalUserId);
|
|
647
|
+
return { handled: true, userId: record.userId, externalUserId: record.externalUserId };
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* 删除跟进成员
|
|
652
|
+
* 关键字段: UserID, ExternalUserId
|
|
653
|
+
*/
|
|
654
|
+
async handleDelFollowUser(event, record) {
|
|
655
|
+
record.userId = event.UserID || event.userid || '';
|
|
656
|
+
record.externalUserId = event.ExternalUserId || event.external_userid || '';
|
|
657
|
+
console.log('[Callback] 删除跟进成员: UserId=' + record.userId + ' ExternalUserId=' + record.externalUserId);
|
|
658
|
+
return { handled: true, userId: record.userId, externalUserId: record.externalUserId };
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* 客户接替失败
|
|
663
|
+
* 事件Key: transfer_fail
|
|
664
|
+
* 关键字段: UserID, ExternalUserId, FailReason
|
|
665
|
+
*/
|
|
666
|
+
async handleTransferFail(event, record) {
|
|
667
|
+
record.userId = event.UserID || event.userid || '';
|
|
668
|
+
record.externalUserId = event.ExternalUserId || event.external_userid || '';
|
|
669
|
+
record.failReason = event.FailReason || event.fail_reason || '';
|
|
670
|
+
console.log('[Callback] 客户接替失败: UserId=' + record.userId + ' ExternalUserId=' + record.externalUserId + ' Reason=' + record.failReason);
|
|
671
|
+
return { handled: true, userId: record.userId, failReason: record.failReason };
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* 客户群创建
|
|
676
|
+
* 关键字段: ChatId, Creator, CreateTime, MemberCount
|
|
677
|
+
*/
|
|
678
|
+
async handleCreateChat(event, record) {
|
|
679
|
+
record.chatId = event.ChatId || event.chat_id || '';
|
|
680
|
+
record.creator = event.Creator || event.creator || '';
|
|
681
|
+
record.createTime = event.CreateTime || event.create_time || 0;
|
|
682
|
+
record.memberCount = event.MemberCount || event.member_count || 0;
|
|
683
|
+
console.log('[Callback] 客户群创建: ChatId=' + record.chatId + ' Creator=' + record.creator);
|
|
684
|
+
this.emit('group_chat:create', event, record);
|
|
685
|
+
return { handled: true, chatId: record.chatId, creator: record.creator };
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* 客户群变更
|
|
690
|
+
* 关键字段: ChatId, UpdateType, UpdateDetail
|
|
691
|
+
*/
|
|
692
|
+
async handleUpdateChat(event, record) {
|
|
693
|
+
record.chatId = event.ChatId || event.chat_id || '';
|
|
694
|
+
record.updateType = event.UpdateType || event.update_type || '';
|
|
695
|
+
record.updateDetail = event.UpdateDetail || event.update_detail || '';
|
|
696
|
+
console.log('[Callback] 客户群变更: ChatId=' + record.chatId + ' UpdateType=' + record.updateType);
|
|
697
|
+
this.emit('group_chat:update', event, record);
|
|
698
|
+
return { handled: true, chatId: record.chatId, updateType: record.updateType };
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* 客户群解散
|
|
703
|
+
* 关键字段: ChatId, Creator, CreateTime
|
|
704
|
+
*/
|
|
705
|
+
async handleDismissChat(event, record) {
|
|
706
|
+
record.chatId = event.ChatId || event.chat_id || '';
|
|
707
|
+
record.creator = event.Creator || event.creator || '';
|
|
708
|
+
record.createTime = event.CreateTime || event.create_time || 0;
|
|
709
|
+
console.log('[Callback] 客户群解散: ChatId=' + record.chatId + ' Creator=' + record.creator);
|
|
710
|
+
this.emit('group_chat:dismiss', event, record);
|
|
711
|
+
return { handled: true, chatId: record.chatId };
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* 用户点击菜单
|
|
716
|
+
*/
|
|
717
|
+
async handleUserClick(event, record) {
|
|
718
|
+
console.log('[Callback] 用户点击:', event);
|
|
719
|
+
this.emit('menu:click', event, record);
|
|
720
|
+
return { handled: true };
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* 用户点击链接
|
|
725
|
+
*/
|
|
726
|
+
async handleUserView(event, record) {
|
|
727
|
+
console.log('[Callback] 用户点击链接:', event);
|
|
728
|
+
return { handled: true };
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* 扫码事件
|
|
733
|
+
*/
|
|
734
|
+
async handleScanCodePush(event, record) {
|
|
735
|
+
console.log('[Callback] 扫码:', event);
|
|
736
|
+
return { handled: true };
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* 审批事件(提交)
|
|
741
|
+
*/
|
|
742
|
+
/**
|
|
743
|
+
* 审批提交事件
|
|
744
|
+
* 事件Key: approval_submit
|
|
745
|
+
* 关键字段: ApprovalId, SpStatus, SubmitterUserid, TaskId, UniqueId
|
|
746
|
+
*/
|
|
747
|
+
async handleSubmitApproval(event, record) {
|
|
748
|
+
const approvalId = event.ApprovalId || event.approval_id || '';
|
|
749
|
+
const spStatus = event.SpStatus || event.sp_status || 0;
|
|
750
|
+
const submitter = event.SubmitterUserid || event.submitter_userid || '';
|
|
751
|
+
const taskId = event.TaskId || event.task_id || '';
|
|
752
|
+
const uniqueId = event.UniqueId || event.unique_id || '';
|
|
753
|
+
record.approvalId = approvalId;
|
|
754
|
+
record.spStatus = spStatus;
|
|
755
|
+
record.submitter = submitter;
|
|
756
|
+
record.taskId = taskId;
|
|
757
|
+
record.uniqueId = uniqueId;
|
|
758
|
+
record.statusText = this._getApprovalStatusText(spStatus);
|
|
759
|
+
console.log('[Callback] 审批提交: ApprovalId=' + approvalId + ' Status=' + record.statusText + ' Submitter=' + submitter);
|
|
760
|
+
await this._fetchAndSaveApprovalDetail(event, record, 'submit');
|
|
761
|
+
this.emit('approval:submit', event, record);
|
|
762
|
+
return { handled: true, approvalId, spStatus: record.statusText };
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* 审批变更事件(官方 key: sys_approval_change)
|
|
767
|
+
* 关键字段: ApprovalId, SpStatus, OpenSpid
|
|
768
|
+
*/
|
|
769
|
+
async handleSysApprovalChange(event, record) {
|
|
770
|
+
const approvalId = event.ApprovalId || event.approval_id || '';
|
|
771
|
+
const spStatus = event.SpStatus || event.sp_status || 0;
|
|
772
|
+
const openSpid = event.OpenSpid || event.open_spid || '';
|
|
773
|
+
record.approvalId = approvalId;
|
|
774
|
+
record.spStatus = spStatus;
|
|
775
|
+
record.openSpid = openSpid;
|
|
776
|
+
record.statusText = this._getApprovalStatusText(spStatus);
|
|
777
|
+
console.log('[Callback] 审批变更: ApprovalId=' + approvalId + ' Status=' + record.statusText);
|
|
778
|
+
await this._fetchAndSaveApprovalDetail(event, record, 'change');
|
|
779
|
+
this.emit('approval:change', event, record);
|
|
780
|
+
return { handled: true, approvalId, spStatus: record.statusText };
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* 审批通过/变更事件(兼容旧 key: Approval)
|
|
785
|
+
*/
|
|
786
|
+
async handleApproval(event, record) {
|
|
787
|
+
const approvalId = event.ApprovalId || event.approval_id || '';
|
|
788
|
+
const spStatus = event.SpStatus || event.sp_status || 0;
|
|
789
|
+
record.approvalId = approvalId;
|
|
790
|
+
record.spStatus = spStatus;
|
|
791
|
+
record.statusText = this._getApprovalStatusText(spStatus);
|
|
792
|
+
console.log('[Callback] 审批变更(Approval): ApprovalId=' + approvalId + ' Status=' + record.statusText);
|
|
793
|
+
await this._fetchAndSaveApprovalDetail(event, record, 'change');
|
|
794
|
+
this.emit('approval:pass', event, record);
|
|
795
|
+
return { handled: true, approvalId, spStatus: record.statusText };
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/** 审批状态码转文本 */
|
|
799
|
+
_getApprovalStatusText(spStatus) {
|
|
800
|
+
const map = { 1:'审批中', 2:'已通过', 3:'已驳回', 4:'已撤回', 5:'未提交', 6:'已通过', 7:'已驳回', 8:'已转交', 10:'已完成', 11:'已取消' };
|
|
801
|
+
return map[spStatus] || '状态'+spStatus;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* 审批通过/变更事件
|
|
807
|
+
*/
|
|
808
|
+
async handleApproval(event, record) {
|
|
809
|
+
console.log('[Callback] 审批变更:', event);
|
|
810
|
+
// 获取审批详情并保存
|
|
811
|
+
await this._fetchAndSaveApprovalDetail(event, record, 'change');
|
|
812
|
+
this.emit('approval:pass', event, record);
|
|
813
|
+
return { handled: true };
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* 根据回调时间拉取审批详情并保存
|
|
818
|
+
* @param {object} event 事件对象
|
|
819
|
+
* @param {object} record 记录对象
|
|
820
|
+
* @param {string} trigger 触发类型 submit|change
|
|
821
|
+
*/
|
|
822
|
+
async _fetchAndSaveApprovalDetail(event, record, trigger) {
|
|
823
|
+
try {
|
|
824
|
+
const callbackTime = (event.CreateTime || record.createTime || 0) * 1000;
|
|
825
|
+
const endTime = Math.floor(Date.now() / 1000);
|
|
826
|
+
const startTime = endTime - 600;
|
|
827
|
+
|
|
828
|
+
const idListRes = await this.approval.getApprovalIds(startTime, endTime);
|
|
829
|
+
const spNoList = idListRes?.sp_no_list || [];
|
|
830
|
+
|
|
831
|
+
if (spNoList.length === 0) {
|
|
832
|
+
console.log('[Callback] 未找到审批单(' + trigger + ')');
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const matchedSpNo = spNoList[0];
|
|
837
|
+
const detail = await this.approval.getApprovalDetail(matchedSpNo);
|
|
838
|
+
if (detail?.errcode === 0 || detail?.sp_detail) {
|
|
839
|
+
record.spNo = matchedSpNo;
|
|
840
|
+
record.approvalDetail = detail.sp_detail || detail;
|
|
841
|
+
record.fetchTime = new Date().toISOString();
|
|
842
|
+
this._appendToFile('approval_detail.jsonl', {
|
|
843
|
+
sp_no: matchedSpNo, trigger,
|
|
844
|
+
callback_time: new Date(callbackTime).toISOString(),
|
|
845
|
+
fetch_time: record.fetchTime,
|
|
846
|
+
detail: record.approvalDetail, raw: event
|
|
847
|
+
});
|
|
848
|
+
console.log('[Callback] 审批详情已保存: ' + matchedSpNo);
|
|
849
|
+
} else {
|
|
850
|
+
console.log('[Callback] 获取审批详情失败: ' + (detail?.errmsg || JSON.stringify(detail).slice(0, 100)));
|
|
851
|
+
}
|
|
852
|
+
} catch (e) {
|
|
853
|
+
console.log('[Callback] _fetchAndSaveApprovalDetail 异常: ' + e.message);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* 打卡事件
|
|
860
|
+
*/
|
|
861
|
+
async handleCheckin(event, record) {
|
|
862
|
+
console.log('[Callback] 打卡:', event);
|
|
863
|
+
return { handled: true };
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* 会议开始
|
|
868
|
+
*/
|
|
869
|
+
/**
|
|
870
|
+
* 会议开始
|
|
871
|
+
* 关键字段: MeetingId, RoomId, Topic, StartTime, EndTime, JoinUrl, HostUserId
|
|
872
|
+
*/
|
|
873
|
+
async handleMeetingStart(event, record) {
|
|
874
|
+
record.meetingId = event.MeetingId || event.meeting_id || '';
|
|
875
|
+
record.roomId = event.RoomId || event.room_id || '';
|
|
876
|
+
record.topic = event.Topic || event.topic || '';
|
|
877
|
+
record.startTime = event.StartTime || event.start_time || 0;
|
|
878
|
+
record.endTime = event.EndTime || event.end_time || 0;
|
|
879
|
+
record.joinUrl = event.JoinUrl || event.join_url || '';
|
|
880
|
+
record.hostUserId = event.HostUserId || event.host_user_id || '';
|
|
881
|
+
console.log('[Callback] 会议开始: MeetingId=' + record.meetingId + ' Topic=' + record.topic + ' Host=' + record.hostUserId);
|
|
882
|
+
this.emit('meeting:start', event, record);
|
|
883
|
+
return { handled: true, meetingId: record.meetingId, topic: record.topic };
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* 会议结束
|
|
889
|
+
*/
|
|
890
|
+
/**
|
|
891
|
+
* 会议结束
|
|
892
|
+
* 关键字段: MeetingId, RoomId, Topic, StartTime, EndTime, HostUserId, RemainDuration
|
|
893
|
+
*/
|
|
894
|
+
async handleMeetingEnd(event, record) {
|
|
895
|
+
record.meetingId = event.MeetingId || event.meeting_id || '';
|
|
896
|
+
record.roomId = event.RoomId || event.room_id || '';
|
|
897
|
+
record.topic = event.Topic || event.topic || '';
|
|
898
|
+
record.startTime = event.StartTime || event.start_time || 0;
|
|
899
|
+
record.endTime = event.EndTime || event.end_time || 0;
|
|
900
|
+
record.hostUserId = event.HostUserId || event.host_user_id || '';
|
|
901
|
+
record.remainDuration = event.RemainDuration || event.remain_duration || 0;
|
|
902
|
+
console.log('[Callback] 会议结束: MeetingId=' + record.meetingId + ' Topic=' + record.topic);
|
|
903
|
+
this.emit('meeting:end', event, record);
|
|
904
|
+
return { handled: true, meetingId: record.meetingId, topic: record.topic };
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
// ========== 新增的回调处理器实现 ==========
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* 添加联系我
|
|
912
|
+
*/
|
|
913
|
+
async handleAddContactWay(event, record) {
|
|
914
|
+
console.log('[Callback] 添加联系我:', event);
|
|
915
|
+
this.emit('contact_way:add', event, record);
|
|
916
|
+
return { handled: true };
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* 删除联系我
|
|
921
|
+
*/
|
|
922
|
+
async handleDelContactWay(event, record) {
|
|
923
|
+
console.log('[Callback] 删除联系我:', event);
|
|
924
|
+
this.emit('contact_way:del', event, record);
|
|
925
|
+
return { handled: true };
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* 添加加入群聊方式
|
|
930
|
+
*/
|
|
931
|
+
async handleAddJoinWay(event, record) {
|
|
932
|
+
console.log('[Callback] 添加加入群聊方式:', event);
|
|
933
|
+
this.emit('join_way:add', event, record);
|
|
934
|
+
return { handled: true };
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* 删除加入群聊方式
|
|
939
|
+
*/
|
|
940
|
+
async handleDelJoinWay(event, record) {
|
|
941
|
+
console.log('[Callback] 删除加入群聊方式:', event);
|
|
942
|
+
this.emit('join_way:del', event, record);
|
|
943
|
+
return { handled: true };
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* 客服消息推送
|
|
948
|
+
*/
|
|
949
|
+
async handleKfMsgPush(event, record) {
|
|
950
|
+
console.log('[Callback] 客服消息推送:', event);
|
|
951
|
+
this.emit('kf_msg:push', event, record);
|
|
952
|
+
return { handled: true };
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* 客服消息发送
|
|
957
|
+
*/
|
|
958
|
+
async handleKfMsgSend(event, record) {
|
|
959
|
+
console.log('[Callback] 客服消息发送:', event);
|
|
960
|
+
this.emit('kf_msg:send', event, record);
|
|
961
|
+
return { handled: true };
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* 消息确认
|
|
966
|
+
*/
|
|
967
|
+
async handleMsgDialogiceSend(event, record) {
|
|
968
|
+
console.log('[Callback] 消息确认:', event);
|
|
969
|
+
this.emit('msg:confirm', event, record);
|
|
970
|
+
return { handled: true };
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* 成员变更通知
|
|
975
|
+
*/
|
|
976
|
+
/**
|
|
977
|
+
* 成员变更事件
|
|
978
|
+
* 关键字段: ChangeType, UserID, NewUserID, Name, Department, Position
|
|
979
|
+
*/
|
|
980
|
+
async handleChangeMember(event, record) {
|
|
981
|
+
const ct = event.ChangeType || event.change_type || 0;
|
|
982
|
+
record.changeType = ct;
|
|
983
|
+
record.userId = event.UserID || event.userid || '';
|
|
984
|
+
record.newUserId = event.NewUserID || event.new_userid || '';
|
|
985
|
+
record.name = event.Name || event.name || '';
|
|
986
|
+
record.department = event.Department || event.department || [];
|
|
987
|
+
record.position = event.Position || event.position || '';
|
|
988
|
+
const ctMap = {1:'新增', 2:'更新', 3:'删除'};
|
|
989
|
+
record.changeTypeText = ctMap[ct] || '变更('+ct+')';
|
|
990
|
+
console.log('[Callback] 成员变更: ' + record.changeTypeText + ' UserId=' + record.userId + ' Name=' + record.name);
|
|
991
|
+
this.emit('contact:member_change', event, record);
|
|
992
|
+
return { handled: true, changeType: record.changeTypeText, userId: record.userId };
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* 部门变更通知
|
|
998
|
+
*/
|
|
999
|
+
/**
|
|
1000
|
+
* 部门变更事件
|
|
1001
|
+
* 关键字段: ChangeType, Id, Name, ParentId, Order
|
|
1002
|
+
*/
|
|
1003
|
+
async handleChangeDepartment(event, record) {
|
|
1004
|
+
const ct = event.ChangeType || event.change_type || 0;
|
|
1005
|
+
record.changeType = ct;
|
|
1006
|
+
record.deptId = event.Id || event.id || '';
|
|
1007
|
+
record.name = event.Name || event.name || '';
|
|
1008
|
+
record.parentId = event.ParentId || event.parentid || '';
|
|
1009
|
+
record.order = event.Order || event.order || 0;
|
|
1010
|
+
const ctMap = {1:'新增', 2:'更新', 3:'删除'};
|
|
1011
|
+
record.changeTypeText = ctMap[ct] || '变更('+ct+')';
|
|
1012
|
+
console.log('[Callback] 部门变更: ' + record.changeTypeText + ' DeptId=' + record.deptId + ' Name=' + record.name);
|
|
1013
|
+
this.emit('contact:department_change', event, record);
|
|
1014
|
+
return { handled: true, changeType: record.changeTypeText, deptId: record.deptId };
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* 标签变更通知
|
|
1020
|
+
*/
|
|
1021
|
+
/**
|
|
1022
|
+
* 标签变更事件
|
|
1023
|
+
* 关键字段: ChangeType, TagId, TagName, AddUserIds, DelUserIds
|
|
1024
|
+
*/
|
|
1025
|
+
async handleChangeTag(event, record) {
|
|
1026
|
+
const ct = event.ChangeType || event.change_type || 0;
|
|
1027
|
+
record.changeType = ct;
|
|
1028
|
+
record.tagId = event.TagId || event.tagid || '';
|
|
1029
|
+
record.tagName = event.TagName || event.tagname || '';
|
|
1030
|
+
record.addUserIds = event.AddUserIds || event.add_userids || [];
|
|
1031
|
+
record.delUserIds = event.DelUserIds || event.del_userids || [];
|
|
1032
|
+
const ctMap = {1:'新增', 2:'更新', 3:'删除'};
|
|
1033
|
+
record.changeTypeText = ctMap[ct] || '变更('+ct+')';
|
|
1034
|
+
console.log('[Callback] 标签变更: ' + record.changeTypeText + ' TagId=' + record.tagId + ' TagName=' + record.tagName);
|
|
1035
|
+
this.emit('contact:tag_change', event, record);
|
|
1036
|
+
return { handled: true, changeType: record.changeTypeText, tagId: record.tagId };
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* 会议真正结束(历史记录生成后)
|
|
1042
|
+
*/
|
|
1043
|
+
async handleMeetingEnded(event, record) {
|
|
1044
|
+
console.log('[Callback] 会议结束(历史记录):', event);
|
|
1045
|
+
this.emit('meeting:ended', event, record);
|
|
1046
|
+
return { handled: true };
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* 参会成员加入
|
|
1051
|
+
*/
|
|
1052
|
+
/**
|
|
1053
|
+
* 参会成员加入
|
|
1054
|
+
* 关键字段: MeetingId, RoomId, ParticipantUserId, JoinTime
|
|
1055
|
+
*/
|
|
1056
|
+
async handleMeetingParticipantJoin(event, record) {
|
|
1057
|
+
record.meetingId = event.MeetingId || event.meeting_id || '';
|
|
1058
|
+
record.roomId = event.RoomId || event.room_id || '';
|
|
1059
|
+
record.participantUserId = event.ParticipantUserId || event.participant_userid || event.UserId || '';
|
|
1060
|
+
record.joinTime = event.JoinTime || event.join_time || 0;
|
|
1061
|
+
console.log('[Callback] 参会成员加入: MeetingId=' + record.meetingId + ' User=' + record.participantUserId);
|
|
1062
|
+
this.emit('meeting:participant_join', event, record);
|
|
1063
|
+
return { handled: true, meetingId: record.meetingId, participantUserId: record.participantUserId };
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* 参会成员离开
|
|
1069
|
+
*/
|
|
1070
|
+
/**
|
|
1071
|
+
* 参会成员离开
|
|
1072
|
+
* 关键字段: MeetingId, RoomId, ParticipantUserId, LeaveTime
|
|
1073
|
+
*/
|
|
1074
|
+
async handleMeetingParticipantLeave(event, record) {
|
|
1075
|
+
record.meetingId = event.MeetingId || event.meeting_id || '';
|
|
1076
|
+
record.roomId = event.RoomId || event.room_id || '';
|
|
1077
|
+
record.participantUserId = event.ParticipantUserId || event.participant_userid || event.UserId || '';
|
|
1078
|
+
record.leaveTime = event.LeaveTime || event.leave_time || 0;
|
|
1079
|
+
console.log('[Callback] 参会成员离开: MeetingId=' + record.meetingId + ' User=' + record.participantUserId);
|
|
1080
|
+
this.emit('meeting:participant_leave', event, record);
|
|
1081
|
+
return { handled: true, meetingId: record.meetingId, participantUserId: record.participantUserId };
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* 直播状态变更
|
|
1087
|
+
*/
|
|
1088
|
+
async handleLivingStatus(event, record) {
|
|
1089
|
+
console.log('[Callback] 直播状态变更:', event);
|
|
1090
|
+
this.emit('living:status_change', event, record);
|
|
1091
|
+
return { handled: true };
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* 微盘容量变更
|
|
1096
|
+
*/
|
|
1097
|
+
async handleChangePsm(event, record) {
|
|
1098
|
+
console.log('[Callback] 微盘容量变更:', event);
|
|
1099
|
+
this.emit('disk:psm_change', event, record);
|
|
1100
|
+
return { handled: true };
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
/**
|
|
1104
|
+
* 微盘文件变更
|
|
1105
|
+
*/
|
|
1106
|
+
async handleChangeDisk(event, record) {
|
|
1107
|
+
console.log('[Callback] 微盘文件变更:', event);
|
|
1108
|
+
this.emit('disk:file_change', event, record);
|
|
1109
|
+
return { handled: true };
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// ========== 消息构建 ==========
|
|
1113
|
+
|
|
1114
|
+
/**
|
|
1115
|
+
* 构建文本回复
|
|
1116
|
+
*/
|
|
1117
|
+
buildTextReply(toUser, fromUser, content) {
|
|
1118
|
+
return this._buildReply(toUser, fromUser, 'text', { content });
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* 构建图片回复
|
|
1123
|
+
*/
|
|
1124
|
+
buildImageReply(toUser, fromUser, mediaId) {
|
|
1125
|
+
return this._buildReply(toUser, fromUser, 'image', { mediaId });
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* 构建通用回复
|
|
1130
|
+
*/
|
|
1131
|
+
_buildReply(toUser, fromUser, msgType, content) {
|
|
1132
|
+
let contentXml = '';
|
|
1133
|
+
|
|
1134
|
+
switch (msgType) {
|
|
1135
|
+
case 'text':
|
|
1136
|
+
contentXml = `<Content><![CDATA[${content.content}]]></Content>`;
|
|
1137
|
+
break;
|
|
1138
|
+
case 'image':
|
|
1139
|
+
contentXml = `<Image><MediaId><![CDATA[${content.mediaId}]]></MediaId></Image>`;
|
|
1140
|
+
break;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
return `<xml>
|
|
1144
|
+
<ToUserName><![CDATA[${toUser}]]></ToUserName>
|
|
1145
|
+
<FromUserName><![CDATA[${fromUser}]]></FromUserName>
|
|
1146
|
+
<CreateTime>${Date.now()}</CreateTime>
|
|
1147
|
+
<MsgType><![CDATA[${msgType}]]></MsgType>
|
|
1148
|
+
${contentXml}
|
|
1149
|
+
</xml>`;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// ========== 工具方法 ==========
|
|
1153
|
+
|
|
1154
|
+
getSignature(timestamp, nonce, encrypt) {
|
|
1155
|
+
const arr = [this.token, timestamp, nonce, encrypt].sort();
|
|
1156
|
+
const str = arr.join('');
|
|
1157
|
+
return crypto.createHash('sha1').update(str).digest('hex');
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
generateRandomStr(length = 16) {
|
|
1161
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
1162
|
+
let result = '';
|
|
1163
|
+
for (let i = 0; i < length; i++) {
|
|
1164
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
1165
|
+
}
|
|
1166
|
+
return result;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
intToBuffer(num) {
|
|
1170
|
+
const buffer = Buffer.alloc(4);
|
|
1171
|
+
buffer.writeUInt32BE(num, 0);
|
|
1172
|
+
return buffer;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
_generateId() {
|
|
1176
|
+
return `evt_${Date.now()}_${this.generateRandomStr(8)}`;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
module.exports = Callback;
|