refactor: share route binding normalization

This commit is contained in:
Peter Steinberger
2026-04-20 13:21:09 +01:00
parent 1e4f3f2123
commit d2b67fbb68
3 changed files with 64 additions and 74 deletions

View File

@@ -1,3 +1,8 @@
import { normalizeChatChannelId } from "../channels/ids.js";
import type { AgentRouteBinding } from "../config/types.agents.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import { normalizeAccountId, normalizeAgentId } from "./session-key.js";
export type RouteBindingScopeConstraint = {
guildId?: string | null;
teamId?: string | null;
@@ -11,6 +16,12 @@ export type RouteBindingScope = {
memberRoleIds?: Iterable<string> | null;
};
export type NormalizedRouteBindingMatch = {
agentId: string;
accountId: string;
channelId: string;
};
export function normalizeRouteBindingId(value: unknown): string {
if (typeof value === "string") {
return value.trim();
@@ -25,6 +36,40 @@ export function normalizeRouteBindingRoles(value: string[] | null | undefined):
return Array.isArray(value) && value.length > 0 ? value : null;
}
export function normalizeRouteBindingChannelId(raw?: string | null): string | null {
const normalized = normalizeChatChannelId(raw);
if (normalized) {
return normalized;
}
const fallback = normalizeLowercaseStringOrEmpty(raw);
return fallback || null;
}
export function resolveNormalizedRouteBindingMatch(
binding: AgentRouteBinding,
): NormalizedRouteBindingMatch | null {
if (!binding || typeof binding !== "object") {
return null;
}
const match = binding.match;
if (!match || typeof match !== "object") {
return null;
}
const channelId = normalizeRouteBindingChannelId(match.channel);
if (!channelId) {
return null;
}
const accountId = typeof match.accountId === "string" ? match.accountId.trim() : "";
if (!accountId || accountId === "*") {
return null;
}
return {
agentId: normalizeAgentId(binding.agentId),
accountId: normalizeAccountId(accountId),
channelId,
};
}
function scopeIdMatches(params: {
constraint: string | null | undefined;
exact: string;

View File

@@ -1,59 +1,25 @@
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
import { normalizeChatChannelId } from "../channels/ids.js";
import { listRouteBindings } from "../config/bindings.js";
import type { AgentRouteBinding } from "../config/types.agents.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import { normalizeAccountId, normalizeAgentId } from "./session-key.js";
function normalizeBindingChannelId(raw?: string | null): string | null {
const normalized = normalizeChatChannelId(raw);
if (normalized) {
return normalized;
}
const fallback = normalizeLowercaseStringOrEmpty(raw);
return fallback || null;
}
import {
normalizeRouteBindingChannelId,
resolveNormalizedRouteBindingMatch,
} from "./binding-scope.js";
import { normalizeAgentId } from "./session-key.js";
export function listBindings(cfg: OpenClawConfig): AgentRouteBinding[] {
return listRouteBindings(cfg);
}
function resolveNormalizedBindingMatch(binding: AgentRouteBinding): {
agentId: string;
accountId: string;
channelId: string;
} | null {
if (!binding || typeof binding !== "object") {
return null;
}
const match = binding.match;
if (!match || typeof match !== "object") {
return null;
}
const channelId = normalizeBindingChannelId(match.channel);
if (!channelId) {
return null;
}
const accountId = typeof match.accountId === "string" ? match.accountId.trim() : "";
if (!accountId || accountId === "*") {
return null;
}
return {
agentId: normalizeAgentId(binding.agentId),
accountId: normalizeAccountId(accountId),
channelId,
};
}
export function listBoundAccountIds(cfg: OpenClawConfig, channelId: string): string[] {
const normalizedChannel = normalizeBindingChannelId(channelId);
const normalizedChannel = normalizeRouteBindingChannelId(channelId);
if (!normalizedChannel) {
return [];
}
const ids = new Set<string>();
for (const binding of listBindings(cfg)) {
const resolved = resolveNormalizedBindingMatch(binding);
const resolved = resolveNormalizedRouteBindingMatch(binding);
if (!resolved || resolved.channelId !== normalizedChannel) {
continue;
}
@@ -66,13 +32,13 @@ export function resolveDefaultAgentBoundAccountId(
cfg: OpenClawConfig,
channelId: string,
): string | null {
const normalizedChannel = normalizeBindingChannelId(channelId);
const normalizedChannel = normalizeRouteBindingChannelId(channelId);
if (!normalizedChannel) {
return null;
}
const defaultAgentId = normalizeAgentId(resolveDefaultAgentId(cfg));
for (const binding of listBindings(cfg)) {
const resolved = resolveNormalizedBindingMatch(binding);
const resolved = resolveNormalizedRouteBindingMatch(binding);
if (
!resolved ||
resolved.channelId !== normalizedChannel ||
@@ -88,7 +54,7 @@ export function resolveDefaultAgentBoundAccountId(
export function buildChannelAccountBindings(cfg: OpenClawConfig) {
const map = new Map<string, Map<string, string[]>>();
for (const binding of listBindings(cfg)) {
const resolved = resolveNormalizedBindingMatch(binding);
const resolved = resolveNormalizedRouteBindingMatch(binding);
if (!resolved) {
continue;
}

View File

@@ -1,27 +1,18 @@
import { normalizeChatType, type ChatType } from "../channels/chat-type.js";
import { normalizeChatChannelId } from "../channels/ids.js";
import { listRouteBindings } from "../config/bindings.js";
import type { AgentRouteBinding } from "../config/types.agents.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import {
normalizeRouteBindingChannelId,
normalizeRouteBindingId,
normalizeRouteBindingRoles,
resolveNormalizedRouteBindingMatch,
routeBindingScopeMatches,
} from "./binding-scope.js";
import { peerKindMatches } from "./peer-kind-match.js";
import { normalizeAccountId, normalizeAgentId } from "./session-key.js";
import { normalizeAgentId } from "./session-key.js";
function normalizeBindingChannelId(raw?: string | null): string | null {
const normalized = normalizeChatChannelId(raw);
if (normalized) {
return normalized;
}
const fallback = normalizeLowercaseStringOrEmpty(raw);
return fallback || null;
}
function resolveNormalizedBindingMatch(binding: AgentRouteBinding): {
function resolveNormalizedBoundAccountMatch(binding: AgentRouteBinding): {
agentId: string;
accountId: string;
channelId: string;
@@ -31,27 +22,15 @@ function resolveNormalizedBindingMatch(binding: AgentRouteBinding): {
teamId?: string | null;
roles?: string[] | null;
} | null {
if (!binding || typeof binding !== "object") {
return null;
}
const baseMatch = resolveNormalizedRouteBindingMatch(binding);
const match = binding.match;
if (!match || typeof match !== "object") {
return null;
}
const channelId = normalizeBindingChannelId(match.channel);
if (!channelId) {
return null;
}
const accountId = typeof match.accountId === "string" ? match.accountId.trim() : "";
if (!accountId || accountId === "*") {
if (!baseMatch || !match || typeof match !== "object") {
return null;
}
const peerId = match.peer && typeof match.peer.id === "string" ? match.peer.id.trim() : undefined;
const peerKind = match.peer ? normalizeChatType(match.peer.kind) : undefined;
return {
agentId: normalizeAgentId(binding.agentId),
accountId: normalizeAccountId(accountId),
channelId,
...baseMatch,
peerId: peerId || undefined,
peerKind: peerKind ?? undefined,
guildId: normalizeRouteBindingId(match.guildId) || null,
@@ -88,7 +67,7 @@ export function resolveFirstBoundAccountId(params: {
groupSpace?: string | null;
memberRoleIds?: string[];
}): string | undefined {
const normalizedChannel = normalizeBindingChannelId(params.channelId);
const normalizedChannel = normalizeRouteBindingChannelId(params.channelId);
if (!normalizedChannel) {
return undefined;
}
@@ -103,7 +82,7 @@ export function resolveFirstBoundAccountId(params: {
let wildcardPeerMatch: string | undefined;
let channelOnlyFallback: string | undefined;
for (const binding of listRouteBindings(params.cfg)) {
const resolved = resolveNormalizedBindingMatch(binding);
const resolved = resolveNormalizedBoundAccountMatch(binding);
if (
!resolved ||
resolved.channelId !== normalizedChannel ||