@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,495 @@
|
|
|
1
|
+
import { VALID_CARD_TYPES } from './const.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 模板卡片解析器
|
|
5
|
+
*
|
|
6
|
+
* 从 LLM 回复文本中提取 markdown JSON 代码块,验证其是否为合法的企业微信模板卡片,
|
|
7
|
+
* 返回提取到的卡片列表和剩余文本。
|
|
8
|
+
*
|
|
9
|
+
* 同时提供 maskTemplateCardBlocks 函数,用于在流式中间帧中隐藏正在构建的卡片代码块,
|
|
10
|
+
* 避免 JSON 源码暴露给终端用户。
|
|
11
|
+
*/
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// LLM 输出字段类型修正
|
|
14
|
+
// ============================================================================
|
|
15
|
+
/**
|
|
16
|
+
* 将 LLM 可能输出的字符串/非法值修正为企业微信 API 要求的整数。
|
|
17
|
+
*/
|
|
18
|
+
function coerceToInt(value) {
|
|
19
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
20
|
+
return Math.round(value);
|
|
21
|
+
}
|
|
22
|
+
if (typeof value === "string") {
|
|
23
|
+
const trimmed = value.trim().toLowerCase();
|
|
24
|
+
const num = Number(trimmed);
|
|
25
|
+
if (!Number.isNaN(num) && Number.isFinite(num)) {
|
|
26
|
+
return Math.round(num);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
/** 将 LLM 可能输出的字符串/非法值修正为布尔值 */
|
|
32
|
+
function coerceToBool(value) {
|
|
33
|
+
if (typeof value === "boolean")
|
|
34
|
+
return value;
|
|
35
|
+
if (typeof value === "string") {
|
|
36
|
+
const t = value.trim().toLowerCase();
|
|
37
|
+
if (t === "true" || t === "1" || t === "yes")
|
|
38
|
+
return true;
|
|
39
|
+
if (t === "false" || t === "0" || t === "no")
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
if (typeof value === "number")
|
|
43
|
+
return value !== 0;
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
/** checkbox.mode 的语义别名映射 */
|
|
47
|
+
const MODE_ALIASES = {
|
|
48
|
+
single: 0,
|
|
49
|
+
radio: 0,
|
|
50
|
+
"单选": 0,
|
|
51
|
+
multi: 1,
|
|
52
|
+
multiple: 1,
|
|
53
|
+
"多选": 1,
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* 修正 checkbox.mode
|
|
57
|
+
*/
|
|
58
|
+
function coerceCheckboxMode(value) {
|
|
59
|
+
let num;
|
|
60
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
61
|
+
num = Math.round(value);
|
|
62
|
+
}
|
|
63
|
+
else if (typeof value === "string") {
|
|
64
|
+
const trimmed = value.trim().toLowerCase();
|
|
65
|
+
if (trimmed in MODE_ALIASES)
|
|
66
|
+
return MODE_ALIASES[trimmed];
|
|
67
|
+
const parsed = Number(trimmed);
|
|
68
|
+
if (!Number.isNaN(parsed))
|
|
69
|
+
num = Math.round(parsed);
|
|
70
|
+
}
|
|
71
|
+
if (num === undefined)
|
|
72
|
+
return undefined;
|
|
73
|
+
if (num <= 0)
|
|
74
|
+
return 0;
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 对 LLM 生成的模板卡片 JSON 做字段类型修正
|
|
79
|
+
*/
|
|
80
|
+
function normalizeTemplateCardFields(card, log) {
|
|
81
|
+
const fixes = [];
|
|
82
|
+
// ── checkbox ──────────────────────────────────────────────────────────
|
|
83
|
+
const checkbox = card.checkbox;
|
|
84
|
+
if (checkbox && typeof checkbox === "object") {
|
|
85
|
+
if ("mode" in checkbox) {
|
|
86
|
+
const fixed = coerceCheckboxMode(checkbox.mode);
|
|
87
|
+
if (fixed !== undefined) {
|
|
88
|
+
if (checkbox.mode !== fixed) {
|
|
89
|
+
fixes.push(`checkbox.mode: ${JSON.stringify(checkbox.mode)} → ${fixed}`);
|
|
90
|
+
}
|
|
91
|
+
checkbox.mode = fixed;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
fixes.push(`checkbox.mode: ${JSON.stringify(checkbox.mode)} → (deleted, invalid)`);
|
|
95
|
+
delete checkbox.mode;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if ("disable" in checkbox) {
|
|
99
|
+
const fixed = coerceToBool(checkbox.disable);
|
|
100
|
+
if (fixed !== undefined && checkbox.disable !== fixed) {
|
|
101
|
+
fixes.push(`checkbox.disable: ${JSON.stringify(checkbox.disable)} → ${fixed}`);
|
|
102
|
+
checkbox.disable = fixed;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (Array.isArray(checkbox.option_list)) {
|
|
106
|
+
for (const opt of checkbox.option_list) {
|
|
107
|
+
if (opt && typeof opt === "object" && "is_checked" in opt) {
|
|
108
|
+
const fixed = coerceToBool(opt.is_checked);
|
|
109
|
+
if (fixed !== undefined && opt.is_checked !== fixed) {
|
|
110
|
+
fixes.push(`checkbox.option_list.is_checked: ${JSON.stringify(opt.is_checked)} → ${fixed}`);
|
|
111
|
+
opt.is_checked = fixed;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// ── source.desc_color ────────────────────────────────────────────────
|
|
118
|
+
const source = card.source;
|
|
119
|
+
if (source && typeof source === "object" && "desc_color" in source) {
|
|
120
|
+
const fixed = coerceToInt(source.desc_color);
|
|
121
|
+
if (fixed !== undefined && source.desc_color !== fixed) {
|
|
122
|
+
fixes.push(`source.desc_color: ${JSON.stringify(source.desc_color)} → ${fixed}`);
|
|
123
|
+
source.desc_color = fixed;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// ── card_action.type ─────────────────────────────────────────────────
|
|
127
|
+
const cardAction = card.card_action;
|
|
128
|
+
if (cardAction && typeof cardAction === "object" && "type" in cardAction) {
|
|
129
|
+
const fixed = coerceToInt(cardAction.type);
|
|
130
|
+
if (fixed !== undefined && cardAction.type !== fixed) {
|
|
131
|
+
fixes.push(`card_action.type: ${JSON.stringify(cardAction.type)} → ${fixed}`);
|
|
132
|
+
cardAction.type = fixed;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// ── quote_area.type ──────────────────────────────────────────────────
|
|
136
|
+
const quoteArea = card.quote_area;
|
|
137
|
+
if (quoteArea && typeof quoteArea === "object" && "type" in quoteArea) {
|
|
138
|
+
const fixed = coerceToInt(quoteArea.type);
|
|
139
|
+
if (fixed !== undefined && quoteArea.type !== fixed) {
|
|
140
|
+
fixes.push(`quote_area.type: ${JSON.stringify(quoteArea.type)} → ${fixed}`);
|
|
141
|
+
quoteArea.type = fixed;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// ── image_text_area.type ─────────────────────────────────────────────
|
|
145
|
+
const imageTextArea = card.image_text_area;
|
|
146
|
+
if (imageTextArea && typeof imageTextArea === "object" && "type" in imageTextArea) {
|
|
147
|
+
const fixed = coerceToInt(imageTextArea.type);
|
|
148
|
+
if (fixed !== undefined && imageTextArea.type !== fixed) {
|
|
149
|
+
fixes.push(`image_text_area.type: ${JSON.stringify(imageTextArea.type)} → ${fixed}`);
|
|
150
|
+
imageTextArea.type = fixed;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// ── horizontal_content_list[].type ───────────────────────────────────
|
|
154
|
+
if (Array.isArray(card.horizontal_content_list)) {
|
|
155
|
+
for (const item of card.horizontal_content_list) {
|
|
156
|
+
if (item && typeof item === "object" && "type" in item) {
|
|
157
|
+
const fixed = coerceToInt(item.type);
|
|
158
|
+
if (fixed !== undefined && item.type !== fixed) {
|
|
159
|
+
fixes.push(`horizontal_content_list.type: ${JSON.stringify(item.type)} → ${fixed}`);
|
|
160
|
+
item.type = fixed;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// ── jump_list[].type ─────────────────────────────────────────────────
|
|
166
|
+
if (Array.isArray(card.jump_list)) {
|
|
167
|
+
for (const item of card.jump_list) {
|
|
168
|
+
if (item && typeof item === "object" && "type" in item) {
|
|
169
|
+
const fixed = coerceToInt(item.type);
|
|
170
|
+
if (fixed !== undefined && item.type !== fixed) {
|
|
171
|
+
fixes.push(`jump_list.type: ${JSON.stringify(item.type)} → ${fixed}`);
|
|
172
|
+
item.type = fixed;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// ── button_list[].style ──────────────────────────────────────────────
|
|
178
|
+
if (Array.isArray(card.button_list)) {
|
|
179
|
+
for (const btn of card.button_list) {
|
|
180
|
+
if (btn && typeof btn === "object" && "style" in btn) {
|
|
181
|
+
const fixed = coerceToInt(btn.style);
|
|
182
|
+
if (fixed !== undefined && btn.style !== fixed) {
|
|
183
|
+
fixes.push(`button_list.style: ${JSON.stringify(btn.style)} → ${fixed}`);
|
|
184
|
+
btn.style = fixed;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// ── button_selection.disable ─────────────────────────────────────────
|
|
190
|
+
const buttonSelection = card.button_selection;
|
|
191
|
+
if (buttonSelection && typeof buttonSelection === "object" && "disable" in buttonSelection) {
|
|
192
|
+
const fixed = coerceToBool(buttonSelection.disable);
|
|
193
|
+
if (fixed !== undefined && buttonSelection.disable !== fixed) {
|
|
194
|
+
fixes.push(`button_selection.disable: ${JSON.stringify(buttonSelection.disable)} → ${fixed}`);
|
|
195
|
+
buttonSelection.disable = fixed;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// ── select_list[].disable ────────────────────────────────────────────
|
|
199
|
+
if (Array.isArray(card.select_list)) {
|
|
200
|
+
for (const sel of card.select_list) {
|
|
201
|
+
if (sel && typeof sel === "object" && "disable" in sel) {
|
|
202
|
+
const fixed = coerceToBool(sel.disable);
|
|
203
|
+
if (fixed !== undefined && sel.disable !== fixed) {
|
|
204
|
+
fixes.push(`select_list.disable: ${JSON.stringify(sel.disable)} → ${fixed}`);
|
|
205
|
+
sel.disable = fixed;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (fixes.length > 0) {
|
|
211
|
+
log?.(`[template-card-parser] normalizeTemplateCardFields: ${fixes.length} fix(es) applied: ${fixes.join("; ")}`);
|
|
212
|
+
}
|
|
213
|
+
return card;
|
|
214
|
+
}
|
|
215
|
+
function validateAndFixRequiredFields(card, log) {
|
|
216
|
+
const cardType = card.card_type;
|
|
217
|
+
const fixes = [];
|
|
218
|
+
const warnings = [];
|
|
219
|
+
// ── task_id ─────────────────────────────
|
|
220
|
+
const rawTid = (typeof card.task_id === "string" && card.task_id.trim()) ? card.task_id.trim() : "";
|
|
221
|
+
const rand = Math.random().toString(36).slice(2, 6);
|
|
222
|
+
const ts = Date.now();
|
|
223
|
+
let finalTid;
|
|
224
|
+
if (rawTid) {
|
|
225
|
+
const prefix = rawTid.replace(/_\d{8,}$/, "").replace(/[^a-zA-Z0-9_\-@]/g, "_").slice(0, 80);
|
|
226
|
+
finalTid = prefix ? `${prefix}_${ts}_${rand}` : `task_${cardType}_${ts}_${rand}`;
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
finalTid = `task_${cardType}_${ts}_${rand}`;
|
|
230
|
+
}
|
|
231
|
+
if (finalTid !== rawTid) {
|
|
232
|
+
fixes.push(`task_id: "${rawTid || "(missing)"}" → "${finalTid}"`);
|
|
233
|
+
}
|
|
234
|
+
card.task_id = finalTid;
|
|
235
|
+
// ── main_title ────────────────────────────────────────────────────────
|
|
236
|
+
const mainTitle = card.main_title;
|
|
237
|
+
const hasMainTitle = mainTitle && typeof mainTitle === "object" &&
|
|
238
|
+
(typeof mainTitle.title === "string" && mainTitle.title.trim());
|
|
239
|
+
const hasSubTitleText = typeof card.sub_title_text === "string" && card.sub_title_text.trim();
|
|
240
|
+
switch (cardType) {
|
|
241
|
+
case "text_notice":
|
|
242
|
+
if (!hasMainTitle && !hasSubTitleText) {
|
|
243
|
+
card.sub_title_text = card.sub_title_text || "通知";
|
|
244
|
+
fixes.push(`sub_title_text: (missing, no main_title either) → fallback "通知"`);
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
247
|
+
case "news_notice":
|
|
248
|
+
case "button_interaction":
|
|
249
|
+
case "vote_interaction":
|
|
250
|
+
case "multiple_interaction":
|
|
251
|
+
if (!mainTitle || typeof mainTitle !== "object") {
|
|
252
|
+
card.main_title = { title: "通知" };
|
|
253
|
+
fixes.push(`main_title: (missing) → { title: "通知" }`);
|
|
254
|
+
}
|
|
255
|
+
else if (!hasMainTitle) {
|
|
256
|
+
mainTitle.title = "通知";
|
|
257
|
+
fixes.push(`main_title.title: (empty) → "通知"`);
|
|
258
|
+
}
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
// ── card_action ──────────────────────────
|
|
262
|
+
if (cardType === "text_notice" || cardType === "news_notice") {
|
|
263
|
+
if (!card.card_action || typeof card.card_action !== "object") {
|
|
264
|
+
card.card_action = { type: 1, url: "https://work.weixin.qq.com" };
|
|
265
|
+
fixes.push(`card_action: (missing) → { type: 1, url: "https://work.weixin.qq.com" }`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// ── submit_button ──────────────────────────
|
|
269
|
+
if (cardType === "vote_interaction" || cardType === "multiple_interaction") {
|
|
270
|
+
if (!card.submit_button || typeof card.submit_button !== "object") {
|
|
271
|
+
card.submit_button = { text: "提交", key: `submit_${cardType}_${Date.now()}` };
|
|
272
|
+
fixes.push(`submit_button: (missing) → auto-generated`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// ── 核心业务字段告警 ──────────────────────────
|
|
276
|
+
if (cardType === "button_interaction") {
|
|
277
|
+
if (!Array.isArray(card.button_list) || card.button_list.length === 0) {
|
|
278
|
+
warnings.push(`button_list is missing or empty (required for button_interaction)`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (cardType === "vote_interaction") {
|
|
282
|
+
if (!card.checkbox || typeof card.checkbox !== "object") {
|
|
283
|
+
warnings.push(`checkbox is missing (required for vote_interaction)`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (cardType === "multiple_interaction") {
|
|
287
|
+
if (!Array.isArray(card.select_list) || card.select_list.length === 0) {
|
|
288
|
+
warnings.push(`select_list is missing or empty (required for multiple_interaction)`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (fixes.length > 0) {
|
|
292
|
+
log?.(`[template-card-parser] validateAndFixRequiredFields: ${fixes.length} fix(es): ${fixes.join("; ")}`);
|
|
293
|
+
}
|
|
294
|
+
if (warnings.length > 0) {
|
|
295
|
+
log?.(`[template-card-parser] validateAndFixRequiredFields: ${warnings.length} warning(s): ${warnings.join("; ")}`);
|
|
296
|
+
}
|
|
297
|
+
return card;
|
|
298
|
+
}
|
|
299
|
+
// ============================================================================
|
|
300
|
+
// 简化格式 → 企微 API 格式转换
|
|
301
|
+
// ============================================================================
|
|
302
|
+
function generateKey(prefix) {
|
|
303
|
+
const rand = Math.random().toString(36).slice(2, 6);
|
|
304
|
+
return `${prefix}_${Date.now()}_${rand}`;
|
|
305
|
+
}
|
|
306
|
+
function transformVoteInteraction(card, log) {
|
|
307
|
+
const existingCheckbox = card.checkbox;
|
|
308
|
+
if (existingCheckbox && typeof existingCheckbox === "object" && Array.isArray(existingCheckbox.option_list)) {
|
|
309
|
+
log?.(`[template-card-parser] transformVoteInteraction: already has checkbox.option_list, skipping transform`);
|
|
310
|
+
return card;
|
|
311
|
+
}
|
|
312
|
+
const options = card.options;
|
|
313
|
+
if (!Array.isArray(options) || options.length === 0) {
|
|
314
|
+
log?.(`[template-card-parser] transformVoteInteraction: no "options" array found, skipping transform`);
|
|
315
|
+
return card;
|
|
316
|
+
}
|
|
317
|
+
log?.(`[template-card-parser] transformVoteInteraction: transforming simplified format → API format`);
|
|
318
|
+
log?.(`[template-card-parser] transformVoteInteraction: input=${JSON.stringify(card)}`);
|
|
319
|
+
const title = card.title;
|
|
320
|
+
const description = card.description;
|
|
321
|
+
if (title || description) {
|
|
322
|
+
card.main_title = {
|
|
323
|
+
...(title ? { title } : {}),
|
|
324
|
+
...(description ? { desc: description } : {}),
|
|
325
|
+
};
|
|
326
|
+
delete card.title;
|
|
327
|
+
delete card.description;
|
|
328
|
+
}
|
|
329
|
+
const mode = coerceCheckboxMode(card.mode) ?? 0;
|
|
330
|
+
const questionKey = generateKey("vote");
|
|
331
|
+
const clampedOptions = options.slice(0, 20);
|
|
332
|
+
if (options.length > 20) {
|
|
333
|
+
log?.(`[template-card-parser] transformVoteInteraction: options count ${options.length} exceeds max 20, clamped to 20`);
|
|
334
|
+
}
|
|
335
|
+
card.checkbox = {
|
|
336
|
+
question_key: questionKey,
|
|
337
|
+
mode,
|
|
338
|
+
option_list: clampedOptions.map((opt) => ({
|
|
339
|
+
id: String(opt.id ?? opt.value ?? `opt_${Math.random().toString(36).slice(2, 6)}`),
|
|
340
|
+
text: String(opt.text ?? opt.label ?? opt.name ?? ""),
|
|
341
|
+
})),
|
|
342
|
+
};
|
|
343
|
+
delete card.options;
|
|
344
|
+
delete card.mode;
|
|
345
|
+
const submitText = card.submit_text || "提交";
|
|
346
|
+
card.submit_button = {
|
|
347
|
+
text: submitText,
|
|
348
|
+
key: generateKey("submit_vote"),
|
|
349
|
+
};
|
|
350
|
+
delete card.submit_text;
|
|
351
|
+
delete card.vote_question;
|
|
352
|
+
delete card.vote_option;
|
|
353
|
+
delete card.vote_options;
|
|
354
|
+
log?.(`[template-card-parser] transformVoteInteraction: output=${JSON.stringify(card)}`);
|
|
355
|
+
return card;
|
|
356
|
+
}
|
|
357
|
+
function transformMultipleInteraction(card, log) {
|
|
358
|
+
const existingSelectList = card.select_list;
|
|
359
|
+
if (Array.isArray(existingSelectList) &&
|
|
360
|
+
existingSelectList.length > 0 &&
|
|
361
|
+
Array.isArray(existingSelectList[0]?.option_list)) {
|
|
362
|
+
log?.(`[template-card-parser] transformMultipleInteraction: already has select_list[].option_list, skipping transform`);
|
|
363
|
+
return card;
|
|
364
|
+
}
|
|
365
|
+
const selectors = card.selectors;
|
|
366
|
+
if (!Array.isArray(selectors) || selectors.length === 0) {
|
|
367
|
+
log?.(`[template-card-parser] transformMultipleInteraction: no "selectors" array found, skipping transform`);
|
|
368
|
+
return card;
|
|
369
|
+
}
|
|
370
|
+
log?.(`[template-card-parser] transformMultipleInteraction: transforming simplified format → API format`);
|
|
371
|
+
log?.(`[template-card-parser] transformMultipleInteraction: input=${JSON.stringify(card)}`);
|
|
372
|
+
const title = card.title;
|
|
373
|
+
const description = card.description;
|
|
374
|
+
if (title || description) {
|
|
375
|
+
card.main_title = {
|
|
376
|
+
...(title ? { title } : {}),
|
|
377
|
+
...(description ? { desc: description } : {}),
|
|
378
|
+
};
|
|
379
|
+
delete card.title;
|
|
380
|
+
delete card.description;
|
|
381
|
+
}
|
|
382
|
+
const clampedSelectors = selectors.slice(0, 3);
|
|
383
|
+
if (selectors.length > 3) {
|
|
384
|
+
log?.(`[template-card-parser] transformMultipleInteraction: selectors count ${selectors.length} exceeds max 3, clamped to 3`);
|
|
385
|
+
}
|
|
386
|
+
card.select_list = clampedSelectors.map((sel, idx) => {
|
|
387
|
+
const selectorOptions = (sel.options ?? []).slice(0, 10);
|
|
388
|
+
return {
|
|
389
|
+
question_key: generateKey(`sel_${idx}`),
|
|
390
|
+
title: String(sel.title ?? sel.label ?? `选择${idx + 1}`),
|
|
391
|
+
option_list: selectorOptions.map((opt) => ({
|
|
392
|
+
id: String(opt.id ?? opt.value ?? `opt_${Math.random().toString(36).slice(2, 6)}`),
|
|
393
|
+
text: String(opt.text ?? opt.label ?? opt.name ?? ""),
|
|
394
|
+
})),
|
|
395
|
+
};
|
|
396
|
+
});
|
|
397
|
+
delete card.selectors;
|
|
398
|
+
const submitText = card.submit_text || "提交";
|
|
399
|
+
card.submit_button = {
|
|
400
|
+
text: submitText,
|
|
401
|
+
key: generateKey("submit_multi"),
|
|
402
|
+
};
|
|
403
|
+
delete card.submit_text;
|
|
404
|
+
log?.(`[template-card-parser] transformMultipleInteraction: output=${JSON.stringify(card)}`);
|
|
405
|
+
return card;
|
|
406
|
+
}
|
|
407
|
+
function transformSimplifiedCard(card, log) {
|
|
408
|
+
const cardType = card.card_type;
|
|
409
|
+
if (cardType === "vote_interaction") {
|
|
410
|
+
return transformVoteInteraction(card, log);
|
|
411
|
+
}
|
|
412
|
+
if (cardType === "multiple_interaction") {
|
|
413
|
+
return transformMultipleInteraction(card, log);
|
|
414
|
+
}
|
|
415
|
+
return card;
|
|
416
|
+
}
|
|
417
|
+
const CODE_BLOCK_RE = /```(?:json)?\s*\n([\s\S]*?)\n```/g;
|
|
418
|
+
const CLOSED_BLOCK_RE = /```(?:json)?\s*\n([\s\S]*?)\n```/g;
|
|
419
|
+
const UNCLOSED_BLOCK_RE = /```(?:json)?\s*\n[\s\S]*$/;
|
|
420
|
+
/**
|
|
421
|
+
* 从文本中提取模板卡片 JSON 代码块
|
|
422
|
+
*/
|
|
423
|
+
function extractTemplateCards(text, log) {
|
|
424
|
+
const cards = [];
|
|
425
|
+
const blocksToRemove = [];
|
|
426
|
+
log?.(`[template-card-parser] extractTemplateCards called, textLength=${text.length}`);
|
|
427
|
+
let match;
|
|
428
|
+
CODE_BLOCK_RE.lastIndex = 0;
|
|
429
|
+
let blockIndex = 0;
|
|
430
|
+
while ((match = CODE_BLOCK_RE.exec(text)) !== null) {
|
|
431
|
+
const fullMatch = match[0];
|
|
432
|
+
const jsonContent = match[1].trim();
|
|
433
|
+
blockIndex++;
|
|
434
|
+
log?.(`[template-card-parser] Found code block #${blockIndex}, length=${fullMatch.length}, preview=${jsonContent.slice(0, 1000)}...`);
|
|
435
|
+
let parsed;
|
|
436
|
+
try {
|
|
437
|
+
parsed = JSON.parse(jsonContent);
|
|
438
|
+
}
|
|
439
|
+
catch (e) {
|
|
440
|
+
log?.(`[template-card-parser] Code block #${blockIndex} JSON parse failed: ${String(e)}`);
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
const cardType = parsed.card_type;
|
|
444
|
+
if (typeof cardType !== "string" || !VALID_CARD_TYPES.includes(cardType)) {
|
|
445
|
+
log?.(`[template-card-parser] Code block #${blockIndex} has invalid card_type="${String(cardType)}", skipping`);
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
log?.(`[template-card-parser] Code block #${blockIndex} is valid template card, card_type="${cardType}"`);
|
|
449
|
+
transformSimplifiedCard(parsed, log);
|
|
450
|
+
normalizeTemplateCardFields(parsed, log);
|
|
451
|
+
validateAndFixRequiredFields(parsed, log);
|
|
452
|
+
cards.push({
|
|
453
|
+
cardJson: parsed,
|
|
454
|
+
cardType,
|
|
455
|
+
});
|
|
456
|
+
blocksToRemove.push(fullMatch);
|
|
457
|
+
}
|
|
458
|
+
let remainingText = text;
|
|
459
|
+
for (const block of blocksToRemove) {
|
|
460
|
+
remainingText = remainingText.replace(block, "");
|
|
461
|
+
}
|
|
462
|
+
remainingText = remainingText.replace(/\n{3,}/g, "\n\n").trim();
|
|
463
|
+
log?.(`[template-card-parser] Extraction done: ${cards.length} card(s) found, remainingTextLength=${remainingText.length}`);
|
|
464
|
+
return { cards, remainingText };
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* 遮罩文本中的模板卡片代码块(用于流式中间帧展示)
|
|
468
|
+
*/
|
|
469
|
+
function maskTemplateCardBlocks(text, log) {
|
|
470
|
+
let masked = text;
|
|
471
|
+
let closedMaskCount = 0;
|
|
472
|
+
let unclosedMasked = false;
|
|
473
|
+
CLOSED_BLOCK_RE.lastIndex = 0;
|
|
474
|
+
masked = masked.replace(CLOSED_BLOCK_RE, (fullMatch, content) => {
|
|
475
|
+
if (/["']card_type["']/.test(content)) {
|
|
476
|
+
closedMaskCount++;
|
|
477
|
+
return "\n\n📋 *正在生成卡片消息...*\n\n";
|
|
478
|
+
}
|
|
479
|
+
return fullMatch;
|
|
480
|
+
});
|
|
481
|
+
const unclosedMatch = UNCLOSED_BLOCK_RE.exec(masked);
|
|
482
|
+
if (unclosedMatch) {
|
|
483
|
+
const unclosedContent = unclosedMatch[0];
|
|
484
|
+
if (/["']card_type["']/.test(unclosedContent)) {
|
|
485
|
+
unclosedMasked = true;
|
|
486
|
+
masked = masked.slice(0, unclosedMatch.index) + "\n\n📋 *正在生成卡片消息...*";
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (closedMaskCount > 0 || unclosedMasked) {
|
|
490
|
+
log?.(`[template-card-parser] maskTemplateCardBlocks: closedMasked=${closedMaskCount}, unclosedMasked=${unclosedMasked}, textLength=${text.length}, maskedLength=${masked.length}`);
|
|
491
|
+
}
|
|
492
|
+
return masked;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export { extractTemplateCards, maskTemplateCardBlocks };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 超时控制工具模块
|
|
3
|
+
*
|
|
4
|
+
* 为异步操作提供统一的超时保护机制
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* 为 Promise 添加超时保护
|
|
8
|
+
*
|
|
9
|
+
* @param promise - 原始 Promise
|
|
10
|
+
* @param timeoutMs - 超时时间(毫秒)
|
|
11
|
+
* @param message - 超时错误消息
|
|
12
|
+
* @returns 带超时保护的 Promise
|
|
13
|
+
*/
|
|
14
|
+
function withTimeout(promise, timeoutMs, message) {
|
|
15
|
+
if (timeoutMs <= 0 || !Number.isFinite(timeoutMs)) {
|
|
16
|
+
return promise;
|
|
17
|
+
}
|
|
18
|
+
let timeoutId;
|
|
19
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
20
|
+
timeoutId = setTimeout(() => {
|
|
21
|
+
reject(new TimeoutError(message ?? `Operation timed out after ${timeoutMs}ms`));
|
|
22
|
+
}, timeoutMs);
|
|
23
|
+
});
|
|
24
|
+
return Promise.race([promise, timeoutPromise]).finally(() => {
|
|
25
|
+
clearTimeout(timeoutId);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 超时错误类型
|
|
30
|
+
*/
|
|
31
|
+
class TimeoutError extends Error {
|
|
32
|
+
constructor(message) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = "TimeoutError";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { TimeoutError, withTimeout };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
/** 从 package.json 中读取版本号,兼容打包产物和直接运行 .ts 两种场景 */
|
|
6
|
+
const getVersion = () => {
|
|
7
|
+
try {
|
|
8
|
+
// ESM 环境使用 import.meta.url,CJS 环境使用全局 __dirname
|
|
9
|
+
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
// 直接运行 .ts 时在 src/ 下,打包后在 dist/ 下,都向上一级找 package.json
|
|
11
|
+
const pkgPath = resolve(currentDir, "..", "package.json");
|
|
12
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
13
|
+
return pkg.version ?? "";
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
/** 插件版本号,来源于 package.json */
|
|
20
|
+
const PLUGIN_VERSION = getVersion();
|
|
21
|
+
|
|
22
|
+
export { PLUGIN_VERSION };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @wecom/aibot-plugin — 企业微信私有部署 AI 机器人插件
|
|
3
|
+
*
|
|
4
|
+
* 独立运行的 Node.js 服务入口
|
|
5
|
+
*
|
|
6
|
+
* 启动流程:
|
|
7
|
+
* 1. 解析命令行参数,确定配置文件路径
|
|
8
|
+
* 2. 加载 JSON 配置文件
|
|
9
|
+
* 3. 注入 CA 证书(如配置)
|
|
10
|
+
* 4. 启动 Express HTTP 服务
|
|
11
|
+
* 5. 为每个启用的账号创建 WSClient 连接
|
|
12
|
+
* 6. 注册进程信号处理(SIGINT/SIGTERM 优雅退出)
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic 适配器
|
|
3
|
+
*
|
|
4
|
+
* 适用于:Anthropic Claude 原生 API,以及阿里百炼、腾讯混元、MiniMax、
|
|
5
|
+
* AWS Bedrock、Google Vertex AI 等提供 Anthropic Messages API 兼容接口的平台
|
|
6
|
+
*
|
|
7
|
+
* 与 OpenAI 的核心差异:
|
|
8
|
+
* - 接口路径:POST /v1/messages(而非 /v1/chat/completions)
|
|
9
|
+
* - 系统提示词:顶层 system 字段(而非 messages[0].role = "system")
|
|
10
|
+
* - 认证方式:x-api-key 头(而非 Authorization: Bearer)
|
|
11
|
+
* - 响应结构:content[] 块数组(而非 choices[0].message.content 字符串)
|
|
12
|
+
* - 流式格式:多事件类型(message_start → content_block_delta → message_stop)
|
|
13
|
+
* - 思维链:原生 type: "thinking" 块(而非私有 reasoning_content 字段)
|
|
14
|
+
* - 必填字段:max_tokens(OpenAI 可选)
|
|
15
|
+
*
|
|
16
|
+
* 推理内容折叠展示格式(collapse 模式,复用 DeepSeek 策略):
|
|
17
|
+
* <think>
|
|
18
|
+
* {thinking}
|
|
19
|
+
* </think>
|
|
20
|
+
*
|
|
21
|
+
* {content}
|
|
22
|
+
*/
|
|
23
|
+
import type { AgentEndpoint } from "../interface.js";
|
|
24
|
+
import { BaseAdapter } from "./base-adapter.js";
|
|
25
|
+
import type { AdapterRequest, AdapterCallbacks } from "./types.js";
|
|
26
|
+
/**
|
|
27
|
+
* Anthropic 适配器
|
|
28
|
+
*/
|
|
29
|
+
export declare class AnthropicAdapter extends BaseAdapter {
|
|
30
|
+
readonly name = "anthropic";
|
|
31
|
+
readonly displayName = "Anthropic";
|
|
32
|
+
/**
|
|
33
|
+
* 获取 Anthropic 配置选项
|
|
34
|
+
*/
|
|
35
|
+
private getOptions;
|
|
36
|
+
/**
|
|
37
|
+
* 获取推理内容展示模式
|
|
38
|
+
*/
|
|
39
|
+
private getReasoningDisplayMode;
|
|
40
|
+
/**
|
|
41
|
+
* 规范化 Anthropic API URL
|
|
42
|
+
*
|
|
43
|
+
* 用户可能配置为 base URL(如 https://api.minimaxi.com/anthropic)
|
|
44
|
+
* 或完整路径(如 https://api.anthropic.com/v1/messages)。
|
|
45
|
+
* 如果 URL 不以 /v1/messages 结尾,则自动拼接。
|
|
46
|
+
*/
|
|
47
|
+
private normalizeUrl;
|
|
48
|
+
/**
|
|
49
|
+
* 构建 Anthropic 请求头
|
|
50
|
+
*
|
|
51
|
+
* 认证方式:
|
|
52
|
+
* - useXApiKey=true(默认):使用 x-api-key 头(Anthropic 原生)
|
|
53
|
+
* - useXApiKey=false:使用 Authorization: Bearer(第三方兼容平台)
|
|
54
|
+
*/
|
|
55
|
+
protected buildHeaders(endpoint: AgentEndpoint): Record<string, string>;
|
|
56
|
+
/**
|
|
57
|
+
* 转发请求到 Anthropic Messages API
|
|
58
|
+
*/
|
|
59
|
+
forward(request: AdapterRequest, endpoint: AgentEndpoint, callbacks: AdapterCallbacks): Promise<string>;
|
|
60
|
+
/**
|
|
61
|
+
* 构建 Anthropic messages 数组
|
|
62
|
+
*
|
|
63
|
+
* 与 OpenAI 的差异:
|
|
64
|
+
* - 系统提示词不在 messages 中,而是顶层 system 字段(在 buildRequestBody 中处理)
|
|
65
|
+
* - 图片使用 { type: "image", source: { type: "base64", ... } } 格式
|
|
66
|
+
* - 不支持 role: "system",只有 "user" 和 "assistant"
|
|
67
|
+
*/
|
|
68
|
+
private buildMessages;
|
|
69
|
+
/**
|
|
70
|
+
* 构建 Anthropic 请求体
|
|
71
|
+
*/
|
|
72
|
+
private buildRequestBody;
|
|
73
|
+
/**
|
|
74
|
+
* 处理 Anthropic 流式响应
|
|
75
|
+
*
|
|
76
|
+
* Anthropic SSE 事件流程:
|
|
77
|
+
* message_start → content_block_start → content_block_delta(多次) →
|
|
78
|
+
* content_block_stop → ... → message_delta → message_stop
|
|
79
|
+
*/
|
|
80
|
+
private handleStreamResponse;
|
|
81
|
+
/**
|
|
82
|
+
* 处理 Anthropic 非流式响应
|
|
83
|
+
*
|
|
84
|
+
* 遍历 content[] 数组,按 type 提取 text 和 thinking 内容
|
|
85
|
+
*/
|
|
86
|
+
private handleNonStreamResponse;
|
|
87
|
+
/**
|
|
88
|
+
* 格式化最终输出(根据 displayMode 配置)
|
|
89
|
+
*
|
|
90
|
+
* 复用 DeepSeek 的 show/hide/collapse 策略
|
|
91
|
+
*/
|
|
92
|
+
private formatOutput;
|
|
93
|
+
}
|