mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix(discord): preserve text after oversized uploads (#99577)
Co-authored-by: Peter Steinberger <steipete@gmail.com> Co-authored-by: lin-hongkuan <lin-hongkuan@users.noreply.github.com>
This commit is contained in:
co-authored by
Peter Steinberger
lin-hongkuan
parent
9b0c3abbca
commit
9607c3a2bf
@@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- **iOS Apple Watch pairing:** react immediately to Watch pairing and companion-install changes, and wait for asynchronous WatchConnectivity activation before sending, preventing stale unavailable state and cold-launch delivery races.
|
||||
- **Discord oversized attachments:** preserve reply text and report the skipped file when Discord rejects an upload as too large. (#99577, #99021) Thanks @lin-hongkuan.
|
||||
- **Small-context compaction:** cap the effective reserve against the known model context window so small local models do not enter compaction from the first token. (#100621) Thanks @vincentkoc.
|
||||
- **Plugin install diagnostics:** suppress the misleading hook-pack fallback after plugin install failures only when the hook manifest is absent, while preserving actionable malformed hook-pack errors. (#100554) Thanks @vincentkoc.
|
||||
- **Config validation diagnostics:** emit each unchanged sanitized validation-warning payload once per config path, reset deduplication after a clean validation, and preserve the warning fingerprint across transient invalid reads and failed refreshes. (#100569, #25574) Thanks @vincentkoc.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Discord tests cover send.sends basic channel messages plugin behavior.
|
||||
import { ChannelType, MessageFlags, PermissionFlagsBits, Routes } from "discord-api-types/v10";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { Container, TextDisplay } from "./internal/discord.js";
|
||||
import { discordWebMediaMockFactory, makeDiscordRest } from "./send.test-harness.js";
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/web-media", () => discordWebMediaMockFactory());
|
||||
@@ -594,6 +595,77 @@ describe("sendMessageDiscord", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves text when Discord rejects an upload with error 40005", async () => {
|
||||
const { rest, postMock } = makeDiscordRest();
|
||||
postMock
|
||||
.mockRejectedValueOnce(
|
||||
Object.assign(new Error("Bad Request"), {
|
||||
status: 400,
|
||||
rawError: { code: 40005 },
|
||||
}),
|
||||
)
|
||||
.mockResolvedValueOnce({ id: "fallback-msg", channel_id: "789" });
|
||||
|
||||
const res = await sendMessageDiscord("channel:789", "Here is the report", {
|
||||
rest,
|
||||
token: "t",
|
||||
cfg: DISCORD_TEST_CFG,
|
||||
mediaUrl: "file:///tmp/report.pdf",
|
||||
replyTo: "orig-123",
|
||||
components: [new Container([new TextDisplay("Attachment controls")])],
|
||||
embeds: [{ title: "Attachment preview" }],
|
||||
});
|
||||
|
||||
expect(res.messageId).toBe("fallback-msg");
|
||||
expect(postMock).toHaveBeenCalledTimes(2);
|
||||
expectBodyFileName(requireRestBody(postMock, 0), "photo.jpg");
|
||||
const fallbackBody = requireRestBody(postMock, 1);
|
||||
expect(fallbackBody.content).toBe(
|
||||
"Here is the report\n\n[Attachment skipped: Discord rejected the file as too large.]",
|
||||
);
|
||||
expect(fallbackBody).not.toHaveProperty("files");
|
||||
expect(fallbackBody).not.toHaveProperty("components");
|
||||
expect(fallbackBody).not.toHaveProperty("embeds");
|
||||
expectReplyReference(fallbackBody, "orig-123");
|
||||
});
|
||||
|
||||
it("reports a media-only upload rejected with HTTP 413", async () => {
|
||||
const { rest, postMock } = makeDiscordRest();
|
||||
postMock
|
||||
.mockRejectedValueOnce(Object.assign(new Error("Bad Request"), { status: 413 }))
|
||||
.mockResolvedValueOnce({ id: "fallback-msg", channel_id: "789" });
|
||||
|
||||
const res = await sendMessageDiscord("channel:789", "", {
|
||||
rest,
|
||||
token: "t",
|
||||
cfg: DISCORD_TEST_CFG,
|
||||
mediaUrl: "file:///tmp/photo.jpg",
|
||||
});
|
||||
|
||||
expect(res.messageId).toBe("fallback-msg");
|
||||
expect(requireRestBody(postMock, 1).content).toBe(
|
||||
"Attachment skipped: Discord rejected the file as too large.",
|
||||
);
|
||||
expect(requireRestBody(postMock, 1)).not.toHaveProperty("files");
|
||||
});
|
||||
|
||||
it("does not mask unrelated media upload failures", async () => {
|
||||
const { rest, postMock } = makeDiscordRest();
|
||||
const error = Object.assign(new Error("Internal Server Error"), { status: 500 });
|
||||
postMock.mockRejectedValue(error);
|
||||
|
||||
await expect(
|
||||
sendMessageDiscord("channel:789", "report", {
|
||||
rest,
|
||||
token: "t",
|
||||
cfg: DISCORD_TEST_CFG,
|
||||
mediaUrl: "file:///tmp/report.pdf",
|
||||
retry: { attempts: 1 },
|
||||
}),
|
||||
).rejects.toBe(error);
|
||||
expect(postMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("passes mediaAccess workspaceDir when loading relative media attachments", async () => {
|
||||
const { rest, postMock } = makeDiscordRest();
|
||||
postMock.mockResolvedValue({ id: "msg", channel_id: "789" });
|
||||
|
||||
@@ -35,6 +35,10 @@ const DISCORD_POLL_MAX_ANSWERS = 10;
|
||||
const DISCORD_POLL_MAX_DURATION_HOURS = 32 * 24;
|
||||
const DISCORD_MISSING_PERMISSIONS = 50013;
|
||||
const DISCORD_CANNOT_DM = 50007;
|
||||
const DISCORD_UPLOAD_TOO_LARGE = 40005;
|
||||
const DISCORD_UPLOAD_TOO_LARGE_STATUS = 413;
|
||||
const DISCORD_UPLOAD_TOO_LARGE_NOTICE =
|
||||
"Attachment skipped: Discord rejected the file as too large.";
|
||||
|
||||
type DiscordRequest = RetryRunner;
|
||||
|
||||
@@ -155,6 +159,19 @@ function getDiscordErrorStatus(err: unknown) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isDiscordUploadTooLargeError(err: unknown) {
|
||||
return (
|
||||
getDiscordErrorCode(err) === DISCORD_UPLOAD_TOO_LARGE ||
|
||||
getDiscordErrorStatus(err) === DISCORD_UPLOAD_TOO_LARGE_STATUS
|
||||
);
|
||||
}
|
||||
|
||||
function buildDiscordUploadTooLargeFallbackText(text: string) {
|
||||
return text.trim()
|
||||
? `${text}\n\n[${DISCORD_UPLOAD_TOO_LARGE_NOTICE}]`
|
||||
: DISCORD_UPLOAD_TOO_LARGE_NOTICE;
|
||||
}
|
||||
|
||||
async function buildDiscordSendError(
|
||||
err: unknown,
|
||||
ctx: {
|
||||
@@ -420,10 +437,34 @@ async function sendDiscordMedia(
|
||||
},
|
||||
],
|
||||
});
|
||||
const res = (await request(
|
||||
() => createChannelMessage<{ id: string; channel_id: string }>(rest, channelId, { body }),
|
||||
"media",
|
||||
)) as { id: string; channel_id: string };
|
||||
let res: { id: string; channel_id: string };
|
||||
try {
|
||||
res = (await request(
|
||||
() => createChannelMessage<{ id: string; channel_id: string }>(rest, channelId, { body }),
|
||||
"media",
|
||||
)) as { id: string; channel_id: string };
|
||||
} catch (err) {
|
||||
if (!isDiscordUploadTooLargeError(err)) {
|
||||
throw err;
|
||||
}
|
||||
// The multipart request is all-or-nothing. Retry the portable text only;
|
||||
// attachment-coupled embeds/components may be invalid or misleading without it.
|
||||
return sendDiscordText(
|
||||
rest,
|
||||
channelId,
|
||||
buildDiscordUploadTooLargeFallbackText(text),
|
||||
replyTo,
|
||||
request,
|
||||
maxLinesPerMessage,
|
||||
undefined,
|
||||
undefined,
|
||||
chunkMode,
|
||||
silent,
|
||||
suppressEmbeds,
|
||||
maxChars,
|
||||
onResult,
|
||||
);
|
||||
}
|
||||
await onResult?.(res, "media");
|
||||
const platformMessageIds = res.id ? [res.id] : [];
|
||||
for (const chunk of chunks.slice(1)) {
|
||||
|
||||
Reference in New Issue
Block a user