fix(discord): allow configured application ids

This commit is contained in:
Peter Steinberger
2026-04-29 19:40:09 +01:00
parent a81e3ee888
commit 485b875d72
10 changed files with 74 additions and 4 deletions

View File

@@ -60,7 +60,7 @@ Docs: https://docs.openclaw.ai
- Ollama: keep explicit local model runs on target-provider runtime hooks when PI discovery is skipped, so one-shot Ollama calls no longer cold-load unrelated provider runtimes before streaming. Fixes #74078. Thanks @sakalaboator.
- Slack/prompts: rely on Slack `interactiveReplies` guidance instead of generic `inlineButtons` config hints so enabled Slack button directives are not contradicted. Fixes #46647. Thanks @jeremykoerber.
- Slack/reactions: treat duplicate `already_reacted` responses as idempotent success so repeated agent reaction adds no longer surface as tool failures. Fixes #69005. Thanks @shipitsteven and @martingarramon.
- Channels/Discord: cool down Cloudflare/Error 1015 HTML 429 REST failures during startup application lookup and gateway metadata fetches, sanitizing HTML bodies before logging and honoring Retry-After before falling back to a conservative cooldown. Fixes #38853. Thanks @djgeorg3 and @Garyko0730.
- Channels/Discord: cool down Cloudflare/Error 1015 HTML 429 REST failures during startup application lookup and gateway metadata fetches, add `channels.discord.applicationId` as an app-id lookup bypass, sanitize HTML bodies before logging, and honor Retry-After before falling back to a conservative cooldown. Fixes #38853. Thanks @djgeorg3 and @Garyko0730.
- Slack/tools: expose `fileId` in the shared message tool schema so `download-file` can receive Slack attachment IDs from inbound placeholders. Fixes #45574. Thanks @chadvegas.
- Exec: reject invalid per-call `host` values instead of silently falling back to the default target, so hostname-like values fail before commands run. Fixes #74426. Thanks @scr00ge-00 and @vyctorbrzezowski.
- Google/Gemini: send non-empty placeholder content when a Gemini run is triggered with empty or filtered user content, avoiding `contents is not specified` API errors. Thanks @CaoYuhaoCarl.

View File

@@ -1,4 +1,4 @@
b095536aeeb273b029a7a96cd8406cbcbc315dfd67dc45f469782ce1ccbc9e08 config-baseline.json
7bf720f6d9040c53323553b1bd351f688137c6b352c4cf2acfd7f7d252644b38 config-baseline.json
ab9a004ec78ed51e646be29eb10aa6700de1d47fee77331a85ca5e2cd15b6e93 config-baseline.core.json
fab66aa304db5697e87259165ad261006719eb6e6cdbd25f957fcba2b7b324e9 config-baseline.channel.json
92712871defa92eeda8161b516db85574681f2b70678b940508a808b987aeae2 config-baseline.channel.json
c4231c2194206547af8ad94342dc00aadb734f43cb49cc79d4c46bdbb80c3f95 config-baseline.plugin.json

View File

@@ -115,6 +115,7 @@ openclaw gateway
If OpenClaw is already running as a background service, restart it via the OpenClaw Mac app or by stopping and restarting the `openclaw gateway run` process.
For managed service installs, run `openclaw gateway install` from a shell where `DISCORD_BOT_TOKEN` is present, or store the variable in `~/.openclaw/.env`, so the service can resolve the env SecretRef after restart.
If your host is blocked or rate-limited by Discord's startup application lookup, set `channels.discord.applicationId` to the application's client ID from the Developer Portal so startup can skip that REST call.
</Step>

View File

@@ -76,6 +76,30 @@ describe("discord config schema", () => {
expect(cfg.historyLimit).toBe(3);
});
it("accepts Discord application IDs at top-level and account scope", () => {
const cfg = expectValidDiscordConfig({
applicationId: "123456789012345678",
accounts: {
work: {
applicationId: 234567890123456,
},
},
});
expect(cfg.applicationId).toBe("123456789012345678");
expect(cfg.accounts?.work?.applicationId).toBe("234567890123456");
});
it("rejects unsafe numeric Discord application IDs", () => {
const issues = expectInvalidDiscordConfig({
applicationId: 106232522769186816,
});
expect(
issues.some((issue) => issue.message.includes("not a valid non-negative safe integer")),
).toBe(true);
});
it("loads guild map and dm group settings", () => {
const cfg = expectValidDiscordConfig({
enabled: true,

View File

@@ -222,4 +222,8 @@ export const discordChannelConfigUiHints = {
help: "Discord bot token used for gateway and REST API authentication for this provider account. Keep this secret out of committed config and rotate immediately after any leak.",
sensitive: true,
},
applicationId: {
label: "Discord Application ID",
help: "Optional Discord application/client ID. Set this when hosted environments cannot reach Discord's application lookup endpoint during startup.",
},
} satisfies Record<string, ChannelConfigUiHint>;

View File

@@ -887,6 +887,30 @@ describe("monitorDiscordProvider", () => {
expect(getConstructedClientOptions().clientId).toBe("123");
});
it("uses configured application id before token parsing or REST lookup", async () => {
const fetchApplicationId = vi.fn(async () => "network-app");
providerTesting.setFetchDiscordApplicationId(fetchApplicationId);
resolveDiscordAccountMock.mockReturnValue({
accountId: "default",
token: "MTIz.abc.def",
config: {
applicationId: "987654321098765432",
commands: { native: true, nativeSkills: false },
voice: { enabled: false },
agentComponents: { enabled: false },
execApprovals: { enabled: false },
},
});
await monitorDiscordProvider({
config: baseConfig(),
runtime: baseRuntime(),
});
expect(fetchApplicationId).not.toHaveBeenCalled();
expect(getConstructedClientOptions().clientId).toBe("987654321098765432");
});
it("reports connected status on startup and shutdown", async () => {
const setStatus = vi.fn();
clientGetPluginMock.mockImplementation((name: string) =>

View File

@@ -317,7 +317,11 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
phase: "fetch-application-id:start",
startAt: startupStartedAt,
});
const parsedApplicationId = parseApplicationIdFromToken(token);
const configuredApplicationId =
typeof discordCfg.applicationId === "string" && discordCfg.applicationId.trim()
? discordCfg.applicationId.trim()
: undefined;
const parsedApplicationId = configuredApplicationId ?? parseApplicationIdFromToken(token);
const applicationId =
parsedApplicationId ??
(await (fetchDiscordApplicationIdForTesting ?? fetchDiscordApplicationId)(

View File

@@ -789,6 +789,9 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
},
],
},
applicationId: {
type: "string",
},
proxy: {
type: "string",
},
@@ -2152,6 +2155,9 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
},
],
},
applicationId: {
type: "string",
},
proxy: {
type: "string",
},
@@ -3622,6 +3628,10 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
help: "Discord bot token used for gateway and REST API authentication for this provider account. Keep this secret out of committed config and rotate immediately after any leak.",
sensitive: true,
},
applicationId: {
label: "Discord Application ID",
help: "Optional Discord application/client ID. Set this when hosted environments cannot reach Discord's application lookup endpoint during startup.",
},
},
unsupportedSecretRefSurfacePatterns: [
"channels.discord.accounts.*.threadBindings.webhookToken",

View File

@@ -241,6 +241,8 @@ export type DiscordAccountConfig = {
/** If false, do not start this Discord account. Default: true. */
enabled?: boolean;
token?: SecretInput;
/** Optional Discord application/client ID. Set this when REST application lookup is blocked. */
applicationId?: string;
/** HTTP(S) proxy URL for Discord gateway WebSocket connections. */
proxy?: string;
/** Timeout for Discord /gateway/bot metadata lookup before falling back to the default gateway URL. Default: 30000. */

View File

@@ -527,6 +527,7 @@ export const DiscordAccountSchema = z
commands: ProviderCommandsSchema,
configWrites: z.boolean().optional(),
token: SecretInputSchema.optional().register(sensitive),
applicationId: DiscordIdSchema.optional(),
proxy: z.string().optional(),
gatewayInfoTimeoutMs: z.number().int().positive().max(120_000).optional(),
allowBots: z.union([z.boolean(), z.literal("mentions")]).optional(),