@theowlops/channelhub 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -20548,9 +20548,9 @@ var require_websocket_server = __commonJS(function(exports2, module2) {
20548
20548
  });
20549
20549
 
20550
20550
  // node_modules/ws/wrapper.mjs
20551
- var import_stream, import_extension, import_permessage_deflate, import_receiver, import_sender, import_subprotocol, import_websocket, import_websocket_server, wrapper_default;
20551
+ var import_stream2, import_extension, import_permessage_deflate, import_receiver, import_sender, import_subprotocol, import_websocket, import_websocket_server, wrapper_default;
20552
20552
  var init_wrapper = __esm(() => {
20553
- import_stream = __toESM(require_stream(), 1);
20553
+ import_stream2 = __toESM(require_stream(), 1);
20554
20554
  import_extension = __toESM(require_extension(), 1);
20555
20555
  import_permessage_deflate = __toESM(require_permessage_deflate(), 1);
20556
20556
  import_receiver = __toESM(require_receiver(), 1);
@@ -37520,6 +37520,7 @@ __export(exports_src, {
37520
37520
  CommandRouter: () => CommandRouter,
37521
37521
  DiscordChannelAdapter: () => DiscordChannelAdapter,
37522
37522
  SlackChannelAdapter: () => SlackChannelAdapter,
37523
+ SmartStreamer: () => SmartStreamer,
37523
37524
  TelegramChannelAdapter: () => TelegramChannelAdapter,
37524
37525
  WebhookBridge: () => WebhookBridge,
37525
37526
  Zalo: () => Zalo,
@@ -37563,6 +37564,126 @@ class ChannelEventBus extends import_node_events2.EventEmitter {
37563
37564
  return this.emit("status", status);
37564
37565
  }
37565
37566
  }
37567
+ // src/core/stream.ts
37568
+ class SmartStreamer {
37569
+ adapter;
37570
+ options;
37571
+ constructor(adapter, options = {}) {
37572
+ this.adapter = adapter;
37573
+ this.options = {
37574
+ editDebounceMs: 1000,
37575
+ typingIntervalMs: 4000,
37576
+ chunkMode: "sentence",
37577
+ minSentenceLength: 60,
37578
+ initialPlaceholder: "...",
37579
+ ...options
37580
+ };
37581
+ }
37582
+ async stream(chatId, tokenStream, sendOptions) {
37583
+ let typingActive = true;
37584
+ const triggerTyping = async () => {
37585
+ if (this.adapter.sendTyping) {
37586
+ try {
37587
+ await this.adapter.sendTyping(chatId);
37588
+ } catch {}
37589
+ }
37590
+ };
37591
+ await triggerTyping();
37592
+ const typingTimer = setInterval(() => {
37593
+ if (typingActive)
37594
+ triggerTyping();
37595
+ }, this.options.typingIntervalMs);
37596
+ try {
37597
+ if (typeof this.adapter.editText === "function") {
37598
+ return await this.streamWithEdit(chatId, tokenStream, sendOptions);
37599
+ } else {
37600
+ return await this.streamWithoutEdit(chatId, tokenStream, sendOptions);
37601
+ }
37602
+ } finally {
37603
+ typingActive = false;
37604
+ clearInterval(typingTimer);
37605
+ }
37606
+ }
37607
+ async streamWithEdit(chatId, tokenStream, sendOptions) {
37608
+ let accumulated = "";
37609
+ let sentMsg = null;
37610
+ let lastEditTime = 0;
37611
+ let pendingEditTimeout = null;
37612
+ const performEdit = async (text) => {
37613
+ if (sentMsg && this.adapter.editText) {
37614
+ await this.adapter.editText(chatId, sentMsg.messageId, text);
37615
+ lastEditTime = Date.now();
37616
+ }
37617
+ };
37618
+ for await (const chunk of tokenStream) {
37619
+ accumulated += chunk;
37620
+ if (!sentMsg) {
37621
+ sentMsg = await this.adapter.sendText(chatId, accumulated.trim() || this.options.initialPlaceholder, sendOptions);
37622
+ lastEditTime = Date.now();
37623
+ continue;
37624
+ }
37625
+ const now = Date.now();
37626
+ const elapsed = now - lastEditTime;
37627
+ if (elapsed >= this.options.editDebounceMs) {
37628
+ if (pendingEditTimeout) {
37629
+ clearTimeout(pendingEditTimeout);
37630
+ pendingEditTimeout = null;
37631
+ }
37632
+ await performEdit(accumulated);
37633
+ } else if (!pendingEditTimeout) {
37634
+ pendingEditTimeout = setTimeout(async () => {
37635
+ pendingEditTimeout = null;
37636
+ await performEdit(accumulated);
37637
+ }, this.options.editDebounceMs - elapsed);
37638
+ }
37639
+ }
37640
+ if (pendingEditTimeout) {
37641
+ clearTimeout(pendingEditTimeout);
37642
+ pendingEditTimeout = null;
37643
+ }
37644
+ if (sentMsg && accumulated) {
37645
+ await performEdit(accumulated);
37646
+ return [sentMsg];
37647
+ } else if (!sentMsg && accumulated) {
37648
+ const res = await this.adapter.sendText(chatId, accumulated, sendOptions);
37649
+ return [res];
37650
+ }
37651
+ return sentMsg ? [sentMsg] : [];
37652
+ }
37653
+ async streamWithoutEdit(chatId, tokenStream, sendOptions) {
37654
+ const results = [];
37655
+ if (this.options.chunkMode === "accumulate") {
37656
+ let accumulated = "";
37657
+ for await (const chunk of tokenStream) {
37658
+ accumulated += chunk;
37659
+ }
37660
+ if (accumulated.trim()) {
37661
+ const res = await this.adapter.sendText(chatId, accumulated, sendOptions);
37662
+ results.push(res);
37663
+ }
37664
+ return results;
37665
+ }
37666
+ let buffer = "";
37667
+ const sentenceEndRegex = /[.?!;\n]\s*$/;
37668
+ for await (const chunk of tokenStream) {
37669
+ buffer += chunk;
37670
+ if (buffer.length >= this.options.minSentenceLength && sentenceEndRegex.test(buffer.trimEnd())) {
37671
+ const textToSend = buffer.trim();
37672
+ if (textToSend) {
37673
+ const res = await this.adapter.sendText(chatId, textToSend, sendOptions);
37674
+ results.push(res);
37675
+ buffer = "";
37676
+ }
37677
+ }
37678
+ }
37679
+ if (buffer.trim()) {
37680
+ const res = await this.adapter.sendText(chatId, buffer.trim(), sendOptions);
37681
+ results.push(res);
37682
+ }
37683
+ return results;
37684
+ }
37685
+ }
37686
+
37566
37687
  // src/core/context.ts
37567
37688
  function createMessageContext(message, channel) {
37568
37689
  return {
@@ -37580,6 +37701,17 @@ function createMessageContext(message, channel) {
37580
37701
  if (channel.addReaction) {
37581
37702
  await channel.addReaction(message.chat.id, message.id, emoji);
37582
37703
  }
37704
+ },
37705
+ sendTyping: async () => {
37706
+ if (channel.sendTyping) {
37707
+ await channel.sendTyping(message.chat.id);
37708
+ }
37709
+ },
37710
+ stream: async (tokenStream, options) => {
37711
+ const streamer = new SmartStreamer(channel, options);
37712
+ return await streamer.stream(message.chat.id, tokenStream, {
37713
+ replyToId: message.id
37714
+ });
37583
37715
  }
37584
37716
  };
37585
37717
  }
@@ -37627,14 +37759,47 @@ class ChannelHub {
37627
37759
  }
37628
37760
  }
37629
37761
  // src/channels/zalo/adapter.ts
37762
+ var EMOJI_TO_ZALO = {
37763
+ "❤️": "HEART",
37764
+ "\uD83D\uDC96": "HEART",
37765
+ "\uD83D\uDC4D": "LIKE",
37766
+ "\uD83D\uDE06": "HAHA",
37767
+ "\uD83D\uDE02": "TEARS_OF_JOY",
37768
+ "\uD83D\uDE2E": "WOW",
37769
+ "\uD83D\uDE2D": "CRY",
37770
+ "\uD83D\uDE21": "ANGRY",
37771
+ "\uD83D\uDE18": "KISS",
37772
+ "\uD83D\uDCA9": "SHIT",
37773
+ "\uD83C\uDF39": "ROSE",
37774
+ "\uD83D\uDC94": "BROKEN_HEART",
37775
+ "\uD83D\uDC4E": "DISLIKE",
37776
+ "\uD83D\uDE0D": "LOVE",
37777
+ "\uD83E\uDD14": "CONFUSED",
37778
+ "\uD83D\uDE09": "WINK",
37779
+ heart: "HEART",
37780
+ like: "LIKE",
37781
+ haha: "HAHA",
37782
+ wow: "WOW",
37783
+ cry: "CRY",
37784
+ angry: "ANGRY"
37785
+ };
37786
+
37630
37787
  class ZaloChannelAdapter extends BaseChannel {
37631
37788
  name = "zalo";
37632
37789
  api;
37633
37790
  ownId;
37634
37791
  config;
37792
+ threadTypeCache = new Map;
37793
+ messageCache = new Map;
37794
+ sendQueue = Promise.resolve();
37635
37795
  constructor(config = {}) {
37636
37796
  super();
37637
- this.config = config;
37797
+ this.config = {
37798
+ minDelayMs: 300,
37799
+ maxDelayMs: 800,
37800
+ cacheLimit: 1000,
37801
+ ...config
37802
+ };
37638
37803
  if (config.api) {
37639
37804
  this.api = config.api;
37640
37805
  }
@@ -37669,17 +37834,101 @@ class ZaloChannelAdapter extends BaseChannel {
37669
37834
  if (!this.api?.listener?.on)
37670
37835
  return;
37671
37836
  this.api.listener.on("message", (raw) => {
37837
+ this.recordInbound(raw);
37672
37838
  const unified = this.normalizeMessage(raw);
37673
37839
  if (unified) {
37674
37840
  this.emit("message", unified);
37675
37841
  }
37676
37842
  });
37843
+ const onSessionDrop = (err) => {
37844
+ this.emit("session:expired", {
37845
+ reason: "SESSION_EXPIRED_OR_DROPPED",
37846
+ raw: err,
37847
+ requiresQrScan: true
37848
+ });
37849
+ this.setConnected(false);
37850
+ };
37851
+ this.api.listener.on("closed", onSessionDrop);
37852
+ this.api.listener.on("error", (err) => {
37853
+ const msg = String(err?.message || err);
37854
+ if (msg.includes("1002") || msg.includes("session") || msg.includes("auth")) {
37855
+ onSessionDrop(err);
37856
+ }
37857
+ });
37677
37858
  if (this.api.listener.start) {
37678
37859
  try {
37679
37860
  this.api.listener.start();
37680
37861
  } catch {}
37681
37862
  }
37682
37863
  }
37864
+ recordInbound(raw) {
37865
+ if (!raw)
37866
+ return;
37867
+ const data = raw.data || raw;
37868
+ const isGroup = raw.type === 1 || raw.type === "group" || Boolean(raw.isGroup);
37869
+ const chatId = String(raw.threadId || data.idTo || data.threadId || "");
37870
+ const msgId = String(data.msgId || raw.msgId || "");
37871
+ const cliMsgId = data.cliMsgId ? String(data.cliMsgId) : undefined;
37872
+ const uidFrom = String(data.uidFrom || raw.senderId || "");
37873
+ const content = typeof data.content === "string" ? data.content : data.msg || "";
37874
+ if (chatId) {
37875
+ this.threadTypeCache.set(chatId, isGroup ? 1 : 0);
37876
+ }
37877
+ const cached = {
37878
+ msgId,
37879
+ cliMsgId,
37880
+ uidFrom,
37881
+ content,
37882
+ threadId: chatId,
37883
+ isGroup
37884
+ };
37885
+ const limit = this.config.cacheLimit || 1000;
37886
+ if (this.messageCache.size >= limit) {
37887
+ const firstKey = this.messageCache.keys().next().value;
37888
+ if (firstKey)
37889
+ this.messageCache.delete(firstKey);
37890
+ }
37891
+ if (msgId)
37892
+ this.messageCache.set(msgId, cached);
37893
+ if (cliMsgId)
37894
+ this.messageCache.set(cliMsgId, cached);
37895
+ }
37896
+ resolveThreadType(chatId) {
37897
+ if (this.threadTypeCache.has(chatId)) {
37898
+ return this.threadTypeCache.get(chatId);
37899
+ }
37900
+ return this.config.defaultIsGroup ?? true ? 1 : 0;
37901
+ }
37902
+ resolveQuote(replyToId) {
37903
+ if (!replyToId)
37904
+ return;
37905
+ const cached = this.messageCache.get(replyToId);
37906
+ if (cached) {
37907
+ return {
37908
+ msgId: cached.msgId,
37909
+ cliMsgId: cached.cliMsgId,
37910
+ uidFrom: cached.uidFrom,
37911
+ content: cached.content
37912
+ };
37913
+ }
37914
+ return replyToId;
37915
+ }
37916
+ async enqueueSend(operation) {
37917
+ const minDelay = this.config.minDelayMs ?? 300;
37918
+ const maxDelay = this.config.maxDelayMs ?? 800;
37919
+ const execute = async () => {
37920
+ if (maxDelay > 0) {
37921
+ const jitter = Math.floor(minDelay + Math.random() * Math.max(0, maxDelay - minDelay));
37922
+ if (jitter > 0) {
37923
+ await new Promise((r) => setTimeout(r, jitter));
37924
+ }
37925
+ }
37926
+ return await operation();
37927
+ };
37928
+ const next = this.sendQueue.then(execute, execute);
37929
+ this.sendQueue = next.catch(() => {});
37930
+ return next;
37931
+ }
37683
37932
  normalizeMessage(raw) {
37684
37933
  if (!raw)
37685
37934
  return null;
@@ -37713,41 +37962,60 @@ class ZaloChannelAdapter extends BaseChannel {
37713
37962
  async sendText(chatId, text, options) {
37714
37963
  if (!this.api)
37715
37964
  throw new Error("Zalo adapter is not connected.");
37716
- const isGroup = this.config.defaultIsGroup ?? true;
37717
- const threadType = isGroup ? 1 : 0;
37718
- const payload = { msg: text, quote: options?.replyToId };
37719
- const res = await this.api.sendMessage(payload, chatId, threadType);
37720
- const resMsgId = res?.message?.msgId || res?.msgId || `z-${Date.now()}`;
37721
- return {
37722
- messageId: String(resMsgId),
37723
- chatId,
37724
- timestamp: Date.now()
37725
- };
37965
+ const threadType = this.resolveThreadType(chatId);
37966
+ const quote = this.resolveQuote(options?.replyToId);
37967
+ const payload = { msg: text, quote };
37968
+ return await this.enqueueSend(async () => {
37969
+ const res = await this.api.sendMessage(payload, chatId, threadType);
37970
+ const resMsgId = res?.message?.msgId || res?.msgId || `z-${Date.now()}`;
37971
+ return {
37972
+ messageId: String(resMsgId),
37973
+ chatId,
37974
+ timestamp: Date.now()
37975
+ };
37976
+ });
37726
37977
  }
37727
- async sendMedia(chatId, media, _options) {
37978
+ async sendMedia(chatId, media, options) {
37728
37979
  if (!this.api)
37729
37980
  throw new Error("Zalo adapter is not connected.");
37730
- const isGroup = this.config.defaultIsGroup ?? true;
37731
- const threadType = isGroup ? 1 : 0;
37732
- let res;
37733
- if (media.type === "image") {
37734
- res = await this.api.sendMessage({ msg: media.caption || "", attachments: [media.source] }, chatId, threadType);
37735
- } else if (media.type === "video" && this.api.sendVideo) {
37736
- res = await this.api.sendVideo({ video: media.source, msg: media.caption || "" }, chatId, threadType);
37737
- } else {
37738
- res = await this.api.sendMessage({ msg: media.caption || "", attachments: [media.source] }, chatId, threadType);
37739
- }
37740
- const resMsgId = res?.message?.msgId || res?.msgId || `z-${Date.now()}`;
37741
- return {
37742
- messageId: String(resMsgId),
37743
- chatId,
37744
- timestamp: Date.now()
37745
- };
37981
+ const threadType = this.resolveThreadType(chatId);
37982
+ const quote = this.resolveQuote(options?.replyToId);
37983
+ return await this.enqueueSend(async () => {
37984
+ let res;
37985
+ if (media.type === "image") {
37986
+ res = await this.api.sendMessage({ msg: media.caption || "", attachments: [media.source], quote }, chatId, threadType);
37987
+ } else if (media.type === "video" && this.api.sendVideo) {
37988
+ res = await this.api.sendVideo({ video: media.source, msg: media.caption || "", quote }, chatId, threadType);
37989
+ } else {
37990
+ res = await this.api.sendMessage({ msg: media.caption || "", attachments: [media.source], quote }, chatId, threadType);
37991
+ }
37992
+ const resMsgId = res?.message?.msgId || res?.msgId || `z-${Date.now()}`;
37993
+ return {
37994
+ messageId: String(resMsgId),
37995
+ chatId,
37996
+ timestamp: Date.now()
37997
+ };
37998
+ });
37746
37999
  }
37747
38000
  async addReaction(chatId, messageId, emoji) {
37748
- if (this.api?.addReaction) {
37749
- await this.api.addReaction(chatId, messageId, emoji);
37750
- }
38001
+ if (!this.api?.addReaction)
38002
+ return;
38003
+ const threadType = this.resolveThreadType(chatId);
38004
+ const isGroup = threadType === 1;
38005
+ const reactionCode = EMOJI_TO_ZALO[emoji] || emoji;
38006
+ const cached = this.messageCache.get(messageId);
38007
+ const cliMsgId = cached?.cliMsgId || messageId;
38008
+ await this.enqueueSend(async () => {
38009
+ await this.api.addReaction(chatId, messageId, cliMsgId, reactionCode, threadType);
38010
+ });
38011
+ }
38012
+ async sendTyping(chatId) {
38013
+ if (!this.api?.sendTypingEvent)
38014
+ return;
38015
+ const threadType = this.resolveThreadType(chatId);
38016
+ try {
38017
+ await this.api.sendTypingEvent(chatId, true, threadType);
38018
+ } catch {}
37751
38019
  }
37752
38020
  }
37753
38021
  // src/personal/client.ts
@@ -38625,6 +38893,24 @@ class TelegramChannelAdapter extends BaseChannel {
38625
38893
  reaction: [{ type: "emoji", emoji }]
38626
38894
  });
38627
38895
  }
38896
+ async sendTyping(chatId) {
38897
+ await this.callApi("sendChatAction", {
38898
+ chat_id: chatId,
38899
+ action: "typing"
38900
+ });
38901
+ }
38902
+ async editText(chatId, messageId, text) {
38903
+ const res = await this.callApi("editMessageText", {
38904
+ chat_id: chatId,
38905
+ message_id: Number(messageId),
38906
+ text
38907
+ });
38908
+ return {
38909
+ messageId: String(res.message_id || messageId),
38910
+ chatId,
38911
+ timestamp: Date.now()
38912
+ };
38913
+ }
38628
38914
  }
38629
38915
  // src/channels/discord/adapter.ts
38630
38916
  class DiscordChannelAdapter extends BaseChannel {
@@ -38735,6 +39021,19 @@ class DiscordChannelAdapter extends BaseChannel {
38735
39021
  const encoded = encodeURIComponent(emoji);
38736
39022
  await this.callApi("PUT", `/channels/${chatId}/messages/${messageId}/reactions/${encoded}/@me`);
38737
39023
  }
39024
+ async sendTyping(chatId) {
39025
+ await this.callApi("POST", `/channels/${chatId}/typing`, {});
39026
+ }
39027
+ async editText(chatId, messageId, text) {
39028
+ const res = await this.callApi("PATCH", `/channels/${chatId}/messages/${messageId}`, {
39029
+ content: text
39030
+ });
39031
+ return {
39032
+ messageId: String(res.id || messageId),
39033
+ chatId,
39034
+ timestamp: Date.now()
39035
+ };
39036
+ }
38738
39037
  }
38739
39038
  // src/channels/slack/adapter.ts
38740
39039
  class SlackChannelAdapter extends BaseChannel {
@@ -38837,6 +39136,19 @@ class SlackChannelAdapter extends BaseChannel {
38837
39136
  name: cleanName
38838
39137
  });
38839
39138
  }
39139
+ async sendTyping(chatId) {}
39140
+ async editText(chatId, messageId, text) {
39141
+ const res = await this.callApi("chat.update", {
39142
+ channel: chatId,
39143
+ ts: messageId,
39144
+ text
39145
+ });
39146
+ return {
39147
+ messageId: String(res.ts || messageId),
39148
+ chatId,
39149
+ timestamp: Date.now()
39150
+ };
39151
+ }
38840
39152
  }
38841
39153
  // src/bridges/mcp/index.ts
38842
39154
  function getChannelHubMcpTools() {