Files
clawdbot/src/routing/resolve-route.ts
2026-01-09 12:48:42 +00:00

220 lines
5.9 KiB
TypeScript

import { resolveDefaultAgentId } from "../agents/agent-scope.js";
import type { ClawdbotConfig } from "../config/config.js";
import {
buildAgentMainSessionKey,
buildAgentPeerSessionKey,
DEFAULT_ACCOUNT_ID,
DEFAULT_MAIN_KEY,
normalizeAgentId,
} from "./session-key.js";
export type RoutePeerKind = "dm" | "group" | "channel";
export type RoutePeer = {
kind: RoutePeerKind;
id: string;
};
export type ResolveAgentRouteInput = {
cfg: ClawdbotConfig;
provider: string;
accountId?: string | null;
peer?: RoutePeer | null;
guildId?: string | null;
teamId?: string | null;
};
export type ResolvedAgentRoute = {
agentId: string;
provider: string;
accountId: string;
/** Internal session key used for persistence + concurrency. */
sessionKey: string;
/** Convenience alias for direct-chat collapse. */
mainSessionKey: string;
/** Match description for debugging/logging. */
matchedBy:
| "binding.peer"
| "binding.guild"
| "binding.team"
| "binding.account"
| "binding.provider"
| "default";
};
export { DEFAULT_ACCOUNT_ID, DEFAULT_AGENT_ID } from "./session-key.js";
function normalizeToken(value: string | undefined | null): string {
return (value ?? "").trim().toLowerCase();
}
function normalizeId(value: string | undefined | null): string {
return (value ?? "").trim();
}
function normalizeAccountId(value: string | undefined | null): string {
const trimmed = (value ?? "").trim();
return trimmed ? trimmed : DEFAULT_ACCOUNT_ID;
}
function matchesAccountId(match: string | undefined, actual: string): boolean {
const trimmed = (match ?? "").trim();
if (!trimmed) return actual === DEFAULT_ACCOUNT_ID;
if (trimmed === "*") return true;
return trimmed === actual;
}
export function buildAgentSessionKey(params: {
agentId: string;
provider: string;
peer?: RoutePeer | null;
}): string {
const provider = normalizeToken(params.provider) || "unknown";
const peer = params.peer;
return buildAgentPeerSessionKey({
agentId: params.agentId,
mainKey: DEFAULT_MAIN_KEY,
provider,
peerKind: peer?.kind ?? "dm",
peerId: peer ? normalizeId(peer.id) || "unknown" : null,
});
}
function listBindings(cfg: ClawdbotConfig) {
const bindings = cfg.bindings;
return Array.isArray(bindings) ? bindings : [];
}
function listAgents(cfg: ClawdbotConfig) {
const agents = cfg.agents?.list;
return Array.isArray(agents) ? agents : [];
}
function pickFirstExistingAgentId(
cfg: ClawdbotConfig,
agentId: string,
): string {
const normalized = normalizeAgentId(agentId);
const agents = listAgents(cfg);
if (agents.length === 0) return normalized;
if (agents.some((agent) => normalizeAgentId(agent.id) === normalized)) {
return normalized;
}
return normalizeAgentId(resolveDefaultAgentId(cfg));
}
function matchesProvider(
match: { provider?: string | undefined } | undefined,
provider: string,
): boolean {
const key = normalizeToken(match?.provider);
if (!key) return false;
return key === provider;
}
function matchesPeer(
match: { peer?: { kind?: string; id?: string } | undefined } | undefined,
peer: RoutePeer,
): boolean {
const m = match?.peer;
if (!m) return false;
const kind = normalizeToken(m.kind);
const id = normalizeId(m.id);
if (!kind || !id) return false;
return kind === peer.kind && id === peer.id;
}
function matchesGuild(
match: { guildId?: string | undefined } | undefined,
guildId: string,
): boolean {
const id = normalizeId(match?.guildId);
if (!id) return false;
return id === guildId;
}
function matchesTeam(
match: { teamId?: string | undefined } | undefined,
teamId: string,
): boolean {
const id = normalizeId(match?.teamId);
if (!id) return false;
return id === teamId;
}
export function resolveAgentRoute(
input: ResolveAgentRouteInput,
): ResolvedAgentRoute {
const provider = normalizeToken(input.provider);
const accountId = normalizeAccountId(input.accountId);
const peer = input.peer
? { kind: input.peer.kind, id: normalizeId(input.peer.id) }
: null;
const guildId = normalizeId(input.guildId);
const teamId = normalizeId(input.teamId);
const bindings = listBindings(input.cfg).filter((binding) => {
if (!binding || typeof binding !== "object") return false;
if (!matchesProvider(binding.match, provider)) return false;
return matchesAccountId(binding.match?.accountId, accountId);
});
const choose = (
agentId: string,
matchedBy: ResolvedAgentRoute["matchedBy"],
) => {
const resolvedAgentId = pickFirstExistingAgentId(input.cfg, agentId);
return {
agentId: resolvedAgentId,
provider,
accountId,
sessionKey: buildAgentSessionKey({
agentId: resolvedAgentId,
provider,
peer,
}),
mainSessionKey: buildAgentMainSessionKey({
agentId: resolvedAgentId,
mainKey: DEFAULT_MAIN_KEY,
}),
matchedBy,
};
};
if (peer) {
const peerMatch = bindings.find((b) => matchesPeer(b.match, peer));
if (peerMatch) return choose(peerMatch.agentId, "binding.peer");
}
if (guildId) {
const guildMatch = bindings.find((b) => matchesGuild(b.match, guildId));
if (guildMatch) return choose(guildMatch.agentId, "binding.guild");
}
if (teamId) {
const teamMatch = bindings.find((b) => matchesTeam(b.match, teamId));
if (teamMatch) return choose(teamMatch.agentId, "binding.team");
}
const accountMatch = bindings.find(
(b) =>
b.match?.accountId?.trim() !== "*" &&
!b.match?.peer &&
!b.match?.guildId &&
!b.match?.teamId,
);
if (accountMatch) return choose(accountMatch.agentId, "binding.account");
const anyAccountMatch = bindings.find(
(b) =>
b.match?.accountId?.trim() === "*" &&
!b.match?.peer &&
!b.match?.guildId &&
!b.match?.teamId,
);
if (anyAccountMatch)
return choose(anyAccountMatch.agentId, "binding.provider");
return choose(resolveDefaultAgentId(input.cfg), "default");
}