Discord: route bound DMs to plugins

This commit is contained in:
huntharo 2026-03-14 22:24:21 -04:00 committed by Vincent Koc
parent d0731c35b2
commit eb4e96573a
3 changed files with 119 additions and 1 deletions

View File

@ -2110,6 +2110,73 @@ describe("dispatchReplyFromConfig", () => {
expect(replyResolver).not.toHaveBeenCalled();
});
it("routes plugin-owned Discord DM bindings to the owning plugin before generic inbound claim broadcast", async () => {
setNoAbort();
hookMocks.runner.hasHooks.mockImplementation(
((hookName?: string) =>
hookName === "inbound_claim" || hookName === "message_received") as () => boolean,
);
sessionBindingMocks.resolveByConversation.mockReturnValue({
bindingId: "binding-dm-1",
targetSessionKey: "plugin-binding:codex:dm123",
targetKind: "session",
conversation: {
channel: "discord",
accountId: "default",
conversationId: "user:1177378744822943744",
},
status: "active",
boundAt: 1710000000000,
metadata: {
pluginBindingOwner: "plugin",
pluginId: "openclaw-codex-app-server",
pluginRoot: "/Users/huntharo/github/openclaw-app-server",
},
} satisfies SessionBindingRecord);
const cfg = emptyConfig;
const dispatcher = createDispatcher();
const ctx = buildTestCtx({
Provider: "discord",
Surface: "discord",
OriginatingChannel: "discord",
From: "discord:1177378744822943744",
OriginatingTo: "channel:1480574946919846079",
To: "channel:1480574946919846079",
AccountId: "default",
SenderId: "user-9",
SenderUsername: "ada",
CommandAuthorized: true,
WasMentioned: false,
CommandBody: "who are you",
RawBody: "who are you",
Body: "who are you",
MessageSid: "msg-claim-plugin-dm-1",
SessionKey: "agent:main:discord:user:1177378744822943744",
});
const replyResolver = vi.fn(async () => ({ text: "should not run" }) satisfies ReplyPayload);
const result = await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver });
expect(result).toEqual({ queuedFinal: false, counts: { tool: 0, block: 0, final: 0 } });
expect(sessionBindingMocks.touch).toHaveBeenCalledWith("binding-dm-1");
expect(hookMocks.runner.runInboundClaimForPlugin).toHaveBeenCalledWith(
"openclaw-codex-app-server",
expect.objectContaining({
channel: "discord",
accountId: "default",
conversationId: "user:1177378744822943744",
content: "who are you",
}),
expect.objectContaining({
channelId: "discord",
accountId: "default",
conversationId: "user:1177378744822943744",
}),
);
expect(hookMocks.runner.runInboundClaim).not.toHaveBeenCalled();
expect(replyResolver).not.toHaveBeenCalled();
});
it("marks diagnostics skipped for duplicate inbound messages", async () => {
setNoAbort();
const cfg = { diagnostics: { enabled: true } } as OpenClawConfig;

View File

@ -116,7 +116,31 @@ describe("message hook mappers", () => {
expect(toPluginInboundClaimContext(canonical)).toEqual({
channelId: "discord",
accountId: "acc-1",
conversationId: "123456789012345678",
conversationId: "channel:123456789012345678",
parentConversationId: undefined,
senderId: "sender-1",
messageId: "msg-1",
});
});
it("normalizes Discord DM targets for inbound claim contexts", () => {
const canonical = deriveInboundMessageHookContext(
makeInboundCtx({
Provider: "discord",
Surface: "discord",
OriginatingChannel: "discord",
From: "discord:1177378744822943744",
To: "channel:1480574946919846079",
OriginatingTo: "channel:1480574946919846079",
GroupChannel: undefined,
GroupSubject: undefined,
}),
);
expect(toPluginInboundClaimContext(canonical)).toEqual({
channelId: "discord",
accountId: "acc-1",
conversationId: "user:1177378744822943744",
parentConversationId: undefined,
senderId: "sender-1",
messageId: "msg-1",

View File

@ -179,6 +179,33 @@ function deriveParentConversationId(
}
function deriveConversationId(canonical: CanonicalInboundMessageHookContext): string | undefined {
if (canonical.channelId === "discord") {
const rawTarget = canonical.to ?? canonical.originatingTo ?? canonical.conversationId;
const rawSender = canonical.from;
const senderUserId = rawSender?.startsWith("discord:user:")
? rawSender.slice("discord:user:".length)
: rawSender?.startsWith("discord:")
? rawSender.slice("discord:".length)
: undefined;
if (!canonical.isGroup && senderUserId) {
return `user:${senderUserId}`;
}
if (!rawTarget) {
return undefined;
}
if (rawTarget.startsWith("discord:channel:")) {
return `channel:${rawTarget.slice("discord:channel:".length)}`;
}
if (rawTarget.startsWith("discord:user:")) {
return `user:${rawTarget.slice("discord:user:".length)}`;
}
if (rawTarget.startsWith("discord:")) {
return `user:${rawTarget.slice("discord:".length)}`;
}
if (rawTarget.startsWith("channel:") || rawTarget.startsWith("user:")) {
return rawTarget;
}
}
const baseConversationId = stripChannelPrefix(
canonical.to ?? canonical.originatingTo ?? canonical.conversationId,
canonical.channelId,