File size: 4,274 Bytes
76289e7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | // Binding scope helpers normalize route binding scope values.
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
import { normalizeChatChannelId } from "../channels/ids.js";
import type { AgentRouteBinding } from "../config/types.agents.js";
import { normalizeAccountId, normalizeAgentId } from "./session-key.js";
// Route binding scopes constrain a configured agent/account binding to a guild,
// team, group space, and optionally channel/platform role ids.
type RouteBindingScopeConstraint = {
guildId?: string | null;
teamId?: string | null;
roles?: string[] | null;
};
type RouteBindingScope = {
guildId?: string | null;
teamId?: string | null;
groupSpace?: string | null;
memberRoleIds?: Iterable<string> | null;
};
type NormalizedRouteBindingMatch = {
agentId: string;
accountId: string;
channelId: string;
};
export function normalizeRouteBindingId(value: unknown): string {
if (typeof value === "string") {
return value.trim();
}
if (typeof value === "number" || typeof value === "bigint") {
return String(value).trim();
}
return "";
}
export function normalizeRouteBindingRoles(value: string[] | null | undefined): string[] | null {
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;
}
// Convert a binding match into the same canonical ids used by session routing.
// Diagnostics include implicit defaults; outbound account selection requires explicit bindings.
export function resolveNormalizedRouteBindingMatch(
binding: AgentRouteBinding,
options?: { includeImplicitDefaultAccount?: boolean },
): 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;
}
if (match.accountId !== undefined && typeof match.accountId !== "string") {
return null;
}
const accountId = match.accountId?.trim();
if (accountId === "*" || (!accountId && !options?.includeImplicitDefaultAccount)) {
return null;
}
return {
agentId: normalizeAgentId(binding.agentId),
accountId: normalizeAccountId(accountId),
channelId,
};
}
function scopeIdMatches(params: {
constraint: string | null | undefined;
exact: string;
groupSpace: string;
}): boolean {
if (!params.constraint) {
return true;
}
return params.constraint === params.exact || params.constraint === params.groupSpace;
}
function hasRoleLookup(
memberRoleIds: Iterable<string>,
): memberRoleIds is Iterable<string> & { has(roleId: string): boolean } {
return typeof (memberRoleIds as { has?: unknown }).has === "function";
}
function hasAnyRouteBindingRole(
roles: readonly string[],
memberRoleIds: Iterable<string> | null | undefined,
): boolean {
if (!memberRoleIds) {
return false;
}
if (hasRoleLookup(memberRoleIds)) {
return roles.some((role) => memberRoleIds.has(role));
}
// Most callers pass a Set, but arrays/iterables from channel adapters are
// accepted to avoid forcing allocation at every routing call site.
const memberRoleIdSet = new Set(memberRoleIds);
return roles.some((role) => memberRoleIdSet.has(role));
}
export function routeBindingScopeMatches(
constraint: RouteBindingScopeConstraint,
scope: RouteBindingScope,
): boolean {
const guildId = normalizeRouteBindingId(scope.guildId);
const teamId = normalizeRouteBindingId(scope.teamId);
const groupSpace = normalizeRouteBindingId(scope.groupSpace);
if (!scopeIdMatches({ constraint: constraint.guildId, exact: guildId, groupSpace })) {
return false;
}
if (!scopeIdMatches({ constraint: constraint.teamId, exact: teamId, groupSpace })) {
return false;
}
const roles = normalizeRouteBindingRoles(constraint.roles);
if (!roles) {
return true;
}
return hasAnyRouteBindingRole(roles, scope.memberRoleIds);
}
|