Commit d8e5aad1 authored by 郭阔's avatar 郭阔

fix: login-tools form-scoped submit + keep-signed-in + tab-id tracking

- submit button now resolved within the filled field's form first
  (document-wide query hit the header search form's identical
  button[type=submit] — clicked Search instead of Next)
- fill/checks/submit merged into one page-side step; checkbox ticking
  with label/name/id pattern filter (keep|remember|signed|记住)
- tab tracking by target id (open→use→close), immune to stale/concurrent
  same-site tabs; verification opens its own tab and closes it
- digitimes recipe: explicit Next button selector + keep-me-signed-in
  checkbox for long-lived session cookie
parent 3a520b32
......@@ -4,13 +4,23 @@
"steps": [
{
"url": "https://www.digitimes.com/register/signin.php",
"fill": { "input[name=signInEMail]": "$USER" },
"submit": "form button[type=submit], form input[type=submit]"
"fill": {
"input[name=signInEMail]": "$USER"
},
"submit": "button[type=submit][name=Submit], button.inlineBtn"
},
{
"marker": "input[type=password]",
"fill": { "input[type=password]": "$PASS" },
"submit": "form button[type=submit], form input[type=submit]",
"fill": {
"input[type=password]": "$PASS"
},
"checks": [
{
"selector": "input[type=checkbox]",
"where": "keep|remember|signed|stayed|记住|保持"
}
],
"submit": "button[type=submit][name=Submit], button.inlineBtn",
"waitMs": 8000
}
]
......
......@@ -112,6 +112,22 @@ function substitute(value) {
return String(value).replace(/\$USER/g, LOGIN_USER).replace(/\$PASS/g, LOGIN_PASS);
}
// 按 target id 精确定位自己打开的标签(导航不换 id;不按 URL 猜,避免匹配到
// 其他残留/并发采集的同站标签),用完关闭
async function tabById(id) {
const tabs = await api('/json');
return tabs.find((t) => t.id === id);
}
async function openTab(url) {
const tab = await api(`/json/new?${encodeURIComponent(url)}`, 'PUT').catch(() => null);
return tab || null;
}
async function closeTab(id) {
await fetch(`http://127.0.0.1:${cdpPort()}/json/close/${id}`, { method: 'PUT' }).catch(() => {});
}
async function main() {
const [command, source] = process.argv.slice(2);
if (!command || !source) {
......@@ -134,50 +150,103 @@ async function main() {
// ── check:打开检查页,看登录态选择器 ──
if (command === 'check') {
await api(`/json/new?${encodeURIComponent(recipe.checkUrl)}`, 'PUT').catch(() => {});
const opened = await openTab(recipe.checkUrl);
if (!opened) { console.error('无法打开检查页'); process.exit(2); }
await sleep(5000);
const tab = await findTab((t) => t.url.includes(new URL(recipe.checkUrl).host));
const tab = (await tabById(opened.id)) || (await findTab((t) => t.url.includes(new URL(recipe.checkUrl).host)));
if (!tab) { console.error('检查页标签丢失(可能被并发采集关闭)'); process.exit(3); }
const logged = await evalIn(tab,
`!!document.querySelector(${JSON.stringify(recipe.loggedInSelector)})`);
await closeTab(opened.id).catch(() => {});
console.log(logged ? `已登录: ${source}` : `未登录: ${source}`);
process.exit(logged ? 0 : 3);
}
// ── login:按配方多步执行 ──
if (command === 'login') {
let ownTabId = null; // 全程复用同一个标签(提交导航不换 target id)
for (const step of recipe.steps) {
if (step.url) {
await api(`/json/new?${encodeURIComponent(step.url)}`, 'PUT').catch(() => {});
const opened = await openTab(step.url);
if (opened) ownTabId = opened.id;
await sleep(5000);
}
// 重新定位自己的标签(并发采集可能关掉闲置标签)
const marker = step.url ? new URL(step.url).pathname : (step.marker || 'input');
let tab = null;
let tab = ownTabId ? await tabById(ownTabId) : null;
if (!tab) {
// 标签被并发采集关掉:按 marker 兜底重开/重找
const marker = step.url ? new URL(step.url).pathname : (step.marker || '');
for (let i = 0; i < 4 && !tab; i++) {
tab = await findTab((t) => (step.url ? t.url.includes(marker) : true));
tab = await findTab((t) => (marker ? t.url.includes(marker) : true));
if (!tab && step.url) { const re = await openTab(step.url); if (re) { ownTabId = re.id; tab = re; } }
if (!tab) await sleep(3000);
}
}
if (!tab) { console.error('登录标签丢失,中止'); process.exit(5); }
const fills = Object.entries(step.fill || {}).map(([sel, val]) =>
`(() => { const el = document.querySelector(${JSON.stringify(sel)}); if (!el) return 'MISSING:${sel}'; el.focus(); el.value = ${JSON.stringify(substitute(val))}; el.dispatchEvent(new Event('input', {bubbles:true})); el.dispatchEvent(new Event('change', {bubbles:true})); return 'OK'; })()`).join(',\n');
const result = await evalIn(tab, `JSON.stringify([${fills}])`);
if (ownTabId) ownTabId = tab.id;
// 填充→勾选→提交 合并为单次页面内执行:
// 提交按钮优先在「所填字段所在的表单」内查找,避免命中页首搜索表单的
// 同类按钮(全文档查询的教训:点到了搜索按钮而不是 Next)。
const fillPairs = Object.entries(step.fill || {})
.map(([sel, val]) => [sel, substitute(val)]);
const stepExpr = `(() => {
const log = [];
let lastEl = null;
const fillPairs = ${JSON.stringify(fillPairs)};
for (const [sel, val] of fillPairs) {
const el = document.querySelector(sel);
if (!el) { log.push('MISSING:' + sel); continue; }
el.focus();
el.value = val;
el.dispatchEvent(new Event('input', {bubbles:true}));
el.dispatchEvent(new Event('change', {bubbles:true}));
lastEl = el;
log.push('OK:' + sel);
}
// 复选框(where 为 null 时全选,否则按 name/id/label 文本过滤)
const checks = ${JSON.stringify(step.checks || [])};
for (const c of checks) {
const sel = typeof c === 'string' ? c : c.selector;
const pat = (typeof c === 'object' && c.where) ? new RegExp(c.where, 'i') : null;
for (const el of document.querySelectorAll(sel)) {
if (el.checked) continue;
if (pat) {
const ctx = ((el.closest('label') || {}).innerText || '') + ' ' + (el.name || '') + ' ' + (el.id || '');
if (!pat.test(ctx)) continue;
}
el.click();
}
}
// 提交:限定在最后填充字段的表单内找按钮,找不到再退全文档
const submitSel = ${JSON.stringify(step.submit || '')};
if (submitSel && lastEl) {
const scope = lastEl.closest('form') || document;
const btn = scope.querySelector(submitSel) || document.querySelector(submitSel);
if (btn) { btn.click(); log.push('SUBMIT:clicked'); }
else if (scope.tagName === 'FORM') { scope.requestSubmit(); log.push('SUBMIT:form'); }
else log.push('SUBMIT:NOT_FOUND');
}
return JSON.stringify(log);
})()`;
const result = await evalIn(tab, stepExpr);
const results = JSON.parse(result || '[]');
if (results.some((r) => String(r).startsWith('MISSING'))) {
console.error('表单字段未找到: ' + results.join('; '));
process.exit(5);
}
if (step.submit) {
await evalIn(tab, `(() => { const b = document.querySelector(${JSON.stringify(step.submit)}); const f = b ? b.closest('form') : null; if (b) b.click(); else if (f) f.requestSubmit(); return 'OK'; })()`);
if (results.includes('SUBMIT:NOT_FOUND')) {
console.error('提交按钮未找到: ' + step.submit);
process.exit(5);
}
await sleep(step.waitMs || 6000);
}
// 验证
await api(`/json/new?${encodeURIComponent(recipe.checkUrl)}`, 'PUT').catch(() => {});
// 验证(独立新标签,按 id 定位,用完关闭;顺带清理登录流程标签)
const vTab = await openTab(recipe.checkUrl);
await sleep(5000);
const tab = await findTab((t) => t.url.includes(new URL(recipe.checkUrl).host));
const tab = vTab ? await tabById(vTab.id) : null;
const logged = tab && await evalIn(tab,
`!!document.querySelector(${JSON.stringify(recipe.loggedInSelector)})`);
if (vTab) await closeTab(vTab.id).catch(() => {});
if (ownTabId) await closeTab(ownTabId).catch(() => {});
console.log(logged ? `✅ 自动登录成功: ${source}` : `❌ 登录后未检测到登录态: ${source}`);
process.exit(logged ? 0 : 5);
}
......
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