refactor: dedupe channel trimmed readers

This commit is contained in:
Peter Steinberger
2026-04-08 00:03:28 +01:00
parent ee0425a705
commit ef903d881e
15 changed files with 62 additions and 48 deletions

View File

@@ -1,12 +1,12 @@
import { normalizeStringEntries } from "../shared/string-normalization.js";
export function mergeDmAllowFromSources(params: {
allowFrom?: Array<string | number>;
storeAllowFrom?: Array<string | number>;
dmPolicy?: string;
}): string[] {
const storeEntries = params.dmPolicy === "allowlist" ? [] : (params.storeAllowFrom ?? []);
return [...(params.allowFrom ?? []), ...storeEntries]
.map((value) => String(value).trim())
.filter(Boolean);
return normalizeStringEntries([...(params.allowFrom ?? []), ...storeEntries]);
}
export function resolveGroupAllowFromSources(params: {
@@ -23,7 +23,7 @@ export function resolveGroupAllowFromSources(params: {
: params.fallbackToAllowFrom === false
? []
: (params.allowFrom ?? []);
return scoped.map((value) => String(value).trim()).filter(Boolean);
return normalizeStringEntries(scoped);
}
export function firstDefined<T>(...values: Array<T | undefined>) {

View File

@@ -1,6 +1,9 @@
import { mapAllowFromEntries } from "openclaw/plugin-sdk/channel-config-helpers";
import type { RuntimeEnv } from "../../runtime.js";
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "../../shared/string-coerce.js";
import { summarizeStringEntries } from "../../shared/string-sample.js";
export type AllowlistUserResolutionLike = {
@@ -62,7 +65,7 @@ export function resolveAllowlistIdAdditions<T extends AllowlistUserResolutionLik
}): string[] {
const additions: string[] = [];
for (const entry of params.existing) {
const trimmed = String(entry).trim();
const trimmed = normalizeOptionalString(entry) ?? "";
const resolved = params.resolvedMap.get(trimmed);
if (resolved?.resolved && resolved.id) {
additions.push(resolved.id);
@@ -76,7 +79,7 @@ export function canonicalizeAllowlistWithResolvedIds<
>(params: { existing?: Array<string | number>; resolvedMap: Map<string, T> }): string[] {
const canonicalized: string[] = [];
for (const entry of params.existing ?? []) {
const trimmed = String(entry).trim();
const trimmed = normalizeOptionalString(entry) ?? "";
if (!trimmed) {
continue;
}
@@ -137,7 +140,7 @@ export function addAllowlistUserEntriesFromConfigEntry(target: Set<string>, entr
return;
}
for (const value of users) {
const trimmed = String(value).trim();
const trimmed = normalizeOptionalString(value) ?? "";
if (trimmed && trimmed !== "*") {
target.add(trimmed);
}

View File

@@ -8,6 +8,7 @@ import {
normalizeAccountId,
normalizeOptionalAccountId,
} from "../../routing/session-key.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import type { ChannelAccountSnapshot } from "./types.core.js";
export function createAccountListHelpers(
@@ -188,10 +189,7 @@ export function describeAccountSnapshot<
}): ChannelAccountSnapshot {
return {
accountId: String(params.account.accountId ?? DEFAULT_ACCOUNT_ID),
name:
typeof params.account.name === "string" && params.account.name.trim()
? params.account.name
: undefined,
name: normalizeOptionalString(params.account.name),
enabled: params.account.enabled !== false,
configured: params.configured,
...params.extra,

View File

@@ -1,3 +1,4 @@
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { listBundledChannelPluginIds } from "./bundled-ids.js";
import {
getBundledChannelPlugin,
@@ -93,7 +94,7 @@ export function listBootstrapChannelPlugins(): readonly ChannelPlugin[] {
}
export function getBootstrapChannelPlugin(id: ChannelId): ChannelPlugin | undefined {
const resolvedId = String(id).trim();
const resolvedId = normalizeOptionalString(id) ?? "";
if (!resolvedId) {
return undefined;
}
@@ -120,7 +121,7 @@ export function getBootstrapChannelPlugin(id: ChannelId): ChannelPlugin | undefi
}
export function getBootstrapChannelSecrets(id: ChannelId): ChannelPlugin["secrets"] | undefined {
const resolvedId = String(id).trim();
const resolvedId = normalizeOptionalString(id) ?? "";
if (!resolvedId) {
return undefined;
}

View File

@@ -1,4 +1,8 @@
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "../../shared/string-coerce.js";
import { normalizeStringEntries } from "../../shared/string-normalization.js";
export type ServicePrefix<TService extends string> = { prefix: string; service: TService };
@@ -34,7 +38,7 @@ function isAllowedParsedChatSender<TParsed extends ParsedChatAllowTarget>(params
normalizeSender: (sender: string) => string;
parseAllowTarget: (entry: string) => TParsed;
}): boolean {
const allowFrom = params.allowFrom.map((entry) => String(entry).trim());
const allowFrom = normalizeStringEntries(params.allowFrom);
if (allowFrom.length === 0) {
return false;
}
@@ -44,8 +48,8 @@ function isAllowedParsedChatSender<TParsed extends ParsedChatAllowTarget>(params
const senderNormalized = params.normalizeSender(params.sender);
const chatId = params.chatId ?? undefined;
const chatGuid = params.chatGuid?.trim();
const chatIdentifier = params.chatIdentifier?.trim();
const chatGuid = normalizeOptionalString(params.chatGuid);
const chatIdentifier = normalizeOptionalString(params.chatIdentifier);
for (const entry of allowFrom) {
if (!entry) {

View File

@@ -1,5 +1,8 @@
import type { OpenClawConfig } from "../../config/types.js";
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "../../shared/string-coerce.js";
import type { DirectoryConfigParams } from "./directory-types.js";
import type { ChannelDirectoryEntry } from "./types.js";
@@ -30,11 +33,11 @@ function normalizeDirectoryIds(params: {
normalizeId?: (entry: string) => string | null | undefined;
}): string[] {
return params.rawIds
.map((entry) => entry.trim())
.map((entry) => normalizeOptionalString(entry) ?? "")
.filter((entry) => Boolean(entry) && entry !== "*")
.map((entry) => {
const normalized = params.normalizeId ? params.normalizeId(entry) : entry;
return typeof normalized === "string" ? normalized.trim() : "";
return normalizeOptionalString(normalized) ?? "";
})
.filter(Boolean);
}
@@ -70,12 +73,12 @@ export function collectNormalizedDirectoryIds(params: {
const ids = new Set<string>();
for (const source of params.sources) {
for (const value of source) {
const raw = String(value).trim();
const raw = normalizeOptionalString(value) ?? "";
if (!raw || raw === "*") {
continue;
}
const normalized = params.normalizeId(raw);
const trimmed = typeof normalized === "string" ? normalized.trim() : "";
const trimmed = normalizeOptionalString(normalized) ?? "";
if (trimmed) {
ids.add(trimmed);
}

View File

@@ -5,6 +5,7 @@ import {
listChannelCatalogEntries,
type PluginChannelCatalogEntry,
} from "../../plugins/channel-catalog-registry.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import {
isJavaScriptModulePath,
loadChannelPluginModule,
@@ -40,8 +41,8 @@ function resolveChannelPackageStateMetadata(
if (!metadata || typeof metadata !== "object") {
return null;
}
const specifier = typeof metadata.specifier === "string" ? metadata.specifier.trim() : "";
const exportName = typeof metadata.exportName === "string" ? metadata.exportName.trim() : "";
const specifier = normalizeOptionalString(metadata.specifier) ?? "";
const exportName = normalizeOptionalString(metadata.exportName) ?? "";
if (!specifier || !exportName) {
return null;
}

View File

@@ -2,6 +2,7 @@ import {
getActivePluginChannelRegistryVersion,
requireActivePluginChannelRegistry,
} from "../../plugins/runtime.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { CHAT_CHANNEL_ORDER, type ChatChannelId, normalizeAnyChannelId } from "../registry.js";
import { getBundledChannelPlugin } from "./bundled.js";
import type { ChannelId, ChannelPlugin } from "./types.js";
@@ -10,7 +11,7 @@ function dedupeChannels(channels: ChannelPlugin[]): ChannelPlugin[] {
const seen = new Set<string>();
const resolved: ChannelPlugin[] = [];
for (const plugin of channels) {
const id = String(plugin.id).trim();
const id = normalizeOptionalString(plugin.id) ?? "";
if (!id || seen.has(id)) {
continue;
}
@@ -83,7 +84,7 @@ export function listChannelPlugins(): ChannelPlugin[] {
}
export function getLoadedChannelPlugin(id: ChannelId): ChannelPlugin | undefined {
const resolvedId = String(id).trim();
const resolvedId = normalizeOptionalString(id) ?? "";
if (!resolvedId) {
return undefined;
}
@@ -91,7 +92,7 @@ export function getLoadedChannelPlugin(id: ChannelId): ChannelPlugin | undefined
}
export function getChannelPlugin(id: ChannelId): ChannelPlugin | undefined {
const resolvedId = String(id).trim();
const resolvedId = normalizeOptionalString(id) ?? "";
if (!resolvedId) {
return undefined;
}

View File

@@ -1,6 +1,7 @@
import { z, type ZodType } from "zod";
import type { OpenClawConfig } from "../../config/config.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { getBundledChannelPlugin } from "./bundled.js";
import { getChannelPlugin } from "./registry.js";
import type { ChannelSetupAdapter } from "./types.adapters.js";
@@ -499,8 +500,9 @@ export function resolveSingleAccountPromotionTarget(params: {
const resolved = surface?.resolveSingleAccountPromotionTarget?.({
channel: params.channel,
});
if (typeof resolved === "string" && resolved.trim()) {
return resolveExistingAccountId(resolved);
const normalizedResolved = normalizeOptionalString(resolved);
if (normalizedResolved) {
return resolveExistingAccountId(normalizedResolved);
}
return resolveExistingAccountId(DEFAULT_ACCOUNT_ID);
}

View File

@@ -2,6 +2,7 @@ import {
getActivePluginRegistryVersion,
requireActivePluginRegistry,
} from "../../plugins/runtime.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { CHAT_CHANNEL_ORDER, type ChatChannelId } from "../registry.js";
import { listBundledChannelSetupPlugins } from "./bundled.js";
import type { ChannelId, ChannelPlugin } from "./types.js";
@@ -26,7 +27,7 @@ function dedupeSetupPlugins(plugins: readonly ChannelPlugin[]): ChannelPlugin[]
const seen = new Set<string>();
const resolved: ChannelPlugin[] = [];
for (const plugin of plugins) {
const id = String(plugin.id).trim();
const id = normalizeOptionalString(plugin.id) ?? "";
if (!id || seen.has(id)) {
continue;
}
@@ -81,7 +82,7 @@ export function listChannelSetupPlugins(): ChannelPlugin[] {
}
export function getChannelSetupPlugin(id: ChannelId): ChannelPlugin | undefined {
const resolvedId = String(id).trim();
const resolvedId = normalizeOptionalString(id) ?? "";
if (!resolvedId) {
return undefined;
}

View File

@@ -4,6 +4,7 @@ import type { SecretInput } from "../../config/types.secrets.js";
import { resolveSecretInputModeForEnvSelection } from "../../plugins/provider-auth-mode.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { normalizeStringEntries } from "../../shared/string-normalization.js";
import type { WizardPrompter } from "../../wizard/prompts.js";
import {
moveSingleAccountChannelSectionToDefaultAccount,
@@ -50,10 +51,10 @@ export const promptAccountId: PromptAccountId = async (params: PromptAccountIdPa
const entered = await params.prompter.text({
message: `New ${params.label} account id`,
validate: (value) => (value?.trim() ? undefined : "Required"),
validate: (value) => (normalizeOptionalString(value) ? undefined : "Required"),
});
const normalized = normalizeAccountId(String(entered));
if (String(entered).trim() !== normalized) {
if ((normalizeOptionalString(entered) ?? "") !== normalized) {
await params.prompter.note(
`Normalized account id to "${normalized}".`,
`${params.label} account`,
@@ -63,7 +64,7 @@ export const promptAccountId: PromptAccountId = async (params: PromptAccountIdPa
};
export function addWildcardAllowFrom(allowFrom?: ReadonlyArray<string | number> | null): string[] {
const next = (allowFrom ?? []).map((v) => String(v).trim()).filter(Boolean);
const next = normalizeStringEntries(allowFrom ?? []);
if (!next.includes("*")) {
next.push("*");
}
@@ -74,7 +75,7 @@ export function mergeAllowFromEntries(
current: Array<string | number> | null | undefined,
additions: Array<string | number>,
): string[] {
const merged = [...(current ?? []), ...additions].map((v) => String(v).trim()).filter(Boolean);
const merged = normalizeStringEntries([...(current ?? []), ...additions]);
return [...new Set(merged)];
}
@@ -144,9 +145,7 @@ export function normalizeAllowFromEntries(
entries: Array<string | number>,
normalizeEntry?: (value: string) => string | null | undefined,
): string[] {
const normalized = entries
.map((entry) => String(entry).trim())
.filter(Boolean)
const normalized = normalizeStringEntries(entries)
.map((entry) => {
if (entry === "*") {
return "*";
@@ -154,8 +153,7 @@ export function normalizeAllowFromEntries(
if (!normalizeEntry) {
return entry;
}
const value = normalizeEntry(entry);
return typeof value === "string" ? value.trim() : "";
return normalizeOptionalString(normalizeEntry(entry)) ?? "";
})
.filter(Boolean);
return [...new Set(normalized)];
@@ -1215,7 +1213,7 @@ export async function promptParsedAllowFromForAccount<TConfig extends OpenClawCo
placeholder: params.placeholder,
initialValue: existing[0] ? String(existing[0]) : undefined,
validate: (value) => {
const raw = String(value ?? "").trim();
const raw = normalizeOptionalString(value) ?? "";
if (!raw) {
return "Required";
}
@@ -1517,7 +1515,7 @@ export async function promptResolvedAllowFrom(params: {
message: params.message,
placeholder: params.placeholder,
initialValue: params.existing[0] ? String(params.existing[0]) : undefined,
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
validate: (value) => (normalizeOptionalString(value) ? undefined : "Required"),
});
const parts = params.parseInputs(String(entry));
if (!params.token) {

View File

@@ -693,7 +693,7 @@ export function buildChannelSetupWizardAdapterFromSetupWizard(params: {
initialValue,
placeholder: textInput.placeholder,
validate: (value) => {
const trimmed = String(value ?? "").trim();
const trimmed = normalizeOptionalString(value) ?? "";
if (!trimmed && textInput.required !== false) {
return "Required";
}

View File

@@ -1,4 +1,5 @@
import type { OpenClawConfig } from "../../config/config.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { projectSafeChannelAccountSnapshotFields } from "../account-snapshot-fields.js";
import { inspectReadOnlyChannelAccount } from "../read-only-account-inspect.js";
import type { ChannelAccountSnapshot, ChannelPlugin } from "./types.js";
@@ -21,7 +22,7 @@ async function buildSnapshotFromAccount<ResolvedAccount>(params: {
probe: params.probe,
audit: params.audit,
});
return typeof snapshot.accountId === "string" && snapshot.accountId.trim().length > 0
return normalizeOptionalString(snapshot.accountId)
? snapshot
: {
...snapshot,

View File

@@ -1,4 +1,5 @@
import { type NodeMatchCandidate, resolveNodeIdFromCandidates } from "./node-match.js";
import { normalizeOptionalString } from "./string-coerce.js";
type ResolveNodeFromListOptions<TNode extends NodeMatchCandidate> = {
allowDefault?: boolean;
@@ -10,7 +11,7 @@ export function resolveNodeIdFromNodeList<TNode extends NodeMatchCandidate>(
query?: string,
options: ResolveNodeFromListOptions<TNode> = {},
): string {
const q = String(query ?? "").trim();
const q = normalizeOptionalString(query) ?? "";
if (!q) {
if (options.allowDefault === true && options.pickDefaultNode) {
const picked = options.pickDefaultNode(nodes);

View File

@@ -1,7 +1,7 @@
import { normalizeOptionalLowercaseString, normalizeOptionalString } from "./string-coerce.js";
export function normalizeStringEntries(list?: ReadonlyArray<unknown>) {
return (list ?? []).map((entry) => String(entry).trim()).filter(Boolean);
return (list ?? []).map((entry) => normalizeOptionalString(String(entry)) ?? "").filter(Boolean);
}
export function normalizeStringEntriesLower(list?: ReadonlyArray<unknown>) {
@@ -40,7 +40,7 @@ export function normalizeSingleOrTrimmedStringList(value: unknown): string[] {
export function normalizeCsvOrLooseStringList(value: unknown): string[] {
if (Array.isArray(value)) {
return value.map((entry) => String(entry).trim()).filter(Boolean);
return normalizeStringEntries(value);
}
if (typeof value === "string") {
return value