@openclaw/irc 2026.7.2-beta.5 → 2026.7.33
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/{channel-BZclM5wY.js → .setup/channel-SaGThQQW.mjs} +134 -193
- package/dist/.setup/channel-plugin-api-04wlnIps.mjs +3 -0
- package/dist/{channel-runtime-CRzAwKGK.js → .setup/channel-runtime-DBtfWzX8.mjs} +112 -325
- package/dist/{config-schema-BPoye3--.js → .setup/config-schema-BQXbFpTD.mjs} +43 -24
- package/dist/{runtime-xy-FcjJC.js → .setup/runtime-DZfYm3vY.mjs} +1 -1
- package/dist/.setup/secret-contract-DdKIs_mx.mjs +82 -0
- package/dist/.setup/secret-contract-api-CmA70kMR.mjs +3 -0
- package/dist/api.js +2 -2
- package/dist/channel-config-api.js +1 -1
- package/dist/channel-plugin-api.js +1 -1
- package/dist/configured-state.js +6 -0
- package/dist/runtime-api.js +1 -1
- package/dist/secret-contract-api.js +1 -1
- package/dist/setup-entry.js +3 -2
- package/openclaw.plugin.json +62 -72
- package/package.json +13 -91
- package/dist/doctor-contract-api.js +0 -16
- package/dist/secret-contract-D_DC3YWj.js +0 -42
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as getIrcRuntime } from "./runtime-
|
|
1
|
+
import { a as resolveIrcRequireMention, c as isChannelTarget, d as connectIrcClient, f as makeIrcMessageId, g as resolveIrcAccount, i as resolveIrcGroupMatch, l as normalizeIrcAllowEntry, o as sendMessageIrc, s as buildIrcAllowlistCandidates, u as buildIrcConnectOptions } from "./channel-SaGThQQW.mjs";
|
|
2
|
+
import { t as getIrcRuntime } from "./runtime-DZfYm3vY.mjs";
|
|
3
3
|
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
4
|
-
import { bindIngressLifecycleToReplyOptions, createChannelIngressMonitor, resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-outbound";
|
|
5
|
-
import { randomUUID } from "node:crypto";
|
|
6
4
|
import { resolveLoggerBackedRuntime } from "openclaw/plugin-sdk/extension-shared";
|
|
7
|
-
import {
|
|
5
|
+
import { logInboundDrop } from "openclaw/plugin-sdk/channel-inbound";
|
|
8
6
|
import { channelIngressRoutes, createChannelIngressResolver, defineStableChannelIngressIdentity } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
9
7
|
import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
|
|
10
8
|
import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
|
|
9
|
+
import { resolveInboundRouteEnvelopeBuilderWithRuntime } from "openclaw/plugin-sdk/inbound-envelope";
|
|
11
10
|
import { deliverFormattedTextWithAttachments } from "openclaw/plugin-sdk/reply-payload";
|
|
12
11
|
import { GROUP_POLICY_BLOCKED_LABEL, resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "openclaw/plugin-sdk/runtime-group-policy";
|
|
13
12
|
//#region extensions/irc/src/inbound.ts
|
|
@@ -121,7 +120,7 @@ async function deliverIrcReply(params) {
|
|
|
121
120
|
});
|
|
122
121
|
}
|
|
123
122
|
async function handleIrcInbound(params) {
|
|
124
|
-
const { message, account, config, runtime, connectedNick, statusSink
|
|
123
|
+
const { message, account, config, runtime, connectedNick, statusSink } = params;
|
|
125
124
|
const core = getIrcRuntime();
|
|
126
125
|
const pairing = createChannelPairingController({
|
|
127
126
|
core,
|
|
@@ -129,11 +128,7 @@ async function handleIrcInbound(params) {
|
|
|
129
128
|
accountId: account.accountId
|
|
130
129
|
});
|
|
131
130
|
const rawBody = message.text?.trim() ?? "";
|
|
132
|
-
if (!rawBody) return
|
|
133
|
-
if (turnAdoptionLifecycle?.abortSignal.aborted) return {
|
|
134
|
-
kind: "failed-retryable",
|
|
135
|
-
error: turnAdoptionLifecycle.abortSignal.reason
|
|
136
|
-
};
|
|
131
|
+
if (!rawBody) return;
|
|
137
132
|
statusSink?.({ lastInboundAt: message.timestamp });
|
|
138
133
|
const senderDisplay = message.senderHost ? `${message.senderNick}!${message.senderUser ?? "?"}@${message.senderHost}` : message.senderNick;
|
|
139
134
|
const allowNameMatching = isDangerousNameMatchingEnabled(account.config);
|
|
@@ -164,9 +159,9 @@ async function handleIrcInbound(params) {
|
|
|
164
159
|
const mentionNick = connectedNick?.trim() || account.nick;
|
|
165
160
|
const explicitMentionRegex = mentionNick ? new RegExp(`\\b${escapeIrcRegexLiteral(mentionNick)}\\b[:,]?`, "i") : null;
|
|
166
161
|
const wasMentioned = core.channel.mentions.matchesMentionPatterns(rawBody, mentionRegexes) || (explicitMentionRegex ? explicitMentionRegex.test(rawBody) : false);
|
|
167
|
-
const requireMention = message.isGroup ?
|
|
168
|
-
|
|
169
|
-
|
|
162
|
+
const requireMention = message.isGroup ? resolveIrcRequireMention({
|
|
163
|
+
groupConfig: groupMatch.groupConfig,
|
|
164
|
+
wildcardConfig: groupMatch.wildcardConfig
|
|
170
165
|
}) : false;
|
|
171
166
|
const routeGroupAllowFrom = normalizeStringEntries(groupMatch.groupConfig?.allowFrom?.length ? groupMatch.groupConfig.allowFrom : groupMatch.wildcardConfig?.allowFrom);
|
|
172
167
|
const accessGroupPolicy = groupPolicy === "open" && (hasEntries(account.config.groupAllowFrom) || hasEntries(routeGroupAllowFrom)) ? "allowlist" : groupPolicy;
|
|
@@ -233,11 +228,11 @@ async function handleIrcInbound(params) {
|
|
|
233
228
|
}
|
|
234
229
|
});
|
|
235
230
|
runtime.log?.(`irc: drop DM sender ${senderDisplay} (dmPolicy=${dmPolicy})`);
|
|
236
|
-
return
|
|
231
|
+
return;
|
|
237
232
|
}
|
|
238
233
|
if (access.ingress.admission === "skip") {
|
|
239
234
|
runtime.log?.(`irc: drop channel ${message.target} (missing-mention)`);
|
|
240
|
-
return
|
|
235
|
+
return;
|
|
241
236
|
}
|
|
242
237
|
if (access.ingress.admission !== "dispatch") {
|
|
243
238
|
if (message.isGroup && access.ingress.decisiveGateId === "command" && access.commandAccess.shouldBlockControlCommand) {
|
|
@@ -247,106 +242,69 @@ async function handleIrcInbound(params) {
|
|
|
247
242
|
reason: "control command (unauthorized)",
|
|
248
243
|
target: senderDisplay
|
|
249
244
|
});
|
|
250
|
-
return
|
|
245
|
+
return;
|
|
251
246
|
}
|
|
252
|
-
if (message.isGroup)
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
247
|
+
if (message.isGroup) {
|
|
248
|
+
if (access.routeAccess.reason === "channel_not_allowlisted") runtime.log?.(`irc: drop channel ${message.target} (not allowlisted)`);
|
|
249
|
+
else if (access.routeAccess.reason === "channel_disabled") runtime.log?.(`irc: drop channel ${message.target} (disabled)`);
|
|
250
|
+
else runtime.log?.(`irc: drop group sender ${senderDisplay} (policy=${groupPolicy})`);
|
|
251
|
+
} else runtime.log?.(`irc: drop DM sender ${senderDisplay} (dmPolicy=${dmPolicy})`);
|
|
252
|
+
return;
|
|
257
253
|
}
|
|
258
|
-
if (turnAdoptionLifecycle?.abortSignal.aborted) return {
|
|
259
|
-
kind: "failed-retryable",
|
|
260
|
-
error: turnAdoptionLifecycle.abortSignal.reason
|
|
261
|
-
};
|
|
262
254
|
const channelTarget = message.target.startsWith("#") || message.target.startsWith("&") ? message.target : `#${message.target}`;
|
|
263
255
|
const peerId = message.isGroup ? channelTarget : message.senderNick;
|
|
264
|
-
const { route, buildEnvelope } =
|
|
256
|
+
const { route, buildEnvelope } = resolveInboundRouteEnvelopeBuilderWithRuntime({
|
|
265
257
|
cfg: config,
|
|
266
258
|
channel: CHANNEL_ID,
|
|
267
259
|
accountId: account.accountId,
|
|
268
260
|
peer: {
|
|
269
261
|
kind: message.isGroup ? "group" : "direct",
|
|
270
262
|
id: peerId
|
|
271
|
-
}
|
|
263
|
+
},
|
|
264
|
+
runtime: core.channel,
|
|
265
|
+
sessionStore: config.session?.store
|
|
272
266
|
});
|
|
273
267
|
const fromLabel = message.isGroup ? message.target : senderDisplay;
|
|
274
|
-
const body = buildEnvelope({
|
|
268
|
+
const { storePath, body } = buildEnvelope({
|
|
275
269
|
channel: "IRC",
|
|
276
270
|
from: fromLabel,
|
|
277
271
|
timestamp: message.timestamp,
|
|
278
272
|
body: rawBody
|
|
279
273
|
});
|
|
280
274
|
const groupSystemPrompt = normalizeOptionalString(groupMatch.groupConfig?.systemPrompt);
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
reply: {
|
|
304
|
-
to: message.isGroup ? `channel:${channelTarget}` : `irc:${peerId}`,
|
|
305
|
-
originatingTo: message.isGroup ? `channel:${channelTarget}` : `irc:${peerId}`
|
|
306
|
-
},
|
|
307
|
-
message: {
|
|
308
|
-
body,
|
|
309
|
-
bodyForAgent: rawBody,
|
|
310
|
-
rawBody,
|
|
311
|
-
commandBody: rawBody
|
|
312
|
-
},
|
|
313
|
-
access: {
|
|
314
|
-
commands: { authorized: commandAuthorized },
|
|
315
|
-
mentions: {
|
|
316
|
-
canDetectMention: message.isGroup,
|
|
317
|
-
wasMentioned
|
|
318
|
-
}
|
|
319
|
-
},
|
|
320
|
-
extra: {
|
|
321
|
-
GroupSubject: message.isGroup ? message.target : void 0,
|
|
322
|
-
GroupSystemPrompt: message.isGroup ? groupSystemPrompt : void 0
|
|
323
|
-
}
|
|
275
|
+
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
276
|
+
Body: body,
|
|
277
|
+
RawBody: rawBody,
|
|
278
|
+
CommandBody: rawBody,
|
|
279
|
+
From: message.isGroup ? `channel:${channelTarget}` : `irc:${senderDisplay}`,
|
|
280
|
+
To: message.isGroup ? `channel:${channelTarget}` : `irc:${peerId}`,
|
|
281
|
+
SessionKey: route.sessionKey,
|
|
282
|
+
AccountId: route.accountId,
|
|
283
|
+
ChatType: message.isGroup ? "group" : "direct",
|
|
284
|
+
ConversationLabel: fromLabel,
|
|
285
|
+
SenderName: message.senderNick || void 0,
|
|
286
|
+
SenderId: senderDisplay,
|
|
287
|
+
GroupSubject: message.isGroup ? message.target : void 0,
|
|
288
|
+
GroupSystemPrompt: message.isGroup ? groupSystemPrompt : void 0,
|
|
289
|
+
Provider: CHANNEL_ID,
|
|
290
|
+
Surface: CHANNEL_ID,
|
|
291
|
+
WasMentioned: message.isGroup ? wasMentioned : void 0,
|
|
292
|
+
MessageSid: message.messageId,
|
|
293
|
+
Timestamp: message.timestamp,
|
|
294
|
+
OriginatingChannel: CHANNEL_ID,
|
|
295
|
+
OriginatingTo: message.isGroup ? `channel:${channelTarget}` : `irc:${peerId}`,
|
|
296
|
+
CommandAuthorized: commandAuthorized
|
|
324
297
|
});
|
|
325
|
-
|
|
326
|
-
const trackedIngressLifecycle = turnAdoptionLifecycle ? {
|
|
327
|
-
...turnAdoptionLifecycle,
|
|
328
|
-
onAdopted: async () => {
|
|
329
|
-
ingressState.handoff = "adopted";
|
|
330
|
-
await turnAdoptionLifecycle.onAdopted();
|
|
331
|
-
},
|
|
332
|
-
onDeferred: () => {
|
|
333
|
-
ingressState.handoff = "deferred";
|
|
334
|
-
turnAdoptionLifecycle.onDeferred();
|
|
335
|
-
},
|
|
336
|
-
onAbandoned: async () => {
|
|
337
|
-
ingressState.handoff = "abandoned";
|
|
338
|
-
await turnAdoptionLifecycle.onAbandoned();
|
|
339
|
-
}
|
|
340
|
-
} : void 0;
|
|
341
|
-
await core.channel.inbound.dispatch({
|
|
298
|
+
await core.channel.inbound.dispatchReply({
|
|
342
299
|
cfg: config,
|
|
343
300
|
channel: CHANNEL_ID,
|
|
344
301
|
accountId: account.accountId,
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
},
|
|
302
|
+
agentId: route.agentId,
|
|
303
|
+
routeSessionKey: route.sessionKey,
|
|
304
|
+
storePath,
|
|
349
305
|
ctxPayload,
|
|
306
|
+
recordInboundSession: core.channel.session.recordInboundSession,
|
|
307
|
+
dispatchReplyWithBufferedBlockDispatcher: core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
|
|
350
308
|
delivery: {
|
|
351
309
|
deliver: async (payload) => {
|
|
352
310
|
await deliverIrcReply({
|
|
@@ -364,170 +322,30 @@ async function handleIrcInbound(params) {
|
|
|
364
322
|
},
|
|
365
323
|
replyPipeline: {},
|
|
366
324
|
replyOptions: {
|
|
367
|
-
...trackedIngressLifecycle ? bindIngressLifecycleToReplyOptions(trackedIngressLifecycle) : {},
|
|
368
325
|
skillFilter: groupMatch.groupConfig?.skills,
|
|
369
|
-
disableBlockStreaming: typeof
|
|
326
|
+
disableBlockStreaming: typeof account.config.blockStreaming === "boolean" ? !account.config.blockStreaming : void 0
|
|
370
327
|
},
|
|
371
328
|
record: { onRecordError: (err) => {
|
|
372
329
|
runtime.error?.(`irc: failed updating session meta: ${String(err)}`);
|
|
373
330
|
} }
|
|
374
331
|
});
|
|
375
|
-
if (turnAdoptionLifecycle?.abortSignal.aborted && ingressState.handoff === "none") return {
|
|
376
|
-
kind: "failed-retryable",
|
|
377
|
-
error: turnAdoptionLifecycle.abortSignal.reason
|
|
378
|
-
};
|
|
379
|
-
if (turnAdoptionLifecycle && ingressState.handoff === "none") {
|
|
380
|
-
await turnAdoptionLifecycle.onAdopted();
|
|
381
|
-
ingressState.handoff = "adopted";
|
|
382
|
-
}
|
|
383
|
-
return ingressState.handoff === "deferred" || ingressState.handoff === "abandoned" ? { kind: "deferred" } : { kind: "completed" };
|
|
384
332
|
}
|
|
385
333
|
//#endregion
|
|
386
|
-
//#region extensions/irc/src/
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
super(message);
|
|
395
|
-
this.name = "IrcIngressPayloadError";
|
|
396
|
-
}
|
|
397
|
-
};
|
|
398
|
-
function inspectRawPrivmsg(rawLine) {
|
|
399
|
-
const line = parseIrcLine(rawLine);
|
|
400
|
-
if (!line || line.command !== "PRIVMSG") throw new IrcIngressPayloadError("IRC ingress row is not a PRIVMSG line.");
|
|
401
|
-
const rawTarget = line.params[0]?.trim() ?? "";
|
|
402
|
-
const text = line.trailing ?? line.params[1] ?? "";
|
|
403
|
-
const prefix = parseIrcPrefix(line.prefix);
|
|
404
|
-
const senderNick = prefix.nick?.trim() ?? "";
|
|
405
|
-
if (!rawTarget || !senderNick || !text.trim()) throw new IrcIngressPayloadError("IRC PRIVMSG line is missing target, sender, or text.");
|
|
406
|
-
const isGroup = isChannelTarget(rawTarget);
|
|
407
|
-
const target = isGroup ? rawTarget : senderNick;
|
|
408
|
-
const lanePeer = normalizeLowercaseStringOrEmpty(target);
|
|
409
|
-
return {
|
|
410
|
-
laneKey: `${isGroup ? "channel" : "direct"}:${lanePeer}`,
|
|
411
|
-
message: {
|
|
412
|
-
target,
|
|
413
|
-
rawTarget,
|
|
414
|
-
senderNick,
|
|
415
|
-
senderUser: prefix.user?.trim() || void 0,
|
|
416
|
-
senderHost: prefix.host?.trim() || void 0,
|
|
417
|
-
text,
|
|
418
|
-
isGroup
|
|
419
|
-
}
|
|
420
|
-
};
|
|
421
|
-
}
|
|
422
|
-
function decodeIrcIngressPayload(payload, claimedId) {
|
|
423
|
-
if (!payload || typeof payload !== "object" || Array.isArray(payload) || typeof payload.eventId !== "string" || !Number.isSafeInteger(payload.receivedAt) || (payload.receivedAt ?? 0) <= 0 || typeof payload.connectionEpoch !== "string" || !payload.connectionEpoch?.trim() || typeof payload.connectedNick !== "string" || !payload.connectedNick?.trim() || typeof payload.rawLine !== "string") throw new IrcIngressPayloadError(`IRC ingress row ${claimedId} has invalid metadata.`);
|
|
424
|
-
const validPayload = payload;
|
|
425
|
-
return {
|
|
426
|
-
version: validPayload.version,
|
|
427
|
-
body: {
|
|
428
|
-
eventId: validPayload.eventId,
|
|
429
|
-
receivedAt: validPayload.receivedAt,
|
|
430
|
-
connectionEpoch: validPayload.connectionEpoch,
|
|
431
|
-
connectedNick: validPayload.connectedNick,
|
|
432
|
-
rawLine: validPayload.rawLine
|
|
433
|
-
}
|
|
334
|
+
//#region extensions/irc/src/monitor.ts
|
|
335
|
+
const IRC_MONITOR_RECONNECT_DELAY_MS = 1e3;
|
|
336
|
+
function resolveIrcInboundTarget(params) {
|
|
337
|
+
const rawTarget = params.target;
|
|
338
|
+
if (isChannelTarget(rawTarget)) return {
|
|
339
|
+
isGroup: true,
|
|
340
|
+
target: rawTarget,
|
|
341
|
+
rawTarget
|
|
434
342
|
};
|
|
435
|
-
}
|
|
436
|
-
function inspectIrcIngress(raw, phase) {
|
|
437
|
-
try {
|
|
438
|
-
return {
|
|
439
|
-
eventId: raw.eventId,
|
|
440
|
-
laneKey: inspectRawPrivmsg(raw.rawLine).laneKey
|
|
441
|
-
};
|
|
442
|
-
} catch (error) {
|
|
443
|
-
if (phase === "admission" && error instanceof IrcIngressPayloadError) return {
|
|
444
|
-
eventId: raw.eventId,
|
|
445
|
-
laneKey: `invalid:${raw.eventId}`
|
|
446
|
-
};
|
|
447
|
-
throw error;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
function resolveIrcIngressNonRetryableFailure(error) {
|
|
451
|
-
return error instanceof IrcIngressPayloadError ? {
|
|
452
|
-
reason: "invalid-event",
|
|
453
|
-
message: error.message
|
|
454
|
-
} : null;
|
|
455
|
-
}
|
|
456
|
-
function createIrcIngressMonitor(options) {
|
|
457
|
-
const monitor = createChannelIngressMonitor({
|
|
458
|
-
queue: options.queue ?? (() => getIrcRuntime().state.openChannelIngressQueue({ accountId: options.accountId })),
|
|
459
|
-
inspect: (raw, context) => inspectIrcIngress(raw, context.phase),
|
|
460
|
-
payload: {
|
|
461
|
-
version: IRC_INGRESS_PAYLOAD_VERSION,
|
|
462
|
-
serialize: (raw) => raw,
|
|
463
|
-
deserialize: (body) => body,
|
|
464
|
-
encode: ({ body }) => ({
|
|
465
|
-
version: IRC_INGRESS_PAYLOAD_VERSION,
|
|
466
|
-
...body
|
|
467
|
-
}),
|
|
468
|
-
decode: (payload, { claim }) => decodeIrcIngressPayload(payload, claim.id),
|
|
469
|
-
createClaimError: (_kind, claim) => new IrcIngressPayloadError(`IRC ingress row ${claim.id} has invalid metadata.`)
|
|
470
|
-
},
|
|
471
|
-
deliver: (raw, lifecycle, claim) => {
|
|
472
|
-
const message = {
|
|
473
|
-
...inspectRawPrivmsg(raw.rawLine).message,
|
|
474
|
-
messageId: claim.id,
|
|
475
|
-
timestamp: raw.receivedAt
|
|
476
|
-
};
|
|
477
|
-
return options.dispatch(message, lifecycle, {
|
|
478
|
-
connectedNick: raw.connectedNick.trim(),
|
|
479
|
-
connectionEpoch: raw.connectionEpoch.trim()
|
|
480
|
-
});
|
|
481
|
-
},
|
|
482
|
-
pollIntervalMs: options.pollIntervalMs ?? IRC_INGRESS_POLL_INTERVAL_MS,
|
|
483
|
-
retention: {
|
|
484
|
-
pruneIntervalMs: IRC_INGRESS_PRUNE_INTERVAL_MS,
|
|
485
|
-
completedTtlMs: IRC_INGRESS_TOMBSTONE_TTL_MS,
|
|
486
|
-
completedMaxEntries: IRC_INGRESS_TOMBSTONE_MAX_ENTRIES,
|
|
487
|
-
failedTtlMs: IRC_INGRESS_TOMBSTONE_TTL_MS,
|
|
488
|
-
failedMaxEntries: IRC_INGRESS_TOMBSTONE_MAX_ENTRIES
|
|
489
|
-
},
|
|
490
|
-
drain: {
|
|
491
|
-
resolveNonRetryableFailure: resolveIrcIngressNonRetryableFailure,
|
|
492
|
-
...options.adoptionStallTimeoutMs === void 0 ? {} : { adoptionStallTimeoutMs: options.adoptionStallTimeoutMs },
|
|
493
|
-
onLog: (message) => options.runtime.log?.(`irc ${message}`)
|
|
494
|
-
},
|
|
495
|
-
createStoppedError: () => /* @__PURE__ */ new Error("IRC ingress is stopped."),
|
|
496
|
-
onError: (error) => options.runtime.error?.(`irc ingress drain failed: ${String(error)}`)
|
|
497
|
-
});
|
|
498
343
|
return {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
let sequence = 0;
|
|
503
|
-
return {
|
|
504
|
-
connectionEpoch: epoch,
|
|
505
|
-
accept: (rawLine, connectedNick) => {
|
|
506
|
-
if (monitor.isStopped()) return Promise.reject(/* @__PURE__ */ new Error("IRC ingress is stopped."));
|
|
507
|
-
sequence += 1;
|
|
508
|
-
const eventId = `local:${epoch}:${String(sequence).padStart(12, "0")}`;
|
|
509
|
-
const receivedAt = Date.now();
|
|
510
|
-
const normalizedNick = connectedNick.trim();
|
|
511
|
-
if (!normalizedNick) return Promise.reject(/* @__PURE__ */ new Error("IRC ingress connected nickname is required."));
|
|
512
|
-
return monitor.admit({
|
|
513
|
-
eventId,
|
|
514
|
-
rawLine,
|
|
515
|
-
receivedAt,
|
|
516
|
-
connectionEpoch: epoch,
|
|
517
|
-
connectedNick: normalizedNick
|
|
518
|
-
}, { receivedAt }).then(() => void 0);
|
|
519
|
-
}
|
|
520
|
-
};
|
|
521
|
-
},
|
|
522
|
-
start: monitor.start,
|
|
523
|
-
pause: monitor.pause,
|
|
524
|
-
stop: monitor.stop,
|
|
525
|
-
waitForIdle: monitor.waitForIdle
|
|
344
|
+
isGroup: false,
|
|
345
|
+
target: params.senderNick.trim() || rawTarget,
|
|
346
|
+
rawTarget
|
|
526
347
|
};
|
|
527
348
|
}
|
|
528
|
-
//#endregion
|
|
529
|
-
//#region extensions/irc/src/monitor.ts
|
|
530
|
-
const IRC_MONITOR_RECONNECT_DELAY_MS = 1e3;
|
|
531
349
|
async function monitorIrcProvider(opts) {
|
|
532
350
|
const core = getIrcRuntime();
|
|
533
351
|
const cfg = opts.config ?? core.config.current();
|
|
@@ -542,8 +360,6 @@ async function monitorIrcProvider(opts) {
|
|
|
542
360
|
accountId: account.accountId
|
|
543
361
|
});
|
|
544
362
|
let client = null;
|
|
545
|
-
let activeConnectionEpoch = null;
|
|
546
|
-
let ingressPause = Promise.resolve();
|
|
547
363
|
let reconnectTimer = null;
|
|
548
364
|
let stopped = false;
|
|
549
365
|
const monitorAbort = new AbortController();
|
|
@@ -556,48 +372,6 @@ async function monitorIrcProvider(opts) {
|
|
|
556
372
|
removeAbortListener = () => opts.abortSignal?.removeEventListener("abort", forwardAbort);
|
|
557
373
|
}
|
|
558
374
|
}
|
|
559
|
-
const ingress = createIrcIngressMonitor({
|
|
560
|
-
accountId: account.accountId,
|
|
561
|
-
runtime,
|
|
562
|
-
...opts.ingressQueue ? { queue: opts.ingressQueue } : {},
|
|
563
|
-
dispatch: async (message, turnAdoptionLifecycle, context) => {
|
|
564
|
-
const activeClient = client;
|
|
565
|
-
if (!activeClient || stopped || monitorAbort.signal.aborted) return {
|
|
566
|
-
kind: "failed-retryable",
|
|
567
|
-
error: /* @__PURE__ */ new Error("IRC transport disconnected before ingress dispatch.")
|
|
568
|
-
};
|
|
569
|
-
if (normalizeLowercaseStringOrEmpty(message.senderNick) === normalizeLowercaseStringOrEmpty(context.connectedNick)) return { kind: "completed" };
|
|
570
|
-
if (!message.isGroup && context.connectionEpoch !== activeConnectionEpoch) {
|
|
571
|
-
logger.warn?.(`[${account.accountId}] dropping replayed IRC DM after the connection changed`);
|
|
572
|
-
return { kind: "completed" };
|
|
573
|
-
}
|
|
574
|
-
if (opts.onMessage) {
|
|
575
|
-
await opts.onMessage(message, activeClient);
|
|
576
|
-
return { kind: "completed" };
|
|
577
|
-
}
|
|
578
|
-
return await handleIrcInbound({
|
|
579
|
-
message,
|
|
580
|
-
account,
|
|
581
|
-
config: cfg,
|
|
582
|
-
runtime,
|
|
583
|
-
connectedNick: context.connectedNick,
|
|
584
|
-
turnAdoptionLifecycle,
|
|
585
|
-
sendReply: async (target, text) => {
|
|
586
|
-
const replyClient = client;
|
|
587
|
-
if (!replyClient || !replyClient.isReady() || stopped || monitorAbort.signal.aborted) throw new Error("IRC transport disconnected before reply send.");
|
|
588
|
-
if (!message.isGroup && context.connectionEpoch !== activeConnectionEpoch) throw new Error("IRC connection changed before private reply send.");
|
|
589
|
-
replyClient.sendPrivmsg(target, text);
|
|
590
|
-
opts.statusSink?.({ lastOutboundAt: Date.now() });
|
|
591
|
-
core.channel.activity.record({
|
|
592
|
-
channel: "irc",
|
|
593
|
-
accountId: account.accountId,
|
|
594
|
-
direction: "outbound"
|
|
595
|
-
});
|
|
596
|
-
},
|
|
597
|
-
statusSink: opts.statusSink
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
});
|
|
601
375
|
function scheduleReconnect() {
|
|
602
376
|
if (stopped || monitorAbort.signal.aborted || reconnectTimer) return;
|
|
603
377
|
reconnectTimer = setTimeout(() => {
|
|
@@ -612,7 +386,6 @@ async function monitorIrcProvider(opts) {
|
|
|
612
386
|
}
|
|
613
387
|
async function connect() {
|
|
614
388
|
if (stopped || monitorAbort.signal.aborted) return;
|
|
615
|
-
const ingressConnection = ingress.openConnection();
|
|
616
389
|
const nextClient = await connectIrcClient(buildIrcConnectOptions(account, {
|
|
617
390
|
channels: account.config.channels,
|
|
618
391
|
abortSignal: monitorAbort.signal,
|
|
@@ -627,20 +400,54 @@ async function monitorIrcProvider(opts) {
|
|
|
627
400
|
},
|
|
628
401
|
onDisconnect: () => {
|
|
629
402
|
if (stopped || monitorAbort.signal.aborted) return;
|
|
630
|
-
ingressPause = ingress.pause();
|
|
631
|
-
if (activeConnectionEpoch === ingressConnection.connectionEpoch) activeConnectionEpoch = null;
|
|
632
403
|
client = null;
|
|
633
404
|
logger.warn?.(`[${account.accountId}] IRC connection closed; reconnecting in ${IRC_MONITOR_RECONNECT_DELAY_MS}ms`);
|
|
634
405
|
scheduleReconnect();
|
|
635
406
|
},
|
|
636
407
|
onPrivmsg: async (event) => {
|
|
637
|
-
|
|
638
|
-
if (normalizeLowercaseStringOrEmpty(event.senderNick) === normalizeLowercaseStringOrEmpty(
|
|
408
|
+
if (!client) return;
|
|
409
|
+
if (normalizeLowercaseStringOrEmpty(event.senderNick) === normalizeLowercaseStringOrEmpty(client.nick)) return;
|
|
410
|
+
const inboundTarget = resolveIrcInboundTarget({
|
|
411
|
+
target: event.target,
|
|
412
|
+
senderNick: event.senderNick
|
|
413
|
+
});
|
|
414
|
+
const message = {
|
|
415
|
+
messageId: makeIrcMessageId(),
|
|
416
|
+
target: inboundTarget.target,
|
|
417
|
+
rawTarget: inboundTarget.rawTarget,
|
|
418
|
+
senderNick: event.senderNick,
|
|
419
|
+
senderUser: event.senderUser,
|
|
420
|
+
senderHost: event.senderHost,
|
|
421
|
+
text: event.text,
|
|
422
|
+
timestamp: Date.now(),
|
|
423
|
+
isGroup: inboundTarget.isGroup
|
|
424
|
+
};
|
|
639
425
|
core.channel.activity.record({
|
|
640
426
|
channel: "irc",
|
|
641
427
|
accountId: account.accountId,
|
|
642
428
|
direction: "inbound",
|
|
643
|
-
at:
|
|
429
|
+
at: message.timestamp
|
|
430
|
+
});
|
|
431
|
+
if (opts.onMessage) {
|
|
432
|
+
await opts.onMessage(message, client);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
await handleIrcInbound({
|
|
436
|
+
message,
|
|
437
|
+
account,
|
|
438
|
+
config: cfg,
|
|
439
|
+
runtime,
|
|
440
|
+
connectedNick: client.nick,
|
|
441
|
+
sendReply: async (target, text) => {
|
|
442
|
+
client?.sendPrivmsg(target, text);
|
|
443
|
+
opts.statusSink?.({ lastOutboundAt: Date.now() });
|
|
444
|
+
core.channel.activity.record({
|
|
445
|
+
channel: "irc",
|
|
446
|
+
accountId: account.accountId,
|
|
447
|
+
direction: "outbound"
|
|
448
|
+
});
|
|
449
|
+
},
|
|
450
|
+
statusSink: opts.statusSink
|
|
644
451
|
});
|
|
645
452
|
}
|
|
646
453
|
}));
|
|
@@ -649,40 +456,20 @@ async function monitorIrcProvider(opts) {
|
|
|
649
456
|
return;
|
|
650
457
|
}
|
|
651
458
|
client = nextClient;
|
|
652
|
-
activeConnectionEpoch = ingressConnection.connectionEpoch;
|
|
653
|
-
await ingressPause;
|
|
654
|
-
if (client !== nextClient || !nextClient.isReady()) {
|
|
655
|
-
if (activeConnectionEpoch === ingressConnection.connectionEpoch) activeConnectionEpoch = null;
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
658
|
-
ingress.start();
|
|
659
459
|
logger.info(`[${account.accountId}] connected to ${account.host}:${account.port}${account.tls ? " (tls)" : ""} as ${nextClient.nick}`);
|
|
660
460
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
461
|
+
await connect();
|
|
462
|
+
return { stop: () => {
|
|
463
|
+
stopped = true;
|
|
664
464
|
removeAbortListener?.();
|
|
665
465
|
removeAbortListener = null;
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
removeAbortListener?.();
|
|
674
|
-
removeAbortListener = null;
|
|
675
|
-
if (!monitorAbort.signal.aborted) monitorAbort.abort();
|
|
676
|
-
if (reconnectTimer) {
|
|
677
|
-
clearTimeout(reconnectTimer);
|
|
678
|
-
reconnectTimer = null;
|
|
679
|
-
}
|
|
680
|
-
client?.quit("shutdown");
|
|
681
|
-
client = null;
|
|
682
|
-
activeConnectionEpoch = null;
|
|
683
|
-
await ingress.stop();
|
|
684
|
-
})();
|
|
685
|
-
return stopTask;
|
|
466
|
+
if (!monitorAbort.signal.aborted) monitorAbort.abort();
|
|
467
|
+
if (reconnectTimer) {
|
|
468
|
+
clearTimeout(reconnectTimer);
|
|
469
|
+
reconnectTimer = null;
|
|
470
|
+
}
|
|
471
|
+
client?.quit("shutdown");
|
|
472
|
+
client = null;
|
|
686
473
|
} };
|
|
687
474
|
}
|
|
688
475
|
//#endregion
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ChannelGroupEntrySchema, DmPolicySchema, GroupPolicySchema, MarkdownConfigSchema, ReplyRuntimeConfigSchemaShape, buildChannelConfigSchema, buildMultiAccountChannelSchema, requireOpenAllowFrom } from "openclaw/plugin-sdk/channel-config-schema";
|
|
1
|
+
import { DmPolicySchema, GroupPolicySchema, MarkdownConfigSchema, ReplyRuntimeConfigSchemaShape, ToolPolicySchema, buildChannelConfigSchema, requireOpenAllowFrom } from "openclaw/plugin-sdk/channel-config-schema";
|
|
3
2
|
import { z } from "zod";
|
|
4
3
|
//#region extensions/irc/src/config-ui-hints.ts
|
|
5
4
|
const ircChannelConfigUiHints = {
|
|
@@ -7,10 +6,10 @@ const ircChannelConfigUiHints = {
|
|
|
7
6
|
label: "IRC",
|
|
8
7
|
help: "IRC channel provider configuration and compatibility settings for classic IRC transport workflows. Use this section when bridging legacy chat infrastructure into OpenClaw."
|
|
9
8
|
},
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
9
|
+
dmPolicy: {
|
|
10
|
+
label: "IRC DM Policy",
|
|
11
|
+
help: "Direct message access control (\"pairing\" recommended). \"open\" requires channels.irc.allowFrom=[\"*\"]."
|
|
12
|
+
},
|
|
14
13
|
"nickserv.enabled": {
|
|
15
14
|
label: "IRC NickServ Enabled",
|
|
16
15
|
help: "Enable NickServ identify/register after connect (defaults to enabled when password is configured)."
|
|
@@ -35,13 +34,22 @@ const ircChannelConfigUiHints = {
|
|
|
35
34
|
label: "IRC NickServ Register Email",
|
|
36
35
|
help: "Email used with NickServ REGISTER (required when register=true)."
|
|
37
36
|
},
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
37
|
+
configWrites: {
|
|
38
|
+
label: "IRC Config Writes",
|
|
39
|
+
help: "Allow IRC to write config in response to channel events/commands (default: true)."
|
|
40
|
+
}
|
|
42
41
|
};
|
|
43
42
|
//#endregion
|
|
44
43
|
//#region extensions/irc/src/config-schema.ts
|
|
44
|
+
const IrcGroupSchema = z.object({
|
|
45
|
+
requireMention: z.boolean().optional(),
|
|
46
|
+
tools: ToolPolicySchema,
|
|
47
|
+
toolsBySender: z.record(z.string(), ToolPolicySchema).optional(),
|
|
48
|
+
skills: z.array(z.string()).optional(),
|
|
49
|
+
enabled: z.boolean().optional(),
|
|
50
|
+
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
51
|
+
systemPrompt: z.string().optional()
|
|
52
|
+
}).strict();
|
|
45
53
|
const IrcNickServSchema = z.object({
|
|
46
54
|
enabled: z.boolean().optional(),
|
|
47
55
|
service: z.string().optional(),
|
|
@@ -56,7 +64,7 @@ const IrcNickServSchema = z.object({
|
|
|
56
64
|
message: "channels.irc.nickserv.register=true requires channels.irc.nickserv.registerEmail"
|
|
57
65
|
});
|
|
58
66
|
});
|
|
59
|
-
const
|
|
67
|
+
const IrcAccountSchemaBase = z.object({
|
|
60
68
|
name: z.string().optional(),
|
|
61
69
|
enabled: z.boolean().optional(),
|
|
62
70
|
dangerouslyAllowNameMatching: z.boolean().optional(),
|
|
@@ -73,22 +81,33 @@ const IrcChannelConfigSchema = buildChannelConfigSchema(buildMultiAccountChannel
|
|
|
73
81
|
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
74
82
|
groupPolicy: GroupPolicySchema.optional().default("allowlist"),
|
|
75
83
|
groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
76
|
-
groups: z.record(z.string(),
|
|
84
|
+
groups: z.record(z.string(), IrcGroupSchema.optional()).optional(),
|
|
77
85
|
channels: z.array(z.string()).optional(),
|
|
78
86
|
mentionPatterns: z.array(z.string()).optional(),
|
|
79
87
|
markdown: MarkdownConfigSchema,
|
|
80
88
|
...ReplyRuntimeConfigSchemaShape
|
|
81
|
-
}).strict()
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
}).strict();
|
|
90
|
+
const IrcAccountSchema = IrcAccountSchemaBase.superRefine((value, ctx) => {
|
|
91
|
+
requireOpenAllowFrom({
|
|
92
|
+
policy: value.dmPolicy,
|
|
93
|
+
allowFrom: value.allowFrom,
|
|
94
|
+
ctx,
|
|
95
|
+
path: ["allowFrom"],
|
|
96
|
+
message: "channels.irc.dmPolicy=\"open\" requires channels.irc.allowFrom to include \"*\""
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
const IrcConfigSchema = IrcAccountSchemaBase.extend({
|
|
100
|
+
accounts: z.record(z.string(), IrcAccountSchema.optional()).optional(),
|
|
101
|
+
defaultAccount: z.string().optional()
|
|
102
|
+
}).superRefine((value, ctx) => {
|
|
103
|
+
requireOpenAllowFrom({
|
|
104
|
+
policy: value.dmPolicy,
|
|
105
|
+
allowFrom: value.allowFrom,
|
|
106
|
+
ctx,
|
|
107
|
+
path: ["allowFrom"],
|
|
108
|
+
message: "channels.irc.dmPolicy=\"open\" requires channels.irc.allowFrom to include \"*\""
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
const IrcChannelConfigSchema = buildChannelConfigSchema(IrcConfigSchema, { uiHints: ircChannelConfigUiHints });
|
|
93
112
|
//#endregion
|
|
94
113
|
export { IrcChannelConfigSchema as t };
|