@openclaw/irc 2026.10.2 → 2026.10.5-beta.2
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/.setup/{channel-DP5YY5l3.mjs → channel-BXrFfvr6.mjs} +135 -53
- package/dist/.setup/{channel-plugin-api-tjqFvfGT.mjs → channel-plugin-api-tb7ep4Ml.mjs} +1 -1
- package/dist/.setup/{channel-runtime-BvC0hwF1.mjs → channel-runtime-FNBqToAo.mjs} +58 -37
- package/dist/api.js +1 -1
- package/dist/channel-plugin-api.js +1 -1
- package/dist/setup-entry.js +1 -1
- package/package.json +4 -4
|
@@ -10,7 +10,6 @@ import { defineStableChannelIngressIdentity, identityEntryAuthenticationClassifi
|
|
|
10
10
|
import { buildMutableAllowEntryDetector, collectStandardAllowlistLists, createAllowlistProviderOpenWarningCollector, createConditionalWarningCollector, createDangerousNameMatchingMutableAllowlistWarningCollector, resolveScopeKeyCaseInsensitive, resolveScopeRequireMention, resolveScopeToolsPolicy } from "openclaw/plugin-sdk/channel-policy";
|
|
11
11
|
import { PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk/channel-status";
|
|
12
12
|
import { createChannelDirectoryAdapter, createResolvedDirectoryEntriesLister } from "openclaw/plugin-sdk/directory-runtime";
|
|
13
|
-
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
|
|
14
13
|
import { buildBaseChannelStatusSummary, createComputedAccountStatusAdapter, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
|
|
15
14
|
import { resolveAccountWithDefaultFallback } from "openclaw/plugin-sdk/account-core";
|
|
16
15
|
import { parseTcpPort } from "openclaw/plugin-sdk/number-runtime";
|
|
@@ -19,9 +18,12 @@ import { tryReadSecretFileSync } from "openclaw/plugin-sdk/secret-file-runtime";
|
|
|
19
18
|
import { resolveSecretInputString } from "openclaw/plugin-sdk/secret-input";
|
|
20
19
|
import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeStringEntries, normalizeStringifiedOptionalString, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
21
20
|
import { createAccountStatusSink, createMessageReceiptFromOutboundResults, createReplyToFanout, defineChannelMessageAdapter, runPassiveAccountLifecycle, sanitizeForPlainText } from "openclaw/plugin-sdk/channel-outbound";
|
|
21
|
+
import { containsAsciiControlCharacter as hasIrcControlChars } from "openclaw/plugin-sdk/string-normalization-runtime";
|
|
22
22
|
import { attachChannelToResult } from "openclaw/plugin-sdk/channel-send-result";
|
|
23
23
|
import { chunkTextForOutbound, convertMarkdownTables, sanitizeAssistantVisibleText, stripMarkdown } from "openclaw/plugin-sdk/text-chunking";
|
|
24
24
|
import { randomUUID } from "node:crypto";
|
|
25
|
+
import { createChannelPartialDeliveryError, isChannelPartialDeliveryError } from "openclaw/plugin-sdk/channel-inbound";
|
|
26
|
+
import { captureEffectAuthority } from "openclaw/plugin-sdk/fetch-runtime";
|
|
25
27
|
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
|
|
26
28
|
import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
|
|
27
29
|
import net from "node:net";
|
|
@@ -197,7 +199,6 @@ const collectIrcMutableAllowlistWarnings = createDangerousNameMatchingMutableAll
|
|
|
197
199
|
});
|
|
198
200
|
//#endregion
|
|
199
201
|
//#region extensions/irc/src/gateway.ts
|
|
200
|
-
const loadIrcChannelRuntime$1 = createLazyRuntimeModule(() => import("./channel-runtime-BvC0hwF1.mjs"));
|
|
201
202
|
async function startIrcGatewayAccount(ctx) {
|
|
202
203
|
const account = ctx.account;
|
|
203
204
|
const statusSink = createAccountStatusSink({
|
|
@@ -206,7 +207,7 @@ async function startIrcGatewayAccount(ctx) {
|
|
|
206
207
|
});
|
|
207
208
|
if (!account.configured) throw new Error(`IRC is not configured for account "${account.accountId}" (need host and nick in channels.irc).`);
|
|
208
209
|
ctx.log?.info?.(`[${account.accountId}] starting IRC provider (${account.host}:${account.port}${account.tls ? " tls" : ""})`);
|
|
209
|
-
const { monitorIrcProvider } = await
|
|
210
|
+
const { monitorIrcProvider } = await import("./channel-runtime-FNBqToAo.mjs");
|
|
210
211
|
await runPassiveAccountLifecycle({
|
|
211
212
|
abortSignal: ctx.abortSignal,
|
|
212
213
|
start: async () => await monitorIrcProvider({
|
|
@@ -226,10 +227,6 @@ async function startIrcGatewayAccount(ctx) {
|
|
|
226
227
|
function isIrcControlChar(charCode) {
|
|
227
228
|
return charCode <= 31 || charCode === 127;
|
|
228
229
|
}
|
|
229
|
-
function hasIrcControlChars(value) {
|
|
230
|
-
for (const char of value) if (isIrcControlChar(char.charCodeAt(0))) return true;
|
|
231
|
-
return false;
|
|
232
|
-
}
|
|
233
230
|
function stripIrcControlChars(value) {
|
|
234
231
|
let out = "";
|
|
235
232
|
for (const char of value) if (!isIrcControlChar(char.charCodeAt(0))) out += char;
|
|
@@ -352,7 +349,7 @@ function normalizeIrcNickHostEntry(value) {
|
|
|
352
349
|
}
|
|
353
350
|
function normalizeIrcNickUserEntry(value) {
|
|
354
351
|
const normalized = normalizeIrcAllowEntry(value);
|
|
355
|
-
if (!
|
|
352
|
+
if (!isHostlessNickUser(normalized)) return null;
|
|
356
353
|
return normalized;
|
|
357
354
|
}
|
|
358
355
|
function normalizeIrcNickEntry(value) {
|
|
@@ -368,7 +365,7 @@ function createIrcIngressSubject(message) {
|
|
|
368
365
|
stableId: stableCandidates[stableCandidates.length - 1] ?? nick,
|
|
369
366
|
aliases: {
|
|
370
367
|
"irc-id-nick-user": candidates.find((candidate) => isHostlessNickUser(candidate)),
|
|
371
|
-
"irc-id-nick-host": stableCandidates.find((candidate) => !candidate.includes("!")
|
|
368
|
+
"irc-id-nick-host": stableCandidates.find((candidate) => !candidate.includes("!")),
|
|
372
369
|
"irc-nick": nick
|
|
373
370
|
}
|
|
374
371
|
};
|
|
@@ -386,6 +383,51 @@ const ircOutboundBaseAdapter = {
|
|
|
386
383
|
sanitizeText: ({ text }) => sanitizeIrcAssistantText(text)
|
|
387
384
|
};
|
|
388
385
|
//#endregion
|
|
386
|
+
//#region packages/normalization-core/src/utf16-slice.ts
|
|
387
|
+
function isHighSurrogate(codeUnit) {
|
|
388
|
+
return codeUnit >= 55296 && codeUnit <= 56319;
|
|
389
|
+
}
|
|
390
|
+
function isLowSurrogate(codeUnit) {
|
|
391
|
+
return codeUnit >= 56320 && codeUnit <= 57343;
|
|
392
|
+
}
|
|
393
|
+
/** Moves a chunk boundary away from the middle of a UTF-16 surrogate pair. */
|
|
394
|
+
function avoidTrailingHighSurrogateBreak(text, start, end) {
|
|
395
|
+
if (end <= start || end >= text.length || !isHighSurrogate(text.charCodeAt(end - 1)) || !isLowSurrogate(text.charCodeAt(end))) return end;
|
|
396
|
+
const adjusted = end - 1;
|
|
397
|
+
return adjusted > start ? adjusted : end + 1;
|
|
398
|
+
}
|
|
399
|
+
//#endregion
|
|
400
|
+
//#region packages/normalization-core/src/grapheme.ts
|
|
401
|
+
let graphemeSegmenter;
|
|
402
|
+
function getGraphemeSegmenter() {
|
|
403
|
+
graphemeSegmenter ??= new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
404
|
+
return graphemeSegmenter;
|
|
405
|
+
}
|
|
406
|
+
/** Uses the same source text that created segments. */
|
|
407
|
+
function containingSegment(segments, text, index) {
|
|
408
|
+
const position = Number.isNaN(index) ? 0 : Math.trunc(index);
|
|
409
|
+
const codePoint = text.codePointAt(position);
|
|
410
|
+
return segments.containing(codePoint !== void 0 && codePoint > 65535 ? position + 1 : position);
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Chooses a whole-grapheme cut within the hard budget, honoring a usable preference.
|
|
414
|
+
* If no whole grapheme fits, allowPartial permits a surrogate-safe progress cut;
|
|
415
|
+
* a leading surrogate pair can exceed maxEnd by one code unit.
|
|
416
|
+
*/
|
|
417
|
+
function findGraphemeChunkEnd(text, start, maxEnd, preferredEnd = maxEnd, allowPartial = true) {
|
|
418
|
+
const hardEnd = Math.min(maxEnd, text.length);
|
|
419
|
+
if (hardEnd <= start) return start;
|
|
420
|
+
const preferred = Number.isInteger(preferredEnd) && preferredEnd > start && preferredEnd <= hardEnd ? preferredEnd : hardEnd;
|
|
421
|
+
if (preferred === text.length) return preferred;
|
|
422
|
+
const before = text.charCodeAt(preferred - 1);
|
|
423
|
+
const after = text.charCodeAt(preferred);
|
|
424
|
+
if (Number.isInteger(preferred) && before <= 127 && after <= 127 && !(before === 13 && after === 10)) return preferred;
|
|
425
|
+
const segments = getGraphemeSegmenter().segment(text);
|
|
426
|
+
let end = containingSegment(segments, text, preferred)?.index ?? preferred;
|
|
427
|
+
if (end <= start && preferred < hardEnd) end = hardEnd === text.length ? hardEnd : containingSegment(segments, text, hardEnd)?.index ?? hardEnd;
|
|
428
|
+
return end > start ? end : allowPartial ? avoidTrailingHighSurrogateBreak(text, start, hardEnd) : start;
|
|
429
|
+
}
|
|
430
|
+
//#endregion
|
|
389
431
|
//#region extensions/irc/src/protocol.ts
|
|
390
432
|
const IRC_TARGET_PATTERN = /^[^\s:]+$/u;
|
|
391
433
|
function parseIrcLine(line) {
|
|
@@ -451,20 +493,13 @@ function parseIrcPrefix(prefix) {
|
|
|
451
493
|
if (prefix.includes(".")) return { server: prefix };
|
|
452
494
|
return { nick: prefix };
|
|
453
495
|
}
|
|
454
|
-
function decodeLiteralEscapes(input) {
|
|
455
|
-
return input.replace(/\\r/g, "\r").replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\0/g, "\0").replace(/\\x([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16))).replace(/\\u([dD][89abAB][0-9a-fA-F]{2})\\u([dD][c-fC-F][0-9a-fA-F]{2})/g, (_match, h, l) => String.fromCodePoint(65536 + (Number.parseInt(h, 16) - 55296 << 10) + (Number.parseInt(l, 16) - 56320))).replace(/\\u([0-9a-fA-F]{4})/g, (match, hex) => {
|
|
456
|
-
const codePoint = Number.parseInt(hex, 16);
|
|
457
|
-
return codePoint >= 55296 && codePoint <= 57343 ? match : String.fromCharCode(codePoint);
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
496
|
function sanitizeIrcOutboundText(text) {
|
|
461
|
-
return stripIrcControlChars(
|
|
497
|
+
return stripIrcControlChars(text.replace(/\r?\n/g, " ")).trim();
|
|
462
498
|
}
|
|
463
499
|
function sanitizeIrcTarget(raw) {
|
|
464
|
-
|
|
465
|
-
if (!
|
|
466
|
-
|
|
467
|
-
return decoded;
|
|
500
|
+
if (!raw) throw new Error("IRC target is required");
|
|
501
|
+
if (raw !== raw.trim() || hasIrcControlChars(raw) || !IRC_TARGET_PATTERN.test(raw)) throw new Error(`Invalid IRC target: ${raw}`);
|
|
502
|
+
return raw;
|
|
468
503
|
}
|
|
469
504
|
//#endregion
|
|
470
505
|
//#region extensions/irc/src/client.ts
|
|
@@ -475,6 +510,9 @@ const IRC_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
|
475
510
|
]);
|
|
476
511
|
const IRC_NICK_COLLISION_CODES = /* @__PURE__ */ new Set(["433", "436"]);
|
|
477
512
|
const IRC_MAX_LINE_BYTES = 512;
|
|
513
|
+
const IRC_MAX_RELAY_USER_BYTES = 11;
|
|
514
|
+
const IRC_MAX_RELAY_HOST_BYTES = 63;
|
|
515
|
+
const IRC_MAX_INBOUND_LINE_LENGTH = 16384;
|
|
478
516
|
function takeIrcPrivmsgChunk(text, maxChars, maxBytes) {
|
|
479
517
|
let end = 0;
|
|
480
518
|
let bytes = 0;
|
|
@@ -486,11 +524,9 @@ function takeIrcPrivmsgChunk(text, maxChars, maxBytes) {
|
|
|
486
524
|
}
|
|
487
525
|
if (end === 0) throw new Error("IRC target leaves no room for message text within the 512-byte line limit");
|
|
488
526
|
if (end === text.length) return text;
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
if (splitAt >= Math.floor(fitted.length / 2)) return fitted.slice(0, splitAt);
|
|
493
|
-
return fitted;
|
|
527
|
+
const splitAt = text.lastIndexOf(" ", end);
|
|
528
|
+
const preferredEnd = splitAt >= Math.floor(end / 2) ? splitAt : end;
|
|
529
|
+
return text.slice(0, findGraphemeChunkEnd(text, 0, end, preferredEnd));
|
|
494
530
|
}
|
|
495
531
|
function toIrcError(err) {
|
|
496
532
|
if (err instanceof Error) return err;
|
|
@@ -498,7 +534,7 @@ function toIrcError(err) {
|
|
|
498
534
|
}
|
|
499
535
|
let nickCollisionFallbackSeq = 0;
|
|
500
536
|
function buildFallbackNick(nick) {
|
|
501
|
-
const base = nick.replace(
|
|
537
|
+
const base = nick.replace(/[^A-Za-z0-9_\-[\]\\`^{}|]/g, "") || "openclaw";
|
|
502
538
|
const seq = ++nickCollisionFallbackSeq;
|
|
503
539
|
const suffix = seq === 1 ? "_" : `_${seq}`;
|
|
504
540
|
const maxNickLen = 30;
|
|
@@ -586,18 +622,35 @@ async function connectIrcClient(options) {
|
|
|
586
622
|
if (!target.startsWith("#") && !target.startsWith("&")) throw new Error(`IRC JOIN target must be a channel: ${channel}`);
|
|
587
623
|
sendRaw(`JOIN ${target}`);
|
|
588
624
|
};
|
|
589
|
-
const sendPrivmsg = (target, text, replyTo) => {
|
|
625
|
+
const sendPrivmsg = async (target, text, replyTo) => {
|
|
626
|
+
const effect = captureEffectAuthority();
|
|
590
627
|
const normalizedTarget = sanitizeIrcTarget(target);
|
|
591
628
|
const cleaned = sanitizeIrcOutboundText(text);
|
|
592
629
|
if (!cleaned) throw new Error("Message must be non-empty for IRC sends");
|
|
593
|
-
const lineOverheadBytes = Buffer.byteLength(`PRIVMSG ${normalizedTarget} :\r\n`, "utf8");
|
|
630
|
+
const lineOverheadBytes = Buffer.byteLength(`:${currentNick}!@ `, "utf8") + IRC_MAX_RELAY_USER_BYTES + IRC_MAX_RELAY_HOST_BYTES + Buffer.byteLength(`PRIVMSG ${normalizedTarget} :\r\n`, "utf8");
|
|
594
631
|
const maxChunkBytes = IRC_MAX_LINE_BYTES - lineOverheadBytes;
|
|
595
632
|
let remaining = replyTo ? sanitizeIrcOutboundText(`${text}\n\n[reply:${replyTo}]`) : cleaned;
|
|
633
|
+
const chunks = [];
|
|
596
634
|
while (remaining.length > 0) {
|
|
597
635
|
const chunk = takeIrcPrivmsgChunk(remaining, messageChunkMaxChars, maxChunkBytes).trim();
|
|
598
|
-
|
|
636
|
+
chunks.push(chunk);
|
|
599
637
|
remaining = remaining.slice(chunk.length).trimStart();
|
|
600
638
|
}
|
|
639
|
+
let sent = false;
|
|
640
|
+
await effect.initiate(() => {
|
|
641
|
+
for (const chunk of chunks) {
|
|
642
|
+
options.abortSignal?.throwIfAborted();
|
|
643
|
+
if (!ready || closed) throw new Error("IRC connection closed before send");
|
|
644
|
+
sendRaw(`PRIVMSG ${normalizedTarget} :${chunk}`);
|
|
645
|
+
sent = true;
|
|
646
|
+
}
|
|
647
|
+
}).catch((error) => {
|
|
648
|
+
if (sent) throw createChannelPartialDeliveryError(error, {
|
|
649
|
+
messageIds: [],
|
|
650
|
+
visibleReplySent: true
|
|
651
|
+
});
|
|
652
|
+
throw error;
|
|
653
|
+
});
|
|
601
654
|
};
|
|
602
655
|
const quit = (reason) => {
|
|
603
656
|
if (closed) return;
|
|
@@ -619,10 +672,22 @@ async function connectIrcClient(options) {
|
|
|
619
672
|
socket.destroy();
|
|
620
673
|
};
|
|
621
674
|
let buffer = "";
|
|
675
|
+
let receiveOverflowed = false;
|
|
676
|
+
const abortOversizedInput = () => {
|
|
677
|
+
receiveOverflowed = true;
|
|
678
|
+
buffer = "";
|
|
679
|
+
fail(/* @__PURE__ */ new Error(`IRC server sent a line longer than ${IRC_MAX_INBOUND_LINE_LENGTH} characters`));
|
|
680
|
+
socket.destroy();
|
|
681
|
+
};
|
|
622
682
|
socket.on("data", (chunk) => {
|
|
683
|
+
if (receiveOverflowed) return;
|
|
623
684
|
buffer += chunk;
|
|
624
685
|
let idx = buffer.indexOf("\n");
|
|
625
686
|
while (idx !== -1) {
|
|
687
|
+
if (idx > IRC_MAX_INBOUND_LINE_LENGTH) {
|
|
688
|
+
abortOversizedInput();
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
626
691
|
const rawLine = buffer.slice(0, idx).replace(/\r$/, "");
|
|
627
692
|
buffer = buffer.slice(idx + 1);
|
|
628
693
|
idx = buffer.indexOf("\n");
|
|
@@ -687,10 +752,12 @@ async function connectIrcClient(options) {
|
|
|
687
752
|
});
|
|
688
753
|
}
|
|
689
754
|
}
|
|
755
|
+
if (buffer.length > IRC_MAX_INBOUND_LINE_LENGTH) abortOversizedInput();
|
|
690
756
|
});
|
|
691
757
|
socket.once("connect", () => {
|
|
692
758
|
try {
|
|
693
|
-
|
|
759
|
+
const password = options.password?.trim();
|
|
760
|
+
if (password) sendRaw(password.includes(" ") || password.startsWith(":") ? `PASS :${password}` : `PASS ${password}`);
|
|
694
761
|
sendRaw(`NICK ${options.nick.trim()}`);
|
|
695
762
|
sendRaw(`USER ${options.username.trim()} 0 * :${sanitizeIrcOutboundText(options.realname)}`);
|
|
696
763
|
} catch (err) {
|
|
@@ -770,6 +837,7 @@ async function sendIrcMessages(to, text, opts, planMessages = (preparedText) =>
|
|
|
770
837
|
text: preparedText,
|
|
771
838
|
replyTo: opts.replyTo
|
|
772
839
|
}], onDeliveryResult) {
|
|
840
|
+
const effect = captureEffectAuthority();
|
|
773
841
|
const cfg = requireRuntimeConfig(opts.cfg, "IRC send");
|
|
774
842
|
const account = resolveIrcAccount({
|
|
775
843
|
cfg,
|
|
@@ -796,19 +864,18 @@ async function sendIrcMessages(to, text, opts, planMessages = (preparedText) =>
|
|
|
796
864
|
const results = [];
|
|
797
865
|
try {
|
|
798
866
|
opts.abortSignal?.throwIfAborted();
|
|
799
|
-
if (transient && (target.startsWith("#") || target.startsWith("&")))
|
|
867
|
+
if (transient && (target.startsWith("#") || target.startsWith("&"))) await effect.initiate(() => {
|
|
868
|
+
opts.abortSignal?.throwIfAborted();
|
|
869
|
+
if (!client.isReady()) throw new Error("IRC connection closed before join");
|
|
870
|
+
client.join(target);
|
|
871
|
+
});
|
|
800
872
|
for (const message of messages) {
|
|
801
873
|
opts.abortSignal?.throwIfAborted();
|
|
802
874
|
if (!client.isReady()) throw new Error("IRC connection closed before send");
|
|
803
875
|
await opts.onPlatformSendDispatch?.();
|
|
804
876
|
opts.abortSignal?.throwIfAborted();
|
|
805
877
|
if (!client.isReady()) throw new Error("IRC connection closed before send");
|
|
806
|
-
client.sendPrivmsg(target, message.text, message.replyTo);
|
|
807
|
-
getOptionalIrcRuntime()?.channel.activity.record({
|
|
808
|
-
channel: "irc",
|
|
809
|
-
accountId: account.accountId,
|
|
810
|
-
direction: "outbound"
|
|
811
|
-
});
|
|
878
|
+
await client.sendPrivmsg(target, message.text, message.replyTo);
|
|
812
879
|
const messageId = randomUUID();
|
|
813
880
|
const result = {
|
|
814
881
|
messageId,
|
|
@@ -824,9 +891,35 @@ async function sendIrcMessages(to, text, opts, planMessages = (preparedText) =>
|
|
|
824
891
|
})
|
|
825
892
|
};
|
|
826
893
|
results.push(result);
|
|
894
|
+
getOptionalIrcRuntime()?.channel.activity.record({
|
|
895
|
+
channel: "irc",
|
|
896
|
+
accountId: account.accountId,
|
|
897
|
+
direction: "outbound"
|
|
898
|
+
});
|
|
827
899
|
await onDeliveryResult?.(result);
|
|
828
900
|
}
|
|
829
901
|
return results;
|
|
902
|
+
} catch (error) {
|
|
903
|
+
if (results.length === 0) throw error;
|
|
904
|
+
const partial = isChannelPartialDeliveryError(error) ? error.deliveryResult : void 0;
|
|
905
|
+
const receipt = createMessageReceiptFromOutboundResults({
|
|
906
|
+
results: [
|
|
907
|
+
...results.map((result) => ({ receipt: result.receipt })),
|
|
908
|
+
...partial?.receipt ? [{ receipt: partial.receipt }] : [],
|
|
909
|
+
...(partial?.messageIds ?? []).filter((messageId) => !partial?.receipt?.platformMessageIds.includes(messageId)).map((messageId) => ({
|
|
910
|
+
channel: "irc",
|
|
911
|
+
messageId,
|
|
912
|
+
conversationId: target
|
|
913
|
+
}))
|
|
914
|
+
],
|
|
915
|
+
kind: "text"
|
|
916
|
+
});
|
|
917
|
+
throw createChannelPartialDeliveryError(error, {
|
|
918
|
+
...partial,
|
|
919
|
+
messageIds: receipt.platformMessageIds,
|
|
920
|
+
receipt,
|
|
921
|
+
visibleReplySent: true
|
|
922
|
+
});
|
|
830
923
|
} finally {
|
|
831
924
|
transient?.quit("sent");
|
|
832
925
|
}
|
|
@@ -972,8 +1065,8 @@ async function probeIrc(cfg, opts) {
|
|
|
972
1065
|
//#endregion
|
|
973
1066
|
//#region extensions/irc/src/setup-core.ts
|
|
974
1067
|
const channel$1 = "irc";
|
|
975
|
-
const
|
|
976
|
-
const
|
|
1068
|
+
const setIrcDmPolicy = createTopLevelChannelDmPolicySetter({ channel: channel$1 });
|
|
1069
|
+
const setIrcAllowFrom = createTopLevelChannelAllowFromSetter({ channel: channel$1 });
|
|
977
1070
|
const validateIrcRequiredSetupInput = createSetupInputPresenceValidator({ whenNotUseEnv: [{
|
|
978
1071
|
someOf: ["host"],
|
|
979
1072
|
message: "IRC requires host."
|
|
@@ -996,12 +1089,6 @@ function updateIrcAccountConfig(cfg, accountId, patch) {
|
|
|
996
1089
|
ensureAccountEnabled: false
|
|
997
1090
|
});
|
|
998
1091
|
}
|
|
999
|
-
function setIrcDmPolicy(cfg, dmPolicy) {
|
|
1000
|
-
return setIrcTopLevelDmPolicy(cfg, dmPolicy);
|
|
1001
|
-
}
|
|
1002
|
-
function setIrcAllowFrom(cfg, allowFrom) {
|
|
1003
|
-
return setIrcTopLevelAllowFrom(cfg, allowFrom);
|
|
1004
|
-
}
|
|
1005
1092
|
function setIrcNickServ(cfg, accountId, nickserv) {
|
|
1006
1093
|
return updateIrcAccountConfig(cfg, accountId, { nickserv });
|
|
1007
1094
|
}
|
|
@@ -1156,7 +1243,7 @@ function normalizeGroupEntry(raw) {
|
|
|
1156
1243
|
return `#${normalized.replace(/^#+/, "")}`;
|
|
1157
1244
|
}
|
|
1158
1245
|
const promptIrcAllowFrom = createPromptParsedAllowFromForAccount({
|
|
1159
|
-
defaultAccountId:
|
|
1246
|
+
defaultAccountId: resolveDefaultIrcAccountId,
|
|
1160
1247
|
noteTitle: t("wizard.irc.allowlistTitle"),
|
|
1161
1248
|
noteLines: [
|
|
1162
1249
|
t("wizard.irc.allowlistIntro"),
|
|
@@ -1222,11 +1309,7 @@ const ircDmPolicy = {
|
|
|
1222
1309
|
allowFromKey: "channels.irc.allowFrom",
|
|
1223
1310
|
getCurrent: (cfg) => cfg.channels?.irc?.dmPolicy ?? "pairing",
|
|
1224
1311
|
setPolicy: (cfg, policy) => setIrcDmPolicy(cfg, policy),
|
|
1225
|
-
promptAllowFrom:
|
|
1226
|
-
cfg,
|
|
1227
|
-
prompter,
|
|
1228
|
-
accountId
|
|
1229
|
-
})
|
|
1312
|
+
promptAllowFrom: promptIrcAllowFrom
|
|
1230
1313
|
};
|
|
1231
1314
|
const ircSetupWizard = {
|
|
1232
1315
|
channel,
|
|
@@ -1459,7 +1542,6 @@ const meta = {
|
|
|
1459
1542
|
systemImage: "number",
|
|
1460
1543
|
markdownCapable: true
|
|
1461
1544
|
};
|
|
1462
|
-
const loadIrcChannelRuntime = createLazyRuntimeModule(() => import("./channel-runtime-BvC0hwF1.mjs"));
|
|
1463
1545
|
function normalizePairingTarget(raw) {
|
|
1464
1546
|
const normalized = normalizeIrcAllowEntry(raw);
|
|
1465
1547
|
if (!normalized) return "";
|
|
@@ -1697,7 +1779,7 @@ const ircPlugin = createChatChannelPlugin({
|
|
|
1697
1779
|
notify: async ({ cfg, id, message, accountId }) => {
|
|
1698
1780
|
const target = normalizePairingTarget(id);
|
|
1699
1781
|
if (!target) throw new Error(`invalid IRC pairing id: ${id}`);
|
|
1700
|
-
const { sendMessageIrc } = await
|
|
1782
|
+
const { sendMessageIrc } = await import("./channel-runtime-FNBqToAo.mjs");
|
|
1701
1783
|
await sendMessageIrc(target, message, {
|
|
1702
1784
|
cfg,
|
|
1703
1785
|
accountId
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { a as resolveIrcGroupRequireMention, c as connectIrcClient, d as sanitizeIrcAssistantText, f as createIrcIngressSubject, h as normalizeIrcAllowEntry, i as resolveIrcGroupMatch, l as parseIrcLine, m as isChannelTarget, o as sendMessageIrc, p as ircIngressIdentity, s as buildIrcConnectOptions, u as parseIrcPrefix, y as resolveIrcAccount } from "./channel-
|
|
1
|
+
import { a as resolveIrcGroupRequireMention, c as connectIrcClient, d as sanitizeIrcAssistantText, f as createIrcIngressSubject, h as normalizeIrcAllowEntry, i as resolveIrcGroupMatch, l as parseIrcLine, m as isChannelTarget, o as sendMessageIrc, p as ircIngressIdentity, s as buildIrcConnectOptions, u as parseIrcPrefix, y as resolveIrcAccount } from "./channel-BXrFfvr6.mjs";
|
|
2
2
|
import { t as getIrcRuntime } from "./runtime-C_V9tmpk.mjs";
|
|
3
3
|
import { channelIngressRoutes } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
4
|
+
import { computeBackoff } from "openclaw/plugin-sdk/runtime-env";
|
|
4
5
|
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
5
6
|
import { bindIngressLifecycleToReplyOptions, createChannelIngressError, createChannelIngressMonitor, resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-outbound";
|
|
6
7
|
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { createChannelInboundEnvelopeBuilderAsync, logInboundDrop } from "openclaw/plugin-sdk/channel-inbound";
|
|
7
9
|
import { resolveLoggerBackedRuntime } from "openclaw/plugin-sdk/extension-shared";
|
|
10
|
+
import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
|
|
8
11
|
import { channelReadyPatch } from "openclaw/plugin-sdk/gateway-runtime";
|
|
9
|
-
import { logInboundDrop, resolveChannelInboundRouteEnvelope } from "openclaw/plugin-sdk/channel-inbound";
|
|
10
12
|
import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
|
|
11
13
|
import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
|
|
12
14
|
import { deliverFormattedTextWithAttachments } from "openclaw/plugin-sdk/reply-payload";
|
|
@@ -34,26 +36,6 @@ function buildIrcNickMentionPattern(value) {
|
|
|
34
36
|
function hasEntries(entries) {
|
|
35
37
|
return normalizeStringEntries(entries).some((entry) => normalizeIrcAllowEntry(entry));
|
|
36
38
|
}
|
|
37
|
-
function routeDescriptorsForIrcGroup(params) {
|
|
38
|
-
if (!params.isGroup) return [];
|
|
39
|
-
return channelIngressRoutes(params.groupPolicy === "allowlist" && {
|
|
40
|
-
id: "irc:channel",
|
|
41
|
-
allowed: params.hasConfiguredGroups && params.groupAllowed,
|
|
42
|
-
precedence: 0,
|
|
43
|
-
matchId: "irc-channel",
|
|
44
|
-
blockReason: "channel_not_allowlisted"
|
|
45
|
-
}, !params.groupEnabled && {
|
|
46
|
-
id: "irc:channel-enabled",
|
|
47
|
-
enabled: false,
|
|
48
|
-
precedence: 10,
|
|
49
|
-
blockReason: "channel_disabled"
|
|
50
|
-
}, hasEntries(params.routeGroupAllowFrom) && {
|
|
51
|
-
id: "irc:channel-sender",
|
|
52
|
-
precedence: 20,
|
|
53
|
-
senderPolicy: "replace",
|
|
54
|
-
senderAllowFrom: params.routeGroupAllowFrom
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
39
|
async function deliverIrcReply(params) {
|
|
58
40
|
await deliverFormattedTextWithAttachments({
|
|
59
41
|
payload: {
|
|
@@ -123,7 +105,7 @@ async function handleIrcInbound(params) {
|
|
|
123
105
|
const accessGroupPolicy = groupPolicy === "open" && (hasEntries(account.config.groupAllowFrom) || hasEntries(routeGroupAllowFrom)) ? "allowlist" : groupPolicy;
|
|
124
106
|
const channelTarget = message.target.startsWith("#") || message.target.startsWith("&") ? message.target : `#${message.target}`;
|
|
125
107
|
const peerId = message.isGroup ? channelTarget : message.senderNick;
|
|
126
|
-
const
|
|
108
|
+
const route = resolveAgentRoute({
|
|
127
109
|
cfg: config,
|
|
128
110
|
channel: CHANNEL_ID,
|
|
129
111
|
accountId: account.accountId,
|
|
@@ -150,14 +132,23 @@ async function handleIrcInbound(params) {
|
|
|
150
132
|
...message.messageId ? { messageId: message.messageId } : {},
|
|
151
133
|
inboundEventKind: "user_request"
|
|
152
134
|
},
|
|
153
|
-
route:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
135
|
+
route: message.isGroup ? channelIngressRoutes(groupPolicy === "allowlist" && {
|
|
136
|
+
id: "irc:channel",
|
|
137
|
+
allowed: groupMatch.hasConfiguredGroups && groupMatch.allowed,
|
|
138
|
+
precedence: 0,
|
|
139
|
+
matchId: "irc-channel",
|
|
140
|
+
blockReason: "channel_not_allowlisted"
|
|
141
|
+
}, (groupMatch.groupConfig?.enabled === false || groupMatch.wildcardConfig?.enabled === false) && {
|
|
142
|
+
id: "irc:channel-enabled",
|
|
143
|
+
enabled: false,
|
|
144
|
+
precedence: 10,
|
|
145
|
+
blockReason: "channel_disabled"
|
|
146
|
+
}, hasEntries(routeGroupAllowFrom) && {
|
|
147
|
+
id: "irc:channel-sender",
|
|
148
|
+
precedence: 20,
|
|
149
|
+
senderPolicy: "replace",
|
|
150
|
+
senderAllowFrom: routeGroupAllowFrom
|
|
151
|
+
}) : [],
|
|
161
152
|
mentionFacts: message.isGroup ? {
|
|
162
153
|
canDetectMention: true,
|
|
163
154
|
wasMentioned,
|
|
@@ -229,7 +220,10 @@ async function handleIrcInbound(params) {
|
|
|
229
220
|
error: turnAdoptionLifecycle.abortSignal.reason
|
|
230
221
|
};
|
|
231
222
|
const fromLabel = message.isGroup ? message.target : senderDisplay;
|
|
232
|
-
const body =
|
|
223
|
+
const body = (await createChannelInboundEnvelopeBuilderAsync({
|
|
224
|
+
cfg: config,
|
|
225
|
+
route
|
|
226
|
+
}))({
|
|
233
227
|
channel: "IRC",
|
|
234
228
|
from: fromLabel,
|
|
235
229
|
timestamp: message.timestamp,
|
|
@@ -476,8 +470,31 @@ function createIrcIngressMonitor(options) {
|
|
|
476
470
|
};
|
|
477
471
|
}
|
|
478
472
|
//#endregion
|
|
473
|
+
//#region extensions/irc/src/reconnect-backoff.ts
|
|
474
|
+
const IRC_RECONNECT_BACKOFF = {
|
|
475
|
+
initialMs: 1e3,
|
|
476
|
+
maxMs: 3e4,
|
|
477
|
+
factor: 2,
|
|
478
|
+
jitter: .2
|
|
479
|
+
};
|
|
480
|
+
const IRC_RECONNECT_STABLE_MS = 6e4;
|
|
481
|
+
function createIrcReconnectBackoff(now = Date.now) {
|
|
482
|
+
let attempt = 0;
|
|
483
|
+
let connectedAt;
|
|
484
|
+
return {
|
|
485
|
+
markConnected: () => {
|
|
486
|
+
connectedAt = now();
|
|
487
|
+
},
|
|
488
|
+
nextDelayMs: () => {
|
|
489
|
+
if (connectedAt !== void 0 && now() - connectedAt >= IRC_RECONNECT_STABLE_MS) attempt = 0;
|
|
490
|
+
connectedAt = void 0;
|
|
491
|
+
attempt += 1;
|
|
492
|
+
return computeBackoff(IRC_RECONNECT_BACKOFF, attempt);
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
//#endregion
|
|
479
497
|
//#region extensions/irc/src/monitor.ts
|
|
480
|
-
const IRC_MONITOR_RECONNECT_DELAY_MS = 1e3;
|
|
481
498
|
async function monitorIrcProvider(opts) {
|
|
482
499
|
const core = getIrcRuntime();
|
|
483
500
|
const cfg = opts.config ?? core.config.current();
|
|
@@ -495,6 +512,7 @@ async function monitorIrcProvider(opts) {
|
|
|
495
512
|
let activeConnectionEpoch = null;
|
|
496
513
|
let ingressPause = Promise.resolve();
|
|
497
514
|
let reconnectTimer = null;
|
|
515
|
+
const reconnectBackoff = createIrcReconnectBackoff();
|
|
498
516
|
let stopped = false;
|
|
499
517
|
const monitorAbort = new AbortController();
|
|
500
518
|
const abortSignal = opts.abortSignal ? AbortSignal.any([opts.abortSignal, monitorAbort.signal]) : monitorAbort.signal;
|
|
@@ -528,7 +546,7 @@ async function monitorIrcProvider(opts) {
|
|
|
528
546
|
const replyClient = client;
|
|
529
547
|
if (!replyClient || !replyClient.isReady() || stopped || abortSignal.aborted) throw new Error("IRC transport disconnected before reply send.");
|
|
530
548
|
if (!message.isGroup && context.connectionEpoch !== activeConnectionEpoch) throw new Error("IRC connection changed before private reply send.");
|
|
531
|
-
replyClient.sendPrivmsg(target, text);
|
|
549
|
+
await replyClient.sendPrivmsg(target, text);
|
|
532
550
|
opts.statusSink?.({ lastOutboundAt: Date.now() });
|
|
533
551
|
core.channel.activity.record({
|
|
534
552
|
channel: "irc",
|
|
@@ -543,6 +561,7 @@ async function monitorIrcProvider(opts) {
|
|
|
543
561
|
function scheduleReconnect() {
|
|
544
562
|
if (stopped || abortSignal.aborted || reconnectTimer) return;
|
|
545
563
|
opts.statusSink?.({ lifecycle: "recovering" });
|
|
564
|
+
const delayMs = reconnectBackoff.nextDelayMs();
|
|
546
565
|
reconnectTimer = setTimeout(() => {
|
|
547
566
|
reconnectTimer = null;
|
|
548
567
|
connect().catch((error) => {
|
|
@@ -551,7 +570,8 @@ async function monitorIrcProvider(opts) {
|
|
|
551
570
|
logger.error(`[${account.accountId}] IRC reconnect failed: ${message}`);
|
|
552
571
|
scheduleReconnect();
|
|
553
572
|
});
|
|
554
|
-
},
|
|
573
|
+
}, delayMs);
|
|
574
|
+
return delayMs;
|
|
555
575
|
}
|
|
556
576
|
async function connect() {
|
|
557
577
|
if (stopped || abortSignal.aborted) return;
|
|
@@ -573,8 +593,8 @@ async function monitorIrcProvider(opts) {
|
|
|
573
593
|
ingressPause = ingress.pause();
|
|
574
594
|
if (activeConnectionEpoch === ingressConnection.connectionEpoch) activeConnectionEpoch = null;
|
|
575
595
|
client = null;
|
|
576
|
-
|
|
577
|
-
|
|
596
|
+
const delayMs = scheduleReconnect();
|
|
597
|
+
if (delayMs !== void 0) logger.warn?.(`[${account.accountId}] IRC connection closed; reconnecting in ${delayMs}ms`);
|
|
578
598
|
},
|
|
579
599
|
onPrivmsg: async (event) => {
|
|
580
600
|
await ingressConnection.accept(event.rawLine, event.connectedNick);
|
|
@@ -599,6 +619,7 @@ async function monitorIrcProvider(opts) {
|
|
|
599
619
|
return;
|
|
600
620
|
}
|
|
601
621
|
ingress.start();
|
|
622
|
+
reconnectBackoff.markConnected();
|
|
602
623
|
opts.statusSink?.(channelReadyPatch());
|
|
603
624
|
logger.info(`[${account.accountId}] connected to ${account.host}:${account.port}${account.tls ? " (tls)" : ""} as ${nextClient.nick}`);
|
|
604
625
|
}
|
package/dist/api.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { _ as listIrcAccountIds, g as listEnabledIrcAccounts, n as ircSetupWizard, r as ircSetupAdapter, t as ircPlugin, v as resolveDefaultIrcAccountId, y as resolveIrcAccount } from "./.setup/channel-
|
|
1
|
+
import { _ as listIrcAccountIds, g as listEnabledIrcAccounts, n as ircSetupWizard, r as ircSetupAdapter, t as ircPlugin, v as resolveDefaultIrcAccountId, y as resolveIrcAccount } from "./.setup/channel-BXrFfvr6.mjs";
|
|
2
2
|
import { r as setIrcRuntime } from "./.setup/runtime-C_V9tmpk.mjs";
|
|
3
3
|
export { ircPlugin, ircSetupAdapter, ircSetupWizard, listEnabledIrcAccounts, listIrcAccountIds, resolveDefaultIrcAccountId, resolveIrcAccount, setIrcRuntime };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as ircPlugin } from "./.setup/channel-
|
|
1
|
+
import { t as ircPlugin } from "./.setup/channel-BXrFfvr6.mjs";
|
|
2
2
|
export { ircPlugin };
|
package/dist/setup-entry.js
CHANGED
|
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
var setup_entry_default = defineBundledChannelSetupEntry({
|
|
5
5
|
importMetaUrl: import.meta.url,
|
|
6
6
|
plugin: {
|
|
7
|
-
specifier: fileURLToPath(new URL(".setup/channel-plugin-api-
|
|
7
|
+
specifier: fileURLToPath(new URL(".setup/channel-plugin-api-tb7ep4Ml.mjs", import.meta.url).href),
|
|
8
8
|
exportName: "ircPlugin"
|
|
9
9
|
},
|
|
10
10
|
secrets: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/irc",
|
|
3
|
-
"version": "2026.10.2",
|
|
3
|
+
"version": "2026.10.5-beta.2",
|
|
4
4
|
"description": "OpenClaw IRC channel plugin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"openclaw": {
|
|
@@ -121,10 +121,10 @@
|
|
|
121
121
|
}
|
|
122
122
|
},
|
|
123
123
|
"compat": {
|
|
124
|
-
"pluginApi": ">=2026.10.2"
|
|
124
|
+
"pluginApi": ">=2026.10.5-beta.2"
|
|
125
125
|
},
|
|
126
126
|
"build": {
|
|
127
|
-
"openclawVersion": "2026.10.2",
|
|
127
|
+
"openclawVersion": "2026.10.5-beta.2",
|
|
128
128
|
"bundledDist": false
|
|
129
129
|
},
|
|
130
130
|
"release": {
|
|
@@ -151,7 +151,7 @@
|
|
|
151
151
|
"assets/activity.svg"
|
|
152
152
|
],
|
|
153
153
|
"peerDependencies": {
|
|
154
|
-
"openclaw": ">=2026.10.2"
|
|
154
|
+
"openclaw": ">=2026.10.5-beta.2"
|
|
155
155
|
},
|
|
156
156
|
"peerDependenciesMeta": {
|
|
157
157
|
"openclaw": {
|