mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 20:04:32 +00:00
Discord: avoid reply spam on chunked sends
This commit is contained in:
@@ -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: 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/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.
|
- 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.
|
- 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.
|
- 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,
|
rest: interaction.client.rest,
|
||||||
runtime,
|
runtime,
|
||||||
replyToId,
|
replyToId,
|
||||||
|
replyToMode,
|
||||||
textLimit,
|
textLimit,
|
||||||
maxLinesPerMessage: ctx.discordConfig?.maxLinesPerMessage,
|
maxLinesPerMessage: ctx.discordConfig?.maxLinesPerMessage,
|
||||||
tableMode,
|
tableMode,
|
||||||
|
|||||||
@@ -628,6 +628,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
|||||||
rest: client.rest,
|
rest: client.rest,
|
||||||
runtime,
|
runtime,
|
||||||
replyToId,
|
replyToId,
|
||||||
|
replyToMode,
|
||||||
textLimit,
|
textLimit,
|
||||||
maxLinesPerMessage: discordConfig?.maxLinesPerMessage,
|
maxLinesPerMessage: discordConfig?.maxLinesPerMessage,
|
||||||
tableMode,
|
tableMode,
|
||||||
|
|||||||
@@ -84,4 +84,24 @@ describe("deliverDiscordReply", () => {
|
|||||||
expect(sendVoiceMessageDiscordMock).toHaveBeenCalledTimes(1);
|
expect(sendVoiceMessageDiscordMock).toHaveBeenCalledTimes(1);
|
||||||
expect(sendMessageDiscordMock).not.toHaveBeenCalled();
|
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 { RequestClient } from "@buape/carbon";
|
||||||
import type { ChunkMode } from "../../auto-reply/chunk.js";
|
import type { ChunkMode } from "../../auto-reply/chunk.js";
|
||||||
import type { ReplyPayload } from "../../auto-reply/types.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 { convertMarkdownTables } from "../../markdown/tables.js";
|
||||||
import type { RuntimeEnv } from "../../runtime.js";
|
import type { RuntimeEnv } from "../../runtime.js";
|
||||||
import { chunkDiscordTextWithMode } from "../chunk.js";
|
import { chunkDiscordTextWithMode } from "../chunk.js";
|
||||||
@@ -17,10 +17,29 @@ export async function deliverDiscordReply(params: {
|
|||||||
textLimit: number;
|
textLimit: number;
|
||||||
maxLinesPerMessage?: number;
|
maxLinesPerMessage?: number;
|
||||||
replyToId?: string;
|
replyToId?: string;
|
||||||
|
replyToMode?: ReplyToMode;
|
||||||
tableMode?: MarkdownTableMode;
|
tableMode?: MarkdownTableMode;
|
||||||
chunkMode?: ChunkMode;
|
chunkMode?: ChunkMode;
|
||||||
}) {
|
}) {
|
||||||
const chunkLimit = Math.min(params.textLimit, 2000);
|
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) {
|
for (const payload of params.replies) {
|
||||||
const mediaList = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
const mediaList = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
||||||
const rawText = payload.text ?? "";
|
const rawText = payload.text ?? "";
|
||||||
@@ -29,8 +48,6 @@ export async function deliverDiscordReply(params: {
|
|||||||
if (!text && mediaList.length === 0) {
|
if (!text && mediaList.length === 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const replyTo = params.replyToId?.trim() || undefined;
|
|
||||||
|
|
||||||
if (mediaList.length === 0) {
|
if (mediaList.length === 0) {
|
||||||
const mode = params.chunkMode ?? "length";
|
const mode = params.chunkMode ?? "length";
|
||||||
const chunks = chunkDiscordTextWithMode(text, {
|
const chunks = chunkDiscordTextWithMode(text, {
|
||||||
@@ -46,6 +63,7 @@ export async function deliverDiscordReply(params: {
|
|||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const replyTo = resolveReplyTo();
|
||||||
await sendMessageDiscord(params.target, trimmed, {
|
await sendMessageDiscord(params.target, trimmed, {
|
||||||
token: params.token,
|
token: params.token,
|
||||||
rest: params.rest,
|
rest: params.rest,
|
||||||
@@ -63,6 +81,7 @@ export async function deliverDiscordReply(params: {
|
|||||||
|
|
||||||
// Voice message path: audioAsVoice flag routes through sendVoiceMessageDiscord
|
// Voice message path: audioAsVoice flag routes through sendVoiceMessageDiscord
|
||||||
if (payload.audioAsVoice) {
|
if (payload.audioAsVoice) {
|
||||||
|
const replyTo = resolveReplyTo();
|
||||||
await sendVoiceMessageDiscord(params.target, firstMedia, {
|
await sendVoiceMessageDiscord(params.target, firstMedia, {
|
||||||
token: params.token,
|
token: params.token,
|
||||||
rest: params.rest,
|
rest: params.rest,
|
||||||
@@ -71,6 +90,7 @@ export async function deliverDiscordReply(params: {
|
|||||||
});
|
});
|
||||||
// Voice messages cannot include text; send remaining text separately if present
|
// Voice messages cannot include text; send remaining text separately if present
|
||||||
if (text.trim()) {
|
if (text.trim()) {
|
||||||
|
const replyTo = resolveReplyTo();
|
||||||
await sendMessageDiscord(params.target, text, {
|
await sendMessageDiscord(params.target, text, {
|
||||||
token: params.token,
|
token: params.token,
|
||||||
rest: params.rest,
|
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)
|
// Additional media items are sent as regular attachments (voice is single-file only)
|
||||||
for (const extra of mediaList.slice(1)) {
|
for (const extra of mediaList.slice(1)) {
|
||||||
|
const replyTo = resolveReplyTo();
|
||||||
await sendMessageDiscord(params.target, "", {
|
await sendMessageDiscord(params.target, "", {
|
||||||
token: params.token,
|
token: params.token,
|
||||||
rest: params.rest,
|
rest: params.rest,
|
||||||
@@ -91,6 +112,7 @@ export async function deliverDiscordReply(params: {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const replyTo = resolveReplyTo();
|
||||||
await sendMessageDiscord(params.target, text, {
|
await sendMessageDiscord(params.target, text, {
|
||||||
token: params.token,
|
token: params.token,
|
||||||
rest: params.rest,
|
rest: params.rest,
|
||||||
@@ -99,6 +121,7 @@ export async function deliverDiscordReply(params: {
|
|||||||
replyTo,
|
replyTo,
|
||||||
});
|
});
|
||||||
for (const extra of mediaList.slice(1)) {
|
for (const extra of mediaList.slice(1)) {
|
||||||
|
const replyTo = resolveReplyTo();
|
||||||
await sendMessageDiscord(params.target, "", {
|
await sendMessageDiscord(params.target, "", {
|
||||||
token: params.token,
|
token: params.token,
|
||||||
rest: params.rest,
|
rest: params.rest,
|
||||||
|
|||||||
Reference in New Issue
Block a user