mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 14:24:30 +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 { logDebug } from "../../logger.js";
|
||||||
import { getChildLogger } from "../../logging.js";
|
import { getChildLogger } from "../../logging.js";
|
||||||
import { buildPairingReply } from "../../pairing/pairing-messages.js";
|
import { buildPairingReply } from "../../pairing/pairing-messages.js";
|
||||||
import { resolveAgentRoute } from "../../routing/resolve-route.js";
|
import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js";
|
||||||
import { DEFAULT_ACCOUNT_ID, resolveAgentIdFromSessionKey } from "../../routing/session-key.js";
|
|
||||||
import { fetchPluralKitMessageInfo } from "../pluralkit.js";
|
import { fetchPluralKitMessageInfo } from "../pluralkit.js";
|
||||||
import { sendMessageDiscord } from "../send.js";
|
import { sendMessageDiscord } from "../send.js";
|
||||||
import {
|
import {
|
||||||
@@ -60,6 +59,11 @@ import {
|
|||||||
resolveDiscordMessageText,
|
resolveDiscordMessageText,
|
||||||
} from "./message-utils.js";
|
} from "./message-utils.js";
|
||||||
import { resolveDiscordPreflightAudioMentionContext } from "./preflight-audio.js";
|
import { resolveDiscordPreflightAudioMentionContext } from "./preflight-audio.js";
|
||||||
|
import {
|
||||||
|
buildDiscordRoutePeer,
|
||||||
|
resolveDiscordConversationRoute,
|
||||||
|
resolveDiscordEffectiveRoute,
|
||||||
|
} from "./route-resolution.js";
|
||||||
import { resolveDiscordSenderIdentity, resolveDiscordWebhookId } from "./sender-identity.js";
|
import { resolveDiscordSenderIdentity, resolveDiscordWebhookId } from "./sender-identity.js";
|
||||||
import { resolveDiscordSystemEvent } from "./system-events.js";
|
import { resolveDiscordSystemEvent } from "./system-events.js";
|
||||||
import { isRecentlyUnboundThreadWebhookMessage } from "./thread-bindings.js";
|
import { isRecentlyUnboundThreadWebhookMessage } from "./thread-bindings.js";
|
||||||
@@ -333,18 +337,18 @@ export async function preflightDiscordMessage(
|
|||||||
? params.data.rawMember.roles.map((roleId: string) => String(roleId))
|
? params.data.rawMember.roles.map((roleId: string) => String(roleId))
|
||||||
: [];
|
: [];
|
||||||
const freshCfg = loadConfig();
|
const freshCfg = loadConfig();
|
||||||
const route = resolveAgentRoute({
|
const route = resolveDiscordConversationRoute({
|
||||||
cfg: freshCfg,
|
cfg: freshCfg,
|
||||||
channel: "discord",
|
|
||||||
accountId: params.accountId,
|
accountId: params.accountId,
|
||||||
guildId: params.data.guild_id ?? undefined,
|
guildId: params.data.guild_id ?? undefined,
|
||||||
memberRoleIds,
|
memberRoleIds,
|
||||||
peer: {
|
peer: buildDiscordRoutePeer({
|
||||||
kind: isDirectMessage ? "direct" : isGroupDm ? "group" : "channel",
|
isDirectMessage,
|
||||||
id: isDirectMessage ? author.id : messageChannelId,
|
isGroupDm,
|
||||||
},
|
directUserId: author.id,
|
||||||
// Pass parent peer for thread binding inheritance
|
conversationId: messageChannelId,
|
||||||
parentPeer: earlyThreadParentId ? { kind: "channel", id: earlyThreadParentId } : undefined,
|
}),
|
||||||
|
parentConversationId: earlyThreadParentId,
|
||||||
});
|
});
|
||||||
let threadBinding: SessionBindingRecord | undefined;
|
let threadBinding: SessionBindingRecord | undefined;
|
||||||
threadBinding =
|
threadBinding =
|
||||||
@@ -381,15 +385,13 @@ export async function preflightDiscordMessage(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const boundSessionKey = threadBinding?.targetSessionKey?.trim();
|
const boundSessionKey = threadBinding?.targetSessionKey?.trim();
|
||||||
const boundAgentId = boundSessionKey ? resolveAgentIdFromSessionKey(boundSessionKey) : undefined;
|
const effectiveRoute = resolveDiscordEffectiveRoute({
|
||||||
const effectiveRoute = boundSessionKey
|
route,
|
||||||
? {
|
boundSessionKey,
|
||||||
...route,
|
configuredRoute,
|
||||||
sessionKey: boundSessionKey,
|
matchedBy: "binding.channel",
|
||||||
agentId: boundAgentId ?? route.agentId,
|
});
|
||||||
matchedBy: "binding.channel" as const,
|
const boundAgentId = boundSessionKey ? effectiveRoute.agentId : undefined;
|
||||||
}
|
|
||||||
: (configuredRoute?.route ?? route);
|
|
||||||
const isBoundThreadSession = Boolean(boundSessionKey && earlyThreadChannel);
|
const isBoundThreadSession = Boolean(boundSessionKey && earlyThreadChannel);
|
||||||
if (
|
if (
|
||||||
isBoundThreadBotSystemMessage({
|
isBoundThreadBotSystemMessage({
|
||||||
|
|||||||
@@ -52,8 +52,7 @@ import { createSubsystemLogger } from "../../logging/subsystem.js";
|
|||||||
import { getAgentScopedMediaLocalRoots } from "../../media/local-roots.js";
|
import { getAgentScopedMediaLocalRoots } from "../../media/local-roots.js";
|
||||||
import { buildPairingReply } from "../../pairing/pairing-messages.js";
|
import { buildPairingReply } from "../../pairing/pairing-messages.js";
|
||||||
import { executePluginCommand, matchPluginCommand } from "../../plugins/commands.js";
|
import { executePluginCommand, matchPluginCommand } from "../../plugins/commands.js";
|
||||||
import { resolveAgentRoute } from "../../routing/resolve-route.js";
|
import type { ResolvedAgentRoute } from "../../routing/resolve-route.js";
|
||||||
import { resolveAgentIdFromSessionKey } from "../../routing/session-key.js";
|
|
||||||
import { buildUntrustedChannelMetadata } from "../../security/channel-metadata.js";
|
import { buildUntrustedChannelMetadata } from "../../security/channel-metadata.js";
|
||||||
import { chunkItems } from "../../utils/chunk-items.js";
|
import { chunkItems } from "../../utils/chunk-items.js";
|
||||||
import { withTimeout } from "../../utils/with-timeout.js";
|
import { withTimeout } from "../../utils/with-timeout.js";
|
||||||
@@ -86,6 +85,11 @@ import {
|
|||||||
toDiscordModelPickerMessagePayload,
|
toDiscordModelPickerMessagePayload,
|
||||||
type DiscordModelPickerCommandContext,
|
type DiscordModelPickerCommandContext,
|
||||||
} from "./model-picker.js";
|
} from "./model-picker.js";
|
||||||
|
import {
|
||||||
|
buildDiscordRoutePeer,
|
||||||
|
resolveDiscordConversationRoute,
|
||||||
|
resolveDiscordEffectiveRoute,
|
||||||
|
} from "./route-resolution.js";
|
||||||
import { resolveDiscordSenderIdentity } from "./sender-identity.js";
|
import { resolveDiscordSenderIdentity } from "./sender-identity.js";
|
||||||
import type { ThreadBindingManager } from "./thread-bindings.js";
|
import type { ThreadBindingManager } from "./thread-bindings.js";
|
||||||
import { resolveDiscordThreadParentInfo } from "./threading.js";
|
import { resolveDiscordThreadParentInfo } from "./threading.js";
|
||||||
@@ -448,36 +452,32 @@ async function resolveDiscordModelPickerRoute(params: {
|
|||||||
threadParentId = parentInfo.id;
|
threadParentId = parentInfo.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const route = resolveAgentRoute({
|
const route = resolveDiscordConversationRoute({
|
||||||
cfg,
|
cfg,
|
||||||
channel: "discord",
|
|
||||||
accountId,
|
accountId,
|
||||||
guildId: interaction.guild?.id ?? undefined,
|
guildId: interaction.guild?.id ?? undefined,
|
||||||
memberRoleIds,
|
memberRoleIds,
|
||||||
peer: {
|
peer: buildDiscordRoutePeer({
|
||||||
kind: isDirectMessage ? "direct" : isGroupDm ? "group" : "channel",
|
isDirectMessage,
|
||||||
id: isDirectMessage ? (interaction.user?.id ?? rawChannelId) : rawChannelId,
|
isGroupDm,
|
||||||
},
|
directUserId: interaction.user?.id ?? rawChannelId,
|
||||||
parentPeer: threadParentId ? { kind: "channel", id: threadParentId } : undefined,
|
conversationId: rawChannelId,
|
||||||
|
}),
|
||||||
|
parentConversationId: threadParentId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const threadBinding = isThreadChannel
|
const threadBinding = isThreadChannel
|
||||||
? params.threadBindings.getByThreadId(rawChannelId)
|
? params.threadBindings.getByThreadId(rawChannelId)
|
||||||
: undefined;
|
: undefined;
|
||||||
const boundSessionKey = threadBinding?.targetSessionKey?.trim();
|
return resolveDiscordEffectiveRoute({
|
||||||
const boundAgentId = boundSessionKey ? resolveAgentIdFromSessionKey(boundSessionKey) : undefined;
|
route,
|
||||||
return boundSessionKey
|
boundSessionKey: threadBinding?.targetSessionKey,
|
||||||
? {
|
});
|
||||||
...route,
|
|
||||||
sessionKey: boundSessionKey,
|
|
||||||
agentId: boundAgentId ?? route.agentId,
|
|
||||||
}
|
|
||||||
: route;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveDiscordModelPickerCurrentModel(params: {
|
function resolveDiscordModelPickerCurrentModel(params: {
|
||||||
cfg: ReturnType<typeof loadConfig>;
|
cfg: ReturnType<typeof loadConfig>;
|
||||||
route: ReturnType<typeof resolveAgentRoute>;
|
route: ResolvedAgentRoute;
|
||||||
data: Awaited<ReturnType<typeof loadDiscordModelPickerData>>;
|
data: Awaited<ReturnType<typeof loadDiscordModelPickerData>>;
|
||||||
}): string {
|
}): string {
|
||||||
const fallback = buildDiscordModelPickerCurrentModel(
|
const fallback = buildDiscordModelPickerCurrentModel(
|
||||||
@@ -1606,17 +1606,18 @@ async function dispatchDiscordCommandInteraction(params: {
|
|||||||
const isGuild = Boolean(interaction.guild);
|
const isGuild = Boolean(interaction.guild);
|
||||||
const channelId = rawChannelId || "unknown";
|
const channelId = rawChannelId || "unknown";
|
||||||
const interactionId = interaction.rawData.id;
|
const interactionId = interaction.rawData.id;
|
||||||
const route = resolveAgentRoute({
|
const route = resolveDiscordConversationRoute({
|
||||||
cfg,
|
cfg,
|
||||||
channel: "discord",
|
|
||||||
accountId,
|
accountId,
|
||||||
guildId: interaction.guild?.id ?? undefined,
|
guildId: interaction.guild?.id ?? undefined,
|
||||||
memberRoleIds,
|
memberRoleIds,
|
||||||
peer: {
|
peer: buildDiscordRoutePeer({
|
||||||
kind: isDirectMessage ? "direct" : isGroupDm ? "group" : "channel",
|
isDirectMessage,
|
||||||
id: isDirectMessage ? user.id : channelId,
|
isGroupDm,
|
||||||
},
|
directUserId: user.id,
|
||||||
parentPeer: threadParentId ? { kind: "channel", id: threadParentId } : undefined,
|
conversationId: channelId,
|
||||||
|
}),
|
||||||
|
parentConversationId: threadParentId,
|
||||||
});
|
});
|
||||||
const threadBinding = isThreadChannel ? threadBindings.getByThreadId(rawChannelId) : undefined;
|
const threadBinding = isThreadChannel ? threadBindings.getByThreadId(rawChannelId) : undefined;
|
||||||
const configuredRoute =
|
const configuredRoute =
|
||||||
@@ -1646,15 +1647,12 @@ async function dispatchDiscordCommandInteraction(params: {
|
|||||||
}
|
}
|
||||||
const configuredBoundSessionKey = configuredRoute?.boundSessionKey?.trim() || undefined;
|
const configuredBoundSessionKey = configuredRoute?.boundSessionKey?.trim() || undefined;
|
||||||
const boundSessionKey = threadBinding?.targetSessionKey?.trim() || configuredBoundSessionKey;
|
const boundSessionKey = threadBinding?.targetSessionKey?.trim() || configuredBoundSessionKey;
|
||||||
const boundAgentId = boundSessionKey ? resolveAgentIdFromSessionKey(boundSessionKey) : undefined;
|
const effectiveRoute = resolveDiscordEffectiveRoute({
|
||||||
const effectiveRoute = boundSessionKey
|
route,
|
||||||
? {
|
boundSessionKey,
|
||||||
...route,
|
configuredRoute,
|
||||||
sessionKey: boundSessionKey,
|
matchedBy: configuredBinding ? "binding.channel" : undefined,
|
||||||
agentId: boundAgentId ?? route.agentId,
|
});
|
||||||
...(configuredBinding ? { matchedBy: "binding.channel" as const } : {}),
|
|
||||||
}
|
|
||||||
: (configuredRoute?.route ?? route);
|
|
||||||
const conversationLabel = isDirectMessage ? (user.globalName ?? user.username) : channelId;
|
const conversationLabel = isDirectMessage ? (user.globalName ?? user.username) : channelId;
|
||||||
const ownerAllowFrom = resolveDiscordOwnerAllowFrom({
|
const ownerAllowFrom = resolveDiscordOwnerAllowFrom({
|
||||||
channelConfig,
|
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