mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:20:43 +00:00
fix(discord): include component text in reply context
This commit is contained in:
@@ -50,6 +50,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Discord/DMs: set inbound direct-message `ctx.To` to the semantic `user:<id>` target while keeping delivery routed through the DM channel, so mirror and recovery paths do not treat DMs as channel conversations. Fixes #68126. Thanks @illuminate0623.
|
||||
- Discord/DMs: keep no-guild inbound messages on direct-message routing when Discord channel lookup is temporarily unavailable, preventing degraded DMs from forking into channel sessions. Fixes #59817. Thanks @DooPeePey.
|
||||
- Discord: retry outbound API calls on HTTP 5xx, request-timeout, and transient transport failures instead of only Discord rate limits, reducing dropped cron and agent replies during short Discord or network outages. Fixes #52396. Thanks @sunshineo.
|
||||
- Discord: include Components v2 Text Display content from referenced replies and forwarded snapshots, so component-only messages still appear in reply context. Fixes #56228. Thanks @HollandDrive.
|
||||
- Gateway/config: log config health-state write failures instead of silently hiding config observe-recovery write errors. Thanks @sallyom.
|
||||
- Diagnostics: reset stuck-session timers on reply, tool, status, block, and ACP progress events, and back off repeated `session.stuck` diagnostics while a session remains unchanged. Supersedes #72010. Thanks @rubencu.
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ export type DiscordSnapshotAuthor = {
|
||||
|
||||
export type DiscordSnapshotMessage = {
|
||||
content?: string | null;
|
||||
components?: unknown;
|
||||
embeds?: Array<{ description?: string | null; title?: string | null }> | null;
|
||||
attachments?: APIAttachment[] | null;
|
||||
stickers?: APIStickerItem[] | null;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ComponentType } from "discord-api-types/v10";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import type { Message } from "../internal/discord.js";
|
||||
import {
|
||||
@@ -30,6 +31,7 @@ export function resolveDiscordMessageText(
|
||||
(message.embeds?.[0] as { title?: string | null; description?: string | null } | undefined) ??
|
||||
null,
|
||||
);
|
||||
const componentText = extractDiscordComponentsV2Text(resolveDiscordMessageComponents(message));
|
||||
const rawText =
|
||||
normalizeOptionalString(message.content) ||
|
||||
buildDiscordMediaPlaceholder({
|
||||
@@ -37,6 +39,7 @@ export function resolveDiscordMessageText(
|
||||
stickers: resolveDiscordMessageStickers(message),
|
||||
}) ||
|
||||
embedText ||
|
||||
componentText ||
|
||||
normalizeOptionalString(options?.fallbackText) ||
|
||||
"";
|
||||
const baseText = resolveDiscordMentions(rawText, message);
|
||||
@@ -87,6 +90,50 @@ function resolveDiscordForwardedMessagesText(message: Message): string {
|
||||
return `${heading}\n${referencedText}`;
|
||||
}
|
||||
|
||||
function resolveDiscordMessageComponents(message: Message): unknown {
|
||||
const components = (message as { components?: unknown }).components;
|
||||
if (components !== undefined) {
|
||||
return components;
|
||||
}
|
||||
try {
|
||||
return (message as { rawData?: { components?: unknown } }).rawData?.components;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function extractDiscordComponentsV2Text(components: unknown): string {
|
||||
const parts: string[] = [];
|
||||
collectDiscordTextDisplayContent(components, parts);
|
||||
return parts.join("\n");
|
||||
}
|
||||
|
||||
function collectDiscordTextDisplayContent(value: unknown, parts: string[]): void {
|
||||
if (Array.isArray(value)) {
|
||||
for (const entry of value) {
|
||||
collectDiscordTextDisplayContent(entry, parts);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!value || typeof value !== "object") {
|
||||
return;
|
||||
}
|
||||
const component = value as {
|
||||
type?: unknown;
|
||||
content?: unknown;
|
||||
components?: unknown;
|
||||
component?: unknown;
|
||||
};
|
||||
if (component.type === ComponentType.TextDisplay) {
|
||||
const content = normalizeOptionalString(component.content);
|
||||
if (content) {
|
||||
parts.push(content);
|
||||
}
|
||||
}
|
||||
collectDiscordTextDisplayContent(component.components, parts);
|
||||
collectDiscordTextDisplayContent(component.component, parts);
|
||||
}
|
||||
|
||||
export function resolveDiscordForwardedMessagesTextFromSnapshots(snapshots: unknown): string {
|
||||
const forwardedBlocks = normalizeDiscordMessageSnapshots(snapshots)
|
||||
.map((snapshot) => buildDiscordForwardedMessageBlock(snapshot.message))
|
||||
@@ -119,5 +166,6 @@ function resolveDiscordSnapshotMessageText(snapshot: DiscordSnapshotMessage): st
|
||||
stickers: resolveDiscordSnapshotStickers(snapshot),
|
||||
});
|
||||
const embedText = resolveDiscordEmbedText(snapshot.embeds?.[0]);
|
||||
return content || attachmentText || embedText || "";
|
||||
const componentText = extractDiscordComponentsV2Text(snapshot.components);
|
||||
return content || attachmentText || embedText || componentText || "";
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { MessageReferenceType, StickerFormatType } from "discord-api-types/v10";
|
||||
import {
|
||||
ComponentType,
|
||||
MessageFlags,
|
||||
MessageReferenceType,
|
||||
StickerFormatType,
|
||||
} from "discord-api-types/v10";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { ChannelType, type Client, type Message } from "../internal/discord.js";
|
||||
|
||||
@@ -137,6 +142,7 @@ function asForwardedSnapshotMessage(params: {
|
||||
|
||||
function asReferencedForwardMessage(params: {
|
||||
content?: string;
|
||||
components?: Array<Record<string, unknown>>;
|
||||
embeds?: Array<{ title?: string; description?: string }>;
|
||||
attachments?: Array<Record<string, unknown>>;
|
||||
messageReferenceType?: MessageReferenceType;
|
||||
@@ -152,8 +158,10 @@ function asReferencedForwardMessage(params: {
|
||||
id: "m0",
|
||||
channelId: "c1",
|
||||
content: params.content ?? "",
|
||||
components: params.components ?? [],
|
||||
attachments: params.attachments ?? [],
|
||||
embeds: params.embeds ?? [],
|
||||
flags: params.components ? MessageFlags.IsComponentsV2 : 0,
|
||||
stickers: [],
|
||||
author: {
|
||||
id: "u2",
|
||||
@@ -980,6 +988,46 @@ describe("resolveDiscordMessageText", () => {
|
||||
expect(text).toBe("Breaking");
|
||||
});
|
||||
|
||||
it("uses Components v2 text display content when normal message text is empty", () => {
|
||||
const text = resolveDiscordMessageText(
|
||||
asMessage({
|
||||
content: "",
|
||||
flags: MessageFlags.IsComponentsV2,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.Container,
|
||||
components: [
|
||||
{ type: ComponentType.TextDisplay, content: "Component headline" },
|
||||
{
|
||||
type: ComponentType.Section,
|
||||
components: [{ type: ComponentType.TextDisplay, content: "Component body" }],
|
||||
accessory: { type: ComponentType.Thumbnail, media: { url: "attachment://x.png" } },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(text).toBe("Component headline\nComponent body");
|
||||
});
|
||||
|
||||
it("uses Components v2 text display content from referenced reply messages", () => {
|
||||
const text = resolveDiscordMessageText(
|
||||
asReferencedForwardMessage({
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.Container,
|
||||
components: [{ type: ComponentType.TextDisplay, content: "Referenced component text" }],
|
||||
},
|
||||
],
|
||||
messageReferenceType: MessageReferenceType.Default,
|
||||
}).referencedMessage!,
|
||||
);
|
||||
|
||||
expect(text).toBe("Referenced component text");
|
||||
});
|
||||
|
||||
it("uses embed description when content is empty", () => {
|
||||
const text = resolveDiscordMessageText(
|
||||
asMessage({
|
||||
@@ -1025,6 +1073,42 @@ describe("resolveDiscordMessageText", () => {
|
||||
expect(text).toContain("[Forwarded message from @Bob]");
|
||||
expect(text).toContain("Forwarded title\nForwarded details");
|
||||
});
|
||||
|
||||
it("includes Components v2 text display content from forwarded snapshots", () => {
|
||||
const text = resolveDiscordMessageText(
|
||||
asMessage({
|
||||
content: "",
|
||||
rawData: {
|
||||
message_snapshots: [
|
||||
{
|
||||
message: {
|
||||
content: "",
|
||||
embeds: [],
|
||||
attachments: [],
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.Container,
|
||||
components: [
|
||||
{ type: ComponentType.TextDisplay, content: "Forwarded component text" },
|
||||
],
|
||||
},
|
||||
],
|
||||
author: {
|
||||
id: "u2",
|
||||
username: "Bob",
|
||||
discriminator: "0",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
{ includeForwarded: true },
|
||||
);
|
||||
|
||||
expect(text).toContain("[Forwarded message from @Bob]");
|
||||
expect(text).toContain("Forwarded component text");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveDiscordChannelInfo", () => {
|
||||
|
||||
Reference in New Issue
Block a user