refactor: dedupe provider and channel string helpers

This commit is contained in:
Peter Steinberger
2026-04-07 09:34:48 +01:00
parent b697cec223
commit 649de6d156
14 changed files with 41 additions and 82 deletions

View File

@@ -24,7 +24,7 @@ import {
import { logVerbose, shouldLogVerbose } from "openclaw/plugin-sdk/runtime-env";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import { normalizeAllowFrom } from "./bot-access.js";
import { resolveLineGroupConfigEntry, resolveLineGroupHistoryKey } from "./group-keys.js";
import { resolveLineGroupConfigEntry } from "./group-keys.js";
import type { LineGroupConfig, ResolvedLineAccount } from "./types.js";
type EventSource = webhook.Source | undefined;
@@ -77,10 +77,9 @@ function buildPeerId(source: EventSource): string {
if (!source) {
return "unknown";
}
const groupKey = resolveLineGroupHistoryKey({
groupId: source.type === "group" ? source.groupId : undefined,
roomId: source.type === "room" ? source.roomId : undefined,
});
const groupKey =
normalizeOptionalString(source.type === "group" ? source.groupId : undefined) ??
normalizeOptionalString(source.type === "room" ? source.roomId : undefined);
if (groupKey) {
return groupKey;
}

View File

@@ -2,7 +2,6 @@ import { describe, expect, it } from "vitest";
import {
resolveExactLineGroupConfigKey,
resolveLineGroupConfigEntry,
resolveLineGroupHistoryKey,
resolveLineGroupLookupIds,
resolveLineGroupsConfig,
} from "./group-keys.js";
@@ -39,14 +38,6 @@ describe("resolveLineGroupConfigEntry", () => {
});
});
describe("resolveLineGroupHistoryKey", () => {
it("uses the raw group or room id as the shared LINE peer key", () => {
expect(resolveLineGroupHistoryKey({ groupId: "g1" })).toBe("g1");
expect(resolveLineGroupHistoryKey({ roomId: "r1" })).toBe("r1");
expect(resolveLineGroupHistoryKey({})).toBeUndefined();
});
});
describe("account-scoped LINE groups", () => {
it("resolves the effective account-scoped groups map", () => {
const cfg = {

View File

@@ -1,7 +1,6 @@
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
import type { OpenClawConfig } from "openclaw/plugin-sdk/account-resolution";
import { resolveAccountEntry } from "openclaw/plugin-sdk/account-resolution";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import type { LineConfig, LineGroupConfig } from "./types.js";
export function resolveLineGroupLookupIds(groupId?: string | null): string[] {
@@ -64,10 +63,3 @@ export function resolveExactLineGroupConfigKey(params: {
Object.hasOwn(groups, candidate),
);
}
export function resolveLineGroupHistoryKey(params: {
groupId?: string | null;
roomId?: string | null;
}): string | undefined {
return normalizeOptionalString(params.groupId) ?? normalizeOptionalString(params.roomId);
}

View File

@@ -1,5 +1,4 @@
import type { OpenClawConfig, HumanDelayConfig, IdentityConfig } from "../config/config.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { resolveAgentConfig } from "./agent-scope.js";
const DEFAULT_ACK_REACTION = "👀";
@@ -57,11 +56,6 @@ export function resolveIdentityNamePrefix(
return `[${name}]`;
}
/** Returns just the identity name (without brackets) for template context. */
export function resolveIdentityName(cfg: OpenClawConfig, agentId: string): string | undefined {
return normalizeOptionalString(resolveAgentIdentity(cfg, agentId)?.name);
}
export function resolveMessagePrefix(
cfg: OpenClawConfig,
agentId: string,

View File

@@ -15,7 +15,6 @@ import {
ACP_SESSIONS_USAGE,
formatAcpCapabilitiesText,
resolveAcpInstallCommandHint,
resolveConfiguredAcpBackendId,
stopWithText,
} from "./shared.js";
import { resolveBoundAcpThreadSessionKey } from "./targets.js";
@@ -28,7 +27,7 @@ export async function handleAcpDoctorAction(
return stopWithText(`⚠️ ${ACP_DOCTOR_USAGE}`);
}
const backendId = resolveConfiguredAcpBackendId(params.cfg);
const backendId = normalizeOptionalString(params.cfg.acp?.backend) ?? "acpx";
const installHint = resolveAcpInstallCommandHint(params.cfg);
const registeredBackend = getAcpRuntimeBackend(backendId);
const managerSnapshot = getAcpSessionManager().getObservabilitySnapshot(params.cfg);
@@ -116,7 +115,7 @@ export function handleAcpInstallAction(
if (restTokens.length > 0) {
return stopWithText(`⚠️ ${ACP_INSTALL_USAGE}`);
}
const backendId = resolveConfiguredAcpBackendId(params.cfg);
const backendId = normalizeOptionalString(params.cfg.acp?.backend) ?? "acpx";
const installHint = resolveAcpInstallCommandHint(params.cfg);
const lines = [
"ACP install:",

View File

@@ -1,7 +1,7 @@
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../../config/config.js";
import { resolveAcpInstallCommandHint, resolveConfiguredAcpBackendId } from "./install-hints.js";
import { resolveAcpInstallCommandHint } from "./install-hints.js";
function withAcpConfig(acp: OpenClawConfig["acp"]): OpenClawConfig {
return { acp } as OpenClawConfig;
@@ -35,7 +35,6 @@ describe("ACP install hints", () => {
it("returns generic plugin hint for non-acpx backend", () => {
const cfg = withAcpConfig({ backend: "custom-backend" });
expect(resolveConfiguredAcpBackendId(cfg)).toBe("custom-backend");
expect(resolveAcpInstallCommandHint(cfg)).toContain('ACP backend "custom-backend"');
});
});

View File

@@ -5,17 +5,13 @@ import { resolveBundledPluginWorkspaceSourcePath } from "../../../plugins/bundle
import { resolveBundledPluginInstallCommandHint } from "../../../plugins/bundled-sources.js";
import { normalizeOptionalString } from "../../../shared/string-coerce.js";
export function resolveConfiguredAcpBackendId(cfg: OpenClawConfig): string {
return normalizeOptionalString(cfg.acp?.backend) || "acpx";
}
export function resolveAcpInstallCommandHint(cfg: OpenClawConfig): string {
const configured = normalizeOptionalString(cfg.acp?.runtime?.installCommand);
if (configured) {
return configured;
}
const workspaceDir = process.cwd();
const backendId = resolveConfiguredAcpBackendId(cfg).toLowerCase();
const backendId = normalizeOptionalString(cfg.acp?.backend)?.toLowerCase() ?? "acpx";
if (backendId === "acpx") {
const workspaceLocalPath = resolveBundledPluginWorkspaceSourcePath({
rootDir: workspaceDir,

View File

@@ -8,7 +8,7 @@ import { normalizeAgentId } from "../../../routing/session-key.js";
import { normalizeOptionalString } from "../../../shared/string-coerce.js";
import type { CommandHandlerResult, HandleCommandsParams } from "../commands-types.js";
import { resolveAcpCommandChannel, resolveAcpCommandThreadId } from "./context.js";
export { resolveAcpInstallCommandHint, resolveConfiguredAcpBackendId } from "./install-hints.js";
export { resolveAcpInstallCommandHint } from "./install-hints.js";
export const COMMAND = "/acp";
export const ACP_SPAWN_USAGE =

View File

@@ -1,10 +1,11 @@
import { resolveEffectiveMessagesConfig, resolveIdentityName } from "../agents/identity.js";
import { resolveAgentIdentity, resolveEffectiveMessagesConfig } from "../agents/identity.js";
import {
extractShortModelName,
type ResponsePrefixContext,
} from "../auto-reply/reply/response-prefix-template.js";
import type { GetReplyOptions } from "../auto-reply/types.js";
import type { OpenClawConfig } from "../config/config.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
type ModelSelectionContext = Parameters<NonNullable<GetReplyOptions["onModelSelected"]>>[0];
@@ -28,7 +29,7 @@ export function createReplyPrefixContext(params: {
}): ReplyPrefixContextBundle {
const { cfg, agentId } = params;
const prefixContext: ResponsePrefixContext = {
identityName: resolveIdentityName(cfg, agentId),
identityName: normalizeOptionalString(resolveAgentIdentity(cfg, agentId)?.name),
};
const onModelSelected = (ctx: ModelSelectionContext) => {

View File

@@ -1,13 +1,10 @@
import { describe, expect, it } from "vitest";
import {
buildCapabilityProviderMaps,
normalizeCapabilityProviderId,
} from "./provider-registry-shared.js";
import { buildCapabilityProviderMaps } from "./provider-registry-shared.js";
describe("provider registry shared", () => {
it("normalizes provider ids case-insensitively", () => {
expect(normalizeCapabilityProviderId(" OpenAI ")).toBe("openai");
expect(normalizeCapabilityProviderId(" ")).toBeUndefined();
const { canonical } = buildCapabilityProviderMaps([{ id: " OpenAI " }, { id: " " }]);
expect([...canonical.keys()]).toEqual(["openai"]);
});
it("indexes providers by id and alias", () => {

View File

@@ -1,14 +1,9 @@
import { normalizeOptionalString } from "../shared/string-coerce.js";
export function normalizeCapabilityProviderId(providerId: string | undefined): string | undefined {
return normalizeOptionalString(providerId)?.toLowerCase();
}
export function buildCapabilityProviderMaps<T extends { id: string; aliases?: readonly string[] }>(
providers: readonly T[],
normalizeId: (
providerId: string | undefined,
) => string | undefined = normalizeCapabilityProviderId,
normalizeId: (providerId: string | undefined) => string | undefined = (providerId) =>
normalizeOptionalString(providerId)?.toLowerCase(),
): {
canonical: Map<string, T>;
aliases: Map<string, T>;

View File

@@ -1,16 +1,14 @@
import type { OpenClawConfig } from "../config/config.js";
import { resolvePluginCapabilityProviders } from "../plugins/capability-provider-runtime.js";
import {
buildCapabilityProviderMaps,
normalizeCapabilityProviderId,
} from "../plugins/provider-registry-shared.js";
import { buildCapabilityProviderMaps } from "../plugins/provider-registry-shared.js";
import type { RealtimeVoiceProviderPlugin } from "../plugins/types.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import type { RealtimeVoiceProviderId } from "./provider-types.js";
export function normalizeRealtimeVoiceProviderId(
providerId: string | undefined,
): RealtimeVoiceProviderId | undefined {
return normalizeCapabilityProviderId(providerId);
return normalizeOptionalString(providerId)?.toLowerCase();
}
function resolveRealtimeVoiceProviderEntries(cfg?: OpenClawConfig): RealtimeVoiceProviderPlugin[] {

View File

@@ -523,8 +523,8 @@ function taskRunScopeKey(
return [
task.runtime,
task.scopeKind,
normalizeComparableText(task.ownerKey),
normalizeComparableText(task.childSessionKey),
normalizeOptionalString(task.ownerKey) ?? "",
normalizeOptionalString(task.childSessionKey) ?? "",
].join("\u0000");
}
@@ -562,9 +562,10 @@ function getPeerTasksForDelivery(task: TaskRecord): TaskRecord[] {
(candidate) =>
candidate.runtime === task.runtime &&
candidate.scopeKind === task.scopeKind &&
normalizeComparableText(candidate.ownerKey) === normalizeComparableText(task.ownerKey) &&
normalizeComparableText(candidate.childSessionKey) ===
normalizeComparableText(task.childSessionKey),
(normalizeOptionalString(candidate.ownerKey) ?? "") ===
(normalizeOptionalString(task.ownerKey) ?? "") &&
(normalizeOptionalString(candidate.childSessionKey) ?? "") ===
(normalizeOptionalString(task.childSessionKey) ?? ""),
);
}
@@ -583,10 +584,6 @@ function pickPreferredRunIdTask(matches: TaskRecord[]): TaskRecord | undefined {
})[0];
}
function normalizeComparableText(value: string | undefined): string {
return normalizeOptionalString(value) ?? "";
}
function compareTasksNewestFirst(
left: Pick<TaskRecord, "createdAt"> & { insertionIndex?: number },
right: Pick<TaskRecord, "createdAt"> & { insertionIndex?: number },
@@ -614,18 +611,21 @@ function findExistingTaskForCreate(params: {
(task) =>
task.runtime === params.runtime &&
task.scopeKind === params.scopeKind &&
normalizeComparableText(task.ownerKey) === normalizeComparableText(params.ownerKey) &&
normalizeComparableText(task.childSessionKey) ===
normalizeComparableText(params.childSessionKey) &&
normalizeComparableText(task.parentFlowId) ===
normalizeComparableText(params.parentFlowId),
(normalizeOptionalString(task.ownerKey) ?? "") ===
(normalizeOptionalString(params.ownerKey) ?? "") &&
(normalizeOptionalString(task.childSessionKey) ?? "") ===
(normalizeOptionalString(params.childSessionKey) ?? "") &&
(normalizeOptionalString(task.parentFlowId) ?? "") ===
(normalizeOptionalString(params.parentFlowId) ?? ""),
)
: [];
const exact = runId
? runScopeMatches.find(
(task) =>
normalizeComparableText(task.label) === normalizeComparableText(params.label) &&
normalizeComparableText(task.task) === normalizeComparableText(params.task),
(normalizeOptionalString(task.label) ?? "") ===
(normalizeOptionalString(params.label) ?? "") &&
(normalizeOptionalString(task.task) ?? "") ===
(normalizeOptionalString(params.task) ?? ""),
)
: undefined;
if (exact) {
@@ -688,11 +688,11 @@ function mergeExistingTaskForCreate(
}
const nextLabel = params.label?.trim();
if (params.preferMetadata) {
if (nextLabel && normalizeComparableText(existing.label) !== nextLabel) {
if (nextLabel && (normalizeOptionalString(existing.label) ?? "") !== nextLabel) {
patch.label = nextLabel;
}
const nextTask = params.task.trim();
if (nextTask && normalizeComparableText(existing.task) !== nextTask) {
if (nextTask && (normalizeOptionalString(existing.task) ?? "") !== nextTask) {
patch.task = nextTask;
}
} else if (nextLabel && !existing.label?.trim()) {

View File

@@ -1,16 +1,14 @@
import type { OpenClawConfig } from "../config/config.js";
import { resolvePluginCapabilityProviders } from "../plugins/capability-provider-runtime.js";
import {
buildCapabilityProviderMaps,
normalizeCapabilityProviderId,
} from "../plugins/provider-registry-shared.js";
import { buildCapabilityProviderMaps } from "../plugins/provider-registry-shared.js";
import type { SpeechProviderPlugin } from "../plugins/types.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import type { SpeechProviderId } from "./provider-types.js";
export function normalizeSpeechProviderId(
providerId: string | undefined,
): SpeechProviderId | undefined {
return normalizeCapabilityProviderId(providerId);
return normalizeOptionalString(providerId)?.toLowerCase();
}
function resolveSpeechProviderPluginEntries(cfg?: OpenClawConfig): SpeechProviderPlugin[] {