opendevbrowser 0.0.16 → 0.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -27
- package/dist/browser/annotation-manager.d.ts +3 -0
- package/dist/browser/annotation-manager.d.ts.map +1 -1
- package/dist/browser/browser-manager.d.ts +6 -1
- package/dist/browser/browser-manager.d.ts.map +1 -1
- package/dist/browser/canvas-client.d.ts +53 -0
- package/dist/browser/canvas-client.d.ts.map +1 -0
- package/dist/browser/canvas-code-sync-manager.d.ts +79 -0
- package/dist/browser/canvas-code-sync-manager.d.ts.map +1 -0
- package/dist/browser/canvas-manager.d.ts +94 -0
- package/dist/browser/canvas-manager.d.ts.map +1 -0
- package/dist/browser/canvas-runtime-preview-bridge.d.ts +20 -0
- package/dist/browser/canvas-runtime-preview-bridge.d.ts.map +1 -0
- package/dist/browser/canvas-session-sync-manager.d.ts +21 -0
- package/dist/browser/canvas-session-sync-manager.d.ts.map +1 -0
- package/dist/browser/manager-types.d.ts +13 -1
- package/dist/browser/manager-types.d.ts.map +1 -1
- package/dist/browser/ops-browser-manager.d.ts +11 -1
- package/dist/browser/ops-browser-manager.d.ts.map +1 -1
- package/dist/canvas/code-sync/apply-tsx.d.ts +23 -0
- package/dist/canvas/code-sync/apply-tsx.d.ts.map +1 -0
- package/dist/canvas/code-sync/graph.d.ts +5 -0
- package/dist/canvas/code-sync/graph.d.ts.map +1 -0
- package/dist/canvas/code-sync/hash.d.ts +3 -0
- package/dist/canvas/code-sync/hash.d.ts.map +1 -0
- package/dist/canvas/code-sync/import.d.ts +18 -0
- package/dist/canvas/code-sync/import.d.ts.map +1 -0
- package/dist/canvas/code-sync/manifest.d.ts +5 -0
- package/dist/canvas/code-sync/manifest.d.ts.map +1 -0
- package/dist/canvas/code-sync/tsx-adapter.d.ts +8 -0
- package/dist/canvas/code-sync/tsx-adapter.d.ts.map +1 -0
- package/dist/canvas/code-sync/types.d.ts +152 -0
- package/dist/canvas/code-sync/types.d.ts.map +1 -0
- package/dist/canvas/code-sync/write.d.ts +9 -0
- package/dist/canvas/code-sync/write.d.ts.map +1 -0
- package/dist/canvas/document-store.d.ts +81 -0
- package/dist/canvas/document-store.d.ts.map +1 -0
- package/dist/canvas/export.d.ts +12 -0
- package/dist/canvas/export.d.ts.map +1 -0
- package/dist/canvas/repo-store.d.ts +10 -0
- package/dist/canvas/repo-store.d.ts.map +1 -0
- package/dist/canvas/surface-palette.d.ts +15 -0
- package/dist/canvas/surface-palette.d.ts.map +1 -0
- package/dist/canvas/types.d.ts +255 -0
- package/dist/canvas/types.d.ts.map +1 -0
- package/dist/canvas-runtime-preview-bridge-HBEHXM4T.js +7 -0
- package/dist/canvas-runtime-preview-bridge-HBEHXM4T.js.map +1 -0
- package/dist/{chunk-ST7CO5FA.js → chunk-5J3IFL3X.js} +11577 -13539
- package/dist/chunk-5J3IFL3X.js.map +1 -0
- package/dist/chunk-D633UO34.js +8149 -0
- package/dist/chunk-D633UO34.js.map +1 -0
- package/dist/{chunk-7W3SPXIB.js → chunk-FUSXMW3G.js} +4 -1
- package/dist/chunk-TBUCZX4A.js +34 -0
- package/dist/chunk-TBUCZX4A.js.map +1 -0
- package/dist/chunk-V7KUDHDG.js +276 -0
- package/dist/chunk-V7KUDHDG.js.map +1 -0
- package/dist/chunk-Y2KL55OG.js +59 -0
- package/dist/chunk-Y2KL55OG.js.map +1 -0
- package/dist/cli/args.d.ts +3 -3
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/commands/annotate.d.ts +11 -0
- package/dist/cli/commands/annotate.d.ts.map +1 -1
- package/dist/cli/commands/canvas.d.ts +45 -0
- package/dist/cli/commands/canvas.d.ts.map +1 -0
- package/dist/cli/commands/devtools/perf.d.ts.map +1 -1
- package/dist/cli/commands/devtools/screenshot.d.ts +1 -0
- package/dist/cli/commands/devtools/screenshot.d.ts.map +1 -1
- package/dist/cli/commands/dom/attr.d.ts.map +1 -1
- package/dist/cli/commands/dom/checked.d.ts.map +1 -1
- package/dist/cli/commands/dom/enabled.d.ts.map +1 -1
- package/dist/cli/commands/dom/html.d.ts.map +1 -1
- package/dist/cli/commands/dom/text.d.ts.map +1 -1
- package/dist/cli/commands/dom/value.d.ts.map +1 -1
- package/dist/cli/commands/dom/visible.d.ts.map +1 -1
- package/dist/cli/commands/export/clone-component.d.ts +9 -0
- package/dist/cli/commands/export/clone-component.d.ts.map +1 -1
- package/dist/cli/commands/export/clone-page.d.ts +8 -0
- package/dist/cli/commands/export/clone-page.d.ts.map +1 -1
- package/dist/cli/commands/interact/check.d.ts.map +1 -1
- package/dist/cli/commands/interact/click.d.ts.map +1 -1
- package/dist/cli/commands/interact/hover.d.ts.map +1 -1
- package/dist/cli/commands/interact/press.d.ts.map +1 -1
- package/dist/cli/commands/interact/scroll-into-view.d.ts.map +1 -1
- package/dist/cli/commands/interact/scroll.d.ts.map +1 -1
- package/dist/cli/commands/interact/select.d.ts.map +1 -1
- package/dist/cli/commands/interact/type.d.ts.map +1 -1
- package/dist/cli/commands/interact/uncheck.d.ts.map +1 -1
- package/dist/cli/commands/native.d.ts +12 -1
- package/dist/cli/commands/native.d.ts.map +1 -1
- package/dist/cli/commands/nav/goto.d.ts.map +1 -1
- package/dist/cli/commands/nav/snapshot.d.ts.map +1 -1
- package/dist/cli/commands/nav/wait.d.ts.map +1 -1
- package/dist/cli/commands/serve.d.ts +5 -0
- package/dist/cli/commands/serve.d.ts.map +1 -1
- package/dist/cli/commands/session/connect.d.ts.map +1 -1
- package/dist/cli/commands/status.d.ts +5 -0
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/daemon-commands.d.ts.map +1 -1
- package/dist/cli/help.d.ts +5 -0
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/index.js +724 -163
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/remote-canvas-manager.d.ts +8 -0
- package/dist/cli/remote-canvas-manager.d.ts.map +1 -0
- package/dist/cli/remote-manager.d.ts +3 -1
- package/dist/cli/remote-manager.d.ts.map +1 -1
- package/dist/cli/remote-relay.d.ts +2 -0
- package/dist/cli/remote-relay.d.ts.map +1 -1
- package/dist/cli/utils/parse.d.ts +1 -0
- package/dist/cli/utils/parse.d.ts.map +1 -1
- package/dist/core/bootstrap.d.ts.map +1 -1
- package/dist/core/types.d.ts +2 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/fs-UMRKOBNN.js +7 -0
- package/dist/fs-UMRKOBNN.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +192 -67
- package/dist/index.js.map +1 -1
- package/dist/{macros-NUBRM44Y.js → macros-ND2M7LWU.js} +2 -2
- package/dist/opendevbrowser.d.ts.map +1 -1
- package/dist/opendevbrowser.js +192 -67
- package/dist/opendevbrowser.js.map +1 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/shopping/index.d.ts.map +1 -1
- package/dist/providers-G3LRHQXX.js +121 -0
- package/dist/providers-G3LRHQXX.js.map +1 -0
- package/dist/relay/protocol.d.ts +85 -3
- package/dist/relay/protocol.d.ts.map +1 -1
- package/dist/relay/relay-server.d.ts +14 -1
- package/dist/relay/relay-server.d.ts.map +1 -1
- package/dist/relay/relay-types.d.ts +3 -0
- package/dist/relay/relay-types.d.ts.map +1 -1
- package/dist/runtime-factory-BICHDPE7.js +13 -0
- package/dist/runtime-factory-BICHDPE7.js.map +1 -0
- package/dist/tools/annotate.d.ts.map +1 -1
- package/dist/tools/canvas.d.ts +4 -0
- package/dist/tools/canvas.d.ts.map +1 -0
- package/dist/tools/check.d.ts.map +1 -1
- package/dist/tools/click.d.ts.map +1 -1
- package/dist/tools/clone_component.d.ts.map +1 -1
- package/dist/tools/clone_page.d.ts.map +1 -1
- package/dist/tools/connect.d.ts.map +1 -1
- package/dist/tools/deps.d.ts +2 -0
- package/dist/tools/deps.d.ts.map +1 -1
- package/dist/tools/dom_get_html.d.ts.map +1 -1
- package/dist/tools/dom_get_text.d.ts.map +1 -1
- package/dist/tools/get_attr.d.ts.map +1 -1
- package/dist/tools/get_value.d.ts.map +1 -1
- package/dist/tools/goto.d.ts.map +1 -1
- package/dist/tools/hover.d.ts.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/is_checked.d.ts.map +1 -1
- package/dist/tools/is_enabled.d.ts.map +1 -1
- package/dist/tools/is_visible.d.ts.map +1 -1
- package/dist/tools/launch.d.ts.map +1 -1
- package/dist/tools/macro_resolve.d.ts.map +1 -1
- package/dist/tools/perf.d.ts.map +1 -1
- package/dist/tools/press.d.ts.map +1 -1
- package/dist/tools/product_video_run.d.ts.map +1 -1
- package/dist/tools/research_run.d.ts.map +1 -1
- package/dist/tools/response.d.ts +4 -1
- package/dist/tools/response.d.ts.map +1 -1
- package/dist/tools/screenshot.d.ts.map +1 -1
- package/dist/tools/scroll.d.ts.map +1 -1
- package/dist/tools/scroll_into_view.d.ts.map +1 -1
- package/dist/tools/select.d.ts.map +1 -1
- package/dist/tools/shopping_run.d.ts.map +1 -1
- package/dist/tools/snapshot.d.ts.map +1 -1
- package/dist/tools/type.d.ts.map +1 -1
- package/dist/tools/uncheck.d.ts.map +1 -1
- package/dist/tools/wait.d.ts.map +1 -1
- package/dist/tools/workflow-runtime.d.ts +1 -2
- package/dist/tools/workflow-runtime.d.ts.map +1 -1
- package/extension/canvas.html +636 -0
- package/extension/dist/annotate-content.css +15 -6
- package/extension/dist/annotate-content.js +119 -9
- package/extension/dist/annotation-payload.js +163 -0
- package/extension/dist/background.js +148 -18
- package/extension/dist/canvas/canvas-runtime.js +1061 -0
- package/extension/dist/canvas/model.js +213 -0
- package/extension/dist/canvas/viewport-fit.js +67 -0
- package/extension/dist/canvas-page.js +1801 -0
- package/extension/dist/ops/dom-bridge.js +116 -3
- package/extension/dist/ops/ops-runtime.js +508 -44
- package/extension/dist/ops/ops-session-store.js +21 -114
- package/extension/dist/ops/target-session-coordinator.js +157 -0
- package/extension/dist/popup.js +155 -31
- package/extension/dist/services/ConnectionManager.js +17 -0
- package/extension/dist/services/RelayClient.js +9 -0
- package/extension/dist/services/TabManager.js +35 -12
- package/extension/dist/types.js +2 -0
- package/extension/manifest.json +1 -1
- package/extension/popup.html +52 -0
- package/package.json +6 -4
- package/skills/AGENTS.md +5 -2
- package/skills/opendevbrowser-best-practices/SKILL.md +71 -3
- package/skills/opendevbrowser-best-practices/artifacts/canvas-governance-playbook.md +141 -0
- package/skills/opendevbrowser-best-practices/artifacts/command-channel-reference.md +113 -17
- package/skills/opendevbrowser-best-practices/assets/templates/canvas-blocker-checklist.json +70 -0
- package/skills/opendevbrowser-best-practices/assets/templates/canvas-feedback-eval.json +73 -0
- package/skills/opendevbrowser-best-practices/assets/templates/canvas-generation-plan.v1.json +67 -0
- package/skills/opendevbrowser-best-practices/assets/templates/canvas-handshake-example.json +126 -0
- package/skills/opendevbrowser-best-practices/assets/templates/robustness-checklist.json +57 -0
- package/skills/opendevbrowser-best-practices/assets/templates/surface-audit-checklist.json +7 -3
- package/skills/opendevbrowser-best-practices/scripts/odb-workflow.sh +26 -0
- package/skills/opendevbrowser-best-practices/scripts/run-robustness-audit.sh +82 -1
- package/skills/opendevbrowser-best-practices/scripts/validate-skill-assets.sh +225 -84
- package/dist/chunk-ST7CO5FA.js.map +0 -1
- /package/dist/{chunk-7W3SPXIB.js.map → chunk-FUSXMW3G.js.map} +0 -0
- /package/dist/{macros-NUBRM44Y.js.map → macros-ND2M7LWU.js.map} +0 -0
|
@@ -34,6 +34,7 @@ const TARGET_SCOPED_COMMANDS = new Set([
|
|
|
34
34
|
"dom.isVisible",
|
|
35
35
|
"dom.isEnabled",
|
|
36
36
|
"dom.isChecked",
|
|
37
|
+
"canvas.applyRuntimePreviewBridge",
|
|
37
38
|
"export.clonePage",
|
|
38
39
|
"export.cloneComponent",
|
|
39
40
|
"devtools.perf",
|
|
@@ -42,6 +43,8 @@ const TARGET_SCOPED_COMMANDS = new Set([
|
|
|
42
43
|
export class OpsRuntime {
|
|
43
44
|
sendEnvelope;
|
|
44
45
|
cdp;
|
|
46
|
+
getCanvasPageState;
|
|
47
|
+
performCanvasPageAction;
|
|
45
48
|
tabs = new TabManager();
|
|
46
49
|
dom = new DomBridge();
|
|
47
50
|
sessions = new OpsSessionStore();
|
|
@@ -51,6 +54,8 @@ export class OpsRuntime {
|
|
|
51
54
|
constructor(options) {
|
|
52
55
|
this.sendEnvelope = options.send;
|
|
53
56
|
this.cdp = options.cdp;
|
|
57
|
+
this.getCanvasPageState = options.getCanvasPageState;
|
|
58
|
+
this.performCanvasPageAction = options.performCanvasPageAction;
|
|
54
59
|
chrome.tabs.onRemoved.addListener(this.handleTabRemoved);
|
|
55
60
|
chrome.tabs.onUpdated.addListener(this.handleTabUpdated);
|
|
56
61
|
chrome.debugger.onEvent.addListener(this.handleDebuggerEvent);
|
|
@@ -252,6 +257,9 @@ export class OpsRuntime {
|
|
|
252
257
|
case "targets.use":
|
|
253
258
|
await this.withSession(message, clientId, (session) => this.handleTargetsUse(message, session));
|
|
254
259
|
return;
|
|
260
|
+
case "targets.registerCanvas":
|
|
261
|
+
await this.withSession(message, clientId, (session) => this.handleTargetsRegisterCanvas(message, session));
|
|
262
|
+
return;
|
|
255
263
|
case "targets.new":
|
|
256
264
|
await this.withSession(message, clientId, (session) => this.handleTargetsNew(message, session));
|
|
257
265
|
return;
|
|
@@ -324,6 +332,9 @@ export class OpsRuntime {
|
|
|
324
332
|
case "dom.isChecked":
|
|
325
333
|
await this.withSession(message, clientId, (session) => this.handleDomIsChecked(message, session));
|
|
326
334
|
return;
|
|
335
|
+
case "canvas.applyRuntimePreviewBridge":
|
|
336
|
+
await this.withSession(message, clientId, (session) => this.handleCanvasRuntimePreviewBridge(message, session));
|
|
337
|
+
return;
|
|
327
338
|
case "export.clonePage":
|
|
328
339
|
await this.withSession(message, clientId, (session) => this.handleClonePage(message, session));
|
|
329
340
|
return;
|
|
@@ -370,28 +381,36 @@ export class OpsRuntime {
|
|
|
370
381
|
this.sendError(message, buildError("ops_unavailable", "No active tab to attach.", true));
|
|
371
382
|
return;
|
|
372
383
|
}
|
|
373
|
-
|
|
374
|
-
|
|
384
|
+
const activeTabId = activeTab.id;
|
|
385
|
+
const resolvedTab = startUrl
|
|
386
|
+
? await this.tabs.waitForTabComplete(activeTabId)
|
|
387
|
+
.catch(() => undefined)
|
|
388
|
+
.then(async () => await this.tabs.getTab(activeTabId) ?? activeTab)
|
|
389
|
+
: activeTab;
|
|
390
|
+
if (resolvedTab.url) {
|
|
391
|
+
const restriction = isRestrictedUrl(resolvedTab.url);
|
|
375
392
|
if (restriction.restricted) {
|
|
376
393
|
this.sendError(message, buildError("restricted_url", restriction.message ?? "Restricted tab.", false));
|
|
377
394
|
return;
|
|
378
395
|
}
|
|
379
396
|
}
|
|
380
397
|
try {
|
|
381
|
-
await this.cdp.attach(
|
|
398
|
+
await this.cdp.attach(activeTabId);
|
|
382
399
|
}
|
|
383
400
|
catch (error) {
|
|
384
401
|
const detail = error instanceof Error ? error.message : "Debugger attach failed";
|
|
385
402
|
this.sendError(message, buildError("cdp_attach_failed", detail, false));
|
|
386
403
|
return;
|
|
387
404
|
}
|
|
388
|
-
|
|
405
|
+
if (!startUrl) {
|
|
406
|
+
await this.tabs.waitForTabComplete(activeTab.id).catch(() => undefined);
|
|
407
|
+
}
|
|
389
408
|
const leaseId = typeof message.leaseId === "string" && message.leaseId.trim().length > 0
|
|
390
409
|
? message.leaseId.trim()
|
|
391
410
|
: createId();
|
|
392
|
-
const session = this.sessions.createSession(clientId,
|
|
393
|
-
url:
|
|
394
|
-
title:
|
|
411
|
+
const session = this.sessions.createSession(clientId, activeTabId, leaseId, {
|
|
412
|
+
url: resolvedTab.url ?? undefined,
|
|
413
|
+
title: resolvedTab.title ?? undefined
|
|
395
414
|
}, {
|
|
396
415
|
parallelismPolicy
|
|
397
416
|
});
|
|
@@ -406,8 +425,8 @@ export class OpsRuntime {
|
|
|
406
425
|
this.sendResponse(message, {
|
|
407
426
|
opsSessionId: session.id,
|
|
408
427
|
activeTargetId: session.activeTargetId,
|
|
409
|
-
url:
|
|
410
|
-
title:
|
|
428
|
+
url: resolvedTab.url ?? undefined,
|
|
429
|
+
title: resolvedTab.title ?? undefined,
|
|
411
430
|
leaseId: session.leaseId
|
|
412
431
|
});
|
|
413
432
|
}
|
|
@@ -437,11 +456,12 @@ export class OpsRuntime {
|
|
|
437
456
|
const includeUrls = payload.includeUrls === true;
|
|
438
457
|
const targets = await Promise.all(Array.from(session.targets.values()).map(async (target) => {
|
|
439
458
|
const tab = await this.tabs.getTab(target.tabId);
|
|
459
|
+
const synthetic = session.syntheticTargets.get(target.targetId);
|
|
440
460
|
return {
|
|
441
461
|
targetId: target.targetId,
|
|
442
462
|
type: "page",
|
|
443
|
-
title: tab?.title
|
|
444
|
-
url: includeUrls ? tab?.url
|
|
463
|
+
title: resolveReportedTargetTitle(target, tab?.title, synthetic),
|
|
464
|
+
url: includeUrls ? resolveReportedTargetUrl(target, tab?.url, synthetic) : undefined
|
|
445
465
|
};
|
|
446
466
|
}));
|
|
447
467
|
this.sendResponse(message, { activeTargetId: session.activeTargetId || null, targets });
|
|
@@ -459,10 +479,66 @@ export class OpsRuntime {
|
|
|
459
479
|
await this.tabs.activateTab(target.tabId).catch(() => undefined);
|
|
460
480
|
}
|
|
461
481
|
const tab = target ? await this.tabs.getTab(target.tabId) : null;
|
|
482
|
+
const synthetic = target ? session.syntheticTargets.get(target.targetId) : undefined;
|
|
462
483
|
this.sendResponse(message, {
|
|
463
484
|
activeTargetId: targetId,
|
|
464
|
-
url:
|
|
465
|
-
title: tab?.title
|
|
485
|
+
url: target ? resolveReportedTargetUrl(target, tab?.url, synthetic) : undefined,
|
|
486
|
+
title: target ? resolveReportedTargetTitle(target, tab?.title, synthetic) : undefined
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
async handleTargetsRegisterCanvas(message, session) {
|
|
490
|
+
const payload = isRecord(message.payload) ? message.payload : {};
|
|
491
|
+
const targetId = typeof payload.targetId === "string" ? payload.targetId.trim() : "";
|
|
492
|
+
if (!targetId) {
|
|
493
|
+
this.sendError(message, buildError("invalid_request", "Missing targetId", false));
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const tabId = parseTabTargetId(targetId);
|
|
497
|
+
if (tabId === null) {
|
|
498
|
+
this.sendError(message, buildError("invalid_request", "Canvas targetId must be tab-<id>.", false));
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
let tab = await this.tabs.getTab(tabId);
|
|
502
|
+
if (!tab) {
|
|
503
|
+
this.sendError(message, buildError("invalid_request", "Unknown targetId", false));
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
await this.tabs.waitForTabComplete(tabId, 5000).catch(() => undefined);
|
|
507
|
+
tab = await this.tabs.getTab(tabId) ?? tab;
|
|
508
|
+
if (!this.isAllowedCanvasTargetUrl(tab.url)) {
|
|
509
|
+
this.sendError(message, buildError("restricted_url", "Only the extension canvas tab can be registered.", false));
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
const existing = session.targets.get(targetId);
|
|
513
|
+
if (existing) {
|
|
514
|
+
existing.url = tab.url ?? existing.url;
|
|
515
|
+
existing.title = tab.title ?? existing.title;
|
|
516
|
+
session.activeTargetId = targetId;
|
|
517
|
+
this.sendResponse(message, {
|
|
518
|
+
targetId,
|
|
519
|
+
url: existing.url,
|
|
520
|
+
title: existing.title,
|
|
521
|
+
adopted: false
|
|
522
|
+
});
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
try {
|
|
526
|
+
await this.cdp.attach(tabId);
|
|
527
|
+
await this.enableTargetDomains(tabId);
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
logError("ops.register_canvas_target", error, {
|
|
531
|
+
code: "canvas_target_attach_failed",
|
|
532
|
+
extra: { tabId, targetId }
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
const target = this.sessions.addTarget(session.id, tabId, { url: tab.url ?? undefined, title: tab.title ?? undefined });
|
|
536
|
+
session.activeTargetId = target.targetId;
|
|
537
|
+
this.sendResponse(message, {
|
|
538
|
+
targetId: target.targetId,
|
|
539
|
+
url: target.url,
|
|
540
|
+
title: target.title,
|
|
541
|
+
adopted: true
|
|
466
542
|
});
|
|
467
543
|
}
|
|
468
544
|
async handleTargetsNew(message, session) {
|
|
@@ -544,11 +620,12 @@ export class OpsRuntime {
|
|
|
544
620
|
const pages = await Promise.all(this.sessions.listNamedTargets(session.id).map(async ({ name, targetId }) => {
|
|
545
621
|
const target = session.targets.get(targetId);
|
|
546
622
|
const tab = target ? await this.tabs.getTab(target.tabId) : null;
|
|
623
|
+
const synthetic = session.syntheticTargets.get(targetId);
|
|
547
624
|
return {
|
|
548
625
|
name,
|
|
549
626
|
targetId,
|
|
550
|
-
url: tab?.url
|
|
551
|
-
title: tab?.title
|
|
627
|
+
url: resolveReportedTargetUrl(target, tab?.url, synthetic),
|
|
628
|
+
title: resolveReportedTargetTitle(target, tab?.title, synthetic)
|
|
552
629
|
};
|
|
553
630
|
}));
|
|
554
631
|
this.sendResponse(message, { pages });
|
|
@@ -601,6 +678,24 @@ export class OpsRuntime {
|
|
|
601
678
|
if (!target)
|
|
602
679
|
return;
|
|
603
680
|
await this.tabs.activateTab(target.tabId).catch(() => undefined);
|
|
681
|
+
const targetRecord = session.targets.get(target.targetId);
|
|
682
|
+
const syntheticHtml = decodeHtmlDataUrl(url);
|
|
683
|
+
if (syntheticHtml !== null) {
|
|
684
|
+
const result = await executeInTab(target.tabId, replaceDocumentWithHtmlScript, [{ html: syntheticHtml }]);
|
|
685
|
+
session.refStore.clearTarget(target.targetId);
|
|
686
|
+
session.syntheticTargets.set(target.targetId, {
|
|
687
|
+
url,
|
|
688
|
+
title: typeof result?.title === "string" && result.title.trim().length > 0
|
|
689
|
+
? result.title
|
|
690
|
+
: targetRecord?.title
|
|
691
|
+
});
|
|
692
|
+
this.sendResponse(message, {
|
|
693
|
+
finalUrl: url,
|
|
694
|
+
status: undefined,
|
|
695
|
+
timingMs: Date.now() - start
|
|
696
|
+
});
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
604
699
|
const updated = await new Promise((resolve) => {
|
|
605
700
|
chrome.tabs.update(target.tabId, { url }, (tab) => {
|
|
606
701
|
resolve(tab ?? null);
|
|
@@ -608,10 +703,13 @@ export class OpsRuntime {
|
|
|
608
703
|
});
|
|
609
704
|
await this.tabs.waitForTabComplete(target.tabId, timeoutMs).catch(() => undefined);
|
|
610
705
|
const refreshed = await this.tabs.getTab(target.tabId);
|
|
611
|
-
|
|
706
|
+
session.syntheticTargets.delete(target.targetId);
|
|
612
707
|
if (targetRecord) {
|
|
613
|
-
|
|
614
|
-
|
|
708
|
+
session.targets.set(target.targetId, {
|
|
709
|
+
...targetRecord,
|
|
710
|
+
url: refreshed?.url ?? updated?.url ?? url,
|
|
711
|
+
title: refreshed?.title ?? updated?.title ?? targetRecord.title
|
|
712
|
+
});
|
|
615
713
|
}
|
|
616
714
|
this.sendResponse(message, {
|
|
617
715
|
finalUrl: refreshed?.url ?? updated?.url ?? url,
|
|
@@ -632,7 +730,7 @@ export class OpsRuntime {
|
|
|
632
730
|
if (!selector)
|
|
633
731
|
return;
|
|
634
732
|
try {
|
|
635
|
-
await this.waitForSelector(target
|
|
733
|
+
await this.waitForSelector(target, selector, state, timeoutMs);
|
|
636
734
|
this.sendResponse(message, { timingMs: Date.now() - start });
|
|
637
735
|
}
|
|
638
736
|
catch (error) {
|
|
@@ -671,10 +769,12 @@ export class OpsRuntime {
|
|
|
671
769
|
return;
|
|
672
770
|
}
|
|
673
771
|
const tab = await this.tabs.getTab(target.tabId);
|
|
772
|
+
const targetRecord = session.targets.get(target.targetId);
|
|
773
|
+
const synthetic = session.syntheticTargets.get(target.targetId);
|
|
674
774
|
this.sendResponse(message, {
|
|
675
775
|
snapshotId: snapshot.snapshotId,
|
|
676
|
-
url: tab?.url
|
|
677
|
-
title: tab?.title
|
|
776
|
+
url: resolveReportedTargetUrl(targetRecord ?? null, tab?.url, synthetic),
|
|
777
|
+
title: resolveReportedTargetTitle(targetRecord ?? null, tab?.title, synthetic),
|
|
678
778
|
content,
|
|
679
779
|
truncated,
|
|
680
780
|
nextCursor,
|
|
@@ -692,7 +792,7 @@ export class OpsRuntime {
|
|
|
692
792
|
return;
|
|
693
793
|
const start = Date.now();
|
|
694
794
|
const before = await this.tabs.getTab(target.tabId);
|
|
695
|
-
await this.dom.click(target.tabId, selector);
|
|
795
|
+
await this.runElementAction(target, selector, { type: "click" }, () => this.dom.click(target.tabId, selector));
|
|
696
796
|
const after = await this.tabs.getTab(target.tabId);
|
|
697
797
|
const navigated = Boolean(before?.url && after?.url && before.url !== after.url);
|
|
698
798
|
this.sendResponse(message, { timingMs: Date.now() - start, navigated });
|
|
@@ -705,7 +805,7 @@ export class OpsRuntime {
|
|
|
705
805
|
if (!target)
|
|
706
806
|
return;
|
|
707
807
|
const start = Date.now();
|
|
708
|
-
await this.dom.hover(target.tabId, selector);
|
|
808
|
+
await this.runElementAction(target, selector, { type: "hover" }, () => this.dom.hover(target.tabId, selector));
|
|
709
809
|
this.sendResponse(message, { timingMs: Date.now() - start });
|
|
710
810
|
}
|
|
711
811
|
async handlePress(message, session) {
|
|
@@ -722,7 +822,7 @@ export class OpsRuntime {
|
|
|
722
822
|
if (payload.ref && !selector)
|
|
723
823
|
return;
|
|
724
824
|
const start = Date.now();
|
|
725
|
-
await this.dom.press(target.tabId, selector, key);
|
|
825
|
+
await this.runCanvasPageAction(target, { type: "press", key }, selector, () => this.dom.press(target.tabId, selector, key));
|
|
726
826
|
this.sendResponse(message, { timingMs: Date.now() - start });
|
|
727
827
|
}
|
|
728
828
|
async handleCheck(message, session, checked) {
|
|
@@ -733,7 +833,7 @@ export class OpsRuntime {
|
|
|
733
833
|
if (!target)
|
|
734
834
|
return;
|
|
735
835
|
const start = Date.now();
|
|
736
|
-
await this.dom.setChecked(target.tabId, selector, checked);
|
|
836
|
+
await this.runElementAction(target, selector, { type: "setChecked", checked }, () => this.dom.setChecked(target.tabId, selector, checked));
|
|
737
837
|
this.sendResponse(message, { timingMs: Date.now() - start });
|
|
738
838
|
}
|
|
739
839
|
async handleType(message, session) {
|
|
@@ -751,7 +851,7 @@ export class OpsRuntime {
|
|
|
751
851
|
if (!target)
|
|
752
852
|
return;
|
|
753
853
|
const start = Date.now();
|
|
754
|
-
await this.dom.type(target.tabId, selector, text, payload.clear === true, payload.submit === true);
|
|
854
|
+
await this.runElementAction(target, selector, { type: "type", value: text, clear: payload.clear === true, submit: payload.submit === true }, () => this.dom.type(target.tabId, selector, text, payload.clear === true, payload.submit === true));
|
|
755
855
|
this.sendResponse(message, { timingMs: Date.now() - start });
|
|
756
856
|
}
|
|
757
857
|
async handleSelect(message, session) {
|
|
@@ -768,7 +868,7 @@ export class OpsRuntime {
|
|
|
768
868
|
const target = this.requireActiveTarget(session, message);
|
|
769
869
|
if (!target)
|
|
770
870
|
return;
|
|
771
|
-
await this.dom.select(target.tabId, selector, values);
|
|
871
|
+
await this.runElementAction(target, selector, { type: "select", values: values }, () => this.dom.select(target.tabId, selector, values));
|
|
772
872
|
this.sendResponse(message, {});
|
|
773
873
|
}
|
|
774
874
|
async handleScroll(message, session) {
|
|
@@ -781,7 +881,7 @@ export class OpsRuntime {
|
|
|
781
881
|
const target = this.requireActiveTarget(session, message);
|
|
782
882
|
if (!target)
|
|
783
883
|
return;
|
|
784
|
-
await this.dom.scroll(target.tabId, dy, selector);
|
|
884
|
+
await this.runCanvasPageAction(target, { type: "scroll", dy }, selector ?? null, () => this.dom.scroll(target.tabId, dy, selector));
|
|
785
885
|
this.sendResponse(message, {});
|
|
786
886
|
}
|
|
787
887
|
async handleScrollIntoView(message, session) {
|
|
@@ -792,7 +892,7 @@ export class OpsRuntime {
|
|
|
792
892
|
if (!target)
|
|
793
893
|
return;
|
|
794
894
|
const start = Date.now();
|
|
795
|
-
await this.dom.scrollIntoView(target.tabId, selector);
|
|
895
|
+
await this.runElementAction(target, selector, { type: "scrollIntoView" }, () => this.dom.scrollIntoView(target.tabId, selector));
|
|
796
896
|
this.sendResponse(message, { timingMs: Date.now() - start });
|
|
797
897
|
}
|
|
798
898
|
async handleDomGetHtml(message, session) {
|
|
@@ -809,7 +909,7 @@ export class OpsRuntime {
|
|
|
809
909
|
const target = this.requireActiveTarget(session, message);
|
|
810
910
|
if (!target)
|
|
811
911
|
return;
|
|
812
|
-
const html = await this.dom.getOuterHtml(target.tabId, selector);
|
|
912
|
+
const html = await this.runElementAction(target, selector, { type: "outerHTML" }, () => this.dom.getOuterHtml(target.tabId, selector));
|
|
813
913
|
const truncated = html.length > maxChars;
|
|
814
914
|
const outerHTML = truncated ? html.slice(0, maxChars) : html;
|
|
815
915
|
this.sendResponse(message, { outerHTML, truncated });
|
|
@@ -828,7 +928,7 @@ export class OpsRuntime {
|
|
|
828
928
|
const target = this.requireActiveTarget(session, message);
|
|
829
929
|
if (!target)
|
|
830
930
|
return;
|
|
831
|
-
const text = await this.dom.getInnerText(target.tabId, selector);
|
|
931
|
+
const text = await this.runElementAction(target, selector, { type: "innerText" }, () => this.dom.getInnerText(target.tabId, selector));
|
|
832
932
|
const truncated = text.length > maxChars;
|
|
833
933
|
this.sendResponse(message, { text: truncated ? text.slice(0, maxChars) : text, truncated });
|
|
834
934
|
}
|
|
@@ -846,7 +946,7 @@ export class OpsRuntime {
|
|
|
846
946
|
const target = this.requireActiveTarget(session, message);
|
|
847
947
|
if (!target)
|
|
848
948
|
return;
|
|
849
|
-
const value = await this.dom.getAttr(target.tabId, selector, name);
|
|
949
|
+
const value = await this.runElementAction(target, selector, { type: "getAttr", name }, () => this.dom.getAttr(target.tabId, selector, name));
|
|
850
950
|
this.sendResponse(message, { value });
|
|
851
951
|
}
|
|
852
952
|
async handleDomGetValue(message, session) {
|
|
@@ -862,7 +962,7 @@ export class OpsRuntime {
|
|
|
862
962
|
const target = this.requireActiveTarget(session, message);
|
|
863
963
|
if (!target)
|
|
864
964
|
return;
|
|
865
|
-
const value = await this.dom.getValue(target.tabId, selector);
|
|
965
|
+
const value = await this.runElementAction(target, selector, { type: "getValue" }, () => this.dom.getValue(target.tabId, selector));
|
|
866
966
|
this.sendResponse(message, { value });
|
|
867
967
|
}
|
|
868
968
|
async handleDomIsVisible(message, session) {
|
|
@@ -872,8 +972,11 @@ export class OpsRuntime {
|
|
|
872
972
|
const target = this.requireActiveTarget(session, message);
|
|
873
973
|
if (!target)
|
|
874
974
|
return;
|
|
875
|
-
const visible = await this.dom.
|
|
876
|
-
|
|
975
|
+
const visible = await this.runElementAction(target, selector, { type: "getSelectorState" }, async () => await this.dom.getSelectorState(target.tabId, selector));
|
|
976
|
+
const isVisible = typeof visible === "object" && visible !== null && "visible" in visible
|
|
977
|
+
? Boolean(visible.visible)
|
|
978
|
+
: Boolean(visible);
|
|
979
|
+
this.sendResponse(message, { value: isVisible });
|
|
877
980
|
}
|
|
878
981
|
async handleDomIsEnabled(message, session) {
|
|
879
982
|
const selector = this.resolveSelector(session, message.payload, message);
|
|
@@ -882,7 +985,7 @@ export class OpsRuntime {
|
|
|
882
985
|
const target = this.requireActiveTarget(session, message);
|
|
883
986
|
if (!target)
|
|
884
987
|
return;
|
|
885
|
-
const enabled = await this.dom.isEnabled(target.tabId, selector);
|
|
988
|
+
const enabled = await this.runElementAction(target, selector, { type: "isEnabled" }, () => this.dom.isEnabled(target.tabId, selector));
|
|
886
989
|
this.sendResponse(message, { value: enabled });
|
|
887
990
|
}
|
|
888
991
|
async handleDomIsChecked(message, session) {
|
|
@@ -892,14 +995,34 @@ export class OpsRuntime {
|
|
|
892
995
|
const target = this.requireActiveTarget(session, message);
|
|
893
996
|
if (!target)
|
|
894
997
|
return;
|
|
895
|
-
const checked = await this.dom.isChecked(target.tabId, selector);
|
|
998
|
+
const checked = await this.runElementAction(target, selector, { type: "isChecked" }, () => this.dom.isChecked(target.tabId, selector));
|
|
896
999
|
this.sendResponse(message, { value: checked });
|
|
897
1000
|
}
|
|
1001
|
+
async handleCanvasRuntimePreviewBridge(message, session) {
|
|
1002
|
+
const payload = isRecord(message.payload) ? message.payload : {};
|
|
1003
|
+
const bindingId = typeof payload.bindingId === "string" ? payload.bindingId.trim() : "";
|
|
1004
|
+
const rootSelector = typeof payload.rootSelector === "string" ? payload.rootSelector.trim() : "";
|
|
1005
|
+
const html = typeof payload.html === "string" ? payload.html : "";
|
|
1006
|
+
if (!bindingId || !rootSelector) {
|
|
1007
|
+
this.sendError(message, buildError("invalid_request", "Missing bindingId or rootSelector", false));
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
const target = this.requireActiveTarget(session, message);
|
|
1011
|
+
if (!target)
|
|
1012
|
+
return;
|
|
1013
|
+
const result = await this.dom.applyRuntimePreviewBridge(target.tabId, bindingId, rootSelector, html);
|
|
1014
|
+
this.sendResponse(message, result);
|
|
1015
|
+
}
|
|
898
1016
|
async handleClonePage(message, session) {
|
|
899
1017
|
const payload = isRecord(message.payload) ? message.payload : {};
|
|
900
1018
|
const target = this.requireActiveTarget(session, message);
|
|
901
1019
|
if (!target)
|
|
902
1020
|
return;
|
|
1021
|
+
const canvasCapture = await this.captureCanvasPage(target.tabId, target.targetId);
|
|
1022
|
+
if (canvasCapture) {
|
|
1023
|
+
this.sendResponse(message, { capture: canvasCapture });
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
903
1026
|
const capture = await this.dom.captureDom(target.tabId, "body", {
|
|
904
1027
|
sanitize: payload.sanitize !== false,
|
|
905
1028
|
maxNodes: typeof payload.maxNodes === "number" ? payload.maxNodes : undefined,
|
|
@@ -1062,10 +1185,13 @@ export class OpsRuntime {
|
|
|
1062
1185
|
});
|
|
1063
1186
|
}
|
|
1064
1187
|
async enableSessionDomains(session) {
|
|
1188
|
+
await this.enableTargetDomains(session.tabId);
|
|
1189
|
+
}
|
|
1190
|
+
async enableTargetDomains(tabId) {
|
|
1065
1191
|
try {
|
|
1066
|
-
await this.cdp.sendCommand({ tabId
|
|
1067
|
-
await this.cdp.sendCommand({ tabId
|
|
1068
|
-
await this.cdp.sendCommand({ tabId
|
|
1192
|
+
await this.cdp.sendCommand({ tabId }, "Runtime.enable", {});
|
|
1193
|
+
await this.cdp.sendCommand({ tabId }, "Network.enable", {});
|
|
1194
|
+
await this.cdp.sendCommand({ tabId }, "Performance.enable", {});
|
|
1069
1195
|
}
|
|
1070
1196
|
catch (error) {
|
|
1071
1197
|
logError("ops.enable_domains", error, { code: "enable_domains_failed" });
|
|
@@ -1297,12 +1423,99 @@ export class OpsRuntime {
|
|
|
1297
1423
|
}
|
|
1298
1424
|
if (target.url) {
|
|
1299
1425
|
const restriction = isRestrictedUrl(target.url);
|
|
1300
|
-
if (restriction.restricted) {
|
|
1426
|
+
if (restriction.restricted && !this.isAllowedCanvasTargetUrl(target.url)) {
|
|
1301
1427
|
this.sendError(message, buildError("restricted_url", restriction.message ?? "Restricted tab.", false));
|
|
1302
1428
|
return null;
|
|
1303
1429
|
}
|
|
1304
1430
|
}
|
|
1305
|
-
return { tabId: target.tabId, targetId: target.targetId };
|
|
1431
|
+
return { tabId: target.tabId, targetId: target.targetId, url: target.url };
|
|
1432
|
+
}
|
|
1433
|
+
isAllowedCanvasTargetUrl(rawUrl) {
|
|
1434
|
+
if (typeof rawUrl !== "string" || rawUrl.length === 0) {
|
|
1435
|
+
return false;
|
|
1436
|
+
}
|
|
1437
|
+
try {
|
|
1438
|
+
const allowedUrl = chrome.runtime.getURL("canvas.html");
|
|
1439
|
+
return rawUrl === allowedUrl || rawUrl.startsWith(`${allowedUrl}#`) || rawUrl.startsWith(`${allowedUrl}?`);
|
|
1440
|
+
}
|
|
1441
|
+
catch {
|
|
1442
|
+
return false;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
async captureCanvasPage(tabId, targetId) {
|
|
1446
|
+
if (!this.getCanvasPageState) {
|
|
1447
|
+
return null;
|
|
1448
|
+
}
|
|
1449
|
+
const state = this.getCanvasPageState(targetId);
|
|
1450
|
+
if (!state) {
|
|
1451
|
+
return null;
|
|
1452
|
+
}
|
|
1453
|
+
const previewHtml = typeof state.html === "string" && state.html.length > 0
|
|
1454
|
+
? extractBodyHtml(state.html)
|
|
1455
|
+
: null;
|
|
1456
|
+
const shouldProbeLiveStage = Boolean(state.pendingMutation)
|
|
1457
|
+
|| (canvasStateContainsRichMedia(state) && !htmlContainsRichMedia(previewHtml));
|
|
1458
|
+
if (shouldProbeLiveStage) {
|
|
1459
|
+
const liveStageCapture = await this.captureLiveCanvasStage(tabId);
|
|
1460
|
+
if (liveStageCapture) {
|
|
1461
|
+
return liveStageCapture;
|
|
1462
|
+
}
|
|
1463
|
+
const documentCapture = buildCanvasDocumentCapture(state);
|
|
1464
|
+
if (documentCapture) {
|
|
1465
|
+
return documentCapture;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
if (!previewHtml) {
|
|
1469
|
+
return buildCanvasDocumentCapture(state);
|
|
1470
|
+
}
|
|
1471
|
+
return {
|
|
1472
|
+
html: previewHtml,
|
|
1473
|
+
styles: {},
|
|
1474
|
+
warnings: ["canvas_state_capture"],
|
|
1475
|
+
inlineStyles: false
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
async runElementAction(target, selector, action, fallback) {
|
|
1479
|
+
return await this.runCanvasPageAction(target, action, selector, fallback);
|
|
1480
|
+
}
|
|
1481
|
+
async runCanvasPageAction(target, action, selector, fallback) {
|
|
1482
|
+
if (!this.isAllowedCanvasTargetUrl(target.url) || !this.performCanvasPageAction) {
|
|
1483
|
+
return await fallback();
|
|
1484
|
+
}
|
|
1485
|
+
return await this.performCanvasPageAction(target.targetId, action, selector ?? null);
|
|
1486
|
+
}
|
|
1487
|
+
async captureLiveCanvasStage(tabId) {
|
|
1488
|
+
try {
|
|
1489
|
+
const results = await chrome.scripting.executeScript({
|
|
1490
|
+
target: { tabId },
|
|
1491
|
+
func: () => {
|
|
1492
|
+
const stage = document.getElementById("canvas-stage-inner");
|
|
1493
|
+
if (!(stage instanceof HTMLElement)) {
|
|
1494
|
+
return null;
|
|
1495
|
+
}
|
|
1496
|
+
const html = stage.innerHTML.trim();
|
|
1497
|
+
if (!html) {
|
|
1498
|
+
return null;
|
|
1499
|
+
}
|
|
1500
|
+
const width = stage.style.width || `${Math.max(stage.scrollWidth, 320)}px`;
|
|
1501
|
+
const height = stage.style.height || `${Math.max(stage.scrollHeight, 240)}px`;
|
|
1502
|
+
return `<body><main data-surface="canvas" style="position:relative;width:${width};min-height:${height};">${html}</main></body>`;
|
|
1503
|
+
}
|
|
1504
|
+
});
|
|
1505
|
+
const html = typeof results[0]?.result === "string" ? results[0].result : null;
|
|
1506
|
+
if (!html) {
|
|
1507
|
+
return null;
|
|
1508
|
+
}
|
|
1509
|
+
return {
|
|
1510
|
+
html,
|
|
1511
|
+
styles: {},
|
|
1512
|
+
warnings: ["canvas_state_capture"],
|
|
1513
|
+
inlineStyles: true
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
catch {
|
|
1517
|
+
return null;
|
|
1518
|
+
}
|
|
1306
1519
|
}
|
|
1307
1520
|
resolveSelector(session, refOrPayload, message) {
|
|
1308
1521
|
const ref = typeof refOrPayload === "string"
|
|
@@ -1324,10 +1537,10 @@ export class OpsRuntime {
|
|
|
1324
1537
|
}
|
|
1325
1538
|
return entry.selector;
|
|
1326
1539
|
}
|
|
1327
|
-
async waitForSelector(
|
|
1540
|
+
async waitForSelector(target, selector, state, timeoutMs) {
|
|
1328
1541
|
const start = Date.now();
|
|
1329
1542
|
while (Date.now() - start < timeoutMs) {
|
|
1330
|
-
const snapshot = await this.dom.getSelectorState(tabId, selector);
|
|
1543
|
+
const snapshot = await this.runElementAction(target, selector, { type: "getSelectorState" }, () => this.dom.getSelectorState(target.tabId, selector));
|
|
1331
1544
|
if (state === "attached" && snapshot.attached)
|
|
1332
1545
|
return;
|
|
1333
1546
|
if (state === "visible" && snapshot.visible)
|
|
@@ -1729,6 +1942,257 @@ const paginate = (lines, startIndex, maxChars) => {
|
|
|
1729
1942
|
};
|
|
1730
1943
|
};
|
|
1731
1944
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1945
|
+
const parseTabTargetId = (targetId) => {
|
|
1946
|
+
const raw = targetId.startsWith("tab-") ? targetId.slice(4) : targetId;
|
|
1947
|
+
const parsed = Number.parseInt(raw, 10);
|
|
1948
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
1949
|
+
return null;
|
|
1950
|
+
}
|
|
1951
|
+
return parsed;
|
|
1952
|
+
};
|
|
1953
|
+
const extractBodyHtml = (html) => {
|
|
1954
|
+
const bodyMatch = html.match(/<body\b[^>]*>[\s\S]*<\/body>/i);
|
|
1955
|
+
if (bodyMatch) {
|
|
1956
|
+
return bodyMatch[0];
|
|
1957
|
+
}
|
|
1958
|
+
return html;
|
|
1959
|
+
};
|
|
1960
|
+
const htmlContainsRichMedia = (html) => {
|
|
1961
|
+
return typeof html === "string" && /<(img|video|audio)\b/i.test(html);
|
|
1962
|
+
};
|
|
1963
|
+
const canvasStateContainsRichMedia = (state) => {
|
|
1964
|
+
const document = isRecord(state.document) ? state.document : null;
|
|
1965
|
+
const pages = Array.isArray(document?.pages) ? document.pages : [];
|
|
1966
|
+
const assets = Array.isArray(document?.assets) ? document.assets : [];
|
|
1967
|
+
const assetsById = new Map(assets.flatMap((asset) => typeof asset?.id === "string" ? [[asset.id, asset]] : []));
|
|
1968
|
+
return pages.some((page) => Array.isArray(page?.nodes) && page.nodes.some((node) => nodeContainsRichMedia(node, assetsById)));
|
|
1969
|
+
};
|
|
1970
|
+
const nodeContainsRichMedia = (node, assetsById) => {
|
|
1971
|
+
const tagName = readCanvasMediaTagName(node);
|
|
1972
|
+
if (tagName === "img" || tagName === "video" || tagName === "audio") {
|
|
1973
|
+
return true;
|
|
1974
|
+
}
|
|
1975
|
+
const assetIds = Array.isArray(node.metadata.assetIds)
|
|
1976
|
+
? node.metadata.assetIds.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
|
|
1977
|
+
: [];
|
|
1978
|
+
return assetIds.some((assetId) => {
|
|
1979
|
+
const asset = assetsById.get(assetId);
|
|
1980
|
+
const kind = typeof asset?.kind === "string" ? asset.kind.toLowerCase() : "";
|
|
1981
|
+
const mime = typeof asset?.mime === "string" ? asset.mime.toLowerCase() : "";
|
|
1982
|
+
return kind === "image" || kind === "video" || kind === "audio" || mime.startsWith("image/") || mime.startsWith("video/") || mime.startsWith("audio/");
|
|
1983
|
+
});
|
|
1984
|
+
};
|
|
1985
|
+
const readCanvasMediaTagName = (node) => {
|
|
1986
|
+
if (typeof node.props.tagName === "string" && node.props.tagName.trim().length > 0) {
|
|
1987
|
+
return node.props.tagName.trim().toLowerCase();
|
|
1988
|
+
}
|
|
1989
|
+
const codeSync = isRecord(node.metadata.codeSync) ? node.metadata.codeSync : null;
|
|
1990
|
+
if (codeSync && typeof codeSync.tagName === "string" && codeSync.tagName.trim().length > 0) {
|
|
1991
|
+
return codeSync.tagName.trim().toLowerCase();
|
|
1992
|
+
}
|
|
1993
|
+
return null;
|
|
1994
|
+
};
|
|
1995
|
+
const buildCanvasDocumentCapture = (state) => {
|
|
1996
|
+
const page = Array.isArray(state.document.pages) ? state.document.pages[0] : null;
|
|
1997
|
+
if (!page || !Array.isArray(page.nodes) || page.nodes.length === 0) {
|
|
1998
|
+
return null;
|
|
1999
|
+
}
|
|
2000
|
+
const { width, height } = computeCanvasDocumentBounds(page.nodes);
|
|
2001
|
+
const nodes = [...page.nodes]
|
|
2002
|
+
.sort(compareCanvasCaptureNodes)
|
|
2003
|
+
.map((node) => renderCanvasDocumentNode(state.document, node))
|
|
2004
|
+
.join("");
|
|
2005
|
+
return {
|
|
2006
|
+
html: `<body><main data-surface="canvas" style="position:relative;width:${width}px;min-height:${height}px;">${nodes}</main></body>`,
|
|
2007
|
+
styles: {},
|
|
2008
|
+
warnings: ["canvas_state_capture"],
|
|
2009
|
+
inlineStyles: true
|
|
2010
|
+
};
|
|
2011
|
+
};
|
|
2012
|
+
const computeCanvasDocumentBounds = (nodes) => {
|
|
2013
|
+
if (nodes.length === 0) {
|
|
2014
|
+
return { width: 1600, height: 1200 };
|
|
2015
|
+
}
|
|
2016
|
+
const maxX = Math.max(...nodes.map((node) => node.rect.x + node.rect.width));
|
|
2017
|
+
const maxY = Math.max(...nodes.map((node) => node.rect.y + node.rect.height));
|
|
2018
|
+
return {
|
|
2019
|
+
width: Math.max(maxX + 240, 1600),
|
|
2020
|
+
height: Math.max(maxY + 240, 1200)
|
|
2021
|
+
};
|
|
2022
|
+
};
|
|
2023
|
+
const compareCanvasCaptureNodes = (left, right) => {
|
|
2024
|
+
const rootOrder = Number(left.parentId !== null) - Number(right.parentId !== null);
|
|
2025
|
+
if (rootOrder !== 0) {
|
|
2026
|
+
return rootOrder;
|
|
2027
|
+
}
|
|
2028
|
+
const areaOrder = (right.rect.width * right.rect.height) - (left.rect.width * left.rect.height);
|
|
2029
|
+
if (areaOrder !== 0) {
|
|
2030
|
+
return areaOrder;
|
|
2031
|
+
}
|
|
2032
|
+
const verticalOrder = left.rect.y - right.rect.y;
|
|
2033
|
+
return verticalOrder !== 0 ? verticalOrder : left.rect.x - right.rect.x;
|
|
2034
|
+
};
|
|
2035
|
+
const renderCanvasDocumentNode = (document, node) => {
|
|
2036
|
+
const media = resolveCanvasDocumentMedia(document, node);
|
|
2037
|
+
const text = escapeCanvasHtml(nodeTextForCapture(node) || node.name);
|
|
2038
|
+
const style = serializeCanvasCaptureStyle({
|
|
2039
|
+
position: "absolute",
|
|
2040
|
+
left: `${node.rect.x}px`,
|
|
2041
|
+
top: `${node.rect.y}px`,
|
|
2042
|
+
width: `${Math.max(node.rect.width, 40)}px`,
|
|
2043
|
+
minHeight: `${Math.max(node.rect.height, readCanvasMediaTagName(node) === "audio" ? 64 : 40)}px`,
|
|
2044
|
+
overflow: "hidden",
|
|
2045
|
+
...node.style
|
|
2046
|
+
});
|
|
2047
|
+
const title = escapeCanvasAttribute(`${node.kind} • ${node.name}`);
|
|
2048
|
+
if (media?.kind === "image" && media.src) {
|
|
2049
|
+
return `<div data-node-id="${escapeCanvasAttribute(node.id)}" title="${title}" style="${style}"><img src="${escapeCanvasAttribute(media.src)}" alt="${escapeCanvasAttribute(media.alt ?? node.name)}" loading="lazy" draggable="false" style="width:100%;height:100%;object-fit:cover;display:block;" /></div>`;
|
|
2050
|
+
}
|
|
2051
|
+
if (media?.kind === "video" && media.src) {
|
|
2052
|
+
const poster = media.poster ? ` poster="${escapeCanvasAttribute(media.poster)}"` : "";
|
|
2053
|
+
return `<div data-node-id="${escapeCanvasAttribute(node.id)}" title="${title}" style="${style}"><video src="${escapeCanvasAttribute(media.src)}"${poster} muted loop autoplay playsinline preload="metadata" style="width:100%;height:100%;object-fit:cover;display:block;"></video></div>`;
|
|
2054
|
+
}
|
|
2055
|
+
if (media?.kind === "audio" && media.src) {
|
|
2056
|
+
return `<div data-node-id="${escapeCanvasAttribute(node.id)}" title="${title}" style="${style}"><audio src="${escapeCanvasAttribute(media.src)}" controls preload="metadata" style="width:100%;display:block;"></audio>${text ? `<div style="margin-top:8px;font:500 12px/1.4 sans-serif;">${text}</div>` : ""}</div>`;
|
|
2057
|
+
}
|
|
2058
|
+
return `<div data-node-id="${escapeCanvasAttribute(node.id)}" title="${title}" style="${style}">${text}</div>`;
|
|
2059
|
+
};
|
|
2060
|
+
const nodeTextForCapture = (node) => {
|
|
2061
|
+
const raw = node.props.text ?? node.metadata.text;
|
|
2062
|
+
if (raw !== undefined && raw !== null) {
|
|
2063
|
+
return typeof raw === "string" ? raw : String(raw);
|
|
2064
|
+
}
|
|
2065
|
+
return node.kind === "text" || node.kind === "note" || node.kind === "component-instance"
|
|
2066
|
+
? node.name
|
|
2067
|
+
: "";
|
|
2068
|
+
};
|
|
2069
|
+
const resolveCanvasDocumentMedia = (document, node) => {
|
|
2070
|
+
const tagName = readCanvasMediaTagName(node);
|
|
2071
|
+
const attributes = isRecord(node.props.attributes) ? node.props.attributes : {};
|
|
2072
|
+
const assetIds = Array.isArray(node.metadata.assetIds)
|
|
2073
|
+
? node.metadata.assetIds.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
|
|
2074
|
+
: [];
|
|
2075
|
+
const asset = assetIds.length > 0
|
|
2076
|
+
? document.assets.find((entry) => entry.id === assetIds[0])
|
|
2077
|
+
: null;
|
|
2078
|
+
const assetKind = typeof asset?.kind === "string" ? asset.kind.toLowerCase() : null;
|
|
2079
|
+
const assetMime = typeof asset?.mime === "string" ? asset.mime.toLowerCase() : null;
|
|
2080
|
+
const src = typeof node.props.src === "string"
|
|
2081
|
+
? node.props.src
|
|
2082
|
+
: typeof attributes.src === "string"
|
|
2083
|
+
? attributes.src
|
|
2084
|
+
: typeof asset?.url === "string"
|
|
2085
|
+
? asset.url
|
|
2086
|
+
: typeof asset?.repoPath === "string"
|
|
2087
|
+
? asset.repoPath
|
|
2088
|
+
: null;
|
|
2089
|
+
const poster = typeof node.props.poster === "string"
|
|
2090
|
+
? node.props.poster
|
|
2091
|
+
: typeof attributes.poster === "string"
|
|
2092
|
+
? attributes.poster
|
|
2093
|
+
: null;
|
|
2094
|
+
const alt = typeof node.props.alt === "string"
|
|
2095
|
+
? node.props.alt
|
|
2096
|
+
: typeof attributes.alt === "string"
|
|
2097
|
+
? attributes.alt
|
|
2098
|
+
: node.name;
|
|
2099
|
+
if (tagName === "img" || assetKind === "image" || assetMime?.startsWith("image/")) {
|
|
2100
|
+
return { kind: "image", src, poster: null, alt };
|
|
2101
|
+
}
|
|
2102
|
+
if (tagName === "video" || assetKind === "video" || assetMime?.startsWith("video/")) {
|
|
2103
|
+
return { kind: "video", src, poster, alt };
|
|
2104
|
+
}
|
|
2105
|
+
if (tagName === "audio" || assetKind === "audio" || assetMime?.startsWith("audio/")) {
|
|
2106
|
+
return { kind: "audio", src, poster: null, alt };
|
|
2107
|
+
}
|
|
2108
|
+
return null;
|
|
2109
|
+
};
|
|
2110
|
+
const serializeCanvasCaptureStyle = (style) => {
|
|
2111
|
+
return Object.entries(style)
|
|
2112
|
+
.flatMap(([key, value]) => {
|
|
2113
|
+
if (typeof value !== "string" && typeof value !== "number") {
|
|
2114
|
+
return [];
|
|
2115
|
+
}
|
|
2116
|
+
const cssKey = key.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
|
|
2117
|
+
const cssValue = typeof value === "number" && !CANVAS_CAPTURE_UNITLESS_STYLES.has(key) ? `${value}px` : String(value);
|
|
2118
|
+
return `${cssKey}:${escapeCanvasAttribute(cssValue)};`;
|
|
2119
|
+
})
|
|
2120
|
+
.join("");
|
|
2121
|
+
};
|
|
2122
|
+
const escapeCanvasHtml = (value) => {
|
|
2123
|
+
return value
|
|
2124
|
+
.replaceAll("&", "&")
|
|
2125
|
+
.replaceAll("<", "<")
|
|
2126
|
+
.replaceAll(">", ">");
|
|
2127
|
+
};
|
|
2128
|
+
const escapeCanvasAttribute = (value) => {
|
|
2129
|
+
return escapeCanvasHtml(value)
|
|
2130
|
+
.replaceAll("\"", """)
|
|
2131
|
+
.replaceAll("'", "'");
|
|
2132
|
+
};
|
|
2133
|
+
const CANVAS_CAPTURE_UNITLESS_STYLES = new Set(["fontWeight", "lineHeight", "opacity", "zIndex"]);
|
|
2134
|
+
const resolveReportedTargetUrl = (target, liveUrl, synthetic) => {
|
|
2135
|
+
if (typeof synthetic?.url === "string" && isHtmlDataUrl(synthetic.url)) {
|
|
2136
|
+
return synthetic.url;
|
|
2137
|
+
}
|
|
2138
|
+
if (typeof target?.url === "string" && isHtmlDataUrl(target.url)) {
|
|
2139
|
+
return target.url;
|
|
2140
|
+
}
|
|
2141
|
+
return liveUrl ?? target?.url;
|
|
2142
|
+
};
|
|
2143
|
+
const resolveReportedTargetTitle = (target, liveTitle, synthetic) => {
|
|
2144
|
+
if (typeof synthetic?.title === "string" && synthetic.title.length > 0) {
|
|
2145
|
+
return synthetic.title;
|
|
2146
|
+
}
|
|
2147
|
+
if (typeof target?.url === "string" && isHtmlDataUrl(target.url) && typeof target.title === "string" && target.title.length > 0) {
|
|
2148
|
+
return target.title;
|
|
2149
|
+
}
|
|
2150
|
+
return liveTitle ?? target?.title;
|
|
2151
|
+
};
|
|
2152
|
+
const isHtmlDataUrl = (url) => {
|
|
2153
|
+
return url.startsWith("data:text/html");
|
|
2154
|
+
};
|
|
2155
|
+
const decodeHtmlDataUrl = (url) => {
|
|
2156
|
+
if (!isHtmlDataUrl(url)) {
|
|
2157
|
+
return null;
|
|
2158
|
+
}
|
|
2159
|
+
const commaIndex = url.indexOf(",");
|
|
2160
|
+
if (commaIndex === -1) {
|
|
2161
|
+
return null;
|
|
2162
|
+
}
|
|
2163
|
+
const metadata = url.slice(0, commaIndex).toLowerCase();
|
|
2164
|
+
const payload = url.slice(commaIndex + 1);
|
|
2165
|
+
if (metadata.includes(";base64")) {
|
|
2166
|
+
const decoded = atob(payload);
|
|
2167
|
+
const bytes = Uint8Array.from(decoded, (char) => char.charCodeAt(0));
|
|
2168
|
+
return new TextDecoder().decode(bytes);
|
|
2169
|
+
}
|
|
2170
|
+
try {
|
|
2171
|
+
return decodeURIComponent(payload);
|
|
2172
|
+
}
|
|
2173
|
+
catch {
|
|
2174
|
+
return payload;
|
|
2175
|
+
}
|
|
2176
|
+
};
|
|
2177
|
+
const executeInTab = async (tabId, func, args) => {
|
|
2178
|
+
return await new Promise((resolve, reject) => {
|
|
2179
|
+
chrome.scripting.executeScript({ target: { tabId }, func: func, args }, (results) => {
|
|
2180
|
+
const lastError = chrome.runtime.lastError;
|
|
2181
|
+
if (lastError) {
|
|
2182
|
+
reject(new Error(lastError.message));
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
2185
|
+
const [first] = results ?? [];
|
|
2186
|
+
resolve((first?.result ?? null));
|
|
2187
|
+
});
|
|
2188
|
+
});
|
|
2189
|
+
};
|
|
2190
|
+
function replaceDocumentWithHtmlScript(input) {
|
|
2191
|
+
document.open();
|
|
2192
|
+
document.write(input.html);
|
|
2193
|
+
document.close();
|
|
2194
|
+
return { title: document.title };
|
|
2195
|
+
}
|
|
1732
2196
|
const withTimeout = async (promise, timeoutMs, message) => {
|
|
1733
2197
|
return await new Promise((resolve, reject) => {
|
|
1734
2198
|
const timeoutId = setTimeout(() => {
|