refactor: dedupe channel helper readers

This commit is contained in:
Peter Steinberger
2026-04-07 06:23:08 +01:00
parent 870cc22cb0
commit 4504efb7ec
12 changed files with 61 additions and 52 deletions

View File

@@ -1,7 +1,9 @@
import { normalizeOptionalString } from "../shared/string-coerce.js";
export type ChatType = "direct" | "group" | "channel";
export function normalizeChatType(raw?: string): ChatType | undefined {
const value = raw?.trim().toLowerCase();
const value = normalizeOptionalString(raw)?.toLowerCase();
if (!value) {
return undefined;
}

View File

@@ -1,4 +1,5 @@
import { listChannelCatalogEntries } from "../plugins/channel-catalog-registry.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
export type ChatChannelId = string;
@@ -9,8 +10,7 @@ type BundledChatChannelEntry = {
};
function normalizeChannelKey(raw?: string | null): string | undefined {
const normalized = raw?.trim().toLowerCase();
return normalized || undefined;
return normalizeOptionalString(raw)?.toLowerCase();
}
function listBundledChatChannelEntries(): BundledChatChannelEntry[] {

View File

@@ -6,6 +6,7 @@ import {
type InboundDebounceCreateParams,
} from "../auto-reply/inbound-debounce.js";
import type { OpenClawConfig } from "../config/types.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
export function shouldDebounceTextInbound(params: {
text: string | null | undefined;
@@ -20,7 +21,7 @@ export function shouldDebounceTextInbound(params: {
if (params.hasMedia) {
return false;
}
const text = params.text?.trim() ?? "";
const text = normalizeOptionalString(params.text) ?? "";
if (!text) {
return false;
}

View File

@@ -1,4 +1,5 @@
import { getActivePluginChannelRegistry, getActivePluginRegistry } from "../plugins/runtime.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { getChatChannelMeta, listChatChannels, type ChatChannelMeta } from "./chat-meta.js";
import {
CHANNEL_IDS,
@@ -31,14 +32,12 @@ function findRegisteredChannelPluginEntry(
normalizedKey: string,
): RegisteredChannelPluginEntry | undefined {
return listRegisteredChannelPluginEntries().find((entry) => {
const id = String(entry.plugin.id ?? "")
.trim()
.toLowerCase();
const id = normalizeOptionalString(String(entry.plugin.id ?? ""))?.toLowerCase() ?? "";
if (id && id === normalizedKey) {
return true;
}
return (entry.plugin.meta?.aliases ?? []).some(
(alias) => alias.trim().toLowerCase() === normalizedKey,
(alias) => normalizeOptionalString(alias)?.toLowerCase() === normalizedKey,
);
});
}
@@ -56,8 +55,7 @@ function findRegisteredChannelPluginEntryById(
}
const normalizeChannelKey = (raw?: string | null): string | undefined => {
const normalized = raw?.trim().toLowerCase();
return normalized || undefined;
return normalizeOptionalString(raw)?.toLowerCase();
};
export {
CHAT_CHANNEL_ALIASES,
@@ -87,7 +85,7 @@ export function normalizeAnyChannelId(raw?: string | null): ChannelId | null {
export function listRegisteredChannelPluginIds(): ChannelId[] {
return listRegisteredChannelPluginEntries().flatMap((entry) => {
const id = entry.plugin.id?.trim();
const id = normalizeOptionalString(entry.plugin.id);
return id ? [id as ChannelId] : [];
});
}

View File

@@ -1,4 +1,5 @@
import type { MsgContext } from "../auto-reply/templating.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { normalizeChatType } from "./chat-type.js";
export function validateSenderIdentity(ctx: MsgContext): string[] {
@@ -7,10 +8,10 @@ export function validateSenderIdentity(ctx: MsgContext): string[] {
const chatType = normalizeChatType(ctx.ChatType);
const isDirect = chatType === "direct";
const senderId = ctx.SenderId?.trim() || "";
const senderName = ctx.SenderName?.trim() || "";
const senderUsername = ctx.SenderUsername?.trim() || "";
const senderE164 = ctx.SenderE164?.trim() || "";
const senderId = normalizeOptionalString(ctx.SenderId) || "";
const senderName = normalizeOptionalString(ctx.SenderName) || "";
const senderUsername = normalizeOptionalString(ctx.SenderUsername) || "";
const senderE164 = normalizeOptionalString(ctx.SenderE164) || "";
if (!isDirect) {
if (!senderId && !senderName && !senderUsername && !senderE164) {

View File

@@ -1,3 +1,5 @@
import { normalizeOptionalString } from "../shared/string-coerce.js";
export type SenderLabelParams = {
name?: string;
username?: string;
@@ -7,8 +9,7 @@ export type SenderLabelParams = {
};
function normalize(value?: string): string | undefined {
const trimmed = value?.trim();
return trimmed ? trimmed : undefined;
return normalizeOptionalString(value);
}
function normalizeSenderLabelParams(params: SenderLabelParams) {

View File

@@ -1,3 +1,5 @@
import { normalizeOptionalString } from "../shared/string-coerce.js";
/**
* Channel-agnostic status reaction controller.
* Provides a unified interface for displaying agent status via message reactions.
@@ -102,7 +104,7 @@ export function resolveToolEmoji(
toolName: string | undefined,
emojis: Required<StatusReactionEmojis>,
): string {
const normalized = toolName?.trim().toLowerCase() ?? "";
const normalized = normalizeOptionalString(toolName)?.toLowerCase() ?? "";
if (!normalized) {
return emojis.tool;
}

View File

@@ -1,8 +1,10 @@
import { normalizeOptionalString } from "../shared/string-coerce.js";
export function resolveThreadBindingConversationIdFromBindingId(params: {
accountId: string;
bindingId?: string;
}): string | undefined {
const bindingId = params.bindingId?.trim();
const bindingId = normalizeOptionalString(params.bindingId);
if (!bindingId) {
return undefined;
}
@@ -10,6 +12,6 @@ export function resolveThreadBindingConversationIdFromBindingId(params: {
if (!bindingId.startsWith(prefix)) {
return undefined;
}
const conversationId = bindingId.slice(prefix.length).trim();
const conversationId = normalizeOptionalString(bindingId.slice(prefix.length));
return conversationId || undefined;
}

View File

@@ -1,4 +1,5 @@
import { prefixSystemMessage } from "../infra/system-message.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
const DEFAULT_THREAD_BINDING_FAREWELL_TEXT =
"Session ended. Messages here will no longer be routed.";
@@ -32,8 +33,8 @@ export function resolveThreadBindingThreadName(params: {
agentId?: string;
label?: string;
}): string {
const label = params.label?.trim();
const base = label || params.agentId?.trim() || "agent";
const label = normalizeOptionalString(params.label);
const base = label || normalizeOptionalString(params.agentId) || "agent";
const raw = `🤖 ${base}`.replace(/\s+/g, " ").trim();
return raw.slice(0, 100);
}
@@ -46,12 +47,12 @@ export function resolveThreadBindingIntroText(params: {
sessionCwd?: string;
sessionDetails?: string[];
}): string {
const label = params.label?.trim();
const base = label || params.agentId?.trim() || "agent";
const label = normalizeOptionalString(params.label);
const base = label || normalizeOptionalString(params.agentId) || "agent";
const normalized = base.replace(/\s+/g, " ").trim().slice(0, 100) || "agent";
const idleTimeoutMs = normalizeThreadBindingDurationMs(params.idleTimeoutMs);
const maxAgeMs = normalizeThreadBindingDurationMs(params.maxAgeMs);
const cwd = params.sessionCwd?.trim();
const cwd = normalizeOptionalString(params.sessionCwd);
const details = (params.sessionDetails ?? [])
.map((entry) => entry.trim())
.filter((entry) => entry.length > 0);
@@ -86,7 +87,7 @@ export function resolveThreadBindingFarewellText(params: {
idleTimeoutMs: number;
maxAgeMs: number;
}): string {
const custom = params.farewellText?.trim();
const custom = normalizeOptionalString(params.farewellText);
if (custom) {
return prefixSystemMessage(custom);
}

View File

@@ -1,11 +1,12 @@
import { normalizeChatType } from "../channels/chat-type.js";
import type { OpenClawConfig } from "../config/config.js";
import type { SessionChatType, SessionEntry } from "../config/sessions.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
export type SessionSendPolicyDecision = "allow" | "deny";
export function normalizeSendPolicy(raw?: string | null): SessionSendPolicyDecision | undefined {
const value = raw?.trim().toLowerCase();
const value = normalizeOptionalString(raw)?.toLowerCase();
if (value === "allow") {
return "allow";
}
@@ -16,7 +17,7 @@ export function normalizeSendPolicy(raw?: string | null): SessionSendPolicyDecis
}
function normalizeMatchValue(raw?: string | null) {
const value = raw?.trim().toLowerCase();
const value = normalizeOptionalString(raw)?.toLowerCase();
return value ? value : undefined;
}
@@ -45,7 +46,7 @@ function deriveChannelFromKey(key?: string) {
}
function deriveChatTypeFromKey(key?: string): SessionChatType | undefined {
const normalizedKey = stripAgentSessionKeyPrefix(key)?.trim().toLowerCase();
const normalizedKey = normalizeOptionalString(stripAgentSessionKeyPrefix(key))?.toLowerCase();
if (!normalizedKey) {
return undefined;
}

View File

@@ -1,3 +1,5 @@
import { normalizeOptionalString } from "../shared/string-coerce.js";
export type ParsedAgentSessionKey = {
agentId: string;
rest: string;
@@ -22,7 +24,7 @@ export type RawSessionConversationRef = {
export function parseAgentSessionKey(
sessionKey: string | undefined | null,
): ParsedAgentSessionKey | null {
const raw = (sessionKey ?? "").trim().toLowerCase();
const raw = normalizeOptionalString(sessionKey)?.toLowerCase();
if (!raw) {
return null;
}
@@ -33,7 +35,7 @@ export function parseAgentSessionKey(
if (parts[0] !== "agent") {
return null;
}
const agentId = parts[1]?.trim();
const agentId = normalizeOptionalString(parts[1]);
const rest = parts.slice(2).join(":");
if (!agentId || !rest) {
return null;
@@ -58,7 +60,7 @@ export function isCronSessionKey(sessionKey: string | undefined | null): boolean
}
export function isSubagentSessionKey(sessionKey: string | undefined | null): boolean {
const raw = (sessionKey ?? "").trim();
const raw = normalizeOptionalString(sessionKey);
if (!raw) {
return false;
}
@@ -70,7 +72,7 @@ export function isSubagentSessionKey(sessionKey: string | undefined | null): boo
}
export function getSubagentDepth(sessionKey: string | undefined | null): number {
const raw = (sessionKey ?? "").trim().toLowerCase();
const raw = normalizeOptionalString(sessionKey)?.toLowerCase();
if (!raw) {
return 0;
}
@@ -78,7 +80,7 @@ export function getSubagentDepth(sessionKey: string | undefined | null): number
}
export function isAcpSessionKey(sessionKey: string | undefined | null): boolean {
const raw = (sessionKey ?? "").trim();
const raw = normalizeOptionalString(sessionKey);
if (!raw) {
return false;
}
@@ -91,14 +93,13 @@ export function isAcpSessionKey(sessionKey: string | undefined | null): boolean
}
function normalizeSessionConversationChannel(value: string | undefined | null): string | undefined {
const trimmed = (value ?? "").trim().toLowerCase();
return trimmed || undefined;
return normalizeOptionalString(value)?.toLowerCase();
}
export function parseThreadSessionSuffix(
sessionKey: string | undefined | null,
): ParsedThreadSessionSuffix {
const raw = (sessionKey ?? "").trim();
const raw = normalizeOptionalString(sessionKey);
if (!raw) {
return { baseSessionKey: undefined, threadId: undefined };
}
@@ -111,7 +112,7 @@ export function parseThreadSessionSuffix(
const baseSessionKey = markerIndex === -1 ? raw : raw.slice(0, markerIndex);
const threadIdRaw = markerIndex === -1 ? undefined : raw.slice(markerIndex + marker.length);
const threadId = threadIdRaw?.trim() || undefined;
const threadId = normalizeOptionalString(threadIdRaw);
return { baseSessionKey, threadId };
}
@@ -119,30 +120,27 @@ export function parseThreadSessionSuffix(
export function parseRawSessionConversationRef(
sessionKey: string | undefined | null,
): RawSessionConversationRef | null {
const raw = (sessionKey ?? "").trim();
const raw = normalizeOptionalString(sessionKey);
if (!raw) {
return null;
}
const rawParts = raw.split(":").filter(Boolean);
const bodyStartIndex =
rawParts.length >= 3 && rawParts[0]?.trim().toLowerCase() === "agent" ? 2 : 0;
rawParts.length >= 3 && normalizeOptionalString(rawParts[0])?.toLowerCase() === "agent" ? 2 : 0;
const parts = rawParts.slice(bodyStartIndex);
if (parts.length < 3) {
return null;
}
const channel = normalizeSessionConversationChannel(parts[0]);
const kind = parts[1]?.trim().toLowerCase();
const kind = normalizeOptionalString(parts[1])?.toLowerCase();
if (!channel || (kind !== "group" && kind !== "channel")) {
return null;
}
const rawId = parts.slice(2).join(":").trim();
const prefix = rawParts
.slice(0, bodyStartIndex + 2)
.join(":")
.trim();
const rawId = normalizeOptionalString(parts.slice(2).join(":"));
const prefix = normalizeOptionalString(rawParts.slice(0, bodyStartIndex + 2).join(":"));
if (!rawId || !prefix) {
return null;
}
@@ -157,7 +155,7 @@ export function resolveThreadParentSessionKey(
if (!threadId) {
return null;
}
const parent = baseSessionKey?.trim();
const parent = normalizeOptionalString(baseSessionKey);
if (!parent) {
return null;
}

View File

@@ -1,3 +1,5 @@
import { normalizeOptionalString } from "../shared/string-coerce.js";
export type SessionTranscriptUpdate = {
sessionFile: string;
sessionKey?: string;
@@ -26,18 +28,18 @@ export function emitSessionTranscriptUpdate(update: string | SessionTranscriptUp
message: update.message,
messageId: update.messageId,
};
const trimmed = normalized.sessionFile.trim();
const trimmed = normalizeOptionalString(normalized.sessionFile);
if (!trimmed) {
return;
}
const nextUpdate: SessionTranscriptUpdate = {
sessionFile: trimmed,
...(typeof normalized.sessionKey === "string" && normalized.sessionKey.trim()
? { sessionKey: normalized.sessionKey.trim() }
...(normalizeOptionalString(normalized.sessionKey)
? { sessionKey: normalizeOptionalString(normalized.sessionKey) }
: {}),
...(normalized.message !== undefined ? { message: normalized.message } : {}),
...(typeof normalized.messageId === "string" && normalized.messageId.trim()
? { messageId: normalized.messageId.trim() }
...(normalizeOptionalString(normalized.messageId)
? { messageId: normalizeOptionalString(normalized.messageId) }
: {}),
};
for (const listener of SESSION_TRANSCRIPT_LISTENERS) {