fix(discord): recover forwarded referenced message content

# Conflicts:
#	extensions/discord/src/monitor/message-utils.ts
This commit is contained in:
XING
2026-04-06 11:57:53 +08:00
committed by Peter Steinberger
parent 2d75be0ea7
commit c5493b15d6
2 changed files with 164 additions and 54 deletions

View File

@@ -1,5 +1,5 @@
import { ChannelType, type Client, type Message } from "@buape/carbon";
import { StickerFormatType } from "discord-api-types/v10";
import { MessageReferenceType, StickerFormatType } from "discord-api-types/v10";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const fetchRemoteMedia = vi.fn();
@@ -135,6 +135,35 @@ function asForwardedSnapshotMessage(params: {
});
}
function asReferencedForwardMessage(params: {
content?: string;
embeds?: Array<{ title?: string; description?: string }>;
attachments?: Array<Record<string, unknown>>;
messageReferenceType?: MessageReferenceType;
}) {
return asMessage({
content: "",
messageReference: {
type: params.messageReferenceType ?? MessageReferenceType.Forward,
message_id: "m0",
channel_id: "c1",
},
referencedMessage: asMessage({
id: "m0",
channelId: "c1",
content: params.content ?? "",
attachments: params.attachments ?? [],
embeds: params.embeds ?? [],
stickers: [],
author: {
id: "u2",
username: "Bob",
discriminator: "0",
},
}),
});
}
describe("resolveDiscordMessageChannelId", () => {
it.each([
{
@@ -295,6 +324,38 @@ describe("resolveForwardedMediaList", () => {
expect(fetchRemoteMedia).not.toHaveBeenCalled();
});
it("downloads forwarded referenced attachments when snapshots are absent", async () => {
const attachment = {
id: "att-ref-1",
url: "https://cdn.discordapp.com/attachments/1/ref-image.png",
filename: "ref-image.png",
content_type: "image/png",
};
fetchRemoteMedia.mockResolvedValueOnce({
buffer: Buffer.from("image"),
contentType: "image/png",
});
saveMediaBuffer.mockResolvedValueOnce({
path: "/tmp/ref-image.png",
contentType: "image/png",
});
const result = await resolveForwardedMediaList(
asReferencedForwardMessage({
attachments: [attachment],
}),
512,
);
expectSinglePngDownload({
result,
expectedUrl: attachment.url,
filePathHint: attachment.filename,
expectedPath: "/tmp/ref-image.png",
placeholder: "<media:image>",
});
});
it("skips snapshots without attachments", async () => {
const result = await resolveForwardedMediaList(
asMessage({
@@ -775,6 +836,30 @@ describe("resolveDiscordMessageText", () => {
expect(text).toContain("forwarded hello");
});
it("falls back to referenced forward message text when snapshots are absent", () => {
const text = resolveDiscordMessageText(
asReferencedForwardMessage({
content: "forwarded from referenced message",
}),
{ includeForwarded: true },
);
expect(text).toContain("[Forwarded message from @Bob]");
expect(text).toContain("forwarded from referenced message");
});
it("does not treat ordinary replies as forwarded context", () => {
const text = resolveDiscordMessageText(
asReferencedForwardMessage({
content: "quoted reply content",
messageReferenceType: MessageReferenceType.Default,
}),
{ includeForwarded: true },
);
expect(text).toBe("");
});
it("resolves user mentions in content", () => {
const text = resolveDiscordMessageText(
asMessage({

View File

@@ -1,5 +1,10 @@
import type { ChannelType, Client, Message } from "@buape/carbon";
import { StickerFormatType, type APIAttachment, type APIStickerItem } from "discord-api-types/v10";
import {
MessageReferenceType,
StickerFormatType,
type APIAttachment,
type APIStickerItem,
} from "discord-api-types/v10";
import { fetchRemoteMedia, type FetchLike } from "openclaw/plugin-sdk/media-runtime";
import { saveMediaBuffer } from "openclaw/plugin-sdk/media-runtime";
import { buildMediaPayload } from "openclaw/plugin-sdk/reply-payload";
@@ -253,35 +258,61 @@ export async function resolveForwardedMediaList(
options?: DiscordMediaResolveOptions,
): Promise<DiscordMediaInfo[]> {
const snapshots = resolveDiscordMessageSnapshots(message);
if (snapshots.length === 0) {
return [];
}
const out: DiscordMediaInfo[] = [];
const resolvedSsrFPolicy = resolveDiscordMediaSsrFPolicy(options?.ssrfPolicy);
for (const snapshot of snapshots) {
await appendResolvedMediaFromAttachments({
attachments: snapshot.message?.attachments,
maxBytes,
out,
errorPrefix: "discord: failed to download forwarded attachment",
fetchImpl: options?.fetchImpl,
ssrfPolicy: resolvedSsrFPolicy,
readIdleTimeoutMs: options?.readIdleTimeoutMs,
totalTimeoutMs: options?.totalTimeoutMs,
abortSignal: options?.abortSignal,
});
await appendResolvedMediaFromStickers({
stickers: snapshot.message ? resolveDiscordSnapshotStickers(snapshot.message) : [],
maxBytes,
out,
errorPrefix: "discord: failed to download forwarded sticker",
fetchImpl: options?.fetchImpl,
ssrfPolicy: resolvedSsrFPolicy,
readIdleTimeoutMs: options?.readIdleTimeoutMs,
totalTimeoutMs: options?.totalTimeoutMs,
abortSignal: options?.abortSignal,
});
if (snapshots.length > 0) {
for (const snapshot of snapshots) {
await appendResolvedMediaFromAttachments({
attachments: snapshot.message?.attachments,
maxBytes,
out,
errorPrefix: "discord: failed to download forwarded attachment",
fetchImpl: options?.fetchImpl,
ssrfPolicy: resolvedSsrFPolicy,
readIdleTimeoutMs: options?.readIdleTimeoutMs,
totalTimeoutMs: options?.totalTimeoutMs,
abortSignal: options?.abortSignal,
});
await appendResolvedMediaFromStickers({
stickers: snapshot.message ? resolveDiscordSnapshotStickers(snapshot.message) : [],
maxBytes,
out,
errorPrefix: "discord: failed to download forwarded sticker",
fetchImpl: options?.fetchImpl,
ssrfPolicy: resolvedSsrFPolicy,
readIdleTimeoutMs: options?.readIdleTimeoutMs,
totalTimeoutMs: options?.totalTimeoutMs,
abortSignal: options?.abortSignal,
});
}
return out;
}
const referencedForward = resolveDiscordReferencedForwardMessage(message);
if (!referencedForward) {
return out;
}
await appendResolvedMediaFromAttachments({
attachments: referencedForward.attachments,
maxBytes,
out,
errorPrefix: "discord: failed to download forwarded attachment",
fetchImpl: options?.fetchImpl,
ssrfPolicy: resolvedSsrFPolicy,
readIdleTimeoutMs: options?.readIdleTimeoutMs,
totalTimeoutMs: options?.totalTimeoutMs,
abortSignal: options?.abortSignal,
});
await appendResolvedMediaFromStickers({
stickers: resolveDiscordMessageStickers(referencedForward),
maxBytes,
out,
errorPrefix: "discord: failed to download forwarded sticker",
fetchImpl: options?.fetchImpl,
ssrfPolicy: resolvedSsrFPolicy,
readIdleTimeoutMs: options?.readIdleTimeoutMs,
totalTimeoutMs: options?.totalTimeoutMs,
abortSignal: options?.abortSignal,
});
return out;
}
@@ -635,35 +666,23 @@ function resolveDiscordMentions(text: string, message: Message): string {
}
function resolveDiscordForwardedMessagesText(message: Message): string {
return resolveDiscordForwardedMessagesTextFromSnapshots(resolveDiscordMessageSnapshots(message));
}
function resolveDiscordMessageSnapshots(message: Message): DiscordMessageSnapshot[] {
const rawData = (message as { rawData?: { message_snapshots?: unknown } }).rawData;
return normalizeDiscordMessageSnapshots(
rawData?.message_snapshots ??
(message as { message_snapshots?: unknown }).message_snapshots ??
(message as { messageSnapshots?: unknown }).messageSnapshots,
);
}
function normalizeDiscordMessageSnapshots(snapshots: unknown): DiscordMessageSnapshot[] {
if (!Array.isArray(snapshots)) {
return [];
const snapshots = resolveDiscordMessageSnapshots(message);
if (snapshots.length > 0) {
return resolveDiscordForwardedMessagesTextFromSnapshots(snapshots);
}
return snapshots.filter(
(entry): entry is DiscordMessageSnapshot => Boolean(entry) && typeof entry === "object",
);
}
export function resolveDiscordForwardedMessagesTextFromSnapshots(snapshots: unknown): string {
const forwardedBlocks = normalizeDiscordMessageSnapshots(snapshots)
.map((snapshot) => buildDiscordForwardedMessageBlock(snapshot.message))
.filter((entry): entry is string => Boolean(entry));
if (forwardedBlocks.length === 0) {
const referencedForward = resolveDiscordReferencedForwardMessage(message);
if (!referencedForward) {
return "";
}
return forwardedBlocks.join("\n\n");
const referencedText = resolveDiscordMessageText(referencedForward, {
includeForwarded: true,
});
if (!referencedText) {
return "";
}
const authorLabel = formatDiscordSnapshotAuthor(referencedForward.author);
const heading = authorLabel ? `[Forwarded message from ${authorLabel}]` : "[Forwarded message]";
return `${heading}\n${referencedText}`;
}
function buildDiscordForwardedMessageBlock(
@@ -681,6 +700,12 @@ function buildDiscordForwardedMessageBlock(
return `${heading}\n${text}`;
}
function resolveDiscordReferencedForwardMessage(message: Message): Message | null {
return message.messageReference?.type === MessageReferenceType.Forward
? message.referencedMessage
: null;
}
function resolveDiscordSnapshotMessageText(snapshot: DiscordSnapshotMessage): string {
const content = snapshot.content?.trim() ?? "";
const attachmentText = buildDiscordMediaPlaceholder({