diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ded4891c4f..6c487db8966 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/docs/.generated/config-baseline.sha256 b/docs/.generated/config-baseline.sha256 index c93d66d7b0d..4fe359e6331 100644 --- a/docs/.generated/config-baseline.sha256 +++ b/docs/.generated/config-baseline.sha256 @@ -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 diff --git a/docs/channels/discord.md b/docs/channels/discord.md index 3c835775764..ddb41ac5255 100644 --- a/docs/channels/discord.md +++ b/docs/channels/discord.md @@ -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. diff --git a/extensions/discord/src/config-schema.test.ts b/extensions/discord/src/config-schema.test.ts index c5b1fc275c9..c06a2c72585 100644 --- a/extensions/discord/src/config-schema.test.ts +++ b/extensions/discord/src/config-schema.test.ts @@ -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, diff --git a/extensions/discord/src/config-ui-hints.ts b/extensions/discord/src/config-ui-hints.ts index bb6055f3a62..48dd76dc98c 100644 --- a/extensions/discord/src/config-ui-hints.ts +++ b/extensions/discord/src/config-ui-hints.ts @@ -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; diff --git a/extensions/discord/src/monitor/provider.test.ts b/extensions/discord/src/monitor/provider.test.ts index 5b01eba4e0b..50636f1338d 100644 --- a/extensions/discord/src/monitor/provider.test.ts +++ b/extensions/discord/src/monitor/provider.test.ts @@ -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) => diff --git a/extensions/discord/src/monitor/provider.ts b/extensions/discord/src/monitor/provider.ts index 1284fe96a09..1e63bfa8f30 100644 --- a/extensions/discord/src/monitor/provider.ts +++ b/extensions/discord/src/monitor/provider.ts @@ -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)( diff --git a/src/config/bundled-channel-config-metadata.generated.ts b/src/config/bundled-channel-config-metadata.generated.ts index 73370f35aad..fbd2d40cec9 100644 --- a/src/config/bundled-channel-config-metadata.generated.ts +++ b/src/config/bundled-channel-config-metadata.generated.ts @@ -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", diff --git a/src/config/types.discord.ts b/src/config/types.discord.ts index 75aa99db887..65446c8753f 100644 --- a/src/config/types.discord.ts +++ b/src/config/types.discord.ts @@ -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. */ diff --git a/src/config/zod-schema.providers-core.ts b/src/config/zod-schema.providers-core.ts index 7c145941139..d8c1b879160 100644 --- a/src/config/zod-schema.providers-core.ts +++ b/src/config/zod-schema.providers-core.ts @@ -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(),