mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-13 21:50:45 +00:00
Reduce WebUI/Gateway latency churn by avoiding redundant session reloads, carrying session keys through transcript update events, and deferring explicit media provider discovery. Includes changelog attribution and closes the referenced runtime latency issues.
190 lines
7.2 KiB
TypeScript
190 lines
7.2 KiB
TypeScript
import { withPluginRuntimeGatewayRequestScope } from "../plugins/runtime/gateway-request-scope.js";
|
|
import { formatControlPlaneActor, resolveControlPlaneActor } from "./control-plane-audit.js";
|
|
import { consumeControlPlaneWriteBudget } from "./control-plane-rate-limit.js";
|
|
import { ADMIN_SCOPE, authorizeOperatorScopesForMethod } from "./method-scopes.js";
|
|
import { ErrorCodes, errorShape } from "./protocol/index.js";
|
|
import {
|
|
gatewayStartupUnavailableDetails,
|
|
GATEWAY_STARTUP_RETRY_AFTER_MS,
|
|
} from "./protocol/startup-unavailable.js";
|
|
import { isRoleAuthorizedForMethod, parseGatewayRole } from "./role-policy.js";
|
|
import { agentHandlers } from "./server-methods/agent.js";
|
|
import { agentsHandlers } from "./server-methods/agents.js";
|
|
import { artifactsHandlers } from "./server-methods/artifacts.js";
|
|
import { channelsHandlers } from "./server-methods/channels.js";
|
|
import { chatHandlers } from "./server-methods/chat.js";
|
|
import { commandsHandlers } from "./server-methods/commands.js";
|
|
import { configHandlers } from "./server-methods/config.js";
|
|
import { connectHandlers } from "./server-methods/connect.js";
|
|
import { cronHandlers } from "./server-methods/cron.js";
|
|
import { deviceHandlers } from "./server-methods/devices.js";
|
|
import { diagnosticsHandlers } from "./server-methods/diagnostics.js";
|
|
import { doctorHandlers } from "./server-methods/doctor.js";
|
|
import { execApprovalsHandlers } from "./server-methods/exec-approvals.js";
|
|
import { healthHandlers } from "./server-methods/health.js";
|
|
import { logsHandlers } from "./server-methods/logs.js";
|
|
import { modelsAuthStatusHandlers } from "./server-methods/models-auth-status.js";
|
|
import { modelsHandlers } from "./server-methods/models.js";
|
|
import { nativeHookRelayHandlers } from "./server-methods/native-hook-relay.js";
|
|
import { nodePendingHandlers } from "./server-methods/nodes-pending.js";
|
|
import { nodeHandlers } from "./server-methods/nodes.js";
|
|
import { pluginHostHookHandlers } from "./server-methods/plugin-host-hooks.js";
|
|
import { pushHandlers } from "./server-methods/push.js";
|
|
import { sendHandlers } from "./server-methods/send.js";
|
|
import { sessionsHandlers } from "./server-methods/sessions.js";
|
|
import { skillsHandlers } from "./server-methods/skills.js";
|
|
import { systemHandlers } from "./server-methods/system.js";
|
|
import { talkHandlers } from "./server-methods/talk.js";
|
|
import { toolsCatalogHandlers } from "./server-methods/tools-catalog.js";
|
|
import { toolsEffectiveHandlers } from "./server-methods/tools-effective.js";
|
|
import { toolsInvokeHandlers } from "./server-methods/tools-invoke.js";
|
|
import { ttsHandlers } from "./server-methods/tts.js";
|
|
import type { GatewayRequestHandlers, GatewayRequestOptions } from "./server-methods/types.js";
|
|
import { updateHandlers } from "./server-methods/update.js";
|
|
import { usageHandlers } from "./server-methods/usage.js";
|
|
import { voicewakeRoutingHandlers } from "./server-methods/voicewake-routing.js";
|
|
import { voicewakeHandlers } from "./server-methods/voicewake.js";
|
|
import { webHandlers } from "./server-methods/web.js";
|
|
import { wizardHandlers } from "./server-methods/wizard.js";
|
|
|
|
const CONTROL_PLANE_WRITE_METHODS = new Set(["config.apply", "config.patch", "update.run"]);
|
|
function authorizeGatewayMethod(method: string, client: GatewayRequestOptions["client"]) {
|
|
if (!client?.connect) {
|
|
return null;
|
|
}
|
|
if (method === "health") {
|
|
return null;
|
|
}
|
|
const roleRaw = client.connect.role ?? "operator";
|
|
const role = parseGatewayRole(roleRaw);
|
|
if (!role) {
|
|
return errorShape(ErrorCodes.INVALID_REQUEST, `unauthorized role: ${roleRaw}`);
|
|
}
|
|
const scopes = client.connect.scopes ?? [];
|
|
if (!isRoleAuthorizedForMethod(role, method)) {
|
|
return errorShape(ErrorCodes.INVALID_REQUEST, `unauthorized role: ${role}`);
|
|
}
|
|
if (role === "node") {
|
|
return null;
|
|
}
|
|
if (scopes.includes(ADMIN_SCOPE)) {
|
|
return null;
|
|
}
|
|
const scopeAuth = authorizeOperatorScopesForMethod(method, scopes);
|
|
if (!scopeAuth.allowed) {
|
|
return errorShape(ErrorCodes.INVALID_REQUEST, `missing scope: ${scopeAuth.missingScope}`);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export const coreGatewayHandlers: GatewayRequestHandlers = {
|
|
...connectHandlers,
|
|
...logsHandlers,
|
|
...voicewakeHandlers,
|
|
...voicewakeRoutingHandlers,
|
|
...healthHandlers,
|
|
...channelsHandlers,
|
|
...chatHandlers,
|
|
...commandsHandlers,
|
|
...cronHandlers,
|
|
...deviceHandlers,
|
|
...diagnosticsHandlers,
|
|
...doctorHandlers,
|
|
...execApprovalsHandlers,
|
|
...webHandlers,
|
|
...modelsHandlers,
|
|
...modelsAuthStatusHandlers,
|
|
...nativeHookRelayHandlers,
|
|
...pluginHostHookHandlers,
|
|
...configHandlers,
|
|
...wizardHandlers,
|
|
...talkHandlers,
|
|
...toolsCatalogHandlers,
|
|
...toolsEffectiveHandlers,
|
|
...toolsInvokeHandlers,
|
|
...ttsHandlers,
|
|
...skillsHandlers,
|
|
...sessionsHandlers,
|
|
...systemHandlers,
|
|
...updateHandlers,
|
|
...nodeHandlers,
|
|
...nodePendingHandlers,
|
|
...pushHandlers,
|
|
...sendHandlers,
|
|
...usageHandlers,
|
|
...agentHandlers,
|
|
...agentsHandlers,
|
|
...artifactsHandlers,
|
|
};
|
|
|
|
export async function handleGatewayRequest(
|
|
opts: GatewayRequestOptions & { extraHandlers?: GatewayRequestHandlers },
|
|
): Promise<void> {
|
|
const { req, respond, client, isWebchatConnect, context } = opts;
|
|
const authError = authorizeGatewayMethod(req.method, client);
|
|
if (authError) {
|
|
respond(false, undefined, authError);
|
|
return;
|
|
}
|
|
if (context.unavailableGatewayMethods?.has(req.method)) {
|
|
respond(
|
|
false,
|
|
undefined,
|
|
errorShape(ErrorCodes.UNAVAILABLE, `${req.method} unavailable during gateway startup`, {
|
|
retryable: true,
|
|
retryAfterMs: GATEWAY_STARTUP_RETRY_AFTER_MS,
|
|
details: { ...gatewayStartupUnavailableDetails(), method: req.method },
|
|
}),
|
|
);
|
|
return;
|
|
}
|
|
if (CONTROL_PLANE_WRITE_METHODS.has(req.method)) {
|
|
const budget = consumeControlPlaneWriteBudget({ client });
|
|
if (!budget.allowed) {
|
|
const actor = resolveControlPlaneActor(client);
|
|
context.logGateway.warn(
|
|
`control-plane write rate-limited method=${req.method} ${formatControlPlaneActor(actor)} retryAfterMs=${budget.retryAfterMs} key=${budget.key}`,
|
|
);
|
|
respond(
|
|
false,
|
|
undefined,
|
|
errorShape(
|
|
ErrorCodes.UNAVAILABLE,
|
|
`rate limit exceeded for ${req.method}; retry after ${Math.ceil(budget.retryAfterMs / 1000)}s`,
|
|
{
|
|
retryable: true,
|
|
retryAfterMs: budget.retryAfterMs,
|
|
details: {
|
|
method: req.method,
|
|
limit: "3 per 60s",
|
|
},
|
|
},
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
const handler = opts.extraHandlers?.[req.method] ?? coreGatewayHandlers[req.method];
|
|
if (!handler) {
|
|
respond(
|
|
false,
|
|
undefined,
|
|
errorShape(ErrorCodes.INVALID_REQUEST, `unknown method: ${req.method}`),
|
|
);
|
|
return;
|
|
}
|
|
const invokeHandler = () =>
|
|
handler({
|
|
req,
|
|
params: (req.params ?? {}) as Record<string, unknown>,
|
|
client,
|
|
isWebchatConnect,
|
|
respond,
|
|
context,
|
|
});
|
|
// All handlers run inside a request scope so that plugin runtime
|
|
// subagent methods (e.g. context engine tools spawning sub-agents
|
|
// during tool execution) can dispatch back into the gateway.
|
|
await withPluginRuntimeGatewayRequestScope({ context, client, isWebchatConnect }, invokeHandler);
|
|
}
|