refactor(discord): extract route resolution helpers

This commit is contained in:
Peter Steinberger
2026-03-08 01:10:31 +00:00
parent 269cc22b61
commit c1d07b09ce
4 changed files with 221 additions and 54 deletions

View File

@@ -29,8 +29,7 @@ import { enqueueSystemEvent } from "../../infra/system-events.js";
import { logDebug } from "../../logger.js";
import { getChildLogger } from "../../logging.js";
import { buildPairingReply } from "../../pairing/pairing-messages.js";
import { resolveAgentRoute } from "../../routing/resolve-route.js";
import { DEFAULT_ACCOUNT_ID, resolveAgentIdFromSessionKey } from "../../routing/session-key.js";
import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js";
import { fetchPluralKitMessageInfo } from "../pluralkit.js";
import { sendMessageDiscord } from "../send.js";
import {
@@ -60,6 +59,11 @@ import {
resolveDiscordMessageText,
} from "./message-utils.js";
import { resolveDiscordPreflightAudioMentionContext } from "./preflight-audio.js";
import {
buildDiscordRoutePeer,
resolveDiscordConversationRoute,
resolveDiscordEffectiveRoute,
} from "./route-resolution.js";
import { resolveDiscordSenderIdentity, resolveDiscordWebhookId } from "./sender-identity.js";
import { resolveDiscordSystemEvent } from "./system-events.js";
import { isRecentlyUnboundThreadWebhookMessage } from "./thread-bindings.js";
@@ -333,18 +337,18 @@ export async function preflightDiscordMessage(
? params.data.rawMember.roles.map((roleId: string) => String(roleId))
: [];
const freshCfg = loadConfig();
const route = resolveAgentRoute({
const route = resolveDiscordConversationRoute({
cfg: freshCfg,
channel: "discord",
accountId: params.accountId,
guildId: params.data.guild_id ?? undefined,
memberRoleIds,
peer: {
kind: isDirectMessage ? "direct" : isGroupDm ? "group" : "channel",
id: isDirectMessage ? author.id : messageChannelId,
},
// Pass parent peer for thread binding inheritance
parentPeer: earlyThreadParentId ? { kind: "channel", id: earlyThreadParentId } : undefined,
peer: buildDiscordRoutePeer({
isDirectMessage,
isGroupDm,
directUserId: author.id,
conversationId: messageChannelId,
}),
parentConversationId: earlyThreadParentId,
});
let threadBinding: SessionBindingRecord | undefined;
threadBinding =
@@ -381,15 +385,13 @@ export async function preflightDiscordMessage(
return null;
}
const boundSessionKey = threadBinding?.targetSessionKey?.trim();
const boundAgentId = boundSessionKey ? resolveAgentIdFromSessionKey(boundSessionKey) : undefined;
const effectiveRoute = boundSessionKey
? {
...route,
sessionKey: boundSessionKey,
agentId: boundAgentId ?? route.agentId,
matchedBy: "binding.channel" as const,
}
: (configuredRoute?.route ?? route);
const effectiveRoute = resolveDiscordEffectiveRoute({
route,
boundSessionKey,
configuredRoute,
matchedBy: "binding.channel",
});
const boundAgentId = boundSessionKey ? effectiveRoute.agentId : undefined;
const isBoundThreadSession = Boolean(boundSessionKey && earlyThreadChannel);
if (
isBoundThreadBotSystemMessage({

View File

@@ -52,8 +52,7 @@ import { createSubsystemLogger } from "../../logging/subsystem.js";
import { getAgentScopedMediaLocalRoots } from "../../media/local-roots.js";
import { buildPairingReply } from "../../pairing/pairing-messages.js";
import { executePluginCommand, matchPluginCommand } from "../../plugins/commands.js";
import { resolveAgentRoute } from "../../routing/resolve-route.js";
import { resolveAgentIdFromSessionKey } from "../../routing/session-key.js";
import type { ResolvedAgentRoute } from "../../routing/resolve-route.js";
import { buildUntrustedChannelMetadata } from "../../security/channel-metadata.js";
import { chunkItems } from "../../utils/chunk-items.js";
import { withTimeout } from "../../utils/with-timeout.js";
@@ -86,6 +85,11 @@ import {
toDiscordModelPickerMessagePayload,
type DiscordModelPickerCommandContext,
} from "./model-picker.js";
import {
buildDiscordRoutePeer,
resolveDiscordConversationRoute,
resolveDiscordEffectiveRoute,
} from "./route-resolution.js";
import { resolveDiscordSenderIdentity } from "./sender-identity.js";
import type { ThreadBindingManager } from "./thread-bindings.js";
import { resolveDiscordThreadParentInfo } from "./threading.js";
@@ -448,36 +452,32 @@ async function resolveDiscordModelPickerRoute(params: {
threadParentId = parentInfo.id;
}
const route = resolveAgentRoute({
const route = resolveDiscordConversationRoute({
cfg,
channel: "discord",
accountId,
guildId: interaction.guild?.id ?? undefined,
memberRoleIds,
peer: {
kind: isDirectMessage ? "direct" : isGroupDm ? "group" : "channel",
id: isDirectMessage ? (interaction.user?.id ?? rawChannelId) : rawChannelId,
},
parentPeer: threadParentId ? { kind: "channel", id: threadParentId } : undefined,
peer: buildDiscordRoutePeer({
isDirectMessage,
isGroupDm,
directUserId: interaction.user?.id ?? rawChannelId,
conversationId: rawChannelId,
}),
parentConversationId: threadParentId,
});
const threadBinding = isThreadChannel
? params.threadBindings.getByThreadId(rawChannelId)
: undefined;
const boundSessionKey = threadBinding?.targetSessionKey?.trim();
const boundAgentId = boundSessionKey ? resolveAgentIdFromSessionKey(boundSessionKey) : undefined;
return boundSessionKey
? {
...route,
sessionKey: boundSessionKey,
agentId: boundAgentId ?? route.agentId,
}
: route;
return resolveDiscordEffectiveRoute({
route,
boundSessionKey: threadBinding?.targetSessionKey,
});
}
function resolveDiscordModelPickerCurrentModel(params: {
cfg: ReturnType<typeof loadConfig>;
route: ReturnType<typeof resolveAgentRoute>;
route: ResolvedAgentRoute;
data: Awaited<ReturnType<typeof loadDiscordModelPickerData>>;
}): string {
const fallback = buildDiscordModelPickerCurrentModel(
@@ -1606,17 +1606,18 @@ async function dispatchDiscordCommandInteraction(params: {
const isGuild = Boolean(interaction.guild);
const channelId = rawChannelId || "unknown";
const interactionId = interaction.rawData.id;
const route = resolveAgentRoute({
const route = resolveDiscordConversationRoute({
cfg,
channel: "discord",
accountId,
guildId: interaction.guild?.id ?? undefined,
memberRoleIds,
peer: {
kind: isDirectMessage ? "direct" : isGroupDm ? "group" : "channel",
id: isDirectMessage ? user.id : channelId,
},
parentPeer: threadParentId ? { kind: "channel", id: threadParentId } : undefined,
peer: buildDiscordRoutePeer({
isDirectMessage,
isGroupDm,
directUserId: user.id,
conversationId: channelId,
}),
parentConversationId: threadParentId,
});
const threadBinding = isThreadChannel ? threadBindings.getByThreadId(rawChannelId) : undefined;
const configuredRoute =
@@ -1646,15 +1647,12 @@ async function dispatchDiscordCommandInteraction(params: {
}
const configuredBoundSessionKey = configuredRoute?.boundSessionKey?.trim() || undefined;
const boundSessionKey = threadBinding?.targetSessionKey?.trim() || configuredBoundSessionKey;
const boundAgentId = boundSessionKey ? resolveAgentIdFromSessionKey(boundSessionKey) : undefined;
const effectiveRoute = boundSessionKey
? {
...route,
sessionKey: boundSessionKey,
agentId: boundAgentId ?? route.agentId,
...(configuredBinding ? { matchedBy: "binding.channel" as const } : {}),
}
: (configuredRoute?.route ?? route);
const effectiveRoute = resolveDiscordEffectiveRoute({
route,
boundSessionKey,
configuredRoute,
matchedBy: configuredBinding ? "binding.channel" : undefined,
});
const conversationLabel = isDirectMessage ? (user.globalName ?? user.username) : channelId;
const ownerAllowFrom = resolveDiscordOwnerAllowFrom({
channelConfig,

View File

@@ -0,0 +1,107 @@
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import type { ResolvedAgentRoute } from "../../routing/resolve-route.js";
import {
buildDiscordRoutePeer,
resolveDiscordConversationRoute,
resolveDiscordEffectiveRoute,
} from "./route-resolution.js";
describe("discord route resolution helpers", () => {
it("builds a direct peer from DM metadata", () => {
expect(
buildDiscordRoutePeer({
isDirectMessage: true,
isGroupDm: false,
directUserId: "user-1",
conversationId: "channel-1",
}),
).toEqual({
kind: "direct",
id: "user-1",
});
});
it("resolves bound session keys on top of the routed session", () => {
const route: ResolvedAgentRoute = {
agentId: "main",
channel: "discord",
accountId: "default",
sessionKey: "agent:main:discord:channel:c1",
mainSessionKey: "agent:main:main",
matchedBy: "default",
};
expect(
resolveDiscordEffectiveRoute({
route,
boundSessionKey: "agent:worker:discord:channel:c1",
matchedBy: "binding.channel",
}),
).toEqual({
...route,
agentId: "worker",
sessionKey: "agent:worker:discord:channel:c1",
matchedBy: "binding.channel",
});
});
it("falls back to configured route when no bound session exists", () => {
const route: ResolvedAgentRoute = {
agentId: "main",
channel: "discord",
accountId: "default",
sessionKey: "agent:main:discord:channel:c1",
mainSessionKey: "agent:main:main",
matchedBy: "default",
};
const configuredRoute = {
route: {
...route,
agentId: "worker",
sessionKey: "agent:worker:discord:channel:c1",
mainSessionKey: "agent:worker:main",
matchedBy: "binding.peer" as const,
},
};
expect(
resolveDiscordEffectiveRoute({
route,
configuredRoute,
}),
).toEqual(configuredRoute.route);
});
it("resolves the same route shape as the inline Discord route inputs", () => {
const cfg: OpenClawConfig = {
agents: {
list: [{ id: "worker" }],
},
bindings: [
{
agentId: "worker",
match: {
channel: "discord",
accountId: "default",
peer: { kind: "channel", id: "c1" },
},
},
],
};
expect(
resolveDiscordConversationRoute({
cfg,
accountId: "default",
guildId: "g1",
memberRoleIds: [],
peer: { kind: "channel", id: "c1" },
}),
).toMatchObject({
agentId: "worker",
sessionKey: "agent:worker:discord:channel:c1",
matchedBy: "binding.peer",
});
});
});

View File

@@ -0,0 +1,60 @@
import type { OpenClawConfig } from "../../config/config.js";
import {
resolveAgentRoute,
type ResolvedAgentRoute,
type RoutePeer,
} from "../../routing/resolve-route.js";
import { resolveAgentIdFromSessionKey } from "../../routing/session-key.js";
export function buildDiscordRoutePeer(params: {
isDirectMessage: boolean;
isGroupDm: boolean;
directUserId?: string | null;
conversationId: string;
}): RoutePeer {
return {
kind: params.isDirectMessage ? "direct" : params.isGroupDm ? "group" : "channel",
id: params.isDirectMessage
? params.directUserId?.trim() || params.conversationId
: params.conversationId,
};
}
export function resolveDiscordConversationRoute(params: {
cfg: OpenClawConfig;
accountId?: string | null;
guildId?: string | null;
memberRoleIds?: string[];
peer: RoutePeer;
parentConversationId?: string | null;
}): ResolvedAgentRoute {
return resolveAgentRoute({
cfg: params.cfg,
channel: "discord",
accountId: params.accountId,
guildId: params.guildId ?? undefined,
memberRoleIds: params.memberRoleIds,
peer: params.peer,
parentPeer: params.parentConversationId
? { kind: "channel", id: params.parentConversationId }
: undefined,
});
}
export function resolveDiscordEffectiveRoute(params: {
route: ResolvedAgentRoute;
boundSessionKey?: string | null;
configuredRoute?: { route: ResolvedAgentRoute } | null;
matchedBy?: ResolvedAgentRoute["matchedBy"];
}): ResolvedAgentRoute {
const boundSessionKey = params.boundSessionKey?.trim();
if (!boundSessionKey) {
return params.configuredRoute?.route ?? params.route;
}
return {
...params.route,
sessionKey: boundSessionKey,
agentId: resolveAgentIdFromSessionKey(boundSessionKey),
...(params.matchedBy ? { matchedBy: params.matchedBy } : {}),
};
}