@wwlocal/aibot-plugin-node 20260409.20.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 +489 -0
- package/config.example.json +169 -0
- package/dist/cjs/index.js +76 -0
- package/dist/cjs/src/adapters/anthropic-adapter.js +534 -0
- package/dist/cjs/src/adapters/base-adapter.js +176 -0
- package/dist/cjs/src/adapters/deepseek-adapter.js +328 -0
- package/dist/cjs/src/adapters/dify-adapter.js +636 -0
- package/dist/cjs/src/adapters/index.js +131 -0
- package/dist/cjs/src/adapters/openai-adapter.js +361 -0
- package/dist/cjs/src/adapters/webhook-adapter.js +260 -0
- package/dist/cjs/src/agent-forwarder.js +87 -0
- package/dist/cjs/src/ca-cert.js +162 -0
- package/dist/cjs/src/config.js +169 -0
- package/dist/cjs/src/const.js +124 -0
- package/dist/cjs/src/conversation-manager.js +147 -0
- package/dist/cjs/src/dm-policy.js +46 -0
- package/dist/cjs/src/group-policy.js +95 -0
- package/dist/cjs/src/media-handler.js +136 -0
- package/dist/cjs/src/media-loader.js +271 -0
- package/dist/cjs/src/media-storage.js +165 -0
- package/dist/cjs/src/media-uploader.js +203 -0
- package/dist/cjs/src/message-parser.js +133 -0
- package/dist/cjs/src/message-sender.js +87 -0
- package/dist/cjs/src/monitor.js +849 -0
- package/dist/cjs/src/reqid-store.js +87 -0
- package/dist/cjs/src/server.js +72 -0
- package/dist/cjs/src/service-manager.js +135 -0
- package/dist/cjs/src/state-manager.js +143 -0
- package/dist/cjs/src/template-card-parser.js +498 -0
- package/dist/cjs/src/timeout.js +41 -0
- package/dist/cjs/src/version.js +25 -0
- package/dist/esm/index.js +74 -0
- package/dist/esm/src/adapters/anthropic-adapter.js +512 -0
- package/dist/esm/src/adapters/base-adapter.js +174 -0
- package/dist/esm/src/adapters/deepseek-adapter.js +326 -0
- package/dist/esm/src/adapters/dify-adapter.js +634 -0
- package/dist/esm/src/adapters/index.js +123 -0
- package/dist/esm/src/adapters/openai-adapter.js +339 -0
- package/dist/esm/src/adapters/webhook-adapter.js +258 -0
- package/dist/esm/src/agent-forwarder.js +84 -0
- package/dist/esm/src/ca-cert.js +136 -0
- package/dist/esm/src/config.js +145 -0
- package/dist/esm/src/const.js +100 -0
- package/dist/esm/src/conversation-manager.js +144 -0
- package/dist/esm/src/dm-policy.js +44 -0
- package/dist/esm/src/group-policy.js +92 -0
- package/dist/esm/src/media-handler.js +133 -0
- package/dist/esm/src/media-loader.js +246 -0
- package/dist/esm/src/media-storage.js +143 -0
- package/dist/esm/src/media-uploader.js +198 -0
- package/dist/esm/src/message-parser.js +131 -0
- package/dist/esm/src/message-sender.js +83 -0
- package/dist/esm/src/monitor.js +841 -0
- package/dist/esm/src/reqid-store.js +85 -0
- package/dist/esm/src/server.js +69 -0
- package/dist/esm/src/service-manager.js +133 -0
- package/dist/esm/src/state-manager.js +134 -0
- package/dist/esm/src/template-card-parser.js +495 -0
- package/dist/esm/src/timeout.js +38 -0
- package/dist/esm/src/version.js +22 -0
- package/dist/esm/types/index.d.ts +14 -0
- package/dist/esm/types/src/adapters/anthropic-adapter.d.ts +93 -0
- package/dist/esm/types/src/adapters/base-adapter.d.ts +76 -0
- package/dist/esm/types/src/adapters/deepseek-adapter.d.ts +87 -0
- package/dist/esm/types/src/adapters/dify-adapter.d.ts +100 -0
- package/dist/esm/types/src/adapters/index.d.ts +60 -0
- package/dist/esm/types/src/adapters/openai-adapter.d.ts +82 -0
- package/dist/esm/types/src/adapters/types.d.ts +373 -0
- package/dist/esm/types/src/adapters/webhook-adapter.d.ts +54 -0
- package/dist/esm/types/src/agent-forwarder.d.ts +32 -0
- package/dist/esm/types/src/ca-cert.d.ts +53 -0
- package/dist/esm/types/src/config.d.ts +29 -0
- package/dist/esm/types/src/const.d.ts +74 -0
- package/dist/esm/types/src/conversation-manager.d.ts +81 -0
- package/dist/esm/types/src/dm-policy.d.ts +27 -0
- package/dist/esm/types/src/group-policy.d.ts +28 -0
- package/dist/esm/types/src/interface.d.ts +332 -0
- package/dist/esm/types/src/media-handler.d.ts +36 -0
- package/dist/esm/types/src/media-loader.d.ts +47 -0
- package/dist/esm/types/src/media-storage.d.ts +35 -0
- package/dist/esm/types/src/media-uploader.d.ts +65 -0
- package/dist/esm/types/src/message-parser.d.ts +89 -0
- package/dist/esm/types/src/message-sender.d.ts +34 -0
- package/dist/esm/types/src/monitor.d.ts +30 -0
- package/dist/esm/types/src/reqid-store.d.ts +23 -0
- package/dist/esm/types/src/server.d.ts +23 -0
- package/dist/esm/types/src/service-manager.d.ts +52 -0
- package/dist/esm/types/src/state-manager.d.ts +76 -0
- package/dist/esm/types/src/template-card-parser.d.ts +18 -0
- package/dist/esm/types/src/timeout.d.ts +20 -0
- package/dist/esm/types/src/version.d.ts +2 -0
- package/dist/index.d.ts +2 -0
- package/package.json +51 -0
|
@@ -0,0 +1,849 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var aibotNodeSdk = require('@wecom/aibot-node-sdk');
|
|
4
|
+
var _const = require('./const.js');
|
|
5
|
+
var messageParser = require('./message-parser.js');
|
|
6
|
+
var messageSender = require('./message-sender.js');
|
|
7
|
+
var mediaHandler = require('./media-handler.js');
|
|
8
|
+
var mediaUploader = require('./media-uploader.js');
|
|
9
|
+
var templateCardParser = require('./template-card-parser.js');
|
|
10
|
+
var groupPolicy = require('./group-policy.js');
|
|
11
|
+
var dmPolicy = require('./dm-policy.js');
|
|
12
|
+
var stateManager = require('./state-manager.js');
|
|
13
|
+
var caCert = require('./ca-cert.js');
|
|
14
|
+
var version = require('./version.js');
|
|
15
|
+
var agentForwarder = require('./agent-forwarder.js');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 企业微信私有部署 WebSocket 监控器主模块
|
|
19
|
+
*
|
|
20
|
+
* 负责:
|
|
21
|
+
* - 建立和管理 WebSocket 连接
|
|
22
|
+
* - 协调消息处理流程(解析→策略检查→下载图片→智能体转发→流式回复)
|
|
23
|
+
* - 资源生命周期管理
|
|
24
|
+
*
|
|
25
|
+
* 子模块:
|
|
26
|
+
* - message-parser.ts : 消息内容解析
|
|
27
|
+
* - message-sender.ts : 消息发送(带超时保护)
|
|
28
|
+
* - media-handler.ts : 图片/文件下载和保存(带超时保护)
|
|
29
|
+
* - media-uploader.ts : 出站媒体上传+发送
|
|
30
|
+
* - group-policy.ts : 群组访问控制
|
|
31
|
+
* - dm-policy.ts : 私聊访问控制
|
|
32
|
+
* - state-manager.ts : 全局状态管理(带 TTL 清理)
|
|
33
|
+
* - agent-forwarder.ts : OpenAI API 兼容智能体转发
|
|
34
|
+
* - template-card-parser.ts : 模板卡片提取/遮罩
|
|
35
|
+
* - timeout.ts : 超时工具
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* 去除文本中的 `<think>...</think>` 标签(支持跨行),返回剩余可见文本。
|
|
39
|
+
* 用于判断大模型回复中是否包含实际用户可见内容(而非仅有 thinking 推理过程)。
|
|
40
|
+
*/
|
|
41
|
+
function stripThinkTags(text) {
|
|
42
|
+
return text.replace(/<think>[\s\S]*?<\/think>/g, "").trim();
|
|
43
|
+
}
|
|
44
|
+
const sentTemplateCardByTaskId = new Map();
|
|
45
|
+
const TEMPLATE_CARD_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
46
|
+
const TEMPLATE_CARD_CACHE_MAX_SIZE = 300;
|
|
47
|
+
function getTemplateCardCacheKey(accountId, taskId) {
|
|
48
|
+
return `${accountId}:${taskId}`;
|
|
49
|
+
}
|
|
50
|
+
function pruneTemplateCardCache() {
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
for (const [key, entry] of sentTemplateCardByTaskId) {
|
|
53
|
+
if (now - entry.createdAt >= TEMPLATE_CARD_CACHE_TTL_MS) {
|
|
54
|
+
sentTemplateCardByTaskId.delete(key);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (sentTemplateCardByTaskId.size <= TEMPLATE_CARD_CACHE_MAX_SIZE) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const sortedEntries = [...sentTemplateCardByTaskId.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);
|
|
61
|
+
const removeCount = sentTemplateCardByTaskId.size - TEMPLATE_CARD_CACHE_MAX_SIZE;
|
|
62
|
+
for (const [key] of sortedEntries.slice(0, removeCount)) {
|
|
63
|
+
sentTemplateCardByTaskId.delete(key);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function cloneTemplateCard(card) {
|
|
67
|
+
return JSON.parse(JSON.stringify(card));
|
|
68
|
+
}
|
|
69
|
+
function saveTemplateCardToCache(params) {
|
|
70
|
+
const { accountId, templateCard, runtime } = params;
|
|
71
|
+
const taskId = templateCard.task_id;
|
|
72
|
+
if (!taskId) {
|
|
73
|
+
runtime.log?.("[wecom][template-card] Skip cache: template card has no task_id");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
sentTemplateCardByTaskId.set(getTemplateCardCacheKey(accountId, taskId), {
|
|
77
|
+
templateCard: cloneTemplateCard(templateCard),
|
|
78
|
+
createdAt: Date.now(),
|
|
79
|
+
});
|
|
80
|
+
pruneTemplateCardCache();
|
|
81
|
+
}
|
|
82
|
+
function getTemplateCardFromCache(accountId, taskId) {
|
|
83
|
+
pruneTemplateCardCache();
|
|
84
|
+
const cached = sentTemplateCardByTaskId.get(getTemplateCardCacheKey(accountId, taskId));
|
|
85
|
+
if (!cached) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
return cloneTemplateCard(cached.templateCard);
|
|
89
|
+
}
|
|
90
|
+
function buildSelectedOptionMap(templateCardEvent) {
|
|
91
|
+
const selectedMap = new Map();
|
|
92
|
+
const selectedItems = templateCardEvent?.selected_items?.selected_item ?? [];
|
|
93
|
+
for (const item of selectedItems) {
|
|
94
|
+
const questionKey = item.question_key?.trim();
|
|
95
|
+
if (!questionKey) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const optionIds = item.option_ids?.option_id?.filter(Boolean) ?? [];
|
|
99
|
+
selectedMap.set(questionKey, optionIds);
|
|
100
|
+
}
|
|
101
|
+
return selectedMap;
|
|
102
|
+
}
|
|
103
|
+
function applySelectedStateToTemplateCard(params) {
|
|
104
|
+
const { templateCard, selectedMap, templateCardEvent } = params;
|
|
105
|
+
const nextCard = cloneTemplateCard(templateCard);
|
|
106
|
+
if (templateCardEvent?.task_id) {
|
|
107
|
+
nextCard.task_id = templateCardEvent.task_id;
|
|
108
|
+
}
|
|
109
|
+
if (templateCardEvent?.card_type) {
|
|
110
|
+
nextCard.card_type = templateCardEvent.card_type;
|
|
111
|
+
}
|
|
112
|
+
// 交互完成后将提交按钮文案更新为已提交,提升用户感知
|
|
113
|
+
if (nextCard.submit_button?.text) {
|
|
114
|
+
nextCard.submit_button.text = "已提交";
|
|
115
|
+
}
|
|
116
|
+
if (nextCard.checkbox?.question_key) {
|
|
117
|
+
const selectedIds = selectedMap.get(nextCard.checkbox.question_key) ?? [];
|
|
118
|
+
nextCard.checkbox.disable = true;
|
|
119
|
+
if (Array.isArray(nextCard.checkbox.option_list)) {
|
|
120
|
+
nextCard.checkbox.option_list = nextCard.checkbox.option_list.map((option) => ({
|
|
121
|
+
...option,
|
|
122
|
+
is_checked: selectedIds.includes(option.id),
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (Array.isArray(nextCard.select_list)) {
|
|
127
|
+
nextCard.select_list = nextCard.select_list.map((selection) => {
|
|
128
|
+
const selectedIds = selectedMap.get(selection.question_key) ?? [];
|
|
129
|
+
return {
|
|
130
|
+
...selection,
|
|
131
|
+
disable: true,
|
|
132
|
+
selected_id: selectedIds[0] ?? selection.selected_id,
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (nextCard.button_selection?.question_key) {
|
|
137
|
+
const selectedIds = selectedMap.get(nextCard.button_selection.question_key) ?? [];
|
|
138
|
+
nextCard.button_selection.disable = true;
|
|
139
|
+
if (selectedIds[0]) {
|
|
140
|
+
nextCard.button_selection.selected_id = selectedIds[0];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return nextCard;
|
|
144
|
+
}
|
|
145
|
+
async function updateTemplateCardOnEvent(params) {
|
|
146
|
+
const { frame, accountId, runtime, wsClient } = params;
|
|
147
|
+
const body = frame.body;
|
|
148
|
+
const templateCardEvent = body.event?.template_card_event;
|
|
149
|
+
const taskId = templateCardEvent?.task_id;
|
|
150
|
+
if (!taskId) {
|
|
151
|
+
runtime.log?.(`[${accountId}] [template-card-update] Skip update: missing task_id in callback`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const cachedCard = getTemplateCardFromCache(accountId, taskId);
|
|
155
|
+
if (!cachedCard) {
|
|
156
|
+
runtime.log?.(`[${accountId}] [template-card-update] Skip update: task_id=${taskId} not found in cache`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const selectedMap = buildSelectedOptionMap(templateCardEvent);
|
|
160
|
+
const updatedCard = applySelectedStateToTemplateCard({
|
|
161
|
+
templateCard: cachedCard,
|
|
162
|
+
selectedMap,
|
|
163
|
+
templateCardEvent,
|
|
164
|
+
});
|
|
165
|
+
await wsClient.updateTemplateCard(frame, updatedCard, [body.from.userid]);
|
|
166
|
+
runtime.log?.(`[${accountId}] [template-card-update] Updated card by task_id=${taskId}`);
|
|
167
|
+
// 将更新后的卡片写回缓存,后续多次点击时状态保持一致
|
|
168
|
+
saveTemplateCardToCache({
|
|
169
|
+
accountId,
|
|
170
|
+
templateCard: updatedCard,
|
|
171
|
+
runtime,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
// ============================================================================
|
|
175
|
+
// 媒体发送错误提示
|
|
176
|
+
// ============================================================================
|
|
177
|
+
/**
|
|
178
|
+
* 根据媒体发送结果生成纯文本错误摘要(用于替换 thinking 流式消息展示给用户)。
|
|
179
|
+
*
|
|
180
|
+
* 使用纯文本而非 markdown 格式,因为 replyStream 只支持纯文本。
|
|
181
|
+
*/
|
|
182
|
+
function buildMediaErrorSummary(mediaUrl, result) {
|
|
183
|
+
if (result.error?.includes("LocalMediaAccessError")) {
|
|
184
|
+
return `⚠️ 文件发送失败:没有权限访问路径 ${mediaUrl}\n请在配置文件的 mediaLocalRoots 中添加该路径的父目录后重启生效。`;
|
|
185
|
+
}
|
|
186
|
+
if (result.rejectReason) {
|
|
187
|
+
return `⚠️ 文件发送失败:${result.rejectReason}`;
|
|
188
|
+
}
|
|
189
|
+
return `⚠️ 文件发送失败:无法处理文件 ${mediaUrl},请稍后再试。`;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* 发送"思考中"消息
|
|
193
|
+
*/
|
|
194
|
+
async function sendThinkingReply(params) {
|
|
195
|
+
const { wsClient, frame, streamId, runtime, state } = params;
|
|
196
|
+
try {
|
|
197
|
+
await messageSender.sendWeComReply({
|
|
198
|
+
wsClient,
|
|
199
|
+
frame,
|
|
200
|
+
text: _const.THINKING_MESSAGE,
|
|
201
|
+
runtime,
|
|
202
|
+
finish: false,
|
|
203
|
+
streamId,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
if (err instanceof messageSender.StreamExpiredError && state) {
|
|
208
|
+
state.streamExpired = true;
|
|
209
|
+
runtime.log?.(`[wecom] Stream expired during thinking reply, will fallback to proactive send`);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
runtime.error?.(`[wecom] Failed to send thinking message: ${String(err)}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* 上传并发送一批媒体文件(统一走主动发送通道)
|
|
218
|
+
*
|
|
219
|
+
* replyMedia(被动回复)无法覆盖 replyStream 发出的 thinking 流式消息,
|
|
220
|
+
* 因此所有媒体统一走 aibot_send_msg 主动发送。
|
|
221
|
+
*/
|
|
222
|
+
async function sendMediaBatch(ctx, mediaUrls) {
|
|
223
|
+
const { wsClient, frame, state, account, runtime } = ctx;
|
|
224
|
+
const body = frame.body;
|
|
225
|
+
const chatId = body.chatid || body.from.userid;
|
|
226
|
+
const mediaLocalRoots = account.mediaLocalRoots;
|
|
227
|
+
runtime.log?.(`[wecom][debug] mediaLocalRoots=${JSON.stringify(mediaLocalRoots)}, mediaUrls=${JSON.stringify(mediaUrls)}`);
|
|
228
|
+
for (const mediaUrl of mediaUrls) {
|
|
229
|
+
const result = await mediaUploader.uploadAndSendMedia({
|
|
230
|
+
wsClient,
|
|
231
|
+
mediaUrl,
|
|
232
|
+
chatId,
|
|
233
|
+
mediaLocalRoots,
|
|
234
|
+
log: (...args) => runtime.log?.(...args),
|
|
235
|
+
errorLog: (...args) => runtime.error?.(...args),
|
|
236
|
+
});
|
|
237
|
+
if (result.ok) {
|
|
238
|
+
state.hasMedia = true;
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
state.hasMediaFailed = true;
|
|
242
|
+
runtime.error?.(`[wecom] Media send failed: url=${mediaUrl}, reason=${result.rejectReason || result.error}`);
|
|
243
|
+
// 收集错误摘要,后续在 finishThinkingStream 中直接替换 thinking 流展示给用户
|
|
244
|
+
const summary = buildMediaErrorSummary(mediaUrl, result);
|
|
245
|
+
state.mediaErrorSummary = state.mediaErrorSummary
|
|
246
|
+
? `${state.mediaErrorSummary}\n\n${summary}`
|
|
247
|
+
: summary;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* 关闭 thinking 流(发送 finish=true 的流式消息)
|
|
253
|
+
*
|
|
254
|
+
* thinking 是通过 replyStream 用 streamId 发的流式消息,
|
|
255
|
+
* 只有同一 streamId 的 replyStream(finish=true) 才能关闭它。
|
|
256
|
+
*
|
|
257
|
+
* ⚠️ 注意:企微会忽略空格等不可见内容,必须用有可见字符的文案才能真正
|
|
258
|
+
* 替换掉 thinking 动画,否则 thinking 会一直残留。
|
|
259
|
+
*
|
|
260
|
+
* 关闭策略(按优先级):
|
|
261
|
+
* 0. 有模板卡片代码块 → 提取卡片并主动发送,用剩余文本关闭流
|
|
262
|
+
* 1. 有可见文本 → 用完整文本关闭
|
|
263
|
+
* 2. 有媒体成功发送(通过 deliver 回调) → 用友好提示"文件已发送"
|
|
264
|
+
* 3. 媒体发送失败 → 直接用错误摘要替换 thinking
|
|
265
|
+
* 4. 其他 → 用通用"处理完成"提示
|
|
266
|
+
*
|
|
267
|
+
* 降级策略:
|
|
268
|
+
* - 当 streamExpired=true(errcode 846608)时,流式通道已不可用(>6分钟),
|
|
269
|
+
* 改用 wsClient.sendMessage 主动发送完整文本。
|
|
270
|
+
*/
|
|
271
|
+
async function finishThinkingStream(ctx) {
|
|
272
|
+
const { wsClient, frame, state, runtime } = ctx;
|
|
273
|
+
const body = frame.body;
|
|
274
|
+
const chatId = body.chatid || body.from.userid;
|
|
275
|
+
const visibleText = stripThinkTags(state.accumulatedText);
|
|
276
|
+
// ── 模板卡片检测与发送 ──────────────────────────────────────────────
|
|
277
|
+
if (visibleText) {
|
|
278
|
+
runtime.log?.(`[wecom][template-card] finishThinkingStream: visibleText exists, length=${visibleText.length}, running extractTemplateCards...`);
|
|
279
|
+
const logFn = (...args) => {
|
|
280
|
+
runtime.log?.(...args);
|
|
281
|
+
};
|
|
282
|
+
const { cards, remainingText } = templateCardParser.extractTemplateCards(state.accumulatedText, logFn);
|
|
283
|
+
runtime.log?.(`[wecom][template-card] finishThinkingStream: extractTemplateCards result — cards=${cards.length}, remainingTextLength=${remainingText.length}`);
|
|
284
|
+
if (cards.length > 0) {
|
|
285
|
+
runtime.log?.(`[wecom][template-card] finishThinkingStream: ${cards.length} card(s) detected, card_types=[${cards.map((c) => c.cardType).join(", ")}]`);
|
|
286
|
+
// 方案C:先关闭 thinking 流,再发模板卡片
|
|
287
|
+
//
|
|
288
|
+
// 原因:sendMessage(template_card) 和 replyStream(finish=true) 都会写会话的
|
|
289
|
+
// 消息版本号。若先发卡片再关流,两个操作几乎同时触达服务端,导致 errcode=6000
|
|
290
|
+
// "data version conflict"。调换顺序后,流关闭完成(版本号落定)再追加发卡片,
|
|
291
|
+
// 两个写操作严格串行,不存在竞争。
|
|
292
|
+
const trimmedRemaining = stripThinkTags(remainingText);
|
|
293
|
+
const streamFinishText = trimmedRemaining ? remainingText : "📋 正在发送卡片消息…";
|
|
294
|
+
runtime.log?.(`[wecom][template-card] finishThinkingStream: closing stream first, finishText="${streamFinishText.slice(0, 100)}"`);
|
|
295
|
+
await messageSender.sendWeComReply({ wsClient, frame, text: streamFinishText, runtime, finish: true, streamId: state.streamId });
|
|
296
|
+
// 流已关闭,再发卡片,无版本竞争
|
|
297
|
+
await sendTemplateCards(ctx, cards);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
runtime.log?.(`[wecom][template-card] finishThinkingStream: no visibleText, skipping template card extraction`);
|
|
303
|
+
}
|
|
304
|
+
// ── 模板卡片检测结束 ────────────────────────────────────────────────
|
|
305
|
+
let finishText = state.accumulatedText;
|
|
306
|
+
if (visibleText) {
|
|
307
|
+
finishText = state.accumulatedText;
|
|
308
|
+
}
|
|
309
|
+
else if (state.hasMedia) {
|
|
310
|
+
if (state.hasMediaFailed && state.mediaErrorSummary) {
|
|
311
|
+
finishText = finishText ? `${finishText}\n\n${state.mediaErrorSummary}` : state.mediaErrorSummary;
|
|
312
|
+
}
|
|
313
|
+
else if (!finishText) {
|
|
314
|
+
finishText = "📎 文件已发送,请查收。";
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (finishText) {
|
|
318
|
+
// 尝试流式发送;若已知过期或发送时发现过期,统一降级为主动发送
|
|
319
|
+
let expired = state.streamExpired;
|
|
320
|
+
if (!expired) {
|
|
321
|
+
try {
|
|
322
|
+
await messageSender.sendWeComReply({ wsClient, frame, text: finishText, runtime, finish: true, streamId: state.streamId });
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
if (err instanceof messageSender.StreamExpiredError) {
|
|
326
|
+
expired = true;
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
throw err;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (expired) {
|
|
334
|
+
// 降级走 sendMessage:企微 markdown 消息不支持 <think> 标签渲染,
|
|
335
|
+
// 使用 stripThinkTags 后的纯可见文本,避免 thinking 内容裸露显示
|
|
336
|
+
const proactiveText = stripThinkTags(finishText) || finishText;
|
|
337
|
+
runtime.log?.(`[wecom] Stream expired, sending final text via sendMessage (proactive), length=${proactiveText.length}`);
|
|
338
|
+
await wsClient.sendMessage(chatId, {
|
|
339
|
+
msgtype: "markdown",
|
|
340
|
+
markdown: { content: proactiveText },
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* 逐个发送已提取的模板卡片(通过 wsClient.sendMessage 主动推送)
|
|
347
|
+
*
|
|
348
|
+
* 发送失败不阻塞流程,仅记录错误日志。
|
|
349
|
+
*/
|
|
350
|
+
async function sendTemplateCards(ctx, cards) {
|
|
351
|
+
const { wsClient, frame, state, runtime, account } = ctx;
|
|
352
|
+
const body = frame.body;
|
|
353
|
+
const chatId = body.chatid || body.from.userid;
|
|
354
|
+
for (const card of cards) {
|
|
355
|
+
try {
|
|
356
|
+
runtime.log?.(`[wecom][template-card] Sending card_type=${card.cardType} to chatId=${chatId}`);
|
|
357
|
+
const rawTemplateCard = card.cardJson;
|
|
358
|
+
if (typeof rawTemplateCard.card_type !== "string") {
|
|
359
|
+
runtime.error?.("[wecom][template-card] Skip sending invalid card: missing card_type");
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
const templateCard = rawTemplateCard;
|
|
363
|
+
await wsClient.sendMessage(chatId, {
|
|
364
|
+
msgtype: "template_card",
|
|
365
|
+
template_card: templateCard,
|
|
366
|
+
});
|
|
367
|
+
state.hasTemplateCard = true;
|
|
368
|
+
saveTemplateCardToCache({
|
|
369
|
+
accountId: account.accountId,
|
|
370
|
+
templateCard,
|
|
371
|
+
runtime,
|
|
372
|
+
});
|
|
373
|
+
runtime.log?.(`[wecom][template-card] Card sent successfully: card_type=${card.cardType}`);
|
|
374
|
+
}
|
|
375
|
+
catch (err) {
|
|
376
|
+
runtime.error?.(`[wecom][template-card] Failed to send card: card_type=${card.cardType}, error=${JSON.stringify(err)}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* 将消息转发到智能体并处理回复
|
|
382
|
+
*
|
|
383
|
+
* 替换原 OpenClaw 的 routeAndDispatchMessage:
|
|
384
|
+
* - resolveAgentRoute → 直接使用配置中的 agent 端点
|
|
385
|
+
* - dispatchReplyWithBufferedBlockDispatcher → forwardToAgent + deliver 回调
|
|
386
|
+
* - finalizeInboundContext → 直接构建转发参数
|
|
387
|
+
*/
|
|
388
|
+
async function routeAndDispatchToAgent(params) {
|
|
389
|
+
const { text, mediaPaths, quoteContent, account, wsClient, frame, state, runtime, onCleanup } = params;
|
|
390
|
+
const ctx = { wsClient, frame, state, account, runtime };
|
|
391
|
+
const body = frame.body;
|
|
392
|
+
// 提取聊天上下文(用于对话历史管理)
|
|
393
|
+
const chatId = body.chatid || body.from.userid;
|
|
394
|
+
const userId = body.from.userid;
|
|
395
|
+
const chatType = body.chattype === "group" ? "group" : "single";
|
|
396
|
+
// 防止 onCleanup 被多次调用
|
|
397
|
+
let cleanedUp = false;
|
|
398
|
+
const safeCleanup = () => {
|
|
399
|
+
if (!cleanedUp) {
|
|
400
|
+
cleanedUp = true;
|
|
401
|
+
onCleanup();
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
let isShowThink = !(account.sendThinkingMessage ?? true);
|
|
405
|
+
// monitor 层统一节流:闭包变量
|
|
406
|
+
let lastDeliverTime = 0;
|
|
407
|
+
let lastDeliverLen = 0;
|
|
408
|
+
try {
|
|
409
|
+
await agentForwarder.forwardToAgent({
|
|
410
|
+
text,
|
|
411
|
+
mediaPaths: mediaPaths.length > 0 ? mediaPaths : undefined,
|
|
412
|
+
quoteContent,
|
|
413
|
+
agent: account.agent,
|
|
414
|
+
runtime,
|
|
415
|
+
abortSignal: undefined,
|
|
416
|
+
context: {
|
|
417
|
+
accountId: account.accountId,
|
|
418
|
+
chatId,
|
|
419
|
+
userId,
|
|
420
|
+
chatType,
|
|
421
|
+
},
|
|
422
|
+
onReplyStart: async () => {
|
|
423
|
+
// 首次收到 AI 回复时发送 thinking 消息
|
|
424
|
+
if (!isShowThink && state.streamId && !state.accumulatedText) {
|
|
425
|
+
try {
|
|
426
|
+
await sendThinkingReply({ wsClient, frame, streamId: state.streamId, runtime, state });
|
|
427
|
+
}
|
|
428
|
+
catch (e) {
|
|
429
|
+
runtime.error?.(`[wecom] sendThinkingReply threw err: ${String(e)}`);
|
|
430
|
+
}
|
|
431
|
+
isShowThink = true;
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
deliver: async (payload, info) => {
|
|
435
|
+
state.deliverCalled = true;
|
|
436
|
+
// 累积文本(适配器传入完整累积文本,直接赋值)
|
|
437
|
+
if (payload.text) {
|
|
438
|
+
state.accumulatedText = payload.text;
|
|
439
|
+
}
|
|
440
|
+
// 仅在 final 帧输出详细日志
|
|
441
|
+
if (info.kind === "final") {
|
|
442
|
+
runtime.log?.(`[wecom][deliver] kind=${info.kind}, textLen=${payload.text?.length ?? 0}, accLen=${state.accumulatedText.length}`);
|
|
443
|
+
}
|
|
444
|
+
// 发送媒体(统一走主动发送)
|
|
445
|
+
const mediaUrls = payload.mediaUrls?.length ? payload.mediaUrls : payload.mediaUrl ? [payload.mediaUrl] : [];
|
|
446
|
+
if (mediaUrls.length > 0) {
|
|
447
|
+
try {
|
|
448
|
+
await sendMediaBatch(ctx, mediaUrls);
|
|
449
|
+
}
|
|
450
|
+
catch (mediaErr) {
|
|
451
|
+
state.hasMediaFailed = true;
|
|
452
|
+
const errMsg = String(mediaErr);
|
|
453
|
+
const summary = `⚠️ 文件发送失败:内部处理异常,请稍后重试。\n错误详情:${errMsg}`;
|
|
454
|
+
state.mediaErrorSummary = state.mediaErrorSummary
|
|
455
|
+
? `${state.mediaErrorSummary}\n\n${summary}`
|
|
456
|
+
: summary;
|
|
457
|
+
runtime.error?.(`[wecom] sendMediaBatch threw: ${errMsg}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
// 中间帧:有可见内容时流式更新(流式过期后跳过)
|
|
461
|
+
// 关键说明:
|
|
462
|
+
// 1. 用 stripThinkTags 判断是否已进入 text 阶段(纯 thinking 阶段跳过,让企微继续显示 thinking 动画)
|
|
463
|
+
// 2. 实际发送的是完整的 accumulatedText(含 <think> 标签),保证企微客户端在整个流式过程中
|
|
464
|
+
// 都能正确识别和渲染 thinking 折叠块。如果中间帧发纯 content,final 帧再发含 <think> 的
|
|
465
|
+
// 完整文本,企微客户端将无法正确渲染 thinking 内容。
|
|
466
|
+
// 3. monitor 层统一节流(时间+增量双重条件),替代各适配器的分散节流
|
|
467
|
+
// 4. 首帧立即发送,不受节流限制
|
|
468
|
+
if (info.kind !== "final" && state.accumulatedText && !state.streamExpired) {
|
|
469
|
+
const visibleText = stripThinkTags(state.accumulatedText);
|
|
470
|
+
// 只有有可见内容才发送(纯 thinking 阶段跳过,企微继续显示 thinking 动画)
|
|
471
|
+
if (visibleText) {
|
|
472
|
+
// monitor 层统一节流
|
|
473
|
+
const now = Date.now();
|
|
474
|
+
const isFirst = lastDeliverTime === 0;
|
|
475
|
+
const elapsed = now - lastDeliverTime;
|
|
476
|
+
const delta = visibleText.length - lastDeliverLen;
|
|
477
|
+
if (isFirst || (elapsed >= _const.MONITOR_DELIVER_THROTTLE_MS && delta >= _const.MONITOR_DELIVER_MIN_DELTA)) {
|
|
478
|
+
try {
|
|
479
|
+
// 中间帧发送完整的 accumulatedText(含 <think> 标签),对其中非 think 部分做模板卡片遮罩
|
|
480
|
+
const displayText = templateCardParser.maskTemplateCardBlocks(state.accumulatedText, (...args) => runtime.log?.(...args));
|
|
481
|
+
if (displayText !== state.accumulatedText) {
|
|
482
|
+
runtime.log?.(`[wecom][template-card] Mid-frame masked: original=${state.accumulatedText.length}chars, masked=${displayText.length}chars`);
|
|
483
|
+
}
|
|
484
|
+
await messageSender.sendWeComReply({ wsClient, frame, text: displayText, runtime, finish: false, streamId: state.streamId });
|
|
485
|
+
lastDeliverTime = Date.now();
|
|
486
|
+
lastDeliverLen = visibleText.length;
|
|
487
|
+
}
|
|
488
|
+
catch (err) {
|
|
489
|
+
if (err instanceof messageSender.StreamExpiredError) {
|
|
490
|
+
state.streamExpired = true;
|
|
491
|
+
runtime.log?.(`[wecom] Stream expired during intermediate reply, will fallback to proactive send`);
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
throw err;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
},
|
|
501
|
+
onError: (err, info) => {
|
|
502
|
+
runtime.error?.(`[wecom] ${info.kind} reply failed: ${String(err)}`);
|
|
503
|
+
},
|
|
504
|
+
});
|
|
505
|
+
// 关闭 thinking 流
|
|
506
|
+
await finishThinkingStream(ctx);
|
|
507
|
+
safeCleanup();
|
|
508
|
+
}
|
|
509
|
+
catch (err) {
|
|
510
|
+
runtime.error?.(`[wecom][plugin] Failed to process message: ${String(err)}`);
|
|
511
|
+
// 即使转发抛异常,也需要关闭 thinking 流
|
|
512
|
+
try {
|
|
513
|
+
await finishThinkingStream(ctx);
|
|
514
|
+
}
|
|
515
|
+
catch (finishErr) {
|
|
516
|
+
runtime.error?.(`[wecom] Failed to finish thinking stream after dispatch error: ${String(finishErr)}`);
|
|
517
|
+
}
|
|
518
|
+
safeCleanup();
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* 处理企业微信消息(主函数)
|
|
523
|
+
*
|
|
524
|
+
* 处理流程:
|
|
525
|
+
* 1. 解析消息内容(文本、图片、引用)
|
|
526
|
+
* 2. 群组策略检查(仅群聊)
|
|
527
|
+
* 3. DM Policy 访问控制检查(仅私聊)
|
|
528
|
+
* 4. 下载并保存图片和文件
|
|
529
|
+
* 5. 初始化消息状态
|
|
530
|
+
* 6. 转发到智能体并流式回复
|
|
531
|
+
*
|
|
532
|
+
* 整体带超时保护,防止单条消息处理阻塞过久
|
|
533
|
+
*/
|
|
534
|
+
async function processWeComMessage(params) {
|
|
535
|
+
const { frame, account, config, runtime, wsClient } = params;
|
|
536
|
+
const body = frame.body;
|
|
537
|
+
const chatId = body.chatid || body.from.userid;
|
|
538
|
+
const chatType = body.chattype === "group" ? "group" : "direct";
|
|
539
|
+
const messageId = body.msgid;
|
|
540
|
+
const reqId = frame.headers.req_id;
|
|
541
|
+
// Step 1: 解析消息内容
|
|
542
|
+
const { textParts, imageUrls, imageAesKeys, fileUrls, fileAesKeys, quoteContent } = messageParser.parseMessageContent(body);
|
|
543
|
+
let text = textParts.join("\n").trim();
|
|
544
|
+
// 如果文本为空但存在引用消息,使用引用消息内容
|
|
545
|
+
if (!text && quoteContent) {
|
|
546
|
+
text = quoteContent;
|
|
547
|
+
runtime.log?.("[wecom][plugin] Using quote content as message body (user only mentioned bot)");
|
|
548
|
+
}
|
|
549
|
+
// 如果既没有文本也没有图片也没有文件也没有引用内容,则跳过
|
|
550
|
+
if (!text && imageUrls.length === 0 && fileUrls.length === 0) {
|
|
551
|
+
runtime.log?.("[wecom][plugin] Skipping empty message (no text, image, file or quote)");
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
// Step 2: 群组策略检查(仅群聊)
|
|
555
|
+
if (chatType === "group") {
|
|
556
|
+
const groupPolicyResult = groupPolicy.checkGroupPolicy({
|
|
557
|
+
chatId,
|
|
558
|
+
senderId: body.from.userid,
|
|
559
|
+
account,
|
|
560
|
+
runtime,
|
|
561
|
+
});
|
|
562
|
+
if (!groupPolicyResult.allowed) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
// Step 3: DM Policy 访问控制检查(仅私聊)
|
|
567
|
+
const dmPolicyResult = await dmPolicy.checkDmPolicy({
|
|
568
|
+
senderId: body.from.userid,
|
|
569
|
+
isGroup: chatType === "group",
|
|
570
|
+
account,
|
|
571
|
+
runtime,
|
|
572
|
+
});
|
|
573
|
+
if (!dmPolicyResult.allowed) {
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
// Step 4: 下载并保存图片和文件
|
|
577
|
+
const [imageMediaList, fileMediaList] = await Promise.all([
|
|
578
|
+
mediaHandler.downloadAndSaveImages({
|
|
579
|
+
imageUrls,
|
|
580
|
+
imageAesKeys,
|
|
581
|
+
account,
|
|
582
|
+
config,
|
|
583
|
+
runtime,
|
|
584
|
+
wsClient,
|
|
585
|
+
}),
|
|
586
|
+
mediaHandler.downloadAndSaveFiles({
|
|
587
|
+
fileUrls,
|
|
588
|
+
fileAesKeys,
|
|
589
|
+
account,
|
|
590
|
+
config,
|
|
591
|
+
runtime,
|
|
592
|
+
wsClient,
|
|
593
|
+
}),
|
|
594
|
+
]);
|
|
595
|
+
const mediaList = [...imageMediaList, ...fileMediaList];
|
|
596
|
+
// Step 5: 初始化消息状态
|
|
597
|
+
stateManager.setReqIdForChat(chatId, reqId, account.accountId);
|
|
598
|
+
const streamId = aibotNodeSdk.generateReqId("stream");
|
|
599
|
+
const state = { accumulatedText: "", streamId };
|
|
600
|
+
stateManager.setMessageState(messageId, state);
|
|
601
|
+
const cleanupState = () => {
|
|
602
|
+
stateManager.deleteMessageState(messageId);
|
|
603
|
+
};
|
|
604
|
+
// Step 6: 转发到智能体并处理回复(带整体超时保护)
|
|
605
|
+
try {
|
|
606
|
+
await routeAndDispatchToAgent({
|
|
607
|
+
text,
|
|
608
|
+
mediaPaths: mediaList,
|
|
609
|
+
quoteContent,
|
|
610
|
+
account,
|
|
611
|
+
config,
|
|
612
|
+
wsClient,
|
|
613
|
+
frame,
|
|
614
|
+
state,
|
|
615
|
+
runtime,
|
|
616
|
+
onCleanup: cleanupState,
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
catch (err) {
|
|
620
|
+
runtime.error?.(`[wecom][plugin] Message processing failed: ${String(err)}`);
|
|
621
|
+
cleanupState();
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
// ============================================================================
|
|
625
|
+
// 创建 SDK Logger 适配器
|
|
626
|
+
// ============================================================================
|
|
627
|
+
/**
|
|
628
|
+
* 创建适配 RuntimeLogger 的 SDK Logger
|
|
629
|
+
*/
|
|
630
|
+
function createSdkLogger(runtime, accountId) {
|
|
631
|
+
return {
|
|
632
|
+
debug: (message, ...args) => {
|
|
633
|
+
runtime.log?.(`[${accountId}] ${message}`, ...args);
|
|
634
|
+
},
|
|
635
|
+
info: (message, ...args) => {
|
|
636
|
+
runtime.log?.(`[${accountId}] ${message}`, ...args);
|
|
637
|
+
},
|
|
638
|
+
warn: (message, ...args) => {
|
|
639
|
+
runtime.log?.(`[${accountId}] WARN: ${message}`, ...args);
|
|
640
|
+
},
|
|
641
|
+
error: (message, ...args) => {
|
|
642
|
+
runtime.error?.(`[${accountId}] ${message}`, ...args);
|
|
643
|
+
},
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
// ============================================================================
|
|
647
|
+
// 主函数
|
|
648
|
+
// ============================================================================
|
|
649
|
+
/**
|
|
650
|
+
* 监听企业微信私有部署 WebSocket 连接
|
|
651
|
+
* 使用 aibot-node-sdk 简化连接管理
|
|
652
|
+
*/
|
|
653
|
+
async function monitorWeComProvider(options) {
|
|
654
|
+
const { account, config, runtime, abortSignal, setStatus } = options;
|
|
655
|
+
runtime.log?.(`[${account.accountId}] [${version.PLUGIN_VERSION}] Initializing WSClient with SDK...`);
|
|
656
|
+
// 前置校验:websocketUrl 为空时无法建立连接,直接报错退出
|
|
657
|
+
if (!account.websocketUrl?.trim()) {
|
|
658
|
+
const errorMsg = `WebSocket URL 未配置,无法启动连接。请在配置文件的 accounts 中配置有效的 websocketUrl 地址。`;
|
|
659
|
+
runtime.error?.(`[${account.accountId}] ${errorMsg}`);
|
|
660
|
+
setStatus?.({
|
|
661
|
+
accountId: account.accountId,
|
|
662
|
+
running: false,
|
|
663
|
+
lastError: errorMsg,
|
|
664
|
+
lastStopAt: Date.now(),
|
|
665
|
+
});
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
// 如果配置了自签名 CA 证书,在创建任何网络连接之前注入到 Node.js 全局 TLS 层
|
|
669
|
+
if (account.caCert) {
|
|
670
|
+
const injected = caCert.injectCaCert(account.caCert, (...args) => runtime.log?.(...args));
|
|
671
|
+
if (!injected) {
|
|
672
|
+
runtime.log?.(`[${account.accountId}] ⚠️ CA 证书注入失败,TLS 连接到私有部署服务可能会失败。请检查证书文件是否存在且为有效 PEM 格式。`);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
// 启动消息状态定期清理
|
|
676
|
+
stateManager.startMessageStateCleanup();
|
|
677
|
+
return new Promise((resolve, reject) => {
|
|
678
|
+
const logger = createSdkLogger(runtime, account.accountId);
|
|
679
|
+
const wsClient = new aibotNodeSdk.WSClient({
|
|
680
|
+
botId: account.botId,
|
|
681
|
+
secret: account.secret,
|
|
682
|
+
wsUrl: account.websocketUrl,
|
|
683
|
+
logger,
|
|
684
|
+
heartbeatInterval: _const.WS_HEARTBEAT_INTERVAL_MS,
|
|
685
|
+
maxReconnectAttempts: _const.WS_MAX_RECONNECT_ATTEMPTS,
|
|
686
|
+
maxAuthFailureAttempts: _const.WS_MAX_AUTH_FAILURE_ATTEMPTS,
|
|
687
|
+
scene: _const.SCENE_WECOM_AIBOT,
|
|
688
|
+
plug_version: version.PLUGIN_VERSION,
|
|
689
|
+
});
|
|
690
|
+
// 防止 cleanup 被多次调用
|
|
691
|
+
let cleanedUp = false;
|
|
692
|
+
const cleanup = async () => {
|
|
693
|
+
if (cleanedUp)
|
|
694
|
+
return;
|
|
695
|
+
cleanedUp = true;
|
|
696
|
+
stateManager.stopMessageStateCleanup();
|
|
697
|
+
if (account.caCert) {
|
|
698
|
+
caCert.removeCaCertPatch((...args) => runtime.log?.(...args));
|
|
699
|
+
}
|
|
700
|
+
await stateManager.cleanupAccount(account.accountId);
|
|
701
|
+
};
|
|
702
|
+
// 处理中止信号
|
|
703
|
+
if (abortSignal) {
|
|
704
|
+
abortSignal.addEventListener("abort", async () => {
|
|
705
|
+
runtime.log?.(`[${account.accountId}] Connection aborted`);
|
|
706
|
+
wsClient.disconnect();
|
|
707
|
+
await cleanup();
|
|
708
|
+
resolve();
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
// 监听连接事件
|
|
712
|
+
wsClient.on("connected", () => {
|
|
713
|
+
runtime.log?.(`[${account.accountId}] WebSocket connected`);
|
|
714
|
+
setStatus?.({
|
|
715
|
+
accountId: account.accountId,
|
|
716
|
+
running: true,
|
|
717
|
+
lastConnectedAt: Date.now(),
|
|
718
|
+
});
|
|
719
|
+
});
|
|
720
|
+
// 监听认证成功事件
|
|
721
|
+
wsClient.on("authenticated", () => {
|
|
722
|
+
runtime.log?.(`[${account.accountId}] Authentication successful`);
|
|
723
|
+
stateManager.setWeComWebSocket(account.accountId, wsClient);
|
|
724
|
+
});
|
|
725
|
+
// 监听断开事件
|
|
726
|
+
wsClient.on("disconnected", (reason) => {
|
|
727
|
+
runtime.log?.(`[${account.accountId}] WebSocket disconnected: ${reason}`);
|
|
728
|
+
});
|
|
729
|
+
// 监听被踢下线事件
|
|
730
|
+
wsClient.on("event.disconnected_event", async () => {
|
|
731
|
+
const errorMsg = `Kicked by server: a new connection was established elsewhere. Auto-restart is suppressed to avoid mutual kicking. Please check for duplicate instances.`;
|
|
732
|
+
runtime.error?.(`[${account.accountId}] ${errorMsg}`);
|
|
733
|
+
wsClient.disconnect();
|
|
734
|
+
await cleanup();
|
|
735
|
+
setStatus?.({
|
|
736
|
+
accountId: account.accountId,
|
|
737
|
+
running: false,
|
|
738
|
+
lastError: errorMsg,
|
|
739
|
+
lastStopAt: Date.now(),
|
|
740
|
+
});
|
|
741
|
+
// Promise 保持 pending,不触发 auto-restart
|
|
742
|
+
});
|
|
743
|
+
// 监听重连事件
|
|
744
|
+
wsClient.on("reconnecting", (attempt) => {
|
|
745
|
+
runtime.log?.(`[${account.accountId}] Reconnecting attempt ${attempt}...`);
|
|
746
|
+
});
|
|
747
|
+
// 监听错误事件
|
|
748
|
+
wsClient.on("error", async (error) => {
|
|
749
|
+
runtime.error?.(`[${account.accountId}] WebSocket error: ${error.message}`);
|
|
750
|
+
if (error instanceof aibotNodeSdk.WSAuthFailureError) {
|
|
751
|
+
const errorMsg = `Auth failure attempts exhausted (${_const.WS_MAX_AUTH_FAILURE_ATTEMPTS} attempts). Please check botId/secret configuration.`;
|
|
752
|
+
runtime.error?.(`[${account.accountId}] ${errorMsg}`);
|
|
753
|
+
wsClient.disconnect();
|
|
754
|
+
await cleanup();
|
|
755
|
+
setStatus?.({
|
|
756
|
+
accountId: account.accountId,
|
|
757
|
+
running: false,
|
|
758
|
+
lastError: errorMsg,
|
|
759
|
+
lastStopAt: Date.now(),
|
|
760
|
+
});
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
if (error instanceof aibotNodeSdk.WSReconnectExhaustedError) {
|
|
764
|
+
wsClient.disconnect();
|
|
765
|
+
cleanup().finally(() => reject(error));
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
// 监听版本检查事件
|
|
770
|
+
wsClient.on(_const.EVENT_ENTER_CHECK_UPDATE, async (frame) => {
|
|
771
|
+
try {
|
|
772
|
+
await wsClient.reply(frame, { version: version.PLUGIN_VERSION }, _const.CMD_ENTER_EVENT_REPLY);
|
|
773
|
+
}
|
|
774
|
+
catch (err) {
|
|
775
|
+
// 版本检查回复失败不影响主流程
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
// 监听普通消息
|
|
779
|
+
wsClient.on("message", async (frame) => {
|
|
780
|
+
try {
|
|
781
|
+
await processWeComMessage({
|
|
782
|
+
frame,
|
|
783
|
+
account,
|
|
784
|
+
config,
|
|
785
|
+
runtime,
|
|
786
|
+
wsClient,
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
catch (err) {
|
|
790
|
+
runtime.error?.(`[${account.accountId}] Failed to process message: ${String(err)}`);
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
// 监听事件回调(aibot_event_callback)
|
|
794
|
+
wsClient.on("event", async (frame) => {
|
|
795
|
+
try {
|
|
796
|
+
const eventBody = frame.body;
|
|
797
|
+
const eventType = eventBody.event?.eventtype;
|
|
798
|
+
runtime.log?.(`[${account.accountId}] Received event callback: eventtype=${eventType ?? ""}, msgid=${eventBody.msgid ?? ""}`);
|
|
799
|
+
if (eventType !== "template_card_event") {
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
const templateCardEvent = eventBody.event?.template_card_event;
|
|
803
|
+
runtime.log?.(`[${account.accountId}] Received template_card_event: event_key=${templateCardEvent?.event_key ?? ""}, task_id=${templateCardEvent?.task_id ?? ""}`);
|
|
804
|
+
try {
|
|
805
|
+
await updateTemplateCardOnEvent({
|
|
806
|
+
frame,
|
|
807
|
+
accountId: account.accountId,
|
|
808
|
+
runtime,
|
|
809
|
+
wsClient,
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
catch (updateErr) {
|
|
813
|
+
runtime.error?.(`[${account.accountId}] [template-card-update] Failed to update template card: ${String(updateErr)}`);
|
|
814
|
+
}
|
|
815
|
+
await processWeComMessage({
|
|
816
|
+
frame,
|
|
817
|
+
account,
|
|
818
|
+
config,
|
|
819
|
+
runtime,
|
|
820
|
+
wsClient,
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
catch (err) {
|
|
824
|
+
runtime.error?.(`[${account.accountId}] Failed to process template_card_event: ${String(err)}`);
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
runtime.log?.(`[${account.accountId}] Event listeners attached: message + event(template_card_event)`);
|
|
828
|
+
// 启动前预热 reqId 缓存
|
|
829
|
+
stateManager.warmupReqIdStore(account.accountId, (...args) => runtime.log?.(...args))
|
|
830
|
+
.then((count) => {
|
|
831
|
+
runtime.log?.(`[${account.accountId}] Warmed up ${count} reqId entries from disk`);
|
|
832
|
+
})
|
|
833
|
+
.catch((err) => {
|
|
834
|
+
runtime.error?.(`[${account.accountId}] Failed to warmup reqId store: ${String(err)}`);
|
|
835
|
+
})
|
|
836
|
+
.finally(() => {
|
|
837
|
+
wsClient.connect();
|
|
838
|
+
});
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
Object.defineProperty(exports, "WeComCommand", {
|
|
843
|
+
enumerable: true,
|
|
844
|
+
get: function () { return _const.WeComCommand; }
|
|
845
|
+
});
|
|
846
|
+
exports.sendWeComReply = messageSender.sendWeComReply;
|
|
847
|
+
exports.setReqIdForChat = stateManager.setReqIdForChat;
|
|
848
|
+
exports.warmupReqIdStore = stateManager.warmupReqIdStore;
|
|
849
|
+
exports.monitorWeComProvider = monitorWeComProvider;
|