refactor: share delimited channel entry parsing

This commit is contained in:
Peter Steinberger 2026-03-14 00:51:14 +00:00
parent e8a80cfbd8
commit d55fa78e40
6 changed files with 68 additions and 27 deletions

View File

@ -81,6 +81,32 @@ describe("resolveDefaultIrcAccountId", () => {
}); });
describe("resolveIrcAccount", () => { describe("resolveIrcAccount", () => {
it("parses delimited IRC_CHANNELS env values for the default account", () => {
const previousChannels = process.env.IRC_CHANNELS;
process.env.IRC_CHANNELS = "alpha, beta\ngamma; delta";
try {
const account = resolveIrcAccount({
cfg: asConfig({
channels: {
irc: {
host: "irc.example.com",
nick: "claw",
},
},
}),
});
expect(account.config.channels).toEqual(["alpha", "beta", "gamma", "delta"]);
} finally {
if (previousChannels === undefined) {
delete process.env.IRC_CHANNELS;
} else {
process.env.IRC_CHANNELS = previousChannels;
}
}
});
it.runIf(process.platform !== "win32")("rejects symlinked password files", () => { it.runIf(process.platform !== "win32")("rejects symlinked password files", () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-irc-account-")); const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-irc-account-"));
const passwordFile = path.join(dir, "password.txt"); const passwordFile = path.join(dir, "password.txt");

View File

@ -3,6 +3,7 @@ import { tryReadSecretFileSync } from "openclaw/plugin-sdk/core";
import { import {
createAccountListHelpers, createAccountListHelpers,
normalizeResolvedSecretInputString, normalizeResolvedSecretInputString,
parseOptionalDelimitedEntries,
} from "openclaw/plugin-sdk/irc"; } from "openclaw/plugin-sdk/irc";
import type { CoreConfig, IrcAccountConfig, IrcNickServConfig } from "./types.js"; import type { CoreConfig, IrcAccountConfig, IrcNickServConfig } from "./types.js";
@ -42,17 +43,6 @@ function parseIntEnv(value?: string): number | undefined {
return parsed; return parsed;
} }
function parseListEnv(value?: string): string[] | undefined {
if (!value?.trim()) {
return undefined;
}
const parsed = value
.split(/[\n,;]+/g)
.map((entry) => entry.trim())
.filter(Boolean);
return parsed.length > 0 ? parsed : undefined;
}
const { listAccountIds: listIrcAccountIds, resolveDefaultAccountId: resolveDefaultIrcAccountId } = const { listAccountIds: listIrcAccountIds, resolveDefaultAccountId: resolveDefaultIrcAccountId } =
createAccountListHelpers("irc", { normalizeAccountId }); createAccountListHelpers("irc", { normalizeAccountId });
export { listIrcAccountIds, resolveDefaultIrcAccountId }; export { listIrcAccountIds, resolveDefaultIrcAccountId };
@ -174,7 +164,9 @@ export function resolveIrcAccount(params: {
accountId === DEFAULT_ACCOUNT_ID ? parseIntEnv(process.env.IRC_PORT) : undefined; accountId === DEFAULT_ACCOUNT_ID ? parseIntEnv(process.env.IRC_PORT) : undefined;
const port = merged.port ?? envPort ?? (tls ? 6697 : 6667); const port = merged.port ?? envPort ?? (tls ? 6697 : 6667);
const envChannels = const envChannels =
accountId === DEFAULT_ACCOUNT_ID ? parseListEnv(process.env.IRC_CHANNELS) : undefined; accountId === DEFAULT_ACCOUNT_ID
? parseOptionalDelimitedEntries(process.env.IRC_CHANNELS)
: undefined;
const host = ( const host = (
merged.host?.trim() || merged.host?.trim() ||

View File

@ -1,6 +1,10 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../../config/config.js"; import type { OpenClawConfig } from "../../config/config.js";
import { buildAccountScopedDmSecurityPolicy, formatPairingApproveHint } from "./helpers.js"; import {
buildAccountScopedDmSecurityPolicy,
formatPairingApproveHint,
parseOptionalDelimitedEntries,
} from "./helpers.js";
function cfgWithChannel(channelKey: string, accounts?: Record<string, unknown>): OpenClawConfig { function cfgWithChannel(channelKey: string, accounts?: Record<string, unknown>): OpenClawConfig {
return { return {
@ -93,3 +97,18 @@ describe("buildAccountScopedDmSecurityPolicy", () => {
}); });
}); });
}); });
describe("parseOptionalDelimitedEntries", () => {
it("returns undefined for empty input", () => {
expect(parseOptionalDelimitedEntries(" ")).toBeUndefined();
});
it("splits comma, newline, and semicolon separated entries", () => {
expect(parseOptionalDelimitedEntries("alpha, beta\ngamma; delta")).toEqual([
"alpha",
"beta",
"gamma",
"delta",
]);
});
});

View File

@ -20,6 +20,17 @@ export function formatPairingApproveHint(channelId: string): string {
return `Approve via: ${listCmd} / ${approveCmd}`; return `Approve via: ${listCmd} / ${approveCmd}`;
} }
export function parseOptionalDelimitedEntries(value?: string): string[] | undefined {
if (!value?.trim()) {
return undefined;
}
const parsed = value
.split(/[\n,;]+/g)
.map((entry) => entry.trim())
.filter(Boolean);
return parsed.length > 0 ? parsed : undefined;
}
export function buildAccountScopedDmSecurityPolicy(params: { export function buildAccountScopedDmSecurityPolicy(params: {
cfg: OpenClawConfig; cfg: OpenClawConfig;
channelKey: string; channelKey: string;

View File

@ -1,5 +1,6 @@
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js"; import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
import { listChannelPluginCatalogEntries } from "../../channels/plugins/catalog.js"; import { listChannelPluginCatalogEntries } from "../../channels/plugins/catalog.js";
import { parseOptionalDelimitedEntries } from "../../channels/plugins/helpers.js";
import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js"; import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js";
import { moveSingleAccountChannelSectionToDefaultAccount } from "../../channels/plugins/setup-helpers.js"; import { moveSingleAccountChannelSectionToDefaultAccount } from "../../channels/plugins/setup-helpers.js";
import type { ChannelId, ChannelSetupInput } from "../../channels/plugins/types.js"; import type { ChannelId, ChannelSetupInput } from "../../channels/plugins/types.js";
@ -28,17 +29,6 @@ export type ChannelsAddOptions = {
dmAllowlist?: string; dmAllowlist?: string;
} & Omit<ChannelSetupInput, "groupChannels" | "dmAllowlist" | "initialSyncLimit">; } & Omit<ChannelSetupInput, "groupChannels" | "dmAllowlist" | "initialSyncLimit">;
function parseList(value: string | undefined): string[] | undefined {
if (!value?.trim()) {
return undefined;
}
const parsed = value
.split(/[\n,;]+/g)
.map((entry) => entry.trim())
.filter(Boolean);
return parsed.length > 0 ? parsed : undefined;
}
function resolveCatalogChannelEntry(raw: string, cfg: OpenClawConfig | null) { function resolveCatalogChannelEntry(raw: string, cfg: OpenClawConfig | null) {
const trimmed = raw.trim().toLowerCase(); const trimmed = raw.trim().toLowerCase();
if (!trimmed) { if (!trimmed) {
@ -225,8 +215,8 @@ export async function channelsAddCommand(
: typeof opts.initialSyncLimit === "string" && opts.initialSyncLimit.trim() : typeof opts.initialSyncLimit === "string" && opts.initialSyncLimit.trim()
? Number.parseInt(opts.initialSyncLimit, 10) ? Number.parseInt(opts.initialSyncLimit, 10)
: undefined; : undefined;
const groupChannels = parseList(opts.groupChannels); const groupChannels = parseOptionalDelimitedEntries(opts.groupChannels);
const dmAllowlist = parseList(opts.dmAllowlist); const dmAllowlist = parseOptionalDelimitedEntries(opts.dmAllowlist);
const input: ChannelSetupInput = { const input: ChannelSetupInput = {
name: opts.name, name: opts.name,

View File

@ -9,7 +9,10 @@ export {
} from "../channels/plugins/config-helpers.js"; } from "../channels/plugins/config-helpers.js";
export { createAccountListHelpers } from "../channels/plugins/account-helpers.js"; export { createAccountListHelpers } from "../channels/plugins/account-helpers.js";
export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js"; export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js";
export { formatPairingApproveHint } from "../channels/plugins/helpers.js"; export {
formatPairingApproveHint,
parseOptionalDelimitedEntries,
} from "../channels/plugins/helpers.js";
export type { export type {
ChannelOnboardingAdapter, ChannelOnboardingAdapter,
ChannelOnboardingDmPolicy, ChannelOnboardingDmPolicy,