Merge cb4d03bb868c02e11f40b7ace1027ef895b21da0 into 5e417b44e1540f528d2ae63e3e20229a902d1db2

This commit is contained in:
winter-loo 2026-03-20 18:52:19 -07:00 committed by GitHub
commit 315b877001
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 35 additions and 12 deletions

View File

@ -6,6 +6,7 @@ import { danger } from "openclaw/plugin-sdk/runtime-env";
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
import { ProxyAgent, fetch as undiciFetch } from "undici";
import WebSocket from "ws";
import { resolveEnvHttpProxyUrl } from "../../../../src/infra/net/proxy-env.js";
const DISCORD_GATEWAY_BOT_URL = "https://discord.com/api/v10/gateway/bot";
const DEFAULT_DISCORD_GATEWAY_URL = "wss://gateway.discord.gg/";
@ -270,7 +271,7 @@ export function createDiscordGatewayPlugin(params: {
runtime: RuntimeEnv;
}): GatewayPlugin {
const intents = resolveDiscordGatewayIntents(params.discordConfig?.intents);
const proxy = params.discordConfig?.proxy?.trim();
const proxy = params.discordConfig?.proxy?.trim() || resolveEnvHttpProxyUrl("https");
const options = {
reconnect: { maxAttempts: 50 },
intents,
@ -287,7 +288,7 @@ export function createDiscordGatewayPlugin(params: {
try {
const wsAgent = new HttpsProxyAgent<string>(proxy);
const fetchAgent = new ProxyAgent(proxy);
const fetchAgent = new ProxyAgent({ uri: proxy, connect: { keepAlive: false } });
params.runtime.log?.("discord: gateway proxy enabled");

View File

@ -89,7 +89,8 @@ vi.mock("https-proxy-agent", () => ({
vi.mock("undici", () => ({
ProxyAgent: class {
proxyUrl: string;
constructor(proxyUrl: string) {
constructor(opts: string | { uri: string }) {
const proxyUrl = typeof opts === "string" ? opts : opts.uri;
this.proxyUrl = proxyUrl;
undiciProxyAgentSpy(proxyUrl);
restProxyAgentSpy(proxyUrl);
@ -98,6 +99,10 @@ vi.mock("undici", () => ({
fetch: undiciFetchMock,
}));
vi.mock("../../../../src/infra/net/proxy-env.js", () => ({
resolveEnvHttpProxyUrl: () => undefined,
}));
vi.mock("ws", () => ({
default: class MockWebSocket {
constructor(url: string, options?: { agent?: unknown }) {

View File

@ -9,7 +9,8 @@ const { undiciFetchMock, proxyAgentSpy } = vi.hoisted(() => ({
vi.mock("undici", () => {
class ProxyAgent {
proxyUrl: string;
constructor(proxyUrl: string) {
constructor(opts: string | { uri: string }) {
const proxyUrl = typeof opts === "string" ? opts : opts.uri;
if (proxyUrl === "bad-proxy") {
throw new Error("bad proxy");
}

View File

@ -12,7 +12,7 @@ export function resolveDiscordRestFetch(
return fetch;
}
try {
const agent = new ProxyAgent(proxy);
const agent = new ProxyAgent({ uri: proxy, connect: { keepAlive: false } });
const fetcher = ((input: RequestInfo | URL, init?: RequestInit) =>
undiciFetch(input as string | URL, {
...(init as Record<string, unknown>),

View File

@ -17,6 +17,10 @@ import type { GatewayWsLogStyle } from "../../gateway/ws-logging.js";
import { setGatewayWsLogStyle } from "../../gateway/ws-logging.js";
import { setVerbose } from "../../globals.js";
import { GatewayLockError } from "../../infra/gateway-lock.js";
import {
ensureGlobalUndiciEnvProxyDispatcher,
ensureGlobalUndiciStreamTimeouts,
} from "../../infra/net/undici-global-dispatcher.js";
import { formatPortDiagnostics, inspectPortUsage } from "../../infra/ports.js";
import { cleanStaleGatewayProcessesSync } from "../../infra/restart-stale-pids.js";
import { setConsoleSubsystemFilter, setConsoleTimestampPrefix } from "../../logging/console.js";
@ -418,6 +422,11 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
}
: undefined;
// Proxy bootstrap must happen before timeout tuning so the timeouts wrap the
// active EnvHttpProxyAgent instead of being replaced by a bare proxy dispatcher.
ensureGlobalUndiciEnvProxyDispatcher();
ensureGlobalUndiciStreamTimeouts();
try {
await runGatewayLoop({
runtime: defaultRuntime,

View File

@ -205,8 +205,9 @@ export function isNodeCommandAllowed(params: {
if (!params.allowlist.has(command)) {
return { ok: false, reason: "command not allowlisted" };
}
if (Array.isArray(params.declaredCommands) && params.declaredCommands.length > 0) {
if (!params.declaredCommands.includes(command)) {
const { declaredCommands } = params;
if (Array.isArray(declaredCommands) && declaredCommands.length > 0) {
if (!declaredCommands.includes(command)) {
return { ok: false, reason: "command not declared by node" };
}
} else {

View File

@ -1003,15 +1003,16 @@ export const nodeHandlers: GatewayRequestHandlers = {
`node wake done node=${nodeId} req=${wakeReqId} connected=true totalMs=${totalDurationMs}`,
);
}
const session = nodeSession;
const cfg = loadConfig();
const allowlist = resolveNodeCommandAllowlist(cfg, nodeSession);
const allowlist = resolveNodeCommandAllowlist(cfg, session);
const allowed = isNodeCommandAllowed({
command,
declaredCommands: nodeSession.commands,
declaredCommands: session.commands,
allowlist,
});
if (!allowed.ok) {
const hint = buildNodeCommandRejectionHint(allowed.reason, command, nodeSession);
const hint = buildNodeCommandRejectionHint(allowed.reason, command, session);
respond(
false,
undefined,

View File

@ -111,6 +111,7 @@ describe("ensureGlobalUndiciStreamTimeouts", () => {
expect(next.options?.bodyTimeout).toBe(DEFAULT_UNDICI_STREAM_TIMEOUT_MS);
expect(next.options?.headersTimeout).toBe(DEFAULT_UNDICI_STREAM_TIMEOUT_MS);
expect(next.options?.connect).toEqual({
keepAlive: false,
autoSelectFamily: false,
autoSelectFamilyAttemptTimeout: 300,
});

View File

@ -93,7 +93,10 @@ export function ensureGlobalUndiciEnvProxyDispatcher(): void {
return;
}
try {
setGlobalDispatcher(new EnvHttpProxyAgent());
// Many local proxies (Clash, Surge, etc.) close HTTP CONNECT tunnels after
// the first request, causing ECONNRESET on reuse. Disable keep-alive so
// undici opens a fresh tunnel per request instead of pooling.
setGlobalDispatcher(new EnvHttpProxyAgent({ connect: { keepAlive: false } }));
lastAppliedProxyBootstrap = true;
} catch {
// Best-effort bootstrap only.
@ -120,10 +123,11 @@ export function ensureGlobalUndiciStreamTimeouts(opts?: { timeoutMs?: number }):
const connect = resolveConnectOptions(autoSelectFamily);
try {
if (kind === "env-proxy") {
const proxyConnect = { ...connect, keepAlive: false };
const proxyOptions = {
bodyTimeout: timeoutMs,
headersTimeout: timeoutMs,
...(connect ? { connect } : {}),
connect: proxyConnect,
} as ConstructorParameters<typeof EnvHttpProxyAgent>[0];
setGlobalDispatcher(new EnvHttpProxyAgent(proxyOptions));
} else {