Commit 01fbc55f authored by 郭阔's avatar 郭阔

fix: login engine robustness + statista/bloomberg recipes

- recognize Auth0-style type=text username fields (not just type=email)
- fill via native value setter so React controlled inputs register the
  value (Auth0 'email required' despite filled field)
- bootstrap checkUrl tab when no site tab open before running steps
- entry picker prefers exact 'Sign In'/'Log In' over long matches
  (was clicking 'Bloomberg Anywhere Remote Login')
- iframe context matching skips the main frame first (same-host login
  iframes like bloomberg.com/account/iframe/signin were executing in
  the main frame and 'seeing' no form)
- new site recipes: statista.json (Auth0 two-step, verified live),
  bloomberg.json (two-step; auto-login still needs trusted-click
  engine support, signin iframe swallows synthetic DOM clicks)
parent 31be2438
{
"checkUrl": "https://www.bloomberg.com/technology",
"loggedOutText": "Sign In",
"steps": [
{
"click": {
"text": "Sign In"
}
},
{
"frame": "www.bloomberg.com",
"fill": {
"#email-form-input": "$USER"
},
"submit": "button[type=submit], form input[type=submit]"
},
{
"frame": "www.bloomberg.com",
"fill": {
"input[type=password]": "$PASS"
},
"submit": "button[type=submit], form input[type=submit]",
"waitMs": 8000
}
]
}
{
"checkUrl": "https://www.statista.com/",
"loggedOutText": "Log in",
"steps": [
{
"click": {
"text": "Log in"
}
},
{
"fill": {
"#username": "$USER"
},
"submit": "button[type=submit][name=action]"
},
{
"fill": {
"input[type=password]": "$PASS"
},
"submit": "button[type=submit][name=action]",
"waitMs": 8000
}
]
}
......@@ -112,7 +112,11 @@ async function evalIn(tab, expression, frameUrlSub) {
const tree = await send('Page.getFrameTree');
const frames = [];
(function walk(n) { if (n.frame) frames.push(n.frame); (n.childFrames || []).forEach(walk); })(tree.result.frameTree);
const frame = frames.find((f) => f.url.includes(frameUrlSub));
// 主框架优先跳过:同站 iframe(如 bloomberg.com/account/iframe/login)
// 与主框架同主机,不跳过会误在主框架执行而"看不到"表单
const mainId = tree.result.frameTree.frame.id;
const frame = frames.find((f) => f.id !== mainId && f.url.includes(frameUrlSub))
|| frames.find((f) => f.url.includes(frameUrlSub));
const ctx = frame && contexts.find((c) => c.auxData && c.auxData.frameId === frame.id);
if (!ctx) throw new Error('未找到 iframe 执行上下文: ' + frameUrlSub);
contextId = ctx.id;
......@@ -184,12 +188,19 @@ async function discover(source, doLogin) {
// ② 找登录入口并点击(按可见文本)
console.log('② 查找登录入口…');
let entry = await evalIn(tab, `(() => {
const re = new RegExp(${JSON.stringify(LOGIN_ENTRY_RE)}, 'i');
const els = [...document.querySelectorAll('button,[role=button],a')];
const el = els.find(e => new RegExp(${JSON.stringify(LOGIN_ENTRY_RE)}, 'i').test((e.innerText || '').trim()) && e.offsetParent);
if (!el) return null;
const text = (el.innerText || '').trim();
// 候选按精确度排序:完整等于 Sign In/Log In 优先,短文本次之,
// 长文本(如 "Bloomberg Anywhere Remote Login")最后——避免误点终端/边缘登录入口
const w = t => /^(sign\s*in|log\s*in|login|sign-in|登录|登入|登陸)$/i.test(t) ? 0 : (t.length <= 20 ? 1 : 2);
const cands = els
.map(e => ({ e, text: (e.innerText || '').trim() }))
.filter(c => c.text && re.test(c.text) && c.e.offsetParent)
.sort((a, b) => w(a.text) - w(b.text));
if (!cands.length) return null;
const el = cands[0].e;
el.click();
return { text, tag: el.tagName };
return { text: cands[0].text, tag: el.tagName };
})()`);
if (!entry) {
// 回退:常见登录路径探测
......@@ -229,7 +240,15 @@ async function discover(source, doLogin) {
// ④ 枚举表单(在对应上下文里执行)
const dumpExpr = `JSON.stringify({
email: (() => { const e = document.querySelector('input[type=email]') || document.querySelector('input[name*=mail i]') || document.querySelector('input[name=email i]'); return e ? (e.id ? '#' + e.id : (e.name ? '[name=' + JSON.stringify(e.name) + ']' : 'input[type=email]')) : null; })(),
email: (() => {
const sel = e => e.id ? '#' + e.id : (e.name ? '[name=' + JSON.stringify(e.name) + ']' : 'input[type=email]');
const e = document.querySelector('input[type=email]') || document.querySelector('input[name*=mail i]') || document.querySelector('input[name=email i]');
if (e) return sel(e);
// Auth0 等用 type=text 的用户名框(name/id/placeholder 含 user/login/account/mail)
const u = [...document.querySelectorAll('input[type=text]')].find(x =>
x.offsetParent !== null && /user|login|account|mail|email/i.test([x.name, x.id, x.placeholder, x.autocomplete].filter(Boolean).join(' ')));
return u ? (u.id ? '#' + u.id : '[name=' + JSON.stringify(u.name) + ']') : null;
})(),
emailVisible: (() => { const e = document.querySelector('input[type=email],input[name*=mail i],input[name=email i]'); return e ? e.offsetParent !== null : false; })(),
pw: !!document.querySelector('input[type=password]'),
pwVisible: (() => { const e = document.querySelector('input[type=password]'); return e ? e.offsetParent !== null : false; })(),
......@@ -334,6 +353,11 @@ async function main() {
// ── login:按配方多步执行(discover 成功后落入此处实登验证) ──
if (command === 'login' || command === 'discover') {
let ownTabId = null; // 全程复用同一个标签(提交导航不换 target id)
if (!await findTab((t) => t.url.includes(new URL(recipe.checkUrl).host))) {
const opened = await openTab(recipe.checkUrl);
if (opened) ownTabId = opened.id;
await sleep(5000);
}
for (const step of recipe.steps) {
if (step.url) {
const opened = await openTab(step.url);
......@@ -370,7 +394,10 @@ async function main() {
const el = document.querySelector(sel);
if (!el) { log.push('MISSING:' + sel); continue; }
el.focus();
el.value = val;
// React 受控输入必须走原生 value setter,否则内部状态不更新、提交校验仍判空(Auth0 实测)
const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
const nativeSet = Object.getOwnPropertyDescriptor(proto, 'value').set;
try { nativeSet.call(el, val); } catch (e) { el.value = val; }
el.dispatchEvent(new Event('input', {bubbles:true}));
el.dispatchEvent(new Event('change', {bubbles:true}));
lastEl = el;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment