@nextclaw/channel-runtime 0.1.7 → 0.1.9

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.d.ts CHANGED
@@ -30,6 +30,7 @@ declare abstract class BaseChannel<TConfig extends Record<string, unknown>> {
30
30
  abstract start(): Promise<void>;
31
31
  abstract stop(): Promise<void>;
32
32
  abstract send(msg: OutboundMessage): Promise<void>;
33
+ handleControlMessage(_msg: OutboundMessage): Promise<boolean>;
33
34
  isAllowed(senderId: string): boolean;
34
35
  protected handleMessage(params: {
35
36
  senderId: string;
@@ -61,9 +62,13 @@ declare class DiscordChannel extends BaseChannel<Config["channels"]["discord"]>
61
62
  constructor(config: Config["channels"]["discord"], bus: MessageBus);
62
63
  start(): Promise<void>;
63
64
  stop(): Promise<void>;
65
+ handleControlMessage(msg: OutboundMessage): Promise<boolean>;
64
66
  send(msg: OutboundMessage): Promise<void>;
65
67
  private handleIncoming;
66
68
  private resolveProxyAgent;
69
+ private resolveAccountId;
70
+ private isAllowedByPolicy;
71
+ private resolveMentionState;
67
72
  private resolveInboundAttachment;
68
73
  private startTyping;
69
74
  private stopTyping;
@@ -192,16 +197,22 @@ declare class TelegramChannel extends BaseChannel<Config["channels"]["telegram"]
192
197
  private sessionManager?;
193
198
  name: string;
194
199
  private bot;
200
+ private botUserId;
201
+ private botUsername;
195
202
  private readonly typingController;
196
203
  private transcriber;
197
204
  constructor(config: Config["channels"]["telegram"], bus: MessageBus, groqApiKey?: string, sessionManager?: SessionManager | undefined);
198
205
  start(): Promise<void>;
199
206
  stop(): Promise<void>;
207
+ handleControlMessage(msg: OutboundMessage): Promise<boolean>;
200
208
  send(msg: OutboundMessage): Promise<void>;
201
209
  private handleIncoming;
202
210
  private dispatchToBus;
203
211
  private startTyping;
204
212
  private stopTyping;
213
+ private resolveAccountId;
214
+ private isAllowedByPolicy;
215
+ private resolveMentionState;
205
216
  }
206
217
 
207
218
  declare class WeComChannel extends BaseChannel<Config["channels"]["wecom"]> {
package/dist/index.js CHANGED
@@ -5,6 +5,9 @@ var BaseChannel = class {
5
5
  this.bus = bus;
6
6
  }
7
7
  running = false;
8
+ async handleControlMessage(_msg) {
9
+ return false;
10
+ }
8
11
  isAllowed(senderId) {
9
12
  const allowList = this.config.allowFrom ?? [];
10
13
  if (!allowList.length) {
@@ -224,10 +227,11 @@ var ChannelTypingController = class {
224
227
  };
225
228
 
226
229
  // src/channels/discord.ts
230
+ import { isTypingStopControlMessage } from "@nextclaw/core";
227
231
  var DEFAULT_MEDIA_MAX_MB = 8;
228
232
  var MEDIA_FETCH_TIMEOUT_MS = 15e3;
229
- var TYPING_HEARTBEAT_MS = 8e3;
230
- var TYPING_AUTO_STOP_MS = 45e3;
233
+ var TYPING_HEARTBEAT_MS = 6e3;
234
+ var TYPING_AUTO_STOP_MS = 12e4;
231
235
  var DISCORD_TEXT_LIMIT = 2e3;
232
236
  var DISCORD_MAX_LINES_PER_MESSAGE = 17;
233
237
  var FENCE_RE = /^( {0,3})(`{3,}|~{3,})(.*)$/;
@@ -278,7 +282,18 @@ var DiscordChannel = class extends BaseChannel {
278
282
  this.client = null;
279
283
  }
280
284
  }
285
+ async handleControlMessage(msg) {
286
+ if (!isTypingStopControlMessage(msg)) {
287
+ return false;
288
+ }
289
+ this.stopTyping(msg.chatId);
290
+ return true;
291
+ }
281
292
  async send(msg) {
293
+ if (isTypingStopControlMessage(msg)) {
294
+ this.stopTyping(msg.chatId);
295
+ return;
296
+ }
282
297
  if (!this.client) {
283
298
  return;
284
299
  }
@@ -316,7 +331,12 @@ var DiscordChannel = class extends BaseChannel {
316
331
  }
317
332
  const senderId = message.author.id;
318
333
  const channelId = message.channelId;
319
- if (!this.isAllowed(senderId)) {
334
+ const isGroup = Boolean(message.guildId);
335
+ if (!this.isAllowedByPolicy({ senderId, channelId, isGroup })) {
336
+ return;
337
+ }
338
+ const mentionState = this.resolveMentionState({ message, selfUserId, channelId, isGroup });
339
+ if (mentionState.requireMention && !mentionState.wasMentioned) {
320
340
  return;
321
341
  }
322
342
  const contentParts = [];
@@ -358,13 +378,22 @@ var DiscordChannel = class extends BaseChannel {
358
378
  attachments,
359
379
  metadata: {
360
380
  message_id: message.id,
381
+ channel_id: channelId,
361
382
  guild_id: message.guildId,
362
383
  reply_to: replyTo,
384
+ account_id: this.resolveAccountId(),
385
+ accountId: this.resolveAccountId(),
386
+ is_group: isGroup,
387
+ peer_kind: isGroup ? "channel" : "direct",
388
+ peer_id: isGroup ? channelId : senderId,
389
+ was_mentioned: mentionState.wasMentioned,
390
+ require_mention: mentionState.requireMention,
363
391
  ...attachmentIssues.length ? { attachment_issues: attachmentIssues } : {}
364
392
  }
365
393
  });
366
- } finally {
394
+ } catch (error) {
367
395
  this.stopTyping(channelId);
396
+ throw error;
368
397
  }
369
398
  }
370
399
  resolveProxyAgent() {
@@ -378,6 +407,62 @@ var DiscordChannel = class extends BaseChannel {
378
407
  return null;
379
408
  }
380
409
  }
410
+ resolveAccountId() {
411
+ const accountId = this.config.accountId?.trim();
412
+ return accountId || "default";
413
+ }
414
+ isAllowedByPolicy(params) {
415
+ if (!params.isGroup) {
416
+ if (this.config.dmPolicy === "disabled") {
417
+ return false;
418
+ }
419
+ const allowFrom = this.config.allowFrom ?? [];
420
+ if (this.config.dmPolicy === "allowlist" || this.config.dmPolicy === "pairing") {
421
+ return this.isAllowed(params.senderId);
422
+ }
423
+ if (allowFrom.includes("*")) {
424
+ return true;
425
+ }
426
+ return allowFrom.length === 0 ? true : this.isAllowed(params.senderId);
427
+ }
428
+ if (this.config.groupPolicy === "disabled") {
429
+ return false;
430
+ }
431
+ if (this.config.groupPolicy === "allowlist") {
432
+ const allowFrom = this.config.groupAllowFrom ?? [];
433
+ return allowFrom.includes("*") || allowFrom.includes(params.channelId);
434
+ }
435
+ return true;
436
+ }
437
+ resolveMentionState(params) {
438
+ if (!params.isGroup) {
439
+ return { wasMentioned: false, requireMention: false };
440
+ }
441
+ const groups = this.config.groups ?? {};
442
+ const groupRule = groups[params.channelId] ?? groups["*"];
443
+ const requireMention = groupRule?.requireMention ?? this.config.requireMention ?? false;
444
+ if (!requireMention) {
445
+ return { wasMentioned: false, requireMention: false };
446
+ }
447
+ const patterns = [
448
+ ...this.config.mentionPatterns ?? [],
449
+ ...groupRule?.mentionPatterns ?? []
450
+ ].map((pattern) => pattern.trim()).filter(Boolean);
451
+ const content = params.message.content ?? "";
452
+ const wasMentionedByUserRef = Boolean(params.selfUserId) && params.message.mentions.users.has(params.selfUserId ?? "");
453
+ const wasMentionedByText = Boolean(params.selfUserId) && (content.includes(`<@${params.selfUserId}>`) || content.includes(`<@!${params.selfUserId}>`));
454
+ const wasMentionedByPattern = patterns.some((pattern) => {
455
+ try {
456
+ return new RegExp(pattern, "i").test(content);
457
+ } catch {
458
+ return content.toLowerCase().includes(pattern.toLowerCase());
459
+ }
460
+ });
461
+ return {
462
+ wasMentioned: wasMentionedByUserRef || wasMentionedByText || wasMentionedByPattern,
463
+ requireMention
464
+ };
465
+ }
381
466
  async resolveInboundAttachment(params) {
382
467
  const { attachment, mediaDir, maxBytes, proxy } = params;
383
468
  const id = attachment.id;
@@ -2373,8 +2458,9 @@ var GroqTranscriptionProvider = class {
2373
2458
  // src/channels/telegram.ts
2374
2459
  import { join as join3 } from "path";
2375
2460
  import { mkdirSync as mkdirSync3 } from "fs";
2376
- var TYPING_HEARTBEAT_MS2 = 4e3;
2377
- var TYPING_AUTO_STOP_MS2 = 45e3;
2461
+ import { isTypingStopControlMessage as isTypingStopControlMessage2 } from "@nextclaw/core";
2462
+ var TYPING_HEARTBEAT_MS2 = 6e3;
2463
+ var TYPING_AUTO_STOP_MS2 = 12e4;
2378
2464
  var BOT_COMMANDS = [
2379
2465
  { command: "start", description: "Start the bot" },
2380
2466
  { command: "reset", description: "Reset conversation history" },
@@ -2395,6 +2481,8 @@ var TelegramChannel = class extends BaseChannel {
2395
2481
  }
2396
2482
  name = "telegram";
2397
2483
  bot = null;
2484
+ botUserId = null;
2485
+ botUsername = null;
2398
2486
  typingController;
2399
2487
  transcriber;
2400
2488
  async start() {
@@ -2407,6 +2495,14 @@ var TelegramChannel = class extends BaseChannel {
2407
2495
  options.request = { proxy: this.config.proxy };
2408
2496
  }
2409
2497
  this.bot = new TelegramBot(this.config.token, options);
2498
+ try {
2499
+ const me = await this.bot.getMe();
2500
+ this.botUserId = me.id;
2501
+ this.botUsername = me.username ?? null;
2502
+ } catch {
2503
+ this.botUserId = null;
2504
+ this.botUsername = null;
2505
+ }
2410
2506
  this.bot.onText(/^\/start$/, async (msg) => {
2411
2507
  await this.bot?.sendMessage(
2412
2508
  msg.chat.id,
@@ -2432,12 +2528,31 @@ Just send me a text message to chat!`;
2432
2528
  await this.bot?.sendMessage(msg.chat.id, "\u26A0\uFE0F Session management is not available.");
2433
2529
  return;
2434
2530
  }
2435
- const sessionKey = `${this.name}:${chatId}`;
2436
- const session = this.sessionManager.getOrCreate(sessionKey);
2437
- const count = session.messages.length;
2438
- this.sessionManager.clear(session);
2439
- this.sessionManager.save(session);
2440
- await this.bot?.sendMessage(msg.chat.id, `\u{1F504} Conversation history cleared (${count} messages).`);
2531
+ const accountId = this.resolveAccountId();
2532
+ const candidates = this.sessionManager.listSessions().filter((entry) => {
2533
+ const metadata = entry.metadata ?? {};
2534
+ const lastChannel = typeof metadata.last_channel === "string" ? metadata.last_channel : "";
2535
+ const lastTo = typeof metadata.last_to === "string" ? metadata.last_to : "";
2536
+ const lastAccountId = typeof metadata.last_account_id === "string" ? metadata.last_account_id : typeof metadata.last_accountId === "string" ? metadata.last_accountId : "default";
2537
+ return lastChannel === this.name && lastTo === chatId && lastAccountId === accountId;
2538
+ }).map((entry) => String(entry.key ?? "")).filter(Boolean);
2539
+ let totalCleared = 0;
2540
+ for (const key of candidates) {
2541
+ const session = this.sessionManager.getIfExists(key);
2542
+ if (!session) {
2543
+ continue;
2544
+ }
2545
+ totalCleared += session.messages.length;
2546
+ this.sessionManager.clear(session);
2547
+ this.sessionManager.save(session);
2548
+ }
2549
+ if (candidates.length === 0) {
2550
+ const legacySession = this.sessionManager.getOrCreate(`${this.name}:${chatId}`);
2551
+ totalCleared = legacySession.messages.length;
2552
+ this.sessionManager.clear(legacySession);
2553
+ this.sessionManager.save(legacySession);
2554
+ }
2555
+ await this.bot?.sendMessage(msg.chat.id, `\u{1F504} Conversation history cleared (${totalCleared} messages).`);
2441
2556
  });
2442
2557
  this.bot.on("message", async (msg) => {
2443
2558
  if (!msg.text && !msg.caption && !msg.photo && !msg.voice && !msg.audio && !msg.document) {
@@ -2467,7 +2582,18 @@ Just send me a text message to chat!`;
2467
2582
  this.bot = null;
2468
2583
  }
2469
2584
  }
2585
+ async handleControlMessage(msg) {
2586
+ if (!isTypingStopControlMessage2(msg)) {
2587
+ return false;
2588
+ }
2589
+ this.stopTyping(msg.chatId);
2590
+ return true;
2591
+ }
2470
2592
  async send(msg) {
2593
+ if (isTypingStopControlMessage2(msg)) {
2594
+ this.stopTyping(msg.chatId);
2595
+ return;
2596
+ }
2471
2597
  if (!this.bot) {
2472
2598
  return;
2473
2599
  }
@@ -2498,6 +2624,14 @@ Just send me a text message to chat!`;
2498
2624
  return;
2499
2625
  }
2500
2626
  const chatId = String(message.chat.id);
2627
+ const isGroup = message.chat.type !== "private";
2628
+ if (!this.isAllowedByPolicy({ senderId: String(sender.id), chatId, isGroup })) {
2629
+ return;
2630
+ }
2631
+ const mentionState = this.resolveMentionState({ message, chatId, isGroup });
2632
+ if (mentionState.requireMention && !mentionState.wasMentioned) {
2633
+ return;
2634
+ }
2501
2635
  let senderId = String(sender.id);
2502
2636
  if (sender.username) {
2503
2637
  senderId = `${senderId}|${sender.username}`;
@@ -2546,10 +2680,17 @@ Just send me a text message to chat!`;
2546
2680
  first_name: sender.firstName,
2547
2681
  sender_type: sender.type,
2548
2682
  is_bot: sender.isBot,
2549
- is_group: message.chat.type !== "private"
2683
+ is_group: isGroup,
2684
+ account_id: this.resolveAccountId(),
2685
+ accountId: this.resolveAccountId(),
2686
+ peer_kind: isGroup ? "group" : "direct",
2687
+ peer_id: isGroup ? chatId : String(sender.id),
2688
+ was_mentioned: mentionState.wasMentioned,
2689
+ require_mention: mentionState.requireMention
2550
2690
  });
2551
- } finally {
2691
+ } catch (error) {
2552
2692
  this.stopTyping(chatId);
2693
+ throw error;
2553
2694
  }
2554
2695
  }
2555
2696
  async dispatchToBus(senderId, chatId, content, attachments, metadata) {
@@ -2561,6 +2702,63 @@ Just send me a text message to chat!`;
2561
2702
  stopTyping(chatId) {
2562
2703
  this.typingController.stop(chatId);
2563
2704
  }
2705
+ resolveAccountId() {
2706
+ const accountId = this.config.accountId?.trim();
2707
+ return accountId || "default";
2708
+ }
2709
+ isAllowedByPolicy(params) {
2710
+ if (!params.isGroup) {
2711
+ if (this.config.dmPolicy === "disabled") {
2712
+ return false;
2713
+ }
2714
+ const allowFrom = this.config.allowFrom ?? [];
2715
+ if (this.config.dmPolicy === "allowlist" || this.config.dmPolicy === "pairing") {
2716
+ return this.isAllowed(params.senderId);
2717
+ }
2718
+ if (allowFrom.includes("*")) {
2719
+ return true;
2720
+ }
2721
+ return allowFrom.length === 0 ? true : this.isAllowed(params.senderId);
2722
+ }
2723
+ if (this.config.groupPolicy === "disabled") {
2724
+ return false;
2725
+ }
2726
+ if (this.config.groupPolicy === "allowlist") {
2727
+ const allowFrom = this.config.groupAllowFrom ?? [];
2728
+ return allowFrom.includes("*") || allowFrom.includes(params.chatId);
2729
+ }
2730
+ return true;
2731
+ }
2732
+ resolveMentionState(params) {
2733
+ if (!params.isGroup) {
2734
+ return { wasMentioned: false, requireMention: false };
2735
+ }
2736
+ const groups = this.config.groups ?? {};
2737
+ const groupRule = groups[params.chatId] ?? groups["*"];
2738
+ const requireMention = groupRule?.requireMention ?? this.config.requireMention ?? false;
2739
+ if (!requireMention) {
2740
+ return { wasMentioned: false, requireMention: false };
2741
+ }
2742
+ const content = `${params.message.text ?? ""}
2743
+ ${params.message.caption ?? ""}`.trim();
2744
+ const patterns = [
2745
+ ...this.config.mentionPatterns ?? [],
2746
+ ...groupRule?.mentionPatterns ?? []
2747
+ ].map((pattern) => pattern.trim()).filter(Boolean);
2748
+ const usernameMentioned = this.botUsername ? content.includes(`@${this.botUsername}`) : false;
2749
+ const replyToBot = Boolean(this.botUserId) && Boolean(params.message.reply_to_message?.from) && params.message.reply_to_message?.from?.id === this.botUserId;
2750
+ const patternMentioned = patterns.some((pattern) => {
2751
+ try {
2752
+ return new RegExp(pattern, "i").test(content);
2753
+ } catch {
2754
+ return content.toLowerCase().includes(pattern.toLowerCase());
2755
+ }
2756
+ });
2757
+ return {
2758
+ wasMentioned: usernameMentioned || replyToBot || patternMentioned,
2759
+ requireMention
2760
+ };
2761
+ }
2564
2762
  };
2565
2763
  function resolveSender(message) {
2566
2764
  if (message.from) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/channel-runtime",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "private": false,
5
5
  "description": "Runtime implementations for NextClaw builtin channel plugins.",
6
6
  "type": "module",
@@ -15,7 +15,7 @@
15
15
  ],
16
16
  "dependencies": {
17
17
  "@larksuiteoapi/node-sdk": "^1.58.0",
18
- "@nextclaw/core": "^0.6.21",
18
+ "@nextclaw/core": "^0.6.23",
19
19
  "@slack/socket-mode": "^1.3.3",
20
20
  "@slack/web-api": "^7.6.0",
21
21
  "dingtalk-stream": "^2.1.4",