@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.
- package/dist/index.js +207 -20
- 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
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1796
|
+
await sleep3(sleepMs);
|
|
1610
1797
|
}
|
|
1611
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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",
|