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
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { evaluateSurfaceContract } from "./capture-surface-contract.js";
|
|
2
|
+
import { evaluateRequestedLanguageState, evaluateRequestedThemeState, } from "./session-profile.js";
|
|
3
|
+
function normalizePath(value) {
|
|
4
|
+
if (!value)
|
|
5
|
+
return null;
|
|
6
|
+
try {
|
|
7
|
+
const parsed = new URL(value);
|
|
8
|
+
const pathname = parsed.pathname.replace(/\/+$/, "") || "/";
|
|
9
|
+
return `${parsed.origin}${pathname}`;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return value.trim().replace(/\/+$/, "") || null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function sameOrigin(left, right) {
|
|
16
|
+
if (!left || !right)
|
|
17
|
+
return false;
|
|
18
|
+
try {
|
|
19
|
+
return new URL(left).origin === new URL(right).origin;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function urlMatchesPolicy(currentUrl, targetUrl, policy) {
|
|
26
|
+
if (policy === "ignore")
|
|
27
|
+
return true;
|
|
28
|
+
if (!currentUrl || !targetUrl)
|
|
29
|
+
return false;
|
|
30
|
+
try {
|
|
31
|
+
const current = new URL(currentUrl);
|
|
32
|
+
const target = new URL(targetUrl);
|
|
33
|
+
if (current.origin !== target.origin)
|
|
34
|
+
return false;
|
|
35
|
+
if (policy === "same_origin")
|
|
36
|
+
return true;
|
|
37
|
+
const normalizedTarget = target.pathname.replace(/\/+$/, "") || "/";
|
|
38
|
+
const normalizedCurrent = current.pathname.replace(/\/+$/, "") || "/";
|
|
39
|
+
if (normalizedCurrent === normalizedTarget)
|
|
40
|
+
return true;
|
|
41
|
+
return normalizedCurrent.startsWith(`${normalizedTarget}/`);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return normalizePath(currentUrl) === normalizePath(targetUrl);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function detectLikelyAuthMismatch(signals, profile) {
|
|
48
|
+
if (!signals || profile?.authState !== "authenticated")
|
|
49
|
+
return false;
|
|
50
|
+
const hasAuthenticatedHints = (signals.authHints?.logoutButtons.length ?? 0) > 0
|
|
51
|
+
|| (signals.authHints?.accountMenuLabels.length ?? 0) > 0
|
|
52
|
+
|| (signals.authHints?.accountLikeText.length ?? 0) > 0;
|
|
53
|
+
const hasAnonymousHints = signals.authHints?.hasAuthForm
|
|
54
|
+
|| signals.authHints?.hasPasswordField
|
|
55
|
+
|| ((signals.authHints?.loginButtons.length ?? 0) > 0 && !hasAuthenticatedHints);
|
|
56
|
+
return !!hasAnonymousHints && !hasAuthenticatedHints;
|
|
57
|
+
}
|
|
58
|
+
function detectLikelyVariantMismatch(params) {
|
|
59
|
+
const { signals, requestedLang, requestedTheme } = params;
|
|
60
|
+
if (!signals)
|
|
61
|
+
return false;
|
|
62
|
+
const languageState = requestedLang
|
|
63
|
+
? evaluateRequestedLanguageState({
|
|
64
|
+
currentUrl: signals.url || signals.canonicalUrl || "",
|
|
65
|
+
requestedLang,
|
|
66
|
+
signals,
|
|
67
|
+
})
|
|
68
|
+
: null;
|
|
69
|
+
const themeState = requestedTheme
|
|
70
|
+
? evaluateRequestedThemeState({
|
|
71
|
+
requestedTheme,
|
|
72
|
+
signals,
|
|
73
|
+
})
|
|
74
|
+
: null;
|
|
75
|
+
const langMismatch = !!languageState?.requested
|
|
76
|
+
&& languageState.detected !== null
|
|
77
|
+
&& languageState.active === false
|
|
78
|
+
&& languageState.ambiguous === false
|
|
79
|
+
&& languageState.confidence === "high";
|
|
80
|
+
const themeMismatch = !!themeState?.requested
|
|
81
|
+
&& themeState.detected !== null
|
|
82
|
+
&& themeState.active === false
|
|
83
|
+
&& themeState.ambiguous === false
|
|
84
|
+
&& themeState.confidence === "high";
|
|
85
|
+
return langMismatch || themeMismatch;
|
|
86
|
+
}
|
|
87
|
+
export function deriveCapturePageContract(params) {
|
|
88
|
+
const identity = params.pageIdentity ?? null;
|
|
89
|
+
switch (identity?.kind) {
|
|
90
|
+
case "modal_selection":
|
|
91
|
+
return {
|
|
92
|
+
expectedSurfaceKind: "dialog_selection",
|
|
93
|
+
dialogPolicy: "must_be_open",
|
|
94
|
+
routePolicy: "same_origin",
|
|
95
|
+
reusePolicy: "verified_only",
|
|
96
|
+
variantPolicy: "must_match",
|
|
97
|
+
};
|
|
98
|
+
case "modal_configuration":
|
|
99
|
+
return {
|
|
100
|
+
expectedSurfaceKind: "dialog_configuration",
|
|
101
|
+
dialogPolicy: "must_be_open",
|
|
102
|
+
routePolicy: "same_origin",
|
|
103
|
+
reusePolicy: "verified_only",
|
|
104
|
+
variantPolicy: "must_match",
|
|
105
|
+
};
|
|
106
|
+
case "editor_route":
|
|
107
|
+
return {
|
|
108
|
+
expectedSurfaceKind: "editor_route",
|
|
109
|
+
dialogPolicy: "must_be_closed",
|
|
110
|
+
routePolicy: "exact_or_descendant",
|
|
111
|
+
reusePolicy: "same_surface_only",
|
|
112
|
+
variantPolicy: "must_match",
|
|
113
|
+
};
|
|
114
|
+
case "detail_route":
|
|
115
|
+
return {
|
|
116
|
+
expectedSurfaceKind: "detail_route",
|
|
117
|
+
dialogPolicy: identity.dedicatedRoute ? "must_be_closed" : "allow_either",
|
|
118
|
+
routePolicy: identity.dedicatedRoute ? "exact_or_descendant" : "same_origin",
|
|
119
|
+
reusePolicy: "same_surface_only",
|
|
120
|
+
variantPolicy: "must_match",
|
|
121
|
+
};
|
|
122
|
+
case "gallery":
|
|
123
|
+
return {
|
|
124
|
+
expectedSurfaceKind: "gallery",
|
|
125
|
+
dialogPolicy: identity.dialogTarget ? "must_be_open" : "must_be_closed",
|
|
126
|
+
routePolicy: identity.dedicatedRoute ? "exact_or_descendant" : "same_origin",
|
|
127
|
+
reusePolicy: "same_surface_only",
|
|
128
|
+
variantPolicy: "must_match",
|
|
129
|
+
};
|
|
130
|
+
default:
|
|
131
|
+
return {
|
|
132
|
+
expectedSurfaceKind: "route",
|
|
133
|
+
dialogPolicy: identity?.dialogTarget ? "must_be_open" : "allow_either",
|
|
134
|
+
routePolicy: identity?.dedicatedRoute ? "exact_or_descendant" : "same_origin",
|
|
135
|
+
reusePolicy: "same_surface_only",
|
|
136
|
+
variantPolicy: "prefer_match",
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
export function buildCaptureCheckpointEvidence(params) {
|
|
141
|
+
const contract = deriveCapturePageContract({ pageIdentity: params.pageIdentity });
|
|
142
|
+
const reasons = [];
|
|
143
|
+
const urlMatched = urlMatchesPolicy(params.currentUrl ?? params.observation.url, params.targetUrl, contract.routePolicy);
|
|
144
|
+
if (!urlMatched) {
|
|
145
|
+
reasons.push("current route does not match the target route policy");
|
|
146
|
+
}
|
|
147
|
+
const dialogMatched = contract.dialogPolicy === "allow_either"
|
|
148
|
+
? true
|
|
149
|
+
: contract.dialogPolicy === "must_be_open"
|
|
150
|
+
? params.observation.dialogCount > 0
|
|
151
|
+
: params.observation.dialogCount === 0;
|
|
152
|
+
if (!dialogMatched) {
|
|
153
|
+
reasons.push(contract.dialogPolicy === "must_be_open"
|
|
154
|
+
? "expected dialog/configuration surface is not open"
|
|
155
|
+
: "unexpected dialog/overlay is still open");
|
|
156
|
+
}
|
|
157
|
+
const authMatched = !detectLikelyAuthMismatch(params.pageSignals ?? null, params.profile);
|
|
158
|
+
if (!authMatched) {
|
|
159
|
+
reasons.push("authenticated session looks degraded on the current page");
|
|
160
|
+
}
|
|
161
|
+
const variantMatched = !detectLikelyVariantMismatch({
|
|
162
|
+
signals: params.pageSignals ?? null,
|
|
163
|
+
requestedLang: params.requestedLang,
|
|
164
|
+
requestedTheme: params.requestedTheme,
|
|
165
|
+
});
|
|
166
|
+
if (!variantMatched && contract.variantPolicy === "must_match") {
|
|
167
|
+
reasons.push("current page hints do not match the requested language/theme variant");
|
|
168
|
+
}
|
|
169
|
+
const strongSurfaceObserved = !!params.observation.strongSurfaceSignature
|
|
170
|
+
&& (!!params.observation.primarySurface
|
|
171
|
+
|| !!params.observation.overlaySurface
|
|
172
|
+
|| !!params.observation.configurationSurface);
|
|
173
|
+
if (!strongSurfaceObserved) {
|
|
174
|
+
reasons.push("current page surface is too weakly identified for a strong checkpoint");
|
|
175
|
+
}
|
|
176
|
+
const surfaceContract = evaluateSurfaceContract({
|
|
177
|
+
contract,
|
|
178
|
+
currentUrl: params.currentUrl,
|
|
179
|
+
observation: params.observation,
|
|
180
|
+
pageSignals: params.pageSignals ?? null,
|
|
181
|
+
pageIdentity: params.pageIdentity ?? null,
|
|
182
|
+
});
|
|
183
|
+
if (!surfaceContract.matched) {
|
|
184
|
+
reasons.push(surfaceContract.reason ?? "current page surface does not match the expected capture contract");
|
|
185
|
+
}
|
|
186
|
+
const evidence = {
|
|
187
|
+
urlMatched,
|
|
188
|
+
dialogMatched,
|
|
189
|
+
authMatched,
|
|
190
|
+
variantMatched: contract.variantPolicy === "ignore" ? true : variantMatched,
|
|
191
|
+
strongSurfaceObserved,
|
|
192
|
+
surfaceMatched: surfaceContract.matched,
|
|
193
|
+
strongSurfaceSignature: params.observation.strongSurfaceSignature ?? params.observation.surfaceSignature ?? null,
|
|
194
|
+
primarySurface: params.observation.primarySurface ?? null,
|
|
195
|
+
overlaySurface: params.observation.overlaySurface ?? null,
|
|
196
|
+
navigationSurface: params.observation.navigationSurface ?? null,
|
|
197
|
+
configurationSurface: params.observation.configurationSurface ?? null,
|
|
198
|
+
reasons: [...reasons],
|
|
199
|
+
};
|
|
200
|
+
const mode = evidence.urlMatched
|
|
201
|
+
&& evidence.dialogMatched
|
|
202
|
+
&& evidence.authMatched
|
|
203
|
+
&& evidence.variantMatched
|
|
204
|
+
&& (evidence.surfaceMatched ?? true)
|
|
205
|
+
&& evidence.strongSurfaceObserved
|
|
206
|
+
? "verified"
|
|
207
|
+
: !evidence.authMatched
|
|
208
|
+
? "dirty"
|
|
209
|
+
: "degraded";
|
|
210
|
+
return {
|
|
211
|
+
mode,
|
|
212
|
+
contract,
|
|
213
|
+
evidence,
|
|
214
|
+
reasons,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
export function decideCaptureTransition(params) {
|
|
218
|
+
const contract = deriveCapturePageContract({ pageIdentity: params.pageIdentity });
|
|
219
|
+
if (!params.observation) {
|
|
220
|
+
return {
|
|
221
|
+
mode: "degraded",
|
|
222
|
+
contract,
|
|
223
|
+
evidence: {
|
|
224
|
+
urlMatched: false,
|
|
225
|
+
dialogMatched: contract.dialogPolicy === "allow_either",
|
|
226
|
+
authMatched: true,
|
|
227
|
+
variantMatched: true,
|
|
228
|
+
strongSurfaceObserved: false,
|
|
229
|
+
surfaceMatched: false,
|
|
230
|
+
strongSurfaceSignature: null,
|
|
231
|
+
primarySurface: null,
|
|
232
|
+
overlaySurface: null,
|
|
233
|
+
navigationSurface: null,
|
|
234
|
+
configurationSurface: null,
|
|
235
|
+
reasons: ["baseline observation unavailable"],
|
|
236
|
+
},
|
|
237
|
+
reasons: ["baseline observation unavailable"],
|
|
238
|
+
shouldNavigateToCanonical: true,
|
|
239
|
+
shouldRecreateContext: false,
|
|
240
|
+
allowFastPath: false,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
const checkpointDecision = buildCaptureCheckpointEvidence({
|
|
244
|
+
currentUrl: params.currentUrl ?? params.observation.url,
|
|
245
|
+
targetUrl: params.resumeUrl ?? params.targetUrl,
|
|
246
|
+
pageIdentity: params.pageIdentity,
|
|
247
|
+
observation: params.observation,
|
|
248
|
+
pageSignals: params.pageSignals ?? null,
|
|
249
|
+
profile: params.profile,
|
|
250
|
+
requestedLang: params.requestedLang,
|
|
251
|
+
requestedTheme: params.requestedTheme,
|
|
252
|
+
});
|
|
253
|
+
const reasons = [...checkpointDecision.reasons];
|
|
254
|
+
const handoffTrust = params.handoffContext?.trust ?? "degraded";
|
|
255
|
+
const sameSessionOrigin = sameOrigin(params.currentUrl ?? params.observation.url, params.targetUrl)
|
|
256
|
+
|| sameOrigin(params.currentUrl ?? params.observation.url, params.resumeUrl ?? null);
|
|
257
|
+
const derivedFromRepair = params.handoffContext?.derivedFromRepair === true;
|
|
258
|
+
let mode = checkpointDecision.mode;
|
|
259
|
+
if (params.pageIndex === 0) {
|
|
260
|
+
mode = checkpointDecision.mode === "dirty" ? "dirty" : "degraded";
|
|
261
|
+
reasons.push("first page in variant must rebase canonically before capture");
|
|
262
|
+
}
|
|
263
|
+
else if (derivedFromRepair) {
|
|
264
|
+
mode = checkpointDecision.mode === "dirty" ? "dirty" : "degraded";
|
|
265
|
+
reasons.push("previous page state came from repair/recovery and cannot be reused as trusted handoff");
|
|
266
|
+
}
|
|
267
|
+
else if (handoffTrust !== "verified") {
|
|
268
|
+
mode = checkpointDecision.mode === "dirty" ? "dirty" : "degraded";
|
|
269
|
+
reasons.push("handoff context is not strongly trusted");
|
|
270
|
+
}
|
|
271
|
+
else if (!sameSessionOrigin) {
|
|
272
|
+
mode = "dirty";
|
|
273
|
+
reasons.push("current live state belongs to a different origin");
|
|
274
|
+
}
|
|
275
|
+
else if (checkpointDecision.mode !== "verified") {
|
|
276
|
+
mode = checkpointDecision.mode;
|
|
277
|
+
}
|
|
278
|
+
else if (contract.reusePolicy === "never") {
|
|
279
|
+
mode = "degraded";
|
|
280
|
+
reasons.push("current page contract forbids opportunistic live-state reuse");
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
mode,
|
|
284
|
+
contract,
|
|
285
|
+
evidence: checkpointDecision.evidence,
|
|
286
|
+
reasons,
|
|
287
|
+
shouldNavigateToCanonical: mode === "degraded",
|
|
288
|
+
shouldRecreateContext: mode === "dirty",
|
|
289
|
+
allowFastPath: mode === "verified",
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
//# sourceMappingURL=capture-transition-engine.js.map
|
|
@@ -3,6 +3,7 @@ import type { ScreenshotPageRunInput } from "./capture-run-optimizer.js";
|
|
|
3
3
|
export interface VariantCaptureState {
|
|
4
4
|
expectedPageIds: string[];
|
|
5
5
|
pageIdentities: Record<string, CapturePageIdentity>;
|
|
6
|
+
pagePromptFingerprints: Record<string, string>;
|
|
6
7
|
validatedCaptures: VariantValidatedCapture[];
|
|
7
8
|
captureStatuses: Record<string, VariantCaptureStatus>;
|
|
8
9
|
lastCheckpointId: string | null;
|
|
@@ -10,6 +11,7 @@ export interface VariantCaptureState {
|
|
|
10
11
|
recoveryAttempts: Record<string, number>;
|
|
11
12
|
repairHistory: VariantCaptureRepairRecord[];
|
|
12
13
|
}
|
|
14
|
+
export declare function buildPageRunPromptFingerprint(pageRun: Pick<ScreenshotPageRunInput, "prompt" | "url">, pageIdentity?: CapturePageIdentity | null): string;
|
|
13
15
|
export declare function createVariantCaptureState(pageRuns: ScreenshotPageRunInput[], precomputedIdentities?: Record<string, CapturePageIdentity>): VariantCaptureState;
|
|
14
16
|
export declare function buildVariantManifestContext(params: {
|
|
15
17
|
state: VariantCaptureState;
|
|
@@ -1,15 +1,33 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
1
2
|
import { inferCapturePageIdentity } from "./capture-page-identity.js";
|
|
2
3
|
function normalizePageId(pageId) {
|
|
3
4
|
return pageId ?? "main";
|
|
4
5
|
}
|
|
6
|
+
export function buildPageRunPromptFingerprint(pageRun, pageIdentity) {
|
|
7
|
+
return createHash("sha1")
|
|
8
|
+
.update(pageRun.prompt ?? "")
|
|
9
|
+
.update("\n")
|
|
10
|
+
.update(pageRun.url ?? "")
|
|
11
|
+
.update("\n")
|
|
12
|
+
.update(pageIdentity?.summary ?? "")
|
|
13
|
+
.digest("hex");
|
|
14
|
+
}
|
|
5
15
|
export function createVariantCaptureState(pageRuns, precomputedIdentities) {
|
|
6
16
|
const pageIdentities = precomputedIdentities ?? Object.fromEntries(pageRuns.map((pageRun) => [
|
|
7
17
|
normalizePageId(pageRun.pageId),
|
|
8
18
|
inferCapturePageIdentity(pageRun),
|
|
9
19
|
]));
|
|
20
|
+
const pagePromptFingerprints = Object.fromEntries(pageRuns.map((pageRun) => {
|
|
21
|
+
const pageId = normalizePageId(pageRun.pageId);
|
|
22
|
+
return [
|
|
23
|
+
pageId,
|
|
24
|
+
buildPageRunPromptFingerprint(pageRun, pageIdentities[pageId] ?? null),
|
|
25
|
+
];
|
|
26
|
+
}));
|
|
10
27
|
return {
|
|
11
28
|
expectedPageIds: pageRuns.map((pageRun) => normalizePageId(pageRun.pageId)),
|
|
12
29
|
pageIdentities,
|
|
30
|
+
pagePromptFingerprints,
|
|
13
31
|
validatedCaptures: [],
|
|
14
32
|
captureStatuses: Object.fromEntries(pageRuns.map((pageRun) => [normalizePageId(pageRun.pageId), "pending"])),
|
|
15
33
|
lastCheckpointId: null,
|
|
@@ -25,6 +43,8 @@ export function buildVariantManifestContext(params) {
|
|
|
25
43
|
expectedPageIds: [...params.state.expectedPageIds],
|
|
26
44
|
currentPageId,
|
|
27
45
|
currentPageIdentity: params.state.pageIdentities[currentPageId] ?? null,
|
|
46
|
+
promptFingerprint: params.state.pagePromptFingerprints[currentPageId] ?? null,
|
|
47
|
+
pagePromptFingerprints: { ...params.state.pagePromptFingerprints },
|
|
28
48
|
completedPages,
|
|
29
49
|
remainingPages: params.state.expectedPageIds.filter((pageId) => !completedPages.includes(pageId)),
|
|
30
50
|
previousValidatedCaptures: [...params.state.validatedCaptures],
|
|
@@ -53,6 +73,7 @@ export function recordValidatedVariantCapture(params) {
|
|
|
53
73
|
state: {
|
|
54
74
|
expectedPageIds: [...params.state.expectedPageIds],
|
|
55
75
|
pageIdentities: { ...params.state.pageIdentities },
|
|
76
|
+
pagePromptFingerprints: { ...params.state.pagePromptFingerprints },
|
|
56
77
|
validatedCaptures: nextValidated,
|
|
57
78
|
captureStatuses: {
|
|
58
79
|
...params.state.captureStatuses,
|
|
@@ -106,6 +127,7 @@ export function markVariantCaptureInProgress(state, pageId) {
|
|
|
106
127
|
const normalized = normalizePageId(pageId);
|
|
107
128
|
return {
|
|
108
129
|
...state,
|
|
130
|
+
pagePromptFingerprints: { ...state.pagePromptFingerprints },
|
|
109
131
|
captureStatuses: {
|
|
110
132
|
...state.captureStatuses,
|
|
111
133
|
[normalized]: "in_progress",
|
|
@@ -120,6 +142,7 @@ export function markVariantCaptureBlocked(params) {
|
|
|
120
142
|
const normalized = normalizePageId(params.pageId);
|
|
121
143
|
return {
|
|
122
144
|
...params.state,
|
|
145
|
+
pagePromptFingerprints: { ...params.state.pagePromptFingerprints },
|
|
123
146
|
captureStatuses: {
|
|
124
147
|
...params.state.captureStatuses,
|
|
125
148
|
[normalized]: "blocked",
|
|
@@ -134,6 +157,7 @@ export function recordVariantRecoveryAttempt(state, pageId) {
|
|
|
134
157
|
const normalized = normalizePageId(pageId);
|
|
135
158
|
return {
|
|
136
159
|
...state,
|
|
160
|
+
pagePromptFingerprints: { ...state.pagePromptFingerprints },
|
|
137
161
|
recoveryAttempts: {
|
|
138
162
|
...state.recoveryAttempts,
|
|
139
163
|
[normalized]: (state.recoveryAttempts[normalized] ?? 0) + 1,
|
|
@@ -144,12 +168,14 @@ export function recordVariantRepair(params) {
|
|
|
144
168
|
const nextHistory = [...params.state.repairHistory, params.repair];
|
|
145
169
|
return {
|
|
146
170
|
...params.state,
|
|
171
|
+
pagePromptFingerprints: { ...params.state.pagePromptFingerprints },
|
|
147
172
|
repairHistory: nextHistory.slice(-12),
|
|
148
173
|
};
|
|
149
174
|
}
|
|
150
175
|
export function recordVariantCheckpoint(params) {
|
|
151
176
|
return {
|
|
152
177
|
...params.state,
|
|
178
|
+
pagePromptFingerprints: { ...params.state.pagePromptFingerprints },
|
|
153
179
|
lastCheckpointId: params.checkpoint.id,
|
|
154
180
|
};
|
|
155
181
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture Agent — Capture Verification
|
|
3
|
+
*
|
|
4
|
+
* Post-capture quality check via LLM vision.
|
|
5
|
+
* Detects: blank pages, error pages, loading spinners, skeletons,
|
|
6
|
+
* obstructing overlays, and other visual issues.
|
|
7
|
+
*
|
|
8
|
+
* This is the ONLY place where AutoKap uses AI at runtime.
|
|
9
|
+
* Cost: ~0.002 EUR per check (1 image + short prompt).
|
|
10
|
+
*/
|
|
11
|
+
import { type LLMProviderConfig, type LLMCallResult } from './llm-provider.js';
|
|
12
|
+
export interface VerificationResult {
|
|
13
|
+
/** Whether the screenshot is acceptable for delivery */
|
|
14
|
+
passed: boolean;
|
|
15
|
+
/** Issue detected (if failed) */
|
|
16
|
+
issue?: 'blank_page' | 'error_page' | 'loading' | 'overlay_blocking' | 'wrong_content' | 'other';
|
|
17
|
+
/** Human-readable explanation */
|
|
18
|
+
reason: string;
|
|
19
|
+
/** LLM call metadata */
|
|
20
|
+
llmResult?: LLMCallResult;
|
|
21
|
+
}
|
|
22
|
+
export interface VerificationContext {
|
|
23
|
+
/** What was this screenshot supposed to show? */
|
|
24
|
+
expectedDescription: string;
|
|
25
|
+
/** Target URL */
|
|
26
|
+
url: string;
|
|
27
|
+
/** Locale of this variant */
|
|
28
|
+
locale?: string;
|
|
29
|
+
/** Theme of this variant */
|
|
30
|
+
theme?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Verifies a captured screenshot is clean and ready for delivery.
|
|
34
|
+
*/
|
|
35
|
+
export declare function verifyCaptureQuality(screenshot: Buffer, context: VerificationContext, llmConfig: LLMProviderConfig): Promise<VerificationResult>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture Agent — Capture Verification
|
|
3
|
+
*
|
|
4
|
+
* Post-capture quality check via LLM vision.
|
|
5
|
+
* Detects: blank pages, error pages, loading spinners, skeletons,
|
|
6
|
+
* obstructing overlays, and other visual issues.
|
|
7
|
+
*
|
|
8
|
+
* This is the ONLY place where AutoKap uses AI at runtime.
|
|
9
|
+
* Cost: ~0.002 EUR per check (1 image + short prompt).
|
|
10
|
+
*/
|
|
11
|
+
import { callLLM } from './llm-provider.js';
|
|
12
|
+
const SYSTEM_PROMPT = `You are a screenshot quality checker for a web capture tool. You receive a screenshot of a web page and must verify it is clean and ready for delivery.
|
|
13
|
+
|
|
14
|
+
Check for these issues:
|
|
15
|
+
1. BLANK - The page is completely white/blank or has no meaningful content
|
|
16
|
+
2. ERROR - The page shows an error message (404, 500, "something went wrong", crash screen)
|
|
17
|
+
3. LOADING - The page has visible loading indicators (spinners, skeletons, progress bars, "Loading..." text)
|
|
18
|
+
4. OVERLAY - A modal, popup, cookie banner, or other overlay is blocking the main content
|
|
19
|
+
5. WRONG - The content doesn't match what was expected
|
|
20
|
+
|
|
21
|
+
Respond with EXACTLY one line in this format:
|
|
22
|
+
PASS: Screenshot looks clean and ready.
|
|
23
|
+
or
|
|
24
|
+
FAIL:<issue>: <explanation>
|
|
25
|
+
|
|
26
|
+
Where <issue> is one of: blank_page, error_page, loading, overlay_blocking, wrong_content, other
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
PASS: Screenshot looks clean and ready.
|
|
30
|
+
FAIL:loading: A skeleton loader is visible in the main content area.
|
|
31
|
+
FAIL:overlay_blocking: A cookie consent banner covers the bottom of the page.
|
|
32
|
+
FAIL:error_page: The page shows a 404 Not Found error.`;
|
|
33
|
+
/**
|
|
34
|
+
* Verifies a captured screenshot is clean and ready for delivery.
|
|
35
|
+
*/
|
|
36
|
+
export async function verifyCaptureQuality(screenshot, context, llmConfig) {
|
|
37
|
+
const userPrompt = buildUserPrompt(context);
|
|
38
|
+
try {
|
|
39
|
+
const result = await callLLM(llmConfig, SYSTEM_PROMPT, userPrompt, screenshot);
|
|
40
|
+
return parseVerificationResponse(result.text, result);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
// If LLM fails, pass the screenshot — don't block delivery because of LLM issues
|
|
44
|
+
return {
|
|
45
|
+
passed: true,
|
|
46
|
+
reason: `verification skipped (LLM error: ${err instanceof Error ? err.message : String(err)})`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function buildUserPrompt(context) {
|
|
51
|
+
const parts = [
|
|
52
|
+
`Expected content: ${context.expectedDescription}`,
|
|
53
|
+
`URL: ${context.url}`,
|
|
54
|
+
];
|
|
55
|
+
if (context.locale)
|
|
56
|
+
parts.push(`Locale: ${context.locale}`);
|
|
57
|
+
if (context.theme)
|
|
58
|
+
parts.push(`Theme: ${context.theme}`);
|
|
59
|
+
parts.push('\nIs this screenshot clean and ready for delivery?');
|
|
60
|
+
return parts.join('\n');
|
|
61
|
+
}
|
|
62
|
+
function parseVerificationResponse(text, llmResult) {
|
|
63
|
+
const trimmed = text.trim();
|
|
64
|
+
if (trimmed.startsWith('PASS')) {
|
|
65
|
+
return {
|
|
66
|
+
passed: true,
|
|
67
|
+
reason: trimmed.replace(/^PASS:?\s*/, ''),
|
|
68
|
+
llmResult,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (trimmed.startsWith('FAIL')) {
|
|
72
|
+
const match = trimmed.match(/^FAIL:(\w+):\s*(.+)/);
|
|
73
|
+
if (match) {
|
|
74
|
+
return {
|
|
75
|
+
passed: false,
|
|
76
|
+
issue: match[1],
|
|
77
|
+
reason: match[2],
|
|
78
|
+
llmResult,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
passed: false,
|
|
83
|
+
issue: 'other',
|
|
84
|
+
reason: trimmed.replace(/^FAIL:?\s*/, ''),
|
|
85
|
+
llmResult,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// Ambiguous response — assume pass
|
|
89
|
+
return {
|
|
90
|
+
passed: true,
|
|
91
|
+
reason: `ambiguous LLM response: "${trimmed.slice(0, 100)}"`,
|
|
92
|
+
llmResult,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=capture-verification.js.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface CaptureViewport {
|
|
2
|
+
width: number;
|
|
3
|
+
height: number;
|
|
4
|
+
}
|
|
5
|
+
export interface ViewportLockBrowser {
|
|
6
|
+
resizeViewport(width: number, height: number): Promise<void>;
|
|
7
|
+
wait(ms: number): Promise<void>;
|
|
8
|
+
takeScreenshot(): Promise<Buffer>;
|
|
9
|
+
currentPage: {
|
|
10
|
+
viewportSize(): {
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
} | null;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export interface LockedViewportScreenshot {
|
|
17
|
+
buffer: Buffer;
|
|
18
|
+
actualViewport: {
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
} | null;
|
|
22
|
+
screenshotSize: {
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
};
|
|
26
|
+
expectedScreenshotSize: {
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
};
|
|
30
|
+
repaired: boolean;
|
|
31
|
+
}
|
|
32
|
+
interface ViewportLockOptions {
|
|
33
|
+
settleMs?: number;
|
|
34
|
+
maxAttempts?: number;
|
|
35
|
+
}
|
|
36
|
+
interface TakeViewportLockedScreenshotOptions extends ViewportLockOptions {
|
|
37
|
+
deviceScaleFactor: number;
|
|
38
|
+
maxScreenshotAttempts?: number;
|
|
39
|
+
}
|
|
40
|
+
export declare function lockCaptureViewport(browser: ViewportLockBrowser, viewport: CaptureViewport, options?: ViewportLockOptions): Promise<{
|
|
41
|
+
actualViewport: {
|
|
42
|
+
width: number;
|
|
43
|
+
height: number;
|
|
44
|
+
} | null;
|
|
45
|
+
repaired: boolean;
|
|
46
|
+
}>;
|
|
47
|
+
export declare function takeViewportLockedScreenshot(browser: ViewportLockBrowser, viewport: CaptureViewport, options: TakeViewportLockedScreenshotOptions): Promise<LockedViewportScreenshot>;
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
const DEFAULT_SETTLE_MS = 300;
|
|
3
|
+
const DEFAULT_MAX_VIEWPORT_ATTEMPTS = 2;
|
|
4
|
+
const DEFAULT_MAX_SCREENSHOT_ATTEMPTS = 2;
|
|
5
|
+
function matchesDimension(actual, expected) {
|
|
6
|
+
return Math.abs(actual - expected) <= 1;
|
|
7
|
+
}
|
|
8
|
+
function matchesViewport(actual, expected) {
|
|
9
|
+
return !!actual
|
|
10
|
+
&& matchesDimension(actual.width, expected.width)
|
|
11
|
+
&& matchesDimension(actual.height, expected.height);
|
|
12
|
+
}
|
|
13
|
+
function formatViewport(value) {
|
|
14
|
+
if (!value)
|
|
15
|
+
return 'unknown';
|
|
16
|
+
return `${value.width}x${value.height}`;
|
|
17
|
+
}
|
|
18
|
+
export async function lockCaptureViewport(browser, viewport, options = {}) {
|
|
19
|
+
const settleMs = options.settleMs ?? DEFAULT_SETTLE_MS;
|
|
20
|
+
const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_VIEWPORT_ATTEMPTS;
|
|
21
|
+
let repaired = false;
|
|
22
|
+
let actualViewport = browser.currentPage.viewportSize();
|
|
23
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
24
|
+
await browser.resizeViewport(viewport.width, viewport.height);
|
|
25
|
+
if (settleMs > 0) {
|
|
26
|
+
await browser.wait(settleMs);
|
|
27
|
+
}
|
|
28
|
+
actualViewport = browser.currentPage.viewportSize();
|
|
29
|
+
if (matchesViewport(actualViewport, viewport)) {
|
|
30
|
+
return {
|
|
31
|
+
actualViewport,
|
|
32
|
+
repaired,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
repaired = true;
|
|
36
|
+
}
|
|
37
|
+
throw new Error(`Viewport lock failed: expected ${viewport.width}x${viewport.height}, got ${formatViewport(actualViewport)}.`);
|
|
38
|
+
}
|
|
39
|
+
export async function takeViewportLockedScreenshot(browser, viewport, options) {
|
|
40
|
+
const deviceScaleFactor = Math.max(0.5, Number(options.deviceScaleFactor) || 1);
|
|
41
|
+
const maxScreenshotAttempts = options.maxScreenshotAttempts ?? DEFAULT_MAX_SCREENSHOT_ATTEMPTS;
|
|
42
|
+
const expectedScreenshotSize = {
|
|
43
|
+
width: Math.round(viewport.width * deviceScaleFactor),
|
|
44
|
+
height: Math.round(viewport.height * deviceScaleFactor),
|
|
45
|
+
};
|
|
46
|
+
let repaired = false;
|
|
47
|
+
let lastViewport = null;
|
|
48
|
+
let lastScreenshotSize = null;
|
|
49
|
+
for (let attempt = 1; attempt <= maxScreenshotAttempts; attempt += 1) {
|
|
50
|
+
const viewportLock = await lockCaptureViewport(browser, viewport, options);
|
|
51
|
+
repaired ||= viewportLock.repaired;
|
|
52
|
+
lastViewport = viewportLock.actualViewport;
|
|
53
|
+
const buffer = await browser.takeScreenshot();
|
|
54
|
+
const metadata = await sharp(buffer).metadata();
|
|
55
|
+
const screenshotSize = {
|
|
56
|
+
width: metadata.width ?? 0,
|
|
57
|
+
height: metadata.height ?? 0,
|
|
58
|
+
};
|
|
59
|
+
lastScreenshotSize = screenshotSize;
|
|
60
|
+
if (matchesDimension(screenshotSize.width, expectedScreenshotSize.width)
|
|
61
|
+
&& matchesDimension(screenshotSize.height, expectedScreenshotSize.height)) {
|
|
62
|
+
return {
|
|
63
|
+
buffer,
|
|
64
|
+
actualViewport: lastViewport,
|
|
65
|
+
screenshotSize,
|
|
66
|
+
expectedScreenshotSize,
|
|
67
|
+
repaired: repaired || attempt > 1,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
repaired = true;
|
|
71
|
+
}
|
|
72
|
+
throw new Error(`Screenshot viewport mismatch: expected ${expectedScreenshotSize.width}x${expectedScreenshotSize.height}, got ${formatViewport(lastScreenshotSize)} (viewport ${formatViewport(lastViewport)}).`);
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=capture-viewport-lock.js.map
|