refactor: adopt chat plugin builder in irc

This commit is contained in:
Peter Steinberger
2026-03-22 21:51:19 +00:00
parent a6f918731f
commit d868ee0882

View File

@@ -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<ResolvedIrcAccount, IrcProbe> = {
id: "irc",
meta: {
...meta,
quickstartAllowFrom: true,
export const ircPlugin: ChannelPlugin<ResolvedIrcAccount, IrcProbe> = 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<ResolvedIrcAccount>({
...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<ResolvedIrcAccount>({
...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<ResolvedIrcAccount, IrcProbe> = {
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<ResolvedIrcAccount>({
...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<ResolvedIrcAccount>({
...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<ResolvedIrcAccount, IrcProbe> = {
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,
}),
});
},
},
};
});