autokap 1.0.7 → 1.0.8
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/assets/cursors/macos.svg +4 -0
- package/assets/cursors/windows.svg +15 -0
- package/assets/skill/OPCODE-REFERENCE.md +607 -0
- package/assets/skill/README.md +39 -0
- package/assets/skill/SKILL.md +453 -468
- package/assets/skill/STUDIO-SKILL.md +476 -0
- package/assets/skill/references/examples.md +104 -0
- package/assets/skill/references/interactive-demo.md +225 -0
- package/assets/skill/references/mock-data.md +178 -0
- package/dist/action-verifier.d.ts +29 -0
- package/dist/action-verifier.js +133 -0
- package/dist/agent-action-recovery.d.ts +45 -0
- package/dist/agent-action-recovery.js +370 -0
- package/dist/agent-message-utils.d.ts +21 -0
- package/dist/agent-message-utils.js +77 -0
- package/dist/agent-url-utils.d.ts +30 -0
- package/dist/agent-url-utils.js +138 -0
- package/dist/agent.d.ts +92 -8
- package/dist/agent.js +2936 -781
- package/dist/ak-tree.d.ts +39 -0
- package/dist/ak-tree.js +368 -0
- package/dist/alt-text.d.ts +26 -0
- package/dist/alt-text.js +55 -0
- package/dist/auth-capture.d.ts +17 -0
- package/dist/auth-capture.js +164 -0
- package/dist/benchmark.d.ts +59 -0
- package/dist/benchmark.js +135 -0
- package/dist/browser-bar.d.ts +14 -6
- package/dist/browser-bar.js +145 -8
- package/dist/browser-pool.d.ts +7 -0
- package/dist/browser-pool.js +15 -5
- package/dist/browser-utils.d.ts +31 -0
- package/dist/browser-utils.js +97 -0
- package/dist/browser.d.ts +51 -1
- package/dist/browser.js +1481 -31
- package/dist/capture-alt-text.js +2 -1
- package/dist/capture-language-preflight.js +14 -0
- package/dist/capture-llm-page-identity.js +22 -10
- package/dist/capture-page-identity.d.ts +5 -7
- package/dist/capture-page-identity.js +211 -78
- package/dist/capture-preset-credentials.d.ts +50 -0
- package/dist/capture-preset-credentials.js +127 -0
- package/dist/capture-request-plan.d.ts +2 -2
- package/dist/capture-request-plan.js +64 -16
- package/dist/capture-run-optimizer.js +48 -33
- package/dist/capture-selector-memory.d.ts +5 -0
- package/dist/capture-selector-memory.js +18 -0
- package/dist/capture-strategy.d.ts +36 -0
- package/dist/capture-strategy.js +95 -0
- package/dist/capture-studio-sync.d.ts +1 -0
- package/dist/capture-studio-sync.js +9 -3
- package/dist/capture-surface-contract.d.ts +36 -0
- package/dist/capture-surface-contract.js +299 -0
- package/dist/capture-transition-engine.d.ts +28 -0
- package/dist/capture-transition-engine.js +292 -0
- package/dist/capture-variant-state.d.ts +2 -0
- package/dist/capture-variant-state.js +26 -0
- package/dist/capture-verification.d.ts +35 -0
- package/dist/capture-verification.js +95 -0
- package/dist/capture-viewport-lock.d.ts +48 -0
- package/dist/capture-viewport-lock.js +74 -0
- package/dist/circuit-breaker.d.ts +42 -0
- package/dist/circuit-breaker.js +119 -0
- package/dist/cli-config.d.ts +8 -1
- package/dist/cli-config.js +62 -6
- package/dist/cli-contract.d.ts +15 -0
- package/dist/cli-contract.js +167 -0
- package/dist/cli-runner-local.d.ts +12 -0
- package/dist/cli-runner-local.js +102 -0
- package/dist/cli-runner.d.ts +34 -0
- package/dist/cli-runner.js +433 -0
- package/dist/cli-utils.d.ts +0 -1
- package/dist/cli-utils.js +2 -5
- package/dist/cli.js +1005 -267
- package/dist/clip-orchestrator.js +9 -2
- package/dist/clip-postprocess.js +25 -16
- package/dist/cookie-dismiss.d.ts +2 -0
- package/dist/cookie-dismiss.js +48 -13
- package/dist/cost-logging.d.ts +8 -0
- package/dist/cost-logging.js +160 -46
- package/dist/cost-resolution-monitor.d.ts +16 -0
- package/dist/cost-resolution-monitor.js +34 -0
- package/dist/credential-templates.js +2 -2
- package/dist/cursor-overlay-script.d.ts +6 -0
- package/dist/cursor-overlay-script.js +169 -0
- package/dist/dom-css-purger.d.ts +65 -0
- package/dist/dom-css-purger.js +333 -0
- package/dist/dom-font-inliner.d.ts +45 -0
- package/dist/dom-font-inliner.js +148 -0
- package/dist/dom-patch-resolver.d.ts +52 -0
- package/dist/dom-patch-resolver.js +242 -0
- package/dist/dom-serializer.d.ts +82 -0
- package/dist/dom-serializer.js +378 -0
- package/dist/element-capture.d.ts +1 -41
- package/dist/element-capture.js +202 -446
- package/dist/env-validation.d.ts +5 -0
- package/dist/env-validation.js +29 -0
- package/dist/execution-schema.d.ts +4423 -0
- package/dist/execution-schema.js +507 -0
- package/dist/execution-types.d.ts +886 -0
- package/dist/execution-types.js +65 -0
- package/dist/fonts-loader.d.ts +14 -0
- package/dist/fonts-loader.js +55 -0
- package/dist/hybrid-navigator.js +12 -12
- package/dist/index.d.ts +9 -6
- package/dist/index.js +10 -4
- package/dist/legacy/agent-action-recovery.d.ts +45 -0
- package/dist/legacy/agent-action-recovery.js +370 -0
- package/dist/legacy/agent-message-utils.d.ts +21 -0
- package/dist/legacy/agent-message-utils.js +77 -0
- package/dist/legacy/agent-url-utils.d.ts +30 -0
- package/dist/legacy/agent-url-utils.js +138 -0
- package/dist/legacy/agent.d.ts +226 -0
- package/dist/legacy/agent.js +6666 -0
- package/dist/legacy/clip-orchestrator.d.ts +148 -0
- package/dist/legacy/clip-orchestrator.js +957 -0
- package/dist/legacy/credential-templates.d.ts +5 -0
- package/dist/legacy/credential-templates.js +60 -0
- package/dist/legacy/hybrid-navigator.d.ts +138 -0
- package/dist/legacy/hybrid-navigator.js +468 -0
- package/dist/legacy/llm-usage.d.ts +17 -0
- package/dist/legacy/llm-usage.js +45 -0
- package/dist/legacy/prompt-cache.d.ts +10 -0
- package/dist/legacy/prompt-cache.js +24 -0
- package/dist/legacy/prompts.d.ts +175 -0
- package/dist/legacy/prompts.js +1038 -0
- package/dist/legacy/tools.d.ts +4 -0
- package/dist/legacy/tools.js +216 -0
- package/dist/legacy/video-agent.d.ts +143 -0
- package/dist/legacy/video-agent.js +4788 -0
- package/dist/legacy/video-observation.d.ts +36 -0
- package/dist/legacy/video-observation.js +192 -0
- package/dist/legacy/video-planner.d.ts +12 -0
- package/dist/legacy/video-planner.js +501 -0
- package/dist/legacy/video-prompts.d.ts +37 -0
- package/dist/legacy/video-prompts.js +569 -0
- package/dist/legacy/video-tools.d.ts +3 -0
- package/dist/legacy/video-tools.js +59 -0
- package/dist/legacy/video-variant-state.d.ts +29 -0
- package/dist/legacy/video-variant-state.js +80 -0
- package/dist/legacy/vision-model.d.ts +17 -0
- package/dist/legacy/vision-model.js +74 -0
- package/dist/llm-healer.d.ts +63 -0
- package/dist/llm-healer.js +166 -0
- package/dist/llm-provider.d.ts +29 -0
- package/dist/llm-provider.js +80 -0
- package/dist/logger.d.ts +6 -2
- package/dist/logger.js +15 -1
- package/dist/mockup-html.js +35 -25
- package/dist/mockup.d.ts +95 -2
- package/dist/mockup.js +427 -166
- package/dist/mouse-animation.d.ts +2 -2
- package/dist/mouse-animation.js +34 -20
- package/dist/opcode-actions.d.ts +42 -0
- package/dist/opcode-actions.js +511 -0
- package/dist/opcode-runner.d.ts +51 -0
- package/dist/opcode-runner.js +770 -0
- package/dist/openrouter-client.d.ts +40 -0
- package/dist/openrouter-client.js +16 -0
- package/dist/overlay-engine.d.ts +24 -0
- package/dist/overlay-engine.js +176 -0
- package/dist/postcondition.d.ts +16 -0
- package/dist/postcondition.js +269 -0
- package/dist/program-patcher.d.ts +25 -0
- package/dist/program-patcher.js +44 -0
- package/dist/prompts.d.ts +13 -5
- package/dist/prompts.js +224 -351
- package/dist/provider-config.d.ts +12 -0
- package/dist/provider-config.js +15 -0
- package/dist/recovery-chain.d.ts +37 -0
- package/dist/recovery-chain.js +350 -0
- package/dist/remote-browser.d.ts +28 -4
- package/dist/remote-browser.js +60 -5
- package/dist/safari-browser-bar.d.ts +15 -0
- package/dist/safari-browser-bar.js +95 -0
- package/dist/safari-toolbar-asset.d.ts +15 -0
- package/dist/safari-toolbar-asset.js +12 -0
- package/dist/security.d.ts +2 -1
- package/dist/security.js +49 -10
- package/dist/selector-resolver.d.ts +34 -0
- package/dist/selector-resolver.js +181 -0
- package/dist/semantic-resolver.d.ts +35 -0
- package/dist/semantic-resolver.js +161 -0
- package/dist/server-capture-runtime.d.ts +5 -3
- package/dist/server-capture-runtime.js +42 -95
- package/dist/server-credit-usage.d.ts +2 -2
- package/dist/server-project-webhooks.d.ts +15 -1
- package/dist/server-project-webhooks.js +34 -8
- package/dist/server-screenshot-watermark.js +27 -5
- package/dist/session-profile.js +164 -1
- package/dist/sf-pro-symbols.d.ts +1 -0
- package/dist/sf-pro-symbols.js +55 -0
- package/dist/skill-packaging.d.ts +28 -0
- package/dist/skill-packaging.js +169 -0
- package/dist/smart-wait.d.ts +27 -0
- package/dist/smart-wait.js +81 -0
- package/dist/status-bar-render.d.ts +20 -0
- package/dist/status-bar-render.js +410 -0
- package/dist/status-bar.d.ts +9 -0
- package/dist/status-bar.js +298 -14
- package/dist/svg-browser-bar.d.ts +33 -0
- package/dist/svg-browser-bar.js +206 -0
- package/dist/svg-status-bar.d.ts +36 -0
- package/dist/svg-status-bar.js +597 -0
- package/dist/svg-text.d.ts +61 -0
- package/dist/svg-text.js +118 -0
- package/dist/tools.js +89 -451
- package/dist/types.d.ts +240 -5
- package/dist/types.js +23 -1
- package/dist/v2/action-verifier.d.ts +29 -0
- package/dist/v2/action-verifier.js +133 -0
- package/dist/v2/alt-text.d.ts +26 -0
- package/dist/v2/alt-text.js +55 -0
- package/dist/v2/benchmark.d.ts +59 -0
- package/dist/v2/benchmark.js +135 -0
- package/dist/v2/capture-strategy.d.ts +30 -0
- package/dist/v2/capture-strategy.js +67 -0
- package/dist/v2/capture-verification.d.ts +35 -0
- package/dist/v2/capture-verification.js +95 -0
- package/dist/v2/circuit-breaker.d.ts +42 -0
- package/dist/v2/circuit-breaker.js +119 -0
- package/dist/v2/cli-runner-local.d.ts +11 -0
- package/dist/v2/cli-runner-local.js +91 -0
- package/dist/v2/cli-runner.d.ts +34 -0
- package/dist/v2/cli-runner.js +300 -0
- package/dist/v2/compiler-prompts.d.ts +27 -0
- package/dist/v2/compiler-prompts.js +123 -0
- package/dist/v2/compiler.d.ts +37 -0
- package/dist/v2/compiler.js +147 -0
- package/dist/v2/explorer.d.ts +41 -0
- package/dist/v2/explorer.js +56 -0
- package/dist/v2/index.d.ts +37 -0
- package/dist/v2/index.js +31 -0
- package/dist/v2/llm-healer.d.ts +62 -0
- package/dist/v2/llm-healer.js +166 -0
- package/dist/v2/llm-provider.d.ts +29 -0
- package/dist/v2/llm-provider.js +80 -0
- package/dist/v2/opcode-runner.d.ts +47 -0
- package/dist/v2/opcode-runner.js +634 -0
- package/dist/v2/overlay-engine.d.ts +24 -0
- package/dist/v2/overlay-engine.js +150 -0
- package/dist/v2/postcondition.d.ts +16 -0
- package/dist/v2/postcondition.js +249 -0
- package/dist/v2/program-patcher.d.ts +25 -0
- package/dist/v2/program-patcher.js +44 -0
- package/dist/v2/recovery-chain.d.ts +30 -0
- package/dist/v2/recovery-chain.js +368 -0
- package/dist/v2/schema.d.ts +2580 -0
- package/dist/v2/schema.js +295 -0
- package/dist/v2/selector-resolver.d.ts +34 -0
- package/dist/v2/selector-resolver.js +181 -0
- package/dist/v2/semantic-resolver.d.ts +35 -0
- package/dist/v2/semantic-resolver.js +161 -0
- package/dist/v2/smart-wait.d.ts +27 -0
- package/dist/v2/smart-wait.js +81 -0
- package/dist/v2/types.d.ts +444 -0
- package/dist/v2/types.js +19 -0
- package/dist/v2/web-playwright-local.d.ts +69 -0
- package/dist/v2/web-playwright-local.js +392 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +5 -0
- package/dist/video-agent.js +18 -13
- package/dist/video-planner.js +2 -1
- package/dist/video-prompts.js +3 -3
- package/dist/web-playwright-local.d.ts +126 -0
- package/dist/web-playwright-local.js +819 -0
- package/dist/ws-auth.js +4 -1
- package/dist/ws-broadcast.d.ts +34 -0
- package/dist/ws-broadcast.js +85 -0
- package/dist/ws-connection-limits.d.ts +12 -0
- package/dist/ws-connection-limits.js +44 -0
- package/dist/ws-handler-utils.d.ts +32 -0
- package/dist/ws-handler-utils.js +139 -0
- package/dist/ws-handler.js +294 -164
- package/dist/ws-metrics-server.d.ts +9 -0
- package/dist/ws-metrics-server.js +31 -0
- package/dist/ws-server.js +41 -1
- package/package.json +51 -34
package/dist/ws-handler.js
CHANGED
|
@@ -6,33 +6,43 @@
|
|
|
6
6
|
* so the WS flow does not drift from the HTTP capture flow.
|
|
7
7
|
*/
|
|
8
8
|
import crypto from 'node:crypto';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
9
11
|
import { chromium } from 'playwright';
|
|
10
12
|
import { replayAgent, runAgent, verifyCaptureReadiness } from './agent.js';
|
|
11
|
-
import {
|
|
13
|
+
import { performDeterministicSessionRepair } from './session-profile.js';
|
|
12
14
|
import { captureIsolatedElement } from './element-capture.js';
|
|
13
|
-
import { getCapturePromptValidationError, getCaptureUrlHostname, resolveCaptureRequestPlan, sanitizeStorageSegment,
|
|
15
|
+
import { getCapturePromptValidationError, getCaptureUrlHostname, resolveCaptureRequestPlan, sanitizeStorageSegment, } from './capture-request-plan.js';
|
|
16
|
+
import { lockCaptureViewport, takeViewportLockedScreenshot, } from './capture-viewport-lock.js';
|
|
17
|
+
import { CHROMIUM_ARGS } from './browser-pool.js';
|
|
14
18
|
import { insertCostLogs, resolveAllCosts, updateCostLogCaptureContext, } from './cost-logging.js';
|
|
15
19
|
import { insertScreenshotOperationLog, reconcilePendingBillingOperationCosts, } from './billing-operation-logging.js';
|
|
16
20
|
import { generateAltText } from './capture-alt-text.js';
|
|
17
21
|
import { ensureScreenshotVariantLanguage } from './capture-language-preflight.js';
|
|
18
22
|
import { inferPageIdentitiesWithLLM } from './capture-llm-page-identity.js';
|
|
19
23
|
import { resolveRunModels } from './capture-model-resolution.js';
|
|
24
|
+
import { decideCaptureTransition } from './capture-transition-engine.js';
|
|
20
25
|
import { extractSelectorUpdates, loadScreenshotSelectorMemory, persistScreenshotSelectorMemoryUpdates, } from './capture-selector-memory.js';
|
|
21
26
|
import { hydratePresetConfigFromStorage } from './capture-preset-credentials.js';
|
|
22
|
-
import { applySelectorMemoryUpdates,
|
|
27
|
+
import { applySelectorMemoryUpdates, buildSessionBootstrapProfile, deriveRunSharedAuthProfile, getUrlOrigin, mergeSelectorMemory, resolveIsolatedElementAssignments, resolveScopedSelectorMemory, scopeSelectorMemoryUpdates, shouldNormalizeDialogsBeforeTargetReuse, urlMatchesCaptureTarget, } from './capture-run-optimizer.js';
|
|
23
28
|
import { decryptSessionField, encryptSessionField, } from './capture-session-profile-encryption.js';
|
|
24
29
|
import { buildVariantManifestContext, createVariantCaptureState, markVariantCaptureBlocked, markVariantCaptureInProgress, recordValidatedVariantCapture, validateVariantCaptureState, } from './capture-variant-state.js';
|
|
25
30
|
import { syncStudioVariantAfterCapture } from './capture-studio-sync.js';
|
|
26
31
|
import { applyDeviceFrame, invalidateDeviceConfigCache } from './mockup.js';
|
|
27
32
|
import { localizeStatusBar } from './status-bar-l10n.js';
|
|
28
|
-
import { logger } from './logger.js';
|
|
33
|
+
import { logger, runWithLoggerCallbacks } from './logger.js';
|
|
34
|
+
import { createCaptureBroadcast } from './ws-broadcast.js';
|
|
35
|
+
import { APP_VERSION } from './version.js';
|
|
36
|
+
import { isTerminalAgentResultSuccess, isTerminalVerificationSuccess, } from './types.js';
|
|
37
|
+
import { buildHandoffContext, buildPersistableProfile, buildPreverifiedAgentResult, loadRunHints, makeFailedAgentResult, restorePreparedPageState, } from './ws-handler-utils.js';
|
|
29
38
|
import { getRemoteBrowserCompatibilityError, RemoteBrowser, } from './remote-browser.js';
|
|
30
|
-
import { recordCreditUsage } from './server-credit-usage.js';
|
|
39
|
+
import { recordCreditUsage, revokeCreditUsage } from './server-credit-usage.js';
|
|
31
40
|
import { cleanupExpiredCapturesForOwner, ensureCaptureConfigAllowed, ensureMonthlyCreditsQuota, ensureResourceNotLocked, getBillingAccountForUser, getCaptureOverageState, getBillingPlan, getIncrementalOverageCount, getLockedResourceIds, getOwnerBillingUsage, getProjectOwnerBillingContext, getRemainingOverageCredits, getSignupBonusCredits, getStripeOveragePriceIdForPlan, isYearlySubscription, PlanLimitError, recordStripeMeterEvent, SCREENSHOT_CREDIT_COST, shouldUseStripePlan, } from './server-capture-runtime.js';
|
|
32
41
|
import { buildCaptureWebhookPayload, dispatchProjectCaptureWebhook, getProjectWebhookConfig, } from './server-project-webhooks.js';
|
|
33
42
|
import { getServerPostHog } from './server-posthog.js';
|
|
34
43
|
import { applyPlanScreenshotWatermark } from './server-screenshot-watermark.js';
|
|
35
44
|
import { validateApiKey, requireScope, getSupabase } from './ws-auth.js';
|
|
45
|
+
import { getUserConnectionCount, trackUserConnection, untrackUserConnection, } from './ws-connection-limits.js';
|
|
36
46
|
const BUCKET = 'screenshots';
|
|
37
47
|
function getOpenRouterKey() {
|
|
38
48
|
const key = process.env.OPENROUTER_API_KEY ?? '';
|
|
@@ -74,7 +84,7 @@ async function waitForClientHello(ws, timeoutMs = 3_000) {
|
|
|
74
84
|
return;
|
|
75
85
|
settled = true;
|
|
76
86
|
cleanup();
|
|
77
|
-
resolve(parsed.client);
|
|
87
|
+
resolve({ client: parsed.client, apiKey: parsed.apiKey });
|
|
78
88
|
}
|
|
79
89
|
catch {
|
|
80
90
|
// Ignore non-JSON or non-hello messages until timeout.
|
|
@@ -87,14 +97,16 @@ async function waitForClientHello(ws, timeoutMs = 3_000) {
|
|
|
87
97
|
}
|
|
88
98
|
export async function handleConnection(ws, req) {
|
|
89
99
|
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
|
|
90
|
-
const rawKey = url.searchParams.get('key') ?? '';
|
|
91
100
|
const presetId = url.searchParams.get('preset_id') ?? '';
|
|
92
|
-
const clientHelloPromise = waitForClientHello(ws);
|
|
93
101
|
const sendEvent = (type, data) => {
|
|
94
102
|
if (ws.readyState === ws.OPEN) {
|
|
95
103
|
ws.send(JSON.stringify({ type, ...data }));
|
|
96
104
|
}
|
|
97
105
|
};
|
|
106
|
+
// Wait for client_hello which now carries the API key (protocol v3+).
|
|
107
|
+
// Fall back to query param for older CLIs during transition.
|
|
108
|
+
const clientHello = await waitForClientHello(ws);
|
|
109
|
+
const rawKey = clientHello?.apiKey ?? url.searchParams.get('key') ?? '';
|
|
98
110
|
const auth = await validateApiKey(rawKey);
|
|
99
111
|
if (!auth) {
|
|
100
112
|
sendEvent('error', { message: 'Invalid API key' });
|
|
@@ -121,7 +133,7 @@ export async function handleConnection(ws, req) {
|
|
|
121
133
|
.eq('id', presetId)
|
|
122
134
|
.single();
|
|
123
135
|
if (presetError || !preset) {
|
|
124
|
-
sendEvent('error', { message:
|
|
136
|
+
sendEvent('error', { message: 'Preset not found' });
|
|
125
137
|
ws.close(4004, 'Preset not found');
|
|
126
138
|
return;
|
|
127
139
|
}
|
|
@@ -140,13 +152,14 @@ export async function handleConnection(ws, req) {
|
|
|
140
152
|
ws.close(4003, 'Forbidden');
|
|
141
153
|
return;
|
|
142
154
|
}
|
|
143
|
-
const clientInfo =
|
|
155
|
+
const clientInfo = clientHello?.client ?? null;
|
|
144
156
|
const compatibilityError = getRemoteBrowserCompatibilityError(clientInfo);
|
|
145
157
|
if (compatibilityError) {
|
|
146
158
|
sendEvent('error', { message: compatibilityError });
|
|
147
159
|
ws.close(4000, 'CLI update required');
|
|
148
160
|
return;
|
|
149
161
|
}
|
|
162
|
+
logger.info(`[capture] cli-version: ${clientInfo?.version ?? 'unknown'}, app-version: ${APP_VERSION}`);
|
|
150
163
|
const config = hydratePresetConfigFromStorage(preset.config);
|
|
151
164
|
const projectUrl = String(project.url ?? '');
|
|
152
165
|
const planInput = {
|
|
@@ -161,18 +174,14 @@ export async function handleConnection(ws, req) {
|
|
|
161
174
|
return;
|
|
162
175
|
}
|
|
163
176
|
invalidateDeviceConfigCache();
|
|
164
|
-
const { pageRuns: pages,
|
|
165
|
-
if (pageDefinitionIssues.length > 0) {
|
|
166
|
-
sendEvent('error', {
|
|
167
|
-
message: pageDefinitionIssues.map((issue) => issue.reason).join(' '),
|
|
168
|
-
});
|
|
169
|
-
ws.close(4000, 'Ambiguous capture pages');
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
177
|
+
const { pageRuns: pages, langs, themes, variantPlan, targets, elements, outputScale, totalCaptures, } = await resolveCaptureRequestPlan(planInput);
|
|
172
178
|
sendEvent('progress', { message: `Loaded preset "${preset.name}" for ${projectUrl}` });
|
|
173
179
|
let resolvedModel = '';
|
|
174
180
|
let resolvedFallback;
|
|
175
181
|
let resolvedVisionModel;
|
|
182
|
+
let enableGeminiExplicitCache = false;
|
|
183
|
+
let enableCacheLayoutV2 = false;
|
|
184
|
+
let useVisionModelForAuxTasks = false;
|
|
176
185
|
const providerPreferences = {};
|
|
177
186
|
try {
|
|
178
187
|
const { data: cfgRows } = await supabase
|
|
@@ -181,6 +190,7 @@ export async function handleConnection(ws, req) {
|
|
|
181
190
|
.in('key', [
|
|
182
191
|
'default_model', 'fallback_model', 'vision_model',
|
|
183
192
|
'default_model_provider', 'fallback_model_provider', 'vision_model_provider',
|
|
193
|
+
'enable_gemini_explicit_cache', 'enable_cache_layout_v2', 'use_vision_model_for_aux_tasks',
|
|
184
194
|
]);
|
|
185
195
|
const cfg = {};
|
|
186
196
|
for (const row of cfgRows ?? [])
|
|
@@ -208,6 +218,9 @@ export async function handleConnection(ws, req) {
|
|
|
208
218
|
// Ignore invalid provider preference JSON.
|
|
209
219
|
}
|
|
210
220
|
}
|
|
221
|
+
enableGeminiExplicitCache = cfg.enable_gemini_explicit_cache === '1';
|
|
222
|
+
enableCacheLayoutV2 = cfg.enable_cache_layout_v2 === '1';
|
|
223
|
+
useVisionModelForAuxTasks = cfg.use_vision_model_for_aux_tasks === '1';
|
|
211
224
|
}
|
|
212
225
|
catch {
|
|
213
226
|
// Non-blocking: the app_config table may be missing during bootstrap.
|
|
@@ -227,6 +240,18 @@ export async function handleConnection(ws, req) {
|
|
|
227
240
|
const billingContext = await getProjectOwnerBillingContext(supabase, project.id);
|
|
228
241
|
plan = billingContext.plan;
|
|
229
242
|
billingOwnerUserId = billingContext.ownerUserId;
|
|
243
|
+
// Enforce per-user concurrent connection limit based on plan
|
|
244
|
+
const maxParallel = plan.entitlements.maxParallelCaptures;
|
|
245
|
+
const currentConns = getUserConnectionCount(auth.userId);
|
|
246
|
+
if (currentConns >= maxParallel) {
|
|
247
|
+
sendEvent('error', {
|
|
248
|
+
message: `Concurrent capture limit reached (${maxParallel}). Upgrade your plan for more parallel captures.`,
|
|
249
|
+
});
|
|
250
|
+
ws.close(4029, 'Concurrent capture limit reached');
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
trackUserConnection(auth.userId, ws);
|
|
254
|
+
ws.on('close', () => untrackUserConnection(auth.userId, ws));
|
|
230
255
|
const lockState = await getLockedResourceIds(supabase, billingOwnerUserId, plan);
|
|
231
256
|
ensureResourceNotLocked(project.id, lockState.lockedProjectIds, 'project');
|
|
232
257
|
const lockedPresets = lockState.lockedPresetIdsByProject.get(project.id);
|
|
@@ -295,16 +320,71 @@ export async function handleConnection(ws, req) {
|
|
|
295
320
|
started_at: runStartedAt,
|
|
296
321
|
});
|
|
297
322
|
if (runInsertError) {
|
|
298
|
-
|
|
323
|
+
logger.error(`[capture] Failed to create run: ${runInsertError.message}`);
|
|
324
|
+
sendEvent('error', { message: 'Failed to create capture run' });
|
|
299
325
|
ws.close(5000, 'Internal error');
|
|
300
326
|
return;
|
|
301
327
|
}
|
|
328
|
+
// ── Live broadcast bridge ─────────────────────────────────────────
|
|
329
|
+
const broadcast = createCaptureBroadcast(runId, supabase);
|
|
330
|
+
const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? process.env.APP_URL ?? '';
|
|
331
|
+
if (appUrl) {
|
|
332
|
+
sendEvent('live_url', { url: `${appUrl}/capture/live?runId=${runId}` });
|
|
333
|
+
}
|
|
334
|
+
broadcast.send({ type: 'run_id', data: { runId } });
|
|
335
|
+
/** Wrap an async function with logger callbacks that forward to the broadcast. */
|
|
336
|
+
function withBroadcastCallbacks(ctx, fn) {
|
|
337
|
+
return runWithLoggerCallbacks({
|
|
338
|
+
onLog: (entry) => {
|
|
339
|
+
broadcast.send({ type: 'log', data: { ...entry, lang: ctx.lang, theme: ctx.theme, targetId: ctx.targetId } });
|
|
340
|
+
if (entry.iteration !== undefined && entry.maxIterations !== undefined) {
|
|
341
|
+
broadcast.send({
|
|
342
|
+
type: 'progress',
|
|
343
|
+
data: {
|
|
344
|
+
iteration: entry.iteration,
|
|
345
|
+
maxIterations: entry.maxIterations,
|
|
346
|
+
lang: ctx.lang,
|
|
347
|
+
theme: ctx.theme,
|
|
348
|
+
targetId: ctx.targetId,
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
onScreenshot: (base64) => {
|
|
354
|
+
broadcast.sendScreenshot(base64, { lang: ctx.lang, theme: ctx.theme, targetId: ctx.targetId });
|
|
355
|
+
},
|
|
356
|
+
onReasoningDelta: (delta, messageId) => {
|
|
357
|
+
broadcast.send({
|
|
358
|
+
type: 'reasoning_delta',
|
|
359
|
+
data: { delta, messageId, lang: ctx.lang, theme: ctx.theme, targetId: ctx.targetId },
|
|
360
|
+
});
|
|
361
|
+
},
|
|
362
|
+
}, fn);
|
|
363
|
+
}
|
|
302
364
|
const remoteBrowser = new RemoteBrowser(ws);
|
|
303
365
|
const abortController = new AbortController();
|
|
304
366
|
let aborted = false;
|
|
367
|
+
// Idle timeout: close connection after 10 minutes of inactivity.
|
|
368
|
+
const IDLE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
369
|
+
let idleTimer = setTimeout(() => {
|
|
370
|
+
logger.warn('[WS] Closing idle connection');
|
|
371
|
+
sendEvent('error', { message: 'Connection timed out due to inactivity' });
|
|
372
|
+
ws.close(4008, 'Idle timeout');
|
|
373
|
+
}, IDLE_TIMEOUT_MS);
|
|
374
|
+
const resetIdleTimer = () => {
|
|
375
|
+
clearTimeout(idleTimer);
|
|
376
|
+
idleTimer = setTimeout(() => {
|
|
377
|
+
logger.warn('[WS] Closing idle connection');
|
|
378
|
+
sendEvent('error', { message: 'Connection timed out due to inactivity' });
|
|
379
|
+
ws.close(4008, 'Idle timeout');
|
|
380
|
+
}, IDLE_TIMEOUT_MS);
|
|
381
|
+
};
|
|
382
|
+
ws.on('message', resetIdleTimer);
|
|
305
383
|
ws.on('close', () => {
|
|
384
|
+
clearTimeout(idleTimer);
|
|
306
385
|
aborted = true;
|
|
307
386
|
abortController.abort();
|
|
387
|
+
broadcast.close();
|
|
308
388
|
});
|
|
309
389
|
const mockupState = {
|
|
310
390
|
browser: null,
|
|
@@ -312,7 +392,10 @@ export async function handleConnection(ws, req) {
|
|
|
312
392
|
};
|
|
313
393
|
const getMockupContext = async () => {
|
|
314
394
|
if (!mockupState.context) {
|
|
315
|
-
mockupState.browser = await chromium.launch({
|
|
395
|
+
mockupState.browser = await chromium.launch({
|
|
396
|
+
headless: true,
|
|
397
|
+
args: CHROMIUM_ARGS,
|
|
398
|
+
});
|
|
316
399
|
mockupState.context = await mockupState.browser.newContext();
|
|
317
400
|
}
|
|
318
401
|
return mockupState.context;
|
|
@@ -540,14 +623,19 @@ export async function handleConnection(ws, req) {
|
|
|
540
623
|
};
|
|
541
624
|
const persistFullPageCapture = async (params) => {
|
|
542
625
|
const { pageRun, target, lang, theme, agentResult, includeActions, includeWorkflowScreenshots } = params;
|
|
626
|
+
broadcast.send({ type: 'capture_start', data: { lang, theme, targetId: target.id, targetLabel: target.label } });
|
|
543
627
|
const pageSegment = sanitizeStorageSegment(pageRun.pageId);
|
|
544
|
-
let finalBuffer =
|
|
628
|
+
let finalBuffer = Buffer.alloc(0);
|
|
545
629
|
let rawScreenshotUrl = null;
|
|
546
630
|
let browserBar;
|
|
547
631
|
try {
|
|
548
|
-
|
|
549
|
-
await remoteBrowser.
|
|
550
|
-
finalBuffer =
|
|
632
|
+
logger.info(`[persist] target=${target.id} viewport=${target.viewport.width}x${target.viewport.height} deviceScaleFactor=${outputScale} deviceFrame=${target.deviceFrame ?? 'none'}`);
|
|
633
|
+
const lockedScreenshot = await takeViewportLockedScreenshot(remoteBrowser, target.viewport, { deviceScaleFactor: outputScale });
|
|
634
|
+
finalBuffer = lockedScreenshot.buffer;
|
|
635
|
+
logger.info(`[persist] screenshot=${lockedScreenshot.screenshotSize.width}x${lockedScreenshot.screenshotSize.height} expected=${lockedScreenshot.expectedScreenshotSize.width}x${lockedScreenshot.expectedScreenshotSize.height} repaired=${lockedScreenshot.repaired}`);
|
|
636
|
+
if (lockedScreenshot.repaired) {
|
|
637
|
+
logger.info(`Viewport lock repaired before persisting ${target.id}: expected ${lockedScreenshot.expectedScreenshotSize.width}x${lockedScreenshot.expectedScreenshotSize.height}, got ${lockedScreenshot.screenshotSize.width}x${lockedScreenshot.screenshotSize.height}.`);
|
|
638
|
+
}
|
|
551
639
|
if (target.deviceFrame) {
|
|
552
640
|
const rawPath = `screenshots/${auth.userId}/${runId}/${pageSegment}/${target.id}_${lang}_${theme}_raw.png`;
|
|
553
641
|
const { error } = await supabase.storage
|
|
@@ -572,6 +660,7 @@ export async function handleConnection(ws, req) {
|
|
|
572
660
|
const localizedOptions = {
|
|
573
661
|
...mockupOptions,
|
|
574
662
|
outputScale,
|
|
663
|
+
colorScheme: theme,
|
|
575
664
|
statusBar: localizeStatusBar(mockupOptions.statusBar, lang),
|
|
576
665
|
browserBar,
|
|
577
666
|
};
|
|
@@ -590,10 +679,11 @@ export async function handleConnection(ws, req) {
|
|
|
590
679
|
const screenshotUrl = uploadError
|
|
591
680
|
? null
|
|
592
681
|
: supabase.storage.from(BUCKET).getPublicUrl(storagePath).data.publicUrl;
|
|
593
|
-
const
|
|
682
|
+
const terminalAgentSuccess = isTerminalAgentResultSuccess(agentResult);
|
|
683
|
+
const persistedSuccess = terminalAgentSuccess && !!screenshotUrl;
|
|
594
684
|
const persistedAssessment = persistedSuccess
|
|
595
685
|
? agentResult.assessment
|
|
596
|
-
:
|
|
686
|
+
: terminalAgentSuccess
|
|
597
687
|
? 'Capture completed but screenshot upload failed.'
|
|
598
688
|
: agentResult.assessment || 'Capture failed';
|
|
599
689
|
captureResults.push({
|
|
@@ -610,6 +700,22 @@ export async function handleConnection(ws, req) {
|
|
|
610
700
|
iterations: agentResult.iterations,
|
|
611
701
|
finalScreenshotUrl: screenshotUrl ?? undefined,
|
|
612
702
|
});
|
|
703
|
+
broadcast.send({
|
|
704
|
+
type: 'result',
|
|
705
|
+
data: {
|
|
706
|
+
url: pageRun.url,
|
|
707
|
+
lang,
|
|
708
|
+
theme,
|
|
709
|
+
targetId: target.id,
|
|
710
|
+
targetLabel: target.label,
|
|
711
|
+
success: persistedSuccess,
|
|
712
|
+
assessment: persistedAssessment,
|
|
713
|
+
iterations: agentResult.iterations,
|
|
714
|
+
finalScreenshotUrl: screenshotUrl ?? undefined,
|
|
715
|
+
// Omit finalScreenshot base64 — too large for broadcast.
|
|
716
|
+
finalScreenshot: '',
|
|
717
|
+
},
|
|
718
|
+
});
|
|
613
719
|
const costLogIds = await logAgentUsage({
|
|
614
720
|
usage: agentResult.usage,
|
|
615
721
|
lang,
|
|
@@ -708,7 +814,7 @@ export async function handleConnection(ws, req) {
|
|
|
708
814
|
});
|
|
709
815
|
void reconcilePendingBillingOperationCosts(supabase).catch(() => undefined);
|
|
710
816
|
if (!persistedSuccess) {
|
|
711
|
-
const errorType =
|
|
817
|
+
const errorType = terminalAgentSuccess
|
|
712
818
|
? 'storage_upload_failure'
|
|
713
819
|
: agentResult.actions.some((action) => action.action === 'give_up')
|
|
714
820
|
? 'give_up'
|
|
@@ -820,6 +926,7 @@ export async function handleConnection(ws, req) {
|
|
|
820
926
|
sendEvent('progress', {
|
|
821
927
|
message: `[${captureIndex}/${totalCaptures}] ${lang}/${theme} · ${pageRun.pageId ?? 'main'} · ${target.label}`,
|
|
822
928
|
});
|
|
929
|
+
broadcast.send({ type: 'capture_complete', data: { lang, theme, targetId: target.id, success: isTerminalAgentResultSuccess(agentResult) } });
|
|
823
930
|
}
|
|
824
931
|
};
|
|
825
932
|
const persistElementCapture = async (params) => {
|
|
@@ -1025,7 +1132,7 @@ export async function handleConnection(ws, req) {
|
|
|
1025
1132
|
});
|
|
1026
1133
|
}
|
|
1027
1134
|
};
|
|
1028
|
-
|
|
1135
|
+
const capturePlanPayload = {
|
|
1029
1136
|
variants: variantPlan.map((variant) => ({
|
|
1030
1137
|
key: variant.key,
|
|
1031
1138
|
lang: variant.lang,
|
|
@@ -1033,8 +1140,11 @@ export async function handleConnection(ws, req) {
|
|
|
1033
1140
|
pages: variant.pages,
|
|
1034
1141
|
})),
|
|
1035
1142
|
totalCaptures,
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1143
|
+
};
|
|
1144
|
+
sendEvent('capture_plan', capturePlanPayload);
|
|
1145
|
+
broadcast.send({ type: 'capture_plan', data: capturePlanPayload });
|
|
1146
|
+
const pageIdentityModel = (useVisionModelForAuxTasks && resolvedVisionModel) ? resolvedVisionModel : resolvedModel;
|
|
1147
|
+
const { identities: sharedPageIdentities } = await inferPageIdentitiesWithLLM(variantPlan[0]?.pages ?? [], pageIdentityModel, getOpenRouterKey(), Object.keys(providerPreferences).length > 0 ? providerPreferences : undefined);
|
|
1038
1148
|
try {
|
|
1039
1149
|
for (const [variantIndex, variant] of variantPlan.entries()) {
|
|
1040
1150
|
if (aborted)
|
|
@@ -1044,6 +1154,7 @@ export async function handleConnection(ws, req) {
|
|
|
1044
1154
|
const firstPage = variantPages[0];
|
|
1045
1155
|
const variantCaptureStartIndex = captureIndex;
|
|
1046
1156
|
const expectedVariantCaptureCount = (variantPages.length * targets.length) + elements.length;
|
|
1157
|
+
const variantCaptureIds = [];
|
|
1047
1158
|
try {
|
|
1048
1159
|
const firstDomain = getCaptureUrlHostname(firstPage.url);
|
|
1049
1160
|
const persistedSessionProfile = await loadSessionProfileForVariant(firstDomain, lang, theme);
|
|
@@ -1105,6 +1216,40 @@ export async function handleConnection(ws, req) {
|
|
|
1105
1216
|
await remoteBrowser.wait(500);
|
|
1106
1217
|
await remoteBrowser.dismissOverlays().catch(() => undefined);
|
|
1107
1218
|
}
|
|
1219
|
+
// Pre-authenticate before the language preflight when credentials are available.
|
|
1220
|
+
// Language controls are often locked behind login — without this the preflight
|
|
1221
|
+
// wastes all its iterations trying to switch language on the public page.
|
|
1222
|
+
// We force login (bypassing authMatches) because the persisted session profile
|
|
1223
|
+
// may report "authenticated" even though the current browser context has no
|
|
1224
|
+
// active session (cookies were restored but the server session expired).
|
|
1225
|
+
if (credentials?.email && credentials?.password && credentials.loginUrl) {
|
|
1226
|
+
try {
|
|
1227
|
+
await remoteBrowser.navigateTo(credentials.loginUrl);
|
|
1228
|
+
await remoteBrowser.wait(500);
|
|
1229
|
+
await performDeterministicSessionRepair(remoteBrowser, {
|
|
1230
|
+
startUrl: firstPage.url,
|
|
1231
|
+
requestedLang: lang,
|
|
1232
|
+
requestedTheme: theme,
|
|
1233
|
+
credentials,
|
|
1234
|
+
profile: activeSessionProfile,
|
|
1235
|
+
selectorMemory: variantStartSelectorMemory,
|
|
1236
|
+
});
|
|
1237
|
+
sendEvent('progress', { message: 'Pre-authenticated before language preflight.' });
|
|
1238
|
+
// Navigate back to the start page after login redirect
|
|
1239
|
+
if (!urlMatchesCaptureTarget(remoteBrowser.currentPage.url(), firstPage.url)) {
|
|
1240
|
+
await remoteBrowser.navigateTo(firstPage.url);
|
|
1241
|
+
await remoteBrowser.wait(500);
|
|
1242
|
+
await remoteBrowser.dismissOverlays().catch(() => undefined);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
catch {
|
|
1246
|
+
// Non-blocking: if pre-auth fails the language preflight will retry.
|
|
1247
|
+
if (!urlMatchesCaptureTarget(remoteBrowser.currentPage.url(), firstPage.url)) {
|
|
1248
|
+
await remoteBrowser.navigateTo(firstPage.url).catch(() => undefined);
|
|
1249
|
+
await remoteBrowser.wait(500);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1108
1253
|
const variantLanguagePreflight = await ensureScreenshotVariantLanguage({
|
|
1109
1254
|
browser: remoteBrowser,
|
|
1110
1255
|
requestedLang: lang,
|
|
@@ -1124,19 +1269,21 @@ export async function handleConnection(ws, req) {
|
|
|
1124
1269
|
await remoteBrowser.dismissOverlays().catch(() => undefined);
|
|
1125
1270
|
},
|
|
1126
1271
|
runLanguageSwitchAgent: async ({ languageState, themeState }) => {
|
|
1127
|
-
return runAgent(remoteBrowser, {
|
|
1272
|
+
return withBroadcastCallbacks({ lang, theme }, () => runAgent(remoteBrowser, {
|
|
1128
1273
|
url: remoteBrowser.currentPage.url() || firstPage.url,
|
|
1129
1274
|
prompt: [
|
|
1130
1275
|
`Prepare the current page so the UI is rendered in "${lang}" and the theme is "${theme}".`,
|
|
1131
1276
|
`Current inspection: language=${languageState?.detected ?? 'unknown'}; theme=${themeState?.detected ?? 'unknown'}.`,
|
|
1277
|
+
`Strategy: try navigate_to /settings first for language/theme controls; if the settings page has no such controls, look for an account/profile button (usually at the bottom of the sidebar — NOT the app logo) and open its dropdown menu. Do not click the same element more than once if it had no effect.`,
|
|
1278
|
+
config.langInstructions ? `Language switch hint: ${config.langInstructions}` : '',
|
|
1132
1279
|
firstPage.prompt,
|
|
1133
|
-
].join(' '),
|
|
1280
|
+
].filter(Boolean).join(' '),
|
|
1134
1281
|
dark: theme === 'dark',
|
|
1135
1282
|
langs: [lang],
|
|
1136
|
-
outputDir: '
|
|
1283
|
+
outputDir: path.join(os.tmpdir(), 'autokap-ws'),
|
|
1137
1284
|
headed: false,
|
|
1138
1285
|
viewport: firstTarget.viewport,
|
|
1139
|
-
maxIterations: Math.min(maxIterations,
|
|
1286
|
+
maxIterations: Math.min(maxIterations, 12),
|
|
1140
1287
|
model: resolvedModel,
|
|
1141
1288
|
fallbackModel: resolvedFallback,
|
|
1142
1289
|
visionModel: resolvedVisionModel,
|
|
@@ -1146,10 +1293,12 @@ export async function handleConnection(ws, req) {
|
|
|
1146
1293
|
currentTheme: theme,
|
|
1147
1294
|
selectorMemory: Object.keys(variantStartSelectorMemory).length > 0 ? variantStartSelectorMemory : undefined,
|
|
1148
1295
|
sessionProfile: activeSessionProfile,
|
|
1296
|
+
langInstructions: config.langInstructions,
|
|
1297
|
+
themeInstructions: config.themeInstructions,
|
|
1149
1298
|
runHints: runHints && runHints.length > 0 ? runHints : undefined,
|
|
1150
1299
|
runMode: 'language_preflight',
|
|
1151
1300
|
abortSignal: abortController.signal,
|
|
1152
|
-
}, getOpenRouterKey());
|
|
1301
|
+
}, getOpenRouterKey()));
|
|
1153
1302
|
},
|
|
1154
1303
|
});
|
|
1155
1304
|
if (variantLanguagePreflight.selectorUpdates.length > 0) {
|
|
@@ -1191,13 +1340,14 @@ export async function handleConnection(ws, req) {
|
|
|
1191
1340
|
pageIdentity: variantManifest.currentPageIdentity ?? null,
|
|
1192
1341
|
pageUrl: pageRun.url,
|
|
1193
1342
|
});
|
|
1343
|
+
let trustedHandoffContext;
|
|
1194
1344
|
const buildConfig = (viewport = firstTarget.viewport) => {
|
|
1195
1345
|
const nextConfig = {
|
|
1196
1346
|
url: pageRun.url,
|
|
1197
1347
|
prompt: pageRun.prompt,
|
|
1198
1348
|
dark: theme === 'dark',
|
|
1199
1349
|
langs: [lang],
|
|
1200
|
-
outputDir: '
|
|
1350
|
+
outputDir: path.join(os.tmpdir(), 'autokap-ws'),
|
|
1201
1351
|
headed: false,
|
|
1202
1352
|
viewport,
|
|
1203
1353
|
maxIterations,
|
|
@@ -1212,13 +1362,17 @@ export async function handleConnection(ws, req) {
|
|
|
1212
1362
|
currentTheme: theme,
|
|
1213
1363
|
reasoningLocale: config.reasoningLocale,
|
|
1214
1364
|
reasoningEffort: config.reasoningEffort ?? 'medium',
|
|
1365
|
+
reasoningCapableModels: config.reasoningCapableModels,
|
|
1215
1366
|
runHints: runHints && runHints.length > 0 ? runHints : undefined,
|
|
1216
1367
|
selectorMemory: Object.keys(selectorMemory).length > 0 ? selectorMemory : undefined,
|
|
1217
1368
|
sessionProfile: activeSessionProfile,
|
|
1218
|
-
handoffContext:
|
|
1369
|
+
handoffContext: trustedHandoffContext,
|
|
1219
1370
|
variantManifest,
|
|
1220
1371
|
runMode: 'capture',
|
|
1221
1372
|
analyticsId: auth.userId,
|
|
1373
|
+
enableGeminiExplicitCache,
|
|
1374
|
+
enableCacheLayoutV2,
|
|
1375
|
+
useVisionModelForAuxTasks,
|
|
1222
1376
|
enableDeterministicRecovery: true,
|
|
1223
1377
|
enableRecoveryEvaluator: true,
|
|
1224
1378
|
enableSalienceCompression: true,
|
|
@@ -1236,49 +1390,90 @@ export async function handleConnection(ws, req) {
|
|
|
1236
1390
|
message: `Preparing ${lang}/${theme} · ${pageId} (${pageIndex + 1}/${variantPages.length})`,
|
|
1237
1391
|
});
|
|
1238
1392
|
let primaryAgentResult = null;
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1393
|
+
await remoteBrowser.dismissOverlays().catch(() => undefined);
|
|
1394
|
+
let baselineObservation = await remoteBrowser.captureObservation().catch(() => null);
|
|
1395
|
+
if (shouldNormalizeDialogsBeforeTargetReuse({
|
|
1396
|
+
currentDialogCount: baselineObservation?.dialogCount,
|
|
1397
|
+
pageIdentity: variantManifest.currentPageIdentity ?? null,
|
|
1398
|
+
})) {
|
|
1399
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1400
|
+
await remoteBrowser.pressKey('Escape').catch(() => undefined);
|
|
1401
|
+
await remoteBrowser.wait(180);
|
|
1402
|
+
baselineObservation = await remoteBrowser.captureObservation().catch(() => null);
|
|
1403
|
+
if (!shouldNormalizeDialogsBeforeTargetReuse({
|
|
1404
|
+
currentDialogCount: baselineObservation?.dialogCount,
|
|
1405
|
+
pageIdentity: variantManifest.currentPageIdentity ?? null,
|
|
1406
|
+
})) {
|
|
1407
|
+
break;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
await remoteBrowser.wait(250);
|
|
1412
|
+
baselineObservation = baselineObservation ?? await remoteBrowser.captureObservation().catch(() => null);
|
|
1413
|
+
const baselineSignals = await remoteBrowser.capturePageSignals({
|
|
1414
|
+
timeoutMs: 1_500,
|
|
1415
|
+
}).catch(() => null);
|
|
1416
|
+
const transition = decideCaptureTransition({
|
|
1251
1417
|
pageIndex,
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1418
|
+
currentUrl: remoteBrowser.currentPage.url(),
|
|
1419
|
+
targetUrl: pageRun.url,
|
|
1420
|
+
pageIdentity: variantManifest.currentPageIdentity ?? null,
|
|
1421
|
+
observation: baselineObservation,
|
|
1422
|
+
pageSignals: baselineSignals,
|
|
1423
|
+
profile: activeSessionProfile,
|
|
1424
|
+
handoffContext: carryoverContext,
|
|
1425
|
+
requestedLang: lang,
|
|
1426
|
+
requestedTheme: theme,
|
|
1427
|
+
});
|
|
1428
|
+
trustedHandoffContext = transition.allowFastPath ? carryoverContext : undefined;
|
|
1429
|
+
sendEvent('progress', {
|
|
1430
|
+
message: `Transition ${lang}/${theme} · ${pageId}: ${transition.mode}`
|
|
1431
|
+
+ (transition.reasons[0] ? ` (${transition.reasons[0]})` : ''),
|
|
1256
1432
|
});
|
|
1257
|
-
if (
|
|
1433
|
+
if (transition.mode === 'dirty') {
|
|
1434
|
+
const storageState = await remoteBrowser.exportStorageState().catch(() => undefined)
|
|
1435
|
+
?? activeSessionProfile?.storageState;
|
|
1436
|
+
const sessionStorage = await remoteBrowser.exportSessionStorage().catch(() => undefined)
|
|
1437
|
+
?? activeSessionProfile?.sessionStorage;
|
|
1438
|
+
await remoteBrowser.recreateContext({
|
|
1439
|
+
viewport: firstTarget.viewport,
|
|
1440
|
+
deviceScaleFactor: outputScale,
|
|
1441
|
+
lang,
|
|
1442
|
+
colorScheme: theme,
|
|
1443
|
+
storageState,
|
|
1444
|
+
});
|
|
1445
|
+
await remoteBrowser.prepareSessionStorage(sessionStorage, { replace: false });
|
|
1446
|
+
await remoteBrowser.navigateTo(pageRun.url);
|
|
1447
|
+
await remoteBrowser.wait(500);
|
|
1448
|
+
await remoteBrowser.dismissOverlays().catch(() => undefined);
|
|
1449
|
+
trustedHandoffContext = undefined;
|
|
1450
|
+
}
|
|
1451
|
+
else if (transition.shouldNavigateToCanonical) {
|
|
1452
|
+
await remoteBrowser.navigateTo(pageRun.url);
|
|
1453
|
+
await remoteBrowser.wait(500);
|
|
1454
|
+
await remoteBrowser.dismissOverlays().catch(() => undefined);
|
|
1455
|
+
trustedHandoffContext = undefined;
|
|
1456
|
+
}
|
|
1457
|
+
if (transition.allowFastPath) {
|
|
1258
1458
|
const readiness = await verifyCaptureReadiness(remoteBrowser, buildConfig(), getOpenRouterKey(), {
|
|
1259
1459
|
assessment: 'Sequential handoff preflight: approve only if the current live page already satisfies this capture.',
|
|
1260
1460
|
stepNumber: 0,
|
|
1261
1461
|
}).catch(() => null);
|
|
1262
|
-
if (readiness
|
|
1462
|
+
if (readiness && isTerminalVerificationSuccess(readiness)) {
|
|
1263
1463
|
primaryAgentResult = buildPreverifiedAgentResult('Capture approved directly from sequential handoff state.', readiness);
|
|
1264
1464
|
}
|
|
1265
1465
|
}
|
|
1266
|
-
if (!primaryAgentResult && !canUseSequentialHandoff && !reuseLiveState && !urlMatchesCaptureTarget(currentUrl, pageRun.url)) {
|
|
1267
|
-
await remoteBrowser.navigateTo(pageRun.url);
|
|
1268
|
-
await remoteBrowser.wait(500);
|
|
1269
|
-
await remoteBrowser.dismissOverlays().catch(() => undefined);
|
|
1270
|
-
}
|
|
1271
1466
|
if (!primaryAgentResult && variantIndex > 0) {
|
|
1272
1467
|
const recordedActions = liveVariantActions.get(pageId);
|
|
1273
1468
|
if (recordedActions && recordedActions.length > 0) {
|
|
1274
1469
|
sendEvent('progress', {
|
|
1275
1470
|
message: `Replaying first successful variant for ${lang}/${theme} · ${pageId}`,
|
|
1276
1471
|
});
|
|
1277
|
-
primaryAgentResult = await replayAgent(remoteBrowser, buildConfig(), getOpenRouterKey(), recordedActions, { allowFullAgentFallback: true }).catch((error) => makeFailedAgentResult(error.message));
|
|
1472
|
+
primaryAgentResult = await withBroadcastCallbacks({ lang, theme, targetId: firstTarget.id }, () => replayAgent(remoteBrowser, buildConfig(), getOpenRouterKey(), recordedActions, { allowFullAgentFallback: true })).catch((error) => makeFailedAgentResult(error.message));
|
|
1278
1473
|
}
|
|
1279
1474
|
}
|
|
1280
1475
|
if (!primaryAgentResult) {
|
|
1281
|
-
primaryAgentResult = await runAgent(remoteBrowser, buildConfig(), getOpenRouterKey()).catch((error) => makeFailedAgentResult(error.message));
|
|
1476
|
+
primaryAgentResult = await withBroadcastCallbacks({ lang, theme, targetId: firstTarget.id }, () => runAgent(remoteBrowser, buildConfig(), getOpenRouterKey())).catch((error) => makeFailedAgentResult(error.message));
|
|
1282
1477
|
}
|
|
1283
1478
|
const preparedPageUrl = remoteBrowser.currentPage.url() || pageRun.url;
|
|
1284
1479
|
await persistFullPageCapture({
|
|
@@ -1291,8 +1486,15 @@ export async function handleConnection(ws, req) {
|
|
|
1291
1486
|
includeWorkflowScreenshots: true,
|
|
1292
1487
|
});
|
|
1293
1488
|
for (const target of targets.slice(1)) {
|
|
1294
|
-
await remoteBrowser
|
|
1295
|
-
|
|
1489
|
+
await lockCaptureViewport(remoteBrowser, target.viewport, { settleMs: 250 });
|
|
1490
|
+
// Re-navigate to force a full CSS re-layout at the new viewport width.
|
|
1491
|
+
// Without this, the page content stays rendered at firstTarget's width
|
|
1492
|
+
// and gets squeezed into the smaller viewport.
|
|
1493
|
+
const currentUrl = remoteBrowser.currentPage.url();
|
|
1494
|
+
if (currentUrl) {
|
|
1495
|
+
await remoteBrowser.navigateTo(currentUrl);
|
|
1496
|
+
await remoteBrowser.wait(500);
|
|
1497
|
+
}
|
|
1296
1498
|
await remoteBrowser.dismissOverlays().catch(() => undefined);
|
|
1297
1499
|
let targetResult;
|
|
1298
1500
|
const targetConfig = buildConfig(target.viewport);
|
|
@@ -1300,11 +1502,11 @@ export async function handleConnection(ws, req) {
|
|
|
1300
1502
|
assessment: 'Same-session target recheck: approve only if the resized page still satisfies the capture.',
|
|
1301
1503
|
stepNumber: 0,
|
|
1302
1504
|
}).catch(() => null);
|
|
1303
|
-
if (targetReadiness
|
|
1505
|
+
if (targetReadiness && isTerminalVerificationSuccess(targetReadiness)) {
|
|
1304
1506
|
targetResult = buildPreverifiedAgentResult('Capture approved after same-session target recheck.', targetReadiness);
|
|
1305
1507
|
}
|
|
1306
1508
|
else {
|
|
1307
|
-
targetResult = await replayAgent(remoteBrowser, targetConfig, getOpenRouterKey(), primaryAgentResult.actions, { allowFullAgentFallback: false }).catch((error) => makeFailedAgentResult(error.message));
|
|
1509
|
+
targetResult = await withBroadcastCallbacks({ lang, theme, targetId: target.id }, () => replayAgent(remoteBrowser, targetConfig, getOpenRouterKey(), primaryAgentResult.actions, { allowFullAgentFallback: false })).catch((error) => makeFailedAgentResult(error.message));
|
|
1308
1510
|
}
|
|
1309
1511
|
await persistFullPageCapture({
|
|
1310
1512
|
pageRun,
|
|
@@ -1317,7 +1519,7 @@ export async function handleConnection(ws, req) {
|
|
|
1317
1519
|
});
|
|
1318
1520
|
await restorePreparedPageState(remoteBrowser, firstTarget.viewport, preparedPageUrl);
|
|
1319
1521
|
}
|
|
1320
|
-
if (primaryAgentResult
|
|
1522
|
+
if (isTerminalAgentResultSuccess(primaryAgentResult)) {
|
|
1321
1523
|
const recorded = recordValidatedVariantCapture({
|
|
1322
1524
|
state: variantState,
|
|
1323
1525
|
capture: {
|
|
@@ -1344,7 +1546,7 @@ export async function handleConnection(ws, req) {
|
|
|
1344
1546
|
};
|
|
1345
1547
|
}
|
|
1346
1548
|
}
|
|
1347
|
-
if (primaryAgentResult
|
|
1549
|
+
if (isTerminalAgentResultSuccess(primaryAgentResult)) {
|
|
1348
1550
|
if (primaryAgentResult.actions.length > 0 && !liveVariantActions.has(pageId)) {
|
|
1349
1551
|
liveVariantActions.set(pageId, primaryAgentResult.actions);
|
|
1350
1552
|
}
|
|
@@ -1434,6 +1636,7 @@ export async function handleConnection(ws, req) {
|
|
|
1434
1636
|
theme,
|
|
1435
1637
|
selectorMemory,
|
|
1436
1638
|
actions: primaryAgentResult.actions,
|
|
1639
|
+
agentResult: primaryAgentResult,
|
|
1437
1640
|
});
|
|
1438
1641
|
}
|
|
1439
1642
|
else {
|
|
@@ -1444,6 +1647,18 @@ export async function handleConnection(ws, req) {
|
|
|
1444
1647
|
});
|
|
1445
1648
|
carryoverContext = undefined;
|
|
1446
1649
|
}
|
|
1650
|
+
// Always export storage state after each page attempt so subsequent
|
|
1651
|
+
// variants can reuse the authenticated session even if this capture failed.
|
|
1652
|
+
try {
|
|
1653
|
+
const latestStorageState = await remoteBrowser.exportStorageState();
|
|
1654
|
+
if (latestStorageState) {
|
|
1655
|
+
runSharedStorageState = latestStorageState;
|
|
1656
|
+
runSharedSessionStorage = await remoteBrowser.exportSessionStorage().catch(() => undefined);
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
catch {
|
|
1660
|
+
// Best-effort: keep previous storage state
|
|
1661
|
+
}
|
|
1447
1662
|
}
|
|
1448
1663
|
const variantValidation = validateVariantCaptureState(variantState);
|
|
1449
1664
|
if (!variantValidation.ok) {
|
|
@@ -1462,12 +1677,17 @@ export async function handleConnection(ws, req) {
|
|
|
1462
1677
|
success: variantValidation.ok,
|
|
1463
1678
|
reason: variantValidation.ok ? null : 'Variant completed with blocking issues.',
|
|
1464
1679
|
});
|
|
1680
|
+
broadcast.send({ type: 'variant_complete', data: { lang, theme, success: variantValidation.ok, reason: variantValidation.ok ? undefined : 'Variant completed with blocking issues.' } });
|
|
1465
1681
|
}
|
|
1466
1682
|
catch (error) {
|
|
1467
1683
|
if (aborted)
|
|
1468
1684
|
throw error;
|
|
1469
1685
|
const message = error.message;
|
|
1470
1686
|
logger.error(`Variant ${lang}/${theme} failed: ${message}`);
|
|
1687
|
+
// Revoke credits for captures that were recorded during this failed variant
|
|
1688
|
+
for (const failedCaptureId of variantCaptureIds) {
|
|
1689
|
+
await revokeCreditUsage(supabase, failedCaptureId).catch((err) => logger.error(`Failed to revoke credit for capture ${failedCaptureId}: ${err.message}`));
|
|
1690
|
+
}
|
|
1471
1691
|
const consumedCaptures = captureIndex - variantCaptureStartIndex;
|
|
1472
1692
|
const remainingCaptures = Math.max(0, expectedVariantCaptureCount - consumedCaptures);
|
|
1473
1693
|
if (remainingCaptures > 0) {
|
|
@@ -1497,6 +1717,7 @@ export async function handleConnection(ws, req) {
|
|
|
1497
1717
|
success: false,
|
|
1498
1718
|
reason: message,
|
|
1499
1719
|
});
|
|
1720
|
+
broadcast.send({ type: 'variant_complete', data: { lang, theme, success: false, reason: message } });
|
|
1500
1721
|
continue;
|
|
1501
1722
|
}
|
|
1502
1723
|
}
|
|
@@ -1513,6 +1734,8 @@ export async function handleConnection(ws, req) {
|
|
|
1513
1734
|
})
|
|
1514
1735
|
.eq('id', runId);
|
|
1515
1736
|
sendEvent('error', { message });
|
|
1737
|
+
broadcast.send({ type: 'error', data: { message } });
|
|
1738
|
+
broadcast.close();
|
|
1516
1739
|
await finalizeRunSideEffects();
|
|
1517
1740
|
remoteBrowser.destroy();
|
|
1518
1741
|
await closeMockup();
|
|
@@ -1540,6 +1763,8 @@ export async function handleConnection(ws, req) {
|
|
|
1540
1763
|
},
|
|
1541
1764
|
});
|
|
1542
1765
|
sendEvent('done', { summary: { successes: successCount, total: totalCaptures } });
|
|
1766
|
+
broadcast.send({ type: 'done', data: { totalCaptures, successCount } });
|
|
1767
|
+
broadcast.close();
|
|
1543
1768
|
if (plan.entitlements.captureCompleteWebhook) {
|
|
1544
1769
|
try {
|
|
1545
1770
|
const webhookConfig = await getProjectWebhookConfig(supabase, project.id);
|
|
@@ -1564,100 +1789,5 @@ export async function handleConnection(ws, req) {
|
|
|
1564
1789
|
await closeMockup();
|
|
1565
1790
|
logger.success(`Run ${runId} completed: ${successCount}/${totalCaptures}`);
|
|
1566
1791
|
}
|
|
1567
|
-
|
|
1568
|
-
return urlsPointToSameCaptureState(left, right);
|
|
1569
|
-
}
|
|
1570
|
-
async function loadRunHints(supabase, presetId) {
|
|
1571
|
-
try {
|
|
1572
|
-
const { data } = await supabase
|
|
1573
|
-
.from('agent_errors')
|
|
1574
|
-
.select('error_type, message, user_response, created_at')
|
|
1575
|
-
.eq('preset_id', presetId)
|
|
1576
|
-
.order('created_at', { ascending: false })
|
|
1577
|
-
.limit(10);
|
|
1578
|
-
const hints = buildAgentRunHints((data ?? []));
|
|
1579
|
-
return hints.length > 0 ? hints : undefined;
|
|
1580
|
-
}
|
|
1581
|
-
catch {
|
|
1582
|
-
return undefined;
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1585
|
-
function buildPersistableProfile(previousProfile, storageState, sessionStorage, currentUrl, lang, theme) {
|
|
1586
|
-
return {
|
|
1587
|
-
storageState,
|
|
1588
|
-
sessionStorage,
|
|
1589
|
-
authState: previousProfile?.authState ?? inferAuthState(storageState, sessionStorage),
|
|
1590
|
-
accountLabel: previousProfile?.accountLabel ?? null,
|
|
1591
|
-
detectedLang: lang,
|
|
1592
|
-
detectedTheme: theme,
|
|
1593
|
-
validatedStartUrl: previousProfile?.validatedStartUrl ?? currentUrl,
|
|
1594
|
-
lastKnownUrl: currentUrl,
|
|
1595
|
-
summary: previousProfile?.summary ?? 'Validated browser state captured from a successful WS-driven run.',
|
|
1596
|
-
validationStatus: previousProfile?.validationStatus ?? 'unknown',
|
|
1597
|
-
lastUsedAt: new Date().toISOString(),
|
|
1598
|
-
profileVersion: previousProfile?.profileVersion ?? 1,
|
|
1599
|
-
};
|
|
1600
|
-
}
|
|
1601
|
-
function inferAuthState(storageState, sessionStorage) {
|
|
1602
|
-
const structuredStorageState = storageState && typeof storageState === 'object' && !Array.isArray(storageState)
|
|
1603
|
-
? storageState
|
|
1604
|
-
: undefined;
|
|
1605
|
-
const cookieCount = structuredStorageState?.cookies?.length ?? 0;
|
|
1606
|
-
const originCount = structuredStorageState?.origins?.length ?? 0;
|
|
1607
|
-
const sessionOriginCount = sessionStorage ? Object.keys(sessionStorage).length : 0;
|
|
1608
|
-
return cookieCount > 0 || originCount > 0 || sessionOriginCount > 0
|
|
1609
|
-
? 'authenticated'
|
|
1610
|
-
: 'unknown';
|
|
1611
|
-
}
|
|
1612
|
-
function buildPreverifiedAgentResult(assessment, verification) {
|
|
1613
|
-
return {
|
|
1614
|
-
success: true,
|
|
1615
|
-
screenshotPath: null,
|
|
1616
|
-
screenshots: [],
|
|
1617
|
-
iterations: 0,
|
|
1618
|
-
actions: [],
|
|
1619
|
-
assessment,
|
|
1620
|
-
usage: verification.usage ? [verification.usage] : [],
|
|
1621
|
-
runtimeStrategy: 'preverified_handoff',
|
|
1622
|
-
deterministicRecoveryUsed: false,
|
|
1623
|
-
evaluatorUsed: false,
|
|
1624
|
-
verification,
|
|
1625
|
-
};
|
|
1626
|
-
}
|
|
1627
|
-
function makeFailedAgentResult(message) {
|
|
1628
|
-
return {
|
|
1629
|
-
success: false,
|
|
1630
|
-
screenshotPath: null,
|
|
1631
|
-
assessment: message,
|
|
1632
|
-
iterations: 0,
|
|
1633
|
-
actions: [],
|
|
1634
|
-
screenshots: [],
|
|
1635
|
-
usage: [],
|
|
1636
|
-
};
|
|
1637
|
-
}
|
|
1638
|
-
async function buildHandoffContext(params) {
|
|
1639
|
-
const handoffArtifacts = buildSequentialHandoffArtifacts(params.actions ?? []);
|
|
1640
|
-
return {
|
|
1641
|
-
previousPageId: params.pageRun.pageId,
|
|
1642
|
-
previousPrompt: params.pageRun.prompt,
|
|
1643
|
-
currentUrl: params.browser.currentPage.url() || params.pageRun.url,
|
|
1644
|
-
pageTitle: await params.browser.currentPage.title().catch(() => null),
|
|
1645
|
-
authState: params.profile.authState,
|
|
1646
|
-
accountLabel: params.profile.accountLabel ?? null,
|
|
1647
|
-
currentLang: params.lang,
|
|
1648
|
-
currentTheme: params.theme,
|
|
1649
|
-
summary: `Continue from the validated live state reached for ${params.pageRun.pageId ?? 'main'}.`,
|
|
1650
|
-
selectorHints: handoffArtifacts.selectorHints,
|
|
1651
|
-
navigationHints: [],
|
|
1652
|
-
selectorMemory: mergeSelectorMemory(params.selectorMemory, handoffArtifacts.selectorMemory),
|
|
1653
|
-
};
|
|
1654
|
-
}
|
|
1655
|
-
async function restorePreparedPageState(browser, viewport, preparedPageUrl) {
|
|
1656
|
-
await browser.resizeViewport(viewport.width, viewport.height);
|
|
1657
|
-
if (!urlsRoughlyMatch(browser.currentPage.url(), preparedPageUrl)) {
|
|
1658
|
-
await browser.navigateTo(preparedPageUrl);
|
|
1659
|
-
}
|
|
1660
|
-
await browser.dismissOverlays().catch(() => undefined);
|
|
1661
|
-
await browser.wait(250);
|
|
1662
|
-
}
|
|
1792
|
+
// Standalone utilities have been extracted to ws-handler-utils.ts
|
|
1663
1793
|
//# sourceMappingURL=ws-handler.js.map
|