UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

523 lines (522 loc) 19.5 kB
import { o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js"; import { n as normalizeAgentId } from "./agent-id-CeT3w4ap.js"; import { D as tryResolveLegacyCompatibilityAgentId, i as listAgentEntries, t as AgentSelectionRequiredError, y as resolveDefaultAgentId } from "./agent-scope-config-DcbEhP0R.js"; import { a as buildAgentMainSessionKey, g as sanitizeAgentId, o as buildAgentPeerSessionKey, t as DEFAULT_AGENT_ID } from "./session-key-BnWWjqNc.js"; import { n as normalizeAccountId } from "./account-id-CETVCrTz.js"; import { a as routeBindingScopeMatches, n as normalizeRouteBindingId, r as normalizeRouteBindingRoles } from "./binding-scope-ZjLCBNrG.js"; import "./agent-scope-DbtJyKUL.js"; import { i as shouldLogVerbose } from "./globals-CTaGxEqj.js"; import { t as logDebug } from "./logger-DwECwNVZ.js"; import { t as normalizeChatType } from "./chat-type-CG0X_HJM.js"; import { n as listBindings } from "./bindings-RJ9_sjHk.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 buildAgentSessionKey(params) { const channel = normalizeLowercaseStringOrEmpty(params.channel) || "unknown"; const peer = params.peer; return buildAgentPeerSessionKey({ agentId: params.agentId, mainKey: params.mainKey ?? "main", channel, accountId: params.accountId, peerKind: peer?.kind ?? "direct", peerId: peer ? normalizeRouteBindingId(peer.id) || "unknown" : null, dmScope: params.dmScope, groupScope: params.groupScope, identityLinks: params.identityLinks }); } 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 listAgentEntries(cfg)) { const rawId = agent.id?.trim(); if (!rawId) continue; byNormalizedId.set(normalizeAgentId(rawId), sanitizeAgentId(rawId)); } const next = { agentsRef, byNormalizedId, fallbackSoleAgentId: tryResolveLegacyCompatibilityAgentId(cfg) }; agentLookupCacheByCfg.set(cfg, next); return next; } function pickFirstExistingAgentId(cfg, agentId) { const lookup = resolveAgentLookupCache(cfg); const trimmed = (agentId ?? "").trim(); if (!trimmed) return sanitizeAgentId(lookup.fallbackSoleAgentId ?? resolveDefaultAgentId(cfg, { surface: "agent lookup", hint: "Pass an explicit agent id instead of relying on an implicit route." })); const normalized = normalizeAgentId(trimmed); const resolved = lookup.byNormalizedId.get(normalized); if (resolved) return resolved; if (normalized === "main") return DEFAULT_AGENT_ID; if (lookup.byNormalizedId.size === 0) return sanitizeAgentId(trimmed); throw new AgentSelectionRequiredError([...lookup.byNormalizedId.values()], { surface: "route binding", hint: `Update the binding agentId "${trimmed}" to a configured agent.` }); } 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 buildEvaluatedBindingsByChannel(cfg) { const byChannel = /* @__PURE__ */ new Map(); let order = 0; for (const binding of listBindings(cfg)) { if (!binding || typeof binding !== "object") continue; const channel = normalizeLowercaseStringOrEmpty(binding.match?.channel); if (!channel) continue; const match = normalizeBindingMatch(binding.match); if (match.peer.state === "invalid") continue; 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 = normalizeAccountId(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 peerLookupKey(kind, id) { return `${kind === "channel" ? "group" : kind}:${id}`; } function getPeerIndexedBindings(index, peer) { return peer ? index.byPeer.get(peerLookupKey(peer.kind, peer.id)) ?? [] : []; } 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") { pushToIndexMap(byPeer, peerLookupKey(binding.match.peer.kind, binding.match.peer.id), 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; } /** @internal Lists matchable candidates from the canonical channel/account binding index. */ function listChannelAccountRouteBindings(input) { return getEvaluatedBindingsForChannelAccount(input.cfg, normalizeLowercaseStringOrEmpty(input.channel), normalizeAccountId(input.accountId)).map(({ binding }) => binding); } /** @internal Lists exact DM peers from the canonical channel/account binding index. */ function listExactDirectMessageBindingPeerIds(input) { const prefix = "direct:"; return [...getEvaluatedBindingIndexForChannelAccount(input.cfg, normalizeLowercaseStringOrEmpty(input.channel), normalizeAccountId(input.accountId)).byPeer.keys()].flatMap((key) => key.startsWith(prefix) ? [key.slice(7)] : []); } function normalizePeerConstraint(peer) { if (!peer) return { state: "none" }; const kind = normalizeChatType(peer.kind); const id = normalizeRouteBindingId(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: normalizeRouteBindingId(match?.guildId) || null, teamId: normalizeRouteBindingId(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) return "-"; return `${peer.kind}:${peer.id}`; } function buildResolvedRouteCacheKey(params) { return JSON.stringify([ params.channel, params.defaultAgentId, params.accountId, formatRouteCachePeer(params.peer), formatRouteCachePeer(params.parentPeer), params.guildId ?? null, params.teamId ?? null, params.memberRoleIds.toSorted(), params.dmScope, params.groupScope ]); } function matchesBindingScope(match, scope) { 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 = normalizeLowercaseStringOrEmpty(input.channel); const defaultAgentId = normalizeLowercaseStringOrEmpty(input.defaultAgentId); const accountId = normalizeAccountId(input.accountId); const peer = input.peer ? { kind: normalizeChatType(input.peer.kind) ?? input.peer.kind, id: normalizeRouteBindingId(input.peer.id) } : null; const guildId = normalizeRouteBindingId(input.guildId); const teamId = normalizeRouteBindingId(input.teamId); const memberRoleIds = input.memberRoleIds ?? []; const memberRoleIdSet = new Set(memberRoleIds); const dmScope = input.dmScope ?? input.cfg.session?.dmScope ?? "main"; const groupScope = input.groupScope ?? input.cfg.session?.groupScope ?? "per-group"; const identityLinks = input.cfg.session?.identityLinks; const shouldLogDebug = shouldLogVerbose(); const parentPeer = input.parentPeer ? { kind: normalizeChatType(input.parentPeer.kind) ?? input.parentPeer.kind, id: normalizeRouteBindingId(input.parentPeer.id) } : null; const routeCache = !shouldLogDebug && !identityLinks ? resolveRouteCacheForConfig(input.cfg) : null; const routeCacheKey = routeCache ? buildResolvedRouteCacheKey({ channel, defaultAgentId, accountId, peer, parentPeer, guildId, teamId, memberRoleIds, dmScope, groupScope }) : ""; 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 effectiveDmScope = sessionOverride?.dmScope ?? dmScope; const effectiveGroupScope = sessionOverride?.groupScope ?? groupScope; const sessionKey = buildAgentSessionKey({ agentId: resolvedAgentId, mainKey: input.cfg.session?.mainKey, channel, accountId, peer, dmScope: effectiveDmScope, groupScope: effectiveGroupScope, identityLinks }); const mainSessionKey = normalizeLowercaseStringOrEmpty(buildAgentMainSessionKey({ agentId: resolvedAgentId, mainKey: input.cfg.session?.mainKey })); const route = { agentId: resolvedAgentId, channel, accountId, dmScope: effectiveDmScope, groupScope: effectiveGroupScope, 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 }; } 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: getPeerIndexedBindings(bindingsIndex, peer) }, { matchedBy: "binding.peer.parent", enabled: Boolean(parentPeer && parentPeer.id), scopePeer: parentPeer && parentPeer.id ? parentPeer : null, candidates: getPeerIndexedBindings(bindingsIndex, parentPeer) }, { matchedBy: "binding.peer.wildcard", enabled: Boolean(peer), scopePeer: peer, candidates: bindingsIndex.byPeerWildcard }, { matchedBy: "binding.guild+roles", enabled: Boolean(guildId && memberRoleIds.length > 0), scopePeer: peer, candidates: guildId ? bindingsIndex.byGuildWithRoles.get(guildId) ?? [] : [] }, { matchedBy: "binding.guild", enabled: Boolean(guildId), scopePeer: peer, candidates: guildId ? bindingsIndex.byGuild.get(guildId) ?? [] : [] }, { matchedBy: "binding.team", enabled: Boolean(teamId), scopePeer: peer, candidates: teamId ? bindingsIndex.byTeam.get(teamId) ?? [] : [] }, { matchedBy: "binding.account", enabled: true, scopePeer: peer, candidates: bindingsIndex.byAccount }, { matchedBy: "binding.channel", enabled: true, scopePeer: peer, candidates: bindingsIndex.byChannel } ]; for (const tier of tiers) { if (!tier.enabled) continue; const matched = tier.candidates.find((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((defaultAgentId || tryResolveLegacyCompatibilityAgentId(input.cfg)) ?? resolveDefaultAgentId(input.cfg, { surface: `${channel} account ${accountId} routing`, hint: `Add a channel-wide binding for ${channel}:${accountId} or configure a sole agent.` }), "default"); } /** @internal Lists bindings selectable by at least one group/channel route under runtime precedence. */ function listEffectiveGroupRouteBindings(cfg) { const bindings = listBindings(cfg); const usedIds = /* @__PURE__ */ new Set(); for (const binding of bindings) { usedIds.add(normalizeAccountId(binding.match.accountId)); for (const value of [ binding.match.peer?.id, binding.match.guildId, binding.match.teamId ]) { const normalized = normalizeRouteBindingId(value); if (normalized) usedIds.add(normalized); } } let sentinel = "openclaw-audit-route"; while (usedIds.has(sentinel)) sentinel += "-next"; const markerForIndex = (index) => `audit-binding-${index}`; const probeCfg = { ...cfg, agents: { entries: {} }, bindings: bindings.map((binding, index) => ({ ...binding, agentId: markerForIndex(index) })) }; return bindings.filter((binding, index) => { const match = normalizeBindingMatch(binding.match); if (match.peer.state === "invalid" || (match.peer.state === "valid" || match.peer.state === "wildcard-kind") && match.peer.kind === "direct") return false; const peer = match.peer.state === "valid" ? { kind: match.peer.kind, id: match.peer.id } : match.peer.state === "wildcard-kind" ? { kind: match.peer.kind, id: sentinel } : { kind: "group", id: sentinel }; const accountId = match.accountPattern === "*" ? sentinel : match.accountPattern; return (match.roles?.map((role) => [role]) ?? [[]]).some((memberRoleIds) => resolveAgentRoute({ cfg: probeCfg, channel: binding.match.channel, defaultAgentId: DEFAULT_AGENT_ID, accountId, peer, guildId: match.guildId, teamId: match.teamId, memberRoleIds }).agentId === markerForIndex(index)); }); } //#endregion export { listExactDirectMessageBindingPeerIds as a, resolveInboundLastRouteSessionKey as c, listEffectiveGroupRouteBindings as i, peerKindMatches as l, deriveLastRoutePolicy as n, pickFirstExistingAgentId as o, listChannelAccountRouteBindings as r, resolveAgentRoute as s, buildAgentSessionKey as t };