@weavetab/mcp 2.5.0-beta.0
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/AGENTS.md +48 -0
- package/ARCHITECTURE.md +590 -0
- package/LICENSE +631 -0
- package/README.md +909 -0
- package/SECURITY.md +196 -0
- package/TROUBLESHOOTING.md +365 -0
- package/dist/anticaptcha/detector.d.ts +49 -0
- package/dist/anticaptcha/detector.js +465 -0
- package/dist/anticaptcha/integration.d.ts +36 -0
- package/dist/anticaptcha/integration.js +123 -0
- package/dist/anticaptcha/solver.d.ts +27 -0
- package/dist/anticaptcha/solver.js +552 -0
- package/dist/audit/logger.d.ts +13 -0
- package/dist/audit/logger.js +177 -0
- package/dist/audit/telemetry.d.ts +36 -0
- package/dist/audit/telemetry.js +162 -0
- package/dist/cdp/bridge.d.ts +16 -0
- package/dist/cdp/bridge.js +826 -0
- package/dist/cdp/confirm.d.ts +26 -0
- package/dist/cdp/confirm.js +79 -0
- package/dist/cdp/connector.d.ts +37 -0
- package/dist/cdp/connector.js +661 -0
- package/dist/cdp/extension.d.ts +36 -0
- package/dist/cdp/extension.js +118 -0
- package/dist/cdp/finder.d.ts +1 -0
- package/dist/cdp/finder.js +2 -0
- package/dist/cdp/helpers.d.ts +11 -0
- package/dist/cdp/helpers.js +195 -0
- package/dist/cdp/label_cache.d.ts +12 -0
- package/dist/cdp/label_cache.js +51 -0
- package/dist/cdp/lock.d.ts +33 -0
- package/dist/cdp/lock.js +206 -0
- package/dist/cdp/omni_broker.d.ts +17 -0
- package/dist/cdp/omni_broker.js +119 -0
- package/dist/cdp/profiles.d.ts +1 -0
- package/dist/cdp/profiles.js +87 -0
- package/dist/cdp/pruner.d.ts +41 -0
- package/dist/cdp/pruner.js +263 -0
- package/dist/cdp/ref_cache.d.ts +101 -0
- package/dist/cdp/ref_cache.js +234 -0
- package/dist/cdp/retry.d.ts +5 -0
- package/dist/cdp/retry.js +39 -0
- package/dist/cdp/retry_enhanced.d.ts +44 -0
- package/dist/cdp/retry_enhanced.js +102 -0
- package/dist/cdp/security.d.ts +50 -0
- package/dist/cdp/security.js +237 -0
- package/dist/cdp/verify.d.ts +39 -0
- package/dist/cdp/verify.js +143 -0
- package/dist/cdp/walker.d.ts +91 -0
- package/dist/cdp/walker.js +896 -0
- package/dist/cdp/walker.test.d.ts +2 -0
- package/dist/cdp/walker.test.js +172 -0
- package/dist/cdp/websocket_transport.d.ts +15 -0
- package/dist/cdp/websocket_transport.js +118 -0
- package/dist/cli/plugin.d.ts +9 -0
- package/dist/cli/plugin.js +1134 -0
- package/dist/cli/updater.d.ts +1 -0
- package/dist/cli/updater.js +72 -0
- package/dist/config/loader.d.ts +120 -0
- package/dist/config/loader.js +704 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1248 -0
- package/dist/intelligence/hints.d.ts +11 -0
- package/dist/intelligence/hints.js +39 -0
- package/dist/intelligence/intent.d.ts +33 -0
- package/dist/intelligence/intent.js +171 -0
- package/dist/intelligence/sessionMemory.d.ts +66 -0
- package/dist/intelligence/sessionMemory.js +284 -0
- package/dist/intelligence/sessionMemory.test.d.ts +2 -0
- package/dist/intelligence/sessionMemory.test.js +62 -0
- package/dist/intelligence/trail.d.ts +64 -0
- package/dist/intelligence/trail.js +233 -0
- package/dist/intelligence/trail.test.d.ts +2 -0
- package/dist/intelligence/trail.test.js +110 -0
- package/dist/intelligence/vision.d.ts +24 -0
- package/dist/intelligence/vision.js +151 -0
- package/dist/lifecycle/mission_manager.d.ts +16 -0
- package/dist/lifecycle/mission_manager.js +44 -0
- package/dist/overlay/bubble.d.ts +16 -0
- package/dist/overlay/bubble.js +37 -0
- package/dist/overlay/ghost.d.ts +35 -0
- package/dist/overlay/ghost.js +508 -0
- package/dist/overlay/injector.d.ts +22 -0
- package/dist/overlay/injector.js +198 -0
- package/dist/plugin/interface.d.ts +134 -0
- package/dist/plugin/interface.js +1 -0
- package/dist/plugin/loader.d.ts +18 -0
- package/dist/plugin/loader.js +155 -0
- package/dist/plugin/registry.d.ts +50 -0
- package/dist/plugin/registry.js +234 -0
- package/dist/security/blacklist.d.ts +24 -0
- package/dist/security/blacklist.js +80 -0
- package/dist/security/filter.d.ts +2 -0
- package/dist/security/filter.js +20 -0
- package/dist/security/guard.d.ts +8 -0
- package/dist/security/guard.js +35 -0
- package/dist/security/ratelimit.d.ts +11 -0
- package/dist/security/ratelimit.js +57 -0
- package/dist/security/secrets.d.ts +75 -0
- package/dist/security/secrets.js +365 -0
- package/dist/sensors/actionability.d.ts +81 -0
- package/dist/sensors/actionability.js +303 -0
- package/dist/sensors/guard.d.ts +25 -0
- package/dist/sensors/guard.js +168 -0
- package/dist/sensors/mutations.d.ts +23 -0
- package/dist/sensors/mutations.js +93 -0
- package/dist/sensors/network.d.ts +67 -0
- package/dist/sensors/network.js +193 -0
- package/dist/sensors/operation_monitor.d.ts +80 -0
- package/dist/sensors/operation_monitor.js +203 -0
- package/dist/sensors/pacing.d.ts +44 -0
- package/dist/sensors/pacing.js +111 -0
- package/dist/sensors/thoughts.d.ts +2 -0
- package/dist/sensors/thoughts.js +59 -0
- package/dist/sensors/wait.d.ts +12 -0
- package/dist/sensors/wait.js +44 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +1687 -0
- package/dist/state/agent.d.ts +36 -0
- package/dist/state/agent.js +101 -0
- package/dist/state/cache.d.ts +16 -0
- package/dist/state/cache.js +92 -0
- package/dist/state/cache.test.d.ts +2 -0
- package/dist/state/cache.test.js +76 -0
- package/dist/state/delta.d.ts +71 -0
- package/dist/state/delta.js +234 -0
- package/dist/state/delta.test.d.ts +2 -0
- package/dist/state/delta.test.js +279 -0
- package/dist/state/groups.d.ts +15 -0
- package/dist/state/groups.js +80 -0
- package/dist/state/session.d.ts +30 -0
- package/dist/state/session.js +155 -0
- package/dist/tools/accessibility.d.ts +14 -0
- package/dist/tools/accessibility.js +32 -0
- package/dist/tools/ask.d.ts +14 -0
- package/dist/tools/ask.js +49 -0
- package/dist/tools/automation.d.ts +36 -0
- package/dist/tools/automation.js +218 -0
- package/dist/tools/browser_detect.d.ts +13 -0
- package/dist/tools/browser_detect.js +73 -0
- package/dist/tools/burst.d.ts +35 -0
- package/dist/tools/burst.js +344 -0
- package/dist/tools/canvas.d.ts +25 -0
- package/dist/tools/canvas.js +132 -0
- package/dist/tools/captcha.d.ts +38 -0
- package/dist/tools/captcha.js +209 -0
- package/dist/tools/click.d.ts +36 -0
- package/dist/tools/click.js +561 -0
- package/dist/tools/click_and_wait.d.ts +33 -0
- package/dist/tools/click_and_wait.js +149 -0
- package/dist/tools/clipboard.d.ts +29 -0
- package/dist/tools/clipboard.js +279 -0
- package/dist/tools/console.d.ts +21 -0
- package/dist/tools/console.js +107 -0
- package/dist/tools/cookies.d.ts +67 -0
- package/dist/tools/cookies.js +121 -0
- package/dist/tools/design_extract.d.ts +109 -0
- package/dist/tools/design_extract.js +641 -0
- package/dist/tools/dialog.d.ts +24 -0
- package/dist/tools/dialog.js +109 -0
- package/dist/tools/drag.d.ts +15 -0
- package/dist/tools/drag.js +29 -0
- package/dist/tools/emulation.d.ts +33 -0
- package/dist/tools/emulation.js +69 -0
- package/dist/tools/eval.d.ts +20 -0
- package/dist/tools/eval.js +92 -0
- package/dist/tools/fill.d.ts +41 -0
- package/dist/tools/fill.js +667 -0
- package/dist/tools/find.d.ts +30 -0
- package/dist/tools/find.js +60 -0
- package/dist/tools/geo.d.ts +19 -0
- package/dist/tools/geo.js +29 -0
- package/dist/tools/github/analyze.d.ts +4 -0
- package/dist/tools/github/analyze.js +113 -0
- package/dist/tools/github/cache.d.ts +21 -0
- package/dist/tools/github/cache.js +28 -0
- package/dist/tools/github/index.d.ts +4 -0
- package/dist/tools/github/index.js +4 -0
- package/dist/tools/github/issues.d.ts +9 -0
- package/dist/tools/github/issues.js +88 -0
- package/dist/tools/github/pr.d.ts +5 -0
- package/dist/tools/github/pr.js +88 -0
- package/dist/tools/github/rateLimiter.d.ts +4 -0
- package/dist/tools/github/rateLimiter.js +46 -0
- package/dist/tools/github/read.d.ts +34 -0
- package/dist/tools/github/read.js +74 -0
- package/dist/tools/github/repoContext.d.ts +20 -0
- package/dist/tools/github/repoContext.js +129 -0
- package/dist/tools/highlight.d.ts +20 -0
- package/dist/tools/highlight.js +140 -0
- package/dist/tools/hover.d.ts +19 -0
- package/dist/tools/hover.js +57 -0
- package/dist/tools/inspect.d.ts +17 -0
- package/dist/tools/inspect.js +125 -0
- package/dist/tools/integration.test.d.ts +2 -0
- package/dist/tools/integration.test.js +96 -0
- package/dist/tools/key.d.ts +25 -0
- package/dist/tools/key.js +173 -0
- package/dist/tools/macro_compiler.d.ts +7 -0
- package/dist/tools/macro_compiler.js +82 -0
- package/dist/tools/mouse.d.ts +21 -0
- package/dist/tools/mouse.js +62 -0
- package/dist/tools/navigate.d.ts +16 -0
- package/dist/tools/navigate.js +188 -0
- package/dist/tools/network_intercept.d.ts +17 -0
- package/dist/tools/network_intercept.js +151 -0
- package/dist/tools/office/index.d.ts +9 -0
- package/dist/tools/office/index.js +7 -0
- package/dist/tools/office/list.d.ts +15 -0
- package/dist/tools/office/list.js +41 -0
- package/dist/tools/office/parse.d.ts +20 -0
- package/dist/tools/office/parse.js +172 -0
- package/dist/tools/pdf.d.ts +22 -0
- package/dist/tools/pdf.js +42 -0
- package/dist/tools/performance.d.ts +12 -0
- package/dist/tools/performance.js +47 -0
- package/dist/tools/performance_test.d.ts +70 -0
- package/dist/tools/performance_test.js +128 -0
- package/dist/tools/plan.d.ts +39 -0
- package/dist/tools/plan.js +161 -0
- package/dist/tools/pointer.d.ts +35 -0
- package/dist/tools/pointer.js +522 -0
- package/dist/tools/read.d.ts +30 -0
- package/dist/tools/read.js +488 -0
- package/dist/tools/recording.d.ts +45 -0
- package/dist/tools/recording.js +124 -0
- package/dist/tools/response.d.ts +129 -0
- package/dist/tools/response.js +5 -0
- package/dist/tools/scrape.d.ts +24 -0
- package/dist/tools/scrape.js +140 -0
- package/dist/tools/screenshot.d.ts +40 -0
- package/dist/tools/screenshot.js +79 -0
- package/dist/tools/scroll.d.ts +23 -0
- package/dist/tools/scroll.js +196 -0
- package/dist/tools/select.d.ts +13 -0
- package/dist/tools/select.js +217 -0
- package/dist/tools/snapshot.d.ts +27 -0
- package/dist/tools/snapshot.js +99 -0
- package/dist/tools/storage.d.ts +18 -0
- package/dist/tools/storage.js +29 -0
- package/dist/tools/tabs.d.ts +77 -0
- package/dist/tools/tabs.js +342 -0
- package/dist/tools/thought.d.ts +16 -0
- package/dist/tools/thought.js +79 -0
- package/dist/tools/type.d.ts +65 -0
- package/dist/tools/type.js +1061 -0
- package/dist/tools/type_secret.d.ts +23 -0
- package/dist/tools/type_secret.js +229 -0
- package/dist/tools/upload.d.ts +19 -0
- package/dist/tools/upload.js +103 -0
- package/dist/tools/video_compiler.d.ts +24 -0
- package/dist/tools/video_compiler.js +199 -0
- package/dist/tools/viewport.d.ts +44 -0
- package/dist/tools/viewport.js +230 -0
- package/dist/tools/wait.d.ts +19 -0
- package/dist/tools/wait.js +202 -0
- package/dist/tools/wait_for.d.ts +23 -0
- package/dist/tools/wait_for.js +30 -0
- package/dist/tools/windows.d.ts +22 -0
- package/dist/tools/windows.js +57 -0
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.js +12 -0
- package/docs/DEV_MODE.md +68 -0
- package/docs/DOCUMENTATION.md +532 -0
- package/docs/PLUGINS.md +538 -0
- package/docs/TOOLS.md +104 -0
- package/docs/dev_plugin.md +310 -0
- package/ext-dist/background.js +467 -0
- package/ext-dist/content.js +3089 -0
- package/ext-dist/dashboard.html +583 -0
- package/ext-dist/hud/hud.js +73 -0
- package/ext-dist/hud/index.html +20 -0
- package/ext-dist/manifest.json +77 -0
- package/ext-dist/sidepanel/sidepanel.js +25 -0
- package/ext-dist/sidepanel.html +547 -0
- package/ext-dist/styles.css +218 -0
- package/icon.png +0 -0
- package/mcp.json +796 -0
- package/package.json +113 -0
- package/server.json +23 -0
- package/smithery.yaml +11 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import { logDebug } from "../audit/logger.js";
|
|
2
|
+
import { buildActionMap, toRefEntries, setShadowDomCache, buildAXTreeMap } from "../cdp/walker.js";
|
|
3
|
+
import { logAction } from "../audit/logger.js";
|
|
4
|
+
import { storeSnapshot, computeDelta, computeRichDelta, seedRichSnapshot } from "../state/delta.js";
|
|
5
|
+
import { readKnowledgeHint } from "../intelligence/hints.js";
|
|
6
|
+
import { saveDomainProfile, detectFramework, detectShadowDom, formatDomainHint } from "../intelligence/sessionMemory.js";
|
|
7
|
+
import { sessionRegistry } from "../state/session.js";
|
|
8
|
+
import { cacheActionMap, getCachedActionMap, getRawCachedActionMap } from "../state/cache.js";
|
|
9
|
+
import { startReadingSimulation } from "../overlay/ghost.js";
|
|
10
|
+
import { suppressOverlays, restoreOverlays } from "../cdp/extension.js";
|
|
11
|
+
import { sendToExtension } from "../cdp/helpers.js";
|
|
12
|
+
import { analyzeVisualFallback } from "../intelligence/vision.js";
|
|
13
|
+
import { getConfigAgentId } from "../config/loader.js";
|
|
14
|
+
// Cap at 80 elements; form/modal groups are always kept, utility trimmed last.
|
|
15
|
+
const MAX_ELEMENTS = 80;
|
|
16
|
+
function withTimeout(p, ms) {
|
|
17
|
+
return Promise.race([
|
|
18
|
+
p,
|
|
19
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`browserMap timed out after ${ms}ms`)), ms)),
|
|
20
|
+
]);
|
|
21
|
+
}
|
|
22
|
+
function formatLite(el, activeElementId, changedIds) {
|
|
23
|
+
const parts = [];
|
|
24
|
+
// ── State badges ────────────────────────────────────────────────────────────
|
|
25
|
+
if (el.disabled)
|
|
26
|
+
parts.push("disabled");
|
|
27
|
+
if (activeElementId && el.id === activeElementId)
|
|
28
|
+
parts.push("FOCUSED");
|
|
29
|
+
if (el.viewport && el.viewport !== "visible")
|
|
30
|
+
parts.push(`offscreen:${el.viewport}`);
|
|
31
|
+
// ── Value / checked / placeholder ──────────────────────────────────────────
|
|
32
|
+
const role = (el.role || '').toLowerCase();
|
|
33
|
+
const type = (el.type || '').toLowerCase();
|
|
34
|
+
// Radio / checkbox checked state — critical for quiz forms
|
|
35
|
+
if ((role === 'radio' || role === 'checkbox' || type === 'radio' || type === 'checkbox') && el.value !== undefined) {
|
|
36
|
+
parts.push(el.value === 'true' || el.value === 'checked' ? '✓ checked' : '○ unchecked');
|
|
37
|
+
}
|
|
38
|
+
else if (el.value) {
|
|
39
|
+
// Truncate long values to avoid token waste
|
|
40
|
+
const v = el.value.length > 60 ? el.value.slice(0, 57) + '…' : el.value;
|
|
41
|
+
parts.push(`"${v}"`);
|
|
42
|
+
}
|
|
43
|
+
else if (el.placeholder) {
|
|
44
|
+
parts.push(`placeholder: "${el.placeholder}"`);
|
|
45
|
+
}
|
|
46
|
+
// ── Type suffix for inputs (e.g. input[email], input[date]) ────────────────
|
|
47
|
+
const typeLabel = (role === 'textbox' || role === 'searchbox' || role === 'combobox') && type
|
|
48
|
+
? `/${type}` : '';
|
|
49
|
+
// ── Link hint ───────────────────────────────────────────────────────────────
|
|
50
|
+
if (role === 'link' && el.hrefType && el.hrefType !== 'same-origin') {
|
|
51
|
+
parts.push(el.hrefType);
|
|
52
|
+
}
|
|
53
|
+
const state = parts.length > 0 ? ` (${parts.join(', ')})` : '';
|
|
54
|
+
const changed = changedIds?.has(el.id) ? ' ★' : '';
|
|
55
|
+
return `[${el.id}] ${el.role}${typeLabel} "${el.label}"${state}${changed}`;
|
|
56
|
+
}
|
|
57
|
+
/** Full markdown table mode — only used when full:true is explicitly passed */
|
|
58
|
+
function formatFull(elements, url, title, hint) {
|
|
59
|
+
const lines = [];
|
|
60
|
+
if (hint)
|
|
61
|
+
lines.push(`> ${hint}\n`);
|
|
62
|
+
lines.push(`**${title || url}** — ${url}\n`);
|
|
63
|
+
for (const el of elements) {
|
|
64
|
+
const role = el.role || '';
|
|
65
|
+
const typeStr = el.type ? `/${el.type}` : '';
|
|
66
|
+
const roleTag = `${role}${typeStr}`;
|
|
67
|
+
const ref = el.id ? ` [${el.id}]` : '';
|
|
68
|
+
const rLow = role.toLowerCase();
|
|
69
|
+
const tLow = (el.type || '').toLowerCase();
|
|
70
|
+
let state = '';
|
|
71
|
+
if (rLow === 'radio' || rLow === 'checkbox' || tLow === 'radio' || tLow === 'checkbox') {
|
|
72
|
+
state = el.value === 'true' || el.value === 'checked' ? ' ✓' : ' ○';
|
|
73
|
+
}
|
|
74
|
+
else if (el.value) {
|
|
75
|
+
state = ` value="${el.value}"`;
|
|
76
|
+
}
|
|
77
|
+
else if (el.placeholder) {
|
|
78
|
+
state = ` placeholder="${el.placeholder}"`;
|
|
79
|
+
}
|
|
80
|
+
if (!el.id && !el.label.trim())
|
|
81
|
+
continue;
|
|
82
|
+
lines.push(`(${roleTag}) "${el.label}"${state}${ref}`);
|
|
83
|
+
}
|
|
84
|
+
return lines.join('\n');
|
|
85
|
+
}
|
|
86
|
+
export async function browserMap(session, options = {}, targetId = "default", config) {
|
|
87
|
+
logDebug(`[browserMap] Initiating read for targetId: ${targetId}, options: ${JSON.stringify(options)}`);
|
|
88
|
+
// ── Wait for DOM and Visual Stability ──────────────────────────────────────
|
|
89
|
+
try {
|
|
90
|
+
await session.Runtime.evaluate({
|
|
91
|
+
expression: `
|
|
92
|
+
new Promise(resolve => {
|
|
93
|
+
if (document.readyState === 'complete') {
|
|
94
|
+
setTimeout(resolve, 300); // 300ms visual stability delay
|
|
95
|
+
} else {
|
|
96
|
+
window.addEventListener('load', () => setTimeout(resolve, 300));
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
`,
|
|
100
|
+
awaitPromise: true
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
logDebug(`[browserMap] Stability wait failed: ${e}`);
|
|
105
|
+
}
|
|
106
|
+
// Signal reading state via HUD only
|
|
107
|
+
sendToExtension(session, { type: 'Weavetab_STATE', hud: { action: 'reading the page...' } }).catch(() => { });
|
|
108
|
+
// Set sticky pruning state if provided, otherwise preserve existing or default to false
|
|
109
|
+
const client = sessionRegistry.getAll().find(c => c.targetId === targetId);
|
|
110
|
+
if (client && options.prune !== undefined) {
|
|
111
|
+
client.pruneActive = options.prune;
|
|
112
|
+
}
|
|
113
|
+
// ── Overlays: default to visible so the user can see the agent's focus ──
|
|
114
|
+
if (options.show_overlays === false) {
|
|
115
|
+
await suppressOverlays(session);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
await restoreOverlays(session);
|
|
119
|
+
}
|
|
120
|
+
// ── Build element map: DOM walker or AX tree ─────────────────────────────
|
|
121
|
+
let map = null;
|
|
122
|
+
let usedAXTree = false;
|
|
123
|
+
// Check cache first if NOT forcing an AX Tree read explicitly
|
|
124
|
+
if (!options.ax_tree) {
|
|
125
|
+
let currentUrl;
|
|
126
|
+
try {
|
|
127
|
+
const { frameTree } = await session.Page.getFrameTree();
|
|
128
|
+
currentUrl = frameTree.frame.url;
|
|
129
|
+
}
|
|
130
|
+
catch { }
|
|
131
|
+
// ── Compute fast DOM hash ──────────────────────────────────────────────────
|
|
132
|
+
let domHash = 0;
|
|
133
|
+
try {
|
|
134
|
+
const { result } = await session.Runtime.evaluate({
|
|
135
|
+
expression: 'document.body ? document.body.innerHTML.length : 0',
|
|
136
|
+
returnByValue: true
|
|
137
|
+
});
|
|
138
|
+
domHash = result.value ?? 0;
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
logDebug(`[browserMap] Failed to compute DOM hash: ${e}`);
|
|
142
|
+
}
|
|
143
|
+
// Try strictly validated cache first
|
|
144
|
+
const cachedMap = getCachedActionMap(targetId, currentUrl);
|
|
145
|
+
if (cachedMap) {
|
|
146
|
+
logDebug(`[browserMap] Cache Hit (strict)! Returning cached action map with ${cachedMap.elements.length} elements.`);
|
|
147
|
+
map = cachedMap;
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
// Fallback: check raw cache with domHash
|
|
151
|
+
const rawMap = getRawCachedActionMap(targetId, currentUrl);
|
|
152
|
+
if (rawMap && rawMap.domHash === domHash) {
|
|
153
|
+
logDebug(`[browserMap] Cache Hit (domHash=${domHash})! MutationObserver invalidated but DOM content length unchanged.`);
|
|
154
|
+
map = rawMap;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (!map) {
|
|
159
|
+
if (options.ax_tree) {
|
|
160
|
+
// Explicit AX tree mode — skips DOM walker entirely
|
|
161
|
+
map = await buildAXTreeMap(session, {
|
|
162
|
+
scope: options.scope,
|
|
163
|
+
query: options.query,
|
|
164
|
+
limit: options.limit,
|
|
165
|
+
}, targetId);
|
|
166
|
+
usedAXTree = map.elements.length > 0;
|
|
167
|
+
logDebug(`[browserMap] AX tree mode: ${map.elements.length} elements`);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const _emptyMap = { url: '', title: '', elements: [], targetId };
|
|
171
|
+
// DOM-based walker (standard path) — 8s timeout prevents hanging on heavy SPAs
|
|
172
|
+
map = await withTimeout(buildActionMap(session, {
|
|
173
|
+
scope: options.scope,
|
|
174
|
+
query: options.query,
|
|
175
|
+
limit: options.limit,
|
|
176
|
+
deep: options.deep,
|
|
177
|
+
prune: options.prune,
|
|
178
|
+
}, targetId), 8000).catch(() => _emptyMap);
|
|
179
|
+
logDebug(`[browserMap] buildActionMap completed. Elements found: ${map.elements.length}`);
|
|
180
|
+
// Auto-fallback to AX tree when DOM walker returns sparse or unlabeled-heavy results
|
|
181
|
+
if (map.elements.length > 0 && map.elements.length < 3) {
|
|
182
|
+
const axMap = await withTimeout(buildAXTreeMap(session, {}, targetId), 6000).catch(() => _emptyMap);
|
|
183
|
+
if (axMap.elements.length > map.elements.length) {
|
|
184
|
+
map = axMap;
|
|
185
|
+
usedAXTree = true;
|
|
186
|
+
logDebug(`[browserMap] AX tree fallback (sparse DOM): ${map.elements.length} elements`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else if (map.elements.length > 0) {
|
|
190
|
+
const total = map.elements.length;
|
|
191
|
+
const unlabeled = map.elements.filter(e => !e.label && !e.value && !e.placeholder).length;
|
|
192
|
+
if (unlabeled / total > 0.5) {
|
|
193
|
+
const axMap = await withTimeout(buildAXTreeMap(session, {}, targetId), 6000).catch(() => _emptyMap);
|
|
194
|
+
if (axMap.elements.length > 0) {
|
|
195
|
+
map = axMap;
|
|
196
|
+
usedAXTree = true;
|
|
197
|
+
logDebug(`[browserMap] AX tree fallback (>50% unlabeled): ${map.elements.length} elements`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Record AX tree strategy in domain profile for future sessions
|
|
202
|
+
if (usedAXTree) {
|
|
203
|
+
try {
|
|
204
|
+
const hostname = new URL(map.url).hostname;
|
|
205
|
+
await saveDomainProfile(hostname, { strategies: ["axtree"] }).catch(() => { });
|
|
206
|
+
}
|
|
207
|
+
catch { /* best-effort */ }
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
// Request document first to prevent CDP error
|
|
212
|
+
await session.DOM.getDocument({ depth: -1 }).catch(() => { });
|
|
213
|
+
// Calculate domHash if not already calculated (e.g. from AX tree fallback)
|
|
214
|
+
try {
|
|
215
|
+
const { result } = await session.Runtime.evaluate({
|
|
216
|
+
expression: 'document.body ? document.body.innerHTML.length : 0',
|
|
217
|
+
returnByValue: true
|
|
218
|
+
});
|
|
219
|
+
map.domHash = result.value ?? 0;
|
|
220
|
+
}
|
|
221
|
+
catch (e) { }
|
|
222
|
+
// Never write an AX-tree result into the primary DOM map cache.
|
|
223
|
+
// AX tree uses sequential w:1…w:N IDs; DOM walker uses stable content-addressable hashes.
|
|
224
|
+
// Caching the AX result would make the next plain browser_map() call return AX-tree refs,
|
|
225
|
+
// causing different tools to see different ref numbering for the same element.
|
|
226
|
+
if (!usedAXTree) {
|
|
227
|
+
cacheActionMap(targetId, map);
|
|
228
|
+
}
|
|
229
|
+
logAction("browser_map", map.url, `${map.elements.length} elements`);
|
|
230
|
+
// Auto-learn domain profile (non-blocking)
|
|
231
|
+
try {
|
|
232
|
+
const hostname = new URL(map.url).hostname;
|
|
233
|
+
const [framework, hasShadowDom] = await Promise.all([
|
|
234
|
+
detectFramework(session).catch(() => undefined),
|
|
235
|
+
detectShadowDom(session).catch(() => false),
|
|
236
|
+
]);
|
|
237
|
+
if (framework || hasShadowDom) {
|
|
238
|
+
await saveDomainProfile(hostname, {
|
|
239
|
+
...(framework ? { framework } : {}),
|
|
240
|
+
...(hasShadowDom ? { shadowDom: true } : {}),
|
|
241
|
+
}).catch(() => { });
|
|
242
|
+
}
|
|
243
|
+
// Proactively seed shadow DOM cache for next browser_map call
|
|
244
|
+
if (hasShadowDom) {
|
|
245
|
+
setShadowDomCache(targetId, true);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch { /* domain profile is best-effort */ }
|
|
249
|
+
// Filter out unresolvable nodes
|
|
250
|
+
const backendIds = map.elements.map(e => e.backendNodeId).filter((id) => id !== undefined);
|
|
251
|
+
logDebug(`[browserMap] Resolving ${backendIds.length} backend node IDs.`);
|
|
252
|
+
const resolved = await session.DOM.pushNodesByBackendIdsToFrontend({
|
|
253
|
+
backendNodeIds: backendIds
|
|
254
|
+
});
|
|
255
|
+
if (resolved.nodeIds) {
|
|
256
|
+
logDebug(`[browserMap] Resolved ${resolved.nodeIds.length} node IDs.`);
|
|
257
|
+
const validIds = new Set(resolved.nodeIds.filter(id => id !== 0));
|
|
258
|
+
map.elements = map.elements.filter(el => {
|
|
259
|
+
if (el.backendNodeId === undefined)
|
|
260
|
+
return true;
|
|
261
|
+
const nodeId = resolved.nodeIds[map.elements.indexOf(el)];
|
|
262
|
+
return nodeId !== undefined && validIds.has(nodeId);
|
|
263
|
+
});
|
|
264
|
+
logDebug(`[browserMap] Elements after resolution filter: ${map.elements.length}`);
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
logDebug(`[browserMap] No node IDs resolved.`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
catch (e) {
|
|
271
|
+
logDebug(`[browserMap] Error during read: ${e}`);
|
|
272
|
+
throw e;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const newRefs = toRefEntries(map.elements);
|
|
276
|
+
const tabs = sessionRegistry.getAll();
|
|
277
|
+
const activeTabId = sessionRegistry.getActive(getConfigAgentId(config))?.targetId ?? targetId;
|
|
278
|
+
// ── Visual fallback when AX tree is empty ──────────────────────────────────
|
|
279
|
+
let visualFallback = null;
|
|
280
|
+
if (map.elements.length === 0) {
|
|
281
|
+
try {
|
|
282
|
+
// Canvas detection — check before full screenshot analysis
|
|
283
|
+
const { result: canvasCheck } = await session.Runtime.evaluate({
|
|
284
|
+
expression: `(function() {
|
|
285
|
+
const canvases = [...document.querySelectorAll('canvas')];
|
|
286
|
+
return JSON.stringify(canvases.map(c => {
|
|
287
|
+
const rect = c.getBoundingClientRect();
|
|
288
|
+
return {
|
|
289
|
+
id: c.id || '', width: Math.round(rect.width), height: Math.round(rect.height),
|
|
290
|
+
x: Math.round(rect.left + rect.width / 2),
|
|
291
|
+
y: Math.round(rect.top + rect.height / 2),
|
|
292
|
+
hasWebGL: !!(c.getContext('webgl') || c.getContext('webgl2'))
|
|
293
|
+
};
|
|
294
|
+
}));
|
|
295
|
+
})()`,
|
|
296
|
+
returnByValue: true,
|
|
297
|
+
}).catch(() => ({ result: { value: '[]' } }));
|
|
298
|
+
const canvases = JSON.parse(canvasCheck.value ?? '[]');
|
|
299
|
+
if (canvases.length > 0) {
|
|
300
|
+
for (let i = 0; i < canvases.length; i++) {
|
|
301
|
+
const c = canvases[i];
|
|
302
|
+
if (c.width > 0 && c.height > 0) {
|
|
303
|
+
map.elements.push({
|
|
304
|
+
id: `w:canvas${i}`,
|
|
305
|
+
role: "canvas",
|
|
306
|
+
label: `${c.hasWebGL ? 'WebGL ' : ''}Canvas ${c.width}x${c.height}${c.id ? ` #${c.id}` : ''}`,
|
|
307
|
+
group: "main_content",
|
|
308
|
+
fingerprint: `canvas:${i}:${c.id}`,
|
|
309
|
+
depth: 0,
|
|
310
|
+
x: c.x,
|
|
311
|
+
y: c.y,
|
|
312
|
+
selector: c.id ? `#${c.id}` : `canvas:nth-of-type(${i + 1})`
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (map.elements.length === 0) {
|
|
318
|
+
const vf = await analyzeVisualFallback(session);
|
|
319
|
+
const zones = vf.clickable_zones
|
|
320
|
+
.map((z) => ` coords(${z.x},${z.y})`)
|
|
321
|
+
.join("\n");
|
|
322
|
+
visualFallback =
|
|
323
|
+
`\nVISUAL FALLBACK (AX tree empty)\n` +
|
|
324
|
+
` page_state: ${vf.page_state}\n` +
|
|
325
|
+
` text_density: ${vf.text_density}%\n` +
|
|
326
|
+
` heatmap: ${vf.content_heatmap}\n` +
|
|
327
|
+
(zones ? ` clickable_zones:\n${zones}\n` : " no clickable zones detected\n");
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
visualFallback = "\nVISUAL FALLBACK: unavailable\n";
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// If we just added canvases, we need to update newRefs
|
|
335
|
+
if (map.elements.length > 0 && newRefs.length === 0) {
|
|
336
|
+
newRefs.push(...toRefEntries(map.elements));
|
|
337
|
+
}
|
|
338
|
+
// ── Delta mode ─────────────────────────────────────────────────────────────
|
|
339
|
+
if (options.delta) {
|
|
340
|
+
const richDiff = await computeRichDelta(targetId, map.url, map.elements);
|
|
341
|
+
if (richDiff) {
|
|
342
|
+
if ("delta_timeout" in richDiff) {
|
|
343
|
+
// Delta mode timed out — fall through to full read
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
const lines = [];
|
|
347
|
+
const header = `Page: ${map.title || map.url} | ${map.elements.length} elements`;
|
|
348
|
+
lines.push(header);
|
|
349
|
+
if (richDiff.added.length === 0 && richDiff.changed.length === 0 && richDiff.removed.length === 0 && (!richDiff.iframes || richDiff.iframes.length === 0)) {
|
|
350
|
+
lines.push("stable — no changes detected");
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
for (const r of richDiff.added)
|
|
354
|
+
lines.push(`+ [${r.id}] ${r.role} "${r.label}"`);
|
|
355
|
+
for (const r of richDiff.changed) {
|
|
356
|
+
const changes = Object.keys(r.changes);
|
|
357
|
+
const changeHints = changes.map(c => `${c}: "${r.changes[c].from}" → "${r.changes[c].to}"`).join(', ');
|
|
358
|
+
const actionable = r.actionable ? ` ⚡${r.recommended_action ? ` ${r.recommended_action}` : ''}` : '';
|
|
359
|
+
lines.push(`~ [${r.ref}] ${r.role} "${r.label}"${changeHints ? ` (${changeHints})` : ''}${actionable}`);
|
|
360
|
+
}
|
|
361
|
+
for (const id of richDiff.removed)
|
|
362
|
+
lines.push(`- ${id}`);
|
|
363
|
+
if (richDiff.iframes) {
|
|
364
|
+
for (const f of richDiff.iframes) {
|
|
365
|
+
const icon = f.status === "added" ? "+" : f.status === "removed" ? "-" : "~";
|
|
366
|
+
lines.push(`${icon} [iframe] ${f.frameUrl} (${f.status}, ${f.elementCount} elements)`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (visualFallback)
|
|
371
|
+
lines.push(visualFallback);
|
|
372
|
+
return lines.join("\n");
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
// No prior snapshot → fall through to full read
|
|
376
|
+
}
|
|
377
|
+
storeSnapshot(targetId, map.url, newRefs);
|
|
378
|
+
seedRichSnapshot(targetId, map.url, map.elements);
|
|
379
|
+
// JIT knowledge hint + session memory
|
|
380
|
+
let hint;
|
|
381
|
+
try {
|
|
382
|
+
const hostname = new URL(map.url).hostname;
|
|
383
|
+
const [knowledge, domainProfile] = await Promise.all([
|
|
384
|
+
readKnowledgeHint(hostname),
|
|
385
|
+
import("../intelligence/sessionMemory.js").then(m => m.loadDomainProfile(hostname)),
|
|
386
|
+
]);
|
|
387
|
+
const memoryHint = domainProfile ? formatDomainHint(hostname, domainProfile) : undefined;
|
|
388
|
+
hint = [knowledge, memoryHint].filter(Boolean).join('\n') || undefined;
|
|
389
|
+
}
|
|
390
|
+
catch { /* ignore */ }
|
|
391
|
+
// ── Full table mode (explicit full:true only) ───────────────────────────────
|
|
392
|
+
if (options.full) {
|
|
393
|
+
const markdown = formatFull(map.elements, map.url, map.title, hint);
|
|
394
|
+
const wordCount = markdown.split(/\s+/).length;
|
|
395
|
+
startReadingSimulation(session, wordCount, config);
|
|
396
|
+
const tabLine = `⬡ ${tabs.length} tab(s) | active: ${activeTabId}`;
|
|
397
|
+
return tabLine + "\n\n" + markdown + (visualFallback ?? "");
|
|
398
|
+
}
|
|
399
|
+
// ── Prioritised element capping ──────────────────────────────────────────────
|
|
400
|
+
// Keep form/modal elements at all costs (they're the most actionable).
|
|
401
|
+
// Trim utility/nav from the end if we're over the cap.
|
|
402
|
+
let capped;
|
|
403
|
+
if (map.elements.length > MAX_ELEMENTS) {
|
|
404
|
+
const priority = map.elements.filter(e => e.group === 'form' || e.group === 'modal');
|
|
405
|
+
const secondary = map.elements.filter(e => e.group === 'main_content' || e.group === 'sidebar');
|
|
406
|
+
const tertiary = map.elements.filter(e => e.group === 'navigation' || e.group === 'utility');
|
|
407
|
+
capped = [...priority, ...secondary, ...tertiary].slice(0, MAX_ELEMENTS);
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
capped = map.elements;
|
|
411
|
+
}
|
|
412
|
+
const overflowCount = map.elements.length - capped.length;
|
|
413
|
+
const lines = [];
|
|
414
|
+
if (hint)
|
|
415
|
+
lines.push(`> ${hint}`);
|
|
416
|
+
// Cure optional-login blindness by explicitly pointing out dismiss buttons
|
|
417
|
+
const dismissButton = map.elements.find(el => /(not now|skip|close|dismiss|no thanks|^x$)/i.test(el.label) &&
|
|
418
|
+
/(button|link)/i.test(el.role));
|
|
419
|
+
if (dismissButton) {
|
|
420
|
+
lines.push(`\n🚨 [POPUP DETECTED]: If an optional modal/login is blocking the page, try clicking [${dismissButton.id}] ("${dismissButton.label}") to dismiss it.\n`);
|
|
421
|
+
}
|
|
422
|
+
const changed = computeDelta(targetId, map.url, newRefs);
|
|
423
|
+
const changedIds = new Set(changed ? [...changed.added.map((r) => r.id), ...changed.changed.map((r) => r.id)] : []);
|
|
424
|
+
const beforePruneCount = map.elements.length;
|
|
425
|
+
lines.push(`Page: ${map.title || map.url} | ${map.elements.length} interactive elements` +
|
|
426
|
+
(changedIds.size > 0 ? ` | ${changedIds.size} changed` : "") +
|
|
427
|
+
(options.prune && beforePruneCount !== map.elements.length
|
|
428
|
+
? ` (pruned ${beforePruneCount - map.elements.length})`
|
|
429
|
+
: ""));
|
|
430
|
+
lines.push("");
|
|
431
|
+
// ── Semantic section grouping ────────────────────────────────────────────────
|
|
432
|
+
const SECTION_LABEL = {
|
|
433
|
+
navigation: "── NAV ──────────────────",
|
|
434
|
+
main_content: "── MAIN ─────────────────",
|
|
435
|
+
form: "── FORM ─────────────────",
|
|
436
|
+
sidebar: "── SIDEBAR ──────────────",
|
|
437
|
+
modal: "── MODAL ────────────────",
|
|
438
|
+
utility: "── OTHER ────────────────",
|
|
439
|
+
};
|
|
440
|
+
let activeElementId;
|
|
441
|
+
try {
|
|
442
|
+
const { result } = await session.Runtime.evaluate({
|
|
443
|
+
expression: `(function() {
|
|
444
|
+
const ae = document.activeElement;
|
|
445
|
+
if (!ae || ae === document.body) return null;
|
|
446
|
+
return ae.getAttribute('data-wt-id') || null;
|
|
447
|
+
})()`,
|
|
448
|
+
returnByValue: true
|
|
449
|
+
});
|
|
450
|
+
if (result && result.value) {
|
|
451
|
+
activeElementId = 'w:' + result.value;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
catch { }
|
|
455
|
+
let currentSection = null;
|
|
456
|
+
for (const el of capped) {
|
|
457
|
+
if (el.group !== currentSection) {
|
|
458
|
+
currentSection = el.group;
|
|
459
|
+
lines.push(SECTION_LABEL[currentSection]);
|
|
460
|
+
}
|
|
461
|
+
lines.push(formatLite(el, activeElementId, changedIds));
|
|
462
|
+
}
|
|
463
|
+
if (overflowCount > 0) {
|
|
464
|
+
lines.push(`(+${overflowCount} more below fold — call browser_scroll down to reveal)`);
|
|
465
|
+
}
|
|
466
|
+
if (visualFallback)
|
|
467
|
+
lines.push(visualFallback);
|
|
468
|
+
// Context-scoped tool hint — collapse 34-tool surface to ~12 relevant tools.
|
|
469
|
+
// Detected from map elements already computed above — zero extra latency.
|
|
470
|
+
const hasForm = map.elements.some(e => e.group === "form" || e.role === "input" || e.role === "combobox" || e.role === "checkbox" || e.role === "radio");
|
|
471
|
+
const hasSelect = map.elements.some(e => e.role === "combobox" || e.role === "listbox");
|
|
472
|
+
const hasCanvas = map.url?.includes("canvas") || false;
|
|
473
|
+
const isGithub = map.url?.includes("github.com") ?? false;
|
|
474
|
+
const baseTools = ["browser_click", "browser_type", "browser_key", "browser_scroll", "browser_wait", "browser_navigate", "browser_tabs"];
|
|
475
|
+
const formTools = hasForm ? ["browser_select", "browser_type_secret", "browser_find"] : [];
|
|
476
|
+
const githubTools = isGithub ? ["github_analyze", "github_read", "github_issues", "github_get_pr"] : [];
|
|
477
|
+
const canvasTools = hasCanvas ? ["browser_canvas", "browser_screenshot"] : ["browser_screenshot"];
|
|
478
|
+
const diagTools = ["browser_inspect", "browser_highlight", "browser_scrape", "browser_burst"];
|
|
479
|
+
const contextTools = [...new Set([...baseTools, ...formTools, ...githubTools, ...canvasTools, ...diagTools])];
|
|
480
|
+
lines.push(`\n_tools [${contextTools.join(", ")}]`);
|
|
481
|
+
const output = lines.join("\n");
|
|
482
|
+
const wordCount = output.split(/\s+/).length;
|
|
483
|
+
startReadingSimulation(session, wordCount, config);
|
|
484
|
+
sendToExtension(session, { type: 'Weavetab_STATE', hud: { action: '' } }).catch(() => { });
|
|
485
|
+
return output;
|
|
486
|
+
}
|
|
487
|
+
// Keep backward-compat alias used by plan.ts
|
|
488
|
+
export { browserMap as weaveRead };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Weavetab — tools/recording.ts
|
|
3
|
+
* browser_recording: Session recording (screencast) for compliance review.
|
|
4
|
+
* Uses CDP Page.startScreencast at 2-5 FPS and compiles frames into video (.webp, .gif, .webm, .mp4).
|
|
5
|
+
* Weavetab by fy2ne
|
|
6
|
+
*/
|
|
7
|
+
import CDP from "chrome-remote-interface";
|
|
8
|
+
import type { Config } from "../config/loader.js";
|
|
9
|
+
export interface RecordingOptions {
|
|
10
|
+
action: "start" | "stop" | "list";
|
|
11
|
+
sessionId?: string;
|
|
12
|
+
fps?: number;
|
|
13
|
+
maxFrames?: number;
|
|
14
|
+
/** Output video/animation format when stopping recording.
|
|
15
|
+
* Options: 'webp' (default pure Node animation), 'gif', 'webm', 'mp4', or 'frames' (raw images). */
|
|
16
|
+
format?: "webp" | "gif" | "webm" | "mp4" | "frames";
|
|
17
|
+
/** If false (default), cleans up raw JPEG image frames after video compilation. */
|
|
18
|
+
keepFrames?: boolean;
|
|
19
|
+
/** If false, keeps the WeaveTab UI overlay visible in the recording.
|
|
20
|
+
* Default: true — overlay is hidden from screencasts so agent never records itself. */
|
|
21
|
+
hideOverlay?: boolean;
|
|
22
|
+
}
|
|
23
|
+
interface RecordingState {
|
|
24
|
+
sessionId: string;
|
|
25
|
+
targetId: string;
|
|
26
|
+
startedAt: string;
|
|
27
|
+
fps: number;
|
|
28
|
+
maxFrames: number;
|
|
29
|
+
frameCount: number;
|
|
30
|
+
dir: string;
|
|
31
|
+
active: boolean;
|
|
32
|
+
}
|
|
33
|
+
export declare function browserRecording(session: CDP.Client, options: RecordingOptions, config: Config): Promise<{
|
|
34
|
+
success: boolean;
|
|
35
|
+
recordings?: RecordingState[];
|
|
36
|
+
error?: string;
|
|
37
|
+
dir?: string;
|
|
38
|
+
frameCount?: number;
|
|
39
|
+
videoPath?: string;
|
|
40
|
+
videoFormat?: string;
|
|
41
|
+
durationSeconds?: number;
|
|
42
|
+
fileSizeBytes?: number;
|
|
43
|
+
compilerUsed?: string;
|
|
44
|
+
}>;
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { getOutputDir } from "../config/loader.js";
|
|
4
|
+
import { logAction } from "../audit/logger.js";
|
|
5
|
+
import { hideWeaveTabOverlay, restoreWeaveTabOverlay } from "../cdp/helpers.js";
|
|
6
|
+
import { sessionRegistry } from "../state/session.js";
|
|
7
|
+
import { compileVideo } from "./video_compiler.js";
|
|
8
|
+
const activeRecordings = new Map();
|
|
9
|
+
let screencastListener = null;
|
|
10
|
+
let currentSession = null;
|
|
11
|
+
function recordingDir(sessionId, config) {
|
|
12
|
+
return path.join(getOutputDir("recordings", config), sessionId);
|
|
13
|
+
}
|
|
14
|
+
export async function browserRecording(session, options, config) {
|
|
15
|
+
const { action, sessionId, fps = 3, maxFrames = 500 } = options;
|
|
16
|
+
if (action === "start") {
|
|
17
|
+
if (!sessionId)
|
|
18
|
+
return { success: false, error: "browser_recording start requires a 'sessionId'" };
|
|
19
|
+
if (activeRecordings.has(sessionId)) {
|
|
20
|
+
return { success: false, error: `Recording '${sessionId}' is already active` };
|
|
21
|
+
}
|
|
22
|
+
const dir = recordingDir(sessionId, config);
|
|
23
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
24
|
+
const active = sessionRegistry.getActive(config.agentId);
|
|
25
|
+
const targetId = active ? active.targetId : "default";
|
|
26
|
+
const state = {
|
|
27
|
+
sessionId,
|
|
28
|
+
targetId,
|
|
29
|
+
startedAt: new Date().toISOString(),
|
|
30
|
+
fps: Math.max(1, Math.min(10, fps)),
|
|
31
|
+
maxFrames,
|
|
32
|
+
frameCount: 0,
|
|
33
|
+
dir,
|
|
34
|
+
active: true,
|
|
35
|
+
};
|
|
36
|
+
activeRecordings.set(sessionId, state);
|
|
37
|
+
currentSession = session;
|
|
38
|
+
const page = session.Page;
|
|
39
|
+
// Register screencast frame handler
|
|
40
|
+
if (screencastListener && currentSession) {
|
|
41
|
+
currentSession.removeListener("Page.screencastFrame", screencastListener);
|
|
42
|
+
}
|
|
43
|
+
screencastListener = async (frame) => {
|
|
44
|
+
const s = activeRecordings.get(sessionId);
|
|
45
|
+
if (!s || !s.active)
|
|
46
|
+
return;
|
|
47
|
+
s.frameCount++;
|
|
48
|
+
const fname = `frame-${String(s.frameCount).padStart(6, "0")}.jpg`;
|
|
49
|
+
const fpath = path.join(s.dir, fname);
|
|
50
|
+
try {
|
|
51
|
+
fs.writeFileSync(fpath, Buffer.from(frame.data, "base64"));
|
|
52
|
+
}
|
|
53
|
+
catch { /* disk full etc — skip frame */ }
|
|
54
|
+
await page.screencastFrameAck({ sessionId: frame.sessionId }).catch(() => { });
|
|
55
|
+
if (s.frameCount >= s.maxFrames) {
|
|
56
|
+
await page.stopScreencast().catch(() => { });
|
|
57
|
+
s.active = false;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
session.on("Page.screencastFrame", screencastListener);
|
|
61
|
+
await page.startScreencast({
|
|
62
|
+
format: "jpeg",
|
|
63
|
+
quality: 60,
|
|
64
|
+
maxWidth: 1280,
|
|
65
|
+
maxHeight: 720,
|
|
66
|
+
everyNthFrame: 1,
|
|
67
|
+
}).catch((e) => {
|
|
68
|
+
activeRecordings.delete(sessionId);
|
|
69
|
+
throw new Error(`Failed to start screencast: ${e.message}`);
|
|
70
|
+
});
|
|
71
|
+
// Always hide overlay during recording (default true; set hideOverlay: false to skip)
|
|
72
|
+
if (options.hideOverlay !== false) {
|
|
73
|
+
await hideWeaveTabOverlay(session);
|
|
74
|
+
}
|
|
75
|
+
logAction("browser_recording", sessionId, `started (${fps}fps, max ${maxFrames} frames)`);
|
|
76
|
+
return { success: true, recordings: [state], dir };
|
|
77
|
+
}
|
|
78
|
+
if (action === "stop") {
|
|
79
|
+
if (!sessionId)
|
|
80
|
+
return { success: false, error: "browser_recording stop requires a 'sessionId'" };
|
|
81
|
+
const state = activeRecordings.get(sessionId);
|
|
82
|
+
if (!state)
|
|
83
|
+
return { success: false, error: `Recording '${sessionId}' not found` };
|
|
84
|
+
state.active = false;
|
|
85
|
+
try {
|
|
86
|
+
await session.Page.stopScreencast().catch(() => { });
|
|
87
|
+
}
|
|
88
|
+
catch { /* ignore */ }
|
|
89
|
+
await restoreWeaveTabOverlay(session);
|
|
90
|
+
if (screencastListener && currentSession) {
|
|
91
|
+
currentSession.removeListener("Page.screencastFrame", screencastListener);
|
|
92
|
+
screencastListener = null;
|
|
93
|
+
currentSession = null;
|
|
94
|
+
}
|
|
95
|
+
const count = state.frameCount;
|
|
96
|
+
activeRecordings.delete(sessionId);
|
|
97
|
+
// Compile frames into video file
|
|
98
|
+
const compileRes = await compileVideo({
|
|
99
|
+
recordingDir: state.dir,
|
|
100
|
+
fps: state.fps,
|
|
101
|
+
format: options.format || "webp",
|
|
102
|
+
keepFrames: options.keepFrames ?? false,
|
|
103
|
+
});
|
|
104
|
+
logAction("browser_recording", sessionId, `stopped (${count} frames, compiled: ${compileRes.videoPath || "none"})`);
|
|
105
|
+
return {
|
|
106
|
+
success: true,
|
|
107
|
+
recordings: Array.from(activeRecordings.values()),
|
|
108
|
+
frameCount: count,
|
|
109
|
+
dir: state.dir,
|
|
110
|
+
videoPath: compileRes.videoPath,
|
|
111
|
+
videoFormat: compileRes.format,
|
|
112
|
+
durationSeconds: compileRes.durationSeconds,
|
|
113
|
+
fileSizeBytes: compileRes.fileSizeBytes,
|
|
114
|
+
compilerUsed: compileRes.compilerUsed,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (action === "list") {
|
|
118
|
+
return {
|
|
119
|
+
success: true,
|
|
120
|
+
recordings: Array.from(activeRecordings.values()),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return { success: false, error: `Unknown action: ${action}` };
|
|
124
|
+
}
|