@nextclaw/channel-runtime 0.1.1

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/dist/index.js ADDED
@@ -0,0 +1,2658 @@
1
+ // src/channels/base.ts
2
+ var BaseChannel = class {
3
+ constructor(config, bus) {
4
+ this.config = config;
5
+ this.bus = bus;
6
+ }
7
+ running = false;
8
+ isAllowed(senderId) {
9
+ const allowList = this.config.allowFrom ?? [];
10
+ if (!allowList.length) {
11
+ return true;
12
+ }
13
+ if (allowList.includes(senderId)) {
14
+ return true;
15
+ }
16
+ if (senderId.includes("|")) {
17
+ return senderId.split("|").some((part) => allowList.includes(part));
18
+ }
19
+ return false;
20
+ }
21
+ async handleMessage(params) {
22
+ if (!this.isAllowed(params.senderId)) {
23
+ return;
24
+ }
25
+ const msg = {
26
+ channel: this.name,
27
+ senderId: params.senderId,
28
+ chatId: params.chatId,
29
+ content: params.content,
30
+ timestamp: /* @__PURE__ */ new Date(),
31
+ attachments: params.attachments ?? [],
32
+ metadata: params.metadata ?? {}
33
+ };
34
+ await this.bus.publishInbound(msg);
35
+ }
36
+ get isRunning() {
37
+ return this.running;
38
+ }
39
+ };
40
+
41
+ // src/config/brand.ts
42
+ import {
43
+ APP_NAME,
44
+ APP_TITLE,
45
+ APP_REPLY_SUBJECT
46
+ } from "@nextclaw/core";
47
+
48
+ // src/channels/dingtalk.ts
49
+ import { DWClient, EventAck, TOPIC_ROBOT } from "dingtalk-stream";
50
+ import { fetch } from "undici";
51
+ var DingTalkChannel = class extends BaseChannel {
52
+ name = "dingtalk";
53
+ client = null;
54
+ accessToken = null;
55
+ tokenExpiry = 0;
56
+ constructor(config, bus) {
57
+ super(config, bus);
58
+ }
59
+ async start() {
60
+ this.running = true;
61
+ if (!this.config.clientId || !this.config.clientSecret) {
62
+ throw new Error("DingTalk clientId/clientSecret not configured");
63
+ }
64
+ this.client = new DWClient({
65
+ clientId: this.config.clientId,
66
+ clientSecret: this.config.clientSecret,
67
+ debug: false
68
+ });
69
+ this.client.registerCallbackListener(TOPIC_ROBOT, async (res) => {
70
+ await this.handleRobotMessage(res);
71
+ });
72
+ this.client.registerAllEventListener(() => ({ status: EventAck.SUCCESS }));
73
+ await this.client.connect();
74
+ }
75
+ async stop() {
76
+ this.running = false;
77
+ if (this.client) {
78
+ this.client.disconnect();
79
+ this.client = null;
80
+ }
81
+ }
82
+ async send(msg) {
83
+ const token = await this.getAccessToken();
84
+ if (!token) {
85
+ return;
86
+ }
87
+ const url = "https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend";
88
+ const payload = {
89
+ robotCode: this.config.clientId,
90
+ userIds: [msg.chatId],
91
+ msgKey: "sampleMarkdown",
92
+ msgParam: JSON.stringify({
93
+ text: msg.content,
94
+ title: `${APP_TITLE} Reply`
95
+ })
96
+ };
97
+ const response = await fetch(url, {
98
+ method: "POST",
99
+ headers: {
100
+ "content-type": "application/json",
101
+ "x-acs-dingtalk-access-token": token
102
+ },
103
+ body: JSON.stringify(payload)
104
+ });
105
+ if (!response.ok) {
106
+ throw new Error(`DingTalk send failed: ${response.status}`);
107
+ }
108
+ }
109
+ async handleRobotMessage(res) {
110
+ if (!res?.data) {
111
+ return;
112
+ }
113
+ let parsed;
114
+ try {
115
+ parsed = JSON.parse(res.data);
116
+ } catch {
117
+ return;
118
+ }
119
+ const text = parsed.text?.content?.trim() ?? "";
120
+ if (!text) {
121
+ this.client?.socketCallBackResponse(res.headers.messageId, { ok: true });
122
+ return;
123
+ }
124
+ const senderId = parsed.senderStaffId || parsed.senderId || "";
125
+ const senderName = parsed.senderNick || "";
126
+ if (!senderId) {
127
+ this.client?.socketCallBackResponse(res.headers.messageId, { ok: true });
128
+ return;
129
+ }
130
+ await this.handleMessage({
131
+ senderId,
132
+ chatId: senderId,
133
+ content: text,
134
+ attachments: [],
135
+ metadata: {
136
+ sender_name: senderName,
137
+ platform: "dingtalk"
138
+ }
139
+ });
140
+ this.client?.socketCallBackResponse(res.headers.messageId, { ok: true });
141
+ }
142
+ async getAccessToken() {
143
+ if (this.accessToken && Date.now() < this.tokenExpiry) {
144
+ return this.accessToken;
145
+ }
146
+ const url = "https://api.dingtalk.com/v1.0/oauth2/accessToken";
147
+ const payload = {
148
+ appKey: this.config.clientId,
149
+ appSecret: this.config.clientSecret
150
+ };
151
+ const response = await fetch(url, {
152
+ method: "POST",
153
+ headers: { "content-type": "application/json" },
154
+ body: JSON.stringify(payload)
155
+ });
156
+ if (!response.ok) {
157
+ return null;
158
+ }
159
+ const data = await response.json();
160
+ const token = data.accessToken;
161
+ const expiresIn = Number(data.expireIn ?? 7200);
162
+ if (!token) {
163
+ return null;
164
+ }
165
+ this.accessToken = token;
166
+ this.tokenExpiry = Date.now() + (expiresIn - 60) * 1e3;
167
+ return token;
168
+ }
169
+ };
170
+
171
+ // src/channels/discord.ts
172
+ import {
173
+ Client,
174
+ GatewayIntentBits,
175
+ Partials,
176
+ MessageFlags
177
+ } from "discord.js";
178
+ import { ProxyAgent, fetch as fetch2 } from "undici";
179
+ import { join } from "path";
180
+ import { mkdirSync, writeFileSync } from "fs";
181
+
182
+ // src/utils/helpers.ts
183
+ import { getDataPath } from "@nextclaw/core";
184
+
185
+ // src/channels/discord.ts
186
+ var DEFAULT_MEDIA_MAX_MB = 8;
187
+ var MEDIA_FETCH_TIMEOUT_MS = 15e3;
188
+ var DiscordChannel = class extends BaseChannel {
189
+ name = "discord";
190
+ client = null;
191
+ typingTasks = /* @__PURE__ */ new Map();
192
+ constructor(config, bus) {
193
+ super(config, bus);
194
+ }
195
+ async start() {
196
+ if (!this.config.token) {
197
+ throw new Error("Discord token not configured");
198
+ }
199
+ this.running = true;
200
+ this.client = new Client({
201
+ intents: this.config.intents ?? GatewayIntentBits.Guilds | GatewayIntentBits.GuildMessages | GatewayIntentBits.DirectMessages,
202
+ partials: [Partials.Channel]
203
+ });
204
+ this.client.on("ready", () => {
205
+ console.log("Discord bot connected");
206
+ });
207
+ this.client.on("messageCreate", async (message) => {
208
+ await this.handleIncoming(message);
209
+ });
210
+ await this.client.login(this.config.token);
211
+ }
212
+ async stop() {
213
+ this.running = false;
214
+ for (const task of this.typingTasks.values()) {
215
+ clearInterval(task);
216
+ }
217
+ this.typingTasks.clear();
218
+ if (this.client) {
219
+ await this.client.destroy();
220
+ this.client = null;
221
+ }
222
+ }
223
+ async send(msg) {
224
+ if (!this.client) {
225
+ return;
226
+ }
227
+ const channel = await this.client.channels.fetch(msg.chatId);
228
+ if (!channel || !channel.isTextBased()) {
229
+ return;
230
+ }
231
+ this.stopTyping(msg.chatId);
232
+ const textChannel = channel;
233
+ const payload = {
234
+ content: msg.content ?? ""
235
+ };
236
+ if (msg.replyTo) {
237
+ payload.reply = { messageReference: msg.replyTo };
238
+ }
239
+ if (msg.metadata?.silent === true) {
240
+ payload.flags = MessageFlags.SuppressNotifications;
241
+ }
242
+ await textChannel.send(payload);
243
+ }
244
+ async handleIncoming(message) {
245
+ const selfUserId = this.client?.user?.id;
246
+ if (selfUserId && message.author.id === selfUserId) {
247
+ return;
248
+ }
249
+ if (message.author.bot && !this.config.allowBots) {
250
+ return;
251
+ }
252
+ const senderId = message.author.id;
253
+ const channelId = message.channelId;
254
+ if (!this.isAllowed(senderId)) {
255
+ return;
256
+ }
257
+ const contentParts = [];
258
+ const attachments = [];
259
+ const attachmentIssues = [];
260
+ if (message.content) {
261
+ contentParts.push(message.content);
262
+ }
263
+ if (message.attachments.size) {
264
+ const mediaDir = join(getDataPath(), "media");
265
+ mkdirSync(mediaDir, { recursive: true });
266
+ const maxBytes = Math.max(1, this.config.mediaMaxMb ?? DEFAULT_MEDIA_MAX_MB) * 1024 * 1024;
267
+ const proxy = this.resolveProxyAgent();
268
+ for (const attachment of message.attachments.values()) {
269
+ const resolved = await this.resolveInboundAttachment({
270
+ attachment,
271
+ mediaDir,
272
+ maxBytes,
273
+ proxy
274
+ });
275
+ if (resolved.attachment) {
276
+ attachments.push(resolved.attachment);
277
+ }
278
+ if (resolved.issue) {
279
+ attachmentIssues.push(resolved.issue);
280
+ }
281
+ }
282
+ if (!message.content && attachments.length > 0) {
283
+ contentParts.push(buildAttachmentSummary(attachments));
284
+ }
285
+ }
286
+ const replyTo = message.reference?.messageId ?? null;
287
+ this.startTyping(channelId);
288
+ await this.handleMessage({
289
+ senderId,
290
+ chatId: channelId,
291
+ content: contentParts.length ? contentParts.join("\n") : "[empty message]",
292
+ attachments,
293
+ metadata: {
294
+ message_id: message.id,
295
+ guild_id: message.guildId,
296
+ reply_to: replyTo,
297
+ ...attachmentIssues.length ? { attachment_issues: attachmentIssues } : {}
298
+ }
299
+ });
300
+ }
301
+ resolveProxyAgent() {
302
+ const proxy = this.config.proxy?.trim();
303
+ if (!proxy) {
304
+ return null;
305
+ }
306
+ try {
307
+ return new ProxyAgent(proxy);
308
+ } catch {
309
+ return null;
310
+ }
311
+ }
312
+ async resolveInboundAttachment(params) {
313
+ const { attachment, mediaDir, maxBytes, proxy } = params;
314
+ const id = attachment.id;
315
+ const name = attachment.name ?? "file";
316
+ const url = attachment.url;
317
+ const mimeType = attachment.contentType ?? guessMimeFromName(name) ?? void 0;
318
+ if (!url) {
319
+ return {
320
+ issue: {
321
+ id,
322
+ name,
323
+ code: "invalid_payload",
324
+ message: "attachment URL missing"
325
+ }
326
+ };
327
+ }
328
+ if (attachment.size && attachment.size > maxBytes) {
329
+ return {
330
+ attachment: {
331
+ id,
332
+ name,
333
+ url,
334
+ mimeType,
335
+ size: attachment.size,
336
+ source: "discord",
337
+ status: "remote-only",
338
+ errorCode: "too_large"
339
+ },
340
+ issue: {
341
+ id,
342
+ name,
343
+ url,
344
+ code: "too_large",
345
+ message: `attachment size ${attachment.size} exceeds ${maxBytes}`
346
+ }
347
+ };
348
+ }
349
+ const controller = new AbortController();
350
+ const timeoutId = setTimeout(() => controller.abort(), MEDIA_FETCH_TIMEOUT_MS);
351
+ try {
352
+ const fetchInit = {
353
+ signal: controller.signal,
354
+ ...proxy ? { dispatcher: proxy } : {}
355
+ };
356
+ const res = await fetch2(url, fetchInit);
357
+ if (!res.ok) {
358
+ return {
359
+ attachment: {
360
+ id,
361
+ name,
362
+ url,
363
+ mimeType,
364
+ size: attachment.size,
365
+ source: "discord",
366
+ status: "remote-only",
367
+ errorCode: "http_error"
368
+ },
369
+ issue: {
370
+ id,
371
+ name,
372
+ url,
373
+ code: "http_error",
374
+ message: `HTTP ${res.status}`
375
+ }
376
+ };
377
+ }
378
+ const buffer = Buffer.from(await res.arrayBuffer());
379
+ if (buffer.length > maxBytes) {
380
+ return {
381
+ attachment: {
382
+ id,
383
+ name,
384
+ url,
385
+ mimeType,
386
+ size: buffer.length,
387
+ source: "discord",
388
+ status: "remote-only",
389
+ errorCode: "too_large"
390
+ },
391
+ issue: {
392
+ id,
393
+ name,
394
+ url,
395
+ code: "too_large",
396
+ message: `downloaded payload ${buffer.length} exceeds ${maxBytes}`
397
+ }
398
+ };
399
+ }
400
+ const filename = `${id}_${sanitizeAttachmentName(name)}`;
401
+ const filePath = join(mediaDir, filename);
402
+ writeFileSync(filePath, buffer);
403
+ return {
404
+ attachment: {
405
+ id,
406
+ name,
407
+ path: filePath,
408
+ url,
409
+ mimeType,
410
+ size: buffer.length,
411
+ source: "discord",
412
+ status: "ready"
413
+ }
414
+ };
415
+ } catch (err) {
416
+ return {
417
+ attachment: {
418
+ id,
419
+ name,
420
+ url,
421
+ mimeType,
422
+ size: attachment.size,
423
+ source: "discord",
424
+ status: "remote-only",
425
+ errorCode: "download_failed"
426
+ },
427
+ issue: {
428
+ id,
429
+ name,
430
+ url,
431
+ code: "download_failed",
432
+ message: String(err)
433
+ }
434
+ };
435
+ } finally {
436
+ clearTimeout(timeoutId);
437
+ }
438
+ }
439
+ startTyping(channelId) {
440
+ this.stopTyping(channelId);
441
+ if (!this.client) {
442
+ return;
443
+ }
444
+ const channel = this.client.channels.cache.get(channelId);
445
+ if (!channel || !channel.isTextBased()) {
446
+ return;
447
+ }
448
+ const textChannel = channel;
449
+ const task = setInterval(() => {
450
+ void textChannel.sendTyping();
451
+ }, 8e3);
452
+ this.typingTasks.set(channelId, task);
453
+ }
454
+ stopTyping(channelId) {
455
+ const task = this.typingTasks.get(channelId);
456
+ if (task) {
457
+ clearInterval(task);
458
+ this.typingTasks.delete(channelId);
459
+ }
460
+ }
461
+ };
462
+ function sanitizeAttachmentName(name) {
463
+ return name.replace(/[\\/:*?"<>|]/g, "_");
464
+ }
465
+ function guessMimeFromName(name) {
466
+ const lower = name.toLowerCase();
467
+ if (lower.endsWith(".png")) return "image/png";
468
+ if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
469
+ if (lower.endsWith(".gif")) return "image/gif";
470
+ if (lower.endsWith(".webp")) return "image/webp";
471
+ if (lower.endsWith(".bmp")) return "image/bmp";
472
+ if (lower.endsWith(".tif") || lower.endsWith(".tiff")) return "image/tiff";
473
+ return null;
474
+ }
475
+ function isImageAttachment(attachment) {
476
+ if (attachment.mimeType?.startsWith("image/")) {
477
+ return true;
478
+ }
479
+ return Boolean(attachment.name && guessMimeFromName(attachment.name));
480
+ }
481
+ function buildAttachmentSummary(attachments) {
482
+ const count = attachments.length;
483
+ if (count === 0) {
484
+ return "";
485
+ }
486
+ const allImages = attachments.every((entry) => isImageAttachment(entry));
487
+ if (allImages) {
488
+ return `<media:image> (${count} ${count === 1 ? "image" : "images"})`;
489
+ }
490
+ return `<media:document> (${count} ${count === 1 ? "file" : "files"})`;
491
+ }
492
+
493
+ // src/channels/email.ts
494
+ import { ImapFlow } from "imapflow";
495
+ import { simpleParser } from "mailparser";
496
+ import nodemailer from "nodemailer";
497
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
498
+ var EmailChannel = class extends BaseChannel {
499
+ name = "email";
500
+ lastSubjectByChat = /* @__PURE__ */ new Map();
501
+ lastMessageIdByChat = /* @__PURE__ */ new Map();
502
+ processedUids = /* @__PURE__ */ new Set();
503
+ maxProcessedUids = 1e5;
504
+ constructor(config, bus) {
505
+ super(config, bus);
506
+ }
507
+ async start() {
508
+ if (!this.config.consentGranted) {
509
+ return;
510
+ }
511
+ if (!this.validateConfig()) {
512
+ return;
513
+ }
514
+ this.running = true;
515
+ const pollSeconds = Math.max(5, Number(this.config.pollIntervalSeconds ?? 30));
516
+ while (this.running) {
517
+ try {
518
+ const items = await this.fetchNewMessages();
519
+ for (const item of items) {
520
+ if (item.subject) {
521
+ this.lastSubjectByChat.set(item.sender, item.subject);
522
+ }
523
+ if (item.messageId) {
524
+ this.lastMessageIdByChat.set(item.sender, item.messageId);
525
+ }
526
+ await this.handleMessage({
527
+ senderId: item.sender,
528
+ chatId: item.sender,
529
+ content: item.content,
530
+ attachments: [],
531
+ metadata: item.metadata ?? {}
532
+ });
533
+ }
534
+ } catch {
535
+ }
536
+ await sleep(pollSeconds * 1e3);
537
+ }
538
+ }
539
+ async stop() {
540
+ this.running = false;
541
+ }
542
+ async send(msg) {
543
+ if (!this.config.consentGranted) {
544
+ return;
545
+ }
546
+ const forceSend = Boolean((msg.metadata ?? {}).force_send);
547
+ if (!this.config.autoReplyEnabled && !forceSend) {
548
+ return;
549
+ }
550
+ if (!this.config.smtpHost) {
551
+ return;
552
+ }
553
+ const toAddr = msg.chatId.trim();
554
+ if (!toAddr) {
555
+ return;
556
+ }
557
+ const baseSubject = this.lastSubjectByChat.get(toAddr) ?? APP_REPLY_SUBJECT;
558
+ const subject = msg.metadata?.subject?.trim() || this.replySubject(baseSubject);
559
+ const transporter = nodemailer.createTransport({
560
+ host: this.config.smtpHost,
561
+ port: this.config.smtpPort,
562
+ secure: this.config.smtpUseSsl,
563
+ auth: {
564
+ user: this.config.smtpUsername,
565
+ pass: this.config.smtpPassword
566
+ },
567
+ tls: this.config.smtpUseTls ? { rejectUnauthorized: false } : void 0
568
+ });
569
+ await transporter.sendMail({
570
+ from: this.config.fromAddress || this.config.smtpUsername || this.config.imapUsername,
571
+ to: toAddr,
572
+ subject,
573
+ text: msg.content ?? "",
574
+ inReplyTo: this.lastMessageIdByChat.get(toAddr) ?? void 0,
575
+ references: this.lastMessageIdByChat.get(toAddr) ?? void 0
576
+ });
577
+ }
578
+ validateConfig() {
579
+ const missing = [];
580
+ if (!this.config.imapHost) missing.push("imapHost");
581
+ if (!this.config.imapUsername) missing.push("imapUsername");
582
+ if (!this.config.imapPassword) missing.push("imapPassword");
583
+ if (!this.config.smtpHost) missing.push("smtpHost");
584
+ if (!this.config.smtpUsername) missing.push("smtpUsername");
585
+ if (!this.config.smtpPassword) missing.push("smtpPassword");
586
+ return missing.length === 0;
587
+ }
588
+ replySubject(subject) {
589
+ const prefix = this.config.subjectPrefix || "Re: ";
590
+ return subject.startsWith(prefix) ? subject : `${prefix}${subject}`;
591
+ }
592
+ async fetchNewMessages() {
593
+ const client = new ImapFlow({
594
+ host: this.config.imapHost,
595
+ port: this.config.imapPort,
596
+ secure: this.config.imapUseSsl,
597
+ auth: {
598
+ user: this.config.imapUsername,
599
+ pass: this.config.imapPassword
600
+ }
601
+ });
602
+ await client.connect();
603
+ const lock = await client.getMailboxLock(this.config.imapMailbox || "INBOX");
604
+ const items = [];
605
+ try {
606
+ const uids = await client.search({ seen: false });
607
+ if (!Array.isArray(uids)) {
608
+ return items;
609
+ }
610
+ for (const uid of uids) {
611
+ const key = String(uid);
612
+ if (this.processedUids.has(key)) {
613
+ continue;
614
+ }
615
+ const message = await client.fetchOne(uid, { uid: true, source: true, envelope: true });
616
+ if (!message || !message.source) {
617
+ continue;
618
+ }
619
+ const parsed = await simpleParser(message.source);
620
+ const sender = parsed.from?.value?.[0]?.address ?? "";
621
+ if (!sender) {
622
+ continue;
623
+ }
624
+ if (!this.isAllowed(sender)) {
625
+ continue;
626
+ }
627
+ const rawContent = parsed.text ?? parsed.html ?? "";
628
+ const content = typeof rawContent === "string" ? rawContent : "";
629
+ const subject = parsed.subject ?? "";
630
+ const messageId = parsed.messageId ?? "";
631
+ items.push({
632
+ sender,
633
+ subject,
634
+ content: content.slice(0, this.config.maxBodyChars),
635
+ messageId,
636
+ metadata: { subject }
637
+ });
638
+ if (this.config.markSeen) {
639
+ await client.messageFlagsAdd(uid, ["\\Seen"]);
640
+ }
641
+ this.processedUids.add(key);
642
+ if (this.processedUids.size > this.maxProcessedUids) {
643
+ const iterator = this.processedUids.values();
644
+ const oldest = iterator.next().value;
645
+ if (oldest) {
646
+ this.processedUids.delete(oldest);
647
+ }
648
+ }
649
+ }
650
+ } finally {
651
+ lock.release();
652
+ await client.logout();
653
+ }
654
+ return items;
655
+ }
656
+ };
657
+
658
+ // src/channels/feishu.ts
659
+ import * as Lark from "@larksuiteoapi/node-sdk";
660
+ var MSG_TYPE_MAP = {
661
+ image: "[image]",
662
+ audio: "[audio]",
663
+ file: "[file]",
664
+ sticker: "[sticker]"
665
+ };
666
+ var TABLE_RE = /((?:^[ \t]*\|.+\|[ \t]*\n)(?:^[ \t]*\|[-:\s|]+\|[ \t]*\n)(?:^[ \t]*\|.+\|[ \t]*\n?)+)/gm;
667
+ var FeishuChannel = class extends BaseChannel {
668
+ name = "feishu";
669
+ client = null;
670
+ wsClient = null;
671
+ processedMessageIds = [];
672
+ processedSet = /* @__PURE__ */ new Set();
673
+ constructor(config, bus) {
674
+ super(config, bus);
675
+ }
676
+ async start() {
677
+ if (!this.config.appId || !this.config.appSecret) {
678
+ throw new Error("Feishu appId/appSecret not configured");
679
+ }
680
+ this.running = true;
681
+ this.client = new Lark.Client({ appId: this.config.appId, appSecret: this.config.appSecret });
682
+ const dispatcher = new Lark.EventDispatcher({
683
+ encryptKey: this.config.encryptKey || void 0,
684
+ verificationToken: this.config.verificationToken || void 0
685
+ }).register({
686
+ "im.message.receive_v1": async (data) => {
687
+ await this.handleIncoming(data);
688
+ }
689
+ });
690
+ this.wsClient = new Lark.WSClient({
691
+ appId: this.config.appId,
692
+ appSecret: this.config.appSecret,
693
+ loggerLevel: Lark.LoggerLevel.info
694
+ });
695
+ this.wsClient.start({ eventDispatcher: dispatcher });
696
+ }
697
+ async stop() {
698
+ this.running = false;
699
+ if (this.wsClient) {
700
+ this.wsClient.close();
701
+ this.wsClient = null;
702
+ }
703
+ }
704
+ async send(msg) {
705
+ if (!this.client) {
706
+ return;
707
+ }
708
+ const receiveIdType = msg.chatId.startsWith("oc_") ? "chat_id" : "open_id";
709
+ const elements = buildCardElements(msg.content ?? "");
710
+ const card = {
711
+ config: { wide_screen_mode: true },
712
+ elements
713
+ };
714
+ const content = JSON.stringify(card);
715
+ await this.client.im.message.create({
716
+ params: { receive_id_type: receiveIdType },
717
+ data: {
718
+ receive_id: msg.chatId,
719
+ msg_type: "interactive",
720
+ content
721
+ }
722
+ });
723
+ }
724
+ async handleIncoming(data) {
725
+ const message = data.message ?? {};
726
+ const sender = message.sender ?? data.sender ?? {};
727
+ const senderIdObj = sender.sender_id ?? {};
728
+ const senderId = senderIdObj.open_id || senderIdObj.user_id || senderIdObj.union_id || sender.open_id || sender.user_id || "";
729
+ const senderType = sender.sender_type ?? sender.senderType;
730
+ if (senderType === "bot") {
731
+ return;
732
+ }
733
+ const chatId = message.chat_id ?? "";
734
+ const chatType = message.chat_type ?? "";
735
+ const msgType = message.msg_type ?? message.message_type ?? "";
736
+ const messageId = message.message_id ?? "";
737
+ if (!senderId || !chatId) {
738
+ return;
739
+ }
740
+ if (!this.isAllowed(String(senderId))) {
741
+ return;
742
+ }
743
+ if (messageId && this.isDuplicate(messageId)) {
744
+ return;
745
+ }
746
+ if (messageId) {
747
+ await this.addReaction(messageId, "THUMBSUP");
748
+ }
749
+ let content = "";
750
+ if (message.content) {
751
+ try {
752
+ const parsed = JSON.parse(String(message.content));
753
+ content = String(parsed.text ?? parsed.content ?? "");
754
+ } catch {
755
+ content = String(message.content);
756
+ }
757
+ }
758
+ if (!content && MSG_TYPE_MAP[msgType]) {
759
+ content = MSG_TYPE_MAP[msgType];
760
+ }
761
+ if (!content) {
762
+ return;
763
+ }
764
+ const replyTo = chatType === "group" ? chatId : String(senderId);
765
+ await this.handleMessage({
766
+ senderId: String(senderId),
767
+ chatId: replyTo,
768
+ content,
769
+ attachments: [],
770
+ metadata: {
771
+ message_id: messageId,
772
+ chat_type: chatType,
773
+ msg_type: msgType
774
+ }
775
+ });
776
+ }
777
+ isDuplicate(messageId) {
778
+ if (this.processedSet.has(messageId)) {
779
+ return true;
780
+ }
781
+ this.processedSet.add(messageId);
782
+ this.processedMessageIds.push(messageId);
783
+ if (this.processedMessageIds.length > 1e3) {
784
+ const removed = this.processedMessageIds.splice(0, 500);
785
+ for (const id of removed) {
786
+ this.processedSet.delete(id);
787
+ }
788
+ }
789
+ return false;
790
+ }
791
+ async addReaction(messageId, emojiType) {
792
+ if (!this.client) {
793
+ return;
794
+ }
795
+ try {
796
+ await this.client.im.messageReaction.create({
797
+ path: { message_id: messageId },
798
+ data: { reaction_type: { emoji_type: emojiType } }
799
+ });
800
+ } catch {
801
+ }
802
+ }
803
+ };
804
+ function buildCardElements(content) {
805
+ const elements = [];
806
+ let lastEnd = 0;
807
+ for (const match of content.matchAll(TABLE_RE)) {
808
+ const start = match.index ?? 0;
809
+ const tableText = match[1] ?? "";
810
+ const before = content.slice(lastEnd, start).trim();
811
+ if (before) {
812
+ elements.push({ tag: "markdown", content: before });
813
+ }
814
+ elements.push(parseMdTable(tableText) ?? { tag: "markdown", content: tableText });
815
+ lastEnd = start + tableText.length;
816
+ }
817
+ const remaining = content.slice(lastEnd).trim();
818
+ if (remaining) {
819
+ elements.push({ tag: "markdown", content: remaining });
820
+ }
821
+ if (!elements.length) {
822
+ elements.push({ tag: "markdown", content });
823
+ }
824
+ return elements;
825
+ }
826
+ function parseMdTable(tableText) {
827
+ const lines = tableText.trim().split("\n").map((line) => line.trim()).filter(Boolean);
828
+ if (lines.length < 3) {
829
+ return null;
830
+ }
831
+ const split = (line) => line.replace(/^\|+|\|+$/g, "").split("|").map((item) => item.trim());
832
+ const headers = split(lines[0]);
833
+ const rows = lines.slice(2).map(split);
834
+ const columns = headers.map((header, index) => ({
835
+ tag: "column",
836
+ name: `c${index}`,
837
+ display_name: header,
838
+ width: "auto"
839
+ }));
840
+ const tableRows = rows.map((row) => {
841
+ const values = {};
842
+ headers.forEach((_, index) => {
843
+ values[`c${index}`] = row[index] ?? "";
844
+ });
845
+ return values;
846
+ });
847
+ return {
848
+ tag: "table",
849
+ page_size: rows.length + 1,
850
+ columns,
851
+ rows: tableRows
852
+ };
853
+ }
854
+
855
+ // src/channels/mochat.ts
856
+ import { io } from "socket.io-client";
857
+ import { fetch as fetch3 } from "undici";
858
+ import { join as join2 } from "path";
859
+ import { mkdirSync as mkdirSync2, existsSync, readFileSync, writeFileSync as writeFileSync2 } from "fs";
860
+ var MAX_SEEN_MESSAGE_IDS = 2e3;
861
+ var CURSOR_SAVE_DEBOUNCE_MS = 500;
862
+ var AsyncLock = class {
863
+ queue = Promise.resolve();
864
+ async run(task) {
865
+ const run = this.queue.then(task, task);
866
+ this.queue = run.then(
867
+ () => void 0,
868
+ () => void 0
869
+ );
870
+ return run;
871
+ }
872
+ };
873
+ var MochatChannel = class extends BaseChannel {
874
+ name = "mochat";
875
+ socket = null;
876
+ wsConnected = false;
877
+ wsReady = false;
878
+ stateDir = join2(getDataPath(), "mochat");
879
+ cursorPath = join2(this.stateDir, "session_cursors.json");
880
+ sessionCursor = {};
881
+ cursorSaveTimer = null;
882
+ sessionSet = /* @__PURE__ */ new Set();
883
+ panelSet = /* @__PURE__ */ new Set();
884
+ autoDiscoverSessions = false;
885
+ autoDiscoverPanels = false;
886
+ coldSessions = /* @__PURE__ */ new Set();
887
+ sessionByConverse = /* @__PURE__ */ new Map();
888
+ seenSet = /* @__PURE__ */ new Map();
889
+ seenQueue = /* @__PURE__ */ new Map();
890
+ delayStates = /* @__PURE__ */ new Map();
891
+ fallbackMode = false;
892
+ sessionFallbackTasks = /* @__PURE__ */ new Map();
893
+ panelFallbackTasks = /* @__PURE__ */ new Map();
894
+ refreshTimer = null;
895
+ targetLocks = /* @__PURE__ */ new Map();
896
+ refreshInFlight = false;
897
+ constructor(config, bus) {
898
+ super(config, bus);
899
+ }
900
+ async start() {
901
+ this.running = true;
902
+ if (!this.config.clawToken) {
903
+ throw new Error("Mochat clawToken not configured");
904
+ }
905
+ mkdirSync2(this.stateDir, { recursive: true });
906
+ await this.loadSessionCursors();
907
+ this.seedTargetsFromConfig();
908
+ await this.refreshTargets(false);
909
+ const socketReady = await this.startSocketClient();
910
+ if (!socketReady) {
911
+ await this.ensureFallbackWorkers();
912
+ }
913
+ const intervalMs = Math.max(1e3, this.config.refreshIntervalMs);
914
+ this.refreshTimer = setInterval(() => {
915
+ void this.refreshLoopTick();
916
+ }, intervalMs);
917
+ }
918
+ async stop() {
919
+ this.running = false;
920
+ if (this.refreshTimer) {
921
+ clearInterval(this.refreshTimer);
922
+ this.refreshTimer = null;
923
+ }
924
+ await this.stopFallbackWorkers();
925
+ await this.cancelDelayTimers();
926
+ if (this.socket) {
927
+ this.socket.disconnect();
928
+ this.socket = null;
929
+ }
930
+ if (this.cursorSaveTimer) {
931
+ clearTimeout(this.cursorSaveTimer);
932
+ this.cursorSaveTimer = null;
933
+ }
934
+ await this.saveSessionCursors();
935
+ this.wsConnected = false;
936
+ this.wsReady = false;
937
+ }
938
+ async send(msg) {
939
+ if (!this.config.clawToken) {
940
+ return;
941
+ }
942
+ const parts = [];
943
+ if (msg.content && msg.content.trim()) {
944
+ parts.push(msg.content.trim());
945
+ }
946
+ if (msg.media?.length) {
947
+ for (const item of msg.media) {
948
+ if (typeof item === "string" && item.trim()) {
949
+ parts.push(item.trim());
950
+ }
951
+ }
952
+ }
953
+ const content = parts.join("\n").trim();
954
+ if (!content) {
955
+ return;
956
+ }
957
+ const target = resolveMochatTarget(msg.chatId);
958
+ if (!target.id) {
959
+ return;
960
+ }
961
+ const isPanel = (target.isPanel || this.panelSet.has(target.id)) && !target.id.startsWith("session_");
962
+ if (isPanel) {
963
+ await this.apiSend(
964
+ "/api/claw/groups/panels/send",
965
+ "panelId",
966
+ target.id,
967
+ content,
968
+ msg.replyTo,
969
+ readGroupId(msg.metadata ?? {})
970
+ );
971
+ return;
972
+ }
973
+ await this.apiSend("/api/claw/sessions/send", "sessionId", target.id, content, msg.replyTo);
974
+ }
975
+ seedTargetsFromConfig() {
976
+ const [sessions, autoSessions] = normalizeIdList(this.config.sessions);
977
+ const [panels, autoPanels] = normalizeIdList(this.config.panels);
978
+ this.autoDiscoverSessions = autoSessions;
979
+ this.autoDiscoverPanels = autoPanels;
980
+ sessions.forEach((sid) => {
981
+ this.sessionSet.add(sid);
982
+ if (!(sid in this.sessionCursor)) {
983
+ this.coldSessions.add(sid);
984
+ }
985
+ });
986
+ panels.forEach((pid) => {
987
+ this.panelSet.add(pid);
988
+ });
989
+ }
990
+ async startSocketClient() {
991
+ let parser = void 0;
992
+ if (!this.config.socketDisableMsgpack) {
993
+ try {
994
+ const mod = await import("socket.io-msgpack-parser");
995
+ parser = mod.default ?? mod;
996
+ } catch {
997
+ parser = void 0;
998
+ }
999
+ }
1000
+ const socketUrl = (this.config.socketUrl || this.config.baseUrl).trim().replace(/\/$/, "");
1001
+ const socketPath = (this.config.socketPath || "/socket.io").trim();
1002
+ const reconnectionDelay = Math.max(100, this.config.socketReconnectDelayMs);
1003
+ const reconnectionDelayMax = Math.max(100, this.config.socketMaxReconnectDelayMs);
1004
+ const timeout = Math.max(1e3, this.config.socketConnectTimeoutMs);
1005
+ const reconnectionAttempts = this.config.maxRetryAttempts > 0 ? this.config.maxRetryAttempts : Number.MAX_SAFE_INTEGER;
1006
+ const socket = io(socketUrl, {
1007
+ path: socketPath.startsWith("/") ? socketPath : `/${socketPath}`,
1008
+ transports: ["websocket"],
1009
+ auth: { token: this.config.clawToken },
1010
+ reconnection: true,
1011
+ reconnectionAttempts,
1012
+ reconnectionDelay,
1013
+ reconnectionDelayMax,
1014
+ timeout,
1015
+ parser
1016
+ });
1017
+ socket.on("connect", async () => {
1018
+ this.wsConnected = true;
1019
+ this.wsReady = false;
1020
+ const subscribed = await this.subscribeAll();
1021
+ this.wsReady = subscribed;
1022
+ if (subscribed) {
1023
+ await this.stopFallbackWorkers();
1024
+ } else {
1025
+ await this.ensureFallbackWorkers();
1026
+ }
1027
+ });
1028
+ socket.on("disconnect", async () => {
1029
+ if (!this.running) {
1030
+ return;
1031
+ }
1032
+ this.wsConnected = false;
1033
+ this.wsReady = false;
1034
+ await this.ensureFallbackWorkers();
1035
+ });
1036
+ socket.on("connect_error", () => {
1037
+ this.wsConnected = false;
1038
+ this.wsReady = false;
1039
+ });
1040
+ socket.on("claw.session.events", async (payload) => {
1041
+ await this.handleWatchPayload(payload, "session");
1042
+ });
1043
+ socket.on("claw.panel.events", async (payload) => {
1044
+ await this.handleWatchPayload(payload, "panel");
1045
+ });
1046
+ const notifyHandler = (eventName) => async (payload) => {
1047
+ if (eventName === "notify:chat.inbox.append") {
1048
+ await this.handleNotifyInboxAppend(payload);
1049
+ return;
1050
+ }
1051
+ if (eventName.startsWith("notify:chat.message.")) {
1052
+ await this.handleNotifyChatMessage(payload);
1053
+ }
1054
+ };
1055
+ [
1056
+ "notify:chat.inbox.append",
1057
+ "notify:chat.message.add",
1058
+ "notify:chat.message.update",
1059
+ "notify:chat.message.recall",
1060
+ "notify:chat.message.delete"
1061
+ ].forEach((eventName) => {
1062
+ socket.on(eventName, notifyHandler(eventName));
1063
+ });
1064
+ this.socket = socket;
1065
+ return new Promise((resolve) => {
1066
+ const timer = setTimeout(() => resolve(false), timeout);
1067
+ socket.once("connect", () => {
1068
+ clearTimeout(timer);
1069
+ resolve(true);
1070
+ });
1071
+ socket.once("connect_error", () => {
1072
+ clearTimeout(timer);
1073
+ resolve(false);
1074
+ });
1075
+ });
1076
+ }
1077
+ async subscribeAll() {
1078
+ const sessions = Array.from(this.sessionSet).sort();
1079
+ const panels = Array.from(this.panelSet).sort();
1080
+ let ok = await this.subscribeSessions(sessions);
1081
+ ok = await this.subscribePanels(panels) && ok;
1082
+ if (this.autoDiscoverSessions || this.autoDiscoverPanels) {
1083
+ await this.refreshTargets(true);
1084
+ }
1085
+ return ok;
1086
+ }
1087
+ async subscribeSessions(sessionIds) {
1088
+ if (!sessionIds.length) {
1089
+ return true;
1090
+ }
1091
+ for (const sid of sessionIds) {
1092
+ if (!(sid in this.sessionCursor)) {
1093
+ this.coldSessions.add(sid);
1094
+ }
1095
+ }
1096
+ const ack = await this.socketCall("com.claw.im.subscribeSessions", {
1097
+ sessionIds,
1098
+ cursors: this.sessionCursor,
1099
+ limit: this.config.watchLimit
1100
+ });
1101
+ if (!ack.result) {
1102
+ return false;
1103
+ }
1104
+ const data = ack.data;
1105
+ let items = [];
1106
+ if (Array.isArray(data)) {
1107
+ items = data.filter((item) => typeof item === "object" && item !== null);
1108
+ } else if (data && typeof data === "object") {
1109
+ const sessions = data.sessions;
1110
+ if (Array.isArray(sessions)) {
1111
+ items = sessions.filter((item) => typeof item === "object" && item !== null);
1112
+ } else if (data.sessionId) {
1113
+ items = [data];
1114
+ }
1115
+ }
1116
+ for (const payload of items) {
1117
+ await this.handleWatchPayload(payload, "session");
1118
+ }
1119
+ return true;
1120
+ }
1121
+ async subscribePanels(panelIds) {
1122
+ if (!this.autoDiscoverPanels && !panelIds.length) {
1123
+ return true;
1124
+ }
1125
+ const ack = await this.socketCall("com.claw.im.subscribePanels", { panelIds });
1126
+ if (!ack.result) {
1127
+ return false;
1128
+ }
1129
+ return true;
1130
+ }
1131
+ async socketCall(eventName, payload) {
1132
+ if (!this.socket) {
1133
+ return { result: false, message: "socket not connected" };
1134
+ }
1135
+ return new Promise((resolve) => {
1136
+ this.socket?.timeout(1e4).emit(eventName, payload, (err, response) => {
1137
+ if (err) {
1138
+ resolve({ result: false, message: String(err) });
1139
+ return;
1140
+ }
1141
+ if (response && typeof response === "object") {
1142
+ resolve(response);
1143
+ return;
1144
+ }
1145
+ resolve({ result: true, data: response });
1146
+ });
1147
+ });
1148
+ }
1149
+ async refreshLoopTick() {
1150
+ if (!this.running || this.refreshInFlight) {
1151
+ return;
1152
+ }
1153
+ this.refreshInFlight = true;
1154
+ try {
1155
+ await this.refreshTargets(this.wsReady);
1156
+ if (this.fallbackMode) {
1157
+ await this.ensureFallbackWorkers();
1158
+ }
1159
+ } finally {
1160
+ this.refreshInFlight = false;
1161
+ }
1162
+ }
1163
+ async refreshTargets(subscribeNew) {
1164
+ if (this.autoDiscoverSessions) {
1165
+ await this.refreshSessionsDirectory(subscribeNew);
1166
+ }
1167
+ if (this.autoDiscoverPanels) {
1168
+ await this.refreshPanels(subscribeNew);
1169
+ }
1170
+ }
1171
+ async refreshSessionsDirectory(subscribeNew) {
1172
+ let response;
1173
+ try {
1174
+ response = await this.postJson("/api/claw/sessions/list", {});
1175
+ } catch {
1176
+ return;
1177
+ }
1178
+ const sessions = response.sessions;
1179
+ if (!Array.isArray(sessions)) {
1180
+ return;
1181
+ }
1182
+ const newIds = [];
1183
+ for (const session of sessions) {
1184
+ const sid = strField(session, "sessionId");
1185
+ if (!sid) {
1186
+ continue;
1187
+ }
1188
+ if (!this.sessionSet.has(sid)) {
1189
+ this.sessionSet.add(sid);
1190
+ newIds.push(sid);
1191
+ if (!(sid in this.sessionCursor)) {
1192
+ this.coldSessions.add(sid);
1193
+ }
1194
+ }
1195
+ const converseId = strField(session, "converseId");
1196
+ if (converseId) {
1197
+ this.sessionByConverse.set(converseId, sid);
1198
+ }
1199
+ }
1200
+ if (!newIds.length) {
1201
+ return;
1202
+ }
1203
+ if (this.wsReady && subscribeNew) {
1204
+ await this.subscribeSessions(newIds);
1205
+ }
1206
+ if (this.fallbackMode) {
1207
+ await this.ensureFallbackWorkers();
1208
+ }
1209
+ }
1210
+ async refreshPanels(subscribeNew) {
1211
+ let response;
1212
+ try {
1213
+ response = await this.postJson("/api/claw/groups/get", {});
1214
+ } catch {
1215
+ return;
1216
+ }
1217
+ const panels = response.panels;
1218
+ if (!Array.isArray(panels)) {
1219
+ return;
1220
+ }
1221
+ const newIds = [];
1222
+ for (const panel of panels) {
1223
+ const panelType = panel.type;
1224
+ if (typeof panelType === "number" && panelType !== 0) {
1225
+ continue;
1226
+ }
1227
+ const pid = strField(panel, "id", "_id");
1228
+ if (pid && !this.panelSet.has(pid)) {
1229
+ this.panelSet.add(pid);
1230
+ newIds.push(pid);
1231
+ }
1232
+ }
1233
+ if (!newIds.length) {
1234
+ return;
1235
+ }
1236
+ if (this.wsReady && subscribeNew) {
1237
+ await this.subscribePanels(newIds);
1238
+ }
1239
+ if (this.fallbackMode) {
1240
+ await this.ensureFallbackWorkers();
1241
+ }
1242
+ }
1243
+ async ensureFallbackWorkers() {
1244
+ if (!this.running) {
1245
+ return;
1246
+ }
1247
+ this.fallbackMode = true;
1248
+ for (const sid of this.sessionSet) {
1249
+ if (this.sessionFallbackTasks.has(sid)) {
1250
+ continue;
1251
+ }
1252
+ const task = this.sessionWatchWorker(sid).finally(() => {
1253
+ if (this.sessionFallbackTasks.get(sid) === task) {
1254
+ this.sessionFallbackTasks.delete(sid);
1255
+ }
1256
+ });
1257
+ this.sessionFallbackTasks.set(sid, task);
1258
+ }
1259
+ for (const pid of this.panelSet) {
1260
+ if (this.panelFallbackTasks.has(pid)) {
1261
+ continue;
1262
+ }
1263
+ const task = this.panelPollWorker(pid).finally(() => {
1264
+ if (this.panelFallbackTasks.get(pid) === task) {
1265
+ this.panelFallbackTasks.delete(pid);
1266
+ }
1267
+ });
1268
+ this.panelFallbackTasks.set(pid, task);
1269
+ }
1270
+ }
1271
+ async stopFallbackWorkers() {
1272
+ this.fallbackMode = false;
1273
+ const tasks = [...this.sessionFallbackTasks.values(), ...this.panelFallbackTasks.values()];
1274
+ this.sessionFallbackTasks.clear();
1275
+ this.panelFallbackTasks.clear();
1276
+ await Promise.allSettled(tasks);
1277
+ }
1278
+ async sessionWatchWorker(sessionId) {
1279
+ while (this.running && this.fallbackMode) {
1280
+ try {
1281
+ const payload = await this.postJson("/api/claw/sessions/watch", {
1282
+ sessionId,
1283
+ cursor: this.sessionCursor[sessionId] ?? 0,
1284
+ timeoutMs: this.config.watchTimeoutMs,
1285
+ limit: this.config.watchLimit
1286
+ });
1287
+ await this.handleWatchPayload(payload, "session");
1288
+ } catch {
1289
+ await sleep2(Math.max(100, this.config.retryDelayMs));
1290
+ }
1291
+ }
1292
+ }
1293
+ async panelPollWorker(panelId) {
1294
+ const sleepMs = Math.max(1e3, this.config.refreshIntervalMs);
1295
+ while (this.running && this.fallbackMode) {
1296
+ try {
1297
+ const payload = await this.postJson("/api/claw/groups/panels/messages", {
1298
+ panelId,
1299
+ limit: Math.min(100, Math.max(1, this.config.watchLimit))
1300
+ });
1301
+ const messages = payload.messages;
1302
+ if (Array.isArray(messages)) {
1303
+ for (const msg of [...messages].reverse()) {
1304
+ const event = makeSyntheticEvent({
1305
+ messageId: String(msg.messageId ?? ""),
1306
+ author: String(msg.author ?? ""),
1307
+ content: msg.content,
1308
+ meta: msg.meta,
1309
+ groupId: String(payload.groupId ?? ""),
1310
+ converseId: panelId,
1311
+ timestamp: msg.createdAt,
1312
+ authorInfo: msg.authorInfo
1313
+ });
1314
+ await this.processInboundEvent(panelId, event, "panel");
1315
+ }
1316
+ }
1317
+ } catch {
1318
+ await sleep2(sleepMs);
1319
+ }
1320
+ await sleep2(sleepMs);
1321
+ }
1322
+ }
1323
+ async handleWatchPayload(payload, targetKind) {
1324
+ if (!payload || typeof payload !== "object") {
1325
+ return;
1326
+ }
1327
+ const targetId = strField(payload, "sessionId");
1328
+ if (!targetId) {
1329
+ return;
1330
+ }
1331
+ const lockKey = `${targetKind}:${targetId}`;
1332
+ const lock = this.targetLocks.get(lockKey) ?? new AsyncLock();
1333
+ this.targetLocks.set(lockKey, lock);
1334
+ await lock.run(async () => {
1335
+ const previousCursor = this.sessionCursor[targetId] ?? 0;
1336
+ const cursor = payload.cursor;
1337
+ if (targetKind === "session" && typeof cursor === "number" && cursor >= 0) {
1338
+ this.markSessionCursor(targetId, cursor);
1339
+ }
1340
+ const rawEvents = payload.events;
1341
+ if (!Array.isArray(rawEvents)) {
1342
+ return;
1343
+ }
1344
+ if (targetKind === "session" && this.coldSessions.has(targetId)) {
1345
+ this.coldSessions.delete(targetId);
1346
+ return;
1347
+ }
1348
+ for (const event of rawEvents) {
1349
+ const seq = event.seq;
1350
+ if (targetKind === "session" && typeof seq === "number" && seq > (this.sessionCursor[targetId] ?? previousCursor)) {
1351
+ this.markSessionCursor(targetId, seq);
1352
+ }
1353
+ if (event.type === "message.add") {
1354
+ await this.processInboundEvent(targetId, event, targetKind);
1355
+ }
1356
+ }
1357
+ });
1358
+ }
1359
+ async processInboundEvent(targetId, event, targetKind) {
1360
+ const payload = event.payload;
1361
+ if (!payload) {
1362
+ return;
1363
+ }
1364
+ const author = strField(payload, "author");
1365
+ if (!author || this.config.agentUserId && author === this.config.agentUserId) {
1366
+ return;
1367
+ }
1368
+ if (!this.isAllowed(author)) {
1369
+ return;
1370
+ }
1371
+ const messageId = strField(payload, "messageId");
1372
+ const seenKey = `${targetKind}:${targetId}`;
1373
+ if (messageId && this.rememberMessageId(seenKey, messageId)) {
1374
+ return;
1375
+ }
1376
+ const rawBody = normalizeMochatContent(payload.content) || "[empty message]";
1377
+ const authorInfo = safeDict(payload.authorInfo);
1378
+ const senderName = strField(authorInfo, "nickname", "email");
1379
+ const senderUsername = strField(authorInfo, "agentId");
1380
+ const groupId = strField(payload, "groupId");
1381
+ const isGroup = Boolean(groupId);
1382
+ const wasMentioned = resolveWasMentioned(payload, this.config.agentUserId);
1383
+ const requireMention = targetKind === "panel" && isGroup && resolveRequireMention(this.config, targetId, groupId);
1384
+ const useDelay = targetKind === "panel" && this.config.replyDelayMode === "non-mention";
1385
+ if (requireMention && !wasMentioned && !useDelay) {
1386
+ return;
1387
+ }
1388
+ const entry = {
1389
+ rawBody,
1390
+ author,
1391
+ senderName,
1392
+ senderUsername,
1393
+ timestamp: parseTimestamp(event.timestamp),
1394
+ messageId,
1395
+ groupId
1396
+ };
1397
+ if (useDelay) {
1398
+ const delayKey = seenKey;
1399
+ if (wasMentioned) {
1400
+ await this.flushDelayedEntries(delayKey, targetId, targetKind, true, entry);
1401
+ } else {
1402
+ await this.enqueueDelayedEntry(delayKey, targetId, targetKind, entry);
1403
+ }
1404
+ return;
1405
+ }
1406
+ await this.dispatchEntries(targetId, targetKind, [entry], wasMentioned);
1407
+ }
1408
+ rememberMessageId(key, messageId) {
1409
+ const seenSet = this.seenSet.get(key) ?? /* @__PURE__ */ new Set();
1410
+ const seenQueue = this.seenQueue.get(key) ?? [];
1411
+ if (seenSet.has(messageId)) {
1412
+ return true;
1413
+ }
1414
+ seenSet.add(messageId);
1415
+ seenQueue.push(messageId);
1416
+ while (seenQueue.length > MAX_SEEN_MESSAGE_IDS) {
1417
+ const removed = seenQueue.shift();
1418
+ if (removed) {
1419
+ seenSet.delete(removed);
1420
+ }
1421
+ }
1422
+ this.seenSet.set(key, seenSet);
1423
+ this.seenQueue.set(key, seenQueue);
1424
+ return false;
1425
+ }
1426
+ async enqueueDelayedEntry(key, targetId, targetKind, entry) {
1427
+ const state = this.delayStates.get(key) ?? { entries: [], timer: null, lock: new AsyncLock() };
1428
+ this.delayStates.set(key, state);
1429
+ await state.lock.run(async () => {
1430
+ state.entries.push(entry);
1431
+ if (state.timer) {
1432
+ clearTimeout(state.timer);
1433
+ }
1434
+ state.timer = setTimeout(() => {
1435
+ void this.flushDelayedEntries(key, targetId, targetKind, false, null);
1436
+ }, Math.max(0, this.config.replyDelayMs));
1437
+ });
1438
+ }
1439
+ async flushDelayedEntries(key, targetId, targetKind, mentioned, entry) {
1440
+ const state = this.delayStates.get(key) ?? { entries: [], timer: null, lock: new AsyncLock() };
1441
+ this.delayStates.set(key, state);
1442
+ let entries = [];
1443
+ await state.lock.run(async () => {
1444
+ if (entry) {
1445
+ state.entries.push(entry);
1446
+ }
1447
+ if (state.timer) {
1448
+ clearTimeout(state.timer);
1449
+ state.timer = null;
1450
+ }
1451
+ entries = [...state.entries];
1452
+ state.entries = [];
1453
+ });
1454
+ if (entries.length) {
1455
+ await this.dispatchEntries(targetId, targetKind, entries, mentioned);
1456
+ }
1457
+ }
1458
+ async dispatchEntries(targetId, targetKind, entries, wasMentioned) {
1459
+ const last = entries[entries.length - 1];
1460
+ const isGroup = Boolean(last.groupId);
1461
+ const body = buildBufferedBody(entries, isGroup) || "[empty message]";
1462
+ await this.handleMessage({
1463
+ senderId: last.author,
1464
+ chatId: targetId,
1465
+ content: body,
1466
+ attachments: [],
1467
+ metadata: {
1468
+ message_id: last.messageId,
1469
+ timestamp: last.timestamp,
1470
+ is_group: isGroup,
1471
+ group_id: last.groupId,
1472
+ sender_name: last.senderName,
1473
+ sender_username: last.senderUsername,
1474
+ target_kind: targetKind,
1475
+ was_mentioned: wasMentioned,
1476
+ buffered_count: entries.length
1477
+ }
1478
+ });
1479
+ }
1480
+ async cancelDelayTimers() {
1481
+ for (const state of this.delayStates.values()) {
1482
+ if (state.timer) {
1483
+ clearTimeout(state.timer);
1484
+ }
1485
+ }
1486
+ this.delayStates.clear();
1487
+ }
1488
+ async handleNotifyChatMessage(payload) {
1489
+ if (!payload || typeof payload !== "object") {
1490
+ return;
1491
+ }
1492
+ const data = payload;
1493
+ const groupId = strField(data, "groupId");
1494
+ const panelId = strField(data, "converseId", "panelId");
1495
+ if (!groupId || !panelId) {
1496
+ return;
1497
+ }
1498
+ if (this.panelSet.size && !this.panelSet.has(panelId)) {
1499
+ return;
1500
+ }
1501
+ const event = makeSyntheticEvent({
1502
+ messageId: String(data._id ?? data.messageId ?? ""),
1503
+ author: String(data.author ?? ""),
1504
+ content: data.content,
1505
+ meta: data.meta,
1506
+ groupId,
1507
+ converseId: panelId,
1508
+ timestamp: data.createdAt,
1509
+ authorInfo: data.authorInfo
1510
+ });
1511
+ await this.processInboundEvent(panelId, event, "panel");
1512
+ }
1513
+ async handleNotifyInboxAppend(payload) {
1514
+ if (!payload || typeof payload !== "object") {
1515
+ return;
1516
+ }
1517
+ const data = payload;
1518
+ if (data.type !== "message") {
1519
+ return;
1520
+ }
1521
+ const detail = data.payload;
1522
+ if (!detail || typeof detail !== "object") {
1523
+ return;
1524
+ }
1525
+ if (strField(detail, "groupId")) {
1526
+ return;
1527
+ }
1528
+ const converseId = strField(detail, "converseId");
1529
+ if (!converseId) {
1530
+ return;
1531
+ }
1532
+ let sessionId = this.sessionByConverse.get(converseId);
1533
+ if (!sessionId) {
1534
+ await this.refreshSessionsDirectory(this.wsReady);
1535
+ sessionId = this.sessionByConverse.get(converseId);
1536
+ }
1537
+ if (!sessionId) {
1538
+ return;
1539
+ }
1540
+ const event = makeSyntheticEvent({
1541
+ messageId: String(detail.messageId ?? data._id ?? ""),
1542
+ author: String(detail.messageAuthor ?? ""),
1543
+ content: String(detail.messagePlainContent ?? detail.messageSnippet ?? ""),
1544
+ meta: { source: "notify:chat.inbox.append", converseId },
1545
+ groupId: "",
1546
+ converseId,
1547
+ timestamp: data.createdAt
1548
+ });
1549
+ await this.processInboundEvent(sessionId, event, "session");
1550
+ }
1551
+ markSessionCursor(sessionId, cursor) {
1552
+ if (cursor < 0) {
1553
+ return;
1554
+ }
1555
+ const current = this.sessionCursor[sessionId] ?? 0;
1556
+ if (cursor < current) {
1557
+ return;
1558
+ }
1559
+ this.sessionCursor[sessionId] = cursor;
1560
+ if (!this.cursorSaveTimer) {
1561
+ this.cursorSaveTimer = setTimeout(() => {
1562
+ this.cursorSaveTimer = null;
1563
+ void this.saveSessionCursors();
1564
+ }, CURSOR_SAVE_DEBOUNCE_MS);
1565
+ }
1566
+ }
1567
+ async loadSessionCursors() {
1568
+ if (!existsSync(this.cursorPath)) {
1569
+ return;
1570
+ }
1571
+ try {
1572
+ const raw = readFileSync(this.cursorPath, "utf-8");
1573
+ const data = JSON.parse(raw);
1574
+ const cursors = data.cursors;
1575
+ if (cursors && typeof cursors === "object") {
1576
+ for (const [sid, value] of Object.entries(cursors)) {
1577
+ if (typeof value === "number" && value >= 0) {
1578
+ this.sessionCursor[sid] = value;
1579
+ }
1580
+ }
1581
+ }
1582
+ } catch {
1583
+ return;
1584
+ }
1585
+ }
1586
+ async saveSessionCursors() {
1587
+ try {
1588
+ mkdirSync2(this.stateDir, { recursive: true });
1589
+ const payload = {
1590
+ schemaVersion: 1,
1591
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1592
+ cursors: this.sessionCursor
1593
+ };
1594
+ writeFileSync2(this.cursorPath, JSON.stringify(payload, null, 2) + "\n");
1595
+ } catch {
1596
+ return;
1597
+ }
1598
+ }
1599
+ async postJson(path, payload) {
1600
+ const url = `${this.config.baseUrl.trim().replace(/\/$/, "")}${path}`;
1601
+ const response = await fetch3(url, {
1602
+ method: "POST",
1603
+ headers: {
1604
+ "content-type": "application/json",
1605
+ "X-Claw-Token": this.config.clawToken
1606
+ },
1607
+ body: JSON.stringify(payload)
1608
+ });
1609
+ if (!response.ok) {
1610
+ throw new Error(`Mochat HTTP ${response.status}`);
1611
+ }
1612
+ let parsed;
1613
+ try {
1614
+ parsed = await response.json();
1615
+ } catch {
1616
+ parsed = await response.text();
1617
+ }
1618
+ if (parsed && typeof parsed === "object" && parsed.code !== void 0) {
1619
+ const data = parsed;
1620
+ if (typeof data.code === "number" && data.code !== 200) {
1621
+ throw new Error(String(data.message ?? data.name ?? "request failed"));
1622
+ }
1623
+ if (data.data && typeof data.data === "object") {
1624
+ return data.data;
1625
+ }
1626
+ return {};
1627
+ }
1628
+ if (parsed && typeof parsed === "object") {
1629
+ return parsed;
1630
+ }
1631
+ return {};
1632
+ }
1633
+ async apiSend(path, idKey, idValue, content, replyTo, groupId) {
1634
+ const body = { [idKey]: idValue, content };
1635
+ if (replyTo) {
1636
+ body.replyTo = replyTo;
1637
+ }
1638
+ if (groupId) {
1639
+ body.groupId = groupId;
1640
+ }
1641
+ await this.postJson(path, body);
1642
+ }
1643
+ };
1644
+ function normalizeIdList(values) {
1645
+ const cleaned = values.map((value) => String(value).trim()).filter(Boolean);
1646
+ const unique = Array.from(new Set(cleaned.filter((value) => value !== "*"))).sort();
1647
+ return [unique, cleaned.includes("*")];
1648
+ }
1649
+ function safeDict(value) {
1650
+ return value && typeof value === "object" ? value : {};
1651
+ }
1652
+ function strField(src, ...keys) {
1653
+ for (const key of keys) {
1654
+ const value = src[key];
1655
+ if (typeof value === "string" && value.trim()) {
1656
+ return value.trim();
1657
+ }
1658
+ }
1659
+ return "";
1660
+ }
1661
+ function makeSyntheticEvent(params) {
1662
+ const payload = {
1663
+ messageId: params.messageId,
1664
+ author: params.author,
1665
+ content: params.content,
1666
+ meta: safeDict(params.meta),
1667
+ groupId: params.groupId,
1668
+ converseId: params.converseId
1669
+ };
1670
+ if (params.authorInfo) {
1671
+ payload.authorInfo = safeDict(params.authorInfo);
1672
+ }
1673
+ return {
1674
+ type: "message.add",
1675
+ timestamp: params.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
1676
+ payload
1677
+ };
1678
+ }
1679
+ function normalizeMochatContent(content) {
1680
+ if (typeof content === "string") {
1681
+ return content.trim();
1682
+ }
1683
+ if (content === null || content === void 0) {
1684
+ return "";
1685
+ }
1686
+ try {
1687
+ return JSON.stringify(content);
1688
+ } catch {
1689
+ return String(content);
1690
+ }
1691
+ }
1692
+ function resolveMochatTarget(raw) {
1693
+ const trimmed = (raw || "").trim();
1694
+ if (!trimmed) {
1695
+ return { id: "", isPanel: false };
1696
+ }
1697
+ const lowered = trimmed.toLowerCase();
1698
+ let cleaned = trimmed;
1699
+ let forcedPanel = false;
1700
+ for (const prefix of ["mochat:", "group:", "channel:", "panel:"]) {
1701
+ if (lowered.startsWith(prefix)) {
1702
+ cleaned = trimmed.slice(prefix.length).trim();
1703
+ forcedPanel = prefix !== "mochat:";
1704
+ break;
1705
+ }
1706
+ }
1707
+ if (!cleaned) {
1708
+ return { id: "", isPanel: false };
1709
+ }
1710
+ return { id: cleaned, isPanel: forcedPanel || !cleaned.startsWith("session_") };
1711
+ }
1712
+ function extractMentionIds(value) {
1713
+ if (!Array.isArray(value)) {
1714
+ return [];
1715
+ }
1716
+ const ids = [];
1717
+ for (const item of value) {
1718
+ if (typeof item === "string" && item.trim()) {
1719
+ ids.push(item.trim());
1720
+ } else if (item && typeof item === "object") {
1721
+ const obj = item;
1722
+ for (const key of ["id", "userId", "_id"]) {
1723
+ const candidate = obj[key];
1724
+ if (typeof candidate === "string" && candidate.trim()) {
1725
+ ids.push(candidate.trim());
1726
+ break;
1727
+ }
1728
+ }
1729
+ }
1730
+ }
1731
+ return ids;
1732
+ }
1733
+ function resolveWasMentioned(payload, agentUserId) {
1734
+ const meta = payload.meta;
1735
+ if (meta) {
1736
+ if (meta.mentioned === true || meta.wasMentioned === true) {
1737
+ return true;
1738
+ }
1739
+ for (const field of ["mentions", "mentionIds", "mentionedUserIds", "mentionedUsers"]) {
1740
+ if (agentUserId && extractMentionIds(meta[field]).includes(agentUserId)) {
1741
+ return true;
1742
+ }
1743
+ }
1744
+ }
1745
+ if (!agentUserId) {
1746
+ return false;
1747
+ }
1748
+ const content = payload.content;
1749
+ if (typeof content !== "string" || !content) {
1750
+ return false;
1751
+ }
1752
+ return content.includes(`<@${agentUserId}>`) || content.includes(`@${agentUserId}`);
1753
+ }
1754
+ function resolveRequireMention(config, sessionId, groupId) {
1755
+ const groups = config.groups ?? {};
1756
+ for (const key of [groupId, sessionId, "*"]) {
1757
+ if (key && groups[key]) {
1758
+ return Boolean(groups[key].requireMention);
1759
+ }
1760
+ }
1761
+ return Boolean(config.mention.requireInGroups);
1762
+ }
1763
+ function buildBufferedBody(entries, isGroup) {
1764
+ if (!entries.length) {
1765
+ return "";
1766
+ }
1767
+ if (entries.length === 1) {
1768
+ return entries[0].rawBody;
1769
+ }
1770
+ const lines = [];
1771
+ for (const entry of entries) {
1772
+ if (!entry.rawBody) {
1773
+ continue;
1774
+ }
1775
+ if (isGroup) {
1776
+ const label = entry.senderName.trim() || entry.senderUsername.trim() || entry.author;
1777
+ if (label) {
1778
+ lines.push(`${label}: ${entry.rawBody}`);
1779
+ continue;
1780
+ }
1781
+ }
1782
+ lines.push(entry.rawBody);
1783
+ }
1784
+ return lines.join("\n").trim();
1785
+ }
1786
+ function parseTimestamp(value) {
1787
+ if (typeof value !== "string" || !value.trim()) {
1788
+ return null;
1789
+ }
1790
+ const parsed = Date.parse(value);
1791
+ return Number.isNaN(parsed) ? null : parsed;
1792
+ }
1793
+ function readGroupId(metadata) {
1794
+ const value = metadata.group_id ?? metadata.groupId;
1795
+ if (typeof value === "string" && value.trim()) {
1796
+ return value.trim();
1797
+ }
1798
+ return null;
1799
+ }
1800
+ function sleep2(ms) {
1801
+ return new Promise((resolve) => setTimeout(resolve, ms));
1802
+ }
1803
+
1804
+ // src/channels/qq.ts
1805
+ import {
1806
+ Bot,
1807
+ ReceiverMode,
1808
+ segment
1809
+ } from "qq-official-bot";
1810
+ var QQChannel = class extends BaseChannel {
1811
+ name = "qq";
1812
+ bot = null;
1813
+ processedIds = [];
1814
+ processedSet = /* @__PURE__ */ new Set();
1815
+ constructor(config, bus) {
1816
+ super(config, bus);
1817
+ }
1818
+ async start() {
1819
+ this.running = true;
1820
+ if (!this.config.appId || !this.config.secret) {
1821
+ throw new Error("QQ appId/appSecret not configured");
1822
+ }
1823
+ this.bot = new Bot({
1824
+ appid: this.config.appId,
1825
+ secret: this.config.secret,
1826
+ mode: ReceiverMode.WEBSOCKET,
1827
+ intents: ["C2C_MESSAGE_CREATE", "GROUP_AT_MESSAGE_CREATE"],
1828
+ removeAt: true,
1829
+ logLevel: "info"
1830
+ });
1831
+ this.bot.on("message.private", async (event) => {
1832
+ await this.handleIncoming(event);
1833
+ });
1834
+ this.bot.on("message.group", async (event) => {
1835
+ await this.handleIncoming(event);
1836
+ });
1837
+ await this.bot.start();
1838
+ console.log("QQ bot connected");
1839
+ }
1840
+ async stop() {
1841
+ this.running = false;
1842
+ if (this.bot) {
1843
+ this.bot.removeAllListeners("message.private");
1844
+ this.bot.removeAllListeners("message.group");
1845
+ await this.bot.stop();
1846
+ this.bot = null;
1847
+ }
1848
+ }
1849
+ async send(msg) {
1850
+ if (!this.bot) {
1851
+ return;
1852
+ }
1853
+ const qqMeta = msg.metadata?.qq ?? {};
1854
+ const messageType = qqMeta.messageType ?? "private";
1855
+ const metadataMessageId = msg.metadata?.message_id ?? null;
1856
+ const sourceId = msg.replyTo ?? metadataMessageId ?? void 0;
1857
+ const source = sourceId ? { id: sourceId } : void 0;
1858
+ const payload = this.config.markdownSupport ? segment.markdown(msg.content ?? "") : msg.content ?? "";
1859
+ if (messageType === "group") {
1860
+ const groupId = qqMeta.groupId ?? msg.chatId;
1861
+ await this.sendWithTokenRetry(() => this.bot?.sendGroupMessage(groupId, payload, source));
1862
+ return;
1863
+ }
1864
+ if (messageType === "direct") {
1865
+ const guildId = qqMeta.guildId ?? msg.chatId;
1866
+ await this.sendWithTokenRetry(() => this.bot?.sendDirectMessage(guildId, payload, source));
1867
+ return;
1868
+ }
1869
+ if (messageType === "guild") {
1870
+ const channelId = qqMeta.channelId ?? msg.chatId;
1871
+ await this.sendWithTokenRetry(() => this.bot?.sendGuildMessage(channelId, payload, source));
1872
+ return;
1873
+ }
1874
+ const userId = qqMeta.userId ?? msg.chatId;
1875
+ await this.sendWithTokenRetry(() => this.bot?.sendPrivateMessage(userId, payload, source));
1876
+ }
1877
+ async handleIncoming(event) {
1878
+ const messageId = event.message_id || event.id || "";
1879
+ if (messageId && this.isDuplicate(messageId)) {
1880
+ return;
1881
+ }
1882
+ if (event.user_id === event.self_id) {
1883
+ return;
1884
+ }
1885
+ const rawEvent = event;
1886
+ const senderId = event.user_id || rawEvent.sender?.member_openid || rawEvent.sender?.user_openid || rawEvent.sender?.user_id || "";
1887
+ if (!senderId) {
1888
+ return;
1889
+ }
1890
+ const content = event.raw_message?.trim() ?? "";
1891
+ const safeContent = content || "[empty message]";
1892
+ let chatId = senderId;
1893
+ let messageType = "private";
1894
+ const qqMeta = {};
1895
+ if (event.message_type === "group") {
1896
+ messageType = "group";
1897
+ const groupId = event.group_id || rawEvent.group_openid || "";
1898
+ chatId = groupId;
1899
+ qqMeta.groupId = groupId;
1900
+ qqMeta.userId = senderId;
1901
+ } else if (event.message_type === "guild") {
1902
+ messageType = "guild";
1903
+ chatId = event.channel_id ?? "";
1904
+ qqMeta.guildId = event.guild_id;
1905
+ qqMeta.channelId = event.channel_id;
1906
+ qqMeta.userId = senderId;
1907
+ } else if (event.sub_type === "direct") {
1908
+ messageType = "direct";
1909
+ chatId = event.guild_id ?? "";
1910
+ qqMeta.guildId = event.guild_id;
1911
+ qqMeta.userId = senderId;
1912
+ } else {
1913
+ qqMeta.userId = senderId;
1914
+ }
1915
+ qqMeta.messageType = messageType;
1916
+ if (!chatId) {
1917
+ return;
1918
+ }
1919
+ if (!this.isAllowed(senderId)) {
1920
+ return;
1921
+ }
1922
+ await this.handleMessage({
1923
+ senderId,
1924
+ chatId,
1925
+ content: safeContent,
1926
+ attachments: [],
1927
+ metadata: {
1928
+ message_id: messageId,
1929
+ qq: qqMeta
1930
+ }
1931
+ });
1932
+ }
1933
+ isDuplicate(messageId) {
1934
+ if (this.processedSet.has(messageId)) {
1935
+ return true;
1936
+ }
1937
+ this.processedSet.add(messageId);
1938
+ this.processedIds.push(messageId);
1939
+ if (this.processedIds.length > 1e3) {
1940
+ const removed = this.processedIds.splice(0, 500);
1941
+ for (const id of removed) {
1942
+ this.processedSet.delete(id);
1943
+ }
1944
+ }
1945
+ return false;
1946
+ }
1947
+ async sendWithTokenRetry(send) {
1948
+ try {
1949
+ await send();
1950
+ } catch (error) {
1951
+ if (!this.isTokenExpiredError(error) || !this.bot) {
1952
+ throw error;
1953
+ }
1954
+ await this.bot.sessionManager.getAccessToken();
1955
+ await send();
1956
+ }
1957
+ }
1958
+ isTokenExpiredError(error) {
1959
+ const message = error instanceof Error ? error.message : String(error);
1960
+ return message.includes("code(11244)") || message.toLowerCase().includes("token not exist or expire");
1961
+ }
1962
+ };
1963
+
1964
+ // src/channels/slack.ts
1965
+ import { WebClient } from "@slack/web-api";
1966
+ import { SocketModeClient } from "@slack/socket-mode";
1967
+ var SlackChannel = class extends BaseChannel {
1968
+ name = "slack";
1969
+ webClient = null;
1970
+ socketClient = null;
1971
+ botUserId = null;
1972
+ botId = null;
1973
+ constructor(config, bus) {
1974
+ super(config, bus);
1975
+ }
1976
+ async start() {
1977
+ if (!this.config.botToken || !this.config.appToken) {
1978
+ throw new Error("Slack bot/app token not configured");
1979
+ }
1980
+ if (this.config.mode !== "socket") {
1981
+ throw new Error(`Unsupported Slack mode: ${this.config.mode}`);
1982
+ }
1983
+ this.running = true;
1984
+ this.webClient = new WebClient(this.config.botToken);
1985
+ this.socketClient = new SocketModeClient({
1986
+ appToken: this.config.appToken
1987
+ });
1988
+ this.socketClient.on("events_api", async ({ body, ack }) => {
1989
+ await ack();
1990
+ await this.handleEvent(body?.event);
1991
+ });
1992
+ try {
1993
+ const auth = await this.webClient.auth.test();
1994
+ this.botUserId = auth.user_id ?? null;
1995
+ this.botId = auth.bot_id ?? null;
1996
+ } catch {
1997
+ this.botUserId = null;
1998
+ this.botId = null;
1999
+ }
2000
+ await this.socketClient.start();
2001
+ }
2002
+ async stop() {
2003
+ this.running = false;
2004
+ if (this.socketClient) {
2005
+ await this.socketClient.disconnect();
2006
+ this.socketClient = null;
2007
+ }
2008
+ this.botUserId = null;
2009
+ this.botId = null;
2010
+ }
2011
+ async send(msg) {
2012
+ if (!this.webClient) {
2013
+ return;
2014
+ }
2015
+ const slackMeta = msg.metadata?.slack ?? {};
2016
+ const threadTs = slackMeta.thread_ts;
2017
+ const channelType = slackMeta.channel_type;
2018
+ const useThread = Boolean(threadTs && channelType !== "im");
2019
+ await this.webClient.chat.postMessage({
2020
+ channel: msg.chatId,
2021
+ text: msg.content ?? "",
2022
+ thread_ts: useThread ? threadTs : void 0
2023
+ });
2024
+ }
2025
+ async handleEvent(event) {
2026
+ if (!event) {
2027
+ return;
2028
+ }
2029
+ const eventType = event.type;
2030
+ if (eventType !== "message" && eventType !== "app_mention") {
2031
+ return;
2032
+ }
2033
+ const subtype = event.subtype;
2034
+ const botId = event.bot_id;
2035
+ const isBotMessage = subtype === "bot_message" || Boolean(botId);
2036
+ if (subtype && subtype !== "bot_message") {
2037
+ return;
2038
+ }
2039
+ if (isBotMessage && !this.config.allowBots) {
2040
+ return;
2041
+ }
2042
+ const senderId = event.user ?? (isBotMessage ? botId : void 0);
2043
+ const chatId = event.channel;
2044
+ const channelType = event.channel_type ?? "";
2045
+ const text = event.text ?? "";
2046
+ if (!senderId || !chatId) {
2047
+ return;
2048
+ }
2049
+ if (this.botUserId && event.user === this.botUserId) {
2050
+ return;
2051
+ }
2052
+ if (this.botId && botId && botId === this.botId) {
2053
+ return;
2054
+ }
2055
+ if (eventType === "message" && !isBotMessage && this.botUserId && text.includes(`<@${this.botUserId}>`)) {
2056
+ return;
2057
+ }
2058
+ if (!this.isAllowedInSlack(senderId, chatId, channelType)) {
2059
+ return;
2060
+ }
2061
+ if (channelType !== "im" && !this.shouldRespondInChannel(eventType, text, chatId)) {
2062
+ return;
2063
+ }
2064
+ const cleanText = this.stripBotMention(text);
2065
+ const threadTs = event.thread_ts ?? event.ts;
2066
+ try {
2067
+ if (this.webClient && event.ts) {
2068
+ await this.webClient.reactions.add({
2069
+ channel: chatId,
2070
+ name: "eyes",
2071
+ timestamp: event.ts
2072
+ });
2073
+ }
2074
+ } catch {
2075
+ }
2076
+ await this.handleMessage({
2077
+ senderId,
2078
+ chatId,
2079
+ content: cleanText,
2080
+ attachments: [],
2081
+ metadata: {
2082
+ slack: {
2083
+ event,
2084
+ thread_ts: threadTs,
2085
+ channel_type: channelType
2086
+ }
2087
+ }
2088
+ });
2089
+ }
2090
+ isAllowedInSlack(senderId, chatId, channelType) {
2091
+ if (channelType === "im") {
2092
+ if (!this.config.dm.enabled) {
2093
+ return false;
2094
+ }
2095
+ if (this.config.dm.policy === "allowlist") {
2096
+ return this.config.dm.allowFrom.includes(senderId);
2097
+ }
2098
+ return true;
2099
+ }
2100
+ if (this.config.groupPolicy === "allowlist") {
2101
+ return this.config.groupAllowFrom.includes(chatId);
2102
+ }
2103
+ return true;
2104
+ }
2105
+ shouldRespondInChannel(eventType, text, chatId) {
2106
+ if (this.config.groupPolicy === "open") {
2107
+ return true;
2108
+ }
2109
+ if (this.config.groupPolicy === "mention") {
2110
+ if (eventType === "app_mention") {
2111
+ return true;
2112
+ }
2113
+ return this.botUserId ? text.includes(`<@${this.botUserId}>`) : false;
2114
+ }
2115
+ if (this.config.groupPolicy === "allowlist") {
2116
+ return this.config.groupAllowFrom.includes(chatId);
2117
+ }
2118
+ return false;
2119
+ }
2120
+ stripBotMention(text) {
2121
+ if (!text || !this.botUserId) {
2122
+ return text;
2123
+ }
2124
+ const pattern = new RegExp(`<@${this.botUserId}>\\s*`, "g");
2125
+ return text.replace(pattern, "").trim();
2126
+ }
2127
+ };
2128
+
2129
+ // src/channels/telegram.ts
2130
+ import TelegramBot from "node-telegram-bot-api";
2131
+
2132
+ // src/providers/transcription.ts
2133
+ import { createReadStream, existsSync as existsSync2 } from "fs";
2134
+ import { basename } from "path";
2135
+ import { FormData, fetch as fetch4 } from "undici";
2136
+ var GroqTranscriptionProvider = class {
2137
+ apiKey;
2138
+ apiUrl = "https://api.groq.com/openai/v1/audio/transcriptions";
2139
+ constructor(apiKey) {
2140
+ this.apiKey = apiKey ?? process.env.GROQ_API_KEY ?? null;
2141
+ }
2142
+ async transcribe(filePath) {
2143
+ if (!this.apiKey) {
2144
+ return "";
2145
+ }
2146
+ if (!existsSync2(filePath)) {
2147
+ return "";
2148
+ }
2149
+ const form = new FormData();
2150
+ form.append("file", createReadStream(filePath), basename(filePath));
2151
+ form.append("model", "whisper-large-v3");
2152
+ const response = await fetch4(this.apiUrl, {
2153
+ method: "POST",
2154
+ headers: {
2155
+ Authorization: `Bearer ${this.apiKey}`
2156
+ },
2157
+ body: form
2158
+ });
2159
+ if (!response.ok) {
2160
+ return "";
2161
+ }
2162
+ const data = await response.json();
2163
+ return data.text ?? "";
2164
+ }
2165
+ };
2166
+
2167
+ // src/channels/telegram.ts
2168
+ import { join as join3 } from "path";
2169
+ import { mkdirSync as mkdirSync3 } from "fs";
2170
+ var BOT_COMMANDS = [
2171
+ { command: "start", description: "Start the bot" },
2172
+ { command: "reset", description: "Reset conversation history" },
2173
+ { command: "help", description: "Show available commands" }
2174
+ ];
2175
+ var TelegramChannel = class extends BaseChannel {
2176
+ constructor(config, bus, groqApiKey, sessionManager) {
2177
+ super(config, bus);
2178
+ this.sessionManager = sessionManager;
2179
+ this.transcriber = new GroqTranscriptionProvider(groqApiKey ?? null);
2180
+ }
2181
+ name = "telegram";
2182
+ bot = null;
2183
+ typingTasks = /* @__PURE__ */ new Map();
2184
+ transcriber;
2185
+ async start() {
2186
+ if (!this.config.token) {
2187
+ throw new Error("Telegram bot token not configured");
2188
+ }
2189
+ this.running = true;
2190
+ const options = { polling: true };
2191
+ if (this.config.proxy) {
2192
+ options.request = { proxy: this.config.proxy };
2193
+ }
2194
+ this.bot = new TelegramBot(this.config.token, options);
2195
+ this.bot.onText(/^\/start$/, async (msg) => {
2196
+ await this.bot?.sendMessage(
2197
+ msg.chat.id,
2198
+ `\u{1F44B} Hi ${msg.from?.first_name ?? ""}! I'm ${APP_NAME}.
2199
+
2200
+ Send me a message and I'll respond!
2201
+ Type /help to see available commands.`
2202
+ );
2203
+ });
2204
+ this.bot.onText(/^\/help$/, async (msg) => {
2205
+ const helpText = `\u{1F916} <b>${APP_NAME} commands</b>
2206
+
2207
+ /start \u2014 Start the bot
2208
+ /reset \u2014 Reset conversation history
2209
+ /help \u2014 Show this help message
2210
+
2211
+ Just send me a text message to chat!`;
2212
+ await this.bot?.sendMessage(msg.chat.id, helpText, { parse_mode: "HTML" });
2213
+ });
2214
+ this.bot.onText(/^\/reset$/, async (msg) => {
2215
+ const chatId = String(msg.chat.id);
2216
+ if (!this.sessionManager) {
2217
+ await this.bot?.sendMessage(msg.chat.id, "\u26A0\uFE0F Session management is not available.");
2218
+ return;
2219
+ }
2220
+ const sessionKey = `${this.name}:${chatId}`;
2221
+ const session = this.sessionManager.getOrCreate(sessionKey);
2222
+ const count = session.messages.length;
2223
+ this.sessionManager.clear(session);
2224
+ this.sessionManager.save(session);
2225
+ await this.bot?.sendMessage(msg.chat.id, `\u{1F504} Conversation history cleared (${count} messages).`);
2226
+ });
2227
+ this.bot.on("message", async (msg) => {
2228
+ if (!msg.text && !msg.caption && !msg.photo && !msg.voice && !msg.audio && !msg.document) {
2229
+ return;
2230
+ }
2231
+ if (msg.text?.startsWith("/")) {
2232
+ return;
2233
+ }
2234
+ await this.handleIncoming(msg);
2235
+ });
2236
+ this.bot.on("channel_post", async (msg) => {
2237
+ if (!msg.text && !msg.caption && !msg.photo && !msg.voice && !msg.audio && !msg.document) {
2238
+ return;
2239
+ }
2240
+ if (msg.text?.startsWith("/")) {
2241
+ return;
2242
+ }
2243
+ await this.handleIncoming(msg);
2244
+ });
2245
+ await this.bot.setMyCommands(BOT_COMMANDS);
2246
+ }
2247
+ async stop() {
2248
+ this.running = false;
2249
+ for (const task of this.typingTasks.values()) {
2250
+ clearInterval(task);
2251
+ }
2252
+ this.typingTasks.clear();
2253
+ if (this.bot) {
2254
+ await this.bot.stopPolling();
2255
+ this.bot = null;
2256
+ }
2257
+ }
2258
+ async send(msg) {
2259
+ if (!this.bot) {
2260
+ return;
2261
+ }
2262
+ this.stopTyping(msg.chatId);
2263
+ const htmlContent = markdownToTelegramHtml(msg.content ?? "");
2264
+ const silent = msg.metadata?.silent === true;
2265
+ const replyTo = msg.replyTo ? Number(msg.replyTo) : void 0;
2266
+ const options = {
2267
+ parse_mode: "HTML",
2268
+ ...replyTo ? { reply_to_message_id: replyTo } : {},
2269
+ ...silent ? { disable_notification: true } : {}
2270
+ };
2271
+ try {
2272
+ await this.bot.sendMessage(Number(msg.chatId), htmlContent, options);
2273
+ } catch {
2274
+ await this.bot.sendMessage(Number(msg.chatId), msg.content ?? "", {
2275
+ ...replyTo ? { reply_to_message_id: replyTo } : {},
2276
+ ...silent ? { disable_notification: true } : {}
2277
+ });
2278
+ }
2279
+ }
2280
+ async handleIncoming(message) {
2281
+ if (!this.bot) {
2282
+ return;
2283
+ }
2284
+ const sender = resolveSender(message);
2285
+ if (!sender) {
2286
+ return;
2287
+ }
2288
+ const chatId = String(message.chat.id);
2289
+ let senderId = String(sender.id);
2290
+ if (sender.username) {
2291
+ senderId = `${senderId}|${sender.username}`;
2292
+ }
2293
+ const contentParts = [];
2294
+ const attachments = [];
2295
+ if (message.text) {
2296
+ contentParts.push(message.text);
2297
+ }
2298
+ if (message.caption) {
2299
+ contentParts.push(message.caption);
2300
+ }
2301
+ const { fileId, mediaType, mimeType } = resolveMedia(message);
2302
+ if (fileId && mediaType) {
2303
+ const mediaDir = join3(getDataPath(), "media");
2304
+ mkdirSync3(mediaDir, { recursive: true });
2305
+ const extension = getExtension(mediaType, mimeType);
2306
+ const downloaded = await this.bot.downloadFile(fileId, mediaDir);
2307
+ const finalPath = extension && !downloaded.endsWith(extension) ? `${downloaded}${extension}` : downloaded;
2308
+ attachments.push({
2309
+ id: fileId,
2310
+ name: finalPath.split("/").pop(),
2311
+ path: finalPath,
2312
+ mimeType: mimeType ?? inferMediaMimeType(mediaType),
2313
+ source: "telegram",
2314
+ status: "ready"
2315
+ });
2316
+ if (mediaType === "voice" || mediaType === "audio") {
2317
+ const transcription = await this.transcriber.transcribe(finalPath);
2318
+ if (transcription) {
2319
+ contentParts.push(`[transcription: ${transcription}]`);
2320
+ } else {
2321
+ contentParts.push(`[${mediaType}: ${finalPath}]`);
2322
+ }
2323
+ } else {
2324
+ contentParts.push(`[${mediaType}: ${finalPath}]`);
2325
+ }
2326
+ }
2327
+ const content = contentParts.length ? contentParts.join("\n") : "[empty message]";
2328
+ this.startTyping(chatId);
2329
+ await this.dispatchToBus(senderId, chatId, content, attachments, {
2330
+ message_id: message.message_id,
2331
+ user_id: sender.id,
2332
+ username: sender.username,
2333
+ first_name: sender.firstName,
2334
+ sender_type: sender.type,
2335
+ is_bot: sender.isBot,
2336
+ is_group: message.chat.type !== "private"
2337
+ });
2338
+ }
2339
+ async dispatchToBus(senderId, chatId, content, attachments, metadata) {
2340
+ await this.handleMessage({ senderId, chatId, content, attachments, metadata });
2341
+ }
2342
+ startTyping(chatId) {
2343
+ this.stopTyping(chatId);
2344
+ if (!this.bot) {
2345
+ return;
2346
+ }
2347
+ const task = setInterval(() => {
2348
+ void this.bot?.sendChatAction(Number(chatId), "typing");
2349
+ }, 4e3);
2350
+ this.typingTasks.set(chatId, task);
2351
+ }
2352
+ stopTyping(chatId) {
2353
+ const task = this.typingTasks.get(chatId);
2354
+ if (task) {
2355
+ clearInterval(task);
2356
+ this.typingTasks.delete(chatId);
2357
+ }
2358
+ }
2359
+ };
2360
+ function resolveSender(message) {
2361
+ if (message.from) {
2362
+ return {
2363
+ id: message.from.id,
2364
+ username: message.from.username,
2365
+ firstName: message.from.first_name,
2366
+ isBot: Boolean(message.from.is_bot),
2367
+ type: "user"
2368
+ };
2369
+ }
2370
+ if (message.sender_chat) {
2371
+ return {
2372
+ id: message.sender_chat.id,
2373
+ username: message.sender_chat.username,
2374
+ firstName: message.sender_chat.title,
2375
+ isBot: true,
2376
+ type: "sender_chat"
2377
+ };
2378
+ }
2379
+ return null;
2380
+ }
2381
+ function resolveMedia(message) {
2382
+ if (message.photo?.length) {
2383
+ const photo = message.photo[message.photo.length - 1];
2384
+ return { fileId: photo.file_id, mediaType: "image", mimeType: "image/jpeg" };
2385
+ }
2386
+ if (message.voice) {
2387
+ return { fileId: message.voice.file_id, mediaType: "voice", mimeType: message.voice.mime_type };
2388
+ }
2389
+ if (message.audio) {
2390
+ return { fileId: message.audio.file_id, mediaType: "audio", mimeType: message.audio.mime_type };
2391
+ }
2392
+ if (message.document) {
2393
+ return { fileId: message.document.file_id, mediaType: "file", mimeType: message.document.mime_type };
2394
+ }
2395
+ return {};
2396
+ }
2397
+ function getExtension(mediaType, mimeType) {
2398
+ const map = {
2399
+ "image/jpeg": ".jpg",
2400
+ "image/png": ".png",
2401
+ "image/gif": ".gif",
2402
+ "audio/ogg": ".ogg",
2403
+ "audio/mpeg": ".mp3",
2404
+ "audio/mp4": ".m4a"
2405
+ };
2406
+ if (mimeType && map[mimeType]) {
2407
+ return map[mimeType];
2408
+ }
2409
+ const fallback = {
2410
+ image: ".jpg",
2411
+ voice: ".ogg",
2412
+ audio: ".mp3",
2413
+ file: ""
2414
+ };
2415
+ return fallback[mediaType] ?? "";
2416
+ }
2417
+ function inferMediaMimeType(mediaType) {
2418
+ if (!mediaType) {
2419
+ return void 0;
2420
+ }
2421
+ if (mediaType === "image") {
2422
+ return "image/jpeg";
2423
+ }
2424
+ if (mediaType === "voice") {
2425
+ return "audio/ogg";
2426
+ }
2427
+ if (mediaType === "audio") {
2428
+ return "audio/mpeg";
2429
+ }
2430
+ return void 0;
2431
+ }
2432
+ function markdownToTelegramHtml(text) {
2433
+ if (!text) {
2434
+ return "";
2435
+ }
2436
+ const codeBlocks = [];
2437
+ text = text.replace(/```[\w]*\n?([\s\S]*?)```/g, (_m, code) => {
2438
+ codeBlocks.push(code);
2439
+ return `\0CB${codeBlocks.length - 1}\0`;
2440
+ });
2441
+ const inlineCodes = [];
2442
+ text = text.replace(/`([^`]+)`/g, (_m, code) => {
2443
+ inlineCodes.push(code);
2444
+ return `\0IC${inlineCodes.length - 1}\0`;
2445
+ });
2446
+ text = text.replace(/^#{1,6}\s+(.+)$/gm, "$1");
2447
+ text = text.replace(/^>\s*(.*)$/gm, "$1");
2448
+ text = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2449
+ text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
2450
+ text = text.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>");
2451
+ text = text.replace(/__(.+?)__/g, "<b>$1</b>");
2452
+ text = text.replace(/(?<![a-zA-Z0-9])_([^_]+)_(?![a-zA-Z0-9])/g, "<i>$1</i>");
2453
+ text = text.replace(/~~(.+?)~~/g, "<s>$1</s>");
2454
+ text = text.replace(/^[-*]\s+/gm, "\u2022 ");
2455
+ inlineCodes.forEach((code, i) => {
2456
+ const escaped = code.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2457
+ text = text.replace(`\0IC${i}\0`, `<code>${escaped}</code>`);
2458
+ });
2459
+ codeBlocks.forEach((code, i) => {
2460
+ const escaped = code.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2461
+ text = text.replace(`\0CB${i}\0`, `<pre><code>${escaped}</code></pre>`);
2462
+ });
2463
+ return text;
2464
+ }
2465
+
2466
+ // src/channels/whatsapp.ts
2467
+ import WebSocket from "ws";
2468
+ var WhatsAppChannel = class extends BaseChannel {
2469
+ name = "whatsapp";
2470
+ ws = null;
2471
+ connected = false;
2472
+ constructor(config, bus) {
2473
+ super(config, bus);
2474
+ }
2475
+ async start() {
2476
+ this.running = true;
2477
+ const bridgeUrl = this.config.bridgeUrl;
2478
+ while (this.running) {
2479
+ try {
2480
+ await new Promise((resolve, reject) => {
2481
+ const ws = new WebSocket(bridgeUrl);
2482
+ this.ws = ws;
2483
+ ws.on("open", () => {
2484
+ this.connected = true;
2485
+ });
2486
+ ws.on("message", (data) => {
2487
+ const payload = data.toString();
2488
+ void this.handleBridgeMessage(payload);
2489
+ });
2490
+ ws.on("close", () => {
2491
+ this.connected = false;
2492
+ this.ws = null;
2493
+ resolve();
2494
+ });
2495
+ ws.on("error", (_err) => {
2496
+ this.connected = false;
2497
+ this.ws = null;
2498
+ reject(_err);
2499
+ });
2500
+ });
2501
+ } catch {
2502
+ if (!this.running) {
2503
+ break;
2504
+ }
2505
+ await sleep3(5e3);
2506
+ }
2507
+ }
2508
+ }
2509
+ async stop() {
2510
+ this.running = false;
2511
+ this.connected = false;
2512
+ if (this.ws) {
2513
+ this.ws.close();
2514
+ this.ws = null;
2515
+ }
2516
+ }
2517
+ async send(msg) {
2518
+ if (!this.ws || !this.connected) {
2519
+ return;
2520
+ }
2521
+ const payload = {
2522
+ type: "send",
2523
+ to: msg.chatId,
2524
+ text: msg.content
2525
+ };
2526
+ this.ws.send(JSON.stringify(payload));
2527
+ }
2528
+ async handleBridgeMessage(raw) {
2529
+ let data;
2530
+ try {
2531
+ data = JSON.parse(raw);
2532
+ } catch {
2533
+ return;
2534
+ }
2535
+ const msgType = data.type;
2536
+ if (msgType === "message") {
2537
+ const pn = data.pn ?? "";
2538
+ const sender = data.sender ?? "";
2539
+ let content = data.content ?? "";
2540
+ const userId = pn || sender;
2541
+ const senderId = userId.includes("@") ? userId.split("@")[0] : userId;
2542
+ if (content === "[Voice Message]") {
2543
+ content = "[Voice Message: Transcription not available for WhatsApp yet]";
2544
+ }
2545
+ await this.handleMessage({
2546
+ senderId,
2547
+ chatId: sender || userId,
2548
+ content,
2549
+ attachments: [],
2550
+ metadata: {
2551
+ message_id: data.id,
2552
+ timestamp: data.timestamp,
2553
+ is_group: Boolean(data.isGroup)
2554
+ }
2555
+ });
2556
+ return;
2557
+ }
2558
+ if (msgType === "status") {
2559
+ const status = data.status;
2560
+ if (status === "connected") {
2561
+ this.connected = true;
2562
+ } else if (status === "disconnected") {
2563
+ this.connected = false;
2564
+ }
2565
+ return;
2566
+ }
2567
+ if (msgType === "qr") {
2568
+ return;
2569
+ }
2570
+ if (msgType === "error") {
2571
+ return;
2572
+ }
2573
+ }
2574
+ };
2575
+ function sleep3(ms) {
2576
+ return new Promise((resolve) => setTimeout(resolve, ms));
2577
+ }
2578
+
2579
+ // src/index.ts
2580
+ var BUILTIN_CHANNEL_RUNTIMES = {
2581
+ telegram: {
2582
+ id: "telegram",
2583
+ isEnabled: (config) => config.channels.telegram.enabled,
2584
+ createChannel: (context) => new TelegramChannel(
2585
+ context.config.channels.telegram,
2586
+ context.bus,
2587
+ context.config.providers.groq.apiKey,
2588
+ context.sessionManager
2589
+ )
2590
+ },
2591
+ whatsapp: {
2592
+ id: "whatsapp",
2593
+ isEnabled: (config) => config.channels.whatsapp.enabled,
2594
+ createChannel: (context) => new WhatsAppChannel(context.config.channels.whatsapp, context.bus)
2595
+ },
2596
+ discord: {
2597
+ id: "discord",
2598
+ isEnabled: (config) => config.channels.discord.enabled,
2599
+ createChannel: (context) => new DiscordChannel(context.config.channels.discord, context.bus)
2600
+ },
2601
+ feishu: {
2602
+ id: "feishu",
2603
+ isEnabled: (config) => config.channels.feishu.enabled,
2604
+ createChannel: (context) => new FeishuChannel(context.config.channels.feishu, context.bus)
2605
+ },
2606
+ mochat: {
2607
+ id: "mochat",
2608
+ isEnabled: (config) => config.channels.mochat.enabled,
2609
+ createChannel: (context) => new MochatChannel(context.config.channels.mochat, context.bus)
2610
+ },
2611
+ dingtalk: {
2612
+ id: "dingtalk",
2613
+ isEnabled: (config) => config.channels.dingtalk.enabled,
2614
+ createChannel: (context) => new DingTalkChannel(context.config.channels.dingtalk, context.bus)
2615
+ },
2616
+ email: {
2617
+ id: "email",
2618
+ isEnabled: (config) => config.channels.email.enabled,
2619
+ createChannel: (context) => new EmailChannel(context.config.channels.email, context.bus)
2620
+ },
2621
+ slack: {
2622
+ id: "slack",
2623
+ isEnabled: (config) => config.channels.slack.enabled,
2624
+ createChannel: (context) => new SlackChannel(context.config.channels.slack, context.bus)
2625
+ },
2626
+ qq: {
2627
+ id: "qq",
2628
+ isEnabled: (config) => config.channels.qq.enabled,
2629
+ createChannel: (context) => new QQChannel(context.config.channels.qq, context.bus)
2630
+ }
2631
+ };
2632
+ var BUILTIN_CHANNEL_PLUGIN_IDS = Object.keys(BUILTIN_CHANNEL_RUNTIMES);
2633
+ function listBuiltinChannelRuntimes() {
2634
+ return BUILTIN_CHANNEL_PLUGIN_IDS.map(
2635
+ (channelId) => resolveBuiltinChannelRuntime(channelId)
2636
+ );
2637
+ }
2638
+ function resolveBuiltinChannelRuntime(channelId) {
2639
+ const runtime = BUILTIN_CHANNEL_RUNTIMES[channelId];
2640
+ if (!runtime) {
2641
+ throw new Error(`builtin channel runtime not found: ${channelId}`);
2642
+ }
2643
+ return runtime;
2644
+ }
2645
+ export {
2646
+ BUILTIN_CHANNEL_PLUGIN_IDS,
2647
+ DingTalkChannel,
2648
+ DiscordChannel,
2649
+ EmailChannel,
2650
+ FeishuChannel,
2651
+ MochatChannel,
2652
+ QQChannel,
2653
+ SlackChannel,
2654
+ TelegramChannel,
2655
+ WhatsAppChannel,
2656
+ listBuiltinChannelRuntimes,
2657
+ resolveBuiltinChannelRuntime
2658
+ };