fix(discord): apply proxy to app-id and allowlist REST lookups

This commit is contained in:
尹凯
2026-02-16 18:23:49 +08:00
committed by Peter Steinberger
parent bc2e02bb34
commit e997545d4b
3 changed files with 92 additions and 2 deletions

View File

@@ -318,7 +318,7 @@ export const FIELD_HELP: Record<string, string> = {
"channels.discord.configWrites":
"Allow Discord to write config in response to channel events/commands (default: true).",
"channels.discord.proxy":
"Proxy URL for Discord gateway WebSocket connections. Set per account via channels.discord.accounts.<id>.proxy.",
"Proxy URL for Discord gateway + API requests (app-id lookup and allowlist resolution). Set per account via channels.discord.accounts.<id>.proxy.",
"channels.whatsapp.configWrites":
"Allow WhatsApp to write config in response to channel events/commands (default: true).",
"channels.signal.configWrites":

View File

@@ -0,0 +1,63 @@
import { describe, expect, it, vi } from "vitest";
const { undiciFetchMock, proxyAgentSpy } = vi.hoisted(() => ({
undiciFetchMock: vi.fn(),
proxyAgentSpy: vi.fn(),
}));
vi.mock("undici", () => {
class ProxyAgent {
proxyUrl: string;
constructor(proxyUrl: string) {
if (proxyUrl === "bad-proxy") {
throw new Error("bad proxy");
}
this.proxyUrl = proxyUrl;
proxyAgentSpy(proxyUrl);
}
}
return {
ProxyAgent,
fetch: undiciFetchMock,
};
});
describe("resolveDiscordRestFetch", () => {
it("uses undici proxy fetch when a proxy URL is configured", async () => {
const runtime = {
log: vi.fn(),
error: vi.fn(),
} as const;
undiciFetchMock.mockReset().mockResolvedValue(new Response("ok", { status: 200 }));
proxyAgentSpy.mockReset();
const { __testing } = await import("./provider.js");
const fetcher = __testing.resolveDiscordRestFetch("http://proxy.test:8080", runtime);
await fetcher("https://discord.com/api/v10/oauth2/applications/@me");
expect(proxyAgentSpy).toHaveBeenCalledWith("http://proxy.test:8080");
expect(undiciFetchMock).toHaveBeenCalledWith(
"https://discord.com/api/v10/oauth2/applications/@me",
expect.objectContaining({
dispatcher: expect.objectContaining({ proxyUrl: "http://proxy.test:8080" }),
}),
);
expect(runtime.log).toHaveBeenCalledWith("discord: rest proxy enabled");
expect(runtime.error).not.toHaveBeenCalled();
});
it("falls back to global fetch when proxy URL is invalid", async () => {
const runtime = {
log: vi.fn(),
error: vi.fn(),
} as const;
const { __testing } = await import("./provider.js");
const fetcher = __testing.resolveDiscordRestFetch("bad-proxy", runtime);
expect(fetcher).toBe(fetch);
expect(runtime.error).toHaveBeenCalled();
expect(runtime.log).not.toHaveBeenCalled();
});
});

View File

@@ -7,6 +7,7 @@ import {
} from "@buape/carbon";
import { Routes } from "discord-api-types/v10";
import { inspect } from "node:util";
import { ProxyAgent, fetch as undiciFetch } from "undici";
import type { HistoryEntry } from "../../auto-reply/reply/history.js";
import type { OpenClawConfig, ReplyToMode } from "../../config/config.js";
import { resolveTextChunkLimit } from "../../auto-reply/chunk.js";
@@ -31,6 +32,7 @@ import { formatErrorMessage } from "../../infra/errors.js";
import { createDiscordRetryRunner } from "../../infra/retry-policy.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import { createNonExitingRuntime, type RuntimeEnv } from "../../runtime.js";
import { wrapFetchWithAbortSignal } from "../../infra/fetch.js";
import { resolveDiscordAccount } from "../accounts.js";
import { attachDiscordGatewayLogging } from "../gateway-logging.js";
import { getDiscordGatewayEmitter, waitForDiscordGatewayStop } from "../monitor.gateway.js";
@@ -116,6 +118,26 @@ function dedupeSkillCommandsForDiscord(
return deduped;
}
function resolveDiscordRestFetch(proxyUrl: string | undefined, runtime: RuntimeEnv): typeof fetch {
const proxy = proxyUrl?.trim();
if (!proxy) {
return fetch;
}
try {
const agent = new ProxyAgent(proxy);
const fetcher = ((input: RequestInfo | URL, init?: RequestInit) =>
undiciFetch(input as string | URL, {
...(init as Record<string, unknown>),
dispatcher: agent,
}) as unknown as Promise<Response>) as typeof fetch;
runtime.log?.("discord: rest proxy enabled");
return wrapFetchWithAbortSignal(fetcher);
} catch (err) {
runtime.error?.(danger(`discord: invalid rest proxy: ${String(err)}`));
return fetch;
}
}
async function deployDiscordCommands(params: {
client: Client;
runtime: RuntimeEnv;
@@ -182,6 +204,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
const runtime: RuntimeEnv = opts.runtime ?? createNonExitingRuntime();
const discordCfg = account.config;
const discordRestFetch = resolveDiscordRestFetch(discordCfg.proxy, runtime);
const dmConfig = discordCfg.dm;
let guildEntries = discordCfg.guilds;
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
@@ -257,6 +280,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
const resolved = await resolveDiscordChannelAllowlist({
token,
entries: entries.map((entry) => entry.input),
fetcher: discordRestFetch,
});
const nextGuilds = { ...guildEntries };
const mapping: string[] = [];
@@ -313,6 +337,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
const resolvedUsers = await resolveDiscordUserAllowlist({
token,
entries: allowEntries.map((entry) => String(entry)),
fetcher: discordRestFetch,
});
const { mapping, unresolved, additions } = buildAllowlistResolutionSummary(resolvedUsers);
allowFrom = mergeAllowlist({ existing: allowFrom, additions });
@@ -342,6 +367,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
const resolvedUsers = await resolveDiscordUserAllowlist({
token,
entries: Array.from(userEntries),
fetcher: discordRestFetch,
});
const { resolvedMap, mapping, unresolved } =
buildAllowlistResolutionSummary(resolvedUsers);
@@ -383,7 +409,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
);
}
const applicationId = await fetchDiscordApplicationId(token, 4000);
const applicationId = await fetchDiscordApplicationId(token, 4000, discordRestFetch);
if (!applicationId) {
throw new Error("Failed to resolve Discord application id");
}
@@ -689,4 +715,5 @@ async function clearDiscordNativeCommands(params: {
export const __testing = {
createDiscordGatewayPlugin,
dedupeSkillCommandsForDiscord,
resolveDiscordRestFetch,
};