@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.js CHANGED
@@ -20519,9 +20519,9 @@ var require_websocket_server = __commonJS(function(exports, module) {
20519
20519
  });
20520
20520
 
20521
20521
  // node_modules/ws/wrapper.mjs
20522
- var import_stream, import_extension, import_permessage_deflate, import_receiver, import_sender, import_subprotocol, import_websocket, import_websocket_server, wrapper_default;
20522
+ var import_stream2, import_extension, import_permessage_deflate, import_receiver, import_sender, import_subprotocol, import_websocket, import_websocket_server, wrapper_default;
20523
20523
  var init_wrapper = __esm(() => {
20524
- import_stream = __toESM(require_stream(), 1);
20524
+ import_stream2 = __toESM(require_stream(), 1);
20525
20525
  import_extension = __toESM(require_extension(), 1);
20526
20526
  import_permessage_deflate = __toESM(require_permessage_deflate(), 1);
20527
20527
  import_receiver = __toESM(require_receiver(), 1);
@@ -37499,6 +37499,126 @@ class ChannelEventBus extends EventEmitter2 {
37499
37499
  return this.emit("status", status);
37500
37500
  }
37501
37501
  }
37502
+ // src/core/stream.ts
37503
+ class SmartStreamer {
37504
+ adapter;
37505
+ options;
37506
+ constructor(adapter, options = {}) {
37507
+ this.adapter = adapter;
37508
+ this.options = {
37509
+ editDebounceMs: 1000,
37510
+ typingIntervalMs: 4000,
37511
+ chunkMode: "sentence",
37512
+ minSentenceLength: 60,
37513
+ initialPlaceholder: "...",
37514
+ ...options
37515
+ };
37516
+ }
37517
+ async stream(chatId, tokenStream, sendOptions) {
37518
+ let typingActive = true;
37519
+ const triggerTyping = async () => {
37520
+ if (this.adapter.sendTyping) {
37521
+ try {
37522
+ await this.adapter.sendTyping(chatId);
37523
+ } catch {}
37524
+ }
37525
+ };
37526
+ await triggerTyping();
37527
+ const typingTimer = setInterval(() => {
37528
+ if (typingActive)
37529
+ triggerTyping();
37530
+ }, this.options.typingIntervalMs);
37531
+ try {
37532
+ if (typeof this.adapter.editText === "function") {
37533
+ return await this.streamWithEdit(chatId, tokenStream, sendOptions);
37534
+ } else {
37535
+ return await this.streamWithoutEdit(chatId, tokenStream, sendOptions);
37536
+ }
37537
+ } finally {
37538
+ typingActive = false;
37539
+ clearInterval(typingTimer);
37540
+ }
37541
+ }
37542
+ async streamWithEdit(chatId, tokenStream, sendOptions) {
37543
+ let accumulated = "";
37544
+ let sentMsg = null;
37545
+ let lastEditTime = 0;
37546
+ let pendingEditTimeout = null;
37547
+ const performEdit = async (text) => {
37548
+ if (sentMsg && this.adapter.editText) {
37549
+ await this.adapter.editText(chatId, sentMsg.messageId, text);
37550
+ lastEditTime = Date.now();
37551
+ }
37552
+ };
37553
+ for await (const chunk of tokenStream) {
37554
+ accumulated += chunk;
37555
+ if (!sentMsg) {
37556
+ sentMsg = await this.adapter.sendText(chatId, accumulated.trim() || this.options.initialPlaceholder, sendOptions);
37557
+ lastEditTime = Date.now();
37558
+ continue;
37559
+ }
37560
+ const now = Date.now();
37561
+ const elapsed = now - lastEditTime;
37562
+ if (elapsed >= this.options.editDebounceMs) {
37563
+ if (pendingEditTimeout) {
37564
+ clearTimeout(pendingEditTimeout);
37565
+ pendingEditTimeout = null;
37566
+ }
37567
+ await performEdit(accumulated);
37568
+ } else if (!pendingEditTimeout) {
37569
+ pendingEditTimeout = setTimeout(async () => {
37570
+ pendingEditTimeout = null;
37571
+ await performEdit(accumulated);
37572
+ }, this.options.editDebounceMs - elapsed);
37573
+ }
37574
+ }
37575
+ if (pendingEditTimeout) {
37576
+ clearTimeout(pendingEditTimeout);
37577
+ pendingEditTimeout = null;
37578
+ }
37579
+ if (sentMsg && accumulated) {
37580
+ await performEdit(accumulated);
37581
+ return [sentMsg];
37582
+ } else if (!sentMsg && accumulated) {
37583
+ const res = await this.adapter.sendText(chatId, accumulated, sendOptions);
37584
+ return [res];
37585
+ }
37586
+ return sentMsg ? [sentMsg] : [];
37587
+ }
37588
+ async streamWithoutEdit(chatId, tokenStream, sendOptions) {
37589
+ const results = [];
37590
+ if (this.options.chunkMode === "accumulate") {
37591
+ let accumulated = "";
37592
+ for await (const chunk of tokenStream) {
37593
+ accumulated += chunk;
37594
+ }
37595
+ if (accumulated.trim()) {
37596
+ const res = await this.adapter.sendText(chatId, accumulated, sendOptions);
37597
+ results.push(res);
37598
+ }
37599
+ return results;
37600
+ }
37601
+ let buffer = "";
37602
+ const sentenceEndRegex = /[.?!;\n]\s*$/;
37603
+ for await (const chunk of tokenStream) {
37604
+ buffer += chunk;
37605
+ if (buffer.length >= this.options.minSentenceLength && sentenceEndRegex.test(buffer.trimEnd())) {
37606
+ const textToSend = buffer.trim();
37607
+ if (textToSend) {
37608
+ const res = await this.adapter.sendText(chatId, textToSend, sendOptions);
37609
+ results.push(res);
37610
+ buffer = "";
37611
+ }
37612
+ }
37613
+ }
37614
+ if (buffer.trim()) {
37615
+ const res = await this.adapter.sendText(chatId, buffer.trim(), sendOptions);
37616
+ results.push(res);
37617
+ }
37618
+ return results;
37619
+ }
37620
+ }
37621
+
37502
37622
  // src/core/context.ts
37503
37623
  function createMessageContext(message, channel) {
37504
37624
  return {
@@ -37516,6 +37636,17 @@ function createMessageContext(message, channel) {
37516
37636
  if (channel.addReaction) {
37517
37637
  await channel.addReaction(message.chat.id, message.id, emoji);
37518
37638
  }
37639
+ },
37640
+ sendTyping: async () => {
37641
+ if (channel.sendTyping) {
37642
+ await channel.sendTyping(message.chat.id);
37643
+ }
37644
+ },
37645
+ stream: async (tokenStream, options) => {
37646
+ const streamer = new SmartStreamer(channel, options);
37647
+ return await streamer.stream(message.chat.id, tokenStream, {
37648
+ replyToId: message.id
37649
+ });
37519
37650
  }
37520
37651
  };
37521
37652
  }
@@ -37563,14 +37694,47 @@ class ChannelHub {
37563
37694
  }
37564
37695
  }
37565
37696
  // src/channels/zalo/adapter.ts
37697
+ var EMOJI_TO_ZALO = {
37698
+ "❤️": "HEART",
37699
+ "\uD83D\uDC96": "HEART",
37700
+ "\uD83D\uDC4D": "LIKE",
37701
+ "\uD83D\uDE06": "HAHA",
37702
+ "\uD83D\uDE02": "TEARS_OF_JOY",
37703
+ "\uD83D\uDE2E": "WOW",
37704
+ "\uD83D\uDE2D": "CRY",
37705
+ "\uD83D\uDE21": "ANGRY",
37706
+ "\uD83D\uDE18": "KISS",
37707
+ "\uD83D\uDCA9": "SHIT",
37708
+ "\uD83C\uDF39": "ROSE",
37709
+ "\uD83D\uDC94": "BROKEN_HEART",
37710
+ "\uD83D\uDC4E": "DISLIKE",
37711
+ "\uD83D\uDE0D": "LOVE",
37712
+ "\uD83E\uDD14": "CONFUSED",
37713
+ "\uD83D\uDE09": "WINK",
37714
+ heart: "HEART",
37715
+ like: "LIKE",
37716
+ haha: "HAHA",
37717
+ wow: "WOW",
37718
+ cry: "CRY",
37719
+ angry: "ANGRY"
37720
+ };
37721
+
37566
37722
  class ZaloChannelAdapter extends BaseChannel {
37567
37723
  name = "zalo";
37568
37724
  api;
37569
37725
  ownId;
37570
37726
  config;
37727
+ threadTypeCache = new Map;
37728
+ messageCache = new Map;
37729
+ sendQueue = Promise.resolve();
37571
37730
  constructor(config = {}) {
37572
37731
  super();
37573
- this.config = config;
37732
+ this.config = {
37733
+ minDelayMs: 300,
37734
+ maxDelayMs: 800,
37735
+ cacheLimit: 1000,
37736
+ ...config
37737
+ };
37574
37738
  if (config.api) {
37575
37739
  this.api = config.api;
37576
37740
  }
@@ -37605,17 +37769,101 @@ class ZaloChannelAdapter extends BaseChannel {
37605
37769
  if (!this.api?.listener?.on)
37606
37770
  return;
37607
37771
  this.api.listener.on("message", (raw) => {
37772
+ this.recordInbound(raw);
37608
37773
  const unified = this.normalizeMessage(raw);
37609
37774
  if (unified) {
37610
37775
  this.emit("message", unified);
37611
37776
  }
37612
37777
  });
37778
+ const onSessionDrop = (err) => {
37779
+ this.emit("session:expired", {
37780
+ reason: "SESSION_EXPIRED_OR_DROPPED",
37781
+ raw: err,
37782
+ requiresQrScan: true
37783
+ });
37784
+ this.setConnected(false);
37785
+ };
37786
+ this.api.listener.on("closed", onSessionDrop);
37787
+ this.api.listener.on("error", (err) => {
37788
+ const msg = String(err?.message || err);
37789
+ if (msg.includes("1002") || msg.includes("session") || msg.includes("auth")) {
37790
+ onSessionDrop(err);
37791
+ }
37792
+ });
37613
37793
  if (this.api.listener.start) {
37614
37794
  try {
37615
37795
  this.api.listener.start();
37616
37796
  } catch {}
37617
37797
  }
37618
37798
  }
37799
+ recordInbound(raw) {
37800
+ if (!raw)
37801
+ return;
37802
+ const data = raw.data || raw;
37803
+ const isGroup = raw.type === 1 || raw.type === "group" || Boolean(raw.isGroup);
37804
+ const chatId = String(raw.threadId || data.idTo || data.threadId || "");
37805
+ const msgId = String(data.msgId || raw.msgId || "");
37806
+ const cliMsgId = data.cliMsgId ? String(data.cliMsgId) : undefined;
37807
+ const uidFrom = String(data.uidFrom || raw.senderId || "");
37808
+ const content = typeof data.content === "string" ? data.content : data.msg || "";
37809
+ if (chatId) {
37810
+ this.threadTypeCache.set(chatId, isGroup ? 1 : 0);
37811
+ }
37812
+ const cached = {
37813
+ msgId,
37814
+ cliMsgId,
37815
+ uidFrom,
37816
+ content,
37817
+ threadId: chatId,
37818
+ isGroup
37819
+ };
37820
+ const limit = this.config.cacheLimit || 1000;
37821
+ if (this.messageCache.size >= limit) {
37822
+ const firstKey = this.messageCache.keys().next().value;
37823
+ if (firstKey)
37824
+ this.messageCache.delete(firstKey);
37825
+ }
37826
+ if (msgId)
37827
+ this.messageCache.set(msgId, cached);
37828
+ if (cliMsgId)
37829
+ this.messageCache.set(cliMsgId, cached);
37830
+ }
37831
+ resolveThreadType(chatId) {
37832
+ if (this.threadTypeCache.has(chatId)) {
37833
+ return this.threadTypeCache.get(chatId);
37834
+ }
37835
+ return this.config.defaultIsGroup ?? true ? 1 : 0;
37836
+ }
37837
+ resolveQuote(replyToId) {
37838
+ if (!replyToId)
37839
+ return;
37840
+ const cached = this.messageCache.get(replyToId);
37841
+ if (cached) {
37842
+ return {
37843
+ msgId: cached.msgId,
37844
+ cliMsgId: cached.cliMsgId,
37845
+ uidFrom: cached.uidFrom,
37846
+ content: cached.content
37847
+ };
37848
+ }
37849
+ return replyToId;
37850
+ }
37851
+ async enqueueSend(operation) {
37852
+ const minDelay = this.config.minDelayMs ?? 300;
37853
+ const maxDelay = this.config.maxDelayMs ?? 800;
37854
+ const execute = async () => {
37855
+ if (maxDelay > 0) {
37856
+ const jitter = Math.floor(minDelay + Math.random() * Math.max(0, maxDelay - minDelay));
37857
+ if (jitter > 0) {
37858
+ await new Promise((r) => setTimeout(r, jitter));
37859
+ }
37860
+ }
37861
+ return await operation();
37862
+ };
37863
+ const next = this.sendQueue.then(execute, execute);
37864
+ this.sendQueue = next.catch(() => {});
37865
+ return next;
37866
+ }
37619
37867
  normalizeMessage(raw) {
37620
37868
  if (!raw)
37621
37869
  return null;
@@ -37649,41 +37897,60 @@ class ZaloChannelAdapter extends BaseChannel {
37649
37897
  async sendText(chatId, text, options) {
37650
37898
  if (!this.api)
37651
37899
  throw new Error("Zalo adapter is not connected.");
37652
- const isGroup = this.config.defaultIsGroup ?? true;
37653
- const threadType = isGroup ? 1 : 0;
37654
- const payload = { msg: text, quote: options?.replyToId };
37655
- const res = await this.api.sendMessage(payload, chatId, threadType);
37656
- const resMsgId = res?.message?.msgId || res?.msgId || `z-${Date.now()}`;
37657
- return {
37658
- messageId: String(resMsgId),
37659
- chatId,
37660
- timestamp: Date.now()
37661
- };
37900
+ const threadType = this.resolveThreadType(chatId);
37901
+ const quote = this.resolveQuote(options?.replyToId);
37902
+ const payload = { msg: text, quote };
37903
+ return await this.enqueueSend(async () => {
37904
+ const res = await this.api.sendMessage(payload, chatId, threadType);
37905
+ const resMsgId = res?.message?.msgId || res?.msgId || `z-${Date.now()}`;
37906
+ return {
37907
+ messageId: String(resMsgId),
37908
+ chatId,
37909
+ timestamp: Date.now()
37910
+ };
37911
+ });
37662
37912
  }
37663
- async sendMedia(chatId, media, _options) {
37913
+ async sendMedia(chatId, media, options) {
37664
37914
  if (!this.api)
37665
37915
  throw new Error("Zalo adapter is not connected.");
37666
- const isGroup = this.config.defaultIsGroup ?? true;
37667
- const threadType = isGroup ? 1 : 0;
37668
- let res;
37669
- if (media.type === "image") {
37670
- res = await this.api.sendMessage({ msg: media.caption || "", attachments: [media.source] }, chatId, threadType);
37671
- } else if (media.type === "video" && this.api.sendVideo) {
37672
- res = await this.api.sendVideo({ video: media.source, msg: media.caption || "" }, chatId, threadType);
37673
- } else {
37674
- res = await this.api.sendMessage({ msg: media.caption || "", attachments: [media.source] }, chatId, threadType);
37675
- }
37676
- const resMsgId = res?.message?.msgId || res?.msgId || `z-${Date.now()}`;
37677
- return {
37678
- messageId: String(resMsgId),
37679
- chatId,
37680
- timestamp: Date.now()
37681
- };
37916
+ const threadType = this.resolveThreadType(chatId);
37917
+ const quote = this.resolveQuote(options?.replyToId);
37918
+ return await this.enqueueSend(async () => {
37919
+ let res;
37920
+ if (media.type === "image") {
37921
+ res = await this.api.sendMessage({ msg: media.caption || "", attachments: [media.source], quote }, chatId, threadType);
37922
+ } else if (media.type === "video" && this.api.sendVideo) {
37923
+ res = await this.api.sendVideo({ video: media.source, msg: media.caption || "", quote }, chatId, threadType);
37924
+ } else {
37925
+ res = await this.api.sendMessage({ msg: media.caption || "", attachments: [media.source], quote }, chatId, threadType);
37926
+ }
37927
+ const resMsgId = res?.message?.msgId || res?.msgId || `z-${Date.now()}`;
37928
+ return {
37929
+ messageId: String(resMsgId),
37930
+ chatId,
37931
+ timestamp: Date.now()
37932
+ };
37933
+ });
37682
37934
  }
37683
37935
  async addReaction(chatId, messageId, emoji) {
37684
- if (this.api?.addReaction) {
37685
- await this.api.addReaction(chatId, messageId, emoji);
37686
- }
37936
+ if (!this.api?.addReaction)
37937
+ return;
37938
+ const threadType = this.resolveThreadType(chatId);
37939
+ const isGroup = threadType === 1;
37940
+ const reactionCode = EMOJI_TO_ZALO[emoji] || emoji;
37941
+ const cached = this.messageCache.get(messageId);
37942
+ const cliMsgId = cached?.cliMsgId || messageId;
37943
+ await this.enqueueSend(async () => {
37944
+ await this.api.addReaction(chatId, messageId, cliMsgId, reactionCode, threadType);
37945
+ });
37946
+ }
37947
+ async sendTyping(chatId) {
37948
+ if (!this.api?.sendTypingEvent)
37949
+ return;
37950
+ const threadType = this.resolveThreadType(chatId);
37951
+ try {
37952
+ await this.api.sendTypingEvent(chatId, true, threadType);
37953
+ } catch {}
37687
37954
  }
37688
37955
  }
37689
37956
  // src/personal/client.ts
@@ -38561,6 +38828,24 @@ class TelegramChannelAdapter extends BaseChannel {
38561
38828
  reaction: [{ type: "emoji", emoji }]
38562
38829
  });
38563
38830
  }
38831
+ async sendTyping(chatId) {
38832
+ await this.callApi("sendChatAction", {
38833
+ chat_id: chatId,
38834
+ action: "typing"
38835
+ });
38836
+ }
38837
+ async editText(chatId, messageId, text) {
38838
+ const res = await this.callApi("editMessageText", {
38839
+ chat_id: chatId,
38840
+ message_id: Number(messageId),
38841
+ text
38842
+ });
38843
+ return {
38844
+ messageId: String(res.message_id || messageId),
38845
+ chatId,
38846
+ timestamp: Date.now()
38847
+ };
38848
+ }
38564
38849
  }
38565
38850
  // src/channels/discord/adapter.ts
38566
38851
  class DiscordChannelAdapter extends BaseChannel {
@@ -38671,6 +38956,19 @@ class DiscordChannelAdapter extends BaseChannel {
38671
38956
  const encoded = encodeURIComponent(emoji);
38672
38957
  await this.callApi("PUT", `/channels/${chatId}/messages/${messageId}/reactions/${encoded}/@me`);
38673
38958
  }
38959
+ async sendTyping(chatId) {
38960
+ await this.callApi("POST", `/channels/${chatId}/typing`, {});
38961
+ }
38962
+ async editText(chatId, messageId, text) {
38963
+ const res = await this.callApi("PATCH", `/channels/${chatId}/messages/${messageId}`, {
38964
+ content: text
38965
+ });
38966
+ return {
38967
+ messageId: String(res.id || messageId),
38968
+ chatId,
38969
+ timestamp: Date.now()
38970
+ };
38971
+ }
38674
38972
  }
38675
38973
  // src/channels/slack/adapter.ts
38676
38974
  class SlackChannelAdapter extends BaseChannel {
@@ -38773,6 +39071,19 @@ class SlackChannelAdapter extends BaseChannel {
38773
39071
  name: cleanName
38774
39072
  });
38775
39073
  }
39074
+ async sendTyping(chatId) {}
39075
+ async editText(chatId, messageId, text) {
39076
+ const res = await this.callApi("chat.update", {
39077
+ channel: chatId,
39078
+ ts: messageId,
39079
+ text
39080
+ });
39081
+ return {
39082
+ messageId: String(res.ts || messageId),
39083
+ chatId,
39084
+ timestamp: Date.now()
39085
+ };
39086
+ }
38776
39087
  }
38777
39088
  // src/bridges/mcp/index.ts
38778
39089
  function getChannelHubMcpTools() {
@@ -39177,6 +39488,7 @@ export {
39177
39488
  CommandRouter,
39178
39489
  DiscordChannelAdapter,
39179
39490
  SlackChannelAdapter,
39491
+ SmartStreamer,
39180
39492
  TelegramChannelAdapter,
39181
39493
  WebhookBridge,
39182
39494
  Zalo,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theowlops/channelhub",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "ChannelHub SDK - Unified multi-channel messaging toolkit (Zalo, Telegram, Discord, Slack) for AI agents, standalone bots, and enterprise automation.",
5
5
  "module": "dist/index.js",
6
6
  "main": "dist/index.cjs",