UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

304 lines (303 loc) 14.4 kB
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js"; import { _ as parseStrictFiniteNumber, b as parseStrictPositiveInteger } from "./number-coercion-CJQ8TR--.js"; import { i as formatErrorMessage } from "./errors-BXgSefBE.js"; import { r as theme } from "./theme-vjDs9tao.js"; import { g as shortenHomePath } from "./utils-CCC-BEJH.js"; import { n as defaultRuntime } from "./runtime-B4lgFmsS.js"; import { t as resolveNodeFromNodeList } from "./node-resolve-BawtWMwa.js"; import "./error-runtime-C8vbtAJt.js"; import "./number-runtime-DBLVDypr.js"; import "./string-coerce-runtime-CEGJWkQ_.js"; import "./text-utility-runtime-BZ3jGr-e.js"; import "./runtime-BhCtPpXL.js"; import "./gateway-runtime-BMyBmMxe.js"; import { n as callGatewayFromCli } from "./gateway-rpc-DV2_JEP0.js"; import { n as runCommandWithRuntime } from "./cli-utils-Cuzzfy1Q.js"; import "./cli-runtime-DmQcvRyD.js"; import { r as parseCanvasSnapshotPayload, t as canvasSnapshotTempPath } from "./cli-helpers-BV-1tCaC.js"; import fs from "node:fs/promises"; import { randomUUID } from "node:crypto"; //#region extensions/canvas/src/a2ui-jsonl.ts /** * A2UI JSONL helpers for Canvas text rendering and validation. */ const A2UI_ACTION_KEYS = [ "beginRendering", "surfaceUpdate", "dataModelUpdate", "deleteSurface", "createSurface" ]; /** Builds a minimal A2UI JSONL payload that renders text in a single surface. */ function buildA2UITextJsonl(text) { const surfaceId = "main"; const rootId = "root"; const textId = "text"; return [{ surfaceUpdate: { surfaceId, components: [{ id: rootId, component: { Column: { children: { explicitList: [textId] } } } }, { id: textId, component: { Text: { text: { literalString: text }, usageHint: "body" } } }] } }, { beginRendering: { surfaceId, root: rootId } }].map((payload) => JSON.stringify(payload)).join("\n"); } /** Validates A2UI JSONL and returns the detected dialect/version metadata. */ function validateA2UIJsonl(jsonl) { const lines = jsonl.split(/\r?\n/); const errors = []; let sawV08 = false; let sawV09 = false; let messageCount = 0; lines.forEach((line, idx) => { const trimmed = line.trim(); if (!trimmed) return; messageCount += 1; let obj; try { obj = JSON.parse(trimmed); } catch (err) { errors.push(`line ${idx + 1}: ${String(err)}`); return; } if (!obj || typeof obj !== "object" || Array.isArray(obj)) { errors.push(`line ${idx + 1}: expected JSON object`); return; } const record = obj; const actionKeys = A2UI_ACTION_KEYS.filter((key) => key in record); if (actionKeys.length !== 1) { errors.push(`line ${idx + 1}: expected exactly one action key (${A2UI_ACTION_KEYS.join(", ")})`); return; } if (actionKeys[0] === "createSurface") sawV09 = true; else sawV08 = true; }); if (messageCount === 0) errors.push("no JSONL messages found"); if (sawV08 && sawV09) errors.push("mixed A2UI v0.8 and v0.9 messages in one file"); if (errors.length > 0) throw new Error(`Invalid A2UI JSONL:\n- ${errors.join("\n- ")}`); return { version: sawV09 ? "v0.9" : "v0.8", messageCount }; } //#endregion //#region extensions/canvas/src/cli.ts /** * Canvas node CLI command registration and runtime dependency wiring. */ function parseCanvasSnapshotRequestFormat(raw) { switch (normalizeLowercaseStringOrEmpty(normalizeOptionalString(raw) ?? "jpg")) { case "png": return "png"; case "jpg": case "jpeg": return "jpeg"; default: throw new Error(`invalid format: ${String(raw)} (expected png|jpg|jpeg)`); } } function parseTimeoutMs(raw) { if (raw === void 0 || raw === null) return; return parseStrictPositiveInteger(raw); } function parseCanvasPositiveIntOption(raw, flag) { if (!raw) return; const parsed = parseStrictPositiveInteger(raw); if (parsed === void 0) throw new Error(`${flag} must be a positive integer.`); return parsed; } function parseCanvasFiniteNumberOption(raw, flag) { if (!raw) return; const parsed = parseStrictFiniteNumber(raw); if (parsed === void 0) throw new Error(`${flag} must be a number.`); return parsed; } function parseNodeCandidates(raw) { const payload = raw && typeof raw === "object" ? raw : {}; return (Array.isArray(payload.nodes) ? payload.nodes : Array.isArray(payload.paired) ? payload.paired : []).map((entry) => { if (!entry || typeof entry !== "object") return null; const node = entry; if (typeof node.nodeId !== "string") return null; const candidate = { nodeId: node.nodeId }; if (typeof node.displayName === "string") candidate.displayName = node.displayName; if (typeof node.remoteIp === "string") candidate.remoteIp = node.remoteIp; if (typeof node.connected === "boolean") candidate.connected = node.connected; if (typeof node.clientId === "string") candidate.clientId = node.clientId; return candidate; }).filter((entry) => entry !== null); } function unauthorizedHintForMessage(message) { const haystack = normalizeLowercaseStringOrEmpty(message); if (haystack.includes("unauthorizedclient") || haystack.includes("bridge client is not authorized") || haystack.includes("unsigned bridge clients are not allowed")) return [ "peekaboo bridge rejected the client.", "sign the peekaboo CLI (TeamID Y5PE65HELJ) or launch the host with", "PEEKABOO_ALLOW_UNSIGNED_SOCKET_CLIENTS=1 for local dev." ].join(" "); return null; } /** Creates the default Canvas CLI dependency bundle backed by the OpenClaw gateway CLI. */ function createDefaultCanvasCliDependencies() { const nodesCallOpts = (cmd, defaults) => cmd.option("--url <url>", "Gateway WebSocket URL (defaults to gateway.remote.url when configured)").option("--token <token>", "Gateway token (if required)").option("--timeout <ms>", "Timeout in ms", String(defaults?.timeoutMs ?? 1e4)).option("--json", "Output JSON", false); const callGatewayCli = async (method, opts, params, callOpts) => { const timeout = String(callOpts?.transportTimeoutMs ?? opts.timeout ?? 1e4); return await callGatewayFromCli(method, { ...opts, timeout }, params, { progress: opts.json !== true }); }; return { defaultRuntime, nodesCallOpts, runNodesCommand: (label, action) => runCommandWithRuntime(defaultRuntime, action, (err) => { const message = formatErrorMessage(err); defaultRuntime.error(theme.error(`nodes ${label} failed: ${message}`)); const hint = unauthorizedHintForMessage(message); if (hint) defaultRuntime.error(theme.warn(hint)); defaultRuntime.exit(1); }), getNodesTheme: () => ({ ok: theme.success }), parseTimeoutMs, resolveNodeId: async (opts, query) => { let raw; try { raw = await callGatewayCli("node.list", opts, {}); } catch { raw = await callGatewayCli("node.pair.list", opts, {}); } return resolveNodeFromNodeList(parseNodeCandidates(raw), query).nodeId; }, buildNodeInvokeParams: ({ nodeId, command, params, timeoutMs }) => ({ nodeId, command, params, idempotencyKey: randomUUID(), ...typeof timeoutMs === "number" && Number.isFinite(timeoutMs) ? { timeoutMs } : {} }), callGatewayCli, writeBase64ToFile: async (filePath, base64) => await fs.writeFile(filePath, Buffer.from(base64, "base64")), shortenHomePath }; } async function invokeCanvas(deps, opts, command, params) { const nodeId = await deps.resolveNodeId(opts, normalizeOptionalString(opts.node) ?? ""); const timeoutMs = deps.parseTimeoutMs(opts.invokeTimeout); return await deps.callGatewayCli("node.invoke", opts, deps.buildNodeInvokeParams({ nodeId, command, params, timeoutMs: typeof timeoutMs === "number" ? timeoutMs : void 0 })); } /** Registers Canvas subcommands under the nodes CLI command group. */ function registerNodesCanvasCommands(nodes, deps) { const canvas = nodes.command("canvas").description("Capture or render canvas content from a paired node"); deps.nodesCallOpts(canvas.command("snapshot").description("Capture a canvas snapshot (prints the saved path)").requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP").option("--format <png|jpg|jpeg>", "Image format", "jpg").option("--max-width <px>", "Max width in px (optional)").option("--quality <0-1>", "JPEG quality (optional)").option("--invoke-timeout <ms>", "Node invoke timeout in ms (default 20000)", "20000").action(async (opts) => { await deps.runNodesCommand("canvas snapshot", async () => { const format = parseCanvasSnapshotRequestFormat(opts.format); const maxWidth = parseCanvasPositiveIntOption(opts.maxWidth, "--max-width"); const quality = parseCanvasFiniteNumberOption(opts.quality, "--quality"); const raw = await invokeCanvas(deps, opts, "canvas.snapshot", { format, maxWidth: Number.isFinite(maxWidth) ? maxWidth : void 0, quality: Number.isFinite(quality) ? quality : void 0 }); const payload = parseCanvasSnapshotPayload((typeof raw === "object" && raw !== null ? raw : {}).payload); const filePath = canvasSnapshotTempPath({ ext: payload.format === "jpeg" ? "jpg" : payload.format }); await deps.writeBase64ToFile(filePath, payload.base64); if (opts.json) { deps.defaultRuntime.writeJson({ file: { path: filePath, format: payload.format } }); return; } deps.defaultRuntime.log(deps.shortenHomePath(filePath)); }); }), { timeoutMs: 6e4 }); deps.nodesCallOpts(canvas.command("present").description("Show the canvas (optionally with a target URL/path)").requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP").option("--target <urlOrPath>", "Target URL/path (optional)").option("--x <px>", "Placement x coordinate").option("--y <px>", "Placement y coordinate").option("--width <px>", "Placement width").option("--height <px>", "Placement height").option("--invoke-timeout <ms>", "Node invoke timeout in ms").action(async (opts) => { await deps.runNodesCommand("canvas present", async () => { const placement = { x: parseCanvasFiniteNumberOption(opts.x, "--x"), y: parseCanvasFiniteNumberOption(opts.y, "--y"), width: parseCanvasFiniteNumberOption(opts.width, "--width"), height: parseCanvasFiniteNumberOption(opts.height, "--height") }; const params = {}; if (opts.target) params.url = opts.target; if (Number.isFinite(placement.x) || Number.isFinite(placement.y) || Number.isFinite(placement.width) || Number.isFinite(placement.height)) params.placement = placement; await invokeCanvas(deps, opts, "canvas.present", params); if (!opts.json) { const { ok } = deps.getNodesTheme(); deps.defaultRuntime.log(ok("canvas present ok")); } }); })); deps.nodesCallOpts(canvas.command("hide").description("Hide the canvas").requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP").option("--invoke-timeout <ms>", "Node invoke timeout in ms").action(async (opts) => { await deps.runNodesCommand("canvas hide", async () => { await invokeCanvas(deps, opts, "canvas.hide", void 0); if (!opts.json) { const { ok } = deps.getNodesTheme(); deps.defaultRuntime.log(ok("canvas hide ok")); } }); })); deps.nodesCallOpts(canvas.command("navigate").description("Navigate the canvas to a URL").argument("<url>", "Target URL/path").requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP").option("--invoke-timeout <ms>", "Node invoke timeout in ms").action(async (url, opts) => { await deps.runNodesCommand("canvas navigate", async () => { await invokeCanvas(deps, opts, "canvas.navigate", { url }); if (!opts.json) { const { ok } = deps.getNodesTheme(); deps.defaultRuntime.log(ok("canvas navigate ok")); } }); })); deps.nodesCallOpts(canvas.command("eval").description("Evaluate JavaScript in the canvas").argument("[js]", "JavaScript to evaluate").option("--js <code>", "JavaScript to evaluate").requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP").option("--invoke-timeout <ms>", "Node invoke timeout in ms").action(async (jsArg, opts) => { await deps.runNodesCommand("canvas eval", async () => { const js = opts.js ?? jsArg; if (!js) throw new Error("missing --js or <js>"); const raw = await invokeCanvas(deps, opts, "canvas.eval", { javaScript: js }); if (opts.json) { deps.defaultRuntime.writeJson(raw); return; } const payload = typeof raw === "object" && raw !== null ? raw.payload : void 0; if (payload?.result) deps.defaultRuntime.log(payload.result); else { const { ok } = deps.getNodesTheme(); deps.defaultRuntime.log(ok("canvas eval ok")); } }); })); const a2ui = canvas.command("a2ui").description("Render A2UI content on the canvas"); deps.nodesCallOpts(a2ui.command("push").description("Push A2UI JSONL to the canvas").option("--jsonl <path>", "Path to JSONL payload").option("--text <text>", "Render a quick A2UI text payload").requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP").option("--invoke-timeout <ms>", "Node invoke timeout in ms").action(async (opts) => { await deps.runNodesCommand("canvas a2ui push", async () => { const hasJsonl = Boolean(opts.jsonl); const hasText = typeof opts.text === "string"; if (hasJsonl === hasText) throw new Error("provide exactly one of --jsonl or --text"); const jsonl = hasText ? buildA2UITextJsonl(opts.text ?? "") : await fs.readFile(String(opts.jsonl), "utf8"); const { version, messageCount } = validateA2UIJsonl(jsonl); if (version === "v0.9") throw new Error("Detected A2UI v0.9 JSONL (createSurface). OpenClaw currently supports v0.8 only."); await invokeCanvas(deps, opts, "canvas.a2ui.pushJSONL", { jsonl }); if (!opts.json) { const { ok } = deps.getNodesTheme(); deps.defaultRuntime.log(ok(`canvas a2ui push ok (v0.8, ${messageCount} message${messageCount === 1 ? "" : "s"})`)); } }); })); deps.nodesCallOpts(a2ui.command("reset").description("Reset A2UI renderer state").requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP").option("--invoke-timeout <ms>", "Node invoke timeout in ms").action(async (opts) => { await deps.runNodesCommand("canvas a2ui reset", async () => { await invokeCanvas(deps, opts, "canvas.a2ui.reset", void 0); if (!opts.json) { const { ok } = deps.getNodesTheme(); deps.defaultRuntime.log(ok("canvas a2ui reset ok")); } }); })); } //#endregion export { registerNodesCanvasCommands as n, createDefaultCanvasCliDependencies as t };