mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-01 06:56:12 +00:00
* refactor: share talk event metric extraction * refactor: reuse shared coercion helpers * refactor: reuse shared primitive guards * refactor: reuse shared record guard * refactor: reuse shared primitive helpers * refactor: reuse shared string guards * refactor: reuse shared non-empty string guard * refactor: share plugin primitive coercion helpers * refactor: reuse plugin coercion helpers * refactor: reuse plugin coercion helpers in more plugins * refactor: reuse channel coercion helpers * refactor: reuse monitor coercion helpers * refactor: reuse provider coercion helpers * refactor: reuse core coercion helpers * refactor: reuse runtime coercion helpers * refactor: reuse helper coercion in codex paths * refactor: reuse helper coercion in runtime paths * refactor: reuse codex app-server coercion helpers * refactor: reuse codex record helpers * refactor: reuse migration and qa record helpers * refactor: reuse feishu and core helper guards * refactor: reuse browser and policy coercion helpers * refactor: reuse memory wiki record helper * refactor: share boolean coercion helpers * refactor: reuse finite number coercion * refactor: reuse trimmed string list helpers * refactor: reuse string list normalization * refactor: reuse remaining string list helpers * refactor: reuse string entry normalizer * refactor: share sorted string helpers * refactor: share string list normalization * test: preserve command registry browser imports * refactor: reuse trimmed list helpers * refactor: reuse string dedupe helpers * refactor: reuse local dedupe helpers * refactor: reuse more string dedupe helpers * refactor: reuse command string dedupe helpers * refactor: dedupe memory path lists with helper * refactor: expose string dedupe helpers to plugins * refactor: reuse core string dedupe helpers * refactor: reuse shared unique value helpers * refactor: reuse unique helpers in agent utilities * refactor: reuse unique helpers in config plumbing * refactor: reuse unique helpers in extensions * refactor: reuse unique helpers in core utilities * refactor: reuse unique helpers in qa plugins * refactor: reuse unique helpers in memory plugins * refactor: reuse unique helpers in channel plugins * refactor: reuse unique helpers in core tails * refactor: reuse unique helper in comfy workflow * refactor: reuse unique helpers in test utilities * refactor: expose unique value helper to plugins * refactor: reuse unique helpers for numeric lists * refactor: replace index dedupe filters * refactor: reuse string entry normalization * refactor: reuse string normalization in plugin helpers * refactor: reuse string normalization in extension helpers * refactor: reuse string normalization in channel parsers * refactor: reuse string normalization in memory search * refactor: reuse string normalization in provider parsers * refactor: reuse string normalization in qa helpers * refactor: reuse string normalization in infra parsers * refactor: reuse string normalization in messaging parsers * refactor: reuse string normalization in core parsers * refactor: reuse string normalization in extension parsers * refactor: reuse string normalization in remaining parsers * refactor: reuse string normalization in final parser spots * refactor: reuse string normalization in qa media helpers * refactor: reuse normalization in provider and media lists * refactor: reuse normalization for remaining set filters * refactor: reuse normalization in policy allowlists * refactor: reuse normalization in session and owner lists * refactor: centralize primitive string lists * refactor: reuse lowercase entry helpers * refactor: reuse sorted string helpers * refactor: reuse unique trimmed helpers * refactor: reuse string normalization helpers * refactor: reuse catalog string helpers * refactor: reuse remaining string helpers * refactor: simplify remaining list normalization * refactor: reuse codex auth order normalization * chore: refresh plugin sdk api baseline * fix: make shared string sorting deterministic * chore: refresh plugin sdk api baseline * fix: align host env security ordering
236 lines
6.1 KiB
TypeScript
236 lines
6.1 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
import { dispatchGatewayMethod } from "openclaw/plugin-sdk/gateway-method-runtime";
|
|
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
import { isAdminHttpRpcAllowedMethod, listAdminHttpRpcAllowedMethods } from "./methods.js";
|
|
|
|
const DEFAULT_RPC_BODY_BYTES = 1024 * 1024;
|
|
|
|
const ErrorCodes = {
|
|
AGENT_TIMEOUT: "AGENT_TIMEOUT",
|
|
APPROVAL_NOT_FOUND: "APPROVAL_NOT_FOUND",
|
|
INVALID_REQUEST: "INVALID_REQUEST",
|
|
NOT_LINKED: "NOT_LINKED",
|
|
NOT_PAIRED: "NOT_PAIRED",
|
|
UNAVAILABLE: "UNAVAILABLE",
|
|
} as const;
|
|
|
|
type RpcBody = {
|
|
id?: unknown;
|
|
method?: unknown;
|
|
params?: unknown;
|
|
};
|
|
|
|
type RpcError = {
|
|
code: string;
|
|
message: string;
|
|
details?: unknown;
|
|
retryable?: boolean;
|
|
retryAfterMs?: number;
|
|
};
|
|
|
|
type RpcResponse =
|
|
| { id: string; ok: true; payload: unknown; meta?: Record<string, unknown> }
|
|
| { id: string; ok: false; error: RpcError; meta?: Record<string, unknown> };
|
|
|
|
type ParsedRequest = {
|
|
id: string;
|
|
method: string;
|
|
params?: unknown;
|
|
};
|
|
|
|
function createError(code: string, message: string): RpcError {
|
|
return { code, message };
|
|
}
|
|
|
|
function rpcHttpStatus(response: RpcResponse): number {
|
|
if (response.ok) {
|
|
return 200;
|
|
}
|
|
switch (response.error.code) {
|
|
case ErrorCodes.INVALID_REQUEST:
|
|
return 400;
|
|
case ErrorCodes.APPROVAL_NOT_FOUND:
|
|
return 404;
|
|
case ErrorCodes.UNAVAILABLE:
|
|
return 503;
|
|
case ErrorCodes.AGENT_TIMEOUT:
|
|
return 504;
|
|
case ErrorCodes.NOT_LINKED:
|
|
case ErrorCodes.NOT_PAIRED:
|
|
return 409;
|
|
default:
|
|
return 500;
|
|
}
|
|
}
|
|
|
|
function sendJson(res: ServerResponse, status: number, body: unknown): void {
|
|
res.statusCode = status;
|
|
res.setHeader("Cache-Control", "no-store");
|
|
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
res.end(JSON.stringify(body));
|
|
}
|
|
|
|
function sendError(res: ServerResponse, status: number, error: { type: string; message: string }) {
|
|
sendJson(res, status, { ok: false, error });
|
|
}
|
|
|
|
async function readJsonBody(
|
|
req: IncomingMessage,
|
|
maxBytes: number,
|
|
): Promise<{ ok: true; value: unknown } | { ok: false; status: number; message: string }> {
|
|
const chunks: Buffer[] = [];
|
|
let totalBytes = 0;
|
|
try {
|
|
for await (const chunk of req) {
|
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
totalBytes += buffer.byteLength;
|
|
if (totalBytes > maxBytes) {
|
|
return { ok: false, status: 413, message: "Payload too large" };
|
|
}
|
|
chunks.push(buffer);
|
|
}
|
|
} catch {
|
|
return { ok: false, status: 400, message: "failed to read request body" };
|
|
}
|
|
|
|
const raw = Buffer.concat(chunks).toString("utf8");
|
|
if (!raw.trim()) {
|
|
return { ok: false, status: 400, message: "request body must be JSON" };
|
|
}
|
|
try {
|
|
return { ok: true, value: JSON.parse(raw) };
|
|
} catch {
|
|
return { ok: false, status: 400, message: "request body must be valid JSON" };
|
|
}
|
|
}
|
|
|
|
function readRpcRequestBody(body: unknown):
|
|
| { ok: true; request: ParsedRequest }
|
|
| {
|
|
ok: false;
|
|
message: string;
|
|
} {
|
|
if (!isRecord(body)) {
|
|
return { ok: false, message: "request body must be an object" };
|
|
}
|
|
const rpcBody = body as RpcBody;
|
|
if (typeof rpcBody.method !== "string" || rpcBody.method.trim().length === 0) {
|
|
return { ok: false, message: "method must be a non-empty string" };
|
|
}
|
|
const id =
|
|
typeof rpcBody.id === "string" && rpcBody.id.trim().length > 0
|
|
? rpcBody.id.trim()
|
|
: randomUUID();
|
|
return {
|
|
ok: true,
|
|
request: {
|
|
id,
|
|
method: rpcBody.method.trim(),
|
|
...(Object.prototype.hasOwnProperty.call(rpcBody, "params")
|
|
? { params: rpcBody.params }
|
|
: {}),
|
|
},
|
|
};
|
|
}
|
|
|
|
function methodNotAllowed(id: string, method: string): RpcResponse {
|
|
return {
|
|
id,
|
|
ok: false,
|
|
error: createError(
|
|
ErrorCodes.INVALID_REQUEST,
|
|
`admin HTTP RPC method is not supported: ${method}`,
|
|
),
|
|
};
|
|
}
|
|
|
|
function commandsList(id: string): RpcResponse {
|
|
return {
|
|
id,
|
|
ok: true,
|
|
payload: {
|
|
methods: listAdminHttpRpcAllowedMethods(),
|
|
},
|
|
};
|
|
}
|
|
|
|
async function dispatchAdminRpc(request: ParsedRequest): Promise<RpcResponse> {
|
|
try {
|
|
const response = await dispatchGatewayMethod(request.method, request.params);
|
|
if (response.ok) {
|
|
return {
|
|
id: request.id,
|
|
ok: true,
|
|
payload: response.payload,
|
|
...(response.meta ? { meta: response.meta } : {}),
|
|
};
|
|
}
|
|
return {
|
|
id: request.id,
|
|
ok: false,
|
|
error:
|
|
response.error ??
|
|
createError(ErrorCodes.UNAVAILABLE, "gateway method failed before returning a response"),
|
|
...(response.meta ? { meta: response.meta } : {}),
|
|
};
|
|
} catch {
|
|
return {
|
|
id: request.id,
|
|
ok: false,
|
|
error: createError(
|
|
ErrorCodes.UNAVAILABLE,
|
|
"gateway method failed before returning a response",
|
|
),
|
|
};
|
|
}
|
|
}
|
|
|
|
export async function handleAdminHttpRpcRequest(
|
|
req: IncomingMessage,
|
|
res: ServerResponse,
|
|
): Promise<boolean> {
|
|
if ((req.method ?? "GET").toUpperCase() !== "POST") {
|
|
res.setHeader("Allow", "POST");
|
|
sendError(res, 405, {
|
|
type: "method_not_allowed",
|
|
message: "Method Not Allowed",
|
|
});
|
|
return true;
|
|
}
|
|
|
|
const body = await readJsonBody(req, DEFAULT_RPC_BODY_BYTES);
|
|
if (!body.ok) {
|
|
sendError(res, body.status, {
|
|
type: "invalid_request",
|
|
message: body.message,
|
|
});
|
|
return true;
|
|
}
|
|
|
|
const parsed = readRpcRequestBody(body.value);
|
|
if (!parsed.ok) {
|
|
sendError(res, 400, {
|
|
type: "invalid_request",
|
|
message: parsed.message,
|
|
});
|
|
return true;
|
|
}
|
|
|
|
if (!isAdminHttpRpcAllowedMethod(parsed.request.method)) {
|
|
const response = methodNotAllowed(parsed.request.id, parsed.request.method);
|
|
sendJson(res, rpcHttpStatus(response), response);
|
|
return true;
|
|
}
|
|
|
|
if (parsed.request.method === "commands.list") {
|
|
const response = commandsList(parsed.request.id);
|
|
sendJson(res, 200, response);
|
|
return true;
|
|
}
|
|
|
|
const response = await dispatchAdminRpc(parsed.request);
|
|
sendJson(res, rpcHttpStatus(response), response);
|
|
return true;
|
|
}
|