autokap 1.0.2 → 1.0.3
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/dist/cli-config.d.ts +13 -0
- package/dist/cli-config.js +42 -0
- package/dist/cli-utils.d.ts +0 -19
- package/dist/cli-utils.js +2 -65
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +266 -305
- package/package.json +23 -16
- 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 +0 -52
- package/assets/devices/iphone-16-pro.json +0 -53
- package/assets/devices/macbook-air-13.json +0 -45
- package/assets/frames/MacBook Air 13.svg +0 -242
- package/assets/frames/Status bar - iPhone.png +0 -0
- package/assets/frames/Status bar and 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 +0 -3
- package/assets/icons/Union.svg +0 -6
- package/assets/icons/Wifi.svg +0 -3
- package/assets/icons/battery.svg +0 -5
- package/assets/icons/battery_charging.svg +0 -8
- package/dist/abort.d.ts +0 -5
- package/dist/abort.js +0 -44
- package/dist/agent.d.ts +0 -142
- package/dist/agent.js +0 -4504
- package/dist/browser-bar.d.ts +0 -40
- package/dist/browser-bar.js +0 -147
- package/dist/clip-orchestrator.d.ts +0 -148
- package/dist/clip-orchestrator.js +0 -950
- package/dist/clip-postprocess.d.ts +0 -42
- package/dist/clip-postprocess.js +0 -192
- package/dist/credential-templates.d.ts +0 -5
- package/dist/credential-templates.js +0 -60
- package/dist/element-capture.d.ts +0 -53
- package/dist/element-capture.js +0 -766
- package/dist/hybrid-navigator.d.ts +0 -138
- package/dist/hybrid-navigator.js +0 -468
- package/dist/index.d.ts +0 -15
- package/dist/index.js +0 -11
- package/dist/llm-usage.d.ts +0 -17
- package/dist/llm-usage.js +0 -45
- package/dist/mockup-html.d.ts +0 -119
- package/dist/mockup-html.js +0 -253
- package/dist/mockup.d.ts +0 -94
- package/dist/mockup.js +0 -604
- package/dist/mouse-animation.d.ts +0 -46
- package/dist/mouse-animation.js +0 -100
- package/dist/overlay-utils.d.ts +0 -14
- package/dist/overlay-utils.js +0 -13
- package/dist/posthog.d.ts +0 -4
- package/dist/posthog.js +0 -26
- package/dist/prompt-cache.d.ts +0 -10
- package/dist/prompt-cache.js +0 -24
- package/dist/prompts.d.ts +0 -167
- package/dist/prompts.js +0 -1165
- package/dist/security.d.ts +0 -20
- package/dist/security.js +0 -569
- package/dist/session-profile.d.ts +0 -86
- package/dist/session-profile.js +0 -1471
- package/dist/sf-pro-fonts.d.ts +0 -4
- package/dist/sf-pro-fonts.js +0 -7
- package/dist/status-bar-l10n.d.ts +0 -14
- package/dist/status-bar-l10n.js +0 -177
- package/dist/status-bar.d.ts +0 -44
- package/dist/status-bar.js +0 -336
- package/dist/tools.d.ts +0 -4
- package/dist/tools.js +0 -578
- package/dist/video-agent.d.ts +0 -143
- package/dist/video-agent.js +0 -4783
- package/dist/video-observation.d.ts +0 -36
- package/dist/video-observation.js +0 -192
- package/dist/video-planner.d.ts +0 -12
- package/dist/video-planner.js +0 -500
- package/dist/video-prompts.d.ts +0 -37
- package/dist/video-prompts.js +0 -554
- package/dist/video-tools.d.ts +0 -3
- package/dist/video-tools.js +0 -59
- package/dist/video-variant-state.d.ts +0 -29
- package/dist/video-variant-state.js +0 -80
- package/dist/vision-model.d.ts +0 -17
- package/dist/vision-model.js +0 -74
package/dist/session-profile.js
DELETED
|
@@ -1,1471 +0,0 @@
|
|
|
1
|
-
const EMAIL_RE = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i;
|
|
2
|
-
const LOGIN_URL_RE = /\/(login|signin|sign-in|auth|session|connexion|connect(?:ion)?)\b/i;
|
|
3
|
-
const LOGIN_BUTTON_RE = /\b(sign[\s-]?in|log[\s-]?in|continue with email|connexion|se connecter|login)\b/i;
|
|
4
|
-
const LOGOUT_BUTTON_RE = /\b(sign[\s-]?out|log[\s-]?out|logout|d[eé]connexion|se d[eé]connecter)\b/i;
|
|
5
|
-
const ACCOUNT_MENU_RE = /\b(account|profile|profil|compte|workspace|organization|organisation|team|billing|settings)\b/i;
|
|
6
|
-
const PERSON_NAME_RE = /^[A-ZÀ-Ý][A-Za-zÀ-ÿ'’-]{1,20}(?:\s+[A-ZÀ-Ý][A-Za-zÀ-ÿ'’-]{1,20}){1,2}$/;
|
|
7
|
-
const CORPORATE_SUFFIX_RE = /\b(inc|llc|ltd|corp|gmbh|sas|sarl|company|studio|agency)\b/i;
|
|
8
|
-
const LANG_CONTROL_RE = /\b(lang|locale|language|idioma|sprache|langue|lingua)\b/i;
|
|
9
|
-
const THEME_CONTROL_RE = /\b(theme|appearance|mode|dark|light|clair|sombre|oscuro|claro)\b/i;
|
|
10
|
-
const LANGUAGE_ALIASES = {
|
|
11
|
-
ar: ['ar', 'arabic', 'arab', 'العربية'],
|
|
12
|
-
cs: ['cs', 'czech', 'cesky', 'čeština'],
|
|
13
|
-
da: ['da', 'danish', 'dansk'],
|
|
14
|
-
de: ['de', 'german', 'deutsch'],
|
|
15
|
-
en: ['en', 'english'],
|
|
16
|
-
es: ['es', 'spanish', 'espanol', 'español'],
|
|
17
|
-
fi: ['fi', 'finnish', 'suomi'],
|
|
18
|
-
fr: ['fr', 'french', 'francais', 'français'],
|
|
19
|
-
hu: ['hu', 'hungarian', 'magyar'],
|
|
20
|
-
it: ['it', 'italian', 'italiano'],
|
|
21
|
-
ja: ['ja', 'japanese', 'japonais', '日本語'],
|
|
22
|
-
ko: ['ko', 'korean', '한국어'],
|
|
23
|
-
nl: ['nl', 'dutch', 'nederlands'],
|
|
24
|
-
no: ['no', 'nb', 'norwegian', 'norsk'],
|
|
25
|
-
pl: ['pl', 'polish', 'polski'],
|
|
26
|
-
pt: ['pt', 'portuguese', 'portugues', 'português'],
|
|
27
|
-
ru: ['ru', 'russian', 'русский'],
|
|
28
|
-
sv: ['sv', 'swedish', 'svenska'],
|
|
29
|
-
tr: ['tr', 'turkish', 'turkce', 'türkçe'],
|
|
30
|
-
zh: ['zh', 'chinese', '中文'],
|
|
31
|
-
};
|
|
32
|
-
function makeDiagnostic(outcome, confidence, reasons) {
|
|
33
|
-
return {
|
|
34
|
-
outcome,
|
|
35
|
-
confidence,
|
|
36
|
-
reasons: reasons.filter((reason) => !!reason),
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
function confidenceFromScore(score, options) {
|
|
40
|
-
if (options.highSignal || score >= options.highAt)
|
|
41
|
-
return 'high';
|
|
42
|
-
if (score >= options.mediumAt)
|
|
43
|
-
return 'medium';
|
|
44
|
-
return 'low';
|
|
45
|
-
}
|
|
46
|
-
const FIXED_UI_LANGUAGE_PATTERNS = {
|
|
47
|
-
en: [
|
|
48
|
-
/\b(settings|dashboard|search|save|cancel|continue|projects?|captures?|gallery|account|profile|billing|language|theme|edit|new)\b/i,
|
|
49
|
-
/\b(sign in|sign out|screenshots?|templates?|navigation|overview)\b/i,
|
|
50
|
-
],
|
|
51
|
-
fr: [
|
|
52
|
-
/\b(param[eè]tres|tableau de bord|rechercher|enregistrer|annuler|continuer|projets?|captures?|galerie|compte|profil|facturation|langue|th[eè]me|modifier|nouveau)\b/i,
|
|
53
|
-
/\b(se connecter|d[eé]connexion|captures d[' ]ecran|mod[eè]les|navigation|aper[uç]u)\b/i,
|
|
54
|
-
],
|
|
55
|
-
de: [
|
|
56
|
-
/\b(einstellungen|dashboard|suche|speichern|abbrechen|weiter|projekte?|galerie|konto|profil|abrechnung|sprache|thema|bearbeiten|neu)\b/i,
|
|
57
|
-
/\b(anmelden|abmelden|vorlage|navigation|ubersicht)\b/i,
|
|
58
|
-
],
|
|
59
|
-
es: [
|
|
60
|
-
/\b(configuraci[oó]n|panel|buscar|guardar|cancelar|continuar|proyectos?|capturas?|galer[ií]a|cuenta|perfil|facturaci[oó]n|idioma|tema|editar|nuevo)\b/i,
|
|
61
|
-
/\b(iniciar sesi[oó]n|cerrar sesi[oó]n|plantillas|navegaci[oó]n|resumen)\b/i,
|
|
62
|
-
],
|
|
63
|
-
it: [
|
|
64
|
-
/\b(impostazioni|dashboard|cerca|salva|annulla|continua|progetti?|schermate|galleria|account|profilo|fatturazione|lingua|tema|modifica|nuovo)\b/i,
|
|
65
|
-
/\b(accedi|disconnetti|modelli|navigazione|panoramica)\b/i,
|
|
66
|
-
],
|
|
67
|
-
pt: [
|
|
68
|
-
/\b(configura[cç][aã]o|painel|pesquisar|salvar|cancelar|continuar|projetos?|capturas?|galeria|conta|perfil|cobran[cç]a|idioma|tema|editar|novo)\b/i,
|
|
69
|
-
/\b(entrar|sair|modelos|navega[cç][aã]o|vis[aã]o geral)\b/i,
|
|
70
|
-
],
|
|
71
|
-
};
|
|
72
|
-
function normalizeLangCode(value) {
|
|
73
|
-
if (!value)
|
|
74
|
-
return null;
|
|
75
|
-
return value.trim().toLowerCase().replace('_', '-').split('-')[0] || null;
|
|
76
|
-
}
|
|
77
|
-
function normalizeSearchText(value) {
|
|
78
|
-
return (value ?? '')
|
|
79
|
-
.normalize('NFD')
|
|
80
|
-
.replace(/[\u0300-\u036f]/g, '')
|
|
81
|
-
.toLowerCase();
|
|
82
|
-
}
|
|
83
|
-
function escapeRegExp(value) {
|
|
84
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
85
|
-
}
|
|
86
|
-
function truncate(value, max = 180) {
|
|
87
|
-
return value.length > max ? `${value.slice(0, max - 1)}...` : value;
|
|
88
|
-
}
|
|
89
|
-
function looksLikeAccountText(value) {
|
|
90
|
-
const normalized = (value ?? '').replace(/\s+/g, ' ').trim();
|
|
91
|
-
if (!normalized)
|
|
92
|
-
return false;
|
|
93
|
-
if (LOGIN_BUTTON_RE.test(normalized) || LOGOUT_BUTTON_RE.test(normalized))
|
|
94
|
-
return false;
|
|
95
|
-
if (EMAIL_RE.test(normalized))
|
|
96
|
-
return true;
|
|
97
|
-
return PERSON_NAME_RE.test(normalized) && !CORPORATE_SUFFIX_RE.test(normalized);
|
|
98
|
-
}
|
|
99
|
-
function extractAccountLabel(signals) {
|
|
100
|
-
const hintedAccount = signals.authHints?.accountLikeText.find((value) => looksLikeAccountText(value));
|
|
101
|
-
if (hintedAccount)
|
|
102
|
-
return hintedAccount;
|
|
103
|
-
const sources = [
|
|
104
|
-
signals.visibleText,
|
|
105
|
-
signals.title,
|
|
106
|
-
...signals.navLabels,
|
|
107
|
-
...signals.breadcrumbLabels,
|
|
108
|
-
];
|
|
109
|
-
for (const source of sources) {
|
|
110
|
-
const match = source.match(EMAIL_RE);
|
|
111
|
-
if (match)
|
|
112
|
-
return match[0];
|
|
113
|
-
}
|
|
114
|
-
const nameCandidate = sources.find((source) => looksLikeAccountText(source));
|
|
115
|
-
if (nameCandidate)
|
|
116
|
-
return nameCandidate.replace(/\s+/g, ' ').trim();
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
119
|
-
function textContainsLanguageAlias(value, lang) {
|
|
120
|
-
const haystack = normalizeSearchText(value);
|
|
121
|
-
if (!haystack)
|
|
122
|
-
return false;
|
|
123
|
-
const aliases = LANGUAGE_ALIASES[lang] ?? [lang];
|
|
124
|
-
return aliases.some((alias) => {
|
|
125
|
-
const normalizedAlias = normalizeSearchText(alias);
|
|
126
|
-
if (!normalizedAlias)
|
|
127
|
-
return false;
|
|
128
|
-
if (/^[a-z]{2,3}$/.test(normalizedAlias)) {
|
|
129
|
-
return new RegExp(`(^|[^a-z])${escapeRegExp(normalizedAlias)}([^a-z]|$)`, 'i').test(haystack);
|
|
130
|
-
}
|
|
131
|
-
return haystack.includes(normalizedAlias);
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
function textContainsThemeAlias(value, theme) {
|
|
135
|
-
const haystack = normalizeSearchText(value);
|
|
136
|
-
if (!haystack)
|
|
137
|
-
return false;
|
|
138
|
-
const aliases = theme === 'dark'
|
|
139
|
-
? ['dark', 'sombre', 'oscuro', 'dunkel']
|
|
140
|
-
: ['light', 'clair', 'claro', 'hell'];
|
|
141
|
-
return aliases.some((alias) => haystack.includes(alias));
|
|
142
|
-
}
|
|
143
|
-
function parseMaybeUrl(rawUrl) {
|
|
144
|
-
if (!rawUrl)
|
|
145
|
-
return null;
|
|
146
|
-
try {
|
|
147
|
-
return new URL(rawUrl);
|
|
148
|
-
}
|
|
149
|
-
catch {
|
|
150
|
-
return null;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
function urlSuggestsLanguage(rawUrl, lang) {
|
|
154
|
-
const url = parseMaybeUrl(rawUrl);
|
|
155
|
-
if (!url)
|
|
156
|
-
return false;
|
|
157
|
-
if (normalizeLangCode(url.hostname.split('.')[0]) === lang) {
|
|
158
|
-
return true;
|
|
159
|
-
}
|
|
160
|
-
for (const segment of url.pathname.split('/')) {
|
|
161
|
-
if (normalizeLangCode(segment) === lang || textContainsLanguageAlias(segment, lang)) {
|
|
162
|
-
return true;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
for (const key of ['lang', 'hl', 'locale']) {
|
|
166
|
-
const value = url.searchParams.get(key);
|
|
167
|
-
if (normalizeLangCode(value) === lang || textContainsLanguageAlias(value, lang)) {
|
|
168
|
-
return true;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return false;
|
|
172
|
-
}
|
|
173
|
-
function parseHreflangCode(entry) {
|
|
174
|
-
const separatorIndex = entry.indexOf(':');
|
|
175
|
-
if (separatorIndex > 0) {
|
|
176
|
-
return normalizeLangCode(entry.slice(0, separatorIndex));
|
|
177
|
-
}
|
|
178
|
-
return normalizeLangCode(entry);
|
|
179
|
-
}
|
|
180
|
-
function normalizePathSegments(pathname) {
|
|
181
|
-
return pathname
|
|
182
|
-
.split('/')
|
|
183
|
-
.map((segment) => segment.trim().toLowerCase())
|
|
184
|
-
.filter(Boolean);
|
|
185
|
-
}
|
|
186
|
-
function stripLeadingLocaleSegment(segments) {
|
|
187
|
-
if (segments.length === 0)
|
|
188
|
-
return segments;
|
|
189
|
-
return normalizeLangCode(segments[0]) ? segments.slice(1) : segments;
|
|
190
|
-
}
|
|
191
|
-
function pathStartsWithSegments(current, expected) {
|
|
192
|
-
if (expected.length === 0)
|
|
193
|
-
return true;
|
|
194
|
-
if (current.length < expected.length)
|
|
195
|
-
return false;
|
|
196
|
-
return expected.every((segment, index) => current[index] === segment);
|
|
197
|
-
}
|
|
198
|
-
function pathsRoughlyMatch(currentPathname, expectedPathname) {
|
|
199
|
-
const currentSegments = normalizePathSegments(currentPathname);
|
|
200
|
-
const expectedSegments = normalizePathSegments(expectedPathname);
|
|
201
|
-
if (pathStartsWithSegments(currentSegments, expectedSegments)) {
|
|
202
|
-
return true;
|
|
203
|
-
}
|
|
204
|
-
return pathStartsWithSegments(stripLeadingLocaleSegment(currentSegments), stripLeadingLocaleSegment(expectedSegments));
|
|
205
|
-
}
|
|
206
|
-
function extractLangCandidates(signals, currentUrl) {
|
|
207
|
-
const candidates = new Set();
|
|
208
|
-
const push = (value) => {
|
|
209
|
-
const normalized = normalizeLangCode(value);
|
|
210
|
-
if (normalized)
|
|
211
|
-
candidates.add(normalized);
|
|
212
|
-
};
|
|
213
|
-
push(signals.htmlLang);
|
|
214
|
-
signals.hreflangs.forEach((entry) => push(parseHreflangCode(entry) ?? entry));
|
|
215
|
-
signals.localeHints.forEach((hint) => {
|
|
216
|
-
const normalizedHint = normalizeLangCode(hint);
|
|
217
|
-
if (normalizedHint) {
|
|
218
|
-
candidates.add(normalizedHint);
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
const lowerHint = hint.toLowerCase();
|
|
222
|
-
for (const [code, aliases] of Object.entries(LANGUAGE_ALIASES)) {
|
|
223
|
-
if (aliases.some((alias) => lowerHint.includes(alias))) {
|
|
224
|
-
candidates.add(code);
|
|
225
|
-
break;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
try {
|
|
230
|
-
const url = new URL(currentUrl);
|
|
231
|
-
push(url.hostname.split('.')[0]);
|
|
232
|
-
for (const segment of url.pathname.split('/')) {
|
|
233
|
-
push(segment);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
catch {
|
|
237
|
-
// Ignore invalid URLs.
|
|
238
|
-
}
|
|
239
|
-
return Array.from(candidates);
|
|
240
|
-
}
|
|
241
|
-
function collectFixedChromeText(signals) {
|
|
242
|
-
const localeControlText = signals.variantControls
|
|
243
|
-
.filter((control) => control.kind === 'locale')
|
|
244
|
-
.flatMap((control) => [
|
|
245
|
-
control.label,
|
|
246
|
-
control.value ?? '',
|
|
247
|
-
...(control.options ?? [])
|
|
248
|
-
.filter((option) => option.selected)
|
|
249
|
-
.flatMap((option) => [option.label, option.value ?? '']),
|
|
250
|
-
]);
|
|
251
|
-
return [
|
|
252
|
-
...signals.navLabels,
|
|
253
|
-
...signals.breadcrumbLabels,
|
|
254
|
-
...signals.headings,
|
|
255
|
-
...localeControlText,
|
|
256
|
-
]
|
|
257
|
-
.filter(Boolean)
|
|
258
|
-
.join(' ');
|
|
259
|
-
}
|
|
260
|
-
function scoreFixedChromeText(signals, lang) {
|
|
261
|
-
const patterns = FIXED_UI_LANGUAGE_PATTERNS[lang];
|
|
262
|
-
if (!patterns || patterns.length === 0) {
|
|
263
|
-
return { lang, score: 0, reasons: [] };
|
|
264
|
-
}
|
|
265
|
-
const text = collectFixedChromeText(signals);
|
|
266
|
-
if (!text.trim()) {
|
|
267
|
-
return { lang, score: 0, reasons: [] };
|
|
268
|
-
}
|
|
269
|
-
const hits = patterns.filter((pattern) => pattern.test(text)).length;
|
|
270
|
-
if (hits === 0) {
|
|
271
|
-
return { lang, score: 0, reasons: [] };
|
|
272
|
-
}
|
|
273
|
-
const normalizedScore = Math.min(1, hits / patterns.length);
|
|
274
|
-
return {
|
|
275
|
-
lang,
|
|
276
|
-
score: normalizedScore * 3.6,
|
|
277
|
-
reasons: [`fixed_ui_text=${lang}(${normalizedScore.toFixed(2)})`],
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
function scoreLocaleControls(signals, lang) {
|
|
281
|
-
const localeControls = signals.variantControls.filter((control) => control.kind === 'locale');
|
|
282
|
-
if (localeControls.length === 0) {
|
|
283
|
-
return { lang, score: 0, reasons: [] };
|
|
284
|
-
}
|
|
285
|
-
let score = 0;
|
|
286
|
-
const reasons = [];
|
|
287
|
-
for (const control of localeControls) {
|
|
288
|
-
const selectedTexts = [
|
|
289
|
-
control.value ?? '',
|
|
290
|
-
...(control.options ?? [])
|
|
291
|
-
.filter((option) => option.selected)
|
|
292
|
-
.flatMap((option) => [option.label, option.value ?? '']),
|
|
293
|
-
].filter(Boolean);
|
|
294
|
-
if (selectedTexts.some((value) => textContainsLanguageAlias(value, lang))) {
|
|
295
|
-
score = Math.max(score, 2.0);
|
|
296
|
-
reasons.splice(0, reasons.length, `locale_control_active=${lang}`);
|
|
297
|
-
continue;
|
|
298
|
-
}
|
|
299
|
-
if (textContainsLanguageAlias(control.label, lang)) {
|
|
300
|
-
score = Math.max(score, 1.0);
|
|
301
|
-
if (reasons.length === 0) {
|
|
302
|
-
reasons.push(`locale_control_label=${lang}`);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
return { lang, score, reasons };
|
|
307
|
-
}
|
|
308
|
-
function scoreLanguageCandidate(params) {
|
|
309
|
-
const { lang, currentUrl, signals } = params;
|
|
310
|
-
let score = 0;
|
|
311
|
-
const reasons = [];
|
|
312
|
-
if (normalizeLangCode(signals.htmlLang) === lang) {
|
|
313
|
-
score += 2.8;
|
|
314
|
-
reasons.push(`html_lang=${signals.htmlLang}`);
|
|
315
|
-
}
|
|
316
|
-
if (urlSuggestsLanguage(currentUrl, lang)) {
|
|
317
|
-
score += 2.4;
|
|
318
|
-
reasons.push(`url=${lang}`);
|
|
319
|
-
}
|
|
320
|
-
else if (urlSuggestsLanguage(signals.canonicalUrl, lang)) {
|
|
321
|
-
score += 1.2;
|
|
322
|
-
reasons.push(`canonical_url=${lang}`);
|
|
323
|
-
}
|
|
324
|
-
if (signals.hreflangs.some((entry) => parseHreflangCode(entry) === lang)) {
|
|
325
|
-
score += 1.6;
|
|
326
|
-
reasons.push(`hreflang=${lang}`);
|
|
327
|
-
}
|
|
328
|
-
if (signals.localeHints.some((hint) => normalizeLangCode(hint) === lang || textContainsLanguageAlias(hint, lang))) {
|
|
329
|
-
score += 1.3;
|
|
330
|
-
reasons.push(`locale_hint=${lang}`);
|
|
331
|
-
}
|
|
332
|
-
const localeControlScore = scoreLocaleControls(signals, lang);
|
|
333
|
-
score += localeControlScore.score;
|
|
334
|
-
reasons.push(...localeControlScore.reasons);
|
|
335
|
-
const textScore = scoreFixedChromeText(signals, lang);
|
|
336
|
-
score += textScore.score;
|
|
337
|
-
reasons.push(...textScore.reasons);
|
|
338
|
-
return { lang, score, reasons };
|
|
339
|
-
}
|
|
340
|
-
export function evaluateRequestedLanguageState(params) {
|
|
341
|
-
const requested = normalizeLangCode(params.requestedLang);
|
|
342
|
-
const candidateLangs = new Set();
|
|
343
|
-
if (requested) {
|
|
344
|
-
candidateLangs.add(requested);
|
|
345
|
-
}
|
|
346
|
-
for (const lang of Object.keys(FIXED_UI_LANGUAGE_PATTERNS)) {
|
|
347
|
-
candidateLangs.add(lang);
|
|
348
|
-
}
|
|
349
|
-
for (const lang of extractLangCandidates(params.signals, params.currentUrl)) {
|
|
350
|
-
candidateLangs.add(lang);
|
|
351
|
-
}
|
|
352
|
-
for (const lang of Object.keys(LANGUAGE_ALIASES)) {
|
|
353
|
-
if (params.signals.localeHints.some((hint) => textContainsLanguageAlias(hint, lang) || normalizeLangCode(hint) === lang)) {
|
|
354
|
-
candidateLangs.add(lang);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
const ranked = Array.from(candidateLangs)
|
|
358
|
-
.map((lang) => scoreLanguageCandidate({
|
|
359
|
-
lang,
|
|
360
|
-
currentUrl: params.currentUrl,
|
|
361
|
-
signals: params.signals,
|
|
362
|
-
}))
|
|
363
|
-
.sort((left, right) => right.score - left.score);
|
|
364
|
-
const best = ranked[0] ?? null;
|
|
365
|
-
const runnerUp = ranked[1] ?? null;
|
|
366
|
-
const bestScore = best?.score ?? 0;
|
|
367
|
-
const runnerUpScore = runnerUp?.score ?? 0;
|
|
368
|
-
const gap = bestScore - runnerUpScore;
|
|
369
|
-
const bestReasons = best?.reasons ?? [];
|
|
370
|
-
const explicitLocaleControl = bestReasons.some((reason) => reason.startsWith('locale_control_active='));
|
|
371
|
-
const fixedChromeEvidence = bestReasons.some((reason) => reason.startsWith('fixed_ui_text='));
|
|
372
|
-
const routeLanguageEvidence = bestReasons.some((reason) => reason.startsWith('url=')
|
|
373
|
-
|| reason.startsWith('canonical_url=')
|
|
374
|
-
|| reason.startsWith('hreflang='));
|
|
375
|
-
const strongSurfaceEvidence = explicitLocaleControl
|
|
376
|
-
|| fixedChromeEvidence
|
|
377
|
-
|| routeLanguageEvidence;
|
|
378
|
-
const confident = bestScore >= 3.0
|
|
379
|
-
&& gap >= 0.85
|
|
380
|
-
&& (strongSurfaceEvidence || bestScore >= 5.6);
|
|
381
|
-
const detected = confident ? best?.lang ?? null : null;
|
|
382
|
-
const requestedScore = requested
|
|
383
|
-
? ranked.find((entry) => entry.lang === requested)?.score ?? 0
|
|
384
|
-
: 0;
|
|
385
|
-
const reasons = [
|
|
386
|
-
requested ? `requested=${requested}(${requestedScore.toFixed(2)})` : null,
|
|
387
|
-
best ? `best=${best.lang}(${best.score.toFixed(2)})` : null,
|
|
388
|
-
runnerUp && runnerUp.score > 0 ? `runner_up=${runnerUp.lang}(${runnerUp.score.toFixed(2)})` : null,
|
|
389
|
-
...(best?.reasons ?? []).slice(0, 4),
|
|
390
|
-
!confident && bestScore > 0 ? `state=ambiguous(gap=${gap.toFixed(2)})` : null,
|
|
391
|
-
bestScore === 0 ? 'state=ambiguous(no_language_signal)' : null,
|
|
392
|
-
].filter((reason) => !!reason);
|
|
393
|
-
const confidence = detected
|
|
394
|
-
? confidenceFromScore(bestScore, {
|
|
395
|
-
mediumAt: 3.2,
|
|
396
|
-
highAt: 5.6,
|
|
397
|
-
highSignal: explicitLocaleControl || (fixedChromeEvidence && gap >= 1.2),
|
|
398
|
-
})
|
|
399
|
-
: confidenceFromScore(bestScore, { mediumAt: 3.0, highAt: 5.2 });
|
|
400
|
-
if (!requested) {
|
|
401
|
-
return {
|
|
402
|
-
requested: null,
|
|
403
|
-
detected,
|
|
404
|
-
active: detected !== null,
|
|
405
|
-
ambiguous: detected === null,
|
|
406
|
-
confidence,
|
|
407
|
-
reasons,
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
if (detected === requested) {
|
|
411
|
-
return {
|
|
412
|
-
requested,
|
|
413
|
-
detected,
|
|
414
|
-
active: true,
|
|
415
|
-
ambiguous: false,
|
|
416
|
-
confidence,
|
|
417
|
-
reasons,
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
return {
|
|
421
|
-
requested,
|
|
422
|
-
detected,
|
|
423
|
-
active: false,
|
|
424
|
-
ambiguous: detected === null || confidence !== 'high',
|
|
425
|
-
confidence,
|
|
426
|
-
reasons,
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
function detectThemeFromControls(signals) {
|
|
430
|
-
for (const control of signals.variantControls) {
|
|
431
|
-
if (control.kind !== 'theme')
|
|
432
|
-
continue;
|
|
433
|
-
const selectedValues = [
|
|
434
|
-
control.value ?? '',
|
|
435
|
-
...(control.options ?? [])
|
|
436
|
-
.filter((option) => option.selected)
|
|
437
|
-
.flatMap((option) => [option.label, option.value ?? '']),
|
|
438
|
-
].filter(Boolean);
|
|
439
|
-
if (selectedValues.some((value) => textContainsThemeAlias(value, 'dark'))) {
|
|
440
|
-
return { theme: 'dark', confidence: 'high', reasons: ['theme_control_active=dark'] };
|
|
441
|
-
}
|
|
442
|
-
if (selectedValues.some((value) => textContainsThemeAlias(value, 'light'))) {
|
|
443
|
-
return { theme: 'light', confidence: 'high', reasons: ['theme_control_active=light'] };
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
return { theme: null, confidence: 'low', reasons: [] };
|
|
447
|
-
}
|
|
448
|
-
function detectThemeFromStorage(signals) {
|
|
449
|
-
for (const hint of signals.storageHints) {
|
|
450
|
-
if (hint.kind !== 'theme')
|
|
451
|
-
continue;
|
|
452
|
-
if (textContainsThemeAlias(hint.valueSample, 'dark')) {
|
|
453
|
-
return { theme: 'dark', confidence: 'high', reasons: [`theme_storage=${hint.key}:dark`] };
|
|
454
|
-
}
|
|
455
|
-
if (textContainsThemeAlias(hint.valueSample, 'light')) {
|
|
456
|
-
return { theme: 'light', confidence: 'high', reasons: [`theme_storage=${hint.key}:light`] };
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
return { theme: null, confidence: 'low', reasons: [] };
|
|
460
|
-
}
|
|
461
|
-
function detectThemeFromRoot(signals) {
|
|
462
|
-
const rootHints = (signals.themeRootHints ?? []).join(' ');
|
|
463
|
-
const hasDark = textContainsThemeAlias(rootHints, 'dark');
|
|
464
|
-
const hasLight = textContainsThemeAlias(rootHints, 'light');
|
|
465
|
-
if (hasDark === hasLight) {
|
|
466
|
-
return { theme: null, confidence: 'low', reasons: [] };
|
|
467
|
-
}
|
|
468
|
-
return {
|
|
469
|
-
theme: hasDark ? 'dark' : 'light',
|
|
470
|
-
confidence: 'medium',
|
|
471
|
-
reasons: [`theme_root_hint=${hasDark ? 'dark' : 'light'}`],
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
function detectThemeFromChrome(signals) {
|
|
475
|
-
const chromeSamples = signals.chromeThemeSamples ?? [];
|
|
476
|
-
const brightChrome = chromeSamples.filter((sample) => sample.luminance !== null && sample.luminance >= 0.58);
|
|
477
|
-
const darkChrome = chromeSamples.filter((sample) => sample.luminance !== null && sample.luminance <= 0.28);
|
|
478
|
-
if (brightChrome.length > 0 && darkChrome.length === 0) {
|
|
479
|
-
return {
|
|
480
|
-
theme: 'light',
|
|
481
|
-
confidence: brightChrome.length >= 2 ? 'high' : 'medium',
|
|
482
|
-
reasons: [`chrome_theme=light(${brightChrome.length})`],
|
|
483
|
-
};
|
|
484
|
-
}
|
|
485
|
-
if (darkChrome.length > 0 && brightChrome.length === 0) {
|
|
486
|
-
return {
|
|
487
|
-
theme: 'dark',
|
|
488
|
-
confidence: darkChrome.length >= 2 ? 'high' : 'medium',
|
|
489
|
-
reasons: [`chrome_theme=dark(${darkChrome.length})`],
|
|
490
|
-
};
|
|
491
|
-
}
|
|
492
|
-
return { theme: null, confidence: 'low', reasons: [] };
|
|
493
|
-
}
|
|
494
|
-
export function evaluateRequestedThemeState(params) {
|
|
495
|
-
const requested = params.requestedTheme ?? null;
|
|
496
|
-
const controlTheme = detectThemeFromControls(params.signals);
|
|
497
|
-
const storageTheme = detectThemeFromStorage(params.signals);
|
|
498
|
-
const rootTheme = detectThemeFromRoot(params.signals);
|
|
499
|
-
const chromeTheme = detectThemeFromChrome(params.signals);
|
|
500
|
-
const hasThemeControls = params.signals.variantControls.some((control) => control.kind === 'theme');
|
|
501
|
-
const hasThemeStorage = params.signals.storageHints.some((hint) => hint.kind === 'theme');
|
|
502
|
-
const hasThemeMechanism = hasThemeControls || hasThemeStorage;
|
|
503
|
-
const themeSources = [controlTheme, storageTheme, rootTheme, chromeTheme].filter((entry) => entry.theme !== null);
|
|
504
|
-
const darkVotes = themeSources.filter((entry) => entry.theme === 'dark').length;
|
|
505
|
-
const lightVotes = themeSources.filter((entry) => entry.theme === 'light').length;
|
|
506
|
-
let detected = null;
|
|
507
|
-
let confidence = 'low';
|
|
508
|
-
const reasons = [
|
|
509
|
-
requested ? `requested=${requested}` : null,
|
|
510
|
-
...controlTheme.reasons,
|
|
511
|
-
...storageTheme.reasons,
|
|
512
|
-
...rootTheme.reasons,
|
|
513
|
-
...chromeTheme.reasons,
|
|
514
|
-
].filter((reason) => !!reason);
|
|
515
|
-
if (controlTheme.theme) {
|
|
516
|
-
detected = controlTheme.theme;
|
|
517
|
-
confidence = controlTheme.confidence;
|
|
518
|
-
}
|
|
519
|
-
else if (storageTheme.theme) {
|
|
520
|
-
detected = storageTheme.theme;
|
|
521
|
-
confidence = storageTheme.confidence;
|
|
522
|
-
}
|
|
523
|
-
else if (rootTheme.theme && chromeTheme.theme && rootTheme.theme !== chromeTheme.theme) {
|
|
524
|
-
return {
|
|
525
|
-
requested,
|
|
526
|
-
detected: null,
|
|
527
|
-
active: false,
|
|
528
|
-
ambiguous: true,
|
|
529
|
-
confidence: 'medium',
|
|
530
|
-
reasons: [
|
|
531
|
-
...reasons,
|
|
532
|
-
'state=ambiguous(root_chrome_conflict)',
|
|
533
|
-
],
|
|
534
|
-
};
|
|
535
|
-
}
|
|
536
|
-
else if (rootTheme.theme) {
|
|
537
|
-
detected = rootTheme.theme;
|
|
538
|
-
confidence = chromeTheme.theme === rootTheme.theme ? 'high' : rootTheme.confidence;
|
|
539
|
-
}
|
|
540
|
-
else if (chromeTheme.theme) {
|
|
541
|
-
detected = chromeTheme.theme;
|
|
542
|
-
confidence = chromeTheme.confidence;
|
|
543
|
-
}
|
|
544
|
-
else if (!hasThemeMechanism && params.signals.detectedTheme) {
|
|
545
|
-
detected = params.signals.detectedTheme;
|
|
546
|
-
confidence = 'medium';
|
|
547
|
-
reasons.push(`detected_theme=${params.signals.detectedTheme}`);
|
|
548
|
-
}
|
|
549
|
-
if (!requested) {
|
|
550
|
-
return {
|
|
551
|
-
requested: null,
|
|
552
|
-
detected: detected ?? params.signals.preferredColorScheme,
|
|
553
|
-
active: true,
|
|
554
|
-
ambiguous: detected === null,
|
|
555
|
-
confidence,
|
|
556
|
-
reasons: reasons.length > 0 ? reasons : [`preferred_color_scheme=${params.signals.preferredColorScheme}`],
|
|
557
|
-
};
|
|
558
|
-
}
|
|
559
|
-
if (!detected) {
|
|
560
|
-
return {
|
|
561
|
-
requested,
|
|
562
|
-
detected: null,
|
|
563
|
-
active: false,
|
|
564
|
-
ambiguous: true,
|
|
565
|
-
confidence: hasThemeMechanism ? 'medium' : 'low',
|
|
566
|
-
reasons: [
|
|
567
|
-
...reasons,
|
|
568
|
-
`preferred_color_scheme=${params.signals.preferredColorScheme}`,
|
|
569
|
-
hasThemeControls ? 'theme_controls_detected' : null,
|
|
570
|
-
hasThemeStorage ? 'theme_storage_detected' : null,
|
|
571
|
-
'state=ambiguous(unresolved_theme_mechanism)',
|
|
572
|
-
].filter((reason) => !!reason),
|
|
573
|
-
};
|
|
574
|
-
}
|
|
575
|
-
if (detected === requested) {
|
|
576
|
-
return {
|
|
577
|
-
requested,
|
|
578
|
-
detected,
|
|
579
|
-
active: true,
|
|
580
|
-
ambiguous: false,
|
|
581
|
-
confidence,
|
|
582
|
-
reasons,
|
|
583
|
-
};
|
|
584
|
-
}
|
|
585
|
-
const conflictingLowConfidence = confidence !== 'high'
|
|
586
|
-
|| (darkVotes > 0 && lightVotes > 0);
|
|
587
|
-
return {
|
|
588
|
-
requested,
|
|
589
|
-
detected,
|
|
590
|
-
active: false,
|
|
591
|
-
ambiguous: conflictingLowConfidence,
|
|
592
|
-
confidence,
|
|
593
|
-
reasons: conflictingLowConfidence
|
|
594
|
-
? [...reasons, 'state=ambiguous(theme_signal_conflict)']
|
|
595
|
-
: reasons,
|
|
596
|
-
};
|
|
597
|
-
}
|
|
598
|
-
function validateRequestedUrl(startUrl, currentUrl) {
|
|
599
|
-
if (!startUrl)
|
|
600
|
-
return true;
|
|
601
|
-
try {
|
|
602
|
-
const expected = new URL(startUrl);
|
|
603
|
-
const current = new URL(currentUrl);
|
|
604
|
-
if (expected.origin !== current.origin)
|
|
605
|
-
return false;
|
|
606
|
-
if (expected.pathname === '/' || expected.pathname === '')
|
|
607
|
-
return true;
|
|
608
|
-
return pathsRoughlyMatch(current.pathname, expected.pathname);
|
|
609
|
-
}
|
|
610
|
-
catch {
|
|
611
|
-
return currentUrl.startsWith(startUrl);
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
function evaluateAuthState(signals, currentUrl, profile) {
|
|
615
|
-
const authHints = signals.authHints ?? {
|
|
616
|
-
hasPasswordField: false,
|
|
617
|
-
hasEmailField: false,
|
|
618
|
-
hasAuthForm: false,
|
|
619
|
-
loginButtons: [],
|
|
620
|
-
logoutButtons: [],
|
|
621
|
-
accountMenuLabels: [],
|
|
622
|
-
accountLikeText: [],
|
|
623
|
-
};
|
|
624
|
-
const accountLabel = extractAccountLabel(signals) ?? profile?.accountLabel ?? null;
|
|
625
|
-
const loginLikeUrl = LOGIN_URL_RE.test(currentUrl);
|
|
626
|
-
const positiveReasons = [];
|
|
627
|
-
const negativeReasons = [];
|
|
628
|
-
const weakNegativeReasons = [];
|
|
629
|
-
if (authHints.logoutButtons.length > 0) {
|
|
630
|
-
positiveReasons.push(`logout_button=${truncate(authHints.logoutButtons[0], 60)}`);
|
|
631
|
-
}
|
|
632
|
-
if (accountLabel) {
|
|
633
|
-
positiveReasons.push(`account_label=${truncate(accountLabel, 60)}`);
|
|
634
|
-
}
|
|
635
|
-
if (authHints.accountMenuLabels.some((value) => ACCOUNT_MENU_RE.test(value))) {
|
|
636
|
-
positiveReasons.push(`account_menu=${truncate(authHints.accountMenuLabels[0], 60)}`);
|
|
637
|
-
}
|
|
638
|
-
if (!accountLabel && authHints.accountLikeText.length > 0) {
|
|
639
|
-
positiveReasons.push(`account_like=${truncate(authHints.accountLikeText[0], 60)}`);
|
|
640
|
-
}
|
|
641
|
-
if (profile?.authState === 'authenticated' && !loginLikeUrl) {
|
|
642
|
-
positiveReasons.push('profile=authenticated');
|
|
643
|
-
}
|
|
644
|
-
if (loginLikeUrl) {
|
|
645
|
-
negativeReasons.push(`login_url=${currentUrl}`);
|
|
646
|
-
}
|
|
647
|
-
if (authHints.hasPasswordField) {
|
|
648
|
-
negativeReasons.push('password_field_visible');
|
|
649
|
-
}
|
|
650
|
-
if (authHints.hasAuthForm) {
|
|
651
|
-
negativeReasons.push('auth_form_visible');
|
|
652
|
-
}
|
|
653
|
-
const dominantLoginCta = authHints.loginButtons.length > 0
|
|
654
|
-
&& positiveReasons.length === 0
|
|
655
|
-
&& (loginLikeUrl
|
|
656
|
-
|| authHints.hasPasswordField
|
|
657
|
-
|| authHints.hasAuthForm
|
|
658
|
-
|| authHints.hasEmailField
|
|
659
|
-
|| (signals.navLabels.length <= 2 && signals.breadcrumbLabels.length === 0));
|
|
660
|
-
if (dominantLoginCta) {
|
|
661
|
-
negativeReasons.push(`login_cta=${truncate(authHints.loginButtons[0], 60)}`);
|
|
662
|
-
}
|
|
663
|
-
else {
|
|
664
|
-
if (authHints.hasEmailField) {
|
|
665
|
-
weakNegativeReasons.push('email_field_visible');
|
|
666
|
-
}
|
|
667
|
-
if (authHints.loginButtons.length > 0) {
|
|
668
|
-
weakNegativeReasons.push(`login_button=${truncate(authHints.loginButtons[0], 60)}`);
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
if (negativeReasons.length > 0 && positiveReasons.length > 0) {
|
|
672
|
-
return {
|
|
673
|
-
authState: 'unknown',
|
|
674
|
-
diagnostic: makeDiagnostic('ambiguous', 'high', [
|
|
675
|
-
...positiveReasons,
|
|
676
|
-
...negativeReasons,
|
|
677
|
-
'state=ambiguous(auth_signal_conflict)',
|
|
678
|
-
]),
|
|
679
|
-
};
|
|
680
|
-
}
|
|
681
|
-
if (negativeReasons.length > 0) {
|
|
682
|
-
return {
|
|
683
|
-
authState: 'login_required',
|
|
684
|
-
diagnostic: makeDiagnostic('mismatch', 'high', negativeReasons),
|
|
685
|
-
};
|
|
686
|
-
}
|
|
687
|
-
if (positiveReasons.length > 0 && weakNegativeReasons.length > 0) {
|
|
688
|
-
return {
|
|
689
|
-
authState: 'unknown',
|
|
690
|
-
diagnostic: makeDiagnostic('ambiguous', 'medium', [
|
|
691
|
-
...positiveReasons,
|
|
692
|
-
...weakNegativeReasons,
|
|
693
|
-
'state=ambiguous(mixed_auth_hints)',
|
|
694
|
-
]),
|
|
695
|
-
};
|
|
696
|
-
}
|
|
697
|
-
if (positiveReasons.length > 0) {
|
|
698
|
-
return {
|
|
699
|
-
authState: 'authenticated',
|
|
700
|
-
diagnostic: makeDiagnostic('match', positiveReasons.some((reason) => reason.startsWith('logout_button=') || reason.startsWith('account_label='))
|
|
701
|
-
? 'high'
|
|
702
|
-
: 'medium', positiveReasons),
|
|
703
|
-
};
|
|
704
|
-
}
|
|
705
|
-
if (weakNegativeReasons.length > 0) {
|
|
706
|
-
return {
|
|
707
|
-
authState: 'unknown',
|
|
708
|
-
diagnostic: makeDiagnostic('ambiguous', 'medium', [
|
|
709
|
-
...weakNegativeReasons,
|
|
710
|
-
'state=ambiguous(weak_login_hints)',
|
|
711
|
-
]),
|
|
712
|
-
};
|
|
713
|
-
}
|
|
714
|
-
return {
|
|
715
|
-
authState: 'unknown',
|
|
716
|
-
diagnostic: makeDiagnostic('ambiguous', 'low', ['auth=unknown']),
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
|
-
function buildSessionSummary(params) {
|
|
720
|
-
const parts = [
|
|
721
|
-
`auth=${params.authState}`,
|
|
722
|
-
params.detectedLang ? `lang=${params.detectedLang}` : null,
|
|
723
|
-
params.detectedTheme ? `theme=${params.detectedTheme}` : null,
|
|
724
|
-
params.accountLabel ? `account=${params.accountLabel}` : null,
|
|
725
|
-
`url=${params.currentUrl}`,
|
|
726
|
-
`state=${params.validationStatus}`,
|
|
727
|
-
].filter(Boolean);
|
|
728
|
-
return truncate(parts.join('; '), 220);
|
|
729
|
-
}
|
|
730
|
-
export function buildAgentRunHints(entries) {
|
|
731
|
-
const severityByType = {
|
|
732
|
-
give_up: 'high',
|
|
733
|
-
verification_failure: 'high',
|
|
734
|
-
max_iterations: 'medium',
|
|
735
|
-
stuck_loop: 'medium',
|
|
736
|
-
storage_upload_failure: 'low',
|
|
737
|
-
};
|
|
738
|
-
const deduped = new Map();
|
|
739
|
-
for (const entry of entries) {
|
|
740
|
-
const correction = entry.user_response ? ` Fix: ${entry.user_response}` : '';
|
|
741
|
-
const message = truncate(`${entry.message}${correction}`, 240);
|
|
742
|
-
const key = `${entry.error_type}:${message.toLowerCase()}`;
|
|
743
|
-
if (deduped.has(key))
|
|
744
|
-
continue;
|
|
745
|
-
deduped.set(key, {
|
|
746
|
-
key: entry.error_type,
|
|
747
|
-
message,
|
|
748
|
-
severity: severityByType[entry.error_type] ?? 'medium',
|
|
749
|
-
source: 'agent_error',
|
|
750
|
-
});
|
|
751
|
-
}
|
|
752
|
-
return Array.from(deduped.values())
|
|
753
|
-
.sort((a, b) => {
|
|
754
|
-
const weight = (severity) => severity === 'high' ? 3 : severity === 'medium' ? 2 : 1;
|
|
755
|
-
return weight(b.severity) - weight(a.severity);
|
|
756
|
-
})
|
|
757
|
-
.slice(0, 6);
|
|
758
|
-
}
|
|
759
|
-
export function validateSessionProfile(params) {
|
|
760
|
-
const requestedLang = normalizeLangCode(params.requestedLang);
|
|
761
|
-
const langState = evaluateRequestedLanguageState({
|
|
762
|
-
currentUrl: params.currentUrl,
|
|
763
|
-
requestedLang: requestedLang ?? undefined,
|
|
764
|
-
signals: params.signals,
|
|
765
|
-
});
|
|
766
|
-
const themeState = evaluateRequestedThemeState({
|
|
767
|
-
requestedTheme: params.requestedTheme,
|
|
768
|
-
signals: params.signals,
|
|
769
|
-
});
|
|
770
|
-
const langCandidates = extractLangCandidates(params.signals, params.currentUrl);
|
|
771
|
-
const detectedLang = langState.detected ?? langCandidates[0] ?? null;
|
|
772
|
-
const detectedTheme = themeState.detected ?? params.profile?.detectedTheme ?? null;
|
|
773
|
-
const accountLabel = extractAccountLabel(params.signals) ?? params.profile?.accountLabel ?? null;
|
|
774
|
-
const authStateEvaluation = evaluateAuthState(params.signals, params.currentUrl, params.profile);
|
|
775
|
-
const authState = authStateEvaluation.authState;
|
|
776
|
-
const authDiagnostic = authStateEvaluation.diagnostic;
|
|
777
|
-
const authMatches = authDiagnostic.outcome !== 'mismatch';
|
|
778
|
-
let langMatches = null;
|
|
779
|
-
if (requestedLang) {
|
|
780
|
-
if (langState.active)
|
|
781
|
-
langMatches = true;
|
|
782
|
-
else if (langState.ambiguous)
|
|
783
|
-
langMatches = null;
|
|
784
|
-
else
|
|
785
|
-
langMatches = false;
|
|
786
|
-
}
|
|
787
|
-
const langDiagnostic = !requestedLang
|
|
788
|
-
? makeDiagnostic('match', 'low', ['lang=not_requested'])
|
|
789
|
-
: langMatches === true
|
|
790
|
-
? makeDiagnostic('match', langState.confidence, langState.reasons)
|
|
791
|
-
: langMatches === false
|
|
792
|
-
? makeDiagnostic('mismatch', langState.confidence, langState.reasons)
|
|
793
|
-
: makeDiagnostic('ambiguous', langState.confidence, langState.reasons);
|
|
794
|
-
let themeMatches = null;
|
|
795
|
-
if (params.requestedTheme) {
|
|
796
|
-
if (themeState.active)
|
|
797
|
-
themeMatches = true;
|
|
798
|
-
else if (themeState.ambiguous)
|
|
799
|
-
themeMatches = null;
|
|
800
|
-
else
|
|
801
|
-
themeMatches = false;
|
|
802
|
-
}
|
|
803
|
-
const themeDiagnostic = !params.requestedTheme
|
|
804
|
-
? makeDiagnostic('match', 'low', ['theme=not_requested'])
|
|
805
|
-
: themeMatches === true
|
|
806
|
-
? makeDiagnostic('match', themeState.confidence, themeState.reasons)
|
|
807
|
-
: themeMatches === false
|
|
808
|
-
? makeDiagnostic('mismatch', themeState.confidence, themeState.reasons)
|
|
809
|
-
: makeDiagnostic('ambiguous', themeState.confidence, themeState.reasons);
|
|
810
|
-
const urlMatches = validateRequestedUrl(params.startUrl, params.currentUrl);
|
|
811
|
-
const urlDiagnostic = !params.startUrl
|
|
812
|
-
? makeDiagnostic('match', 'low', ['url=not_requested'])
|
|
813
|
-
: urlMatches
|
|
814
|
-
? makeDiagnostic('match', 'high', [`url_matches=${params.startUrl}`])
|
|
815
|
-
: makeDiagnostic('mismatch', 'high', [`url_mismatch=${params.startUrl}`]);
|
|
816
|
-
const issues = [];
|
|
817
|
-
if (authDiagnostic.outcome === 'mismatch')
|
|
818
|
-
issues.push('auth');
|
|
819
|
-
if (langDiagnostic.outcome === 'mismatch')
|
|
820
|
-
issues.push('lang');
|
|
821
|
-
if (themeDiagnostic.outcome === 'mismatch')
|
|
822
|
-
issues.push('theme');
|
|
823
|
-
if (urlDiagnostic.outcome === 'mismatch')
|
|
824
|
-
issues.push('url');
|
|
825
|
-
let validationStatus = 'valid';
|
|
826
|
-
if (issues.length > 0) {
|
|
827
|
-
validationStatus = 'invalid';
|
|
828
|
-
}
|
|
829
|
-
else if (authDiagnostic.outcome === 'ambiguous'
|
|
830
|
-
|| langDiagnostic.outcome === 'ambiguous'
|
|
831
|
-
|| themeDiagnostic.outcome === 'ambiguous'
|
|
832
|
-
|| urlDiagnostic.outcome === 'ambiguous') {
|
|
833
|
-
validationStatus = 'unknown';
|
|
834
|
-
}
|
|
835
|
-
return {
|
|
836
|
-
validationStatus,
|
|
837
|
-
authState,
|
|
838
|
-
authMatches,
|
|
839
|
-
langMatches,
|
|
840
|
-
themeMatches,
|
|
841
|
-
urlMatches,
|
|
842
|
-
authDiagnostic,
|
|
843
|
-
langDiagnostic,
|
|
844
|
-
themeDiagnostic,
|
|
845
|
-
urlDiagnostic,
|
|
846
|
-
detectedLang,
|
|
847
|
-
detectedTheme,
|
|
848
|
-
accountLabel,
|
|
849
|
-
issues,
|
|
850
|
-
summary: buildSessionSummary({
|
|
851
|
-
currentUrl: params.currentUrl,
|
|
852
|
-
authState,
|
|
853
|
-
detectedLang,
|
|
854
|
-
detectedTheme,
|
|
855
|
-
accountLabel,
|
|
856
|
-
validationStatus,
|
|
857
|
-
}),
|
|
858
|
-
};
|
|
859
|
-
}
|
|
860
|
-
export async function collectValidatedSessionProfile(browser, params) {
|
|
861
|
-
const currentUrl = browser.currentPage.url();
|
|
862
|
-
const includeStorageSnapshots = params.includeStorageSnapshots !== false;
|
|
863
|
-
const [signals, storageState, sessionStorage] = await Promise.all([
|
|
864
|
-
browser.capturePageSignals(),
|
|
865
|
-
includeStorageSnapshots
|
|
866
|
-
? browser.exportStorageState().catch(() => undefined)
|
|
867
|
-
: Promise.resolve(params.existingProfile?.storageState),
|
|
868
|
-
includeStorageSnapshots
|
|
869
|
-
? browser.exportSessionStorage().catch(() => undefined)
|
|
870
|
-
: Promise.resolve(params.existingProfile?.sessionStorage),
|
|
871
|
-
]);
|
|
872
|
-
const validation = validateSessionProfile({
|
|
873
|
-
currentUrl,
|
|
874
|
-
startUrl: params.startUrl,
|
|
875
|
-
requestedLang: params.requestedLang,
|
|
876
|
-
requestedTheme: params.requestedTheme,
|
|
877
|
-
signals,
|
|
878
|
-
profile: params.existingProfile,
|
|
879
|
-
});
|
|
880
|
-
return {
|
|
881
|
-
profile: {
|
|
882
|
-
storageState,
|
|
883
|
-
sessionStorage,
|
|
884
|
-
authState: validation.authState,
|
|
885
|
-
accountLabel: validation.accountLabel,
|
|
886
|
-
detectedLang: validation.detectedLang,
|
|
887
|
-
detectedTheme: validation.detectedTheme,
|
|
888
|
-
validatedStartUrl: params.startUrl ?? params.existingProfile?.validatedStartUrl ?? currentUrl,
|
|
889
|
-
lastKnownUrl: currentUrl,
|
|
890
|
-
summary: validation.summary,
|
|
891
|
-
validationStatus: validation.validationStatus,
|
|
892
|
-
lastUsedAt: new Date().toISOString(),
|
|
893
|
-
profileVersion: params.existingProfile?.profileVersion ?? 1,
|
|
894
|
-
},
|
|
895
|
-
validation,
|
|
896
|
-
};
|
|
897
|
-
}
|
|
898
|
-
function uniqueSelectors(values) {
|
|
899
|
-
if (!values)
|
|
900
|
-
return [];
|
|
901
|
-
return Array.from(new Set(values.filter((value) => value && !value.startsWith('[index:'))));
|
|
902
|
-
}
|
|
903
|
-
function buildRequestedLanguageValues(requestedLang, sample) {
|
|
904
|
-
const normalized = normalizeLangCode(requestedLang);
|
|
905
|
-
if (!normalized)
|
|
906
|
-
return [];
|
|
907
|
-
const values = new Set([normalized]);
|
|
908
|
-
const raw = requestedLang.trim();
|
|
909
|
-
if (raw) {
|
|
910
|
-
values.add(raw);
|
|
911
|
-
values.add(raw.toLowerCase());
|
|
912
|
-
values.add(raw.toUpperCase());
|
|
913
|
-
values.add(raw.replace('-', '_'));
|
|
914
|
-
values.add(raw.toLowerCase().replace('-', '_'));
|
|
915
|
-
}
|
|
916
|
-
const sampleText = sample ?? '';
|
|
917
|
-
if (/^[A-Z]{2}$/.test(sampleText.trim())) {
|
|
918
|
-
values.add(normalized.toUpperCase());
|
|
919
|
-
}
|
|
920
|
-
return Array.from(values).filter(Boolean);
|
|
921
|
-
}
|
|
922
|
-
function buildRequestedThemeValues(requestedTheme, sample) {
|
|
923
|
-
const normalizedSample = (sample ?? '').trim().toLowerCase();
|
|
924
|
-
const values = new Set([requestedTheme]);
|
|
925
|
-
if (normalizedSample === '1' || normalizedSample === '0') {
|
|
926
|
-
values.add(requestedTheme === 'dark' ? '1' : '0');
|
|
927
|
-
}
|
|
928
|
-
if (normalizedSample === 'true' || normalizedSample === 'false') {
|
|
929
|
-
values.add(requestedTheme === 'dark' ? 'true' : 'false');
|
|
930
|
-
}
|
|
931
|
-
if (normalizedSample === 'enabled' || normalizedSample === 'disabled') {
|
|
932
|
-
values.add(requestedTheme === 'dark' ? 'enabled' : 'disabled');
|
|
933
|
-
}
|
|
934
|
-
if (normalizedSample === 'on' || normalizedSample === 'off') {
|
|
935
|
-
values.add(requestedTheme === 'dark' ? 'on' : 'off');
|
|
936
|
-
}
|
|
937
|
-
return Array.from(values);
|
|
938
|
-
}
|
|
939
|
-
function buildStorageReplacementCandidates(hint, params) {
|
|
940
|
-
if (hint.kind === 'locale' && params.requestedLang) {
|
|
941
|
-
return buildRequestedLanguageValues(params.requestedLang, hint.valueSample);
|
|
942
|
-
}
|
|
943
|
-
if (hint.kind === 'theme' && params.requestedTheme) {
|
|
944
|
-
return buildRequestedThemeValues(params.requestedTheme, hint.valueSample);
|
|
945
|
-
}
|
|
946
|
-
return [];
|
|
947
|
-
}
|
|
948
|
-
async function writeStorageHintCandidate(browser, hint, candidateValue) {
|
|
949
|
-
return browser.currentPage.evaluate(({ storageName, key, candidate, kind }) => {
|
|
950
|
-
const storage = storageName === 'localStorage' ? window.localStorage : window.sessionStorage;
|
|
951
|
-
const current = storage.getItem(key);
|
|
952
|
-
if (current == null)
|
|
953
|
-
return false;
|
|
954
|
-
if (current === candidate)
|
|
955
|
-
return true;
|
|
956
|
-
// Strict key matching for storage mutation to avoid rewriting unrelated keys.
|
|
957
|
-
// Keys must be an exact known term OR contain it as an isolated segment
|
|
958
|
-
// (e.g. "settings.lang" matches, "appLanguageVersion" does NOT).
|
|
959
|
-
const LOCALE_KEY_WHITELIST = ['lang', 'locale', 'language', 'i18n', 'intl', 'i18n-locale', 'next-i18next', 'NEXT_LOCALE', 'nuxt-i18n-locale'];
|
|
960
|
-
const THEME_KEY_WHITELIST = ['theme', 'color-scheme', 'colorScheme', 'dark-mode', 'darkMode', 'appearance'];
|
|
961
|
-
const whitelist = kind === 'locale' ? LOCALE_KEY_WHITELIST : THEME_KEY_WHITELIST;
|
|
962
|
-
const isAllowedKey = (k) => {
|
|
963
|
-
const lower = k.toLowerCase();
|
|
964
|
-
return whitelist.some(w => {
|
|
965
|
-
const wl = w.toLowerCase();
|
|
966
|
-
if (lower === wl)
|
|
967
|
-
return true;
|
|
968
|
-
// Match as isolated segment: preceded/followed by non-alphanumeric
|
|
969
|
-
const re = new RegExp(`(?:^|[^a-zA-Z0-9])${wl.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')}(?:$|[^a-zA-Z0-9])`, 'i');
|
|
970
|
-
return re.test(lower);
|
|
971
|
-
});
|
|
972
|
-
};
|
|
973
|
-
// Validate the current value looks like a lang code (2-5 chars) or theme value
|
|
974
|
-
// before rewriting, to avoid corrupting unrelated data.
|
|
975
|
-
const looksLikeTargetValue = (v) => {
|
|
976
|
-
if (kind === 'locale') {
|
|
977
|
-
return /^[a-z]{2,3}(-[a-zA-Z]{2,4})?$/.test(v.trim());
|
|
978
|
-
}
|
|
979
|
-
return /^(light|dark|auto|system|dim|high-contrast)$/i.test(v.trim());
|
|
980
|
-
};
|
|
981
|
-
const rewrite = (input, parentKeyAllowed = false) => {
|
|
982
|
-
if (typeof input === 'string') {
|
|
983
|
-
if (!looksLikeTargetValue(input))
|
|
984
|
-
return { changed: false, value: input };
|
|
985
|
-
return { changed: input !== candidate, value: candidate };
|
|
986
|
-
}
|
|
987
|
-
if (Array.isArray(input)) {
|
|
988
|
-
// Only rewrite array elements when the parent key is an allowed locale/theme key.
|
|
989
|
-
// This prevents corrupting arrays of short strings (e.g. abbreviations, country codes)
|
|
990
|
-
// that happen to match locale patterns but are not linguistic in nature.
|
|
991
|
-
if (!parentKeyAllowed)
|
|
992
|
-
return { changed: false, value: input };
|
|
993
|
-
let changed = false;
|
|
994
|
-
const next = input.map((entry) => {
|
|
995
|
-
const rewritten = rewrite(entry, true);
|
|
996
|
-
changed = changed || rewritten.changed;
|
|
997
|
-
return rewritten.value;
|
|
998
|
-
});
|
|
999
|
-
return { changed, value: next };
|
|
1000
|
-
}
|
|
1001
|
-
if (input && typeof input === 'object') {
|
|
1002
|
-
let changed = false;
|
|
1003
|
-
const next = {};
|
|
1004
|
-
for (const [entryKey, entryValue] of Object.entries(input)) {
|
|
1005
|
-
const keyAllowed = isAllowedKey(entryKey);
|
|
1006
|
-
if (keyAllowed && typeof entryValue === 'string' && looksLikeTargetValue(entryValue)) {
|
|
1007
|
-
if (entryValue !== candidate)
|
|
1008
|
-
changed = true;
|
|
1009
|
-
next[entryKey] = candidate;
|
|
1010
|
-
continue;
|
|
1011
|
-
}
|
|
1012
|
-
const rewritten = rewrite(entryValue, keyAllowed);
|
|
1013
|
-
changed = changed || rewritten.changed;
|
|
1014
|
-
next[entryKey] = rewritten.value;
|
|
1015
|
-
}
|
|
1016
|
-
return { changed, value: next };
|
|
1017
|
-
}
|
|
1018
|
-
return { changed: false, value: input };
|
|
1019
|
-
};
|
|
1020
|
-
let nextValue = candidate;
|
|
1021
|
-
try {
|
|
1022
|
-
const parsed = JSON.parse(current);
|
|
1023
|
-
const rewritten = rewrite(parsed);
|
|
1024
|
-
if (rewritten.changed) {
|
|
1025
|
-
nextValue = JSON.stringify(rewritten.value);
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
catch {
|
|
1029
|
-
nextValue = candidate;
|
|
1030
|
-
}
|
|
1031
|
-
storage.setItem(key, nextValue);
|
|
1032
|
-
return true;
|
|
1033
|
-
}, {
|
|
1034
|
-
storageName: hint.storage,
|
|
1035
|
-
key: hint.key,
|
|
1036
|
-
candidate: candidateValue,
|
|
1037
|
-
kind: hint.kind,
|
|
1038
|
-
});
|
|
1039
|
-
}
|
|
1040
|
-
async function probeSelector(browser, selector) {
|
|
1041
|
-
try {
|
|
1042
|
-
return await browser.currentPage.locator(selector).first().evaluate((node) => ({
|
|
1043
|
-
tag: node.tagName.toLowerCase(),
|
|
1044
|
-
role: node.getAttribute('role') || '',
|
|
1045
|
-
href: node instanceof HTMLAnchorElement ? node.href : node.getAttribute('href'),
|
|
1046
|
-
label: (node.getAttribute('aria-label')
|
|
1047
|
-
|| node.getAttribute('title')
|
|
1048
|
-
|| node.innerText
|
|
1049
|
-
|| node.textContent
|
|
1050
|
-
|| '').replace(/\s+/g, ' ').trim().slice(0, 120),
|
|
1051
|
-
inputType: node instanceof HTMLInputElement ? node.type : node.getAttribute('type'),
|
|
1052
|
-
}));
|
|
1053
|
-
}
|
|
1054
|
-
catch {
|
|
1055
|
-
return null;
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
function languageTerms(requestedLang) {
|
|
1059
|
-
const normalized = normalizeLangCode(requestedLang);
|
|
1060
|
-
if (!normalized)
|
|
1061
|
-
return [];
|
|
1062
|
-
return LANGUAGE_ALIASES[normalized] ?? [normalized];
|
|
1063
|
-
}
|
|
1064
|
-
function controlMatchesVariant(control, kind, target) {
|
|
1065
|
-
if (control.kind !== kind)
|
|
1066
|
-
return false;
|
|
1067
|
-
const targetLower = target.toLowerCase();
|
|
1068
|
-
const options = control.options
|
|
1069
|
-
?.map((option) => `${option.label || ''} ${option.value || ''}`.toLowerCase())
|
|
1070
|
-
.join(' ')
|
|
1071
|
-
?? '';
|
|
1072
|
-
const haystack = `${control.label} ${control.value || ''} ${control.href || ''} ${options}`.toLowerCase();
|
|
1073
|
-
if (kind === 'locale') {
|
|
1074
|
-
return languageTerms(target).some((term) => haystack.includes(term));
|
|
1075
|
-
}
|
|
1076
|
-
return haystack.includes(targetLower);
|
|
1077
|
-
}
|
|
1078
|
-
async function applyVariantSelector(browser, selector, params) {
|
|
1079
|
-
const probe = await probeSelector(browser, selector);
|
|
1080
|
-
if (!probe)
|
|
1081
|
-
return false;
|
|
1082
|
-
if (probe.tag === 'select') {
|
|
1083
|
-
const optionLabel = params.kind === 'locale'
|
|
1084
|
-
? languageTerms(params.target)[0] ?? params.target
|
|
1085
|
-
: params.target;
|
|
1086
|
-
await browser.selectOption({
|
|
1087
|
-
selector,
|
|
1088
|
-
optionLabel,
|
|
1089
|
-
});
|
|
1090
|
-
return true;
|
|
1091
|
-
}
|
|
1092
|
-
if (probe.href && controlMatchesVariant({
|
|
1093
|
-
kind: params.kind,
|
|
1094
|
-
mechanism: 'link',
|
|
1095
|
-
selector,
|
|
1096
|
-
label: probe.label,
|
|
1097
|
-
value: null,
|
|
1098
|
-
href: probe.href,
|
|
1099
|
-
tag: probe.tag,
|
|
1100
|
-
role: probe.role,
|
|
1101
|
-
}, params.kind, params.target)) {
|
|
1102
|
-
await browser.navigateTo(probe.href);
|
|
1103
|
-
return true;
|
|
1104
|
-
}
|
|
1105
|
-
if (params.kind === 'theme' && (probe.role === 'switch' || probe.inputType === 'checkbox')) {
|
|
1106
|
-
await browser.clickBySelector(selector);
|
|
1107
|
-
return true;
|
|
1108
|
-
}
|
|
1109
|
-
if (LANG_CONTROL_RE.test(`${probe.label} ${selector}`) || THEME_CONTROL_RE.test(`${probe.label} ${selector}`)) {
|
|
1110
|
-
await browser.safeExpand({ selector });
|
|
1111
|
-
return true;
|
|
1112
|
-
}
|
|
1113
|
-
await browser.clickBySelector(selector);
|
|
1114
|
-
return true;
|
|
1115
|
-
}
|
|
1116
|
-
async function revalidate(browser, params) {
|
|
1117
|
-
const signals = await browser.capturePageSignals();
|
|
1118
|
-
return validateSessionProfile({
|
|
1119
|
-
currentUrl: browser.currentPage.url(),
|
|
1120
|
-
startUrl: params.startUrl,
|
|
1121
|
-
requestedLang: params.requestedLang,
|
|
1122
|
-
requestedTheme: params.requestedTheme,
|
|
1123
|
-
signals,
|
|
1124
|
-
profile: params.profile,
|
|
1125
|
-
});
|
|
1126
|
-
}
|
|
1127
|
-
async function trySelectorSet(browser, selectors, action, params) {
|
|
1128
|
-
let validation = await revalidate(browser, params.validationParams);
|
|
1129
|
-
const updates = [];
|
|
1130
|
-
for (const selector of selectors) {
|
|
1131
|
-
try {
|
|
1132
|
-
await action(selector);
|
|
1133
|
-
await browser.wait(350);
|
|
1134
|
-
validation = await revalidate(browser, params.validationParams);
|
|
1135
|
-
updates.push({
|
|
1136
|
-
stepSignature: params.signature,
|
|
1137
|
-
selector,
|
|
1138
|
-
source: 'deterministic',
|
|
1139
|
-
success: validation.validationStatus !== 'invalid',
|
|
1140
|
-
});
|
|
1141
|
-
if (validation.validationStatus === 'valid') {
|
|
1142
|
-
return { success: true, validation, updates };
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
catch {
|
|
1146
|
-
updates.push({
|
|
1147
|
-
stepSignature: params.signature,
|
|
1148
|
-
selector,
|
|
1149
|
-
source: 'deterministic',
|
|
1150
|
-
success: false,
|
|
1151
|
-
});
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
return { success: false, validation, updates };
|
|
1155
|
-
}
|
|
1156
|
-
function needsHardLangRepair(validation) {
|
|
1157
|
-
return validation.langDiagnostic.outcome === 'mismatch';
|
|
1158
|
-
}
|
|
1159
|
-
function needsHardThemeRepair(validation) {
|
|
1160
|
-
return validation.themeDiagnostic.outcome === 'mismatch';
|
|
1161
|
-
}
|
|
1162
|
-
export async function performDeterministicSessionRepair(browser, params) {
|
|
1163
|
-
let validation = await revalidate(browser, params);
|
|
1164
|
-
const updates = [];
|
|
1165
|
-
if (validation.validationStatus === 'valid') {
|
|
1166
|
-
return { repaired: false, validation, pathUsed: null, updates };
|
|
1167
|
-
}
|
|
1168
|
-
const remember = (next) => {
|
|
1169
|
-
updates.push(...next.filter((update) => !!update.selector));
|
|
1170
|
-
};
|
|
1171
|
-
const refreshSignals = async (fallback) => browser.capturePageSignals().catch(() => fallback);
|
|
1172
|
-
if (!validation.authMatches && params.credentials?.email && params.credentials?.password) {
|
|
1173
|
-
const emailSelectors = uniqueSelectors(params.selectorMemory?.['login:email_field']);
|
|
1174
|
-
const passwordSelectors = uniqueSelectors(params.selectorMemory?.['login:password_field']);
|
|
1175
|
-
const submitSelectors = uniqueSelectors(params.selectorMemory?.['login:submit']);
|
|
1176
|
-
if (params.credentials.loginUrl && !/\/(login|signin|sign-in|auth)\b/i.test(browser.currentPage.url())) {
|
|
1177
|
-
await browser.navigateTo(params.credentials.loginUrl).catch(() => undefined);
|
|
1178
|
-
await browser.wait(300);
|
|
1179
|
-
}
|
|
1180
|
-
for (const selector of emailSelectors.slice(0, 2)) {
|
|
1181
|
-
try {
|
|
1182
|
-
await browser.typeText(params.credentials.email, { selector, clearFirst: true });
|
|
1183
|
-
remember([{ stepSignature: 'login:email_field', selector, source: 'deterministic', success: true }]);
|
|
1184
|
-
break;
|
|
1185
|
-
}
|
|
1186
|
-
catch {
|
|
1187
|
-
remember([{ stepSignature: 'login:email_field', selector, source: 'deterministic', success: false }]);
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
for (const selector of passwordSelectors.slice(0, 2)) {
|
|
1191
|
-
try {
|
|
1192
|
-
await browser.typeText(params.credentials.password, { selector, clearFirst: true });
|
|
1193
|
-
remember([{ stepSignature: 'login:password_field', selector, source: 'deterministic', success: true }]);
|
|
1194
|
-
break;
|
|
1195
|
-
}
|
|
1196
|
-
catch {
|
|
1197
|
-
remember([{ stepSignature: 'login:password_field', selector, source: 'deterministic', success: false }]);
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
const submitAttempt = await trySelectorSet(browser, submitSelectors.slice(0, 2), async (selector) => {
|
|
1201
|
-
await browser.clickBySelector(selector);
|
|
1202
|
-
}, {
|
|
1203
|
-
signature: 'login:submit',
|
|
1204
|
-
validationParams: params,
|
|
1205
|
-
});
|
|
1206
|
-
remember(submitAttempt.updates);
|
|
1207
|
-
validation = submitAttempt.validation;
|
|
1208
|
-
if (validation.validationStatus === 'valid') {
|
|
1209
|
-
return { repaired: true, validation, pathUsed: 'login_memory', updates };
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
let signals = await browser.capturePageSignals().catch(() => null);
|
|
1213
|
-
if (params.requestedLang && needsHardLangRepair(validation)) {
|
|
1214
|
-
const requestedLang = normalizeLangCode(params.requestedLang) ?? params.requestedLang;
|
|
1215
|
-
const langOptionSelectors = uniqueSelectors(params.selectorMemory?.[`lang:option:${requestedLang}`]);
|
|
1216
|
-
const langSwitchers = uniqueSelectors(params.selectorMemory?.['lang:switcher']);
|
|
1217
|
-
const langMemoryAttempt = await trySelectorSet(browser, [...langOptionSelectors.slice(0, 3), ...langSwitchers.slice(0, 2)], async (selector) => {
|
|
1218
|
-
await applyVariantSelector(browser, selector, {
|
|
1219
|
-
kind: 'locale',
|
|
1220
|
-
target: params.requestedLang,
|
|
1221
|
-
});
|
|
1222
|
-
}, {
|
|
1223
|
-
signature: langOptionSelectors.length > 0 ? `lang:option:${requestedLang}` : 'lang:switcher',
|
|
1224
|
-
validationParams: params,
|
|
1225
|
-
});
|
|
1226
|
-
remember(langMemoryAttempt.updates);
|
|
1227
|
-
validation = langMemoryAttempt.validation;
|
|
1228
|
-
if (langMemoryAttempt.updates.length > 0) {
|
|
1229
|
-
signals = await refreshSignals(signals);
|
|
1230
|
-
}
|
|
1231
|
-
if (validation.validationStatus === 'valid') {
|
|
1232
|
-
return { repaired: true, validation, pathUsed: 'lang_memory', updates };
|
|
1233
|
-
}
|
|
1234
|
-
if (signals) {
|
|
1235
|
-
const control = signals.variantControls.find((entry) => controlMatchesVariant(entry, 'locale', params.requestedLang));
|
|
1236
|
-
if (control) {
|
|
1237
|
-
try {
|
|
1238
|
-
await applyVariantSelector(browser, control.selector, {
|
|
1239
|
-
kind: 'locale',
|
|
1240
|
-
target: params.requestedLang,
|
|
1241
|
-
});
|
|
1242
|
-
await browser.wait(350);
|
|
1243
|
-
validation = await revalidate(browser, params);
|
|
1244
|
-
signals = await refreshSignals(signals);
|
|
1245
|
-
remember([{
|
|
1246
|
-
stepSignature: `lang:option:${requestedLang}`,
|
|
1247
|
-
selector: control.selector,
|
|
1248
|
-
source: 'deterministic',
|
|
1249
|
-
success: validation.validationStatus !== 'invalid',
|
|
1250
|
-
}]);
|
|
1251
|
-
if (validation.validationStatus === 'valid') {
|
|
1252
|
-
return { repaired: true, validation, pathUsed: 'lang_control', updates };
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
catch {
|
|
1256
|
-
remember([{
|
|
1257
|
-
stepSignature: `lang:option:${requestedLang}`,
|
|
1258
|
-
selector: control.selector,
|
|
1259
|
-
source: 'deterministic',
|
|
1260
|
-
success: false,
|
|
1261
|
-
}]);
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
// Recapture signals before theme repair — lang repair may have changed the DOM structure,
|
|
1267
|
-
// opened/closed menus, or navigated to different settings. Theme variant controls detected
|
|
1268
|
-
// before lang repair may no longer be accurate.
|
|
1269
|
-
if (params.requestedTheme && needsHardThemeRepair(validation)) {
|
|
1270
|
-
signals = await refreshSignals(signals);
|
|
1271
|
-
const themeOptionSelectors = uniqueSelectors(params.selectorMemory?.[`theme:option:${params.requestedTheme}`]);
|
|
1272
|
-
const themeToggles = uniqueSelectors(params.selectorMemory?.['theme:toggle']);
|
|
1273
|
-
const themeAttempt = await trySelectorSet(browser, [...themeOptionSelectors.slice(0, 3), ...themeToggles.slice(0, 2)], async (selector) => {
|
|
1274
|
-
await applyVariantSelector(browser, selector, {
|
|
1275
|
-
kind: 'theme',
|
|
1276
|
-
target: params.requestedTheme,
|
|
1277
|
-
});
|
|
1278
|
-
}, {
|
|
1279
|
-
signature: themeOptionSelectors.length > 0 ? `theme:option:${params.requestedTheme}` : 'theme:toggle',
|
|
1280
|
-
validationParams: params,
|
|
1281
|
-
});
|
|
1282
|
-
remember(themeAttempt.updates);
|
|
1283
|
-
validation = themeAttempt.validation;
|
|
1284
|
-
if (themeAttempt.updates.length > 0) {
|
|
1285
|
-
signals = await refreshSignals(signals);
|
|
1286
|
-
}
|
|
1287
|
-
if (validation.validationStatus === 'valid') {
|
|
1288
|
-
return { repaired: true, validation, pathUsed: 'theme_memory', updates };
|
|
1289
|
-
}
|
|
1290
|
-
if (signals) {
|
|
1291
|
-
const control = signals.variantControls.find((entry) => controlMatchesVariant(entry, 'theme', params.requestedTheme));
|
|
1292
|
-
if (control) {
|
|
1293
|
-
try {
|
|
1294
|
-
await applyVariantSelector(browser, control.selector, {
|
|
1295
|
-
kind: 'theme',
|
|
1296
|
-
target: params.requestedTheme,
|
|
1297
|
-
});
|
|
1298
|
-
await browser.wait(350);
|
|
1299
|
-
validation = await revalidate(browser, params);
|
|
1300
|
-
signals = await refreshSignals(signals);
|
|
1301
|
-
remember([{
|
|
1302
|
-
stepSignature: `theme:option:${params.requestedTheme}`,
|
|
1303
|
-
selector: control.selector,
|
|
1304
|
-
source: 'deterministic',
|
|
1305
|
-
success: validation.validationStatus !== 'invalid',
|
|
1306
|
-
}]);
|
|
1307
|
-
if (validation.validationStatus === 'valid') {
|
|
1308
|
-
return { repaired: true, validation, pathUsed: 'theme_control', updates };
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
catch {
|
|
1312
|
-
remember([{
|
|
1313
|
-
stepSignature: `theme:option:${params.requestedTheme}`,
|
|
1314
|
-
selector: control.selector,
|
|
1315
|
-
source: 'deterministic',
|
|
1316
|
-
success: false,
|
|
1317
|
-
}]);
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
if (signals
|
|
1323
|
-
&& ((params.requestedLang && validation.langMatches !== true)
|
|
1324
|
-
|| (params.requestedTheme && validation.themeMatches !== true))) {
|
|
1325
|
-
const storageHints = signals.storageHints
|
|
1326
|
-
.filter((hint) => (hint.kind === 'locale' && params.requestedLang && needsHardLangRepair(validation))
|
|
1327
|
-
|| (hint.kind === 'theme' && params.requestedTheme && needsHardThemeRepair(validation)))
|
|
1328
|
-
.slice(0, 6);
|
|
1329
|
-
for (const hint of storageHints) {
|
|
1330
|
-
const candidates = buildStorageReplacementCandidates(hint, params).slice(0, 4);
|
|
1331
|
-
for (const candidate of candidates) {
|
|
1332
|
-
try {
|
|
1333
|
-
const written = await writeStorageHintCandidate(browser, hint, candidate);
|
|
1334
|
-
if (!written)
|
|
1335
|
-
continue;
|
|
1336
|
-
await browser.currentPage.reload({ waitUntil: 'domcontentloaded', timeout: 15000 }).catch(async () => {
|
|
1337
|
-
await browser.navigateTo(browser.currentPage.url());
|
|
1338
|
-
});
|
|
1339
|
-
await browser.wait(350);
|
|
1340
|
-
validation = await revalidate(browser, params);
|
|
1341
|
-
signals = await refreshSignals(signals);
|
|
1342
|
-
if (validation.validationStatus === 'valid') {
|
|
1343
|
-
return {
|
|
1344
|
-
repaired: true,
|
|
1345
|
-
validation,
|
|
1346
|
-
pathUsed: hint.kind === 'locale' ? 'lang_storage' : 'theme_storage',
|
|
1347
|
-
updates,
|
|
1348
|
-
};
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1351
|
-
catch {
|
|
1352
|
-
// Try the next candidate/value.
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
const expandableSelectors = [
|
|
1358
|
-
...uniqueSelectors(params.selectorMemory?.['menu:primary']).slice(0, 2),
|
|
1359
|
-
...uniqueSelectors(params.selectorMemory?.['account:menu']).slice(0, 2),
|
|
1360
|
-
];
|
|
1361
|
-
if (expandableSelectors.length > 0 && (needsHardLangRepair(validation) || needsHardThemeRepair(validation))) {
|
|
1362
|
-
const menuAttempt = await trySelectorSet(browser, expandableSelectors, async (selector) => {
|
|
1363
|
-
await browser.safeExpand({ selector });
|
|
1364
|
-
}, {
|
|
1365
|
-
signature: 'menu:primary',
|
|
1366
|
-
validationParams: params,
|
|
1367
|
-
});
|
|
1368
|
-
remember(menuAttempt.updates);
|
|
1369
|
-
validation = menuAttempt.validation;
|
|
1370
|
-
if (menuAttempt.updates.length > 0) {
|
|
1371
|
-
signals = await refreshSignals(signals);
|
|
1372
|
-
}
|
|
1373
|
-
if (validation.validationStatus === 'valid') {
|
|
1374
|
-
return { repaired: true, validation, pathUsed: 'menu_expand', updates };
|
|
1375
|
-
}
|
|
1376
|
-
const expandedSignals = await refreshSignals(signals);
|
|
1377
|
-
if (expandedSignals) {
|
|
1378
|
-
if (params.requestedLang && needsHardLangRepair(validation)) {
|
|
1379
|
-
const control = expandedSignals.variantControls.find((entry) => controlMatchesVariant(entry, 'locale', params.requestedLang));
|
|
1380
|
-
if (control) {
|
|
1381
|
-
try {
|
|
1382
|
-
await applyVariantSelector(browser, control.selector, {
|
|
1383
|
-
kind: 'locale',
|
|
1384
|
-
target: params.requestedLang,
|
|
1385
|
-
});
|
|
1386
|
-
await browser.wait(350);
|
|
1387
|
-
validation = await revalidate(browser, params);
|
|
1388
|
-
signals = await refreshSignals(expandedSignals);
|
|
1389
|
-
remember([{
|
|
1390
|
-
stepSignature: `lang:option:${normalizeLangCode(params.requestedLang) ?? params.requestedLang}`,
|
|
1391
|
-
selector: control.selector,
|
|
1392
|
-
source: 'deterministic',
|
|
1393
|
-
success: validation.validationStatus !== 'invalid',
|
|
1394
|
-
}]);
|
|
1395
|
-
if (validation.validationStatus === 'valid') {
|
|
1396
|
-
return { repaired: true, validation, pathUsed: 'menu_expand_lang_control', updates };
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1399
|
-
catch {
|
|
1400
|
-
remember([{
|
|
1401
|
-
stepSignature: `lang:option:${normalizeLangCode(params.requestedLang) ?? params.requestedLang}`,
|
|
1402
|
-
selector: control.selector,
|
|
1403
|
-
source: 'deterministic',
|
|
1404
|
-
success: false,
|
|
1405
|
-
}]);
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
if (params.requestedTheme && needsHardThemeRepair(validation)) {
|
|
1410
|
-
const control = expandedSignals.variantControls.find((entry) => controlMatchesVariant(entry, 'theme', params.requestedTheme));
|
|
1411
|
-
if (control) {
|
|
1412
|
-
try {
|
|
1413
|
-
await applyVariantSelector(browser, control.selector, {
|
|
1414
|
-
kind: 'theme',
|
|
1415
|
-
target: params.requestedTheme,
|
|
1416
|
-
});
|
|
1417
|
-
await browser.wait(350);
|
|
1418
|
-
validation = await revalidate(browser, params);
|
|
1419
|
-
signals = await refreshSignals(expandedSignals);
|
|
1420
|
-
remember([{
|
|
1421
|
-
stepSignature: `theme:option:${params.requestedTheme}`,
|
|
1422
|
-
selector: control.selector,
|
|
1423
|
-
source: 'deterministic',
|
|
1424
|
-
success: validation.validationStatus !== 'invalid',
|
|
1425
|
-
}]);
|
|
1426
|
-
if (validation.validationStatus === 'valid') {
|
|
1427
|
-
return { repaired: true, validation, pathUsed: 'menu_expand_theme_control', updates };
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1430
|
-
catch {
|
|
1431
|
-
remember([{
|
|
1432
|
-
stepSignature: `theme:option:${params.requestedTheme}`,
|
|
1433
|
-
selector: control.selector,
|
|
1434
|
-
source: 'deterministic',
|
|
1435
|
-
success: false,
|
|
1436
|
-
}]);
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
return { repaired: false, validation, pathUsed: null, updates };
|
|
1443
|
-
}
|
|
1444
|
-
export function buildProfileSelectorUpdates(signals, params) {
|
|
1445
|
-
const updates = [];
|
|
1446
|
-
const requestedLang = normalizeLangCode(params.requestedLang);
|
|
1447
|
-
for (const control of signals.variantControls) {
|
|
1448
|
-
if (control.kind === 'locale') {
|
|
1449
|
-
updates.push({
|
|
1450
|
-
stepSignature: requestedLang && controlMatchesVariant(control, 'locale', requestedLang)
|
|
1451
|
-
? `lang:option:${requestedLang}`
|
|
1452
|
-
: 'lang:switcher',
|
|
1453
|
-
selector: control.selector,
|
|
1454
|
-
source: 'deterministic',
|
|
1455
|
-
success: true,
|
|
1456
|
-
});
|
|
1457
|
-
}
|
|
1458
|
-
else if (control.kind === 'theme') {
|
|
1459
|
-
updates.push({
|
|
1460
|
-
stepSignature: params.requestedTheme && controlMatchesVariant(control, 'theme', params.requestedTheme)
|
|
1461
|
-
? `theme:option:${params.requestedTheme}`
|
|
1462
|
-
: 'theme:toggle',
|
|
1463
|
-
selector: control.selector,
|
|
1464
|
-
source: 'deterministic',
|
|
1465
|
-
success: true,
|
|
1466
|
-
});
|
|
1467
|
-
}
|
|
1468
|
-
}
|
|
1469
|
-
return updates;
|
|
1470
|
-
}
|
|
1471
|
-
//# sourceMappingURL=session-profile.js.map
|