mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:00:42 +00:00
perf: split canvas a2ui shared imports
This commit is contained in:
72
src/canvas-host/a2ui-shared.ts
Normal file
72
src/canvas-host/a2ui-shared.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { lowercasePreservingWhitespace } from "../shared/string-coerce.js";
|
||||
|
||||
export const A2UI_PATH = "/__openclaw__/a2ui";
|
||||
|
||||
export const CANVAS_HOST_PATH = "/__openclaw__/canvas";
|
||||
|
||||
export const CANVAS_WS_PATH = "/__openclaw__/ws";
|
||||
|
||||
export function isA2uiPath(pathname: string): boolean {
|
||||
return pathname === A2UI_PATH || pathname.startsWith(`${A2UI_PATH}/`);
|
||||
}
|
||||
|
||||
export function injectCanvasLiveReload(html: string): string {
|
||||
const snippet = `
|
||||
<script>
|
||||
(() => {
|
||||
// Cross-platform action bridge helper.
|
||||
// Works on:
|
||||
// - iOS: window.webkit.messageHandlers.openclawCanvasA2UIAction.postMessage(...)
|
||||
// - Android: window.openclawCanvasA2UIAction.postMessage(...)
|
||||
const handlerNames = ["openclawCanvasA2UIAction"];
|
||||
function postToNode(payload) {
|
||||
try {
|
||||
const raw = typeof payload === "string" ? payload : JSON.stringify(payload);
|
||||
for (const name of handlerNames) {
|
||||
const iosHandler = globalThis.webkit?.messageHandlers?.[name];
|
||||
if (iosHandler && typeof iosHandler.postMessage === "function") {
|
||||
iosHandler.postMessage(raw);
|
||||
return true;
|
||||
}
|
||||
const androidHandler = globalThis[name];
|
||||
if (androidHandler && typeof androidHandler.postMessage === "function") {
|
||||
// Important: call as a method on the interface object (binding matters on Android WebView).
|
||||
androidHandler.postMessage(raw);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
return false;
|
||||
}
|
||||
function sendUserAction(userAction) {
|
||||
const id =
|
||||
(userAction && typeof userAction.id === "string" && userAction.id.trim()) ||
|
||||
(globalThis.crypto?.randomUUID?.() ?? String(Date.now()));
|
||||
const action = { ...userAction, id };
|
||||
return postToNode({ userAction: action });
|
||||
}
|
||||
globalThis.OpenClaw = globalThis.OpenClaw ?? {};
|
||||
globalThis.OpenClaw.postMessage = postToNode;
|
||||
globalThis.OpenClaw.sendUserAction = sendUserAction;
|
||||
globalThis.openclawPostMessage = postToNode;
|
||||
globalThis.openclawSendUserAction = sendUserAction;
|
||||
|
||||
try {
|
||||
const cap = new URLSearchParams(location.search).get("oc_cap");
|
||||
const proto = location.protocol === "https:" ? "wss" : "ws";
|
||||
const capQuery = cap ? "?oc_cap=" + encodeURIComponent(cap) : "";
|
||||
const ws = new WebSocket(proto + "://" + location.host + ${JSON.stringify(CANVAS_WS_PATH)} + capQuery);
|
||||
ws.onmessage = (ev) => {
|
||||
if (String(ev.data || "") === "reload") location.reload();
|
||||
};
|
||||
} catch {}
|
||||
})();
|
||||
</script>
|
||||
`.trim();
|
||||
|
||||
const idx = lowercasePreservingWhitespace(html).lastIndexOf("</body>");
|
||||
if (idx >= 0) {
|
||||
return `${html.slice(0, idx)}\n${snippet}\n${html.slice(idx)}`;
|
||||
}
|
||||
return `${html}\n${snippet}\n`;
|
||||
}
|
||||
@@ -4,13 +4,16 @@ import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { detectMime } from "../media/mime.js";
|
||||
import { lowercasePreservingWhitespace } from "../shared/string-coerce.js";
|
||||
import { A2UI_PATH, injectCanvasLiveReload, isA2uiPath } from "./a2ui-shared.js";
|
||||
import { resolveFileWithinRoot } from "./file-resolver.js";
|
||||
|
||||
export const A2UI_PATH = "/__openclaw__/a2ui";
|
||||
|
||||
export const CANVAS_HOST_PATH = "/__openclaw__/canvas";
|
||||
|
||||
export const CANVAS_WS_PATH = "/__openclaw__/ws";
|
||||
export {
|
||||
A2UI_PATH,
|
||||
CANVAS_HOST_PATH,
|
||||
CANVAS_WS_PATH,
|
||||
injectCanvasLiveReload,
|
||||
isA2uiPath,
|
||||
} from "./a2ui-shared.js";
|
||||
|
||||
let cachedA2uiRootReal: string | null | undefined;
|
||||
let resolvingA2uiRoot: Promise<string | null> | null = null;
|
||||
@@ -79,67 +82,6 @@ async function resolveA2uiRootReal(): Promise<string | null> {
|
||||
return resolvingA2uiRoot;
|
||||
}
|
||||
|
||||
export function injectCanvasLiveReload(html: string): string {
|
||||
const snippet = `
|
||||
<script>
|
||||
(() => {
|
||||
// Cross-platform action bridge helper.
|
||||
// Works on:
|
||||
// - iOS: window.webkit.messageHandlers.openclawCanvasA2UIAction.postMessage(...)
|
||||
// - Android: window.openclawCanvasA2UIAction.postMessage(...)
|
||||
const handlerNames = ["openclawCanvasA2UIAction"];
|
||||
function postToNode(payload) {
|
||||
try {
|
||||
const raw = typeof payload === "string" ? payload : JSON.stringify(payload);
|
||||
for (const name of handlerNames) {
|
||||
const iosHandler = globalThis.webkit?.messageHandlers?.[name];
|
||||
if (iosHandler && typeof iosHandler.postMessage === "function") {
|
||||
iosHandler.postMessage(raw);
|
||||
return true;
|
||||
}
|
||||
const androidHandler = globalThis[name];
|
||||
if (androidHandler && typeof androidHandler.postMessage === "function") {
|
||||
// Important: call as a method on the interface object (binding matters on Android WebView).
|
||||
androidHandler.postMessage(raw);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
return false;
|
||||
}
|
||||
function sendUserAction(userAction) {
|
||||
const id =
|
||||
(userAction && typeof userAction.id === "string" && userAction.id.trim()) ||
|
||||
(globalThis.crypto?.randomUUID?.() ?? String(Date.now()));
|
||||
const action = { ...userAction, id };
|
||||
return postToNode({ userAction: action });
|
||||
}
|
||||
globalThis.OpenClaw = globalThis.OpenClaw ?? {};
|
||||
globalThis.OpenClaw.postMessage = postToNode;
|
||||
globalThis.OpenClaw.sendUserAction = sendUserAction;
|
||||
globalThis.openclawPostMessage = postToNode;
|
||||
globalThis.openclawSendUserAction = sendUserAction;
|
||||
|
||||
try {
|
||||
const cap = new URLSearchParams(location.search).get("oc_cap");
|
||||
const proto = location.protocol === "https:" ? "wss" : "ws";
|
||||
const capQuery = cap ? "?oc_cap=" + encodeURIComponent(cap) : "";
|
||||
const ws = new WebSocket(proto + "://" + location.host + ${JSON.stringify(CANVAS_WS_PATH)} + capQuery);
|
||||
ws.onmessage = (ev) => {
|
||||
if (String(ev.data || "") === "reload") location.reload();
|
||||
};
|
||||
} catch {}
|
||||
})();
|
||||
</script>
|
||||
`.trim();
|
||||
|
||||
const idx = lowercasePreservingWhitespace(html).lastIndexOf("</body>");
|
||||
if (idx >= 0) {
|
||||
return `${html.slice(0, idx)}\n${snippet}\n${html.slice(idx)}`;
|
||||
}
|
||||
return `${html}\n${snippet}\n`;
|
||||
}
|
||||
|
||||
export async function handleA2uiHttpRequest(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
@@ -150,8 +92,7 @@ export async function handleA2uiHttpRequest(
|
||||
}
|
||||
|
||||
const url = new URL(urlRaw, "http://localhost");
|
||||
const basePath =
|
||||
url.pathname === A2UI_PATH || url.pathname.startsWith(`${A2UI_PATH}/`) ? A2UI_PATH : undefined;
|
||||
const basePath = isA2uiPath(url.pathname) ? A2UI_PATH : undefined;
|
||||
if (!basePath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -9,9 +9,8 @@ import {
|
||||
A2UI_PATH,
|
||||
CANVAS_HOST_PATH,
|
||||
CANVAS_WS_PATH,
|
||||
handleA2uiHttpRequest,
|
||||
injectCanvasLiveReload,
|
||||
} from "./a2ui.js";
|
||||
} from "./a2ui-shared.js";
|
||||
|
||||
type MockWatcher = {
|
||||
on: (event: string, cb: (...args: unknown[]) => void) => MockWatcher;
|
||||
@@ -106,6 +105,7 @@ async function captureHandlerResponse(
|
||||
}
|
||||
|
||||
async function captureA2uiResponse(url: string, method = "GET"): Promise<CapturedResponse> {
|
||||
const { handleA2uiHttpRequest } = await import("./a2ui.js");
|
||||
return await captureHttpResponse(handleA2uiHttpRequest, url, method);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,9 +20,9 @@ import { ensureDir, resolveUserPath } from "../utils.js";
|
||||
import {
|
||||
CANVAS_HOST_PATH,
|
||||
CANVAS_WS_PATH,
|
||||
handleA2uiHttpRequest,
|
||||
injectCanvasLiveReload,
|
||||
} from "./a2ui.js";
|
||||
isA2uiPath,
|
||||
} from "./a2ui-shared.js";
|
||||
import { normalizeUrlPath, resolveFileWithinRoot } from "./file-resolver.js";
|
||||
|
||||
type ChokidarWatch = typeof import("chokidar").watch;
|
||||
@@ -469,8 +469,11 @@ export async function startCanvasHost(opts: CanvasHostServerOpts): Promise<Canva
|
||||
return;
|
||||
}
|
||||
void (async () => {
|
||||
if (await handleA2uiHttpRequest(req, res)) {
|
||||
return;
|
||||
if (req.url && isA2uiPath(new URL(req.url, "http://localhost").pathname)) {
|
||||
const { handleA2uiHttpRequest } = await import("./a2ui.js");
|
||||
if (await handleA2uiHttpRequest(req, res)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (await handler.handleHttpRequest(req, res)) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user