fix(discord): proxy Carbon REST, webhook and monitor fetch paths; stagger multi-bot startup

This commit is contained in:
geekhuashan
2026-03-31 09:18:30 +08:00
committed by Peter Steinberger
parent dfb423532b
commit c8223606ca
5 changed files with 82 additions and 4 deletions

View File

@@ -18,6 +18,7 @@ import {
resolveOutboundSendDep,
} from "openclaw/plugin-sdk/outbound-runtime";
import { normalizeMessageChannel } from "openclaw/plugin-sdk/routing";
import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
import {
createComputedAccountStatusAdapter,
createDefaultChannelRuntimeState,
@@ -86,6 +87,19 @@ async function loadDiscordProbeRuntime() {
const meta = getChatChannelMeta("discord");
const REQUIRED_DISCORD_PERMISSIONS = ["ViewChannel", "SendMessages"] as const;
const DISCORD_ACCOUNT_STARTUP_STAGGER_MS = 10_000;
function resolveDiscordStartupDelayMs(cfg: OpenClawConfig, accountId: string): number {
const startupAccountIds = listDiscordAccountIds(cfg).filter((candidateId) => {
const candidate = resolveDiscordAccount({ cfg, accountId: candidateId });
return (
candidate.enabled &&
(resolveConfiguredFromCredentialStatuses(candidate) ?? Boolean(candidate.token.trim()))
);
});
const startupIndex = startupAccountIds.findIndex((candidateId) => candidateId === accountId);
return startupIndex <= 0 ? 0 : startupIndex * DISCORD_ACCOUNT_STARTUP_STAGGER_MS;
}
const resolveDiscordDmPolicy = createScopedDmSecurityResolver<ResolvedDiscordAccount>({
channelKey: "discord",
@@ -561,6 +575,17 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount, DiscordProbe>
gateway: {
startAccount: async (ctx) => {
const account = ctx.account;
const startupDelayMs = resolveDiscordStartupDelayMs(ctx.cfg, account.accountId);
if (startupDelayMs > 0) {
ctx.log?.info(
`[${account.accountId}] delaying provider startup ${Math.round(startupDelayMs / 1000)}s to reduce Discord startup rate limits`,
);
try {
await sleepWithAbort(startupDelayMs, ctx.abortSignal);
} catch {
return;
}
}
const token = account.token.trim();
let discordBotLabel = "";
try {

View File

@@ -1,5 +1,6 @@
import { RequestClient } from "@buape/carbon";
import { loadConfig } from "openclaw/plugin-sdk/config-runtime";
import { makeProxyFetch } from "openclaw/plugin-sdk/infra-runtime";
import type { RetryConfig, RetryRunner } from "openclaw/plugin-sdk/retry-runtime";
import { normalizeAccountId } from "openclaw/plugin-sdk/routing";
import {
@@ -29,8 +30,53 @@ function resolveToken(params: { accountId: string; fallbackToken?: string }) {
return fallback;
}
function resolveRest(token: string, rest?: RequestClient) {
return rest ?? new RequestClient(token);
function resolveDiscordProxyUrl(
account: Pick<ResolvedDiscordAccount, "config">,
cfg?: ReturnType<typeof loadConfig>,
): string | undefined {
const accountProxy = account.config.proxy?.trim();
if (accountProxy) {
return accountProxy;
}
const channelProxy = cfg?.channels?.discord?.proxy;
if (typeof channelProxy !== "string") {
return undefined;
}
const trimmed = channelProxy.trim();
return trimmed || undefined;
}
export function resolveDiscordProxyFetchForAccount(
account: Pick<ResolvedDiscordAccount, "config">,
cfg?: ReturnType<typeof loadConfig>,
): typeof fetch | undefined {
const proxy = resolveDiscordProxyUrl(account, cfg);
return proxy ? makeProxyFetch(proxy) : undefined;
}
export function resolveDiscordProxyFetch(
opts: Pick<DiscordClientOpts, "cfg" | "accountId">,
cfg?: ReturnType<typeof loadConfig>,
): typeof fetch | undefined {
const resolvedCfg = opts.cfg ?? cfg ?? loadConfig();
const account = resolveAccountWithoutToken({
cfg: resolvedCfg,
accountId: opts.accountId,
});
return resolveDiscordProxyFetchForAccount(account, resolvedCfg);
}
function resolveRest(
token: string,
account: ResolvedDiscordAccount,
cfg: ReturnType<typeof loadConfig>,
rest?: RequestClient,
) {
if (rest) {
return rest;
}
const proxyFetch = resolveDiscordProxyFetchForAccount(account, cfg);
return new RequestClient(token, proxyFetch ? { fetch: proxyFetch } : undefined);
}
function resolveAccountWithoutToken(params: {
@@ -66,7 +112,7 @@ export function createDiscordRestClient(
accountId: account.accountId,
fallbackToken: account.token,
});
const rest = resolveRest(token, opts.rest);
const rest = resolveRest(token, account, resolvedCfg, opts.rest);
return { token, rest, account };
}

View File

@@ -70,6 +70,7 @@ export function createDiscordMonitorClient(params: {
accountId: string;
applicationId: string;
token: string;
proxyFetch?: typeof fetch;
commands: BaseCommand[];
components: BaseMessageInteractiveComponent[];
modals: Modal[];
@@ -115,6 +116,7 @@ export function createDiscordMonitorClient(params: {
token: params.token,
autoDeploy: false,
eventQueue: eventQueueOpts,
...(params.proxyFetch ? { requestOptions: { fetch: params.proxyFetch } } : {}),
},
{
commands: params.commands,

View File

@@ -42,6 +42,7 @@ import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime";
import { summarizeStringEntries } from "openclaw/plugin-sdk/text-runtime";
import { resolveDiscordAccount } from "../accounts.js";
import { isDiscordExecApprovalClientEnabled } from "../exec-approvals.js";
import { resolveDiscordProxyFetchForAccount } from "../client.js";
import { fetchDiscordApplicationId } from "../probe.js";
import { normalizeDiscordToken } from "../token.js";
import { createDiscordVoiceCommand } from "../voice/command.js";
@@ -592,6 +593,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
const discordAccountThreadBindings =
cfg.channels?.discord?.accounts?.[account.accountId]?.threadBindings;
const discordRestFetch = resolveDiscordRestFetch(rawDiscordCfg.proxy, runtime);
const discordProxyFetch = resolveDiscordProxyFetchForAccount(account, cfg);
const dmConfig = rawDiscordCfg.dm;
let guildEntries = rawDiscordCfg.guilds;
const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
@@ -903,6 +905,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
accountId: account.accountId,
applicationId,
token,
proxyFetch: discordProxyFetch,
commands,
components,
modals,

View File

@@ -16,6 +16,7 @@ import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
import { convertMarkdownTables } from "openclaw/plugin-sdk/text-runtime";
import { loadWebMediaRaw } from "openclaw/plugin-sdk/web-media";
import { resolveDiscordAccount } from "./accounts.js";
import { resolveDiscordProxyFetch } from "./client.js";
import { rewriteDiscordKnownMentions } from "./mentions.js";
import {
buildDiscordMessagePayload,
@@ -369,8 +370,9 @@ export async function sendWebhookMessageDiscord(
});
const replyTo = typeof opts.replyTo === "string" ? opts.replyTo.trim() : "";
const messageReference = replyTo ? { message_id: replyTo, fail_if_not_exists: false } : undefined;
const fetchImpl = resolveDiscordProxyFetch({ cfg: opts.cfg, accountId: opts.accountId });
const response = await fetch(
const response = await (fetchImpl ?? fetch)(
resolveWebhookExecutionUrl({
webhookId,
webhookToken,