UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

483 lines (482 loc) 17.4 kB
import { a as normalizeLowercaseStringOrEmpty } from "./string-coerce-mnp54Vah.js"; import "./agent-scope-MrLta7Pq.js"; import { n as normalizeAccountId, t as DEFAULT_ACCOUNT_ID } from "./account-id-Df9e41E6.js"; import { a as buildAgentPeerSessionKey, h as sanitizeAgentId, i as buildAgentMainSessionKey, n as DEFAULT_MAIN_KEY, u as normalizeAgentId } from "./session-key-B_NoIfpX.js"; import { c as resolveDefaultAgentId } from "./agent-scope-config-CgCYpZfK.js"; import { a as shouldLogVerbose } from "./globals-GTrXU4s9.js"; import { t as logDebug } from "./logger-lqqYRtFw.js"; import { t as normalizeChatType } from "./chat-type-CxEvM45e.js"; import { c as normalizeRouteBindingRoles, n as listBindings, s as normalizeRouteBindingId, u as routeBindingScopeMatches } from "./bindings-BzzSfrl7.js"; //#region src/routing/peer-kind-match.ts function peerKindMatches(bindingKind, scopeKind) { if (bindingKind === scopeKind) return true; return bindingKind === "group" && scopeKind === "channel" || bindingKind === "channel" && scopeKind === "group"; } //#endregion //#region src/routing/resolve-route.ts function deriveLastRoutePolicy(params) { return params.sessionKey === params.mainSessionKey ? "main" : "session"; } function resolveInboundLastRouteSessionKey(params) { return params.route.lastRoutePolicy === "main" ? params.route.mainSessionKey : params.sessionKey; } function normalizeToken(value) { return normalizeLowercaseStringOrEmpty(value); } function normalizeId(value) { return normalizeRouteBindingId(value); } function buildAgentSessionKey(params) { const channel = normalizeToken(params.channel) || "unknown"; const peer = params.peer; return buildAgentPeerSessionKey({ agentId: params.agentId, mainKey: DEFAULT_MAIN_KEY, channel, accountId: params.accountId, peerKind: peer?.kind ?? "direct", peerId: peer ? normalizeId(peer.id) || "unknown" : null, dmScope: params.dmScope, identityLinks: params.identityLinks }); } function listAgents(cfg) { const agents = cfg.agents?.list; return Array.isArray(agents) ? agents : []; } const agentLookupCacheByCfg = /* @__PURE__ */ new WeakMap(); function resolveAgentLookupCache(cfg) { const agentsRef = cfg.agents; const existing = agentLookupCacheByCfg.get(cfg); if (existing && existing.agentsRef === agentsRef) return existing; const byNormalizedId = /* @__PURE__ */ new Map(); for (const agent of listAgents(cfg)) { const rawId = agent.id?.trim(); if (!rawId) continue; byNormalizedId.set(normalizeAgentId(rawId), sanitizeAgentId(rawId)); } const next = { agentsRef, byNormalizedId, fallbackDefaultAgentId: sanitizeAgentId(resolveDefaultAgentId(cfg)) }; agentLookupCacheByCfg.set(cfg, next); return next; } function pickFirstExistingAgentId(cfg, agentId) { const lookup = resolveAgentLookupCache(cfg); const trimmed = (agentId ?? "").trim(); if (!trimmed) return lookup.fallbackDefaultAgentId; const normalized = normalizeAgentId(trimmed); if (lookup.byNormalizedId.size === 0) return sanitizeAgentId(trimmed); const resolved = lookup.byNormalizedId.get(normalized); if (resolved) return resolved; return lookup.fallbackDefaultAgentId; } const evaluatedBindingsCacheByCfg = /* @__PURE__ */ new WeakMap(); const MAX_EVALUATED_BINDINGS_CACHE_KEYS = 2e3; const resolvedRouteCacheByCfg = /* @__PURE__ */ new WeakMap(); const MAX_RESOLVED_ROUTE_CACHE_KEYS = 4e3; function resolveAccountPatternKey(accountPattern) { if (!accountPattern.trim()) return DEFAULT_ACCOUNT_ID; return normalizeAccountId(accountPattern); } function buildEvaluatedBindingsByChannel(cfg) { const byChannel = /* @__PURE__ */ new Map(); let order = 0; for (const binding of listBindings(cfg)) { if (!binding || typeof binding !== "object") continue; const channel = normalizeToken(binding.match?.channel); if (!channel) continue; const match = normalizeBindingMatch(binding.match); const evaluated = { binding, match, order }; order += 1; let bucket = byChannel.get(channel); if (!bucket) { bucket = { byAccount: /* @__PURE__ */ new Map(), byAnyAccount: [] }; byChannel.set(channel, bucket); } if (match.accountPattern === "*") { bucket.byAnyAccount.push(evaluated); continue; } const accountKey = resolveAccountPatternKey(match.accountPattern); const existing = bucket.byAccount.get(accountKey); if (existing) { existing.push(evaluated); continue; } bucket.byAccount.set(accountKey, [evaluated]); } return byChannel; } function mergeEvaluatedBindingsInSourceOrder(accountScoped, anyAccount) { if (accountScoped.length === 0) return anyAccount; if (anyAccount.length === 0) return accountScoped; const merged = []; let accountIdx = 0; let anyIdx = 0; while (accountIdx < accountScoped.length && anyIdx < anyAccount.length) { const accountBinding = accountScoped[accountIdx]; const anyBinding = anyAccount[anyIdx]; if ((accountBinding?.order ?? Number.MAX_SAFE_INTEGER) <= (anyBinding?.order ?? Number.MAX_SAFE_INTEGER)) { if (accountBinding) merged.push(accountBinding); accountIdx += 1; continue; } if (anyBinding) merged.push(anyBinding); anyIdx += 1; } if (accountIdx < accountScoped.length) merged.push(...accountScoped.slice(accountIdx)); if (anyIdx < anyAccount.length) merged.push(...anyAccount.slice(anyIdx)); return merged; } function pushToIndexMap(map, key, binding) { if (!key) return; const existing = map.get(key); if (existing) { existing.push(binding); return; } map.set(key, [binding]); } function peerLookupKeys(kind, id) { if (kind === "group") return [`group:${id}`, `channel:${id}`]; if (kind === "channel") return [`channel:${id}`, `group:${id}`]; return [`${kind}:${id}`]; } function collectPeerIndexedBindings(index, peer) { if (!peer) return []; const out = []; const seen = /* @__PURE__ */ new Set(); for (const key of peerLookupKeys(peer.kind, peer.id)) { const matches = index.byPeer.get(key); if (!matches) continue; for (const match of matches) { if (seen.has(match)) continue; seen.add(match); out.push(match); } } return out; } function buildEvaluatedBindingsIndex(bindings) { const byPeer = /* @__PURE__ */ new Map(); const byPeerWildcard = []; const byGuildWithRoles = /* @__PURE__ */ new Map(); const byGuild = /* @__PURE__ */ new Map(); const byTeam = /* @__PURE__ */ new Map(); const byAccount = []; const byChannel = []; for (const binding of bindings) { if (binding.match.peer.state === "valid") { for (const key of peerLookupKeys(binding.match.peer.kind, binding.match.peer.id)) pushToIndexMap(byPeer, key, binding); continue; } if (binding.match.peer.state === "wildcard-kind") { byPeerWildcard.push(binding); continue; } if (binding.match.guildId && binding.match.roles) { pushToIndexMap(byGuildWithRoles, binding.match.guildId, binding); continue; } if (binding.match.guildId && !binding.match.roles) { pushToIndexMap(byGuild, binding.match.guildId, binding); continue; } if (binding.match.teamId) { pushToIndexMap(byTeam, binding.match.teamId, binding); continue; } if (binding.match.accountPattern !== "*") { byAccount.push(binding); continue; } byChannel.push(binding); } return { byPeer, byPeerWildcard, byGuildWithRoles, byGuild, byTeam, byAccount, byChannel }; } function getEvaluatedBindingsForChannelAccount(cfg, channel, accountId) { const bindingsRef = cfg.bindings; const existing = evaluatedBindingsCacheByCfg.get(cfg); const cache = existing && existing.bindingsRef === bindingsRef ? existing : { bindingsRef, byChannel: buildEvaluatedBindingsByChannel(cfg), byChannelAccount: /* @__PURE__ */ new Map(), byChannelAccountIndex: /* @__PURE__ */ new Map() }; if (cache !== existing) evaluatedBindingsCacheByCfg.set(cfg, cache); const cacheKey = `${channel}\t${accountId}`; const hit = cache.byChannelAccount.get(cacheKey); if (hit) return hit; const channelBindings = cache.byChannel.get(channel); const evaluated = mergeEvaluatedBindingsInSourceOrder(channelBindings?.byAccount.get(accountId) ?? [], channelBindings?.byAnyAccount ?? []); cache.byChannelAccount.set(cacheKey, evaluated); cache.byChannelAccountIndex.set(cacheKey, buildEvaluatedBindingsIndex(evaluated)); if (cache.byChannelAccount.size > MAX_EVALUATED_BINDINGS_CACHE_KEYS) { cache.byChannelAccount.clear(); cache.byChannelAccountIndex.clear(); cache.byChannelAccount.set(cacheKey, evaluated); cache.byChannelAccountIndex.set(cacheKey, buildEvaluatedBindingsIndex(evaluated)); } return evaluated; } function getEvaluatedBindingIndexForChannelAccount(cfg, channel, accountId) { const bindings = getEvaluatedBindingsForChannelAccount(cfg, channel, accountId); const existing = evaluatedBindingsCacheByCfg.get(cfg); const cacheKey = `${channel}\t${accountId}`; const indexed = existing?.byChannelAccountIndex.get(cacheKey); if (indexed) return indexed; const built = buildEvaluatedBindingsIndex(bindings); existing?.byChannelAccountIndex.set(cacheKey, built); return built; } function normalizePeerConstraint(peer) { if (!peer) return { state: "none" }; const kind = normalizeChatType(peer.kind); const id = normalizeId(peer.id); if (!kind || !id) return { state: "invalid" }; if (id === "*") return { state: "wildcard-kind", kind }; return { state: "valid", kind, id }; } function normalizeBindingMatch(match) { const rawRoles = match?.roles; return { accountPattern: (match?.accountId ?? "").trim(), peer: normalizePeerConstraint(match?.peer), guildId: normalizeId(match?.guildId) || null, teamId: normalizeId(match?.teamId) || null, roles: normalizeRouteBindingRoles(rawRoles) }; } function resolveRouteCacheForConfig(cfg) { const existing = resolvedRouteCacheByCfg.get(cfg); if (existing && existing.bindingsRef === cfg.bindings && existing.agentsRef === cfg.agents && existing.sessionRef === cfg.session) return existing.byKey; const byKey = /* @__PURE__ */ new Map(); resolvedRouteCacheByCfg.set(cfg, { bindingsRef: cfg.bindings, agentsRef: cfg.agents, sessionRef: cfg.session, byKey }); return byKey; } function formatRouteCachePeer(peer) { if (!peer || !peer.id) return "-"; return `${peer.kind}:${peer.id}`; } function formatRoleIdsCacheKey(roleIds) { const count = roleIds.length; if (count === 0) return "-"; if (count === 1) return roleIds[0] ?? "-"; if (count === 2) { const first = roleIds[0] ?? ""; const second = roleIds[1] ?? ""; return first <= second ? `${first},${second}` : `${second},${first}`; } return roleIds.toSorted().join(","); } function buildResolvedRouteCacheKey(params) { return `${params.channel}\t${params.accountId}\t${formatRouteCachePeer(params.peer)}\t${formatRouteCachePeer(params.parentPeer)}\t${params.guildId || "-"}\t${params.teamId || "-"}\t${formatRoleIdsCacheKey(params.memberRoleIds)}\t${params.dmScope}`; } function hasGuildConstraint(match) { return Boolean(match.guildId); } function hasTeamConstraint(match) { return Boolean(match.teamId); } function hasRolesConstraint(match) { return Boolean(match.roles); } function matchesBindingScope(match, scope) { if (match.peer.state === "invalid") return false; if (match.peer.state === "valid") { if (!scope.peer || !peerKindMatches(match.peer.kind, scope.peer.kind) || scope.peer.id !== match.peer.id) return false; } if (match.peer.state === "wildcard-kind") { if (!scope.peer || !peerKindMatches(match.peer.kind, scope.peer.kind)) return false; } return routeBindingScopeMatches(match, scope); } function resolveAgentRoute(input) { const channel = normalizeToken(input.channel); const accountId = normalizeAccountId(input.accountId); const peer = input.peer ? { kind: normalizeChatType(input.peer.kind) ?? input.peer.kind, id: normalizeId(input.peer.id) } : null; const guildId = normalizeId(input.guildId); const teamId = normalizeId(input.teamId); const memberRoleIds = input.memberRoleIds ?? []; const memberRoleIdSet = new Set(memberRoleIds); const dmScope = input.cfg.session?.dmScope ?? "main"; const identityLinks = input.cfg.session?.identityLinks; const shouldLogDebug = shouldLogVerbose(); const parentPeer = input.parentPeer ? { kind: normalizeChatType(input.parentPeer.kind) ?? input.parentPeer.kind, id: normalizeId(input.parentPeer.id) } : null; const routeCache = !shouldLogDebug && !identityLinks ? resolveRouteCacheForConfig(input.cfg) : null; const routeCacheKey = routeCache ? buildResolvedRouteCacheKey({ channel, accountId, peer, parentPeer, guildId, teamId, memberRoleIds, dmScope }) : ""; if (routeCache && routeCacheKey) { const cachedRoute = routeCache.get(routeCacheKey); if (cachedRoute) return { ...cachedRoute }; } const bindings = getEvaluatedBindingsForChannelAccount(input.cfg, channel, accountId); const bindingsIndex = getEvaluatedBindingIndexForChannelAccount(input.cfg, channel, accountId); const choose = (agentId, matchedBy, sessionOverride) => { const resolvedAgentId = pickFirstExistingAgentId(input.cfg, agentId); const sessionKey = buildAgentSessionKey({ agentId: resolvedAgentId, channel, accountId, peer, dmScope: sessionOverride?.dmScope ?? dmScope, identityLinks }); const mainSessionKey = normalizeLowercaseStringOrEmpty(buildAgentMainSessionKey({ agentId: resolvedAgentId, mainKey: DEFAULT_MAIN_KEY })); const route = { agentId: resolvedAgentId, channel, accountId, sessionKey, mainSessionKey, lastRoutePolicy: deriveLastRoutePolicy({ sessionKey, mainSessionKey }), matchedBy }; if (routeCache && routeCacheKey) { routeCache.set(routeCacheKey, route); if (routeCache.size > MAX_RESOLVED_ROUTE_CACHE_KEYS) { routeCache.clear(); routeCache.set(routeCacheKey, route); } } return route; }; const formatPeer = (value) => value?.kind && value?.id ? `${value.kind}:${value.id}` : "none"; const formatNormalizedPeer = (value) => { if (value.state === "none") return "none"; if (value.state === "invalid") return "invalid"; if (value.state === "wildcard-kind") return `${value.kind}:*`; return `${value.kind}:${value.id}`; }; if (shouldLogDebug) { logDebug(`[routing] resolveAgentRoute: channel=${channel} accountId=${accountId} peer=${formatPeer(peer)} guildId=${guildId || "none"} teamId=${teamId || "none"} bindings=${bindings.length}`); for (const entry of bindings) logDebug(`[routing] binding: agentId=${entry.binding.agentId} accountPattern=${entry.match.accountPattern || "default"} peer=${formatNormalizedPeer(entry.match.peer)} guildId=${entry.match.guildId ?? "none"} teamId=${entry.match.teamId ?? "none"} roles=${entry.match.roles?.length ?? 0}`); } const baseScope = { guildId, teamId, memberRoleIds: memberRoleIdSet }; const tiers = [ { matchedBy: "binding.peer", enabled: Boolean(peer), scopePeer: peer, candidates: collectPeerIndexedBindings(bindingsIndex, peer), predicate: (candidate) => candidate.match.peer.state === "valid" }, { matchedBy: "binding.peer.parent", enabled: Boolean(parentPeer && parentPeer.id), scopePeer: parentPeer && parentPeer.id ? parentPeer : null, candidates: collectPeerIndexedBindings(bindingsIndex, parentPeer), predicate: (candidate) => candidate.match.peer.state === "valid" }, { matchedBy: "binding.peer.wildcard", enabled: Boolean(peer), scopePeer: peer, candidates: bindingsIndex.byPeerWildcard, predicate: (candidate) => candidate.match.peer.state === "wildcard-kind" }, { matchedBy: "binding.guild+roles", enabled: Boolean(guildId && memberRoleIds.length > 0), scopePeer: peer, candidates: guildId ? bindingsIndex.byGuildWithRoles.get(guildId) ?? [] : [], predicate: (candidate) => hasGuildConstraint(candidate.match) && hasRolesConstraint(candidate.match) }, { matchedBy: "binding.guild", enabled: Boolean(guildId), scopePeer: peer, candidates: guildId ? bindingsIndex.byGuild.get(guildId) ?? [] : [], predicate: (candidate) => hasGuildConstraint(candidate.match) && !hasRolesConstraint(candidate.match) }, { matchedBy: "binding.team", enabled: Boolean(teamId), scopePeer: peer, candidates: teamId ? bindingsIndex.byTeam.get(teamId) ?? [] : [], predicate: (candidate) => hasTeamConstraint(candidate.match) }, { matchedBy: "binding.account", enabled: true, scopePeer: peer, candidates: bindingsIndex.byAccount, predicate: (candidate) => candidate.match.accountPattern !== "*" }, { matchedBy: "binding.channel", enabled: true, scopePeer: peer, candidates: bindingsIndex.byChannel, predicate: (candidate) => candidate.match.accountPattern === "*" } ]; for (const tier of tiers) { if (!tier.enabled) continue; const matched = tier.candidates.find((candidate) => tier.predicate(candidate) && matchesBindingScope(candidate.match, { ...baseScope, peer: tier.scopePeer })); if (matched) { if (shouldLogDebug) logDebug(`[routing] match: matchedBy=${tier.matchedBy} agentId=${matched.binding.agentId}`); return choose(matched.binding.agentId, tier.matchedBy, matched.binding.session); } } return choose(resolveDefaultAgentId(input.cfg), "default"); } //#endregion export { resolveInboundLastRouteSessionKey as a, resolveAgentRoute as i, deriveLastRoutePolicy as n, peerKindMatches as o, pickFirstExistingAgentId as r, buildAgentSessionKey as t };