Discord: avoid reply spam on chunked sends
This commit is contained in:
parent
df002ef840
commit
64c29c3755
@ -58,6 +58,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Telegram/Streaming: always clean up draft previews even when dispatch throws before fallback handling, preventing orphaned preview messages during failed runs. (#19041) thanks @mudrii.
|
||||
- Telegram/Streaming: split reasoning and answer draft preview lanes to prevent cross-lane overwrites, and ignore literal `<think>` tags inside inline/fenced code snippets so sample markup is not misrouted as reasoning. (#20774) Thanks @obviyus.
|
||||
- Telegram/Status reactions: refresh stall timers on repeated phase updates and honor ack-reaction scope when lifecycle reactions are enabled, preventing false stall emojis and unwanted group reactions. Thanks @wolly-tundracube and @thewilloftheshadow.
|
||||
- Discord/Streaming: apply `replyToMode: first` only to the first Discord chunk so block-streamed replies do not spam mention pings. (#20726) Thanks @thewilloftheshadow for the report.
|
||||
|
||||
- Discord/Gateway: handle close code 4014 (missing privileged gateway intents) without crashing the gateway. Thanks @thewilloftheshadow.
|
||||
- Security/Net: strip sensitive headers (`Authorization`, `Proxy-Authorization`, `Cookie`, `Cookie2`) on cross-origin redirects in `fetchWithSsrFGuard` to prevent credential forwarding across origin boundaries. (#20313) Thanks @afurm.
|
||||
|
||||
@ -887,6 +887,7 @@ async function dispatchDiscordComponentEvent(params: {
|
||||
rest: interaction.client.rest,
|
||||
runtime,
|
||||
replyToId,
|
||||
replyToMode,
|
||||
textLimit,
|
||||
maxLinesPerMessage: ctx.discordConfig?.maxLinesPerMessage,
|
||||
tableMode,
|
||||
|
||||
@ -628,6 +628,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
||||
rest: client.rest,
|
||||
runtime,
|
||||
replyToId,
|
||||
replyToMode,
|
||||
textLimit,
|
||||
maxLinesPerMessage: discordConfig?.maxLinesPerMessage,
|
||||
tableMode,
|
||||
|
||||
@ -84,4 +84,24 @@ describe("deliverDiscordReply", () => {
|
||||
expect(sendVoiceMessageDiscordMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendMessageDiscordMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses replyToId only for the first chunk when replyToMode is first", async () => {
|
||||
await deliverDiscordReply({
|
||||
replies: [
|
||||
{
|
||||
text: "1234567890",
|
||||
},
|
||||
],
|
||||
target: "channel:789",
|
||||
token: "token",
|
||||
runtime,
|
||||
textLimit: 5,
|
||||
replyToId: "reply-1",
|
||||
replyToMode: "first",
|
||||
});
|
||||
|
||||
expect(sendMessageDiscordMock).toHaveBeenCalledTimes(2);
|
||||
expect(sendMessageDiscordMock.mock.calls[0]?.[2]?.replyTo).toBe("reply-1");
|
||||
expect(sendMessageDiscordMock.mock.calls[1]?.[2]?.replyTo).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { RequestClient } from "@buape/carbon";
|
||||
import type { ChunkMode } from "../../auto-reply/chunk.js";
|
||||
import type { ReplyPayload } from "../../auto-reply/types.js";
|
||||
import type { MarkdownTableMode } from "../../config/types.base.js";
|
||||
import type { MarkdownTableMode, ReplyToMode } from "../../config/types.base.js";
|
||||
import { convertMarkdownTables } from "../../markdown/tables.js";
|
||||
import type { RuntimeEnv } from "../../runtime.js";
|
||||
import { chunkDiscordTextWithMode } from "../chunk.js";
|
||||
@ -17,10 +17,29 @@ export async function deliverDiscordReply(params: {
|
||||
textLimit: number;
|
||||
maxLinesPerMessage?: number;
|
||||
replyToId?: string;
|
||||
replyToMode?: ReplyToMode;
|
||||
tableMode?: MarkdownTableMode;
|
||||
chunkMode?: ChunkMode;
|
||||
}) {
|
||||
const chunkLimit = Math.min(params.textLimit, 2000);
|
||||
const replyTo = params.replyToId?.trim() || undefined;
|
||||
const replyToMode = params.replyToMode ?? "all";
|
||||
// replyToMode=first should only apply to the first physical send.
|
||||
const replyOnce = replyToMode === "first";
|
||||
let replyUsed = false;
|
||||
const resolveReplyTo = () => {
|
||||
if (!replyTo) {
|
||||
return undefined;
|
||||
}
|
||||
if (!replyOnce) {
|
||||
return replyTo;
|
||||
}
|
||||
if (replyUsed) {
|
||||
return undefined;
|
||||
}
|
||||
replyUsed = true;
|
||||
return replyTo;
|
||||
};
|
||||
for (const payload of params.replies) {
|
||||
const mediaList = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
||||
const rawText = payload.text ?? "";
|
||||
@ -29,8 +48,6 @@ export async function deliverDiscordReply(params: {
|
||||
if (!text && mediaList.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const replyTo = params.replyToId?.trim() || undefined;
|
||||
|
||||
if (mediaList.length === 0) {
|
||||
const mode = params.chunkMode ?? "length";
|
||||
const chunks = chunkDiscordTextWithMode(text, {
|
||||
@ -46,6 +63,7 @@ export async function deliverDiscordReply(params: {
|
||||
if (!trimmed) {
|
||||
continue;
|
||||
}
|
||||
const replyTo = resolveReplyTo();
|
||||
await sendMessageDiscord(params.target, trimmed, {
|
||||
token: params.token,
|
||||
rest: params.rest,
|
||||
@ -63,6 +81,7 @@ export async function deliverDiscordReply(params: {
|
||||
|
||||
// Voice message path: audioAsVoice flag routes through sendVoiceMessageDiscord
|
||||
if (payload.audioAsVoice) {
|
||||
const replyTo = resolveReplyTo();
|
||||
await sendVoiceMessageDiscord(params.target, firstMedia, {
|
||||
token: params.token,
|
||||
rest: params.rest,
|
||||
@ -71,6 +90,7 @@ export async function deliverDiscordReply(params: {
|
||||
});
|
||||
// Voice messages cannot include text; send remaining text separately if present
|
||||
if (text.trim()) {
|
||||
const replyTo = resolveReplyTo();
|
||||
await sendMessageDiscord(params.target, text, {
|
||||
token: params.token,
|
||||
rest: params.rest,
|
||||
@ -80,6 +100,7 @@ export async function deliverDiscordReply(params: {
|
||||
}
|
||||
// Additional media items are sent as regular attachments (voice is single-file only)
|
||||
for (const extra of mediaList.slice(1)) {
|
||||
const replyTo = resolveReplyTo();
|
||||
await sendMessageDiscord(params.target, "", {
|
||||
token: params.token,
|
||||
rest: params.rest,
|
||||
@ -91,6 +112,7 @@ export async function deliverDiscordReply(params: {
|
||||
continue;
|
||||
}
|
||||
|
||||
const replyTo = resolveReplyTo();
|
||||
await sendMessageDiscord(params.target, text, {
|
||||
token: params.token,
|
||||
rest: params.rest,
|
||||
@ -99,6 +121,7 @@ export async function deliverDiscordReply(params: {
|
||||
replyTo,
|
||||
});
|
||||
for (const extra of mediaList.slice(1)) {
|
||||
const replyTo = resolveReplyTo();
|
||||
await sendMessageDiscord(params.target, "", {
|
||||
token: params.token,
|
||||
rest: params.rest,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user