import { randomUUID } from "node:crypto"; import * as carbonGateway from "@buape/carbon/gateway"; import type { APIGatewayBotInfo } from "discord-api-types/v10"; import * as httpsProxyAgent from "https-proxy-agent"; import type { DiscordAccountConfig } from "openclaw/plugin-sdk/config-runtime"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import {
captureHttpExchange,
captureWsEvent,
resolveEffectiveDebugProxyUrl,
resolveDebugProxySettings,
} from "openclaw/plugin-sdk/proxy-capture"; import { danger } from "openclaw/plugin-sdk/runtime-env"; import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime"; import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import * as ws from "ws"; import { validateDiscordProxyUrl } from "../proxy-fetch.js"; import { DISCORD_GATEWAY_TRANSPORT_ACTIVITY_EVENT } from "./gateway-handle.js";
public override connect(resume = false): void { // Guard against stale heartbeat timers from the @buape/carbon // firstHeartbeatTimeout race (openclaw/openclaw#65009, #64011, #63387). // Parent connect() only calls stopHeartbeat() when isConnecting=false. // If isConnecting=true it returns early — leaving a stale setInterval // that fires with a closed reconnectCallback and crashes the process. if (this.heartbeatInterval !== undefined) {
clearInterval(this.heartbeatInterval); this.heartbeatInterval = undefined;
} if (this.firstHeartbeatTimeout !== undefined) {
clearTimeout(this.firstHeartbeatTimeout); this.firstHeartbeatTimeout = undefined;
} super.connect(resume);
}
override registerClient(client: Parameters<carbonGateway.GatewayPlugin["registerClient"]>[0]) { const registration = this.registerClientInternal(client); // Carbon 0.16 invokes async plugin hooks from Client construction without // awaiting them. Mark the promise handled immediately, then let OpenClaw // startup await the original promise explicitly.
registration.catch(() => {});
registrationPromises.set(this, registration); return registration;
}
private async registerClientInternal(
client: Parameters<carbonGateway.GatewayPlugin["registerClient"]>[0],
) { // Carbon's Client constructor does not await plugin registerClient(). // Match Carbon's own GatewayPlugin ordering by publishing the client // reference before our metadata fetch can yield, so an external // connect()->identify() cannot silently drop IDENTIFY (#52372).
assignCarbonGatewayClient(this, client);
if (!this.gatewayInfo || this.gatewayInfoUsedFallback) { const resolved = await fetchDiscordGatewayInfoWithTimeout({
token: client.options.token,
fetchImpl: params.fetchImpl,
fetchInit: params.fetchInit,
})
.then((info) => ({
info,
usedFallback: false,
}))
.catch((error) => resolveGatewayInfoWithFallback({ runtime: params.runtime, error })); this.gatewayInfo = resolved.info; this.gatewayInfoUsedFallback = resolved.usedFallback;
} if (params.testing?.registerClient) {
await params.testing.registerClient(this, client); return;
} // If the lifecycle timeout already started a socket while metadata was // loading, do not call Carbon's registerClient() again; it would close // that socket and open another one. Carbon stores these as runtime fields // even though they are protected/private in the .d.ts. if (hasCarbonGatewaySocketStarted(this)) { return;
} returnsuper.registerClient(client);
}
override createWebSocket(url: string) { if (!url) { thrownew Error("Gateway URL is required");
} const wsFlowId = randomUUID(); // Avoid Node's undici-backed global WebSocket here. We have seen late // close-path crashes during Discord gateway teardown; the ws transport is // already our proxy path and behaves predictably for lifecycle cleanup. const WebSocketCtor = params.testing?.webSocketCtor ?? ws.default; const socket = new WebSocketCtor(url, params.wsAgent ? { agent: params.wsAgent } : undefined); const emitTransportActivity = () => { if ((this as unknown as { ws?: unknown }).ws !== socket) { return;
} this.emitter.emit(DISCORD_GATEWAY_TRANSPORT_ACTIVITY_EVENT, { at: Date.now() });
};
captureWsEvent({
url,
direction: "local",
kind: "ws-open",
flowId: wsFlowId,
meta: { subsystem: "discord-gateway" },
});
socket.on?.("message", (data: unknown) => {
emitTransportActivity();
captureWsEvent({
url,
direction: "inbound",
kind: "ws-frame",
flowId: wsFlowId,
payload: Buffer.isBuffer(data) ? data : Buffer.from(String(data)),
meta: { subsystem: "discord-gateway" },
});
});
socket.on?.("close", (code: number, reason: Buffer) => {
captureWsEvent({
url,
direction: "local",
kind: "ws-close",
flowId: wsFlowId,
closeCode: code,
payload: reason,
meta: { subsystem: "discord-gateway" },
});
});
socket.on?.("error", (error: Error) => {
captureWsEvent({
url,
direction: "local",
kind: "error",
flowId: wsFlowId,
errorText: error.message,
meta: { subsystem: "discord-gateway" },
});
}); if ("binaryType" in socket) { try {
socket.binaryType = "arraybuffer";
} catch { // Ignore runtimes that expose a readonly binaryType.
}
} return socket;
}
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.