@openclaw/irc 2026.7.2-beta.7 → 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-DoavT7vs.js → .setup/channel-SaGThQQW.mjs} +146 -199
- package/dist/.setup/channel-plugin-api-04wlnIps.mjs +3 -0
- package/dist/{channel-runtime-DsN_0hPp.js → .setup/channel-runtime-DBtfWzX8.mjs} +113 -318
- 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 { a as
|
|
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, createChannelIngressError, 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
|
|
@@ -108,10 +107,7 @@ function routeDescriptorsForIrcGroup(params) {
|
|
|
108
107
|
}
|
|
109
108
|
async function deliverIrcReply(params) {
|
|
110
109
|
await deliverFormattedTextWithAttachments({
|
|
111
|
-
payload:
|
|
112
|
-
...params.payload,
|
|
113
|
-
text: sanitizeIrcAssistantText(params.payload.text ?? "")
|
|
114
|
-
},
|
|
110
|
+
payload: params.payload,
|
|
115
111
|
send: async ({ text, replyToId }) => {
|
|
116
112
|
if (params.sendReply) await params.sendReply(params.target, text, replyToId);
|
|
117
113
|
else await sendMessageIrc(params.target, text, {
|
|
@@ -124,7 +120,7 @@ async function deliverIrcReply(params) {
|
|
|
124
120
|
});
|
|
125
121
|
}
|
|
126
122
|
async function handleIrcInbound(params) {
|
|
127
|
-
const { message, account, config, runtime, connectedNick, statusSink
|
|
123
|
+
const { message, account, config, runtime, connectedNick, statusSink } = params;
|
|
128
124
|
const core = getIrcRuntime();
|
|
129
125
|
const pairing = createChannelPairingController({
|
|
130
126
|
core,
|
|
@@ -132,11 +128,7 @@ async function handleIrcInbound(params) {
|
|
|
132
128
|
accountId: account.accountId
|
|
133
129
|
});
|
|
134
130
|
const rawBody = message.text?.trim() ?? "";
|
|
135
|
-
if (!rawBody) return
|
|
136
|
-
if (turnAdoptionLifecycle?.abortSignal.aborted) return {
|
|
137
|
-
kind: "failed-retryable",
|
|
138
|
-
error: turnAdoptionLifecycle.abortSignal.reason
|
|
139
|
-
};
|
|
131
|
+
if (!rawBody) return;
|
|
140
132
|
statusSink?.({ lastInboundAt: message.timestamp });
|
|
141
133
|
const senderDisplay = message.senderHost ? `${message.senderNick}!${message.senderUser ?? "?"}@${message.senderHost}` : message.senderNick;
|
|
142
134
|
const allowNameMatching = isDangerousNameMatchingEnabled(account.config);
|
|
@@ -167,9 +159,9 @@ async function handleIrcInbound(params) {
|
|
|
167
159
|
const mentionNick = connectedNick?.trim() || account.nick;
|
|
168
160
|
const explicitMentionRegex = mentionNick ? new RegExp(`\\b${escapeIrcRegexLiteral(mentionNick)}\\b[:,]?`, "i") : null;
|
|
169
161
|
const wasMentioned = core.channel.mentions.matchesMentionPatterns(rawBody, mentionRegexes) || (explicitMentionRegex ? explicitMentionRegex.test(rawBody) : false);
|
|
170
|
-
const requireMention = message.isGroup ?
|
|
171
|
-
|
|
172
|
-
|
|
162
|
+
const requireMention = message.isGroup ? resolveIrcRequireMention({
|
|
163
|
+
groupConfig: groupMatch.groupConfig,
|
|
164
|
+
wildcardConfig: groupMatch.wildcardConfig
|
|
173
165
|
}) : false;
|
|
174
166
|
const routeGroupAllowFrom = normalizeStringEntries(groupMatch.groupConfig?.allowFrom?.length ? groupMatch.groupConfig.allowFrom : groupMatch.wildcardConfig?.allowFrom);
|
|
175
167
|
const accessGroupPolicy = groupPolicy === "open" && (hasEntries(account.config.groupAllowFrom) || hasEntries(routeGroupAllowFrom)) ? "allowlist" : groupPolicy;
|
|
@@ -236,11 +228,11 @@ async function handleIrcInbound(params) {
|
|
|
236
228
|
}
|
|
237
229
|
});
|
|
238
230
|
runtime.log?.(`irc: drop DM sender ${senderDisplay} (dmPolicy=${dmPolicy})`);
|
|
239
|
-
return
|
|
231
|
+
return;
|
|
240
232
|
}
|
|
241
233
|
if (access.ingress.admission === "skip") {
|
|
242
234
|
runtime.log?.(`irc: drop channel ${message.target} (missing-mention)`);
|
|
243
|
-
return
|
|
235
|
+
return;
|
|
244
236
|
}
|
|
245
237
|
if (access.ingress.admission !== "dispatch") {
|
|
246
238
|
if (message.isGroup && access.ingress.decisiveGateId === "command" && access.commandAccess.shouldBlockControlCommand) {
|
|
@@ -250,106 +242,69 @@ async function handleIrcInbound(params) {
|
|
|
250
242
|
reason: "control command (unauthorized)",
|
|
251
243
|
target: senderDisplay
|
|
252
244
|
});
|
|
253
|
-
return
|
|
245
|
+
return;
|
|
254
246
|
}
|
|
255
|
-
if (message.isGroup)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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;
|
|
260
253
|
}
|
|
261
|
-
if (turnAdoptionLifecycle?.abortSignal.aborted) return {
|
|
262
|
-
kind: "failed-retryable",
|
|
263
|
-
error: turnAdoptionLifecycle.abortSignal.reason
|
|
264
|
-
};
|
|
265
254
|
const channelTarget = message.target.startsWith("#") || message.target.startsWith("&") ? message.target : `#${message.target}`;
|
|
266
255
|
const peerId = message.isGroup ? channelTarget : message.senderNick;
|
|
267
|
-
const { route, buildEnvelope } =
|
|
256
|
+
const { route, buildEnvelope } = resolveInboundRouteEnvelopeBuilderWithRuntime({
|
|
268
257
|
cfg: config,
|
|
269
258
|
channel: CHANNEL_ID,
|
|
270
259
|
accountId: account.accountId,
|
|
271
260
|
peer: {
|
|
272
261
|
kind: message.isGroup ? "group" : "direct",
|
|
273
262
|
id: peerId
|
|
274
|
-
}
|
|
263
|
+
},
|
|
264
|
+
runtime: core.channel,
|
|
265
|
+
sessionStore: config.session?.store
|
|
275
266
|
});
|
|
276
267
|
const fromLabel = message.isGroup ? message.target : senderDisplay;
|
|
277
|
-
const body = buildEnvelope({
|
|
268
|
+
const { storePath, body } = buildEnvelope({
|
|
278
269
|
channel: "IRC",
|
|
279
270
|
from: fromLabel,
|
|
280
271
|
timestamp: message.timestamp,
|
|
281
272
|
body: rawBody
|
|
282
273
|
});
|
|
283
274
|
const groupSystemPrompt = normalizeOptionalString(groupMatch.groupConfig?.systemPrompt);
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
reply: {
|
|
307
|
-
to: message.isGroup ? `channel:${channelTarget}` : `irc:${peerId}`,
|
|
308
|
-
originatingTo: message.isGroup ? `channel:${channelTarget}` : `irc:${peerId}`
|
|
309
|
-
},
|
|
310
|
-
message: {
|
|
311
|
-
body,
|
|
312
|
-
bodyForAgent: rawBody,
|
|
313
|
-
rawBody,
|
|
314
|
-
commandBody: rawBody
|
|
315
|
-
},
|
|
316
|
-
access: {
|
|
317
|
-
commands: { authorized: commandAuthorized },
|
|
318
|
-
mentions: {
|
|
319
|
-
canDetectMention: message.isGroup,
|
|
320
|
-
wasMentioned
|
|
321
|
-
}
|
|
322
|
-
},
|
|
323
|
-
extra: {
|
|
324
|
-
GroupSubject: message.isGroup ? message.target : void 0,
|
|
325
|
-
GroupSystemPrompt: message.isGroup ? groupSystemPrompt : void 0
|
|
326
|
-
}
|
|
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
|
|
327
297
|
});
|
|
328
|
-
|
|
329
|
-
const trackedIngressLifecycle = turnAdoptionLifecycle ? {
|
|
330
|
-
...turnAdoptionLifecycle,
|
|
331
|
-
onAdopted: async () => {
|
|
332
|
-
ingressState.handoff = "adopted";
|
|
333
|
-
await turnAdoptionLifecycle.onAdopted();
|
|
334
|
-
},
|
|
335
|
-
onDeferred: () => {
|
|
336
|
-
ingressState.handoff = "deferred";
|
|
337
|
-
turnAdoptionLifecycle.onDeferred();
|
|
338
|
-
},
|
|
339
|
-
onAbandoned: async () => {
|
|
340
|
-
ingressState.handoff = "abandoned";
|
|
341
|
-
await turnAdoptionLifecycle.onAbandoned();
|
|
342
|
-
}
|
|
343
|
-
} : void 0;
|
|
344
|
-
await core.channel.inbound.dispatch({
|
|
298
|
+
await core.channel.inbound.dispatchReply({
|
|
345
299
|
cfg: config,
|
|
346
300
|
channel: CHANNEL_ID,
|
|
347
301
|
accountId: account.accountId,
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
},
|
|
302
|
+
agentId: route.agentId,
|
|
303
|
+
routeSessionKey: route.sessionKey,
|
|
304
|
+
storePath,
|
|
352
305
|
ctxPayload,
|
|
306
|
+
recordInboundSession: core.channel.session.recordInboundSession,
|
|
307
|
+
dispatchReplyWithBufferedBlockDispatcher: core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
|
|
353
308
|
delivery: {
|
|
354
309
|
deliver: async (payload) => {
|
|
355
310
|
await deliverIrcReply({
|
|
@@ -367,159 +322,30 @@ async function handleIrcInbound(params) {
|
|
|
367
322
|
},
|
|
368
323
|
replyPipeline: {},
|
|
369
324
|
replyOptions: {
|
|
370
|
-
...trackedIngressLifecycle ? bindIngressLifecycleToReplyOptions(trackedIngressLifecycle) : {},
|
|
371
325
|
skillFilter: groupMatch.groupConfig?.skills,
|
|
372
|
-
disableBlockStreaming: typeof
|
|
326
|
+
disableBlockStreaming: typeof account.config.blockStreaming === "boolean" ? !account.config.blockStreaming : void 0
|
|
373
327
|
},
|
|
374
328
|
record: { onRecordError: (err) => {
|
|
375
329
|
runtime.error?.(`irc: failed updating session meta: ${String(err)}`);
|
|
376
330
|
} }
|
|
377
331
|
});
|
|
378
|
-
if (turnAdoptionLifecycle?.abortSignal.aborted && ingressState.handoff === "none") return {
|
|
379
|
-
kind: "failed-retryable",
|
|
380
|
-
error: turnAdoptionLifecycle.abortSignal.reason
|
|
381
|
-
};
|
|
382
|
-
if (turnAdoptionLifecycle && ingressState.handoff === "none") {
|
|
383
|
-
await turnAdoptionLifecycle.onAdopted();
|
|
384
|
-
ingressState.handoff = "adopted";
|
|
385
|
-
}
|
|
386
|
-
return ingressState.handoff === "deferred" || ingressState.handoff === "abandoned" ? { kind: "deferred" } : { kind: "completed" };
|
|
387
332
|
}
|
|
388
333
|
//#endregion
|
|
389
|
-
//#region extensions/irc/src/
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
const text = line.trailing ?? line.params[1] ?? "";
|
|
398
|
-
const prefix = parseIrcPrefix(line.prefix);
|
|
399
|
-
const senderNick = prefix.nick?.trim() ?? "";
|
|
400
|
-
if (!rawTarget || !senderNick || !text.trim()) throw new IrcIngressPayloadError("IRC PRIVMSG line is missing target, sender, or text.");
|
|
401
|
-
const isGroup = isChannelTarget(rawTarget);
|
|
402
|
-
const target = isGroup ? rawTarget : senderNick;
|
|
403
|
-
const lanePeer = normalizeLowercaseStringOrEmpty(target);
|
|
404
|
-
return {
|
|
405
|
-
laneKey: `${isGroup ? "channel" : "direct"}:${lanePeer}`,
|
|
406
|
-
message: {
|
|
407
|
-
target,
|
|
408
|
-
rawTarget,
|
|
409
|
-
senderNick,
|
|
410
|
-
senderUser: prefix.user?.trim() || void 0,
|
|
411
|
-
senderHost: prefix.host?.trim() || void 0,
|
|
412
|
-
text,
|
|
413
|
-
isGroup
|
|
414
|
-
}
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
function decodeIrcIngressPayload(payload, claimedId) {
|
|
418
|
-
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.`);
|
|
419
|
-
const validPayload = payload;
|
|
420
|
-
return {
|
|
421
|
-
version: validPayload.version,
|
|
422
|
-
body: {
|
|
423
|
-
eventId: validPayload.eventId,
|
|
424
|
-
receivedAt: validPayload.receivedAt,
|
|
425
|
-
connectionEpoch: validPayload.connectionEpoch,
|
|
426
|
-
connectedNick: validPayload.connectedNick,
|
|
427
|
-
rawLine: validPayload.rawLine
|
|
428
|
-
}
|
|
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
|
|
429
342
|
};
|
|
430
|
-
}
|
|
431
|
-
function inspectIrcIngress(raw, phase) {
|
|
432
|
-
try {
|
|
433
|
-
return {
|
|
434
|
-
eventId: raw.eventId,
|
|
435
|
-
laneKey: inspectRawPrivmsg(raw.rawLine).laneKey
|
|
436
|
-
};
|
|
437
|
-
} catch (error) {
|
|
438
|
-
if (phase === "admission" && error instanceof IrcIngressPayloadError) return {
|
|
439
|
-
eventId: raw.eventId,
|
|
440
|
-
laneKey: `invalid:${raw.eventId}`
|
|
441
|
-
};
|
|
442
|
-
throw error;
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
function resolveIrcIngressNonRetryableFailure(error) {
|
|
446
|
-
return error instanceof IrcIngressPayloadError ? {
|
|
447
|
-
reason: "invalid-event",
|
|
448
|
-
message: error.message
|
|
449
|
-
} : null;
|
|
450
|
-
}
|
|
451
|
-
function createIrcIngressMonitor(options) {
|
|
452
|
-
const monitor = createChannelIngressMonitor({
|
|
453
|
-
queue: options.queue ?? (() => getIrcRuntime().state.openChannelIngressQueue({ accountId: options.accountId })),
|
|
454
|
-
inspect: (raw, context) => inspectIrcIngress(raw, context.phase),
|
|
455
|
-
payload: {
|
|
456
|
-
version: IRC_INGRESS_PAYLOAD_VERSION,
|
|
457
|
-
serialize: (raw) => raw,
|
|
458
|
-
deserialize: (body) => body,
|
|
459
|
-
encode: ({ body }) => ({
|
|
460
|
-
version: IRC_INGRESS_PAYLOAD_VERSION,
|
|
461
|
-
...body
|
|
462
|
-
}),
|
|
463
|
-
decode: (payload, { claim }) => decodeIrcIngressPayload(payload, claim.id),
|
|
464
|
-
createClaimError: (_kind, claim) => new IrcIngressPayloadError(`IRC ingress row ${claim.id} has invalid metadata.`)
|
|
465
|
-
},
|
|
466
|
-
deliver: (raw, lifecycle, claim) => {
|
|
467
|
-
const message = {
|
|
468
|
-
...inspectRawPrivmsg(raw.rawLine).message,
|
|
469
|
-
messageId: claim.id,
|
|
470
|
-
timestamp: raw.receivedAt
|
|
471
|
-
};
|
|
472
|
-
return options.dispatch(message, lifecycle, {
|
|
473
|
-
connectedNick: raw.connectedNick.trim(),
|
|
474
|
-
connectionEpoch: raw.connectionEpoch.trim()
|
|
475
|
-
});
|
|
476
|
-
},
|
|
477
|
-
pollIntervalMs: options.pollIntervalMs ?? IRC_INGRESS_POLL_INTERVAL_MS,
|
|
478
|
-
retention: {
|
|
479
|
-
completedMaxEntries: 1e3,
|
|
480
|
-
failedMaxEntries: 1e3
|
|
481
|
-
},
|
|
482
|
-
drain: {
|
|
483
|
-
resolveNonRetryableFailure: resolveIrcIngressNonRetryableFailure,
|
|
484
|
-
...options.adoptionStallTimeoutMs === void 0 ? {} : { adoptionStallTimeoutMs: options.adoptionStallTimeoutMs },
|
|
485
|
-
onLog: (message) => options.runtime.log?.(`irc ${message}`)
|
|
486
|
-
},
|
|
487
|
-
createStoppedError: () => /* @__PURE__ */ new Error("IRC ingress is stopped."),
|
|
488
|
-
onError: (error) => options.runtime.error?.(`irc ingress drain failed: ${String(error)}`)
|
|
489
|
-
});
|
|
490
343
|
return {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
let sequence = 0;
|
|
495
|
-
return {
|
|
496
|
-
connectionEpoch: epoch,
|
|
497
|
-
accept: (rawLine, connectedNick) => {
|
|
498
|
-
if (monitor.isStopped()) return Promise.reject(/* @__PURE__ */ new Error("IRC ingress is stopped."));
|
|
499
|
-
sequence += 1;
|
|
500
|
-
const eventId = `local:${epoch}:${String(sequence).padStart(12, "0")}`;
|
|
501
|
-
const receivedAt = Date.now();
|
|
502
|
-
const normalizedNick = connectedNick.trim();
|
|
503
|
-
if (!normalizedNick) return Promise.reject(/* @__PURE__ */ new Error("IRC ingress connected nickname is required."));
|
|
504
|
-
return monitor.admit({
|
|
505
|
-
eventId,
|
|
506
|
-
rawLine,
|
|
507
|
-
receivedAt,
|
|
508
|
-
connectionEpoch: epoch,
|
|
509
|
-
connectedNick: normalizedNick
|
|
510
|
-
}, { receivedAt }).then(() => void 0);
|
|
511
|
-
}
|
|
512
|
-
};
|
|
513
|
-
},
|
|
514
|
-
start: monitor.start,
|
|
515
|
-
pause: monitor.pause,
|
|
516
|
-
stop: monitor.stop,
|
|
517
|
-
waitForIdle: monitor.waitForIdle
|
|
344
|
+
isGroup: false,
|
|
345
|
+
target: params.senderNick.trim() || rawTarget,
|
|
346
|
+
rawTarget
|
|
518
347
|
};
|
|
519
348
|
}
|
|
520
|
-
//#endregion
|
|
521
|
-
//#region extensions/irc/src/monitor.ts
|
|
522
|
-
const IRC_MONITOR_RECONNECT_DELAY_MS = 1e3;
|
|
523
349
|
async function monitorIrcProvider(opts) {
|
|
524
350
|
const core = getIrcRuntime();
|
|
525
351
|
const cfg = opts.config ?? core.config.current();
|
|
@@ -534,8 +360,6 @@ async function monitorIrcProvider(opts) {
|
|
|
534
360
|
accountId: account.accountId
|
|
535
361
|
});
|
|
536
362
|
let client = null;
|
|
537
|
-
let activeConnectionEpoch = null;
|
|
538
|
-
let ingressPause = Promise.resolve();
|
|
539
363
|
let reconnectTimer = null;
|
|
540
364
|
let stopped = false;
|
|
541
365
|
const monitorAbort = new AbortController();
|
|
@@ -548,48 +372,6 @@ async function monitorIrcProvider(opts) {
|
|
|
548
372
|
removeAbortListener = () => opts.abortSignal?.removeEventListener("abort", forwardAbort);
|
|
549
373
|
}
|
|
550
374
|
}
|
|
551
|
-
const ingress = createIrcIngressMonitor({
|
|
552
|
-
accountId: account.accountId,
|
|
553
|
-
runtime,
|
|
554
|
-
...opts.ingressQueue ? { queue: opts.ingressQueue } : {},
|
|
555
|
-
dispatch: async (message, turnAdoptionLifecycle, context) => {
|
|
556
|
-
const activeClient = client;
|
|
557
|
-
if (!activeClient || stopped || monitorAbort.signal.aborted) return {
|
|
558
|
-
kind: "failed-retryable",
|
|
559
|
-
error: /* @__PURE__ */ new Error("IRC transport disconnected before ingress dispatch.")
|
|
560
|
-
};
|
|
561
|
-
if (normalizeLowercaseStringOrEmpty(message.senderNick) === normalizeLowercaseStringOrEmpty(context.connectedNick)) return { kind: "completed" };
|
|
562
|
-
if (!message.isGroup && context.connectionEpoch !== activeConnectionEpoch) {
|
|
563
|
-
logger.warn?.(`[${account.accountId}] dropping replayed IRC DM after the connection changed`);
|
|
564
|
-
return { kind: "completed" };
|
|
565
|
-
}
|
|
566
|
-
if (opts.onMessage) {
|
|
567
|
-
await opts.onMessage(message, activeClient);
|
|
568
|
-
return { kind: "completed" };
|
|
569
|
-
}
|
|
570
|
-
return await handleIrcInbound({
|
|
571
|
-
message,
|
|
572
|
-
account,
|
|
573
|
-
config: cfg,
|
|
574
|
-
runtime,
|
|
575
|
-
connectedNick: context.connectedNick,
|
|
576
|
-
turnAdoptionLifecycle,
|
|
577
|
-
sendReply: async (target, text) => {
|
|
578
|
-
const replyClient = client;
|
|
579
|
-
if (!replyClient || !replyClient.isReady() || stopped || monitorAbort.signal.aborted) throw new Error("IRC transport disconnected before reply send.");
|
|
580
|
-
if (!message.isGroup && context.connectionEpoch !== activeConnectionEpoch) throw new Error("IRC connection changed before private reply send.");
|
|
581
|
-
replyClient.sendPrivmsg(target, text);
|
|
582
|
-
opts.statusSink?.({ lastOutboundAt: Date.now() });
|
|
583
|
-
core.channel.activity.record({
|
|
584
|
-
channel: "irc",
|
|
585
|
-
accountId: account.accountId,
|
|
586
|
-
direction: "outbound"
|
|
587
|
-
});
|
|
588
|
-
},
|
|
589
|
-
statusSink: opts.statusSink
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
});
|
|
593
375
|
function scheduleReconnect() {
|
|
594
376
|
if (stopped || monitorAbort.signal.aborted || reconnectTimer) return;
|
|
595
377
|
reconnectTimer = setTimeout(() => {
|
|
@@ -604,7 +386,6 @@ async function monitorIrcProvider(opts) {
|
|
|
604
386
|
}
|
|
605
387
|
async function connect() {
|
|
606
388
|
if (stopped || monitorAbort.signal.aborted) return;
|
|
607
|
-
const ingressConnection = ingress.openConnection();
|
|
608
389
|
const nextClient = await connectIrcClient(buildIrcConnectOptions(account, {
|
|
609
390
|
channels: account.config.channels,
|
|
610
391
|
abortSignal: monitorAbort.signal,
|
|
@@ -619,20 +400,54 @@ async function monitorIrcProvider(opts) {
|
|
|
619
400
|
},
|
|
620
401
|
onDisconnect: () => {
|
|
621
402
|
if (stopped || monitorAbort.signal.aborted) return;
|
|
622
|
-
ingressPause = ingress.pause();
|
|
623
|
-
if (activeConnectionEpoch === ingressConnection.connectionEpoch) activeConnectionEpoch = null;
|
|
624
403
|
client = null;
|
|
625
404
|
logger.warn?.(`[${account.accountId}] IRC connection closed; reconnecting in ${IRC_MONITOR_RECONNECT_DELAY_MS}ms`);
|
|
626
405
|
scheduleReconnect();
|
|
627
406
|
},
|
|
628
407
|
onPrivmsg: async (event) => {
|
|
629
|
-
|
|
630
|
-
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
|
+
};
|
|
631
425
|
core.channel.activity.record({
|
|
632
426
|
channel: "irc",
|
|
633
427
|
accountId: account.accountId,
|
|
634
428
|
direction: "inbound",
|
|
635
|
-
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
|
|
636
451
|
});
|
|
637
452
|
}
|
|
638
453
|
}));
|
|
@@ -641,40 +456,20 @@ async function monitorIrcProvider(opts) {
|
|
|
641
456
|
return;
|
|
642
457
|
}
|
|
643
458
|
client = nextClient;
|
|
644
|
-
activeConnectionEpoch = ingressConnection.connectionEpoch;
|
|
645
|
-
await ingressPause;
|
|
646
|
-
if (client !== nextClient || !nextClient.isReady()) {
|
|
647
|
-
if (activeConnectionEpoch === ingressConnection.connectionEpoch) activeConnectionEpoch = null;
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
650
|
-
ingress.start();
|
|
651
459
|
logger.info(`[${account.accountId}] connected to ${account.host}:${account.port}${account.tls ? " (tls)" : ""} as ${nextClient.nick}`);
|
|
652
460
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
461
|
+
await connect();
|
|
462
|
+
return { stop: () => {
|
|
463
|
+
stopped = true;
|
|
656
464
|
removeAbortListener?.();
|
|
657
465
|
removeAbortListener = null;
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
removeAbortListener?.();
|
|
666
|
-
removeAbortListener = null;
|
|
667
|
-
if (!monitorAbort.signal.aborted) monitorAbort.abort();
|
|
668
|
-
if (reconnectTimer) {
|
|
669
|
-
clearTimeout(reconnectTimer);
|
|
670
|
-
reconnectTimer = null;
|
|
671
|
-
}
|
|
672
|
-
client?.quit("shutdown");
|
|
673
|
-
client = null;
|
|
674
|
-
activeConnectionEpoch = null;
|
|
675
|
-
await ingress.stop();
|
|
676
|
-
})();
|
|
677
|
-
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;
|
|
678
473
|
} };
|
|
679
474
|
}
|
|
680
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 };
|