220 lines
5.9 KiB
TypeScript
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");
|
|
}
|