diff --git a/extensions/irc/src/channel.ts b/extensions/irc/src/channel.ts index a249d7d9172..4280dad5508 100644 --- a/extensions/irc/src/channel.ts +++ b/extensions/irc/src/channel.ts @@ -11,7 +11,7 @@ import { createAllowlistProviderOpenWarningCollector, createConditionalWarningCollector, } from "openclaw/plugin-sdk/channel-policy"; -import { createAttachedChannelResultAdapter } from "openclaw/plugin-sdk/channel-send-result"; +import { createChatChannelPlugin } from "openclaw/plugin-sdk/core"; import { createChannelDirectoryAdapter, listResolvedDirectoryEntriesFromSources, @@ -127,14 +127,192 @@ const collectIrcSecurityWarnings = composeWarningCollectors<{ ), ); -export const ircPlugin: ChannelPlugin = { - id: "irc", - meta: { - ...meta, - quickstartAllowFrom: true, +export const ircPlugin: ChannelPlugin = createChatChannelPlugin({ + base: { + id: "irc", + meta: { + ...meta, + quickstartAllowFrom: true, + }, + setup: ircSetupAdapter, + setupWizard: ircSetupWizard, + capabilities: { + chatTypes: ["direct", "group"], + media: true, + blockStreaming: true, + }, + reload: { configPrefixes: ["channels.irc"] }, + configSchema: buildChannelConfigSchema(IrcConfigSchema), + config: { + ...ircConfigAdapter, + isConfigured: (account) => account.configured, + describeAccount: (account) => + describeAccountSnapshot({ + account, + configured: account.configured, + extra: { + host: account.host, + port: account.port, + tls: account.tls, + nick: account.nick, + passwordSource: account.passwordSource, + }, + }), + }, + groups: { + resolveRequireMention: ({ cfg, accountId, groupId }) => { + const account = resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }); + if (!groupId) { + return true; + } + const match = resolveIrcGroupMatch({ groups: account.config.groups, target: groupId }); + return resolveIrcRequireMention({ + groupConfig: match.groupConfig, + wildcardConfig: match.wildcardConfig, + }); + }, + resolveToolPolicy: ({ cfg, accountId, groupId }) => { + const account = resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }); + if (!groupId) { + return undefined; + } + const match = resolveIrcGroupMatch({ groups: account.config.groups, target: groupId }); + return match.groupConfig?.tools ?? match.wildcardConfig?.tools; + }, + }, + messaging: { + normalizeTarget: normalizeIrcMessagingTarget, + targetResolver: { + looksLikeId: looksLikeIrcTargetId, + hint: "<#channel|nick>", + }, + }, + resolver: { + resolveTargets: async ({ inputs, kind }) => { + return inputs.map((input) => { + const normalized = normalizeIrcMessagingTarget(input); + if (!normalized) { + return { + input, + resolved: false, + note: "invalid IRC target", + }; + } + if (kind === "group") { + const groupId = isChannelTarget(normalized) ? normalized : `#${normalized}`; + return { + input, + resolved: true, + id: groupId, + name: groupId, + }; + } + if (isChannelTarget(normalized)) { + return { + input, + resolved: false, + note: "expected user target", + }; + } + return { + input, + resolved: true, + id: normalized, + name: normalized, + }; + }); + }, + }, + directory: createChannelDirectoryAdapter({ + listPeers: async (params) => + listResolvedDirectoryEntriesFromSources({ + ...params, + kind: "user", + resolveAccount: adaptScopedAccountAccessor(resolveIrcAccount), + resolveSources: (account) => [ + account.config.allowFrom ?? [], + account.config.groupAllowFrom ?? [], + ...Object.values(account.config.groups ?? {}).map((group) => group.allowFrom ?? []), + ], + normalizeId: (entry) => normalizePairingTarget(entry) || null, + }), + listGroups: async (params) => { + const entries = listResolvedDirectoryEntriesFromSources({ + ...params, + kind: "group", + resolveAccount: adaptScopedAccountAccessor(resolveIrcAccount), + resolveSources: (account) => [ + account.config.channels ?? [], + Object.keys(account.config.groups ?? {}), + ], + normalizeId: (entry) => { + const normalized = normalizeIrcMessagingTarget(entry); + return normalized && isChannelTarget(normalized) ? normalized : null; + }, + }); + return entries.map((entry) => ({ ...entry, name: entry.id })); + }, + }), + status: { + defaultRuntime: { + accountId: DEFAULT_ACCOUNT_ID, + running: false, + lastStartAt: null, + lastStopAt: null, + lastError: null, + }, + buildChannelSummary: ({ account, snapshot }) => ({ + ...buildBaseChannelStatusSummary(snapshot), + host: account.host, + port: snapshot.port, + tls: account.tls, + nick: account.nick, + probe: snapshot.probe, + lastProbeAt: snapshot.lastProbeAt ?? null, + }), + probeAccount: async ({ cfg, account, timeoutMs }) => + probeIrc(cfg as CoreConfig, { accountId: account.accountId, timeoutMs }), + buildAccountSnapshot: ({ account, runtime, probe }) => + buildBaseAccountStatusSnapshot( + { account, runtime, probe }, + { + host: account.host, + port: account.port, + tls: account.tls, + nick: account.nick, + passwordSource: account.passwordSource, + }, + ), + }, + gateway: { + startAccount: async (ctx) => { + const account = ctx.account; + const statusSink = createAccountStatusSink({ + accountId: ctx.accountId, + setStatus: ctx.setStatus, + }); + if (!account.configured) { + throw new Error( + `IRC is not configured for account "${account.accountId}" (need host and nick in channels.irc).`, + ); + } + ctx.log?.info( + `[${account.accountId}] starting IRC provider (${account.host}:${account.port}${account.tls ? " tls" : ""})`, + ); + await runStoppablePassiveMonitor({ + abortSignal: ctx.abortSignal, + start: async () => + await monitorIrcProvider({ + accountId: account.accountId, + config: ctx.cfg as CoreConfig, + runtime: ctx.runtime, + abortSignal: ctx.abortSignal, + statusSink, + }), + }); + }, + }, }, - setup: ircSetupAdapter, - setupWizard: ircSetupWizard, pairing: createTextPairingAdapter({ idLabel: "ircUser", message: PAIRING_APPROVED_MESSAGE, @@ -147,133 +325,18 @@ export const ircPlugin: ChannelPlugin = { await sendMessageIrc(target, message); }, }), - capabilities: { - chatTypes: ["direct", "group"], - media: true, - blockStreaming: true, - }, - reload: { configPrefixes: ["channels.irc"] }, - configSchema: buildChannelConfigSchema(IrcConfigSchema), - config: { - ...ircConfigAdapter, - isConfigured: (account) => account.configured, - describeAccount: (account) => - describeAccountSnapshot({ - account, - configured: account.configured, - extra: { - host: account.host, - port: account.port, - tls: account.tls, - nick: account.nick, - passwordSource: account.passwordSource, - }, - }), - }, security: { resolveDmPolicy: resolveIrcDmPolicy, collectWarnings: collectIrcSecurityWarnings, }, - groups: { - resolveRequireMention: ({ cfg, accountId, groupId }) => { - const account = resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }); - if (!groupId) { - return true; - } - const match = resolveIrcGroupMatch({ groups: account.config.groups, target: groupId }); - return resolveIrcRequireMention({ - groupConfig: match.groupConfig, - wildcardConfig: match.wildcardConfig, - }); - }, - resolveToolPolicy: ({ cfg, accountId, groupId }) => { - const account = resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }); - if (!groupId) { - return undefined; - } - const match = resolveIrcGroupMatch({ groups: account.config.groups, target: groupId }); - return match.groupConfig?.tools ?? match.wildcardConfig?.tools; - }, - }, - messaging: { - normalizeTarget: normalizeIrcMessagingTarget, - targetResolver: { - looksLikeId: looksLikeIrcTargetId, - hint: "<#channel|nick>", - }, - }, - resolver: { - resolveTargets: async ({ inputs, kind }) => { - return inputs.map((input) => { - const normalized = normalizeIrcMessagingTarget(input); - if (!normalized) { - return { - input, - resolved: false, - note: "invalid IRC target", - }; - } - if (kind === "group") { - const groupId = isChannelTarget(normalized) ? normalized : `#${normalized}`; - return { - input, - resolved: true, - id: groupId, - name: groupId, - }; - } - if (isChannelTarget(normalized)) { - return { - input, - resolved: false, - note: "expected user target", - }; - } - return { - input, - resolved: true, - id: normalized, - name: normalized, - }; - }); - }, - }, - directory: createChannelDirectoryAdapter({ - listPeers: async (params) => - listResolvedDirectoryEntriesFromSources({ - ...params, - kind: "user", - resolveAccount: adaptScopedAccountAccessor(resolveIrcAccount), - resolveSources: (account) => [ - account.config.allowFrom ?? [], - account.config.groupAllowFrom ?? [], - ...Object.values(account.config.groups ?? {}).map((group) => group.allowFrom ?? []), - ], - normalizeId: (entry) => normalizePairingTarget(entry) || null, - }), - listGroups: async (params) => { - const entries = listResolvedDirectoryEntriesFromSources({ - ...params, - kind: "group", - resolveAccount: adaptScopedAccountAccessor(resolveIrcAccount), - resolveSources: (account) => [ - account.config.channels ?? [], - Object.keys(account.config.groups ?? {}), - ], - normalizeId: (entry) => { - const normalized = normalizeIrcMessagingTarget(entry); - return normalized && isChannelTarget(normalized) ? normalized : null; - }, - }); - return entries.map((entry) => ({ ...entry, name: entry.id })); - }, - }), outbound: { - deliveryMode: "direct", - chunker: (text, limit) => getIrcRuntime().channel.text.chunkMarkdownText(text, limit), - chunkerMode: "markdown", - textChunkLimit: 350, - ...createAttachedChannelResultAdapter({ + base: { + deliveryMode: "direct", + chunker: (text, limit) => getIrcRuntime().channel.text.chunkMarkdownText(text, limit), + chunkerMode: "markdown", + textChunkLimit: 350, + }, + attachedResults: { channel: "irc", sendText: async ({ cfg, to, text, accountId, replyToId }) => await sendMessageIrc(to, text, { @@ -287,65 +350,6 @@ export const ircPlugin: ChannelPlugin = { accountId: accountId ?? undefined, replyTo: replyToId ?? undefined, }), - }), - }, - status: { - defaultRuntime: { - accountId: DEFAULT_ACCOUNT_ID, - running: false, - lastStartAt: null, - lastStopAt: null, - lastError: null, - }, - buildChannelSummary: ({ account, snapshot }) => ({ - ...buildBaseChannelStatusSummary(snapshot), - host: account.host, - port: snapshot.port, - tls: account.tls, - nick: account.nick, - probe: snapshot.probe, - lastProbeAt: snapshot.lastProbeAt ?? null, - }), - probeAccount: async ({ cfg, account, timeoutMs }) => - probeIrc(cfg as CoreConfig, { accountId: account.accountId, timeoutMs }), - buildAccountSnapshot: ({ account, runtime, probe }) => - buildBaseAccountStatusSnapshot( - { account, runtime, probe }, - { - host: account.host, - port: account.port, - tls: account.tls, - nick: account.nick, - passwordSource: account.passwordSource, - }, - ), - }, - gateway: { - startAccount: async (ctx) => { - const account = ctx.account; - const statusSink = createAccountStatusSink({ - accountId: ctx.accountId, - setStatus: ctx.setStatus, - }); - if (!account.configured) { - throw new Error( - `IRC is not configured for account "${account.accountId}" (need host and nick in channels.irc).`, - ); - } - ctx.log?.info( - `[${account.accountId}] starting IRC provider (${account.host}:${account.port}${account.tls ? " tls" : ""})`, - ); - await runStoppablePassiveMonitor({ - abortSignal: ctx.abortSignal, - start: async () => - await monitorIrcProvider({ - accountId: account.accountId, - config: ctx.cfg as CoreConfig, - runtime: ctx.runtime, - abortSignal: ctx.abortSignal, - statusSink, - }), - }); }, }, -}; +});