@nextclaw/channel-runtime 0.1.13 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +207 -20
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -234,6 +234,8 @@ var TYPING_HEARTBEAT_MS = 6e3;
234
234
  var TYPING_AUTO_STOP_MS = 12e4;
235
235
  var DISCORD_TEXT_LIMIT = 2e3;
236
236
  var DISCORD_MAX_LINES_PER_MESSAGE = 17;
237
+ var STREAM_EDIT_MIN_INTERVAL_MS = 600;
238
+ var STREAM_MAX_UPDATES_PER_MESSAGE = 40;
237
239
  var FENCE_RE = /^( {0,3})(`{3,}|~{3,})(.*)$/;
238
240
  var DiscordChannel = class extends BaseChannel {
239
241
  name = "discord";
@@ -303,23 +305,34 @@ var DiscordChannel = class extends BaseChannel {
303
305
  }
304
306
  this.stopTyping(msg.chatId);
305
307
  const textChannel = channel;
306
- const chunks = chunkDiscordText(msg.content ?? "");
308
+ const content = msg.content ?? "";
309
+ const textChunkLimit = resolveTextChunkLimit(this.config);
310
+ const chunks = chunkDiscordText(content, {
311
+ maxChars: textChunkLimit,
312
+ maxLines: DISCORD_MAX_LINES_PER_MESSAGE
313
+ });
307
314
  if (chunks.length === 0) {
308
315
  return;
309
316
  }
310
317
  const flags = msg.metadata?.silent === true ? MessageFlags.SuppressNotifications : void 0;
311
- for (const chunk of chunks) {
312
- const payload = {
313
- content: chunk
314
- };
315
- if (msg.replyTo) {
316
- payload.reply = { messageReference: msg.replyTo };
317
- }
318
- if (flags !== void 0) {
319
- payload.flags = flags;
320
- }
321
- await textChannel.send(payload);
318
+ const streamingMode = resolveDiscordStreamingMode(this.config);
319
+ if (streamingMode === "off") {
320
+ await sendDiscordChunks({
321
+ textChannel,
322
+ chunks,
323
+ replyTo: msg.replyTo ?? void 0,
324
+ flags
325
+ });
326
+ return;
322
327
  }
328
+ await sendDiscordDraftStreaming({
329
+ textChannel,
330
+ chunks,
331
+ replyTo: msg.replyTo ?? void 0,
332
+ flags,
333
+ draftChunk: resolveDraftChunkConfig(this.config, textChunkLimit),
334
+ streamingMode
335
+ });
323
336
  }
324
337
  async handleIncoming(message) {
325
338
  const selfUserId = this.client?.user?.id;
@@ -780,12 +793,186 @@ function chunkDiscordText(text, opts = {}) {
780
793
  }
781
794
  return chunks;
782
795
  }
796
+ function clampInt(value, min, max) {
797
+ if (Number.isNaN(value)) {
798
+ return min;
799
+ }
800
+ return Math.min(max, Math.max(min, Math.floor(value)));
801
+ }
802
+ function resolveTextChunkLimit(config) {
803
+ const configured = typeof config.textChunkLimit === "number" ? config.textChunkLimit : DISCORD_TEXT_LIMIT;
804
+ return clampInt(configured, 1, DISCORD_TEXT_LIMIT);
805
+ }
806
+ function resolveDiscordStreamingMode(config) {
807
+ const raw = config.streaming;
808
+ if (raw === true) {
809
+ return "partial";
810
+ }
811
+ if (raw === false || raw === void 0 || raw === null) {
812
+ return "off";
813
+ }
814
+ if (raw === "progress") {
815
+ return "partial";
816
+ }
817
+ if (raw === "partial" || raw === "block" || raw === "off") {
818
+ return raw;
819
+ }
820
+ return "off";
821
+ }
822
+ function resolveDraftChunkConfig(config, textChunkLimit) {
823
+ const raw = config.draftChunk ?? {};
824
+ const minChars = clampInt(raw.minChars ?? 200, 1, textChunkLimit);
825
+ const maxChars = clampInt(raw.maxChars ?? 800, minChars, textChunkLimit);
826
+ const breakPreference = raw.breakPreference === "line" || raw.breakPreference === "none" ? raw.breakPreference : "paragraph";
827
+ return {
828
+ minChars,
829
+ maxChars,
830
+ breakPreference
831
+ };
832
+ }
833
+ function findDraftBreakIndex(text, start, end, preference) {
834
+ const slice = text.slice(start, end);
835
+ if (slice.length === 0) {
836
+ return null;
837
+ }
838
+ if (preference === "paragraph") {
839
+ const idx = slice.lastIndexOf("\n\n");
840
+ if (idx >= 0) {
841
+ return start + idx + 2;
842
+ }
843
+ }
844
+ if (preference === "paragraph" || preference === "line") {
845
+ const idx = slice.lastIndexOf("\n");
846
+ if (idx >= 0) {
847
+ return start + idx + 1;
848
+ }
849
+ }
850
+ for (let i = slice.length - 1; i >= 0; i -= 1) {
851
+ if (/\s/.test(slice[i])) {
852
+ return start + i + 1;
853
+ }
854
+ }
855
+ return null;
856
+ }
857
+ function splitDraftChunks(text, config) {
858
+ const chunks = [];
859
+ if (!text) {
860
+ return chunks;
861
+ }
862
+ let cursor = 0;
863
+ const length = text.length;
864
+ while (cursor < length) {
865
+ const remaining = length - cursor;
866
+ if (remaining <= config.maxChars) {
867
+ chunks.push(text.slice(cursor));
868
+ break;
869
+ }
870
+ const minEnd = Math.min(length, cursor + config.minChars);
871
+ const maxEnd = Math.min(length, cursor + config.maxChars);
872
+ let nextEnd = maxEnd;
873
+ const breakIndex = findDraftBreakIndex(text, minEnd, maxEnd, config.breakPreference);
874
+ if (breakIndex !== null && breakIndex > cursor) {
875
+ nextEnd = breakIndex;
876
+ }
877
+ if (nextEnd <= cursor) {
878
+ nextEnd = maxEnd;
879
+ }
880
+ chunks.push(text.slice(cursor, nextEnd));
881
+ cursor = nextEnd;
882
+ }
883
+ return chunks;
884
+ }
885
+ async function sleep(ms) {
886
+ await new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
887
+ }
888
+ async function sendDiscordChunks(params) {
889
+ const { textChannel, chunks, replyTo, flags } = params;
890
+ let first = true;
891
+ for (const chunk of chunks) {
892
+ const payload = {
893
+ content: chunk
894
+ };
895
+ if (first && replyTo) {
896
+ payload.reply = { messageReference: replyTo };
897
+ }
898
+ if (flags !== void 0) {
899
+ payload.flags = flags;
900
+ }
901
+ await textChannel.send(payload);
902
+ first = false;
903
+ }
904
+ }
905
+ async function sendDiscordDraftStreaming(params) {
906
+ const { textChannel, chunks, replyTo, flags, draftChunk, streamingMode } = params;
907
+ let first = true;
908
+ const effectiveDraftChunk = streamingMode === "block" ? draftChunk : {
909
+ ...draftChunk,
910
+ minChars: Math.max(1, Math.floor(draftChunk.minChars / 2)),
911
+ maxChars: Math.max(draftChunk.minChars, Math.floor(draftChunk.maxChars / 2))
912
+ };
913
+ for (const chunk of chunks) {
914
+ const draftChunks = splitDraftChunks(chunk, effectiveDraftChunk);
915
+ if (draftChunks.length === 0) {
916
+ continue;
917
+ }
918
+ if (draftChunks.length > STREAM_MAX_UPDATES_PER_MESSAGE) {
919
+ await sendDiscordChunks({
920
+ textChannel,
921
+ chunks: [chunk],
922
+ replyTo: first ? replyTo : void 0,
923
+ flags
924
+ });
925
+ first = false;
926
+ continue;
927
+ }
928
+ let draftMessage = null;
929
+ let current = "";
930
+ let lastEditAt = 0;
931
+ for (const draftPart of draftChunks) {
932
+ current += draftPart;
933
+ if (!draftMessage) {
934
+ const payload = {
935
+ content: current
936
+ };
937
+ if (first && replyTo) {
938
+ payload.reply = { messageReference: replyTo };
939
+ }
940
+ if (flags !== void 0) {
941
+ payload.flags = flags;
942
+ }
943
+ draftMessage = await textChannel.send(
944
+ payload
945
+ );
946
+ first = false;
947
+ lastEditAt = Date.now();
948
+ continue;
949
+ }
950
+ const waitMs = Math.max(0, lastEditAt + STREAM_EDIT_MIN_INTERVAL_MS - Date.now());
951
+ if (waitMs > 0) {
952
+ await sleep(waitMs);
953
+ }
954
+ try {
955
+ await draftMessage.edit({ content: current });
956
+ } catch {
957
+ await sendDiscordChunks({
958
+ textChannel,
959
+ chunks: [chunk],
960
+ replyTo: void 0,
961
+ flags
962
+ });
963
+ draftMessage = null;
964
+ break;
965
+ }
966
+ lastEditAt = Date.now();
967
+ }
968
+ }
969
+ }
783
970
 
784
971
  // src/channels/email.ts
785
972
  import { ImapFlow } from "imapflow";
786
973
  import { simpleParser } from "mailparser";
787
974
  import nodemailer from "nodemailer";
788
- var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
975
+ var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
789
976
  var EmailChannel = class extends BaseChannel {
790
977
  name = "email";
791
978
  lastSubjectByChat = /* @__PURE__ */ new Map();
@@ -824,7 +1011,7 @@ var EmailChannel = class extends BaseChannel {
824
1011
  }
825
1012
  } catch {
826
1013
  }
827
- await sleep(pollSeconds * 1e3);
1014
+ await sleep2(pollSeconds * 1e3);
828
1015
  }
829
1016
  }
830
1017
  async stop() {
@@ -1577,7 +1764,7 @@ var MochatChannel = class extends BaseChannel {
1577
1764
  });
1578
1765
  await this.handleWatchPayload(payload, "session");
1579
1766
  } catch {
1580
- await sleep2(Math.max(100, this.config.retryDelayMs));
1767
+ await sleep3(Math.max(100, this.config.retryDelayMs));
1581
1768
  }
1582
1769
  }
1583
1770
  }
@@ -1606,9 +1793,9 @@ var MochatChannel = class extends BaseChannel {
1606
1793
  }
1607
1794
  }
1608
1795
  } catch {
1609
- await sleep2(sleepMs);
1796
+ await sleep3(sleepMs);
1610
1797
  }
1611
- await sleep2(sleepMs);
1798
+ await sleep3(sleepMs);
1612
1799
  }
1613
1800
  }
1614
1801
  async handleWatchPayload(payload, targetKind) {
@@ -2088,7 +2275,7 @@ function readGroupId(metadata) {
2088
2275
  }
2089
2276
  return null;
2090
2277
  }
2091
- function sleep2(ms) {
2278
+ function sleep3(ms) {
2092
2279
  return new Promise((resolve) => setTimeout(resolve, ms));
2093
2280
  }
2094
2281
 
@@ -3172,7 +3359,7 @@ var WhatsAppChannel = class extends BaseChannel {
3172
3359
  if (!this.running) {
3173
3360
  break;
3174
3361
  }
3175
- await sleep3(5e3);
3362
+ await sleep4(5e3);
3176
3363
  }
3177
3364
  }
3178
3365
  }
@@ -3242,7 +3429,7 @@ var WhatsAppChannel = class extends BaseChannel {
3242
3429
  }
3243
3430
  }
3244
3431
  };
3245
- function sleep3(ms) {
3432
+ function sleep4(ms) {
3246
3433
  return new Promise((resolve) => setTimeout(resolve, ms));
3247
3434
  }
3248
3435
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/channel-runtime",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
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.27",
18
+ "@nextclaw/core": "^0.6.30",
19
19
  "@slack/socket-mode": "^1.3.3",
20
20
  "@slack/web-api": "^7.6.0",
21
21
  "dingtalk-stream": "^2.1.4",