File size: 6,393 Bytes
e18726e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | import { addTimerTimeoutGraceMs } from "@openclaw/normalization-core/number-coercion";
import type { PluginRuntime } from "../plugins/runtime/types.js";
import type {
MeetingBrowserRequestParams,
MeetingPlatformAdapter,
} from "./platform-adapter-contract.js";
import type { MeetingBrowserHealth, MeetingTranscriptSnapshot } from "./session-types.js";
type BrowserProxyResult = { result?: unknown };
export type MeetingBrowserNodeInfo = {
caps?: string[];
commands?: string[];
connected?: boolean;
nodeId?: string;
displayName?: string;
remoteIp?: string;
};
type NodeAdapter = Pick<
MeetingPlatformAdapter<unknown, string, MeetingBrowserHealth, MeetingTranscriptSnapshot>,
"displayName" | "nodeCommandName" | "nodeConfigPath"
>;
function isMeetingBrowserNode(node: MeetingBrowserNodeInfo, adapter: NodeAdapter) {
const commands = Array.isArray(node.commands) ? node.commands : [];
const caps = Array.isArray(node.caps) ? node.caps : [];
return (
node.connected === true &&
commands.includes(adapter.nodeCommandName) &&
(commands.includes("browser.proxy") || caps.includes("browser"))
);
}
function matchesRequestedNode(node: MeetingBrowserNodeInfo, requested: string): boolean {
return [node.nodeId, node.displayName, node.remoteIp].some((value) => value === requested);
}
function formatNodeLabel(node: MeetingBrowserNodeInfo): string {
const parts = [node.displayName, node.nodeId, node.remoteIp].filter(Boolean);
return parts.length > 0 ? parts.join(" / ") : "unknown node";
}
function describeNodeUsabilityIssues(node: MeetingBrowserNodeInfo, adapter: NodeAdapter): string[] {
const commands = Array.isArray(node.commands) ? node.commands : [];
const caps = Array.isArray(node.caps) ? node.caps : [];
const issues: string[] = [];
if (node.connected !== true) {
issues.push("offline");
}
if (!commands.includes(adapter.nodeCommandName)) {
issues.push(`missing ${adapter.nodeCommandName}`);
}
if (!commands.includes("browser.proxy") && !caps.includes("browser")) {
issues.push("missing browser.proxy/browser capability");
}
return issues;
}
async function listMeetingNodes(
runtime: PluginRuntime,
adapter: NodeAdapter,
params?: { connected?: boolean },
): Promise<{ nodes: MeetingBrowserNodeInfo[] }> {
try {
return params ? await runtime.nodes.list(params) : await runtime.nodes.list();
} catch (error) {
throw new Error(`${adapter.displayName} node inventory unavailable`, { cause: error });
}
}
export async function resolveMeetingBrowserNodeInfo(params: {
runtime: PluginRuntime;
adapter: NodeAdapter;
requestedNode?: string;
}): Promise<MeetingBrowserNodeInfo> {
const requested = params.requestedNode?.trim();
if (requested) {
const list = await listMeetingNodes(params.runtime, params.adapter);
const matches = list.nodes.filter((node) => matchesRequestedNode(node, requested));
if (matches.length > 1) {
throw new Error(
`Configured ${params.adapter.displayName} node ${requested} is ambiguous (${matches.length} matches). Pin ${params.adapter.nodeConfigPath} to a unique node id, display name, or remote IP.`,
);
}
const [node] = matches;
if (!node) {
throw new Error(
`Configured ${params.adapter.displayName} node ${requested} was not found. Run \`openclaw nodes status\` and start or approve the Chrome node.`,
);
}
if (isMeetingBrowserNode(node, params.adapter)) {
return node;
}
throw new Error(
`Configured ${params.adapter.displayName} node ${requested} is not usable (${formatNodeLabel(node)}): ${describeNodeUsabilityIssues(node, params.adapter).join("; ")}. Start or reinstall \`openclaw node run\` on that Chrome host, approve pairing, and allow ${params.adapter.nodeCommandName} plus browser.proxy.`,
);
}
const list = await listMeetingNodes(params.runtime, params.adapter, { connected: true });
const nodes = list.nodes.filter((node) => isMeetingBrowserNode(node, params.adapter));
const [node] = nodes;
if (!node) {
throw new Error(
`No connected ${params.adapter.displayName}-capable node with browser proxy. Run \`openclaw node run\` on the Chrome host with browser proxy enabled, approve pairing, and allow ${params.adapter.nodeCommandName} plus browser.proxy.`,
);
}
if (nodes.length === 1) {
return node;
}
throw new Error(
`Multiple ${params.adapter.displayName}-capable nodes connected. Set ${params.adapter.nodeConfigPath}.`,
);
}
export async function resolveMeetingBrowserNode(params: {
runtime: PluginRuntime;
adapter: NodeAdapter;
requestedNode?: string;
}): Promise<string> {
const node = await resolveMeetingBrowserNodeInfo(params);
if (!node.nodeId) {
throw new Error(`${params.adapter.displayName} node did not include a node id.`);
}
return node.nodeId;
}
function unwrapNodeInvokePayload(raw: unknown, adapter: NodeAdapter): unknown {
const record = raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {};
if (typeof record.payloadJSON === "string" && record.payloadJSON.trim()) {
try {
return JSON.parse(record.payloadJSON);
} catch (error) {
throw new Error(`${adapter.displayName} browser proxy returned malformed payloadJSON.`, {
cause: error,
});
}
}
if ("payload" in record) {
return record.payload;
}
return raw;
}
function parseBrowserProxyResult(raw: unknown, adapter: NodeAdapter): unknown {
const payload = unwrapNodeInvokePayload(raw, adapter);
const proxy =
payload && typeof payload === "object" ? (payload as BrowserProxyResult) : undefined;
if (!proxy || !("result" in proxy)) {
throw new Error(`${adapter.displayName} browser proxy returned an invalid result.`);
}
return proxy.result;
}
export async function callMeetingBrowserProxyOnNode(
params: {
runtime: PluginRuntime;
adapter: NodeAdapter;
nodeId: string;
} & MeetingBrowserRequestParams,
) {
const raw = await params.runtime.nodes.invoke({
nodeId: params.nodeId,
command: "browser.proxy",
params: {
method: params.method,
path: params.path,
body: params.body,
timeoutMs: params.timeoutMs,
},
timeoutMs: addTimerTimeoutGraceMs(params.timeoutMs) ?? 1,
scopes: ["operator.admin"],
});
return parseBrowserProxyResult(raw, params.adapter);
}
|