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