mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
fix(discord): avoid startup rest amplification
This commit is contained in:
@@ -76,6 +76,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Discord: add configurable gateway READY timeouts for startup and runtime reconnects, so staggered multi-account setups can avoid false restart loops. Fixes #72273. Thanks @sergionsantos.
|
||||
- Discord: preserve native slash-command description localizations through command reconcile, so localized Discord descriptions no longer get overwritten by English defaults. Fixes #56580. Thanks @mhseo93.
|
||||
- Discord: add configured outbound mention aliases so known `@Name` references can be rewritten to real Discord user mentions instead of relying only on the transient directory cache. Fixes #67587. Thanks @McoreD.
|
||||
- Discord: avoid startup REST amplification by skipping native command deploy retries after Discord rate limits and deriving the bot id from parseable bot tokens instead of requiring a `/users/@me` lookup. Fixes #75341. Thanks @PrinceOfEgypt.
|
||||
- Plugins/hooks: derive hook `ctx.channelId` from the conversation target instead of the provider name, so Discord and other channel plugins can keep per-channel state isolated. Fixes #59881. Thanks @bradfreels.
|
||||
- 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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { formatDurationSeconds, warn, type RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { warn, type RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import { Client, overwriteApplicationCommands, type RequestClient } from "../internal/discord.js";
|
||||
import {
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
formatDiscordDeployRateLimitDetails,
|
||||
formatDiscordDeployRateLimitWarning,
|
||||
isDiscordDeployDailyCreateLimit,
|
||||
resolveDiscordDeployRateLimitDetails,
|
||||
} from "./provider.deploy-errors.js";
|
||||
import { logDiscordStartupPhase } from "./provider.startup-log.js";
|
||||
|
||||
@@ -130,9 +129,6 @@ async function deployDiscordCommands(params: {
|
||||
}
|
||||
const startupStartedAt = params.startupStartedAt ?? Date.now();
|
||||
const accountId = params.accountId ?? "default";
|
||||
const maxAttempts = 3;
|
||||
const maxRetryDelayMs = 15_000;
|
||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
||||
const restoreDeployRestLogging = installDeployRestLogging({
|
||||
rest: params.client.rest,
|
||||
runtime: params.runtime,
|
||||
@@ -141,46 +137,29 @@ async function deployDiscordCommands(params: {
|
||||
shouldLogVerbose: params.shouldLogVerbose,
|
||||
});
|
||||
try {
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||||
try {
|
||||
await params.client.deployCommands({ mode: "reconcile" });
|
||||
try {
|
||||
await params.client.deployCommands({ mode: "reconcile" });
|
||||
return;
|
||||
} catch (err) {
|
||||
if (isDiscordDeployDailyCreateLimit(err)) {
|
||||
params.runtime.log?.(
|
||||
warn(
|
||||
`discord: native slash command deploy skipped for ${accountId}; daily application command create limit reached. Existing slash commands stay active until Discord resets the quota. Message send/receive is unaffected.`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
} catch (err) {
|
||||
if (isDiscordDeployDailyCreateLimit(err)) {
|
||||
params.runtime.log?.(
|
||||
warn(
|
||||
`discord: native slash command deploy skipped for ${accountId}; daily application command create limit reached. Existing slash commands stay active until Discord resets the quota. Message send/receive is unaffected.`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const rateLimitDetails = resolveDiscordDeployRateLimitDetails(err);
|
||||
if (!rateLimitDetails || attempt >= maxAttempts) {
|
||||
throw err;
|
||||
}
|
||||
const retryAfterMs = Math.max(0, Math.ceil(rateLimitDetails.retryAfterMs ?? 0));
|
||||
if (retryAfterMs > maxRetryDelayMs) {
|
||||
params.runtime.log?.(
|
||||
warn(
|
||||
`discord: native slash command deploy skipped for ${accountId}; retry after ${formatDurationSeconds(retryAfterMs, { decimals: 1 })} exceeds startup budget. Existing slash commands stay active. Message send/receive is unaffected.`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (params.shouldLogVerbose()) {
|
||||
params.runtime.log?.(
|
||||
`discord startup [${accountId}] deploy-retry ${Math.max(0, Date.now() - startupStartedAt)}ms attempt=${attempt}/${maxAttempts - 1} retryAfterMs=${retryAfterMs} scope=${rateLimitDetails.scope ?? "unknown"} code=${rateLimitDetails.discordCode ?? "unknown"}`,
|
||||
);
|
||||
}
|
||||
await sleep(retryAfterMs);
|
||||
}
|
||||
const rateLimitWarning = formatDiscordDeployRateLimitWarning(err, accountId);
|
||||
if (rateLimitWarning) {
|
||||
params.runtime.log?.(warn(rateLimitWarning));
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
} catch (err) {
|
||||
const rateLimitWarning = formatDiscordDeployRateLimitWarning(err, accountId);
|
||||
params.runtime.log?.(
|
||||
warn(
|
||||
rateLimitWarning ??
|
||||
`discord: native slash command deploy warning (not message send): ${formatDiscordDeployErrorMessage(err)}${formatDiscordDeployErrorDetails(err)}`,
|
||||
`discord: native slash command deploy warning (not message send): ${formatDiscordDeployErrorMessage(err)}${formatDiscordDeployErrorDetails(err)}`,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
|
||||
@@ -81,7 +81,7 @@ vi.mock("./presence.js", () => ({
|
||||
}));
|
||||
|
||||
import { createDiscordRequestClient, DISCORD_REST_TIMEOUT_MS } from "../proxy-request-client.js";
|
||||
import { createDiscordMonitorClient } from "./provider.startup.js";
|
||||
import { createDiscordMonitorClient, fetchDiscordBotIdentity } from "./provider.startup.js";
|
||||
|
||||
describe("createDiscordMonitorClient", () => {
|
||||
beforeEach(() => {
|
||||
@@ -295,3 +295,28 @@ describe("createDiscordMonitorClient", () => {
|
||||
expect(createAutoPresenceControllerForTest).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetchDiscordBotIdentity", () => {
|
||||
it("derives the bot id from a Discord bot token without calling /users/@me", async () => {
|
||||
const fetchUser = vi.fn(async () => {
|
||||
throw new Error("network should not be used");
|
||||
});
|
||||
const logStartupPhase = vi.fn();
|
||||
const botId = "1477179610322964541";
|
||||
|
||||
await expect(
|
||||
fetchDiscordBotIdentity({
|
||||
client: { fetchUser } as never,
|
||||
token: `${Buffer.from(botId).toString("base64")}.GhIiP9.vU1xEpJ6NjFm`,
|
||||
runtime: { log: vi.fn(), error: vi.fn(), exit: vi.fn() },
|
||||
logStartupPhase,
|
||||
}),
|
||||
).resolves.toEqual({ botUserId: botId, botUserName: undefined });
|
||||
|
||||
expect(fetchUser).not.toHaveBeenCalled();
|
||||
expect(logStartupPhase).toHaveBeenCalledWith(
|
||||
"fetch-bot-identity:done",
|
||||
`botUserId=${botId} botUserName=<missing> source=token`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "../internal/discord.js";
|
||||
import type { GatewayPlugin } from "../internal/gateway.js";
|
||||
import { VoicePlugin } from "../internal/voice.js";
|
||||
import { parseApplicationIdFromToken } from "../probe.js";
|
||||
import { createDiscordRequestClient, DISCORD_REST_TIMEOUT_MS } from "../proxy-request-client.js";
|
||||
import type { DiscordGuildEntryResolved } from "./allow-list.js";
|
||||
import { createDiscordAutoPresenceController } from "./auto-presence.js";
|
||||
@@ -190,10 +191,20 @@ export async function createDiscordMonitorClient(params: {
|
||||
|
||||
export async function fetchDiscordBotIdentity(params: {
|
||||
client: Pick<Client, "fetchUser">;
|
||||
token?: string;
|
||||
runtime: RuntimeEnv;
|
||||
logStartupPhase: (phase: string, details?: string) => void;
|
||||
}) {
|
||||
params.logStartupPhase("fetch-bot-identity:start");
|
||||
const parsedBotUserId = parseApplicationIdFromToken(params.token ?? "");
|
||||
if (parsedBotUserId) {
|
||||
params.logStartupPhase(
|
||||
"fetch-bot-identity:done",
|
||||
`botUserId=${parsedBotUserId} botUserName=<missing> source=token`,
|
||||
);
|
||||
return { botUserId: parsedBotUserId, botUserName: undefined };
|
||||
}
|
||||
|
||||
let botUser: Awaited<ReturnType<typeof params.client.fetchUser>>;
|
||||
try {
|
||||
botUser = await params.client.fetchUser("@me");
|
||||
|
||||
@@ -879,7 +879,7 @@ describe("monitorDiscordProvider", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("logs repeated native command deploy rate limits as one concise warning", async () => {
|
||||
it("skips native command deploy retries after one rate limit warning", async () => {
|
||||
const runtime = baseRuntime();
|
||||
const rateLimitError = createRateLimitError(
|
||||
new Response(null, {
|
||||
@@ -898,7 +898,7 @@ describe("monitorDiscordProvider", () => {
|
||||
runtime,
|
||||
});
|
||||
|
||||
await vi.waitFor(() => expect(clientDeployCommandsMock).toHaveBeenCalledTimes(3));
|
||||
await vi.waitFor(() => expect(clientDeployCommandsMock).toHaveBeenCalledTimes(1));
|
||||
const warningMessages = vi
|
||||
.mocked(runtime.log)
|
||||
.mock.calls.map((call) => String(call[0]))
|
||||
@@ -1012,6 +1012,7 @@ describe("monitorDiscordProvider", () => {
|
||||
});
|
||||
|
||||
expect(fetchApplicationId).not.toHaveBeenCalled();
|
||||
expect(clientFetchUserMock).not.toHaveBeenCalled();
|
||||
expect(getConstructedClientOptions().clientId).toBe("123");
|
||||
});
|
||||
|
||||
|
||||
@@ -491,6 +491,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
>();
|
||||
let { botUserId, botUserName } = await fetchDiscordBotIdentity({
|
||||
client,
|
||||
token,
|
||||
runtime,
|
||||
logStartupPhase: (phase, details) =>
|
||||
logDiscordStartupPhase({
|
||||
|
||||
Reference in New Issue
Block a user