mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:50:43 +00:00
fix: canonicalize discord pluralkit inbound ids
This commit is contained in:
@@ -41,6 +41,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Discord: allow explicitly configured ack reactions in tool-only guild channels while keeping automatic lifecycle/status reactions suppressed. Fixes #74922. Thanks @samvilian and @BlueBirdBack.
|
||||
- Discord: preserve attachment and sticker filenames when saving inbound media, so agents can see human-readable file names instead of only UUID-based paths. Fixes #59744. Thanks @xela92 and @rockcent.
|
||||
- Discord: preserve non-ASCII channel names in session display labels while keeping allowlist matching on the existing ASCII slug contract. Thanks @swjeong9.
|
||||
- Discord/PluralKit: canonicalize proxied webhook turns to the original Discord message id for inbound dedupe, while preserving the proxy message id for reply routing. Thanks @acgh213.
|
||||
- Gateway/diagnostics: include a bounded redacted startup error message in stability bundles, so crash-loop reports identify the failing plugin or contract without exposing secrets. Refs #75797. Thanks @ymebosma.
|
||||
- Gateway/pricing: abort in-flight model pricing catalog fetches when Gateway shutdown stops the refresh loop, and avoid post-stop cache writes or refresh timers. Fixes #72208. Thanks @rzcq.
|
||||
- Codex/app-server: make startup retry cleanup ownership-aware so concurrent Codex lanes cannot close another lane's freshly restarted shared app-server client. Thanks @vincentkoc.
|
||||
|
||||
@@ -52,6 +52,7 @@ export async function buildDiscordMessageProcessContext(params: {
|
||||
message,
|
||||
author,
|
||||
sender,
|
||||
canonicalMessageId,
|
||||
data,
|
||||
client,
|
||||
channelInfo,
|
||||
@@ -323,7 +324,10 @@ export async function buildDiscordMessageProcessContext(params: {
|
||||
Provider: "discord" as const,
|
||||
Surface: "discord" as const,
|
||||
WasMentioned: ctx.effectiveWasMentioned,
|
||||
MessageSid: message.id,
|
||||
MessageSid: canonicalMessageId ?? message.id,
|
||||
...(canonicalMessageId && canonicalMessageId !== message.id
|
||||
? { MessageSidFull: message.id }
|
||||
: {}),
|
||||
ReplyToId: filteredReplyContext?.id,
|
||||
ReplyToBody: filteredReplyContext?.body,
|
||||
ReplyToSender: filteredReplyContext?.sender,
|
||||
|
||||
@@ -4,13 +4,12 @@ import type { DiscordMessageEvent } from "./message-handler.preflight.types.js";
|
||||
|
||||
export async function resolveDiscordPreflightPluralKitInfo(params: {
|
||||
message: DiscordMessageEvent["message"];
|
||||
webhookId?: string | null;
|
||||
config?: NonNullable<
|
||||
NonNullable<import("openclaw/plugin-sdk/config-types").OpenClawConfig["channels"]>["discord"]
|
||||
>["pluralkit"];
|
||||
abortSignal?: AbortSignal;
|
||||
}): Promise<Awaited<ReturnType<typeof import("../pluralkit.js").fetchPluralKitMessageInfo>>> {
|
||||
if (!params.config?.enabled || params.webhookId) {
|
||||
if (!params.config?.enabled) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -3,9 +3,13 @@ import { ChannelType } from "../internal/discord.js";
|
||||
import { createPartialDiscordChannelWithThrowingGetters } from "../test-support/partial-channel.js";
|
||||
|
||||
const transcribeFirstAudioMock = vi.hoisted(() => vi.fn());
|
||||
const fetchPluralKitMessageInfoMock = vi.hoisted(() => vi.fn());
|
||||
const resolveDiscordDmCommandAccessMock = vi.hoisted(() => vi.fn());
|
||||
const handleDiscordDmCommandDecisionMock = vi.hoisted(() => vi.fn(async () => {}));
|
||||
|
||||
vi.mock("../pluralkit.js", () => ({
|
||||
fetchPluralKitMessageInfo: (...args: unknown[]) => fetchPluralKitMessageInfoMock(...args),
|
||||
}));
|
||||
vi.mock("./preflight-audio.runtime.js", () => ({
|
||||
transcribeFirstAudio: transcribeFirstAudioMock,
|
||||
}));
|
||||
@@ -45,6 +49,10 @@ beforeAll(async () => {
|
||||
await import("./thread-bindings.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fetchPluralKitMessageInfoMock.mockReset();
|
||||
});
|
||||
|
||||
function createThreadBinding(
|
||||
overrides?: Partial<import("openclaw/plugin-sdk/conversation-runtime").SessionBindingRecord>,
|
||||
) {
|
||||
@@ -717,6 +725,77 @@ describe("preflightDiscordMessage", () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("canonicalizes PluralKit webhook messages to the original Discord message id", async () => {
|
||||
fetchPluralKitMessageInfoMock.mockResolvedValue({
|
||||
id: "proxy-456",
|
||||
original: "orig-123",
|
||||
member: { id: "member-1", name: "Echo" },
|
||||
system: { id: "system-1", name: "System" },
|
||||
});
|
||||
|
||||
const result = await runGuildPreflight({
|
||||
channelId: "c1",
|
||||
guildId: "g1",
|
||||
message: createDiscordMessage({
|
||||
id: "proxy-456",
|
||||
channelId: "c1",
|
||||
content: "<@openclaw-bot> hello",
|
||||
webhookId: "pluralkit-webhook-1",
|
||||
author: {
|
||||
id: "webhook-author",
|
||||
bot: true,
|
||||
username: "PluralKit",
|
||||
},
|
||||
mentionedUsers: [{ id: "openclaw-bot" }],
|
||||
}),
|
||||
discordConfig: {
|
||||
pluralkit: { enabled: true },
|
||||
} as DiscordConfig,
|
||||
});
|
||||
|
||||
expect(fetchPluralKitMessageInfoMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
messageId: "proxy-456",
|
||||
config: expect.objectContaining({ enabled: true }),
|
||||
}),
|
||||
);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.sender.isPluralKit).toBe(true);
|
||||
expect(result?.canonicalMessageId).toBe("orig-123");
|
||||
});
|
||||
|
||||
it("skips PluralKit lookup for bound-thread webhook echoes", async () => {
|
||||
const threadBinding = createThreadBinding({
|
||||
targetKind: "session",
|
||||
targetSessionKey: "agent:main:acp:discord-thread-1",
|
||||
});
|
||||
const threadId = "thread-webhook-pk-echo-1";
|
||||
const parentId = "channel-parent-webhook-pk-echo-1";
|
||||
|
||||
const result = await runThreadBoundPreflight({
|
||||
threadId,
|
||||
parentId,
|
||||
threadBinding,
|
||||
message: createDiscordMessage({
|
||||
id: "m-webhook-pk-echo-1",
|
||||
channelId: threadId,
|
||||
content: "proxied user message",
|
||||
webhookId: "pluralkit-webhook-1",
|
||||
author: {
|
||||
id: "relay-bot-1",
|
||||
bot: true,
|
||||
username: "Proxy",
|
||||
},
|
||||
}),
|
||||
discordConfig: {
|
||||
pluralkit: { enabled: true },
|
||||
} as DiscordConfig,
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(fetchPluralKitMessageInfoMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("bypasses mention gating in bound threads for allowed bot senders", async () => {
|
||||
const threadBinding = createThreadBinding();
|
||||
const threadId = "thread-bot-focus";
|
||||
|
||||
@@ -121,28 +121,6 @@ export async function preflightDiscordMessage(
|
||||
|
||||
const pluralkitConfig = params.discordConfig?.pluralkit;
|
||||
const webhookId = resolveDiscordWebhookId(message);
|
||||
const pluralkitInfo = await resolveDiscordPreflightPluralKitInfo({
|
||||
message,
|
||||
webhookId,
|
||||
config: pluralkitConfig,
|
||||
abortSignal: params.abortSignal,
|
||||
});
|
||||
if (isPreflightAborted(params.abortSignal)) {
|
||||
return null;
|
||||
}
|
||||
const sender = resolveDiscordSenderIdentity({
|
||||
author,
|
||||
member: params.data.member,
|
||||
pluralkitInfo,
|
||||
});
|
||||
|
||||
if (author.bot) {
|
||||
if (allowBotsMode === "off" && !sender.isPluralKit) {
|
||||
logVerbose("discord: drop bot message (allowBots=false)");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const isGuildMessage = Boolean(params.data.guild_id);
|
||||
const channelInfo = await resolveDiscordChannelInfo(params.client, messageChannelId);
|
||||
if (isPreflightAborted(params.abortSignal)) {
|
||||
@@ -189,6 +167,26 @@ export async function preflightDiscordMessage(
|
||||
logVerbose(`discord: drop bound-thread bot system message ${message.id}`);
|
||||
return null;
|
||||
}
|
||||
const pluralkitInfo = await resolveDiscordPreflightPluralKitInfo({
|
||||
message,
|
||||
config: pluralkitConfig,
|
||||
abortSignal: params.abortSignal,
|
||||
});
|
||||
if (isPreflightAborted(params.abortSignal)) {
|
||||
return null;
|
||||
}
|
||||
const sender = resolveDiscordSenderIdentity({
|
||||
author,
|
||||
member: params.data.member,
|
||||
pluralkitInfo,
|
||||
});
|
||||
|
||||
if (author.bot) {
|
||||
if (allowBotsMode === "off" && !sender.isPluralKit) {
|
||||
logVerbose("discord: drop bot message (allowBots=false)");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const data = message === params.data.message ? params.data : { ...params.data, message };
|
||||
logDebug(
|
||||
`[discord-preflight] channelId=${messageChannelId} guild_id=${params.data.guild_id} channelType=${channelInfo?.type} isGuild=${isGuildMessage} isDM=${isDirectMessage} isGroupDm=${isGroupDm}`,
|
||||
@@ -639,6 +637,7 @@ export async function preflightDiscordMessage(
|
||||
messageChannelId,
|
||||
author,
|
||||
sender,
|
||||
canonicalMessageId: pluralkitInfo?.original?.trim() || undefined,
|
||||
memberRoleIds,
|
||||
channelInfo,
|
||||
channelName,
|
||||
|
||||
@@ -42,6 +42,7 @@ export type DiscordMessagePreflightContext = DiscordMessagePreflightSharedFields
|
||||
messageChannelId: string;
|
||||
author: User;
|
||||
sender: DiscordSenderIdentity;
|
||||
canonicalMessageId?: string;
|
||||
memberRoleIds: string[];
|
||||
|
||||
channelInfo: DiscordChannelInfo | null;
|
||||
|
||||
@@ -357,6 +357,8 @@ function getLastDispatchCtx():
|
||||
CommandBody?: string;
|
||||
From?: string;
|
||||
MediaTranscribedIndexes?: number[];
|
||||
MessageSid?: string;
|
||||
MessageSidFull?: string;
|
||||
MessageThreadId?: string | number;
|
||||
ModelParentSessionKey?: string;
|
||||
OriginatingTo?: string;
|
||||
@@ -375,6 +377,8 @@ function getLastDispatchCtx():
|
||||
CommandBody?: string;
|
||||
From?: string;
|
||||
MediaTranscribedIndexes?: number[];
|
||||
MessageSid?: string;
|
||||
MessageSidFull?: string;
|
||||
MessageThreadId?: string | number;
|
||||
ModelParentSessionKey?: string;
|
||||
OriginatingTo?: string;
|
||||
@@ -925,6 +929,25 @@ describe("processDiscordMessage session routing", () => {
|
||||
expect(sendMocks.removeReactionDiscord).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses PluralKit original ids for inbound dedupe while preserving the Discord message id", async () => {
|
||||
const ctx = await createBaseContext({
|
||||
canonicalMessageId: "orig-123",
|
||||
message: {
|
||||
id: "proxy-456",
|
||||
channelId: "c1",
|
||||
timestamp: new Date().toISOString(),
|
||||
attachments: [],
|
||||
},
|
||||
});
|
||||
|
||||
await runProcessDiscordMessage(ctx);
|
||||
|
||||
expect(getLastDispatchCtx()).toMatchObject({
|
||||
MessageSid: "orig-123",
|
||||
MessageSidFull: "proxy-456",
|
||||
});
|
||||
});
|
||||
|
||||
it("defaults guild replies to message-tool-only source delivery", async () => {
|
||||
await runProcessDiscordMessage(
|
||||
await createBaseContext({
|
||||
|
||||
Reference in New Issue
Block a user