mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 21:58:26 +00:00
refactor(discord): extract route resolution helpers
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
107
src/discord/monitor/route-resolution.test.ts
Normal file
107
src/discord/monitor/route-resolution.test.ts
Normal 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",
|
||||
});
|
||||
});
|
||||
});
|
||||
60
src/discord/monitor/route-resolution.ts
Normal file
60
src/discord/monitor/route-resolution.ts
Normal 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 } : {}),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user