refactor: dedupe auto-reply lowercase readers

This commit is contained in:
Peter Steinberger
2026-04-07 12:09:07 +01:00
parent 934927fd13
commit 353678ec05
13 changed files with 67 additions and 39 deletions

View File

@@ -2,7 +2,10 @@ import { getChannelPlugin, listChannelPlugins } from "../channels/plugins/index.
import type { ChannelId, ChannelPlugin } from "../channels/plugins/types.js";
import { normalizeAnyChannelId } from "../channels/registry.js";
import type { OpenClawConfig } from "../config/config.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
} from "../shared/string-coerce.js";
import { normalizeStringEntries } from "../shared/string-normalization.js";
import {
INTERNAL_MESSAGE_CHANNEL,
@@ -446,7 +449,7 @@ function resolveCommandSenderAuthorization(params: {
}
function isConversationLikeIdentity(value: string): boolean {
const normalized = value.trim().toLowerCase();
const normalized = normalizeOptionalLowercaseString(value);
if (!normalized) {
return false;
}
@@ -570,7 +573,7 @@ function resolveFallbackAccountConfig(
| undefined,
accountId?: string | null,
) {
const normalizedAccountId = accountId?.trim().toLowerCase();
const normalizedAccountId = normalizeOptionalLowercaseString(accountId);
if (!accounts || !normalizedAccountId) {
return undefined;
}
@@ -579,7 +582,7 @@ function resolveFallbackAccountConfig(
return direct;
}
const matchKey = Object.keys(accounts).find(
(key) => key.trim().toLowerCase() === normalizedAccountId,
(key) => normalizeOptionalLowercaseString(key) === normalizedAccountId,
);
return matchKey ? accounts[matchKey] : undefined;
}

View File

@@ -4,7 +4,10 @@ import type { SkillCommandSpec } from "../agents/skills.js";
import { getChannelPlugin } from "../channels/plugins/index.js";
import { isCommandFlagEnabled } from "../config/commands.js";
import type { OpenClawConfig } from "../config/types.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
} from "../shared/string-coerce.js";
import { escapeRegExp } from "../utils.js";
import { getChatCommands, getNativeCommandSurfaces } from "./commands-registry.data.js";
import type {
@@ -395,7 +398,7 @@ export function normalizeCommandBody(raw: string, options?: CommandNormalizeOpti
})()
: singleLine;
const normalizedBotUsername = options?.botUsername?.trim().toLowerCase();
const normalizedBotUsername = normalizeOptionalLowercaseString(options?.botUsername);
const mentionMatch = normalizedBotUsername
? normalized.match(/^\/([^\s@]+)@([^\s]+)(.*)$/)
: null;
@@ -404,7 +407,7 @@ export function normalizeCommandBody(raw: string, options?: CommandNormalizeOpti
? `/${mentionMatch[1]}${mentionMatch[3] ?? ""}`
: normalized;
const lowered = commandBody.toLowerCase();
const lowered = normalizeLowercaseStringOrEmpty(commandBody);
const textAliasMap = getTextAliasMap();
const exact = textAliasMap.get(lowered);
if (exact) {
@@ -442,7 +445,7 @@ export function getCommandDetection(_cfg?: OpenClawConfig): CommandDetection {
const patterns: string[] = [];
for (const cmd of commands) {
for (const alias of cmd.textAliases) {
const normalized = alias.trim().toLowerCase();
const normalized = normalizeOptionalLowercaseString(alias);
if (!normalized) {
continue;
}
@@ -472,7 +475,7 @@ export function maybeResolveTextAlias(raw: string, cfg?: OpenClawConfig) {
return null;
}
const detection = getCommandDetection(cfg);
const normalized = trimmed.toLowerCase();
const normalized = normalizeLowercaseStringOrEmpty(trimmed);
if (detection.exact.has(normalized)) {
return normalized;
}
@@ -518,7 +521,7 @@ export function isNativeCommandSurface(surface?: string): boolean {
if (!surface) {
return false;
}
return getNativeCommandSurfaces().has(surface.toLowerCase());
return getNativeCommandSurfaces().has(normalizeLowercaseStringOrEmpty(surface));
}
export function shouldHandleTextCommands(params: ShouldHandleTextCommandsParams): boolean {

View File

@@ -6,7 +6,10 @@ import { isCommandFlagEnabled } from "../../config/commands.js";
import type { OpenClawConfig } from "../../config/config.js";
import { logVerbose } from "../../globals.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "../../shared/string-coerce.js";
import { clampInt } from "../../utils.js";
import type { MsgContext } from "../templating.js";
import type { ReplyPayload } from "../types.js";
@@ -63,7 +66,7 @@ function formatOutputBlock(text: string) {
function parseBashRequest(raw: string): BashRequest | null {
const trimmed = raw.trimStart();
let restSource = "";
if (trimmed.toLowerCase().startsWith("/bash")) {
if (normalizeLowercaseStringOrEmpty(trimmed).startsWith("/bash")) {
const match = trimmed.match(/^\/bash(?:\s*:\s*|\s+|$)([\s\S]*)$/i);
if (!match) {
return null;
@@ -85,7 +88,7 @@ function parseBashRequest(raw: string): BashRequest | null {
const tokenMatch = rest.match(/^(\S+)(?:\s+([\s\S]+))?$/);
const token = normalizeOptionalString(tokenMatch?.[1]) ?? "";
const remainder = normalizeOptionalString(tokenMatch?.[2]) ?? "";
const lowered = token.toLowerCase();
const lowered = normalizeLowercaseStringOrEmpty(token);
if (lowered === "poll") {
return { action: "poll", sessionId: remainder || undefined };
}

View File

@@ -78,7 +78,8 @@ function resolveAllowlistAccountId(params: {
function parseAllowlistCommand(raw: string): AllowlistCommand | null {
const trimmed = raw.trim();
if (!trimmed.toLowerCase().startsWith("/allowlist")) {
const trimmedLower = normalizeOptionalLowercaseString(trimmed) ?? "";
if (!trimmedLower.startsWith("/allowlist")) {
return null;
}
const rest = trimmed.slice("/allowlist".length).trim();
@@ -96,18 +97,20 @@ function parseAllowlistCommand(raw: string): AllowlistCommand | null {
const entryTokens: string[] = [];
let i = 0;
if (tokens[i] && ACTIONS.has(tokens[i].toLowerCase())) {
action = tokens[i].toLowerCase() as AllowlistAction;
const firstAction = normalizeOptionalLowercaseString(tokens[i]);
if (firstAction && ACTIONS.has(firstAction)) {
action = firstAction as AllowlistAction;
i += 1;
}
if (tokens[i] && SCOPES.has(tokens[i].toLowerCase() as AllowlistScope)) {
scope = tokens[i].toLowerCase() as AllowlistScope;
const firstScope = normalizeOptionalLowercaseString(tokens[i]);
if (firstScope && SCOPES.has(firstScope as AllowlistScope)) {
scope = firstScope as AllowlistScope;
i += 1;
}
for (; i < tokens.length; i += 1) {
const token = tokens[i];
const lowered = token.toLowerCase();
const lowered = normalizeOptionalLowercaseString(token) ?? "";
if (lowered === "--resolve" || lowered === "resolve") {
resolve = true;
continue;
@@ -146,8 +149,9 @@ function parseAllowlistCommand(raw: string): AllowlistCommand | null {
}
continue;
}
if (key === "scope" && value && SCOPES.has(value.toLowerCase() as AllowlistScope)) {
scope = value.toLowerCase() as AllowlistScope;
const normalizedValue = normalizeOptionalLowercaseString(value);
if (key === "scope" && normalizedValue && SCOPES.has(normalizedValue as AllowlistScope)) {
scope = normalizedValue as AllowlistScope;
continue;
}
}

View File

@@ -1,3 +1,5 @@
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
export type SlashCommandParseResult =
| { kind: "no-match" }
| { kind: "empty" }
@@ -10,8 +12,8 @@ export type ParsedSlashCommand =
export function parseSlashCommandActionArgs(raw: string, slash: string): SlashCommandParseResult {
const trimmed = raw.trim();
const slashLower = slash.toLowerCase();
if (!trimmed.toLowerCase().startsWith(slashLower)) {
const slashLower = normalizeLowercaseStringOrEmpty(slash);
if (!normalizeLowercaseStringOrEmpty(trimmed).startsWith(slashLower)) {
return { kind: "no-match" };
}
const rest = trimmed.slice(slash.length).trim();
@@ -22,7 +24,7 @@ export function parseSlashCommandActionArgs(raw: string, slash: string): SlashCo
if (!match) {
return { kind: "invalid" };
}
const action = match[1]?.toLowerCase() ?? "";
const action = normalizeLowercaseStringOrEmpty(match[1]);
const args = (match[2] ?? "").trim();
return { kind: "parsed", action, args };
}

View File

@@ -3,7 +3,10 @@ import type { MessagingToolSend } from "../../agents/pi-embedded-runner.js";
import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js";
import { normalizeTargetForProvider } from "../../infra/outbound/target-normalization.js";
import { normalizeOptionalAccountId } from "../../routing/account-id.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "../../shared/string-coerce.js";
import type { ReplyPayload } from "../types.js";
export function filterMessagingToolDuplicates(params: {
@@ -26,7 +29,7 @@ export function filterMessagingToolMediaDuplicates(params: {
if (!trimmed) {
return "";
}
if (!trimmed.toLowerCase().startsWith("file://")) {
if (!normalizeLowercaseStringOrEmpty(trimmed).startsWith("file://")) {
return trimmed;
}
try {
@@ -66,7 +69,7 @@ function normalizeProviderForComparison(value?: string): string | undefined {
if (!trimmed) {
return undefined;
}
const lowered = trimmed.toLowerCase();
const lowered = normalizeLowercaseStringOrEmpty(trimmed);
const normalizedChannel = normalizeChannelId(trimmed);
if (normalizedChannel) {
return normalizedChannel;
@@ -82,7 +85,7 @@ function normalizeThreadIdForComparison(value?: string): string | undefined {
if (/^-?\d+$/.test(trimmed)) {
return String(Number.parseInt(trimmed, 10));
}
return trimmed.toLowerCase();
return normalizeLowercaseStringOrEmpty(trimmed);
}
function resolveTargetProviderForComparison(params: {

View File

@@ -16,6 +16,7 @@ import type { OpenClawConfig } from "../../config/config.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { buildOutboundSessionContext } from "../../infra/outbound/session-context.js";
import { hasReplyPayloadContent } from "../../interactive/payload.js";
import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js";
import { INTERNAL_MESSAGE_CHANNEL, normalizeMessageChannel } from "../../utils/message-channel.js";
import type { OriginatingChannelType } from "../templating.js";
import type { ReplyPayload } from "../types.js";
@@ -83,8 +84,7 @@ export async function routeReply(params: RouteReplyParams): Promise<RouteReplyRe
}
const normalizedChannel = normalizeMessageChannel(channel);
const channelId =
normalizeChannelId(channel) ??
(typeof channel === "string" ? channel.trim().toLowerCase() : null);
normalizeChannelId(channel) ?? normalizeOptionalLowercaseString(channel) ?? null;
const loadedPlugin = channelId ? getLoadedChannelPlugin(channelId) : undefined;
const bundledPlugin = channelId ? getBundledChannelPlugin(channelId) : undefined;
const messaging = loadedPlugin?.messaging ?? bundledPlugin?.messaging;

View File

@@ -3,6 +3,7 @@ import { buildAgentMainSessionKey } from "../../routing/session-key.js";
import { parseAgentSessionKey } from "../../sessions/session-key-utils.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "../../shared/string-coerce.js";
import {
@@ -27,7 +28,7 @@ function resolveSessionKeyChannelHint(sessionKey?: string): string | undefined {
if (!parsed?.rest) {
return undefined;
}
const head = normalizeOptionalString(parsed.rest.split(":")[0])?.toLowerCase();
const head = normalizeOptionalLowercaseString(parsed.rest.split(":")[0]);
if (!head || head === "main" || head === "cron" || head === "subagent" || head === "acp") {
return undefined;
}

View File

@@ -9,6 +9,7 @@ import { buildWorkspaceSkillCommandSpecs, type SkillCommandSpec } from "../agent
import type { OpenClawConfig } from "../config/config.js";
import { logVerbose } from "../globals.js";
import { getRemoteSkillEligibility } from "../infra/skills-remote.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
import { listReservedChatSlashCommandNames } from "./skill-commands-base.js";
export {
listReservedChatSlashCommandNames,
@@ -41,7 +42,7 @@ function dedupeBySkillName(commands: SkillCommandSpec[]): SkillCommandSpec[] {
const seen = new Set<string>();
const out: SkillCommandSpec[] = [];
for (const cmd of commands) {
const key = cmd.skillName.trim().toLowerCase();
const key = normalizeOptionalLowercaseString(cmd.skillName);
if (key && seen.has(key)) {
continue;
}

View File

@@ -33,6 +33,7 @@ import {
resolveProviderDefaultThinkingLevel,
resolveProviderXHighThinking,
} from "../plugins/provider-thinking.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
export function isBinaryThinkingProvider(provider?: string | null, model?: string | null): boolean {
const normalizedProvider = provider?.trim() ? normalizeProviderId(provider) : "";
@@ -54,7 +55,7 @@ export function isBinaryThinkingProvider(provider?: string | null, model?: strin
}
export function supportsXHighThinking(provider?: string | null, model?: string | null): boolean {
const modelKey = model?.trim().toLowerCase();
const modelKey = normalizeOptionalLowercaseString(model);
if (!modelKey) {
return false;
}

View File

@@ -1,3 +1,5 @@
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
export type {
AllowlistMatch,
AllowlistMatchSource,
@@ -36,7 +38,8 @@ export function formatAllowFromLowercase(params: {
.map((entry) => String(entry).trim())
.filter(Boolean)
.map((entry) => (params.stripPrefixRe ? entry.replace(params.stripPrefixRe, "") : entry))
.map((entry) => entry.toLowerCase());
.map((entry) => normalizeOptionalLowercaseString(entry))
.filter((entry): entry is string => Boolean(entry));
}
/** Normalize allowlist entries through a channel-provided parser or canonicalizer. */
@@ -67,7 +70,7 @@ export function isNormalizedSenderAllowed(params: {
if (normalizedAllow.includes("*")) {
return true;
}
const sender = String(params.senderId).trim().toLowerCase();
const sender = normalizeOptionalLowercaseString(String(params.senderId));
return normalizedAllow.includes(sender);
}

View File

@@ -4,7 +4,10 @@ import { matchesApprovalRequestFilters } from "../infra/approval-request-filters
import { getExecApprovalReplyMetadata } from "../infra/exec-approval-reply.js";
import type { ExecApprovalRequest } from "../infra/exec-approvals.js";
import type { PluginApprovalRequest } from "../infra/plugin-approvals.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import {
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "../shared/string-coerce.js";
import type { OpenClawConfig } from "./config-runtime.js";
import { normalizeAccountId } from "./routing.js";
@@ -58,7 +61,7 @@ export function isChannelExecApprovalTargetRecipient(params: {
}): boolean {
const normalizeSenderId = params.normalizeSenderId ?? normalizeOptionalString;
const normalizedSenderId = params.senderId ? normalizeSenderId(params.senderId) : undefined;
const normalizedChannel = params.channel.trim().toLowerCase();
const normalizedChannel = normalizeOptionalLowercaseString(params.channel);
if (!normalizedSenderId || !isApprovalTargetsMode(params.cfg)) {
return false;
}
@@ -68,7 +71,7 @@ export function isChannelExecApprovalTargetRecipient(params: {
}
const normalizedAccountId = params.accountId ? normalizeAccountId(params.accountId) : undefined;
return targets.some((target) => {
if (target.channel?.trim().toLowerCase() !== normalizedChannel) {
if (normalizeOptionalLowercaseString(target.channel) !== normalizedChannel) {
return false;
}
if (

View File

@@ -15,6 +15,7 @@ import type { ChannelConfigAdapter } from "../channels/plugins/types.adapters.js
import { formatCliCommand } from "../cli/command-format.js";
import type { OpenClawConfig } from "../config/config.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
import { normalizeStringEntries } from "../shared/string-normalization.js";
const INTERNAL_MESSAGE_CHANNEL = "webchat";
@@ -113,7 +114,7 @@ export function canBypassConfigWritePolicy(params: {
return canBypassConfigWritePolicyShared({
...params,
isInternalMessageChannel: (channel) =>
channel?.trim().toLowerCase() === INTERNAL_MESSAGE_CHANNEL,
normalizeOptionalLowercaseString(channel) === INTERNAL_MESSAGE_CHANNEL,
});
}