teamshare-bridge 0.21.26 → 0.21.28
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/commands/browse.js +390 -37
- package/dist/cli/commands/browse.js.map +1 -1
- package/dist/cli/commands/research.js +5 -0
- package/dist/cli/commands/research.js.map +1 -1
- package/dist/cli/index.js +6 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/lib/browser/human-input-guard.d.ts +15 -2
- package/dist/lib/browser/human-input-guard.js +18 -4
- package/dist/lib/browser/human-input-guard.js.map +1 -1
- package/package.json +2 -2
|
@@ -59,6 +59,8 @@ const child_process_1 = require("child_process");
|
|
|
59
59
|
const cdp_1 = require("../../lib/browser/cdp");
|
|
60
60
|
const launch_1 = require("../../lib/browser/launch");
|
|
61
61
|
const session_1 = require("../../lib/browser/session");
|
|
62
|
+
const personal_fields_1 = require("../../lib/browser/personal-fields");
|
|
63
|
+
const human_input_guard_1 = require("../../lib/browser/human-input-guard");
|
|
62
64
|
/** Create a fingerprint from the current element list. */
|
|
63
65
|
function createFingerprint(elements) {
|
|
64
66
|
const groups = new Map();
|
|
@@ -114,12 +116,25 @@ function calculateChangeScore(prev, curr) {
|
|
|
114
116
|
}
|
|
115
117
|
return score;
|
|
116
118
|
}
|
|
119
|
+
/** Fast path (easy objective questions): small reasoning budget. */
|
|
120
|
+
const FAST_MAX_TOKENS = 2048;
|
|
121
|
+
/** Deep path (hard coding questions): enough tokens to emit a full solution. */
|
|
122
|
+
const DEEP_MAX_TOKENS = 16384;
|
|
123
|
+
/** Attempt-1 ceiling: abandon a slow easy answer and escalate to the deep path. */
|
|
124
|
+
const FAST_ATTEMPT_TIMEOUT_MS = 45_000;
|
|
125
|
+
/**
|
|
126
|
+
* Rule 0 is swapped per attempt: the fast path forbids long deliberation, the
|
|
127
|
+
* deep path (entered when the fast attempt fails) explicitly allows it so hard
|
|
128
|
+
* coding questions get reasoned/complete answers.
|
|
129
|
+
*/
|
|
130
|
+
const FAST_RULE_0 = '0. BE FAST AND DECISIVE. Answer in a single pass with minimal reasoning - do NOT deliberate at length, re-check repeatedly, or restate the page. Emit the final JSON array as your primary output. Speed matters more than exhaustive deliberation.';
|
|
131
|
+
const DEEP_RULE_0 = '0. This may be a HARD problem (advanced coding / algorithms). Reason carefully before acting; correctness and efficiency matter more than speed. For coding questions, write the most time-efficient solution and emit the FULL solution via write_code.';
|
|
117
132
|
const SYSTEM_PROMPT = `You are a browser automation agent. A screenshot and a grouped list of interactive elements are attached.
|
|
118
133
|
|
|
119
134
|
Your task: identify unanswered questions and answer them by clicking the correct elements.
|
|
120
135
|
|
|
121
136
|
RULES (STRICT):
|
|
122
|
-
|
|
137
|
+
${FAST_RULE_0}
|
|
123
138
|
1. NEVER click Next, Submit, Back, Save, Continue, or any navigation. ONLY answer questions on the current screen.
|
|
124
139
|
2. NEVER ask for help. If uncertain, pick the BEST GUESS. Always decide — never wait.
|
|
125
140
|
3. Handle MULTIPLE unanswered questions in a SINGLE response.
|
|
@@ -239,10 +254,10 @@ CORRECTION MODE:
|
|
|
239
254
|
- If you previously typed wrong code, use write_code to replace with correct code
|
|
240
255
|
- You may be asked the same question multiple times — each time, check if the current answer is correct before acting
|
|
241
256
|
|
|
242
|
-
PERSONAL QUESTIONS —
|
|
243
|
-
-
|
|
244
|
-
- If a question asks for personal info, do NOT click or type —
|
|
245
|
-
- If ALL remaining questions are personal, return done
|
|
257
|
+
PERSONAL QUESTIONS — NEVER ANSWER THEM:
|
|
258
|
+
- Questions about the person operating the browser (name, email, phone, address, SSN, passport, password, card, bank, date of birth, salary, work authorization, personal profile links) are OFF LIMITS. Read the PERSONAL QUESTIONS addendum appended below and follow it exactly.
|
|
259
|
+
- If a question asks for personal info, do NOT click or type — leave it empty.
|
|
260
|
+
- A personal field left empty is NOT an unanswered question. If ALL remaining questions are personal, return done immediately.
|
|
246
261
|
|
|
247
262
|
RESPONSE FORMAT RULES (CRITICAL — follow exactly):
|
|
248
263
|
- Return ONLY a valid JSON array. No explanations, no markdown fences, no extra text.
|
|
@@ -444,16 +459,16 @@ async function cmdBrowse(flags) {
|
|
|
444
459
|
const maxActions = continuous ? Number.MAX_SAFE_INTEGER : parseInt(flags.get('max-actions') ?? '50', 10);
|
|
445
460
|
const intervalMs = parseInt(flags.get('interval') ?? '2000', 10);
|
|
446
461
|
const modelSpec = flags.get('model') ?? models_1.DEFAULT_VISION_MODEL;
|
|
447
|
-
//
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
const timeoutSec = Math.min(Math.max(parseInt(flags.get('timeout') ?? '
|
|
462
|
+
// Hard problems (advanced coding) need time; easy objective questions must
|
|
463
|
+
// not pay for it. 300s is the ceiling for a single decision (overridable);
|
|
464
|
+
// the fast retry below abandons a slow easy answer at ~45s and escalates.
|
|
465
|
+
const timeoutSec = Math.min(Math.max(parseInt(flags.get('timeout') ?? '300', 10), 30), 600);
|
|
451
466
|
const timeoutMs = timeoutSec * 1000;
|
|
452
467
|
const quality = Math.min(Math.max(parseInt(flags.get('quality') ?? '70', 10), 50), 100);
|
|
453
468
|
/**
|
|
454
|
-
* Downscale the screenshot
|
|
455
|
-
*
|
|
456
|
-
*
|
|
469
|
+
* Downscale the FAST-path screenshot (attempt 1) to keep easy questions
|
|
470
|
+
* quick. The deep retry re-captures at full resolution so code/text stays
|
|
471
|
+
* legible. Clicks are unaffected either way (actions use element coords).
|
|
457
472
|
*/
|
|
458
473
|
const SCREENSHOT_MAX_EDGE = 1024;
|
|
459
474
|
const maxConsecutiveFailures = parseInt(flags.get('max-consecutive-failures') ?? '0', 10); // 0 = unlimited
|
|
@@ -462,6 +477,15 @@ async function cmdBrowse(flags) {
|
|
|
462
477
|
const stealth = flags.has('stealth');
|
|
463
478
|
const hybrid = flags.has('hybrid');
|
|
464
479
|
const autoLaunch = flags.has('auto-launch');
|
|
480
|
+
// Field safety (see personal-fields.ts / human-input-guard.ts):
|
|
481
|
+
// --stop-on-user-input opt-in: stop typing and skip the field for the run
|
|
482
|
+
// as soon as the human types into it
|
|
483
|
+
// --personal-questions "allow" answers personal questions; credentials and
|
|
484
|
+
// card/bank fields stay blocked either way
|
|
485
|
+
const stopOnUserInput = flags.has('stop-on-user-input') && flags.get('stop-on-user-input') !== 'false';
|
|
486
|
+
const personalMode = (flags.get('personal-questions') ?? 'skip').toLowerCase();
|
|
487
|
+
fieldSafety.stopOnUserInput = stopOnUserInput;
|
|
488
|
+
fieldSafety.allowPersonal = personalMode === 'allow';
|
|
465
489
|
// The auto-answer browser is ALWAYS visible (2026-09): a headless window is
|
|
466
490
|
// the "profile seems mismatched" case and hides the pages the loop works on.
|
|
467
491
|
// `--headless` is accepted for backward compatibility but ignored.
|
|
@@ -493,6 +517,8 @@ async function cmdBrowse(flags) {
|
|
|
493
517
|
}
|
|
494
518
|
console.log(`[browse] Char delay: ${charDelayMs}ms`);
|
|
495
519
|
console.log(`[browse] Screenshot interval: ${intervalMs}ms`);
|
|
520
|
+
console.log(`[browse] Personal questions: ${fieldSafety.allowPersonal ? 'answered (credentials still blocked)' : 'never answered (left for you)'}`);
|
|
521
|
+
console.log(`[browse] Stop typing when you type in the field: ${fieldSafety.stopOnUserInput ? 'on' : 'off'}`);
|
|
496
522
|
if (!stealth) {
|
|
497
523
|
console.log(`[browse] Max consecutive failures: ${maxConsecutiveFailures === 0 ? 'unlimited' : maxConsecutiveFailures}`);
|
|
498
524
|
}
|
|
@@ -676,7 +702,20 @@ async function cmdBrowse(flags) {
|
|
|
676
702
|
/** The page changed under the LLM: re-analyze immediately, no backoff. */
|
|
677
703
|
let screenChangedDuringLlm = false;
|
|
678
704
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
679
|
-
|
|
705
|
+
// Fast-first ladder: attempt 1 uses a small budget + downscaled shot so
|
|
706
|
+
// easy objective questions answer quickly. If it fails, attempts 2+
|
|
707
|
+
// switch to the DEEP budget (full timeout + tokens, full-res capture)
|
|
708
|
+
// for hard coding questions. `max_tokens`/timeout are ceilings, so the
|
|
709
|
+
// deep path never slows a question that answers on the fast path.
|
|
710
|
+
const deep = attempt > 1;
|
|
711
|
+
const attemptTimeoutMs = deep ? timeoutMs : Math.min(FAST_ATTEMPT_TIMEOUT_MS, timeoutMs);
|
|
712
|
+
if (deep && attempt === 2) {
|
|
713
|
+
console.log('[browse] Fast attempt did not answer — escalating to the deep budget (more time + tokens)');
|
|
714
|
+
}
|
|
715
|
+
const attemptShot = deep
|
|
716
|
+
? ((await captureScreenshot(page, quality)) ?? screenshot)
|
|
717
|
+
: screenshot;
|
|
718
|
+
result = await analyzeScreenshot(baseUrl, apiKey, model, attemptShot, elements, pageContext, attemptTimeoutMs, attempt, maxRetries, page, lastFingerprint, { maxTokens: deep ? DEEP_MAX_TOKENS : FAST_MAX_TOKENS, deep });
|
|
680
719
|
llmCallCount++;
|
|
681
720
|
llmStats.calls++;
|
|
682
721
|
llmStats.totalMs += result.durationMs;
|
|
@@ -780,7 +819,7 @@ async function cmdBrowse(flags) {
|
|
|
780
819
|
if (hasUnansweredQuestions(elements)) {
|
|
781
820
|
console.log(`[browse] LLM says done but unanswered questions remain — retrying...`);
|
|
782
821
|
// Force retry with stronger prompt
|
|
783
|
-
result = await analyzeScreenshot(baseUrl, apiKey, model, screenshot, elements, pageContext, timeoutMs, 1, 1, page, lastFingerprint);
|
|
822
|
+
result = await analyzeScreenshot(baseUrl, apiKey, model, screenshot, elements, pageContext, timeoutMs, 1, 1, page, lastFingerprint, { maxTokens: DEEP_MAX_TOKENS, deep: true });
|
|
784
823
|
if (result.actions.length > 0 && result.actions[0].action !== 'done') {
|
|
785
824
|
// LLM returned actual actions — re-process from the start of this loop
|
|
786
825
|
executedAny = false;
|
|
@@ -826,6 +865,18 @@ async function cmdBrowse(flags) {
|
|
|
826
865
|
console.log(`[browse] Skipping ${act.action} — code already confirmed optimal`);
|
|
827
866
|
continue;
|
|
828
867
|
}
|
|
868
|
+
// Field safety: never type into a personal field, a credential field, or
|
|
869
|
+
// a field the human already answered themselves.
|
|
870
|
+
if (act.action === 'write_code' || act.action === 'type') {
|
|
871
|
+
const targetEl = act.elementIndex !== undefined
|
|
872
|
+
? elements.find((e) => e.i === act.elementIndex)
|
|
873
|
+
: undefined;
|
|
874
|
+
const gated = fieldGate(targetEl);
|
|
875
|
+
if (gated) {
|
|
876
|
+
console.log(`[browse] Skipping element ${act.elementIndex} - ${gated}`);
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
829
880
|
// Ask LLM to decide if action is redundant
|
|
830
881
|
const redundancy = await isActionRedundant(baseUrl, apiKey, model, page, elements, act, timeoutMs);
|
|
831
882
|
if (redundancy.decision === 'skip') {
|
|
@@ -1017,6 +1068,14 @@ Do NOT simplify variable names or change the function signature. Only optimize t
|
|
|
1017
1068
|
console.log(`[browse] Reached max actions (${maxActions})`);
|
|
1018
1069
|
}
|
|
1019
1070
|
console.log(`[browse] Agent finished. ${actionCount} actions performed.`);
|
|
1071
|
+
const skipped = personalSkipSummary();
|
|
1072
|
+
if (skipped.personal > 0 || skipped.human > 0) {
|
|
1073
|
+
console.log(`[browse] Left for you: ${skipped.personal} personal question(s) never answered` +
|
|
1074
|
+
(skipped.human > 0 ? `, ${skipped.human} field(s) you answered yourself` : ''));
|
|
1075
|
+
for (const entry of skippedFields.values()) {
|
|
1076
|
+
console.log(` - ${entry.note}`);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1020
1079
|
page.ws.close();
|
|
1021
1080
|
}
|
|
1022
1081
|
// ── CDP Helpers ──────────────────────────────────────────────────────────────
|
|
@@ -1184,6 +1243,124 @@ async function captureScreenshot(page, quality, maxEdge = 0) {
|
|
|
1184
1243
|
return null;
|
|
1185
1244
|
}
|
|
1186
1245
|
}
|
|
1246
|
+
/**
|
|
1247
|
+
* Field-safety addendum appended to every system prompt (main loop, custom /
|
|
1248
|
+
* optimization passes, stealth loop). The loop may not answer questions that
|
|
1249
|
+
* ask about the human behind the browser.
|
|
1250
|
+
*/
|
|
1251
|
+
const PERSONAL_SAFETY_RULES = `
|
|
1252
|
+
PERSONAL QUESTIONS - NEVER ANSWER THEM:
|
|
1253
|
+
- Some questions ask about YOU (the human operating the browser), not about the
|
|
1254
|
+
task. NEVER answer them with an invented value.
|
|
1255
|
+
- Never type: names, email addresses, phone numbers, postal addresses, dates of
|
|
1256
|
+
birth or age, government IDs (SSN/NIN/passport/driving licence/tax id),
|
|
1257
|
+
passwords, one-time codes, card or bank details, salary or compensation,
|
|
1258
|
+
notice periods, work-authorization/visa/sponsorship answers, or personal
|
|
1259
|
+
profile links (LinkedIn, GitHub, portfolio, personal website).
|
|
1260
|
+
- Fields marked "[NEVER ANSWER: ...]" or "[PERSONAL: ...]" in the element list
|
|
1261
|
+
MUST be left untouched - do not click them, do not type into them.
|
|
1262
|
+
- Leaving a personal field EMPTY is correct and expected. A personal field is
|
|
1263
|
+
NOT an unanswered question: never treat it as one, and never delay "done"
|
|
1264
|
+
because of it.
|
|
1265
|
+
- If every remaining question is personal, return done.
|
|
1266
|
+
- Demographics and free-text personal questions (gender, marital status, "tell
|
|
1267
|
+
us about yourself") are the human's to answer: prefer to skip them too.
|
|
1268
|
+
`;
|
|
1269
|
+
const fieldSafety = {
|
|
1270
|
+
stopOnUserInput: false,
|
|
1271
|
+
allowPersonal: false,
|
|
1272
|
+
};
|
|
1273
|
+
/** fieldKey -> why the field must not be typed into for the rest of the run. */
|
|
1274
|
+
const skippedFields = new Map();
|
|
1275
|
+
/**
|
|
1276
|
+
* Stealth mode has no DOM, so fields are identified by the focused OS control
|
|
1277
|
+
* (AutomationId|ClassName|Name) instead of a field key.
|
|
1278
|
+
*/
|
|
1279
|
+
const skippedOsFields = new Map();
|
|
1280
|
+
function signalsOf(el) {
|
|
1281
|
+
return {
|
|
1282
|
+
tag: el.tag,
|
|
1283
|
+
type: el.type,
|
|
1284
|
+
name: el.name,
|
|
1285
|
+
id: el.id,
|
|
1286
|
+
autocomplete: el.autocomplete,
|
|
1287
|
+
placeholder: el.placeholder,
|
|
1288
|
+
ariaLabel: el.ariaLabel,
|
|
1289
|
+
label: el.label,
|
|
1290
|
+
questionText: el.questionText,
|
|
1291
|
+
text: el.text,
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
function classificationOf(el) {
|
|
1295
|
+
return {
|
|
1296
|
+
tier: el.personalTier ?? null,
|
|
1297
|
+
category: el.personalCategory ?? null,
|
|
1298
|
+
label: el.personalLabel ?? '',
|
|
1299
|
+
reason: el.personalReason ?? '',
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Classify every typable element and stamp its stable field key. Elements are
|
|
1304
|
+
* annotated in place so the gate below never re-parses the DOM.
|
|
1305
|
+
*/
|
|
1306
|
+
function annotateFieldSafety(elements) {
|
|
1307
|
+
for (const el of elements) {
|
|
1308
|
+
const signals = signalsOf(el);
|
|
1309
|
+
el.fieldKey = (0, personal_fields_1.fieldKeyOf)(signals);
|
|
1310
|
+
if (!el.typable)
|
|
1311
|
+
continue;
|
|
1312
|
+
const cls = (0, personal_fields_1.classifyField)(signals);
|
|
1313
|
+
el.personalTier = cls.tier;
|
|
1314
|
+
el.personalCategory = cls.category;
|
|
1315
|
+
el.personalLabel = cls.label;
|
|
1316
|
+
el.personalReason = cls.reason;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
function shortLabel(el) {
|
|
1320
|
+
const raw = el.label || el.questionText || el.text || el.placeholder || el.name || '';
|
|
1321
|
+
return raw.replace(/\s+/g, ' ').trim().slice(0, 60);
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* Returns a human-readable reason when this element must not be typed into,
|
|
1325
|
+
* or null when it is fine to type.
|
|
1326
|
+
*/
|
|
1327
|
+
function fieldGate(el) {
|
|
1328
|
+
if (!el || !el.typable)
|
|
1329
|
+
return null;
|
|
1330
|
+
const key = el.fieldKey ?? (0, personal_fields_1.fieldKeyOf)(signalsOf(el));
|
|
1331
|
+
const already = skippedFields.get(key);
|
|
1332
|
+
if (already)
|
|
1333
|
+
return already.note;
|
|
1334
|
+
const cls = classificationOf(el);
|
|
1335
|
+
if (cls.tier === null)
|
|
1336
|
+
return null;
|
|
1337
|
+
if (!(0, personal_fields_1.isBlocked)(cls, { allowPersonal: fieldSafety.allowPersonal }))
|
|
1338
|
+
return null;
|
|
1339
|
+
const note = `left to you (${cls.category}: "${shortLabel(el) || cls.label}")`;
|
|
1340
|
+
skippedFields.set(key, { kind: 'personal', note });
|
|
1341
|
+
console.log(`[browse] Not answering a personal question - ${note}`);
|
|
1342
|
+
return note;
|
|
1343
|
+
}
|
|
1344
|
+
/** Record a field the human took over mid-typing; it is never typed again. */
|
|
1345
|
+
function noteHumanTookOver(el, reason) {
|
|
1346
|
+
const key = el?.fieldKey ?? (el ? (0, personal_fields_1.fieldKeyOf)(signalsOf(el)) : '');
|
|
1347
|
+
if (!key)
|
|
1348
|
+
return;
|
|
1349
|
+
const note = `you answered this one yourself (${reason})`;
|
|
1350
|
+
skippedFields.set(key, { kind: 'human', note });
|
|
1351
|
+
console.log(`[browse] Stopped typing - ${note}`);
|
|
1352
|
+
}
|
|
1353
|
+
function personalSkipSummary() {
|
|
1354
|
+
let personal = 0;
|
|
1355
|
+
let human = 0;
|
|
1356
|
+
for (const entry of skippedFields.values()) {
|
|
1357
|
+
if (entry.kind === 'personal')
|
|
1358
|
+
personal++;
|
|
1359
|
+
else
|
|
1360
|
+
human++;
|
|
1361
|
+
}
|
|
1362
|
+
return { personal, human };
|
|
1363
|
+
}
|
|
1187
1364
|
/**
|
|
1188
1365
|
* Extract all interactive elements from the DOM with their exact text,
|
|
1189
1366
|
* bounding boxes, state, and question group assignments.
|
|
@@ -1224,6 +1401,29 @@ async function extractElements(page) {
|
|
|
1224
1401
|
text,
|
|
1225
1402
|
tag: el.tagName.toLowerCase(),
|
|
1226
1403
|
type: el.type || el.getAttribute('role') || '',
|
|
1404
|
+
typable: el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.isContentEditable === true,
|
|
1405
|
+
name: el.getAttribute('name') || '',
|
|
1406
|
+
id: el.getAttribute('id') || '',
|
|
1407
|
+
autocomplete: el.getAttribute('autocomplete') || '',
|
|
1408
|
+
placeholder: el.getAttribute('placeholder') || '',
|
|
1409
|
+
ariaLabel: el.getAttribute('aria-label') || '',
|
|
1410
|
+
label: (() => {
|
|
1411
|
+
try {
|
|
1412
|
+
const id = el.getAttribute('id');
|
|
1413
|
+
if (id) {
|
|
1414
|
+
const forLabel = document.querySelector('label[for="' + CSS.escape(id) + '"]');
|
|
1415
|
+
if (forLabel && forLabel.innerText) return forLabel.innerText.trim().replace(/\\s+/g, ' ').slice(0, 200);
|
|
1416
|
+
}
|
|
1417
|
+
const wrap = el.closest ? el.closest('label') : null;
|
|
1418
|
+
if (wrap && wrap.innerText) return wrap.innerText.trim().replace(/\\s+/g, ' ').slice(0, 200);
|
|
1419
|
+
const labelled = el.getAttribute('aria-labelledby');
|
|
1420
|
+
if (labelled) {
|
|
1421
|
+
const t = document.getElementById(labelled);
|
|
1422
|
+
if (t && t.innerText) return t.innerText.trim().replace(/\\s+/g, ' ').slice(0, 200);
|
|
1423
|
+
}
|
|
1424
|
+
} catch (err) {}
|
|
1425
|
+
return '';
|
|
1426
|
+
})(),
|
|
1227
1427
|
checked: el.checked || el.getAttribute('aria-checked') === 'true' || false,
|
|
1228
1428
|
x: Math.round(rect.x + rect.width / 2),
|
|
1229
1429
|
y: Math.round(rect.y + rect.height / 2),
|
|
@@ -1336,6 +1536,13 @@ async function extractElements(page) {
|
|
|
1336
1536
|
y: item.y,
|
|
1337
1537
|
group: item.groupId,
|
|
1338
1538
|
inViewport: item.inViewport,
|
|
1539
|
+
typable: item.typable,
|
|
1540
|
+
name: item.name,
|
|
1541
|
+
id: item.id,
|
|
1542
|
+
autocomplete: item.autocomplete,
|
|
1543
|
+
placeholder: item.placeholder,
|
|
1544
|
+
ariaLabel: item.ariaLabel,
|
|
1545
|
+
label: item.label,
|
|
1339
1546
|
});
|
|
1340
1547
|
});
|
|
1341
1548
|
|
|
@@ -1344,7 +1551,9 @@ async function extractElements(page) {
|
|
|
1344
1551
|
`;
|
|
1345
1552
|
try {
|
|
1346
1553
|
const result = await cdpEval(page, js);
|
|
1347
|
-
|
|
1554
|
+
const elements = result?.result?.value || [];
|
|
1555
|
+
annotateFieldSafety(elements);
|
|
1556
|
+
return elements;
|
|
1348
1557
|
}
|
|
1349
1558
|
catch {
|
|
1350
1559
|
return [];
|
|
@@ -1393,6 +1602,11 @@ async function executeAction(page, act, elements, charDelayMs) {
|
|
|
1393
1602
|
console.log(`[browse] Element index ${act.elementIndex} not found, will retry`);
|
|
1394
1603
|
return false;
|
|
1395
1604
|
}
|
|
1605
|
+
const gated = fieldGate(el);
|
|
1606
|
+
if (gated) {
|
|
1607
|
+
console.log(`[browse] Skipping element ${el.i} - ${gated}`);
|
|
1608
|
+
return true;
|
|
1609
|
+
}
|
|
1396
1610
|
// 1. Detect editor type
|
|
1397
1611
|
const editor = await detectEditor(page);
|
|
1398
1612
|
const isCodeEditor = editor !== null;
|
|
@@ -1407,12 +1621,19 @@ async function executeAction(page, act, elements, charDelayMs) {
|
|
|
1407
1621
|
// 3. Normalize indentation for code editors (LLM often adds extra indent)
|
|
1408
1622
|
const text = isCodeEditor ? normalizeCodeIndent(rawText) : rawText;
|
|
1409
1623
|
console.log(`[browse] Typing ${text.length} characters...`);
|
|
1624
|
+
const guard = new human_input_guard_1.TypingGuard({
|
|
1625
|
+
enabled: fieldSafety.stopOnUserInput,
|
|
1626
|
+
page,
|
|
1627
|
+
label: shortLabel(el),
|
|
1628
|
+
});
|
|
1410
1629
|
if (isCodeEditor) {
|
|
1411
|
-
await insertIntoCodeEditor(page, editor.selector, text, charDelayMs);
|
|
1630
|
+
await insertIntoCodeEditor(page, editor.selector, text, charDelayMs, guard);
|
|
1412
1631
|
}
|
|
1413
1632
|
else {
|
|
1414
|
-
await insertIntoGenericInput(page, el, text, charDelayMs);
|
|
1633
|
+
await insertIntoGenericInput(page, el, text, charDelayMs, guard);
|
|
1415
1634
|
}
|
|
1635
|
+
if (guard.stopped)
|
|
1636
|
+
noteHumanTookOver(el, guard.describeStop());
|
|
1416
1637
|
return true;
|
|
1417
1638
|
}
|
|
1418
1639
|
case 'write_code': {
|
|
@@ -1423,6 +1644,11 @@ async function executeAction(page, act, elements, charDelayMs) {
|
|
|
1423
1644
|
console.log(`[browse] Element index ${act.elementIndex} not found, will retry`);
|
|
1424
1645
|
return false;
|
|
1425
1646
|
}
|
|
1647
|
+
const gated = fieldGate(el);
|
|
1648
|
+
if (gated) {
|
|
1649
|
+
console.log(`[browse] Skipping element ${el.i} - ${gated}`);
|
|
1650
|
+
return true;
|
|
1651
|
+
}
|
|
1426
1652
|
// 1. Detect editor type
|
|
1427
1653
|
const editor = await detectEditor(page);
|
|
1428
1654
|
const isCodeEditor = editor !== null;
|
|
@@ -1435,13 +1661,20 @@ async function executeAction(page, act, elements, charDelayMs) {
|
|
|
1435
1661
|
.replace(/\\"/g, '"')
|
|
1436
1662
|
.replace(/\\\\/g, '\\');
|
|
1437
1663
|
console.log(`[browse] Writing code (${text.length} chars)...`);
|
|
1664
|
+
const guard = new human_input_guard_1.TypingGuard({
|
|
1665
|
+
enabled: fieldSafety.stopOnUserInput,
|
|
1666
|
+
page,
|
|
1667
|
+
label: shortLabel(el),
|
|
1668
|
+
});
|
|
1438
1669
|
// 3. write_code inserts EXACTLY as-is — no normalizeCodeIndent
|
|
1439
1670
|
if (isCodeEditor) {
|
|
1440
|
-
await insertIntoCodeEditor(page, editor.selector, text, charDelayMs);
|
|
1671
|
+
await insertIntoCodeEditor(page, editor.selector, text, charDelayMs, guard);
|
|
1441
1672
|
}
|
|
1442
1673
|
else {
|
|
1443
|
-
await insertIntoGenericInput(page, el, text, charDelayMs);
|
|
1674
|
+
await insertIntoGenericInput(page, el, text, charDelayMs, guard);
|
|
1444
1675
|
}
|
|
1676
|
+
if (guard.stopped)
|
|
1677
|
+
noteHumanTookOver(el, guard.describeStop());
|
|
1445
1678
|
return true;
|
|
1446
1679
|
}
|
|
1447
1680
|
case 'scroll': {
|
|
@@ -1515,6 +1748,11 @@ async function executeActionHybrid(page, act, elements, charDelayMs) {
|
|
|
1515
1748
|
console.log(`[browse] Element index ${act.elementIndex} not found, will retry`);
|
|
1516
1749
|
return false;
|
|
1517
1750
|
}
|
|
1751
|
+
const gated = fieldGate(el);
|
|
1752
|
+
if (gated) {
|
|
1753
|
+
console.log(`[browse] Skipping element ${el.i} - ${gated}`);
|
|
1754
|
+
return true;
|
|
1755
|
+
}
|
|
1518
1756
|
const editor = await detectEditor(page);
|
|
1519
1757
|
const isCodeEditor = editor !== null;
|
|
1520
1758
|
if (editor)
|
|
@@ -1533,7 +1771,16 @@ async function executeActionHybrid(page, act, elements, charDelayMs) {
|
|
|
1533
1771
|
pressKeyOS('ctrl+a');
|
|
1534
1772
|
await sleep(randomInt(50, 100));
|
|
1535
1773
|
}
|
|
1536
|
-
|
|
1774
|
+
const guard = new human_input_guard_1.TypingGuard({
|
|
1775
|
+
enabled: fieldSafety.stopOnUserInput,
|
|
1776
|
+
page,
|
|
1777
|
+
label: shortLabel(el),
|
|
1778
|
+
});
|
|
1779
|
+
if (guard)
|
|
1780
|
+
await guard.arm({ isCodeEditor });
|
|
1781
|
+
await typeHumanLike(text, charDelayMs, guard);
|
|
1782
|
+
if (guard.stopped)
|
|
1783
|
+
noteHumanTookOver(el, guard.describeStop());
|
|
1537
1784
|
return true;
|
|
1538
1785
|
}
|
|
1539
1786
|
case 'write_code': {
|
|
@@ -1544,6 +1791,11 @@ async function executeActionHybrid(page, act, elements, charDelayMs) {
|
|
|
1544
1791
|
console.log(`[browse] Element index ${act.elementIndex} not found, will retry`);
|
|
1545
1792
|
return false;
|
|
1546
1793
|
}
|
|
1794
|
+
const gated = fieldGate(el);
|
|
1795
|
+
if (gated) {
|
|
1796
|
+
console.log(`[browse] Skipping element ${el.i} - ${gated}`);
|
|
1797
|
+
return true;
|
|
1798
|
+
}
|
|
1547
1799
|
const editor = await detectEditor(page);
|
|
1548
1800
|
const isCodeEditor = editor !== null;
|
|
1549
1801
|
if (editor)
|
|
@@ -1561,7 +1813,16 @@ async function executeActionHybrid(page, act, elements, charDelayMs) {
|
|
|
1561
1813
|
pressKeyOS('ctrl+a');
|
|
1562
1814
|
await sleep(randomInt(50, 100));
|
|
1563
1815
|
}
|
|
1564
|
-
|
|
1816
|
+
const guard = new human_input_guard_1.TypingGuard({
|
|
1817
|
+
enabled: fieldSafety.stopOnUserInput,
|
|
1818
|
+
page,
|
|
1819
|
+
label: shortLabel(el),
|
|
1820
|
+
});
|
|
1821
|
+
if (guard)
|
|
1822
|
+
await guard.arm({ isCodeEditor });
|
|
1823
|
+
await typeHumanLike(text, charDelayMs, guard);
|
|
1824
|
+
if (guard.stopped)
|
|
1825
|
+
noteHumanTookOver(el, guard.describeStop());
|
|
1565
1826
|
return true;
|
|
1566
1827
|
}
|
|
1567
1828
|
case 'scroll': {
|
|
@@ -1748,13 +2009,21 @@ function hasUnansweredQuestions(elements) {
|
|
|
1748
2009
|
const types = els.map((e) => e.type);
|
|
1749
2010
|
const isRadio = types.some((t) => t === 'radio');
|
|
1750
2011
|
const isCheckbox = types.some((t) => t === 'checkbox');
|
|
1751
|
-
|
|
2012
|
+
// Personal fields are the human's to answer: they are never "unanswered"
|
|
2013
|
+
// for the agent, so a `done` from the model with them left empty sticks
|
|
2014
|
+
// instead of triggering the "you must answer everything" retry loop.
|
|
2015
|
+
const inputs = els.filter((e) => (e.tag === 'input' || e.tag === 'textarea') &&
|
|
2016
|
+
e.type !== 'radio' &&
|
|
2017
|
+
e.type !== 'checkbox' &&
|
|
2018
|
+
e.typable !== false);
|
|
2019
|
+
const openInputs = inputs.filter((e) => !(0, personal_fields_1.excludesFromCompleteness)(classificationOf(e)));
|
|
2020
|
+
const hasInput = openInputs.length > 0;
|
|
1752
2021
|
const checkedCount = els.filter((e) => e.checked).length;
|
|
1753
2022
|
if (isRadio && checkedCount === 0)
|
|
1754
2023
|
return true;
|
|
1755
2024
|
if (isCheckbox && checkedCount < els.length)
|
|
1756
2025
|
return true;
|
|
1757
|
-
if (hasInput && !
|
|
2026
|
+
if (hasInput && !openInputs.some((e) => e.text && e.tag === 'input'))
|
|
1758
2027
|
return true;
|
|
1759
2028
|
}
|
|
1760
2029
|
return false;
|
|
@@ -1979,7 +2248,7 @@ async function removeTrailingBrackets(page) {
|
|
|
1979
2248
|
console.log(`[browse] Removed ${extra} extra trailing bracket(s)`);
|
|
1980
2249
|
}
|
|
1981
2250
|
/** Insert text into ANY code editor — character-by-character typing, no pasting. */
|
|
1982
|
-
async function insertIntoCodeEditor(page, selector, text, charDelayMs) {
|
|
2251
|
+
async function insertIntoCodeEditor(page, selector, text, charDelayMs, guard) {
|
|
1983
2252
|
// Focus the editor
|
|
1984
2253
|
await focusSelector(page, selector);
|
|
1985
2254
|
await sleep(100);
|
|
@@ -2031,8 +2300,12 @@ async function insertIntoCodeEditor(page, selector, text, charDelayMs) {
|
|
|
2031
2300
|
// Type character by character — NO pasting
|
|
2032
2301
|
const est = Math.round(text.length * charDelayMs / 1000);
|
|
2033
2302
|
console.log(`[browse] Typing ${text.length} chars (~${est}s est.)...`);
|
|
2303
|
+
if (guard)
|
|
2304
|
+
await guard.arm({ selector, isCodeEditor: true });
|
|
2034
2305
|
for (let i = 0; i < text.length; i++) {
|
|
2035
2306
|
const char = text[i];
|
|
2307
|
+
if (guard)
|
|
2308
|
+
await guard.markOwn(char, char === '\n' ? 'Enter' : char === '\t' ? 'Tab' : undefined);
|
|
2036
2309
|
if (char === '\n') {
|
|
2037
2310
|
await cdpSend(page, 'Input.dispatchKeyEvent', {
|
|
2038
2311
|
type: 'keyDown', key: 'Enter', code: 'Enter', text: '\r',
|
|
@@ -2070,7 +2343,16 @@ async function insertIntoCodeEditor(page, selector, text, charDelayMs) {
|
|
|
2070
2343
|
await cdpSend(page, 'Input.dispatchKeyEvent', upParams);
|
|
2071
2344
|
}
|
|
2072
2345
|
await sleep(variableDelay(charDelayMs, char, i, text.length));
|
|
2346
|
+
if (guard) {
|
|
2347
|
+
await guard.tick(text.slice(0, i + 1));
|
|
2348
|
+
if (guard.stopped) {
|
|
2349
|
+
console.log(`[browse] Typing stopped after ${i + 1}/${text.length} chars`);
|
|
2350
|
+
break;
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2073
2353
|
}
|
|
2354
|
+
if (guard)
|
|
2355
|
+
await guard.dispose();
|
|
2074
2356
|
// Post-typing: detect and remove extra trailing brackets
|
|
2075
2357
|
await removeTrailingBrackets(page);
|
|
2076
2358
|
// Re-enable auto-formatting
|
|
@@ -2091,7 +2373,7 @@ async function insertIntoCodeEditor(page, selector, text, charDelayMs) {
|
|
|
2091
2373
|
console.log(`[browse] Typing complete (${text.length} chars)`);
|
|
2092
2374
|
}
|
|
2093
2375
|
/** Insert text into a generic input — click focus + char-by-char typing. */
|
|
2094
|
-
async function insertIntoGenericInput(page, el, text, charDelayMs) {
|
|
2376
|
+
async function insertIntoGenericInput(page, el, text, charDelayMs, guard) {
|
|
2095
2377
|
await cdpSend(page, 'Input.dispatchMouseEvent', {
|
|
2096
2378
|
type: 'mousePressed', x: el.x, y: el.y, button: 'left', clickCount: 1,
|
|
2097
2379
|
});
|
|
@@ -2101,8 +2383,12 @@ async function insertIntoGenericInput(page, el, text, charDelayMs) {
|
|
|
2101
2383
|
await sleep(200);
|
|
2102
2384
|
await clearEditor(page);
|
|
2103
2385
|
await sleep(200);
|
|
2386
|
+
if (guard)
|
|
2387
|
+
await guard.arm({ isCodeEditor: false });
|
|
2104
2388
|
for (let i = 0; i < text.length; i++) {
|
|
2105
2389
|
const char = text[i];
|
|
2390
|
+
if (guard)
|
|
2391
|
+
await guard.markOwn(char, char === '\n' ? 'Enter' : char === '\t' ? 'Tab' : undefined);
|
|
2106
2392
|
if (char === '\n') {
|
|
2107
2393
|
await cdpSend(page, 'Input.dispatchKeyEvent', {
|
|
2108
2394
|
type: 'keyDown', key: 'Enter', code: 'Enter', text: '\r',
|
|
@@ -2140,7 +2426,16 @@ async function insertIntoGenericInput(page, el, text, charDelayMs) {
|
|
|
2140
2426
|
await cdpSend(page, 'Input.dispatchKeyEvent', upParams);
|
|
2141
2427
|
}
|
|
2142
2428
|
await sleep(variableDelay(charDelayMs, char, i, text.length));
|
|
2429
|
+
if (guard) {
|
|
2430
|
+
await guard.tick(text.slice(0, i + 1));
|
|
2431
|
+
if (guard.stopped) {
|
|
2432
|
+
console.log(`[browse] Typing stopped after ${i + 1}/${text.length} chars`);
|
|
2433
|
+
break;
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2143
2436
|
}
|
|
2437
|
+
if (guard)
|
|
2438
|
+
await guard.dispose();
|
|
2144
2439
|
}
|
|
2145
2440
|
/**
|
|
2146
2441
|
* Extract page context for the LLM: title, URL, headings, errors, editor content.
|
|
@@ -2445,7 +2740,10 @@ function extractScatteredActions(str) {
|
|
|
2445
2740
|
}
|
|
2446
2741
|
return actions;
|
|
2447
2742
|
}
|
|
2448
|
-
async function analyzeScreenshot(baseUrl, apiKey, model, screenshotBase64, elements, pageContext, timeoutMs, attempt, maxAttempts, page, lastFingerprint
|
|
2743
|
+
async function analyzeScreenshot(baseUrl, apiKey, model, screenshotBase64, elements, pageContext, timeoutMs, attempt, maxAttempts, page, lastFingerprint, budget = {
|
|
2744
|
+
maxTokens: DEEP_MAX_TOKENS,
|
|
2745
|
+
deep: false,
|
|
2746
|
+
}) {
|
|
2449
2747
|
const startTime = Date.now();
|
|
2450
2748
|
// Format element list grouped by question
|
|
2451
2749
|
const groups = new Map();
|
|
@@ -2492,7 +2790,11 @@ async function analyzeScreenshot(baseUrl, apiKey, model, screenshotBase64, eleme
|
|
|
2492
2790
|
for (const el of els) {
|
|
2493
2791
|
const checked = el.checked ? ' [CHECKED]' : '';
|
|
2494
2792
|
const visibility = el.inViewport ? '' : ' [OFF-SCREEN]';
|
|
2495
|
-
|
|
2793
|
+
const skipNote = el.fieldKey && skippedFields.has(el.fieldKey)
|
|
2794
|
+
? ` [SKIPPED: ${skippedFields.get(el.fieldKey).note}]`
|
|
2795
|
+
: '';
|
|
2796
|
+
const personal = el.typable ? (0, personal_fields_1.promptMarker)(classificationOf(el)) : '';
|
|
2797
|
+
lines.push(` ${el.i}: [${el.type || el.tag}] "${el.text}"${checked}${visibility}${personal}${skipNote}`);
|
|
2496
2798
|
}
|
|
2497
2799
|
lines.push('');
|
|
2498
2800
|
}
|
|
@@ -2504,7 +2806,12 @@ async function analyzeScreenshot(baseUrl, apiKey, model, screenshotBase64, eleme
|
|
|
2504
2806
|
const body = {
|
|
2505
2807
|
model,
|
|
2506
2808
|
messages: [
|
|
2507
|
-
{
|
|
2809
|
+
{
|
|
2810
|
+
role: 'system',
|
|
2811
|
+
content: (budget.deep
|
|
2812
|
+
? SYSTEM_PROMPT.replace(FAST_RULE_0, DEEP_RULE_0)
|
|
2813
|
+
: SYSTEM_PROMPT) + PERSONAL_SAFETY_RULES,
|
|
2814
|
+
},
|
|
2508
2815
|
{
|
|
2509
2816
|
role: 'user',
|
|
2510
2817
|
content: [
|
|
@@ -2514,7 +2821,7 @@ async function analyzeScreenshot(baseUrl, apiKey, model, screenshotBase64, eleme
|
|
|
2514
2821
|
},
|
|
2515
2822
|
],
|
|
2516
2823
|
temperature: 0.1,
|
|
2517
|
-
max_tokens:
|
|
2824
|
+
max_tokens: budget.maxTokens,
|
|
2518
2825
|
stream: false,
|
|
2519
2826
|
};
|
|
2520
2827
|
const controller = new AbortController();
|
|
@@ -2667,7 +2974,7 @@ async function analyzeScreenshotWithPrompt(baseUrl, apiKey, model, screenshotBas
|
|
|
2667
2974
|
const body = {
|
|
2668
2975
|
model,
|
|
2669
2976
|
messages: [
|
|
2670
|
-
{ role: 'system', content: customSystemPrompt },
|
|
2977
|
+
{ role: 'system', content: customSystemPrompt + PERSONAL_SAFETY_RULES },
|
|
2671
2978
|
{
|
|
2672
2979
|
role: 'user',
|
|
2673
2980
|
content: [
|
|
@@ -2677,7 +2984,7 @@ async function analyzeScreenshotWithPrompt(baseUrl, apiKey, model, screenshotBas
|
|
|
2677
2984
|
},
|
|
2678
2985
|
],
|
|
2679
2986
|
temperature: 0.1,
|
|
2680
|
-
max_tokens:
|
|
2987
|
+
max_tokens: DEEP_MAX_TOKENS,
|
|
2681
2988
|
stream: false,
|
|
2682
2989
|
};
|
|
2683
2990
|
const controller = new AbortController();
|
|
@@ -2945,19 +3252,24 @@ function getAdjacentKey(char) {
|
|
|
2945
3252
|
* Type text with human-like patterns: variable speed, occasional typos,
|
|
2946
3253
|
* backspaces, natural pauses. Uses OS-level keyboard (no CDP).
|
|
2947
3254
|
*/
|
|
2948
|
-
async function typeHumanLike(text, baseDelayMs = 120) {
|
|
3255
|
+
async function typeHumanLike(text, baseDelayMs = 120, guard) {
|
|
3256
|
+
const send = async (ch, keyName) => {
|
|
3257
|
+
if (guard)
|
|
3258
|
+
await guard.markOwn(ch, keyName);
|
|
3259
|
+
typeCharOS(ch);
|
|
3260
|
+
};
|
|
2949
3261
|
for (let i = 0; i < text.length; i++) {
|
|
2950
3262
|
const char = text[i];
|
|
2951
3263
|
// 1. Occasional typo (2% chance) — then backspace to correct
|
|
2952
3264
|
if (char !== '\n' && char !== '\t' && Math.random() < 0.02) {
|
|
2953
3265
|
const wrong = getAdjacentKey(char);
|
|
2954
|
-
|
|
3266
|
+
await send(wrong);
|
|
2955
3267
|
await sleep(randomInt(40, 120));
|
|
2956
|
-
|
|
3268
|
+
await send('\b', 'Backspace'); // backspace
|
|
2957
3269
|
await sleep(randomInt(25, 70));
|
|
2958
3270
|
}
|
|
2959
3271
|
// 2. Type the correct character
|
|
2960
|
-
|
|
3272
|
+
await send(char, char === '\n' ? 'Enter' : char === '\t' ? 'Tab' : undefined);
|
|
2961
3273
|
// 3. Variable delay based on character type (human rhythm)
|
|
2962
3274
|
let delay = baseDelayMs + randomInt(-Math.floor(baseDelayMs * 0.3), Math.floor(baseDelayMs * 0.4));
|
|
2963
3275
|
if (char === ' ')
|
|
@@ -2979,7 +3291,16 @@ async function typeHumanLike(text, baseDelayMs = 120) {
|
|
|
2979
3291
|
await sleep(randomInt(200, 600));
|
|
2980
3292
|
}
|
|
2981
3293
|
await sleep(Math.max(delay, 30));
|
|
3294
|
+
if (guard) {
|
|
3295
|
+
await guard.tick(text.slice(0, i + 1));
|
|
3296
|
+
if (guard.stopped) {
|
|
3297
|
+
console.log(`[browse] Typing stopped after ${i + 1}/${text.length} chars`);
|
|
3298
|
+
break;
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
2982
3301
|
}
|
|
3302
|
+
if (guard)
|
|
3303
|
+
await guard.dispose();
|
|
2983
3304
|
}
|
|
2984
3305
|
/**
|
|
2985
3306
|
* Wait for the screen to change by comparing screenshots.
|
|
@@ -3042,7 +3363,7 @@ async function callStealthLLM(baseUrl, apiKey, model, screenshotPath, timeoutMs,
|
|
|
3042
3363
|
body: JSON.stringify({
|
|
3043
3364
|
model,
|
|
3044
3365
|
messages: [
|
|
3045
|
-
{ role: 'system', content: systemPrompt },
|
|
3366
|
+
{ role: 'system', content: systemPrompt + PERSONAL_SAFETY_RULES },
|
|
3046
3367
|
{
|
|
3047
3368
|
role: 'user',
|
|
3048
3369
|
content: [
|
|
@@ -3139,9 +3460,41 @@ async function runStealthLoop(baseUrl, apiKey, model, maxActions, intervalMs, ti
|
|
|
3139
3460
|
}
|
|
3140
3461
|
case 'type': {
|
|
3141
3462
|
if (result.text && result.text.length > 0) {
|
|
3463
|
+
const guard = new human_input_guard_1.TypingGuard({
|
|
3464
|
+
enabled: fieldSafety.stopOnUserInput,
|
|
3465
|
+
osLevel: true,
|
|
3466
|
+
label: result.text.slice(0, 40),
|
|
3467
|
+
});
|
|
3468
|
+
// Stealth has no DOM: use the focused control's own name as the
|
|
3469
|
+
// label for both the personal classifier and the human-skip list.
|
|
3470
|
+
const control = await guard.currentOsControl().catch(() => null);
|
|
3471
|
+
if (control) {
|
|
3472
|
+
if (control.signature && skippedOsFields.has(control.signature)) {
|
|
3473
|
+
console.log(`[stealth] Skipping field - ${skippedOsFields.get(control.signature)}`);
|
|
3474
|
+
actionCount++;
|
|
3475
|
+
break;
|
|
3476
|
+
}
|
|
3477
|
+
if (control.name && fieldSafety.allowPersonal === false) {
|
|
3478
|
+
const cls = (0, personal_fields_1.classifyField)({ label: control.name, ariaLabel: control.name });
|
|
3479
|
+
if ((0, personal_fields_1.isBlocked)(cls, { allowPersonal: fieldSafety.allowPersonal })) {
|
|
3480
|
+
skippedOsFields.set(control.signature, `left to you (${cls.category}: "${control.name.slice(0, 60)}")`);
|
|
3481
|
+
console.log(`[stealth] Not answering a personal question - left to you (${cls.category}: "${control.name.slice(0, 60)}")`);
|
|
3482
|
+
actionCount++;
|
|
3483
|
+
break;
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3142
3487
|
console.log(`[stealth] Typing ${result.text.length} chars...`);
|
|
3143
3488
|
await sleep(randomInt(300, 700)); // natural pause before typing
|
|
3144
|
-
await
|
|
3489
|
+
await guard.arm({ isCodeEditor: false });
|
|
3490
|
+
await typeHumanLike(result.text, charDelayMs, guard);
|
|
3491
|
+
if (guard.stopped) {
|
|
3492
|
+
const after = await guard.currentOsControl().catch(() => null);
|
|
3493
|
+
const sig = after?.signature || control?.signature;
|
|
3494
|
+
if (sig)
|
|
3495
|
+
skippedOsFields.set(sig, `you answered this one yourself (${guard.describeStop()})`);
|
|
3496
|
+
console.log(`[stealth] Stopped typing - you answered this one yourself (${guard.describeStop()})`);
|
|
3497
|
+
}
|
|
3145
3498
|
actionCount++;
|
|
3146
3499
|
}
|
|
3147
3500
|
else {
|