






















@@ -4,14 +4,16 @@ import {
44retryAsync,
55type RetryConfig,
66} from "openclaw/plugin-sdk/retry-runtime";
7+import { isDiscordHtmlResponseBody, summarizeDiscordResponseBody } from "./error-body.js";
7889const DISCORD_API_BASE = "https://discord.com/api/v10";
910const DISCORD_API_RETRY_DEFAULTS = {
1011attempts: 3,
1112minDelayMs: 500,
12-maxDelayMs: 30_000,
13+maxDelayMs: 5 * 60_000,
1314jitter: 0.1,
1415};
16+const DISCORD_API_429_FALLBACK_RETRY_AFTER_SECONDS = 60;
15171618type DiscordApiErrorPayload = {
1719message?: string;
@@ -50,7 +52,14 @@ function parseRetryAfterSeconds(text: string, response: Response): number | unde
5052return undefined;
5153}
5254const parsed = Number(header);
53-return Number.isFinite(parsed) ? parsed : undefined;
55+if (Number.isFinite(parsed) && parsed >= 0) {
56+return parsed;
57+}
58+const retryAt = Date.parse(header);
59+if (!Number.isFinite(retryAt)) {
60+return undefined;
61+}
62+return Math.max(0, (retryAt - Date.now()) / 1000);
5463}
55645665function formatRetryAfterSeconds(value: number | undefined): string | undefined {
@@ -61,15 +70,25 @@ function formatRetryAfterSeconds(value: number | undefined): string | undefined
6170return `${rounded}s`;
6271}
637264-function formatDiscordApiErrorText(text: string): string | undefined {
73+function formatDiscordApiErrorText(text: string, response: Response): string | undefined {
6574const trimmed = text.trim();
6675if (!trimmed) {
6776return undefined;
6877}
6978const payload = parseDiscordApiErrorPayload(trimmed);
7079if (!payload) {
7180const looksJson = trimmed.startsWith("{") && trimmed.endsWith("}");
72-return looksJson ? "unknown error" : trimmed;
81+if (looksJson) {
82+return "unknown error";
83+}
84+const summary = summarizeDiscordResponseBody(trimmed);
85+if (isDiscordHtmlResponseBody(trimmed, response.headers.get("content-type"))) {
86+if (!summary) {
87+return response.status === 429 ? "rate limited by Discord upstream" : undefined;
88+}
89+return response.status === 429 ? `rate limited by Discord upstream: ${summary}` : summary;
90+}
91+return summary;
7392}
7493const message =
7594typeof payload.message === "string" && payload.message.trim()
@@ -92,6 +111,16 @@ export class DiscordApiError extends Error {
92111}
93112}
94113114+function getDiscordApiRetryAfterMs(
115+err: unknown,
116+retryConfig: Required<RetryConfig>,
117+): number | undefined {
118+if (!(err instanceof DiscordApiError) || typeof err.retryAfter !== "number") {
119+return undefined;
120+}
121+return Math.min(Math.max(0, err.retryAfter * 1000), retryConfig.maxDelayMs);
122+}
123+95124export type DiscordFetchOptions = {
96125retry?: RetryConfig;
97126label?: string;
@@ -116,9 +145,12 @@ export async function fetchDiscord<T>(
116145});
117146if (!res.ok) {
118147const text = await res.text().catch(() => "");
119-const detail = formatDiscordApiErrorText(text);
148+const detail = formatDiscordApiErrorText(text, res);
120149const suffix = detail ? `: ${detail}` : "";
121-const retryAfter = res.status === 429 ? parseRetryAfterSeconds(text, res) : undefined;
150+const retryAfter =
151+res.status === 429
152+ ? (parseRetryAfterSeconds(text, res) ?? DISCORD_API_429_FALLBACK_RETRY_AFTER_SECONDS)
153+ : undefined;
122154throw new DiscordApiError(
123155`Discord API ${path} failed (${res.status})${suffix}`,
124156res.status,
@@ -131,10 +163,7 @@ export async function fetchDiscord<T>(
131163 ...retryConfig,
132164label: options?.label ?? path,
133165shouldRetry: (err) => err instanceof DiscordApiError && err.status === 429,
134-retryAfterMs: (err) =>
135-err instanceof DiscordApiError && typeof err.retryAfter === "number"
136- ? err.retryAfter * 1000
137- : undefined,
166+retryAfterMs: (err) => getDiscordApiRetryAfterMs(err, retryConfig),
138167},
139168);
140169}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。