autokap 1.0.6 → 1.0.7
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/chrome/ios-statusbar-comparison-reference.jpg +0 -0
- package/assets/chrome/ios-statusbar-dark-reference.jpg +0 -0
- package/assets/chrome/ios-statusbar-light-reference.jpg +0 -0
- package/assets/devices/ipad-pro-11-m4.json +52 -0
- package/assets/devices/iphone-16-pro.json +53 -0
- package/assets/devices/macbook-air-13.json +45 -0
- package/assets/frames/MacBook Air 13.svg +242 -0
- package/assets/frames/Status bar - iPhone.png +0 -0
- Menu bar- iPad.png +0 -0
- package/assets/frames/iPad Pro M4 11_.png +0 -0
- package/assets/frames/iPhone 16 Pro.png +0 -0
- package/assets/icons/Cellular Connection.svg +3 -0
- package/assets/icons/Union.svg +6 -0
- package/assets/icons/Wifi.svg +3 -0
- package/assets/icons/battery.svg +5 -0
- package/assets/icons/battery_charging.svg +8 -0
- package/dist/abort.d.ts +5 -0
- package/dist/abort.js +44 -0
- package/dist/agent.d.ts +142 -0
- package/dist/agent.js +4511 -0
- package/dist/billing-operation-logging.d.ts +38 -0
- package/dist/billing-operation-logging.js +248 -0
- package/dist/browser-bar.d.ts +40 -0
- package/dist/browser-bar.js +147 -0
- package/dist/browser.d.ts +25 -0
- package/dist/browser.js +177 -9
- package/dist/capture-alt-text.d.ts +12 -0
- package/dist/capture-alt-text.js +51 -0
- package/dist/capture-encryption.d.ts +10 -0
- package/dist/capture-encryption.js +41 -0
- package/dist/capture-language-preflight.d.ts +41 -0
- package/dist/capture-language-preflight.js +286 -0
- package/dist/capture-llm-page-identity.d.ts +15 -0
- package/dist/capture-llm-page-identity.js +116 -0
- package/dist/capture-model-resolution.d.ts +9 -0
- package/dist/capture-model-resolution.js +21 -0
- package/dist/capture-page-identity.d.ts +9 -0
- package/dist/capture-page-identity.js +219 -0
- package/dist/capture-preset-credentials.d.ts +12 -0
- package/dist/capture-preset-credentials.js +57 -0
- package/dist/capture-request-plan.d.ts +58 -0
- package/dist/capture-request-plan.js +216 -0
- package/dist/capture-run-optimizer.d.ts +139 -0
- package/dist/capture-run-optimizer.js +848 -0
- package/dist/capture-selector-memory.d.ts +26 -0
- package/dist/capture-selector-memory.js +327 -0
- package/dist/capture-session-profile-encryption.d.ts +2 -0
- package/dist/capture-session-profile-encryption.js +22 -0
- package/dist/capture-step-timeout.d.ts +10 -0
- package/dist/capture-step-timeout.js +30 -0
- package/dist/capture-studio-sync.d.ts +22 -0
- package/dist/capture-studio-sync.js +166 -0
- package/dist/capture-variant-state.d.ts +54 -0
- package/dist/capture-variant-state.js +156 -0
- package/dist/cli.js +15 -0
- package/dist/clip-orchestrator.d.ts +148 -0
- package/dist/clip-orchestrator.js +950 -0
- package/dist/clip-postprocess.d.ts +42 -0
- package/dist/clip-postprocess.js +192 -0
- package/dist/cost-logging.d.ts +27 -0
- package/dist/cost-logging.js +128 -0
- package/dist/credential-templates.d.ts +5 -0
- package/dist/credential-templates.js +60 -0
- package/dist/element-capture.d.ts +53 -0
- package/dist/element-capture.js +766 -0
- package/dist/hybrid-navigator.d.ts +138 -0
- package/dist/hybrid-navigator.js +468 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +11 -0
- package/dist/llm-usage.d.ts +17 -0
- package/dist/llm-usage.js +45 -0
- package/dist/mockup-html.d.ts +119 -0
- package/dist/mockup-html.js +253 -0
- package/dist/mockup.d.ts +94 -0
- package/dist/mockup.js +608 -0
- package/dist/mouse-animation.d.ts +46 -0
- package/dist/mouse-animation.js +100 -0
- package/dist/overlay-utils.d.ts +14 -0
- package/dist/overlay-utils.js +13 -0
- package/dist/posthog.d.ts +4 -0
- package/dist/posthog.js +26 -0
- package/dist/prompt-cache.d.ts +10 -0
- package/dist/prompt-cache.js +24 -0
- package/dist/prompts.d.ts +167 -0
- package/dist/prompts.js +1165 -0
- package/dist/remote-browser.d.ts +191 -0
- package/dist/remote-browser.js +305 -0
- package/dist/security.d.ts +20 -0
- package/dist/security.js +569 -0
- package/dist/server-capture-runtime.d.ts +123 -0
- package/dist/server-capture-runtime.js +638 -0
- package/dist/server-credit-usage.d.ts +12 -0
- package/dist/server-credit-usage.js +41 -0
- package/dist/server-posthog.d.ts +2 -0
- package/dist/server-posthog.js +16 -0
- package/dist/server-project-webhooks.d.ts +45 -0
- package/dist/server-project-webhooks.js +97 -0
- package/dist/server-screenshot-watermark.d.ts +7 -0
- package/dist/server-screenshot-watermark.js +38 -0
- package/dist/session-profile.d.ts +86 -0
- package/dist/session-profile.js +1373 -0
- package/dist/sf-pro-fonts.d.ts +4 -0
- package/dist/sf-pro-fonts.js +7 -0
- package/dist/status-bar-l10n.d.ts +14 -0
- package/dist/status-bar-l10n.js +177 -0
- package/dist/status-bar.d.ts +44 -0
- package/dist/status-bar.js +336 -0
- package/dist/tools.d.ts +4 -0
- package/dist/tools.js +578 -0
- package/dist/video-agent.d.ts +143 -0
- package/dist/video-agent.js +4783 -0
- package/dist/video-observation.d.ts +36 -0
- package/dist/video-observation.js +192 -0
- package/dist/video-planner.d.ts +12 -0
- package/dist/video-planner.js +500 -0
- package/dist/video-prompts.d.ts +37 -0
- package/dist/video-prompts.js +554 -0
- package/dist/video-tools.d.ts +3 -0
- package/dist/video-tools.js +59 -0
- package/dist/video-variant-state.d.ts +29 -0
- package/dist/video-variant-state.js +80 -0
- package/dist/vision-model.d.ts +17 -0
- package/dist/vision-model.js +74 -0
- package/dist/ws-auth.d.ts +20 -0
- package/dist/ws-auth.js +67 -0
- package/dist/ws-handler.d.ts +10 -0
- package/dist/ws-handler.js +1663 -0
- package/dist/ws-server.d.ts +9 -0
- package/dist/ws-server.js +52 -0
- package/package.json +93 -39
|
@@ -0,0 +1,1663 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WS handler — orchestrates a capture run for a single CLI connection.
|
|
3
|
+
*
|
|
4
|
+
* The CLI stays a thin RPC adapter around a local Playwright browser.
|
|
5
|
+
* The orchestration stays server-side and reuses shared capture helpers
|
|
6
|
+
* so the WS flow does not drift from the HTTP capture flow.
|
|
7
|
+
*/
|
|
8
|
+
import crypto from 'node:crypto';
|
|
9
|
+
import { chromium } from 'playwright';
|
|
10
|
+
import { replayAgent, runAgent, verifyCaptureReadiness } from './agent.js';
|
|
11
|
+
import { buildAgentRunHints } from './session-profile.js';
|
|
12
|
+
import { captureIsolatedElement } from './element-capture.js';
|
|
13
|
+
import { getCapturePromptValidationError, getCaptureUrlHostname, resolveCaptureRequestPlan, sanitizeStorageSegment, urlsPointToSameCaptureState, } from './capture-request-plan.js';
|
|
14
|
+
import { insertCostLogs, resolveAllCosts, updateCostLogCaptureContext, } from './cost-logging.js';
|
|
15
|
+
import { insertScreenshotOperationLog, reconcilePendingBillingOperationCosts, } from './billing-operation-logging.js';
|
|
16
|
+
import { generateAltText } from './capture-alt-text.js';
|
|
17
|
+
import { ensureScreenshotVariantLanguage } from './capture-language-preflight.js';
|
|
18
|
+
import { inferPageIdentitiesWithLLM } from './capture-llm-page-identity.js';
|
|
19
|
+
import { resolveRunModels } from './capture-model-resolution.js';
|
|
20
|
+
import { extractSelectorUpdates, loadScreenshotSelectorMemory, persistScreenshotSelectorMemoryUpdates, } from './capture-selector-memory.js';
|
|
21
|
+
import { hydratePresetConfigFromStorage } from './capture-preset-credentials.js';
|
|
22
|
+
import { applySelectorMemoryUpdates, buildSequentialHandoffArtifacts, buildSessionBootstrapProfile, deriveRunSharedAuthProfile, getUrlOrigin, mergeSelectorMemory, resolveIsolatedElementAssignments, resolveScopedSelectorMemory, scopeSelectorMemoryUpdates, shouldReuseLivePageState, shouldUseSequentialPageHandoff, urlMatchesCaptureTarget, } from './capture-run-optimizer.js';
|
|
23
|
+
import { decryptSessionField, encryptSessionField, } from './capture-session-profile-encryption.js';
|
|
24
|
+
import { buildVariantManifestContext, createVariantCaptureState, markVariantCaptureBlocked, markVariantCaptureInProgress, recordValidatedVariantCapture, validateVariantCaptureState, } from './capture-variant-state.js';
|
|
25
|
+
import { syncStudioVariantAfterCapture } from './capture-studio-sync.js';
|
|
26
|
+
import { applyDeviceFrame, invalidateDeviceConfigCache } from './mockup.js';
|
|
27
|
+
import { localizeStatusBar } from './status-bar-l10n.js';
|
|
28
|
+
import { logger } from './logger.js';
|
|
29
|
+
import { getRemoteBrowserCompatibilityError, RemoteBrowser, } from './remote-browser.js';
|
|
30
|
+
import { recordCreditUsage } from './server-credit-usage.js';
|
|
31
|
+
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
|
+
import { buildCaptureWebhookPayload, dispatchProjectCaptureWebhook, getProjectWebhookConfig, } from './server-project-webhooks.js';
|
|
33
|
+
import { getServerPostHog } from './server-posthog.js';
|
|
34
|
+
import { applyPlanScreenshotWatermark } from './server-screenshot-watermark.js';
|
|
35
|
+
import { validateApiKey, requireScope, getSupabase } from './ws-auth.js';
|
|
36
|
+
const BUCKET = 'screenshots';
|
|
37
|
+
function getOpenRouterKey() {
|
|
38
|
+
const key = process.env.OPENROUTER_API_KEY ?? '';
|
|
39
|
+
if (!key)
|
|
40
|
+
logger.error('OPENROUTER_API_KEY is not set');
|
|
41
|
+
return key;
|
|
42
|
+
}
|
|
43
|
+
async function waitForClientHello(ws, timeoutMs = 3_000) {
|
|
44
|
+
return await new Promise((resolve) => {
|
|
45
|
+
let settled = false;
|
|
46
|
+
let timer = setTimeout(() => {
|
|
47
|
+
settled = true;
|
|
48
|
+
cleanup();
|
|
49
|
+
resolve(null);
|
|
50
|
+
}, timeoutMs);
|
|
51
|
+
const cleanup = () => {
|
|
52
|
+
if (timer) {
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
timer = null;
|
|
55
|
+
}
|
|
56
|
+
ws.off('message', onMessage);
|
|
57
|
+
ws.off('close', onClose);
|
|
58
|
+
ws.off('error', onClose);
|
|
59
|
+
};
|
|
60
|
+
const onClose = () => {
|
|
61
|
+
if (settled)
|
|
62
|
+
return;
|
|
63
|
+
settled = true;
|
|
64
|
+
cleanup();
|
|
65
|
+
resolve(null);
|
|
66
|
+
};
|
|
67
|
+
const onMessage = (data) => {
|
|
68
|
+
if (settled)
|
|
69
|
+
return;
|
|
70
|
+
try {
|
|
71
|
+
const raw = typeof data === 'string' ? data : data.toString('utf-8');
|
|
72
|
+
const parsed = JSON.parse(raw);
|
|
73
|
+
if (parsed.type !== 'client_hello' || !parsed.client)
|
|
74
|
+
return;
|
|
75
|
+
settled = true;
|
|
76
|
+
cleanup();
|
|
77
|
+
resolve(parsed.client);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Ignore non-JSON or non-hello messages until timeout.
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
ws.on('message', onMessage);
|
|
84
|
+
ws.on('close', onClose);
|
|
85
|
+
ws.on('error', onClose);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
export async function handleConnection(ws, req) {
|
|
89
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
|
|
90
|
+
const rawKey = url.searchParams.get('key') ?? '';
|
|
91
|
+
const presetId = url.searchParams.get('preset_id') ?? '';
|
|
92
|
+
const clientHelloPromise = waitForClientHello(ws);
|
|
93
|
+
const sendEvent = (type, data) => {
|
|
94
|
+
if (ws.readyState === ws.OPEN) {
|
|
95
|
+
ws.send(JSON.stringify({ type, ...data }));
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const auth = await validateApiKey(rawKey);
|
|
99
|
+
if (!auth) {
|
|
100
|
+
sendEvent('error', { message: 'Invalid API key' });
|
|
101
|
+
ws.close(4001, 'Invalid API key');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
requireScope(auth.scopes, 'presets:execute');
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
sendEvent('error', { message: 'Missing scope: presets:execute' });
|
|
109
|
+
ws.close(4003, 'Forbidden');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (!presetId) {
|
|
113
|
+
sendEvent('error', { message: 'Missing preset_id parameter' });
|
|
114
|
+
ws.close(4000, 'Missing preset_id');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const supabase = getSupabase();
|
|
118
|
+
const { data: preset, error: presetError } = await supabase
|
|
119
|
+
.from('presets')
|
|
120
|
+
.select('id, project_id, name, config')
|
|
121
|
+
.eq('id', presetId)
|
|
122
|
+
.single();
|
|
123
|
+
if (presetError || !preset) {
|
|
124
|
+
sendEvent('error', { message: `Preset not found: ${presetId}` });
|
|
125
|
+
ws.close(4004, 'Preset not found');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const { data: project, error: projectError } = await supabase
|
|
129
|
+
.from('projects')
|
|
130
|
+
.select('id, url, name, user_id')
|
|
131
|
+
.eq('id', preset.project_id)
|
|
132
|
+
.single();
|
|
133
|
+
if (projectError || !project) {
|
|
134
|
+
sendEvent('error', { message: 'Project not found' });
|
|
135
|
+
ws.close(4004, 'Project not found');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (project.user_id !== auth.userId) {
|
|
139
|
+
sendEvent('error', { message: 'Not authorized to run this preset' });
|
|
140
|
+
ws.close(4003, 'Forbidden');
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const clientInfo = await clientHelloPromise;
|
|
144
|
+
const compatibilityError = getRemoteBrowserCompatibilityError(clientInfo);
|
|
145
|
+
if (compatibilityError) {
|
|
146
|
+
sendEvent('error', { message: compatibilityError });
|
|
147
|
+
ws.close(4000, 'CLI update required');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const config = hydratePresetConfigFromStorage(preset.config);
|
|
151
|
+
const projectUrl = String(project.url ?? '');
|
|
152
|
+
const planInput = {
|
|
153
|
+
...config,
|
|
154
|
+
url: projectUrl,
|
|
155
|
+
prompt: String(config.prompt ?? ''),
|
|
156
|
+
};
|
|
157
|
+
const promptValidationError = getCapturePromptValidationError(planInput);
|
|
158
|
+
if (promptValidationError) {
|
|
159
|
+
sendEvent('error', { message: promptValidationError });
|
|
160
|
+
ws.close(4000, 'Invalid capture prompt config');
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
invalidateDeviceConfigCache();
|
|
164
|
+
const { pageRuns: pages, pageDefinitionIssues, langs, themes, variantPlan, targets, elements, outputScale, totalCaptures, } = await resolveCaptureRequestPlan(planInput);
|
|
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
|
+
}
|
|
172
|
+
sendEvent('progress', { message: `Loaded preset "${preset.name}" for ${projectUrl}` });
|
|
173
|
+
let resolvedModel = '';
|
|
174
|
+
let resolvedFallback;
|
|
175
|
+
let resolvedVisionModel;
|
|
176
|
+
const providerPreferences = {};
|
|
177
|
+
try {
|
|
178
|
+
const { data: cfgRows } = await supabase
|
|
179
|
+
.from('app_config')
|
|
180
|
+
.select('key, value')
|
|
181
|
+
.in('key', [
|
|
182
|
+
'default_model', 'fallback_model', 'vision_model',
|
|
183
|
+
'default_model_provider', 'fallback_model_provider', 'vision_model_provider',
|
|
184
|
+
]);
|
|
185
|
+
const cfg = {};
|
|
186
|
+
for (const row of cfgRows ?? [])
|
|
187
|
+
cfg[row.key] = row.value;
|
|
188
|
+
const resolved = resolveRunModels({
|
|
189
|
+
requestModel: config.model ?? null,
|
|
190
|
+
defaultModel: cfg.default_model,
|
|
191
|
+
fallbackModel: cfg.fallback_model,
|
|
192
|
+
presetId,
|
|
193
|
+
});
|
|
194
|
+
resolvedModel = resolved.model;
|
|
195
|
+
resolvedFallback = resolved.fallbackModel;
|
|
196
|
+
resolvedVisionModel = cfg.vision_model?.trim() || undefined;
|
|
197
|
+
for (const [cfgKey, modelVal] of [
|
|
198
|
+
['default_model_provider', resolvedModel],
|
|
199
|
+
['fallback_model_provider', resolvedFallback],
|
|
200
|
+
['vision_model_provider', resolvedVisionModel],
|
|
201
|
+
]) {
|
|
202
|
+
try {
|
|
203
|
+
const raw = cfg[cfgKey];
|
|
204
|
+
if (raw && modelVal)
|
|
205
|
+
providerPreferences[modelVal] = JSON.parse(raw);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// Ignore invalid provider preference JSON.
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// Non-blocking: the app_config table may be missing during bootstrap.
|
|
214
|
+
}
|
|
215
|
+
if (!resolvedModel) {
|
|
216
|
+
sendEvent('error', { message: 'No model configured. Set a default model in the admin dashboard.' });
|
|
217
|
+
ws.close(5000, 'No model');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
let plan = getBillingPlan('free');
|
|
221
|
+
let billingOwnerUserId = auth.userId;
|
|
222
|
+
let billingAccount = null;
|
|
223
|
+
let allowOverage = false;
|
|
224
|
+
let usedCreditsBefore = 0;
|
|
225
|
+
let remainingOverageCreditsBefore = null;
|
|
226
|
+
try {
|
|
227
|
+
const billingContext = await getProjectOwnerBillingContext(supabase, project.id);
|
|
228
|
+
plan = billingContext.plan;
|
|
229
|
+
billingOwnerUserId = billingContext.ownerUserId;
|
|
230
|
+
const lockState = await getLockedResourceIds(supabase, billingOwnerUserId, plan);
|
|
231
|
+
ensureResourceNotLocked(project.id, lockState.lockedProjectIds, 'project');
|
|
232
|
+
const lockedPresets = lockState.lockedPresetIdsByProject.get(project.id);
|
|
233
|
+
if (lockedPresets) {
|
|
234
|
+
ensureResourceNotLocked(presetId, lockedPresets, 'preset');
|
|
235
|
+
}
|
|
236
|
+
ensureCaptureConfigAllowed(plan, {
|
|
237
|
+
langs,
|
|
238
|
+
themes: [...themes],
|
|
239
|
+
elements,
|
|
240
|
+
});
|
|
241
|
+
const usage = await getOwnerBillingUsage(supabase, billingOwnerUserId);
|
|
242
|
+
usedCreditsBefore = usage.credits;
|
|
243
|
+
billingAccount = await getBillingAccountForUser(supabase, billingOwnerUserId);
|
|
244
|
+
const hasActiveStripeSub = !!billingAccount?.stripe_subscription_id
|
|
245
|
+
&& shouldUseStripePlan(billingAccount.stripe_subscription_status);
|
|
246
|
+
const overageLimitCents = billingAccount?.capture_overage_limit_cents ?? null;
|
|
247
|
+
const overageState = getCaptureOverageState({
|
|
248
|
+
planId: plan.id,
|
|
249
|
+
quota: plan.entitlements.creditsPerMonth,
|
|
250
|
+
usedCredits: usage.credits,
|
|
251
|
+
allowOverages: billingAccount?.allow_capture_overages ?? false,
|
|
252
|
+
overageLimitCents,
|
|
253
|
+
hasActiveSubscription: hasActiveStripeSub,
|
|
254
|
+
hasOveragePrice: !!getStripeOveragePriceIdForPlan(plan.id),
|
|
255
|
+
isYearlySubscription: isYearlySubscription(billingAccount),
|
|
256
|
+
});
|
|
257
|
+
allowOverage = overageState.eligible;
|
|
258
|
+
remainingOverageCreditsBefore = getRemainingOverageCredits({
|
|
259
|
+
planId: plan.id,
|
|
260
|
+
quota: plan.entitlements.creditsPerMonth,
|
|
261
|
+
usedCredits: usage.credits,
|
|
262
|
+
overageLimitCents,
|
|
263
|
+
});
|
|
264
|
+
const requestedCredits = totalCaptures * SCREENSHOT_CREDIT_COST;
|
|
265
|
+
const bonusCredits = getSignupBonusCredits(billingAccount, usage.billingPeriodStart);
|
|
266
|
+
ensureMonthlyCreditsQuota(plan, usage.credits, requestedCredits, allowOverage, overageLimitCents, bonusCredits);
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
270
|
+
sendEvent('error', {
|
|
271
|
+
message,
|
|
272
|
+
...(error instanceof PlanLimitError ? { code: error.code } : {}),
|
|
273
|
+
});
|
|
274
|
+
ws.close(error instanceof PlanLimitError ? 4003 : 5000, 'Capture unavailable');
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const runHints = await loadRunHints(supabase, presetId);
|
|
278
|
+
const runId = crypto.randomUUID();
|
|
279
|
+
const runStartedAt = new Date().toISOString();
|
|
280
|
+
const credentials = config.credentials ?? undefined;
|
|
281
|
+
const maxIterations = Number(config.maxIterations ?? 60) || 60;
|
|
282
|
+
const { error: runInsertError } = await supabase
|
|
283
|
+
.from('capture_runs')
|
|
284
|
+
.insert({
|
|
285
|
+
id: runId,
|
|
286
|
+
user_id: auth.userId,
|
|
287
|
+
api_key_id: auth.apiKeyId,
|
|
288
|
+
preset_id: presetId,
|
|
289
|
+
project_id: project.id,
|
|
290
|
+
status: 'running',
|
|
291
|
+
progress_current: 0,
|
|
292
|
+
progress_total: totalCaptures,
|
|
293
|
+
captures: [],
|
|
294
|
+
credits_used: 0,
|
|
295
|
+
started_at: runStartedAt,
|
|
296
|
+
});
|
|
297
|
+
if (runInsertError) {
|
|
298
|
+
sendEvent('error', { message: `Failed to create run: ${runInsertError.message}` });
|
|
299
|
+
ws.close(5000, 'Internal error');
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const remoteBrowser = new RemoteBrowser(ws);
|
|
303
|
+
const abortController = new AbortController();
|
|
304
|
+
let aborted = false;
|
|
305
|
+
ws.on('close', () => {
|
|
306
|
+
aborted = true;
|
|
307
|
+
abortController.abort();
|
|
308
|
+
});
|
|
309
|
+
const mockupState = {
|
|
310
|
+
browser: null,
|
|
311
|
+
context: null,
|
|
312
|
+
};
|
|
313
|
+
const getMockupContext = async () => {
|
|
314
|
+
if (!mockupState.context) {
|
|
315
|
+
mockupState.browser = await chromium.launch({ headless: true });
|
|
316
|
+
mockupState.context = await mockupState.browser.newContext();
|
|
317
|
+
}
|
|
318
|
+
return mockupState.context;
|
|
319
|
+
};
|
|
320
|
+
const closeMockup = async () => {
|
|
321
|
+
try {
|
|
322
|
+
await mockupState.context?.close();
|
|
323
|
+
}
|
|
324
|
+
catch { /* ignore */ }
|
|
325
|
+
try {
|
|
326
|
+
await mockupState.browser?.close();
|
|
327
|
+
}
|
|
328
|
+
catch { /* ignore */ }
|
|
329
|
+
};
|
|
330
|
+
const selectorMemoryCache = new Map();
|
|
331
|
+
const sessionProfileCache = new Map();
|
|
332
|
+
const liveVariantActions = new Map();
|
|
333
|
+
const liveVariantReference = new Map();
|
|
334
|
+
const runSharedAuthProfiles = new Map();
|
|
335
|
+
const captureResults = [];
|
|
336
|
+
let runSharedStorageState;
|
|
337
|
+
let runSharedSessionStorage;
|
|
338
|
+
let successCount = 0;
|
|
339
|
+
let captureIndex = 0;
|
|
340
|
+
const rootOrigin = getUrlOrigin(projectUrl);
|
|
341
|
+
const updateRunProgress = async () => {
|
|
342
|
+
await supabase
|
|
343
|
+
.from('capture_runs')
|
|
344
|
+
.update({
|
|
345
|
+
progress_current: captureIndex,
|
|
346
|
+
credits_used: successCount,
|
|
347
|
+
})
|
|
348
|
+
.eq('id', runId);
|
|
349
|
+
};
|
|
350
|
+
const finalizeRunSideEffects = async () => {
|
|
351
|
+
try {
|
|
352
|
+
if (allowOverage && billingAccount?.stripe_customer_id && successCount > 0) {
|
|
353
|
+
const rawOverageCount = getIncrementalOverageCount({
|
|
354
|
+
quota: plan.entitlements.creditsPerMonth,
|
|
355
|
+
usedBefore: usedCreditsBefore,
|
|
356
|
+
completedCount: successCount * SCREENSHOT_CREDIT_COST,
|
|
357
|
+
});
|
|
358
|
+
const overageCount = remainingOverageCreditsBefore === null
|
|
359
|
+
? rawOverageCount
|
|
360
|
+
: Math.min(rawOverageCount, remainingOverageCreditsBefore);
|
|
361
|
+
if (overageCount > 0) {
|
|
362
|
+
await recordStripeMeterEvent({
|
|
363
|
+
customerId: billingAccount.stripe_customer_id,
|
|
364
|
+
value: overageCount,
|
|
365
|
+
identifier: runId,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
// Non-blocking.
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
await cleanupExpiredCapturesForOwner(supabase, billingOwnerUserId);
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
// Non-blocking.
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
const logAgentUsage = async (params) => {
|
|
381
|
+
if (params.usage.length === 0) {
|
|
382
|
+
return [];
|
|
383
|
+
}
|
|
384
|
+
try {
|
|
385
|
+
const ids = await insertCostLogs(supabase, {
|
|
386
|
+
runId,
|
|
387
|
+
userId: auth.userId,
|
|
388
|
+
projectId: project.id,
|
|
389
|
+
presetId,
|
|
390
|
+
lang: params.lang,
|
|
391
|
+
theme: params.theme,
|
|
392
|
+
captureType: params.captureType,
|
|
393
|
+
elementName: params.elementName,
|
|
394
|
+
targetId: params.targetId,
|
|
395
|
+
viewportWidth: params.viewportWidth,
|
|
396
|
+
viewportHeight: params.viewportHeight,
|
|
397
|
+
}, params.usage);
|
|
398
|
+
if (ids.length > 0) {
|
|
399
|
+
void resolveAllCosts(supabase, ids.map((id, index) => ({
|
|
400
|
+
costLogId: id,
|
|
401
|
+
generationId: params.usage[index]?.generationId ?? null,
|
|
402
|
+
})), getOpenRouterKey(), supabase).catch((error) => {
|
|
403
|
+
logger.error(`Cost resolution failed: ${error.message}`);
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
return ids;
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
logger.error(`Failed to log agent usage: ${error.message}`);
|
|
410
|
+
return [];
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
const loadSelectorMemoryForVariant = async (domain, lang, theme) => {
|
|
414
|
+
const cacheKey = `${domain}:${lang}:${theme}`;
|
|
415
|
+
if (selectorMemoryCache.has(cacheKey)) {
|
|
416
|
+
return selectorMemoryCache.get(cacheKey) ?? {};
|
|
417
|
+
}
|
|
418
|
+
const memory = await loadScreenshotSelectorMemory(supabase, {
|
|
419
|
+
projectId: project.id,
|
|
420
|
+
presetId,
|
|
421
|
+
domain,
|
|
422
|
+
lang,
|
|
423
|
+
theme,
|
|
424
|
+
}).catch(() => ({}));
|
|
425
|
+
selectorMemoryCache.set(cacheKey, memory);
|
|
426
|
+
return memory;
|
|
427
|
+
};
|
|
428
|
+
const loadSessionProfileForVariant = async (domain, lang, theme) => {
|
|
429
|
+
const cacheKey = `${domain}:${lang}:${theme}`;
|
|
430
|
+
if (sessionProfileCache.has(cacheKey)) {
|
|
431
|
+
return sessionProfileCache.get(cacheKey);
|
|
432
|
+
}
|
|
433
|
+
try {
|
|
434
|
+
const { data } = await supabase
|
|
435
|
+
.from('screenshot_session_profiles')
|
|
436
|
+
.select('storage_state, session_storage, auth_state, account_label, detected_lang, detected_theme, validated_start_url, last_known_url, summary, validation_status, last_used_at, profile_version')
|
|
437
|
+
.eq('project_id', project.id)
|
|
438
|
+
.eq('preset_id', presetId)
|
|
439
|
+
.eq('domain', domain)
|
|
440
|
+
.eq('lang', lang)
|
|
441
|
+
.eq('theme', theme)
|
|
442
|
+
.gt('expires_at', new Date().toISOString())
|
|
443
|
+
.limit(1)
|
|
444
|
+
.single();
|
|
445
|
+
if (!data?.storage_state) {
|
|
446
|
+
sessionProfileCache.set(cacheKey, undefined);
|
|
447
|
+
return undefined;
|
|
448
|
+
}
|
|
449
|
+
const profile = {
|
|
450
|
+
storageState: decryptSessionField(data.storage_state),
|
|
451
|
+
sessionStorage: decryptSessionField(data.session_storage) ?? undefined,
|
|
452
|
+
authState: data.auth_state ?? 'unknown',
|
|
453
|
+
accountLabel: data.account_label ?? null,
|
|
454
|
+
detectedLang: data.detected_lang ?? null,
|
|
455
|
+
detectedTheme: data.detected_theme ?? null,
|
|
456
|
+
validatedStartUrl: data.validated_start_url ?? null,
|
|
457
|
+
lastKnownUrl: data.last_known_url ?? null,
|
|
458
|
+
summary: data.summary ?? null,
|
|
459
|
+
validationStatus: data.validation_status ?? 'unknown',
|
|
460
|
+
lastUsedAt: data.last_used_at ?? null,
|
|
461
|
+
profileVersion: data.profile_version ?? 1,
|
|
462
|
+
};
|
|
463
|
+
sessionProfileCache.set(cacheKey, profile);
|
|
464
|
+
return profile;
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
sessionProfileCache.set(cacheKey, undefined);
|
|
468
|
+
return undefined;
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
const persistSessionProfile = async (domain, lang, theme, profile) => {
|
|
472
|
+
if (!profile.storageState)
|
|
473
|
+
return;
|
|
474
|
+
const cacheKey = `${domain}:${lang}:${theme}`;
|
|
475
|
+
const storedProfile = {
|
|
476
|
+
...profile,
|
|
477
|
+
lastUsedAt: new Date().toISOString(),
|
|
478
|
+
};
|
|
479
|
+
try {
|
|
480
|
+
await supabase
|
|
481
|
+
.from('screenshot_session_profiles')
|
|
482
|
+
.upsert({
|
|
483
|
+
project_id: project.id,
|
|
484
|
+
preset_id: presetId,
|
|
485
|
+
domain,
|
|
486
|
+
lang,
|
|
487
|
+
theme,
|
|
488
|
+
storage_state: encryptSessionField(storedProfile.storageState),
|
|
489
|
+
session_storage: encryptSessionField(storedProfile.sessionStorage) ?? null,
|
|
490
|
+
auth_state: storedProfile.authState,
|
|
491
|
+
account_label: storedProfile.accountLabel ?? null,
|
|
492
|
+
detected_lang: storedProfile.detectedLang ?? null,
|
|
493
|
+
detected_theme: storedProfile.detectedTheme ?? null,
|
|
494
|
+
validated_start_url: storedProfile.validatedStartUrl ?? null,
|
|
495
|
+
last_known_url: storedProfile.lastKnownUrl ?? null,
|
|
496
|
+
summary: storedProfile.summary ?? null,
|
|
497
|
+
validation_status: storedProfile.validationStatus,
|
|
498
|
+
last_validated_at: new Date().toISOString(),
|
|
499
|
+
last_used_at: storedProfile.lastUsedAt,
|
|
500
|
+
profile_version: storedProfile.profileVersion ?? 1,
|
|
501
|
+
expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
502
|
+
}, {
|
|
503
|
+
onConflict: 'project_id,preset_id,domain,lang,theme',
|
|
504
|
+
});
|
|
505
|
+
sessionProfileCache.set(cacheKey, storedProfile);
|
|
506
|
+
}
|
|
507
|
+
catch (error) {
|
|
508
|
+
logger.error(`Session profile persistence failed for ${domain}/${lang}/${theme}: ${error.message}`);
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
const getRunSharedAuthProfile = (startUrl) => {
|
|
512
|
+
const startOrigin = getUrlOrigin(startUrl);
|
|
513
|
+
if (startOrigin && runSharedAuthProfiles.has(startOrigin)) {
|
|
514
|
+
return runSharedAuthProfiles.get(startOrigin);
|
|
515
|
+
}
|
|
516
|
+
if (!startOrigin && rootOrigin && runSharedAuthProfiles.has(rootOrigin)) {
|
|
517
|
+
return runSharedAuthProfiles.get(rootOrigin);
|
|
518
|
+
}
|
|
519
|
+
if (!startOrigin && runSharedAuthProfiles.size === 1) {
|
|
520
|
+
return Array.from(runSharedAuthProfiles.values())[0];
|
|
521
|
+
}
|
|
522
|
+
return undefined;
|
|
523
|
+
};
|
|
524
|
+
const rememberRunSharedAuthProfile = (profile, startUrl) => {
|
|
525
|
+
const sharedProfile = deriveRunSharedAuthProfile(profile);
|
|
526
|
+
if (!sharedProfile)
|
|
527
|
+
return;
|
|
528
|
+
const keys = new Set();
|
|
529
|
+
const startOrigin = getUrlOrigin(startUrl);
|
|
530
|
+
const profileOrigin = getUrlOrigin(profile?.lastKnownUrl ?? profile?.validatedStartUrl ?? null);
|
|
531
|
+
if (startOrigin)
|
|
532
|
+
keys.add(startOrigin);
|
|
533
|
+
if (profileOrigin)
|
|
534
|
+
keys.add(profileOrigin);
|
|
535
|
+
if (rootOrigin)
|
|
536
|
+
keys.add(rootOrigin);
|
|
537
|
+
for (const key of keys) {
|
|
538
|
+
runSharedAuthProfiles.set(key, sharedProfile);
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
const persistFullPageCapture = async (params) => {
|
|
542
|
+
const { pageRun, target, lang, theme, agentResult, includeActions, includeWorkflowScreenshots } = params;
|
|
543
|
+
const pageSegment = sanitizeStorageSegment(pageRun.pageId);
|
|
544
|
+
let finalBuffer = await remoteBrowser.takeScreenshot();
|
|
545
|
+
let rawScreenshotUrl = null;
|
|
546
|
+
let browserBar;
|
|
547
|
+
try {
|
|
548
|
+
await remoteBrowser.resizeViewport(target.viewport.width, target.viewport.height);
|
|
549
|
+
await remoteBrowser.wait(300);
|
|
550
|
+
finalBuffer = await remoteBrowser.takeScreenshot();
|
|
551
|
+
if (target.deviceFrame) {
|
|
552
|
+
const rawPath = `screenshots/${auth.userId}/${runId}/${pageSegment}/${target.id}_${lang}_${theme}_raw.png`;
|
|
553
|
+
const { error } = await supabase.storage
|
|
554
|
+
.from(BUCKET)
|
|
555
|
+
.upload(rawPath, finalBuffer, { contentType: 'image/png', upsert: true });
|
|
556
|
+
if (!error) {
|
|
557
|
+
rawScreenshotUrl = supabase.storage.from(BUCKET).getPublicUrl(rawPath).data.publicUrl;
|
|
558
|
+
}
|
|
559
|
+
try {
|
|
560
|
+
const pageUrl = remoteBrowser.currentPage.url();
|
|
561
|
+
const pageTitle = await remoteBrowser.currentPage.title();
|
|
562
|
+
const mockupOptions = target.mockupOptions ?? {};
|
|
563
|
+
browserBar = (mockupOptions.autoBrowserBar ?? true)
|
|
564
|
+
? {
|
|
565
|
+
url: pageUrl,
|
|
566
|
+
pageTitle,
|
|
567
|
+
tabIconUrl: pageUrl
|
|
568
|
+
? `https://www.google.com/s2/favicons?domain=${new URL(pageUrl).hostname}&sz=64`
|
|
569
|
+
: undefined,
|
|
570
|
+
}
|
|
571
|
+
: mockupOptions.browserBar;
|
|
572
|
+
const localizedOptions = {
|
|
573
|
+
...mockupOptions,
|
|
574
|
+
outputScale,
|
|
575
|
+
statusBar: localizeStatusBar(mockupOptions.statusBar, lang),
|
|
576
|
+
browserBar,
|
|
577
|
+
};
|
|
578
|
+
const mockupContext = await getMockupContext();
|
|
579
|
+
finalBuffer = await applyDeviceFrame(finalBuffer, target.deviceFrame, mockupContext, localizedOptions);
|
|
580
|
+
}
|
|
581
|
+
catch (error) {
|
|
582
|
+
logger.error(`Mockup failed for ${target.deviceFrame}: ${error.message}`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
finalBuffer = await applyPlanScreenshotWatermark(plan, finalBuffer);
|
|
586
|
+
const storagePath = `screenshots/${auth.userId}/${runId}/${pageSegment}/${target.id}_${lang}_${theme}.png`;
|
|
587
|
+
const { error: uploadError } = await supabase.storage
|
|
588
|
+
.from(BUCKET)
|
|
589
|
+
.upload(storagePath, finalBuffer, { contentType: 'image/png', upsert: true });
|
|
590
|
+
const screenshotUrl = uploadError
|
|
591
|
+
? null
|
|
592
|
+
: supabase.storage.from(BUCKET).getPublicUrl(storagePath).data.publicUrl;
|
|
593
|
+
const persistedSuccess = agentResult.success && !!screenshotUrl;
|
|
594
|
+
const persistedAssessment = persistedSuccess
|
|
595
|
+
? agentResult.assessment
|
|
596
|
+
: agentResult.success
|
|
597
|
+
? 'Capture completed but screenshot upload failed.'
|
|
598
|
+
: agentResult.assessment || 'Capture failed';
|
|
599
|
+
captureResults.push({
|
|
600
|
+
url: pageRun.url,
|
|
601
|
+
lang,
|
|
602
|
+
theme,
|
|
603
|
+
captureType: 'fullpage',
|
|
604
|
+
elementName: pageRun.pageId ?? undefined,
|
|
605
|
+
targetId: target.id,
|
|
606
|
+
targetLabel: target.label,
|
|
607
|
+
deviceFrame: target.deviceFrame,
|
|
608
|
+
success: persistedSuccess,
|
|
609
|
+
assessment: persistedAssessment,
|
|
610
|
+
iterations: agentResult.iterations,
|
|
611
|
+
finalScreenshotUrl: screenshotUrl ?? undefined,
|
|
612
|
+
});
|
|
613
|
+
const costLogIds = await logAgentUsage({
|
|
614
|
+
usage: agentResult.usage,
|
|
615
|
+
lang,
|
|
616
|
+
theme,
|
|
617
|
+
captureType: 'fullpage',
|
|
618
|
+
elementName: pageRun.pageId ?? undefined,
|
|
619
|
+
targetId: target.id,
|
|
620
|
+
viewportWidth: target.viewport.width,
|
|
621
|
+
viewportHeight: target.viewport.height,
|
|
622
|
+
});
|
|
623
|
+
const captureInsert = await supabase
|
|
624
|
+
.from('captures')
|
|
625
|
+
.insert({
|
|
626
|
+
run_id: runId,
|
|
627
|
+
preset_id: presetId,
|
|
628
|
+
project_id: project.id,
|
|
629
|
+
target_id: target.id,
|
|
630
|
+
element_name: pageRun.pageId ?? null,
|
|
631
|
+
url: pageRun.url,
|
|
632
|
+
prompt: pageRun.prompt,
|
|
633
|
+
lang,
|
|
634
|
+
theme,
|
|
635
|
+
success: persistedSuccess,
|
|
636
|
+
assessment: persistedAssessment,
|
|
637
|
+
iterations: agentResult.iterations,
|
|
638
|
+
screenshot_url: screenshotUrl,
|
|
639
|
+
raw_screenshot_url: rawScreenshotUrl,
|
|
640
|
+
device_frame: target.deviceFrame ?? null,
|
|
641
|
+
capture_type: 'fullpage',
|
|
642
|
+
credits: persistedSuccess ? SCREENSHOT_CREDIT_COST : 0,
|
|
643
|
+
config: {
|
|
644
|
+
targets,
|
|
645
|
+
pageId: pageRun.pageId,
|
|
646
|
+
model: resolvedModel,
|
|
647
|
+
outputScale,
|
|
648
|
+
actions: includeActions ? agentResult.actions : [],
|
|
649
|
+
workflowScreenshots: includeWorkflowScreenshots ? agentResult.screenshots.length : 0,
|
|
650
|
+
},
|
|
651
|
+
})
|
|
652
|
+
.select('id')
|
|
653
|
+
.single();
|
|
654
|
+
const captureId = captureInsert.data?.id ?? null;
|
|
655
|
+
if (captureInsert.error) {
|
|
656
|
+
logger.error(`Capture insert failed: ${captureInsert.error.message}`);
|
|
657
|
+
}
|
|
658
|
+
if (persistedSuccess && captureId) {
|
|
659
|
+
successCount += 1;
|
|
660
|
+
await recordCreditUsage(supabase, {
|
|
661
|
+
userId: auth.userId,
|
|
662
|
+
projectId: project.id,
|
|
663
|
+
presetId,
|
|
664
|
+
runId,
|
|
665
|
+
type: 'screenshot',
|
|
666
|
+
credits: SCREENSHOT_CREDIT_COST,
|
|
667
|
+
sourceId: captureId,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
if (captureId && costLogIds.length > 0) {
|
|
671
|
+
await updateCostLogCaptureContext(supabase, costLogIds, {
|
|
672
|
+
captureId,
|
|
673
|
+
targetId: target.id,
|
|
674
|
+
viewportWidth: target.viewport.width,
|
|
675
|
+
viewportHeight: target.viewport.height,
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
await insertScreenshotOperationLog(supabase, {
|
|
679
|
+
runId,
|
|
680
|
+
userId: auth.userId,
|
|
681
|
+
projectId: project.id,
|
|
682
|
+
presetId,
|
|
683
|
+
captureId,
|
|
684
|
+
captureType: 'fullpage',
|
|
685
|
+
lang,
|
|
686
|
+
theme,
|
|
687
|
+
elementName: pageRun.pageId ?? undefined,
|
|
688
|
+
targetId: target.id,
|
|
689
|
+
viewportWidth: target.viewport.width,
|
|
690
|
+
viewportHeight: target.viewport.height,
|
|
691
|
+
deviceFrame: target.deviceFrame ?? null,
|
|
692
|
+
}, {
|
|
693
|
+
outcome: persistedSuccess ? 'succeeded' : 'failed',
|
|
694
|
+
outcomeReason: persistedSuccess ? null : persistedAssessment,
|
|
695
|
+
billable: persistedSuccess && !!captureId,
|
|
696
|
+
creditsCharged: persistedSuccess && captureId ? SCREENSHOT_CREDIT_COST : 0,
|
|
697
|
+
costLogIds,
|
|
698
|
+
usage: agentResult.usage,
|
|
699
|
+
metadata: {
|
|
700
|
+
pageId: pageRun.pageId,
|
|
701
|
+
pageUrl: pageRun.url,
|
|
702
|
+
prompt: pageRun.prompt,
|
|
703
|
+
runtimeStrategy: agentResult.runtimeStrategy ?? null,
|
|
704
|
+
source: 'ws_remote_playwright',
|
|
705
|
+
workflowScreenshotsIncluded: includeWorkflowScreenshots,
|
|
706
|
+
actionsIncluded: includeActions,
|
|
707
|
+
},
|
|
708
|
+
});
|
|
709
|
+
void reconcilePendingBillingOperationCosts(supabase).catch(() => undefined);
|
|
710
|
+
if (!persistedSuccess) {
|
|
711
|
+
const errorType = agentResult.success
|
|
712
|
+
? 'storage_upload_failure'
|
|
713
|
+
: agentResult.actions.some((action) => action.action === 'give_up')
|
|
714
|
+
? 'give_up'
|
|
715
|
+
: agentResult.iterations >= maxIterations
|
|
716
|
+
? 'max_iterations'
|
|
717
|
+
: 'verification_failure';
|
|
718
|
+
getServerPostHog()?.capture({
|
|
719
|
+
distinctId: auth.userId,
|
|
720
|
+
event: 'capture_page_error',
|
|
721
|
+
properties: {
|
|
722
|
+
runId,
|
|
723
|
+
presetId,
|
|
724
|
+
projectId: project.id,
|
|
725
|
+
pageUrl: pageRun.url,
|
|
726
|
+
pageId: pageRun.pageId,
|
|
727
|
+
errorType,
|
|
728
|
+
lang,
|
|
729
|
+
theme,
|
|
730
|
+
iteration: agentResult.iterations,
|
|
731
|
+
assessment: persistedAssessment?.slice(0, 500) ?? null,
|
|
732
|
+
},
|
|
733
|
+
});
|
|
734
|
+
if (captureId) {
|
|
735
|
+
try {
|
|
736
|
+
await supabase.from('agent_errors').insert({
|
|
737
|
+
preset_id: presetId,
|
|
738
|
+
run_id: runId,
|
|
739
|
+
capture_id: captureId,
|
|
740
|
+
error_type: errorType,
|
|
741
|
+
message: persistedAssessment,
|
|
742
|
+
description: persistedAssessment,
|
|
743
|
+
screenshot_url: screenshotUrl,
|
|
744
|
+
agent_context: {
|
|
745
|
+
iterations: agentResult.iterations,
|
|
746
|
+
maxIterations,
|
|
747
|
+
captureTarget: {
|
|
748
|
+
lang,
|
|
749
|
+
theme,
|
|
750
|
+
targetLabel: target.label,
|
|
751
|
+
viewport: target.viewport,
|
|
752
|
+
},
|
|
753
|
+
},
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
catch {
|
|
757
|
+
// Non-blocking.
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
if (persistedSuccess && captureId && screenshotUrl) {
|
|
762
|
+
await syncStudioVariantAfterCapture({
|
|
763
|
+
supabase,
|
|
764
|
+
projectId: project.id,
|
|
765
|
+
presetId,
|
|
766
|
+
targetId: target.id,
|
|
767
|
+
lang,
|
|
768
|
+
theme,
|
|
769
|
+
elementName: pageRun.pageId,
|
|
770
|
+
captureId,
|
|
771
|
+
screenshotUrl,
|
|
772
|
+
rawScreenshotUrl,
|
|
773
|
+
deviceFrame: target.deviceFrame ?? null,
|
|
774
|
+
browserBar,
|
|
775
|
+
}).catch((error) => {
|
|
776
|
+
logger.error(`Studio sync failed: ${error.message}`);
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
if (captureId && screenshotUrl) {
|
|
780
|
+
const altText = await generateAltText({
|
|
781
|
+
url: pageRun.url,
|
|
782
|
+
prompt: pageRun.prompt,
|
|
783
|
+
lang,
|
|
784
|
+
theme,
|
|
785
|
+
targetLabel: target.label,
|
|
786
|
+
elementName: pageRun.pageId ?? undefined,
|
|
787
|
+
model: resolvedModel,
|
|
788
|
+
apiKey: getOpenRouterKey(),
|
|
789
|
+
}).catch(() => null);
|
|
790
|
+
if (altText) {
|
|
791
|
+
await supabase
|
|
792
|
+
.from('captures')
|
|
793
|
+
.update({ alt_text: altText })
|
|
794
|
+
.eq('id', captureId);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
getServerPostHog()?.capture({
|
|
798
|
+
distinctId: auth.userId,
|
|
799
|
+
event: 'capture_page_optimization',
|
|
800
|
+
properties: {
|
|
801
|
+
runId,
|
|
802
|
+
projectId: project.id,
|
|
803
|
+
presetId,
|
|
804
|
+
targetId: target.id,
|
|
805
|
+
pageId: pageRun.pageId,
|
|
806
|
+
lang,
|
|
807
|
+
theme,
|
|
808
|
+
success: persistedSuccess,
|
|
809
|
+
runtimeStrategy: agentResult.runtimeStrategy ?? null,
|
|
810
|
+
deterministicRecoveryUsed: agentResult.deterministicRecoveryUsed ?? false,
|
|
811
|
+
evaluatorUsed: agentResult.evaluatorUsed ?? false,
|
|
812
|
+
iterationCount: agentResult.iterations,
|
|
813
|
+
source: 'ws_remote_playwright',
|
|
814
|
+
},
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
finally {
|
|
818
|
+
captureIndex += 1;
|
|
819
|
+
await updateRunProgress();
|
|
820
|
+
sendEvent('progress', {
|
|
821
|
+
message: `[${captureIndex}/${totalCaptures}] ${lang}/${theme} · ${pageRun.pageId ?? 'main'} · ${target.label}`,
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
const persistElementCapture = async (params) => {
|
|
826
|
+
const { pageRun, element, lang, theme, result } = params;
|
|
827
|
+
try {
|
|
828
|
+
if (!result.success || !result.buffer) {
|
|
829
|
+
captureResults.push({
|
|
830
|
+
url: pageRun.url,
|
|
831
|
+
lang,
|
|
832
|
+
theme,
|
|
833
|
+
captureType: 'element',
|
|
834
|
+
elementName: element.name,
|
|
835
|
+
targetId: targets[0]?.id,
|
|
836
|
+
targetLabel: targets[0]?.label,
|
|
837
|
+
success: false,
|
|
838
|
+
assessment: result.assessment || 'Element capture failed.',
|
|
839
|
+
iterations: 0,
|
|
840
|
+
});
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
const finalBuffer = await applyPlanScreenshotWatermark(plan, result.buffer);
|
|
844
|
+
const costLogIds = await logAgentUsage({
|
|
845
|
+
usage: result.usage,
|
|
846
|
+
lang,
|
|
847
|
+
theme,
|
|
848
|
+
captureType: 'element',
|
|
849
|
+
elementName: element.name,
|
|
850
|
+
targetId: targets[0]?.id,
|
|
851
|
+
viewportWidth: targets[0]?.viewport.width ?? null,
|
|
852
|
+
viewportHeight: targets[0]?.viewport.height ?? null,
|
|
853
|
+
});
|
|
854
|
+
const storagePath = `screenshots/${auth.userId}/${runId}/elements/${sanitizeStorageSegment(pageRun.pageId)}/${element.name}_${lang}_${theme}.png`;
|
|
855
|
+
const { error: uploadError } = await supabase.storage
|
|
856
|
+
.from(BUCKET)
|
|
857
|
+
.upload(storagePath, finalBuffer, { contentType: 'image/png', upsert: true });
|
|
858
|
+
if (uploadError) {
|
|
859
|
+
logger.error(`Element upload failed: ${uploadError.message}`);
|
|
860
|
+
captureResults.push({
|
|
861
|
+
url: pageRun.url,
|
|
862
|
+
lang,
|
|
863
|
+
theme,
|
|
864
|
+
captureType: 'element',
|
|
865
|
+
elementName: element.name,
|
|
866
|
+
targetId: targets[0]?.id,
|
|
867
|
+
targetLabel: targets[0]?.label,
|
|
868
|
+
success: false,
|
|
869
|
+
assessment: uploadError.message,
|
|
870
|
+
iterations: 0,
|
|
871
|
+
});
|
|
872
|
+
await insertScreenshotOperationLog(supabase, {
|
|
873
|
+
runId,
|
|
874
|
+
userId: auth.userId,
|
|
875
|
+
projectId: project.id,
|
|
876
|
+
presetId,
|
|
877
|
+
captureId: null,
|
|
878
|
+
captureType: 'element',
|
|
879
|
+
lang,
|
|
880
|
+
theme,
|
|
881
|
+
elementName: element.name,
|
|
882
|
+
targetId: targets[0]?.id,
|
|
883
|
+
viewportWidth: targets[0]?.viewport.width ?? null,
|
|
884
|
+
viewportHeight: targets[0]?.viewport.height ?? null,
|
|
885
|
+
deviceFrame: null,
|
|
886
|
+
}, {
|
|
887
|
+
outcome: 'failed',
|
|
888
|
+
outcomeReason: uploadError.message,
|
|
889
|
+
billable: false,
|
|
890
|
+
creditsCharged: 0,
|
|
891
|
+
costLogIds,
|
|
892
|
+
usage: result.usage,
|
|
893
|
+
metadata: {
|
|
894
|
+
pageId: pageRun.pageId,
|
|
895
|
+
pageUrl: pageRun.url,
|
|
896
|
+
prompt: pageRun.prompt,
|
|
897
|
+
source: 'ws_remote_playwright',
|
|
898
|
+
},
|
|
899
|
+
});
|
|
900
|
+
void reconcilePendingBillingOperationCosts(supabase).catch(() => undefined);
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
const screenshotUrl = supabase.storage.from(BUCKET).getPublicUrl(storagePath).data.publicUrl;
|
|
904
|
+
const { data: captureRow, error } = await supabase
|
|
905
|
+
.from('captures')
|
|
906
|
+
.insert({
|
|
907
|
+
run_id: runId,
|
|
908
|
+
preset_id: presetId,
|
|
909
|
+
project_id: project.id,
|
|
910
|
+
target_id: targets[0]?.id ?? null,
|
|
911
|
+
element_name: element.name,
|
|
912
|
+
url: pageRun.url,
|
|
913
|
+
prompt: pageRun.prompt,
|
|
914
|
+
lang,
|
|
915
|
+
theme,
|
|
916
|
+
success: true,
|
|
917
|
+
assessment: result.assessment,
|
|
918
|
+
screenshot_url: screenshotUrl,
|
|
919
|
+
capture_type: 'element',
|
|
920
|
+
credits: SCREENSHOT_CREDIT_COST,
|
|
921
|
+
config: {
|
|
922
|
+
element,
|
|
923
|
+
outputScale,
|
|
924
|
+
pageId: pageRun.pageId,
|
|
925
|
+
},
|
|
926
|
+
})
|
|
927
|
+
.select('id')
|
|
928
|
+
.single();
|
|
929
|
+
if (error) {
|
|
930
|
+
logger.error(`Element capture insert failed: ${error.message}`);
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
captureResults.push({
|
|
934
|
+
url: pageRun.url,
|
|
935
|
+
lang,
|
|
936
|
+
theme,
|
|
937
|
+
captureType: 'element',
|
|
938
|
+
elementName: element.name,
|
|
939
|
+
targetId: targets[0]?.id,
|
|
940
|
+
targetLabel: targets[0]?.label,
|
|
941
|
+
success: true,
|
|
942
|
+
assessment: result.assessment,
|
|
943
|
+
iterations: 0,
|
|
944
|
+
finalScreenshotUrl: screenshotUrl,
|
|
945
|
+
});
|
|
946
|
+
successCount += 1;
|
|
947
|
+
await recordCreditUsage(supabase, {
|
|
948
|
+
userId: auth.userId,
|
|
949
|
+
projectId: project.id,
|
|
950
|
+
presetId,
|
|
951
|
+
runId,
|
|
952
|
+
type: 'screenshot',
|
|
953
|
+
credits: SCREENSHOT_CREDIT_COST,
|
|
954
|
+
sourceId: captureRow.id,
|
|
955
|
+
});
|
|
956
|
+
await updateCostLogCaptureContext(supabase, costLogIds, {
|
|
957
|
+
captureId: captureRow.id,
|
|
958
|
+
targetId: targets[0]?.id ?? null,
|
|
959
|
+
viewportWidth: targets[0]?.viewport.width ?? null,
|
|
960
|
+
viewportHeight: targets[0]?.viewport.height ?? null,
|
|
961
|
+
});
|
|
962
|
+
await insertScreenshotOperationLog(supabase, {
|
|
963
|
+
runId,
|
|
964
|
+
userId: auth.userId,
|
|
965
|
+
projectId: project.id,
|
|
966
|
+
presetId,
|
|
967
|
+
captureId: captureRow.id,
|
|
968
|
+
captureType: 'element',
|
|
969
|
+
lang,
|
|
970
|
+
theme,
|
|
971
|
+
elementName: element.name,
|
|
972
|
+
targetId: targets[0]?.id,
|
|
973
|
+
viewportWidth: targets[0]?.viewport.width ?? null,
|
|
974
|
+
viewportHeight: targets[0]?.viewport.height ?? null,
|
|
975
|
+
deviceFrame: null,
|
|
976
|
+
}, {
|
|
977
|
+
outcome: 'succeeded',
|
|
978
|
+
outcomeReason: null,
|
|
979
|
+
billable: true,
|
|
980
|
+
creditsCharged: SCREENSHOT_CREDIT_COST,
|
|
981
|
+
costLogIds,
|
|
982
|
+
usage: result.usage,
|
|
983
|
+
metadata: {
|
|
984
|
+
pageId: pageRun.pageId,
|
|
985
|
+
pageUrl: pageRun.url,
|
|
986
|
+
prompt: pageRun.prompt,
|
|
987
|
+
source: 'ws_remote_playwright',
|
|
988
|
+
},
|
|
989
|
+
});
|
|
990
|
+
void reconcilePendingBillingOperationCosts(supabase).catch(() => undefined);
|
|
991
|
+
await syncStudioVariantAfterCapture({
|
|
992
|
+
supabase,
|
|
993
|
+
projectId: project.id,
|
|
994
|
+
presetId,
|
|
995
|
+
targetId: targets[0]?.id ?? null,
|
|
996
|
+
lang,
|
|
997
|
+
theme,
|
|
998
|
+
elementName: element.name,
|
|
999
|
+
captureId: captureRow.id,
|
|
1000
|
+
screenshotUrl,
|
|
1001
|
+
matchTargetId: false,
|
|
1002
|
+
}).catch(() => { });
|
|
1003
|
+
const altText = await generateAltText({
|
|
1004
|
+
url: pageRun.url,
|
|
1005
|
+
prompt: pageRun.prompt,
|
|
1006
|
+
lang,
|
|
1007
|
+
theme,
|
|
1008
|
+
targetLabel: targets[0]?.label,
|
|
1009
|
+
elementName: element.name,
|
|
1010
|
+
model: resolvedModel,
|
|
1011
|
+
apiKey: getOpenRouterKey(),
|
|
1012
|
+
}).catch(() => null);
|
|
1013
|
+
if (altText) {
|
|
1014
|
+
await supabase
|
|
1015
|
+
.from('captures')
|
|
1016
|
+
.update({ alt_text: altText })
|
|
1017
|
+
.eq('id', captureRow.id);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
finally {
|
|
1021
|
+
captureIndex += 1;
|
|
1022
|
+
await updateRunProgress();
|
|
1023
|
+
sendEvent('progress', {
|
|
1024
|
+
message: `[${captureIndex}/${totalCaptures}] ${lang}/${theme} · ${pageRun.pageId ?? 'main'} · element:${element.name}`,
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
sendEvent('capture_plan', {
|
|
1029
|
+
variants: variantPlan.map((variant) => ({
|
|
1030
|
+
key: variant.key,
|
|
1031
|
+
lang: variant.lang,
|
|
1032
|
+
theme: variant.theme,
|
|
1033
|
+
pages: variant.pages,
|
|
1034
|
+
})),
|
|
1035
|
+
totalCaptures,
|
|
1036
|
+
});
|
|
1037
|
+
const { identities: sharedPageIdentities } = await inferPageIdentitiesWithLLM(variantPlan[0]?.pages ?? [], resolvedModel, getOpenRouterKey(), Object.keys(providerPreferences).length > 0 ? providerPreferences : undefined);
|
|
1038
|
+
try {
|
|
1039
|
+
for (const [variantIndex, variant] of variantPlan.entries()) {
|
|
1040
|
+
if (aborted)
|
|
1041
|
+
throw new Error('Client disconnected');
|
|
1042
|
+
const { lang, theme, pages: variantPages } = variant;
|
|
1043
|
+
const firstTarget = targets[0];
|
|
1044
|
+
const firstPage = variantPages[0];
|
|
1045
|
+
const variantCaptureStartIndex = captureIndex;
|
|
1046
|
+
const expectedVariantCaptureCount = (variantPages.length * targets.length) + elements.length;
|
|
1047
|
+
try {
|
|
1048
|
+
const firstDomain = getCaptureUrlHostname(firstPage.url);
|
|
1049
|
+
const persistedSessionProfile = await loadSessionProfileForVariant(firstDomain, lang, theme);
|
|
1050
|
+
const bootstrapSelection = buildSessionBootstrapProfile({
|
|
1051
|
+
runAuthProfile: getRunSharedAuthProfile(firstPage.url),
|
|
1052
|
+
persistedVariantProfile: persistedSessionProfile,
|
|
1053
|
+
requestedLang: lang,
|
|
1054
|
+
requestedTheme: theme,
|
|
1055
|
+
startUrl: firstPage.url,
|
|
1056
|
+
});
|
|
1057
|
+
let bootstrapProfile = bootstrapSelection.profile;
|
|
1058
|
+
if (runSharedStorageState) {
|
|
1059
|
+
bootstrapProfile = bootstrapProfile
|
|
1060
|
+
? {
|
|
1061
|
+
...bootstrapProfile,
|
|
1062
|
+
storageState: runSharedStorageState,
|
|
1063
|
+
sessionStorage: runSharedSessionStorage ?? bootstrapProfile.sessionStorage,
|
|
1064
|
+
}
|
|
1065
|
+
: {
|
|
1066
|
+
storageState: runSharedStorageState,
|
|
1067
|
+
sessionStorage: runSharedSessionStorage,
|
|
1068
|
+
authState: 'authenticated',
|
|
1069
|
+
accountLabel: null,
|
|
1070
|
+
detectedLang: null,
|
|
1071
|
+
detectedTheme: null,
|
|
1072
|
+
validatedStartUrl: firstPage.url,
|
|
1073
|
+
lastKnownUrl: firstPage.url,
|
|
1074
|
+
summary: 'Reusing live storage state from the previous successful variant.',
|
|
1075
|
+
validationStatus: 'unknown',
|
|
1076
|
+
lastUsedAt: null,
|
|
1077
|
+
profileVersion: 1,
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
sendEvent('progress', {
|
|
1081
|
+
message: `Variant ${variantIndex + 1}/${variantPlan.length}: ${lang}/${theme}`,
|
|
1082
|
+
});
|
|
1083
|
+
await remoteBrowser.recreateContext({
|
|
1084
|
+
viewport: firstTarget.viewport,
|
|
1085
|
+
deviceScaleFactor: outputScale,
|
|
1086
|
+
lang,
|
|
1087
|
+
colorScheme: theme,
|
|
1088
|
+
storageState: bootstrapProfile?.storageState,
|
|
1089
|
+
});
|
|
1090
|
+
await remoteBrowser.prepareSessionStorage(bootstrapProfile?.sessionStorage, { replace: false });
|
|
1091
|
+
let activeSessionProfile = bootstrapProfile;
|
|
1092
|
+
let carryoverContext;
|
|
1093
|
+
let variantState = createVariantCaptureState(variantPages, sharedPageIdentities);
|
|
1094
|
+
const elementAssignments = resolveIsolatedElementAssignments({
|
|
1095
|
+
elements,
|
|
1096
|
+
pageRuns: variantPages,
|
|
1097
|
+
});
|
|
1098
|
+
const variantStartSelectorMemory = resolveScopedSelectorMemory(await loadSelectorMemoryForVariant(firstDomain, lang, theme), {
|
|
1099
|
+
pageId: firstPage.pageId,
|
|
1100
|
+
pageIdentity: sharedPageIdentities[firstPage.pageId ?? 'main'] ?? null,
|
|
1101
|
+
pageUrl: firstPage.url,
|
|
1102
|
+
});
|
|
1103
|
+
if (!urlMatchesCaptureTarget(remoteBrowser.currentPage.url(), firstPage.url)) {
|
|
1104
|
+
await remoteBrowser.navigateTo(firstPage.url);
|
|
1105
|
+
await remoteBrowser.wait(500);
|
|
1106
|
+
await remoteBrowser.dismissOverlays().catch(() => undefined);
|
|
1107
|
+
}
|
|
1108
|
+
const variantLanguagePreflight = await ensureScreenshotVariantLanguage({
|
|
1109
|
+
browser: remoteBrowser,
|
|
1110
|
+
requestedLang: lang,
|
|
1111
|
+
requestedTheme: theme,
|
|
1112
|
+
startUrl: firstPage.url,
|
|
1113
|
+
profile: activeSessionProfile,
|
|
1114
|
+
credentials,
|
|
1115
|
+
selectorMemory: variantStartSelectorMemory,
|
|
1116
|
+
langInstructions: config.langInstructions,
|
|
1117
|
+
themeInstructions: config.themeInstructions,
|
|
1118
|
+
onLog: (message) => {
|
|
1119
|
+
sendEvent('progress', { message });
|
|
1120
|
+
},
|
|
1121
|
+
rebaseToStartUrl: async () => {
|
|
1122
|
+
await remoteBrowser.navigateTo(firstPage.url);
|
|
1123
|
+
await remoteBrowser.wait(500);
|
|
1124
|
+
await remoteBrowser.dismissOverlays().catch(() => undefined);
|
|
1125
|
+
},
|
|
1126
|
+
runLanguageSwitchAgent: async ({ languageState, themeState }) => {
|
|
1127
|
+
return runAgent(remoteBrowser, {
|
|
1128
|
+
url: remoteBrowser.currentPage.url() || firstPage.url,
|
|
1129
|
+
prompt: [
|
|
1130
|
+
`Prepare the current page so the UI is rendered in "${lang}" and the theme is "${theme}".`,
|
|
1131
|
+
`Current inspection: language=${languageState?.detected ?? 'unknown'}; theme=${themeState?.detected ?? 'unknown'}.`,
|
|
1132
|
+
firstPage.prompt,
|
|
1133
|
+
].join(' '),
|
|
1134
|
+
dark: theme === 'dark',
|
|
1135
|
+
langs: [lang],
|
|
1136
|
+
outputDir: '/tmp/autokap-ws',
|
|
1137
|
+
headed: false,
|
|
1138
|
+
viewport: firstTarget.viewport,
|
|
1139
|
+
maxIterations: Math.min(maxIterations, 10),
|
|
1140
|
+
model: resolvedModel,
|
|
1141
|
+
fallbackModel: resolvedFallback,
|
|
1142
|
+
visionModel: resolvedVisionModel,
|
|
1143
|
+
providerPreferences: Object.keys(providerPreferences).length > 0 ? providerPreferences : undefined,
|
|
1144
|
+
credentials,
|
|
1145
|
+
currentLang: lang,
|
|
1146
|
+
currentTheme: theme,
|
|
1147
|
+
selectorMemory: Object.keys(variantStartSelectorMemory).length > 0 ? variantStartSelectorMemory : undefined,
|
|
1148
|
+
sessionProfile: activeSessionProfile,
|
|
1149
|
+
runHints: runHints && runHints.length > 0 ? runHints : undefined,
|
|
1150
|
+
runMode: 'language_preflight',
|
|
1151
|
+
abortSignal: abortController.signal,
|
|
1152
|
+
}, getOpenRouterKey());
|
|
1153
|
+
},
|
|
1154
|
+
});
|
|
1155
|
+
if (variantLanguagePreflight.selectorUpdates.length > 0) {
|
|
1156
|
+
const scopedUpdates = scopeSelectorMemoryUpdates(variantLanguagePreflight.selectorUpdates, {
|
|
1157
|
+
pageId: firstPage.pageId,
|
|
1158
|
+
pageIdentity: sharedPageIdentities[firstPage.pageId ?? 'main'] ?? null,
|
|
1159
|
+
pageUrl: firstPage.url,
|
|
1160
|
+
});
|
|
1161
|
+
const cacheKey = `${firstDomain}:${lang}:${theme}`;
|
|
1162
|
+
selectorMemoryCache.set(cacheKey, applySelectorMemoryUpdates(selectorMemoryCache.get(cacheKey) ?? {}, scopedUpdates));
|
|
1163
|
+
void persistScreenshotSelectorMemoryUpdates(supabase, {
|
|
1164
|
+
projectId: project.id,
|
|
1165
|
+
presetId,
|
|
1166
|
+
domain: firstDomain,
|
|
1167
|
+
lang,
|
|
1168
|
+
theme,
|
|
1169
|
+
}, scopedUpdates).catch(() => undefined);
|
|
1170
|
+
}
|
|
1171
|
+
if (!variantLanguagePreflight.ok) {
|
|
1172
|
+
throw new Error(variantLanguagePreflight.reason
|
|
1173
|
+
?? `Language preflight failed before starting the main workflow for ${lang}/${theme}.`);
|
|
1174
|
+
}
|
|
1175
|
+
if (activeSessionProfile?.authState === 'authenticated'
|
|
1176
|
+
&& activeSessionProfile.validationStatus !== 'invalid') {
|
|
1177
|
+
rememberRunSharedAuthProfile(activeSessionProfile, remoteBrowser.currentPage.url());
|
|
1178
|
+
}
|
|
1179
|
+
for (const [pageIndex, pageRun] of variantPages.entries()) {
|
|
1180
|
+
if (aborted)
|
|
1181
|
+
throw new Error('Client disconnected');
|
|
1182
|
+
const pageId = pageRun.pageId ?? 'main';
|
|
1183
|
+
const pageDomain = getCaptureUrlHostname(pageRun.url);
|
|
1184
|
+
variantState = markVariantCaptureInProgress(variantState, pageRun.pageId);
|
|
1185
|
+
const variantManifest = buildVariantManifestContext({
|
|
1186
|
+
state: variantState,
|
|
1187
|
+
currentPageRun: pageRun,
|
|
1188
|
+
});
|
|
1189
|
+
const selectorMemory = resolveScopedSelectorMemory(mergeSelectorMemory(await loadSelectorMemoryForVariant(pageDomain, lang, theme), carryoverContext?.selectorMemory), {
|
|
1190
|
+
pageId: pageRun.pageId,
|
|
1191
|
+
pageIdentity: variantManifest.currentPageIdentity ?? null,
|
|
1192
|
+
pageUrl: pageRun.url,
|
|
1193
|
+
});
|
|
1194
|
+
const buildConfig = (viewport = firstTarget.viewport) => {
|
|
1195
|
+
const nextConfig = {
|
|
1196
|
+
url: pageRun.url,
|
|
1197
|
+
prompt: pageRun.prompt,
|
|
1198
|
+
dark: theme === 'dark',
|
|
1199
|
+
langs: [lang],
|
|
1200
|
+
outputDir: '/tmp/autokap-ws',
|
|
1201
|
+
headed: false,
|
|
1202
|
+
viewport,
|
|
1203
|
+
maxIterations,
|
|
1204
|
+
model: resolvedModel,
|
|
1205
|
+
fallbackModel: resolvedFallback,
|
|
1206
|
+
visionModel: resolvedVisionModel,
|
|
1207
|
+
providerPreferences: Object.keys(providerPreferences).length > 0 ? providerPreferences : undefined,
|
|
1208
|
+
credentials,
|
|
1209
|
+
langInstructions: config.langInstructions,
|
|
1210
|
+
themeInstructions: config.themeInstructions,
|
|
1211
|
+
currentLang: lang,
|
|
1212
|
+
currentTheme: theme,
|
|
1213
|
+
reasoningLocale: config.reasoningLocale,
|
|
1214
|
+
reasoningEffort: config.reasoningEffort ?? 'medium',
|
|
1215
|
+
runHints: runHints && runHints.length > 0 ? runHints : undefined,
|
|
1216
|
+
selectorMemory: Object.keys(selectorMemory).length > 0 ? selectorMemory : undefined,
|
|
1217
|
+
sessionProfile: activeSessionProfile,
|
|
1218
|
+
handoffContext: carryoverContext,
|
|
1219
|
+
variantManifest,
|
|
1220
|
+
runMode: 'capture',
|
|
1221
|
+
analyticsId: auth.userId,
|
|
1222
|
+
enableDeterministicRecovery: true,
|
|
1223
|
+
enableRecoveryEvaluator: true,
|
|
1224
|
+
enableSalienceCompression: true,
|
|
1225
|
+
abortSignal: abortController.signal,
|
|
1226
|
+
};
|
|
1227
|
+
if (variantIndex > 1) {
|
|
1228
|
+
const reference = liveVariantReference.get(pageId);
|
|
1229
|
+
if (reference) {
|
|
1230
|
+
nextConfig.variantReference = reference;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
return nextConfig;
|
|
1234
|
+
};
|
|
1235
|
+
sendEvent('progress', {
|
|
1236
|
+
message: `Preparing ${lang}/${theme} · ${pageId} (${pageIndex + 1}/${variantPages.length})`,
|
|
1237
|
+
});
|
|
1238
|
+
let primaryAgentResult = null;
|
|
1239
|
+
const currentUrl = remoteBrowser.currentPage.url();
|
|
1240
|
+
const canUseSequentialHandoff = !!(carryoverContext
|
|
1241
|
+
&& shouldUseSequentialPageHandoff({
|
|
1242
|
+
pageIndex,
|
|
1243
|
+
currentUrl,
|
|
1244
|
+
pageUrl: pageRun.url,
|
|
1245
|
+
previousPageId: carryoverContext.previousPageId,
|
|
1246
|
+
pageId: pageRun.pageId,
|
|
1247
|
+
previousPrompt: carryoverContext.previousPrompt,
|
|
1248
|
+
pagePrompt: pageRun.prompt,
|
|
1249
|
+
}));
|
|
1250
|
+
const reuseLiveState = shouldReuseLivePageState({
|
|
1251
|
+
pageIndex,
|
|
1252
|
+
presetRootUrl: projectUrl,
|
|
1253
|
+
pageUrl: pageRun.url,
|
|
1254
|
+
currentUrl,
|
|
1255
|
+
isAuthenticated: activeSessionProfile?.authState === 'authenticated',
|
|
1256
|
+
});
|
|
1257
|
+
if (canUseSequentialHandoff) {
|
|
1258
|
+
const readiness = await verifyCaptureReadiness(remoteBrowser, buildConfig(), getOpenRouterKey(), {
|
|
1259
|
+
assessment: 'Sequential handoff preflight: approve only if the current live page already satisfies this capture.',
|
|
1260
|
+
stepNumber: 0,
|
|
1261
|
+
}).catch(() => null);
|
|
1262
|
+
if (readiness?.verified) {
|
|
1263
|
+
primaryAgentResult = buildPreverifiedAgentResult('Capture approved directly from sequential handoff state.', readiness);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
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
|
+
if (!primaryAgentResult && variantIndex > 0) {
|
|
1272
|
+
const recordedActions = liveVariantActions.get(pageId);
|
|
1273
|
+
if (recordedActions && recordedActions.length > 0) {
|
|
1274
|
+
sendEvent('progress', {
|
|
1275
|
+
message: `Replaying first successful variant for ${lang}/${theme} · ${pageId}`,
|
|
1276
|
+
});
|
|
1277
|
+
primaryAgentResult = await replayAgent(remoteBrowser, buildConfig(), getOpenRouterKey(), recordedActions, { allowFullAgentFallback: true }).catch((error) => makeFailedAgentResult(error.message));
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
if (!primaryAgentResult) {
|
|
1281
|
+
primaryAgentResult = await runAgent(remoteBrowser, buildConfig(), getOpenRouterKey()).catch((error) => makeFailedAgentResult(error.message));
|
|
1282
|
+
}
|
|
1283
|
+
const preparedPageUrl = remoteBrowser.currentPage.url() || pageRun.url;
|
|
1284
|
+
await persistFullPageCapture({
|
|
1285
|
+
pageRun,
|
|
1286
|
+
target: firstTarget,
|
|
1287
|
+
lang,
|
|
1288
|
+
theme,
|
|
1289
|
+
agentResult: primaryAgentResult,
|
|
1290
|
+
includeActions: true,
|
|
1291
|
+
includeWorkflowScreenshots: true,
|
|
1292
|
+
});
|
|
1293
|
+
for (const target of targets.slice(1)) {
|
|
1294
|
+
await remoteBrowser.resizeViewport(target.viewport.width, target.viewport.height);
|
|
1295
|
+
await remoteBrowser.wait(250);
|
|
1296
|
+
await remoteBrowser.dismissOverlays().catch(() => undefined);
|
|
1297
|
+
let targetResult;
|
|
1298
|
+
const targetConfig = buildConfig(target.viewport);
|
|
1299
|
+
const targetReadiness = await verifyCaptureReadiness(remoteBrowser, targetConfig, getOpenRouterKey(), {
|
|
1300
|
+
assessment: 'Same-session target recheck: approve only if the resized page still satisfies the capture.',
|
|
1301
|
+
stepNumber: 0,
|
|
1302
|
+
}).catch(() => null);
|
|
1303
|
+
if (targetReadiness?.verified) {
|
|
1304
|
+
targetResult = buildPreverifiedAgentResult('Capture approved after same-session target recheck.', targetReadiness);
|
|
1305
|
+
}
|
|
1306
|
+
else {
|
|
1307
|
+
targetResult = await replayAgent(remoteBrowser, targetConfig, getOpenRouterKey(), primaryAgentResult.actions, { allowFullAgentFallback: false }).catch((error) => makeFailedAgentResult(error.message));
|
|
1308
|
+
}
|
|
1309
|
+
await persistFullPageCapture({
|
|
1310
|
+
pageRun,
|
|
1311
|
+
target,
|
|
1312
|
+
lang,
|
|
1313
|
+
theme,
|
|
1314
|
+
agentResult: targetResult,
|
|
1315
|
+
includeActions: false,
|
|
1316
|
+
includeWorkflowScreenshots: false,
|
|
1317
|
+
});
|
|
1318
|
+
await restorePreparedPageState(remoteBrowser, firstTarget.viewport, preparedPageUrl);
|
|
1319
|
+
}
|
|
1320
|
+
if (primaryAgentResult.success) {
|
|
1321
|
+
const recorded = recordValidatedVariantCapture({
|
|
1322
|
+
state: variantState,
|
|
1323
|
+
capture: {
|
|
1324
|
+
pageId,
|
|
1325
|
+
prompt: pageRun.prompt,
|
|
1326
|
+
url: pageRun.url,
|
|
1327
|
+
assessment: primaryAgentResult.assessment,
|
|
1328
|
+
fingerprint: primaryAgentResult.verification?.pageFingerprint ?? null,
|
|
1329
|
+
identity: variantManifest.currentPageIdentity ?? null,
|
|
1330
|
+
},
|
|
1331
|
+
});
|
|
1332
|
+
variantState = recorded.state;
|
|
1333
|
+
if (recorded.duplicateOfPageId) {
|
|
1334
|
+
const blockingReason = `Duplicate capture blocked: page "${pageId}" matches previously validated page "${recorded.duplicateOfPageId}".`;
|
|
1335
|
+
variantState = markVariantCaptureBlocked({
|
|
1336
|
+
state: variantState,
|
|
1337
|
+
pageId,
|
|
1338
|
+
reason: blockingReason,
|
|
1339
|
+
});
|
|
1340
|
+
primaryAgentResult = {
|
|
1341
|
+
...primaryAgentResult,
|
|
1342
|
+
success: false,
|
|
1343
|
+
assessment: blockingReason,
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
if (primaryAgentResult.success) {
|
|
1348
|
+
if (primaryAgentResult.actions.length > 0 && !liveVariantActions.has(pageId)) {
|
|
1349
|
+
liveVariantActions.set(pageId, primaryAgentResult.actions);
|
|
1350
|
+
}
|
|
1351
|
+
if (!liveVariantReference.has(pageId)) {
|
|
1352
|
+
liveVariantReference.set(pageId, {
|
|
1353
|
+
finalUrl: remoteBrowser.currentPage.url() || pageRun.url,
|
|
1354
|
+
assessment: primaryAgentResult.assessment,
|
|
1355
|
+
pageTitle: await remoteBrowser.currentPage.title().catch(() => ''),
|
|
1356
|
+
actions: primaryAgentResult.actions,
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
if (primaryAgentResult.actions.length > 0) {
|
|
1360
|
+
const scopeContext = {
|
|
1361
|
+
pageId: pageRun.pageId,
|
|
1362
|
+
pageIdentity: variantManifest.currentPageIdentity ?? null,
|
|
1363
|
+
pageUrl: pageRun.url,
|
|
1364
|
+
};
|
|
1365
|
+
const selectorUpdates = scopeSelectorMemoryUpdates(extractSelectorUpdates(primaryAgentResult.actions), scopeContext);
|
|
1366
|
+
if (selectorUpdates.length > 0) {
|
|
1367
|
+
const cacheKey = `${pageDomain}:${lang}:${theme}`;
|
|
1368
|
+
selectorMemoryCache.set(cacheKey, applySelectorMemoryUpdates(selectorMemoryCache.get(cacheKey) ?? {}, selectorUpdates));
|
|
1369
|
+
void persistScreenshotSelectorMemoryUpdates(supabase, {
|
|
1370
|
+
projectId: project.id,
|
|
1371
|
+
presetId,
|
|
1372
|
+
domain: pageDomain,
|
|
1373
|
+
lang,
|
|
1374
|
+
theme,
|
|
1375
|
+
}, selectorUpdates).catch(() => undefined);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
const pageElements = elementAssignments
|
|
1379
|
+
.filter((assignment) => assignment.pageId === pageId)
|
|
1380
|
+
.map((assignment) => assignment.element);
|
|
1381
|
+
if (pageElements.length > 0) {
|
|
1382
|
+
await restorePreparedPageState(remoteBrowser, firstTarget.viewport, preparedPageUrl);
|
|
1383
|
+
await remoteBrowser.forceLoadLazyImages({ timeout: 8000 }).catch(() => undefined);
|
|
1384
|
+
}
|
|
1385
|
+
for (const element of pageElements) {
|
|
1386
|
+
const elementResult = await captureIsolatedElement(remoteBrowser, element, getOpenRouterKey(), resolvedModel).catch((error) => {
|
|
1387
|
+
logger.error(`Element "${element.name}" capture failed: ${error.message}`);
|
|
1388
|
+
return null;
|
|
1389
|
+
});
|
|
1390
|
+
if (elementResult) {
|
|
1391
|
+
await persistElementCapture({
|
|
1392
|
+
pageRun,
|
|
1393
|
+
element,
|
|
1394
|
+
lang,
|
|
1395
|
+
theme,
|
|
1396
|
+
result: elementResult,
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
else {
|
|
1400
|
+
captureResults.push({
|
|
1401
|
+
url: pageRun.url,
|
|
1402
|
+
lang,
|
|
1403
|
+
theme,
|
|
1404
|
+
captureType: 'element',
|
|
1405
|
+
elementName: element.name,
|
|
1406
|
+
targetId: targets[0]?.id,
|
|
1407
|
+
targetLabel: targets[0]?.label,
|
|
1408
|
+
success: false,
|
|
1409
|
+
assessment: `Element capture failed for ${element.name}.`,
|
|
1410
|
+
iterations: 0,
|
|
1411
|
+
});
|
|
1412
|
+
captureIndex += 1;
|
|
1413
|
+
await updateRunProgress();
|
|
1414
|
+
sendEvent('progress', {
|
|
1415
|
+
message: `[${captureIndex}/${totalCaptures}] ${lang}/${theme} · ${pageRun.pageId ?? 'main'} · element:${element.name} failed`,
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
const storageState = await remoteBrowser.exportStorageState().catch(() => undefined);
|
|
1420
|
+
const sessionStorage = await remoteBrowser.exportSessionStorage().catch(() => undefined);
|
|
1421
|
+
const currentUrl = remoteBrowser.currentPage.url() || preparedPageUrl;
|
|
1422
|
+
activeSessionProfile = buildPersistableProfile(activeSessionProfile, storageState, sessionStorage, currentUrl, lang, theme);
|
|
1423
|
+
if (activeSessionProfile.storageState) {
|
|
1424
|
+
runSharedStorageState = activeSessionProfile.storageState;
|
|
1425
|
+
runSharedSessionStorage = activeSessionProfile.sessionStorage;
|
|
1426
|
+
await persistSessionProfile(pageDomain, lang, theme, activeSessionProfile);
|
|
1427
|
+
}
|
|
1428
|
+
rememberRunSharedAuthProfile(activeSessionProfile, currentUrl);
|
|
1429
|
+
carryoverContext = await buildHandoffContext({
|
|
1430
|
+
browser: remoteBrowser,
|
|
1431
|
+
pageRun,
|
|
1432
|
+
profile: activeSessionProfile,
|
|
1433
|
+
lang,
|
|
1434
|
+
theme,
|
|
1435
|
+
selectorMemory,
|
|
1436
|
+
actions: primaryAgentResult.actions,
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
else {
|
|
1440
|
+
variantState = markVariantCaptureBlocked({
|
|
1441
|
+
state: variantState,
|
|
1442
|
+
pageId,
|
|
1443
|
+
reason: primaryAgentResult.assessment || `Capture failed for ${pageId}.`,
|
|
1444
|
+
});
|
|
1445
|
+
carryoverContext = undefined;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
const variantValidation = validateVariantCaptureState(variantState);
|
|
1449
|
+
if (!variantValidation.ok) {
|
|
1450
|
+
sendEvent('progress', {
|
|
1451
|
+
message: `Variant ${lang}/${theme} completed with blocking issues: `
|
|
1452
|
+
+ [
|
|
1453
|
+
...variantValidation.missingPages.map((pageId) => `missing ${pageId}`),
|
|
1454
|
+
...variantValidation.blockedPages.map((entry) => `blocked ${entry.pageId}`),
|
|
1455
|
+
...variantValidation.duplicatePageIds.map((entry) => `duplicate ${entry.pageId}->${entry.duplicateOfPageId}`),
|
|
1456
|
+
].join(', '),
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
sendEvent('variant_complete', {
|
|
1460
|
+
lang,
|
|
1461
|
+
theme,
|
|
1462
|
+
success: variantValidation.ok,
|
|
1463
|
+
reason: variantValidation.ok ? null : 'Variant completed with blocking issues.',
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
catch (error) {
|
|
1467
|
+
if (aborted)
|
|
1468
|
+
throw error;
|
|
1469
|
+
const message = error.message;
|
|
1470
|
+
logger.error(`Variant ${lang}/${theme} failed: ${message}`);
|
|
1471
|
+
const consumedCaptures = captureIndex - variantCaptureStartIndex;
|
|
1472
|
+
const remainingCaptures = Math.max(0, expectedVariantCaptureCount - consumedCaptures);
|
|
1473
|
+
if (remainingCaptures > 0) {
|
|
1474
|
+
captureIndex += remainingCaptures;
|
|
1475
|
+
await updateRunProgress();
|
|
1476
|
+
}
|
|
1477
|
+
getServerPostHog()?.capture({
|
|
1478
|
+
distinctId: auth.userId,
|
|
1479
|
+
event: 'capture_variant_error',
|
|
1480
|
+
properties: {
|
|
1481
|
+
runId,
|
|
1482
|
+
presetId,
|
|
1483
|
+
projectId: project.id,
|
|
1484
|
+
lang,
|
|
1485
|
+
theme,
|
|
1486
|
+
error: message.slice(0, 500),
|
|
1487
|
+
variantIndex,
|
|
1488
|
+
source: 'ws_remote_playwright',
|
|
1489
|
+
},
|
|
1490
|
+
});
|
|
1491
|
+
sendEvent('progress', {
|
|
1492
|
+
message: `Variant ${lang}/${theme} failed: ${message}`,
|
|
1493
|
+
});
|
|
1494
|
+
sendEvent('variant_complete', {
|
|
1495
|
+
lang,
|
|
1496
|
+
theme,
|
|
1497
|
+
success: false,
|
|
1498
|
+
reason: message,
|
|
1499
|
+
});
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
catch (error) {
|
|
1505
|
+
const message = error.message;
|
|
1506
|
+
logger.error(`Capture run failed: ${message}`);
|
|
1507
|
+
await supabase
|
|
1508
|
+
.from('capture_runs')
|
|
1509
|
+
.update({
|
|
1510
|
+
status: 'failed',
|
|
1511
|
+
error_message: message,
|
|
1512
|
+
completed_at: new Date().toISOString(),
|
|
1513
|
+
})
|
|
1514
|
+
.eq('id', runId);
|
|
1515
|
+
sendEvent('error', { message });
|
|
1516
|
+
await finalizeRunSideEffects();
|
|
1517
|
+
remoteBrowser.destroy();
|
|
1518
|
+
await closeMockup();
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
await supabase
|
|
1522
|
+
.from('capture_runs')
|
|
1523
|
+
.update({
|
|
1524
|
+
status: 'completed',
|
|
1525
|
+
progress_current: totalCaptures,
|
|
1526
|
+
credits_used: successCount,
|
|
1527
|
+
completed_at: new Date().toISOString(),
|
|
1528
|
+
})
|
|
1529
|
+
.eq('id', runId);
|
|
1530
|
+
getServerPostHog()?.capture({
|
|
1531
|
+
distinctId: auth.userId,
|
|
1532
|
+
event: 'capture_run_optimization_summary',
|
|
1533
|
+
properties: {
|
|
1534
|
+
runId,
|
|
1535
|
+
presetId,
|
|
1536
|
+
projectId: project.id,
|
|
1537
|
+
successCount,
|
|
1538
|
+
totalCaptures,
|
|
1539
|
+
source: 'ws_remote_playwright',
|
|
1540
|
+
},
|
|
1541
|
+
});
|
|
1542
|
+
sendEvent('done', { summary: { successes: successCount, total: totalCaptures } });
|
|
1543
|
+
if (plan.entitlements.captureCompleteWebhook) {
|
|
1544
|
+
try {
|
|
1545
|
+
const webhookConfig = await getProjectWebhookConfig(supabase, project.id);
|
|
1546
|
+
if (webhookConfig) {
|
|
1547
|
+
await dispatchProjectCaptureWebhook({
|
|
1548
|
+
config: webhookConfig,
|
|
1549
|
+
payload: buildCaptureWebhookPayload({
|
|
1550
|
+
runId,
|
|
1551
|
+
projectId: project.id,
|
|
1552
|
+
presetId,
|
|
1553
|
+
results: captureResults,
|
|
1554
|
+
}),
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
catch {
|
|
1559
|
+
// Non-blocking.
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
await finalizeRunSideEffects();
|
|
1563
|
+
remoteBrowser.destroy();
|
|
1564
|
+
await closeMockup();
|
|
1565
|
+
logger.success(`Run ${runId} completed: ${successCount}/${totalCaptures}`);
|
|
1566
|
+
}
|
|
1567
|
+
function urlsRoughlyMatch(left, right) {
|
|
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
|
+
}
|
|
1663
|
+
//# sourceMappingURL=ws-handler.js.map
|