mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-22 14:51:10 +00:00
* feat(browser): send pages to the main session from the Chrome extension One-click page share in the OpenClaw Chrome extension: toolbar popup with an optional note, page/selection context menu, and Alt+Shift+S. Capture is selection-first with a readability heuristic, X/Twitter thread extraction, and Google Docs plain-text export via the user's session cookies. Payloads ride the existing paired relay WebSocket as a new pageShare message; the gateway-only page-share sink wraps page text in the external-content safety boundary, then enqueues a main-session system event and requests an immediate heartbeat (hooks/wake semantics). Node-hosted relays report a clear unsupported error. Capture heuristics adapted from Nat Eliason's MIT-licensed send-to-openclaw. Co-authored-by: Codex <codex@openai.com> * fix(browser): keep page-controlled metadata inside the share safety boundary Review findings: move title/URL inside wrapExternalContent (a hostile <title> must not become trusted header text), prefer the user's selection over the full Google Docs export, and pass the context-menu selectionText through so iframe selections and selections cleared during relay reconnect still win. * fix(browser): bind context-menu shares to the click-time document Selection shares from the context menu now send the click snapshot directly (no recapture), so navigations during relay reconnect cannot mislabel the source and iframe selections are preserved. The Google Docs selection probe scans all accessible frames before falling back to the full-document export. * test(browser): expect the page-share handler in relay server args * fix(browser): probe only the main frame for Google Docs selections All-frame injection rejects wholesale when one frame is inaccessible and returns child frames in nondeterministic order, so the probe now reads the main frame only. Child-frame selections still share correctly through the context menu's click-time selectionText; toolbar/shortcut entry sends the full page for that case (named tradeoff in the code comment). * fix(browser): satisfy page-share CI gates --------- Co-authored-by: Codex <codex@openai.com>
72 lines
2.6 KiB
TypeScript
72 lines
2.6 KiB
TypeScript
/**
|
|
* Browser plugin service factory that lazily starts the control server.
|
|
*/
|
|
import {
|
|
startLazyPluginServiceModule,
|
|
type LazyPluginServiceHandle,
|
|
type OpenClawPluginService,
|
|
} from "./sdk-node-runtime.js";
|
|
|
|
type BrowserControlHandle = LazyPluginServiceHandle | null;
|
|
const EAGER_BROWSER_CONTROL_SERVICE_ENV = "OPENCLAW_EAGER_BROWSER_CONTROL_SERVER";
|
|
const UNSAFE_BROWSER_CONTROL_OVERRIDE_SPECIFIER = /^(?:data|http|https|node):/i;
|
|
|
|
function isTruthyEnvValue(value: string | undefined): boolean {
|
|
return /^(?:1|true|yes|on)$/iu.test(value?.trim() ?? "");
|
|
}
|
|
|
|
function validateBrowserControlOverrideSpecifier(specifier: string): string {
|
|
const trimmed = specifier.trim();
|
|
if (UNSAFE_BROWSER_CONTROL_OVERRIDE_SPECIFIER.test(trimmed)) {
|
|
throw new Error(`Refusing unsafe browser control override specifier: ${trimmed}`);
|
|
}
|
|
return trimmed;
|
|
}
|
|
|
|
/** Creates the Browser plugin service registered by the plugin entrypoint. */
|
|
export function createBrowserPluginService(): OpenClawPluginService {
|
|
let handle: BrowserControlHandle = null;
|
|
|
|
return {
|
|
id: "browser-control",
|
|
start: async () => {
|
|
const pageShare = await import("./browser/extension-relay/page-share.js");
|
|
// Plugin services start only in the Gateway process. The sink marks this
|
|
// process as able to deliver page shares to the main session.
|
|
pageShare.setPageShareSink(pageShare.createGatewayPageShareSink());
|
|
if (!isTruthyEnvValue(process.env[EAGER_BROWSER_CONTROL_SERVICE_ENV])) {
|
|
return;
|
|
}
|
|
if (handle) {
|
|
return;
|
|
}
|
|
handle = await startLazyPluginServiceModule({
|
|
skipEnvVar: "OPENCLAW_SKIP_BROWSER_CONTROL_SERVER",
|
|
overrideEnvVar: "OPENCLAW_BROWSER_CONTROL_MODULE",
|
|
validateOverrideSpecifier: validateBrowserControlOverrideSpecifier,
|
|
// Keep the default module import static so compiled builds still bundle it.
|
|
loadDefaultModule: async () => await import("./server.js"),
|
|
startExportNames: [
|
|
"startBrowserControlServiceFromConfig",
|
|
"startBrowserControlServerFromConfig",
|
|
],
|
|
stopExportNames: ["stopBrowserControlService", "stopBrowserControlServer"],
|
|
});
|
|
},
|
|
stop: async () => {
|
|
const { setPageShareSink } = await import("./browser/extension-relay/page-share.js");
|
|
setPageShareSink(null);
|
|
const current = handle;
|
|
if (current) {
|
|
await current.stop();
|
|
if (handle === current) {
|
|
handle = null;
|
|
}
|
|
return;
|
|
}
|
|
const { stopBrowserControlService } = await import("./control-service.js");
|
|
await stopBrowserControlService();
|
|
},
|
|
};
|
|
}
|