@qing3a/flow-rpa-engine 0.5.2 → 0.5.6
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/behavior.d.ts +25 -6
- package/dist/behavior.js +51 -7
- package/dist/behavior.js.map +1 -1
- package/dist/cdp/city-switch.d.ts +63 -0
- package/dist/cdp/city-switch.js +276 -0
- package/dist/cdp/city-switch.js.map +1 -0
- package/dist/cdp/page.d.ts +22 -3
- package/dist/cdp/page.js +256 -42
- package/dist/cdp/page.js.map +1 -1
- package/dist/executor.d.ts +5 -2
- package/dist/executor.js +110 -12
- package/dist/executor.js.map +1 -1
- package/dist/record.d.ts +2 -0
- package/dist/record.js +1 -0
- package/dist/record.js.map +1 -1
- package/dist/region.d.ts +30 -0
- package/dist/region.js +39 -0
- package/dist/region.js.map +1 -0
- package/dist/validate.js +23 -0
- package/dist/validate.js.map +1 -1
- package/package.json +1 -1
package/dist/cdp/page.d.ts
CHANGED
|
@@ -17,7 +17,12 @@ import type { HitFingerprint } from '../behavior.js';
|
|
|
17
17
|
* T1 simcheck:模拟质量评分(验收工具,不进生产链路)。
|
|
18
18
|
*/
|
|
19
19
|
/** 页面内 DOM 辅助(window.__rpaDom) */
|
|
20
|
-
export declare const DOM_SOURCE = "\nwindow.__rpaDom = (function() {\n function isVisible(el) {\n if (!el || el.nodeType !== 1) return false;\n const s = window.getComputedStyle(el);\n if (s.display === 'none' || s.visibility === 'hidden' || parseFloat(s.opacity) === 0) return false;\n const r = el.getBoundingClientRect();\n return r.width > 0 && r.height > 0;\n }\n function byText(text) {\n const needle = String(text).trim();\n const visible = Array.from(document.querySelectorAll('body *')).filter(isVisible);\n const leaf = visible.filter(el => el.children.length === 0);\n const exact = leaf.filter(el => (el.textContent || '').trim() === needle);\n if (exact.length > 0) return exact;\n return leaf.filter(el => (el.textContent || '').includes(needle));\n }\n function safeQuery(sel) {\n try {\n return Array.from(document.querySelectorAll(sel)).filter(isVisible);\n } catch (e) {\n return [];\n }\n }\n function getBySels(selector) {\n // A3 \u6570\u7EC4\u94FE\uFF08#17 \u4FEE\u590D\uFF09\uFF1A\u4F9D\u6B21\u5C1D\u8BD5\uFF0C\u7B2C\u4E00\u4E2A\u547D\u4E2D\u7684\u8FD4\u56DE\n if (Array.isArray(selector)) {\n for (const s of selector) {\n const nodes = getBySels(s);\n if (nodes.length > 0) return nodes;\n }\n return [];\n }\n const sel = String(selector);\n if (sel.startsWith('text:')) return byText(sel.slice(5));\n // #16-B \u7F16\u53F7\u5BFB\u5740\uFF08\u865A\u62DF\u7A7A\u95F4\u89C6\u89C9\uFF09\uFF1A#N = numberedElements \u5217\u8868\u7B2C N \u4E2A\u53EF\u89C1\u4EA4\u4E92\u5143\u7D20\uFF081 \u8D77\uFF09\u3002\n // \u5FEB\u7167\u7F16\u53F7\u4E0E\u5BFB\u5740\u5171\u7528\u540C\u4E00\u679A\u4E3E \u2192 Agent/\u65E5\u5FD7\u53EF\u300C\u770B\u7F16\u53F7\u76F4\u63A5\u70B9\u300D\uFF1B\u7F16\u53F7\u968F DOM \u53D8\u5316\uFF0C\u64CD\u4F5C\u65F6\u73B0\u53D6\u3002\n const numMatch = sel.match(/^#(d+)$/);\n if (numMatch) {\n const idx = Number(numMatch[1]) - 1;\n const tags = 'a,button,input,select,textarea,[role=\"button\"],[role=\"link\"],[contenteditable=\"true\"]';\n const nodes = Array.from(document.querySelectorAll(tags)).filter(isVisible);\n return idx >= 0 && idx < nodes.length ? [nodes[idx]] : [];\n }\n // \u6DF7\u5408\u4E32\uFF08CSS \u4E0E text: \u9017\u53F7\u5206\u9694\uFF0C\u5982 \"a.search-btn, text:\u641C\u7D22\"\uFF09\uFF1A\u9010\u6BB5\u5C1D\u8BD5\u5408\u5E76\u53BB\u91CD\n // \uFF08#17 \u4FEE\u590D\uFF1Atext: \u6BB5\u4E0D\u80FD\u518D\u6DF7\u8FDB querySelectorAll\u2014\u2014\u6574\u4F53\u4F1A SyntaxError \u5931\u6548\uFF09\n const parts = sel.split(',').map((p) => p.trim()).filter(Boolean);\n if (parts.length > 1 || parts.some((p) => p.startsWith('text:'))) {\n const out = [];\n for (const p of parts) {\n const nodes = p.startsWith('text:') ? byText(p.slice(5)) : safeQuery(p);\n for (const n of nodes) {\n if (isVisible(n) && !out.includes(n)) out.push(n);\n }\n }\n return out;\n }\n return safeQuery(sel);\n }\n function pick(selector, index) {\n const nodes = getBySels(selector);\n return nodes[index] || nodes[0] || null;\n }\n function scrollIntoView(selector, index) {\n const el = pick(selector, index);\n if (!el) return false;\n el.scrollIntoView({ block: 'center', inline: 'center' });\n return true;\n }\n function textOf(selector, index) {\n const el = pick(selector, index);\n return el ? (el.innerText || el.textContent || '') : '';\n }\n function focusInput(selector, index) {\n const el = pick(selector, index);\n if (!el) return false;\n if (typeof el.select === 'function') el.select();\n el.focus();\n return true;\n }\n function pageText() {\n let t = document.body ? document.body.innerText : '';\n const inputs = Array.from(document.querySelectorAll('input, textarea, [contenteditable=\"true\"]'));\n for (const el of inputs) {\n const v = el.value || el.textContent || '';\n if (v) t += '\\n' + v;\n }\n return t;\n }\n function numberedElements(maxN) {\n const tags = 'a,button,input,select,textarea,[role=\"button\"],[role=\"link\"],[contenteditable=\"true\"]';\n const nodes = Array.from(document.querySelectorAll(tags)).filter(isVisible);\n const cap = maxN || 50;\n return nodes.slice(0, cap).map(el => ({\n tag: el.tagName.toLowerCase(),\n text: (el.innerText || el.value || el.textContent || '').trim().slice(0, 40)\n }));\n }\n // #16-A \u865A\u62DF\u7A7A\u95F4\u89C6\u89C9\uFF1A\u5143\u7D20\u8BED\u4E49\u63CF\u8FF0\uFF08\u906E\u6321\u68C0\u6D4B/\u65E5\u5FD7\u7528\uFF09\n function describeEl(el) {\n if (!el || el.nodeType !== 1) return null;\n const t = (el.innerText || el.value || el.textContent || '').trim();\n return {\n tag: el.tagName.toLowerCase(),\n cls: (el.className || '').toString().slice(0, 60),\n text: t.slice(0, 30),\n role: el.getAttribute && el.getAttribute('role'),\n };\n }\n // P3 \u8BED\u4E49\u951A\u70B9\uFF1A\u547D\u4E2D\u5143\u7D20\u6307\u7EB9\uFF08\u52A8\u4F5C\u6210\u529F\u65F6\u91C7\u96C6\uFF0C\u4F9B\u9009\u62E9\u5668\u5931\u6548\u65F6\u751F\u6210\u66FF\u4EE3\u5B9A\u4F4D\uFF09\u3002\n // \u53EA\u6709\u53EF\u91CD\u5B9A\u4F4D\u7684\u8BED\u4E49\u7279\u5F81\uFF08tag/\u6587\u672C\u622A\u65AD/placeholder/aria-label/name\uFF09\u2014\u2014\u65E0\u5750\u6807\u65E0\u539F\u6587\u3002\n // \u63A9\u7801\u524D\u79FB\uFF08\u94C1\u5F8B\uFF09\uFF1A\u8868\u5355\u5143\u7D20\uFF08input/textarea/select\uFF09\u7EDD\u4E0D\u53D6 value\u2014\u2014\u90A3\u662F\u7528\u6237\u8F93\u5165\u503C\n // \uFF08\u641C\u7D22\u8BCD/\u7535\u8BDD\u7B49\u654F\u611F\u539F\u6587\uFF09\uFF0C\u843D\u76D8\u5373\u8FDD\u89C4\uFF1B\u8868\u5355\u951A\u70B9\u53EA\u7528 placeholder/name/aria-label\u3002\n function describeHit(selector, index) {\n const el = pick(selector, index);\n if (!el) return null;\n const tag = el.tagName.toLowerCase();\n const isForm = tag === 'input' || tag === 'textarea' || tag === 'select';\n const t = isForm ? '' : (el.innerText || el.textContent || '').trim().slice(0, 30);\n const get = (k) => { try { return el.getAttribute(k) || undefined; } catch (e) { return undefined; } };\n return {\n tag,\n ...(t ? { text: t } : {}),\n ...(get('placeholder') ? { placeholder: get('placeholder') } : {}),\n ...(get('aria-label') ? { ariaLabel: get('aria-label') } : {}),\n ...(get('name') ? { name: get('name') } : {}),\n };\n }\n // #16-B/P2 \u533A\u57DF\u8BC6\u522B\uFF1A\u76EE\u6807\u5143\u7D20\u5411\u4E0A\u904D\u5386\u7956\u5148\uFF08\u22645 \u5C42\uFF09\uFF0C\u7528\u884C\u4E3A\u89C4\u5219\u5339\u914D\u51FA\u533A\u57DF\u6807\u7B7E\u3002\n // \u89C4\u5219\uFF08behavior.rules.json \u968F\u6D41\u7A0B\u5305\u5206\u53D1\uFF0C\u5F15\u64CE\u96F6\u4E1A\u52A1\u903B\u8F91\uFF09\uFF1A\u5168\u90E8 match \u6761\u4EF6\u6EE1\u8DB3\u624D\u547D\u4E2D\u3002\n // \u8FD4\u56DE { label, ruleId, ancestry }\u2014\u2014ancestry \u4E3A\u7956\u5148\u94FE\u6458\u8981\uFF08\u6297\u6539\u7248\u91CD\u5B9A\u4F4D\u7528\uFF09\u3002\n function identifyRegion(selector, index, rulesJson) {\n const target = pick(selector, index);\n if (!target) return null;\n let rules = [];\n try { rules = JSON.parse(rulesJson || '[]'); } catch (e) { rules = []; }\n const ancestry = [];\n let el = target;\n for (let d = 0; d < 5 && el; d++) {\n if (el !== target && el.nodeType === 1) {\n const cls = (el.className || '').toString().slice(0, 40);\n ancestry.push(el.tagName.toLowerCase() + (cls ? '.' + cls : ''));\n }\n // \u7528\u89C4\u5219\u5339\u914D\u5F53\u524D\u7956\u5148\n for (const r of rules) {\n const m = r.match || {};\n let hit = true;\n if (m.textContains && !((el.innerText || '').includes(m.textContains))) hit = false;\n if (m.textContainsAll && Array.isArray(m.textContainsAll)) {\n const txt = el.innerText || '';\n for (const kw of m.textContainsAll) {\n if (!txt.includes(kw)) { hit = false; break; }\n }\n }\n if (m.hasInput && !el.querySelector('input, textarea')) hit = false;\n if (m.hasButton && !el.querySelector('button, a[role=\"button\"], [role=\"button\"]')) hit = false;\n if (m.ancestorTag && !(el.tagName || '').toLowerCase().includes(m.ancestorTag)) hit = false;\n if (m.ancestorClass && !((el.className || '').toString().includes(m.ancestorClass))) hit = false;\n if (hit) {\n return { label: r.label, ruleId: r.id, ancestry: ancestry.slice(0, 3) };\n }\n }\n el = el.parentElement;\n }\n return { label: undefined, ruleId: undefined, ancestry: ancestry.slice(0, 3) };\n }\n // #16-A \u70B9\u51FB\u524D\u906E\u6321\u68C0\u6D4B\uFF1A\u8FD4\u56DE (x,y) \u5904\u6700\u9876\u5C42\u5143\u7D20 + \u8BE5\u5143\u7D20\u662F\u5426\u5C5E\u4E8E\u76EE\u6807\uFF08\u76EE\u6807\u6216\u5176\u540E\u4EE3\uFF09\n function hitTestAt(selector, index, x, y) {\n const target = pick(selector, index);\n if (!target) return { found: false };\n const top = document.elementFromPoint(x, y);\n if (!top) return { found: true, hit: false, top: null };\n const hit = top === target || target.contains(top);\n return {\n found: true,\n hit,\n top: describeEl(top),\n target: describeEl(target),\n };\n }\n // #16-A \u5BF9\u8BDD\u6846\u5173\u95ED\uFF1A\u906E\u6321\u8005\uFF08dialog-layer \u7B49\u906E\u7F69\uFF09\u5185\u7684\u5173\u95ED\u6309\u94AE\u5750\u6807\uFF08\u4F9B\u5F15\u64CE\u70B9\u51FB\u5173\u95ED\u540E\u91CD\u8BD5\uFF09\n function findDialogClose(selector, index) {\n const target = pick(selector, index);\n if (!target) return null;\n const cx = Math.round(target.getBoundingClientRect().left + target.getBoundingClientRect().width / 2);\n const cy = Math.round(target.getBoundingClientRect().top + target.getBoundingClientRect().height / 2);\n const top = document.elementFromPoint(cx, cy);\n if (!top) return null;\n // \u627E\u6700\u9876\u5C42\u5143\u7D20\u4F5C\u4E3A\u906E\u6321\u8005\uFF08dialog-layer/\u906E\u7F69\uFF09\uFF0C\u5728\u5176\u5185\u627E\u5173\u95ED\u6309\u94AE\n let blocker = top;\n while (blocker && blocker !== document.body) {\n const s = getComputedStyle(blocker);\n if ((s.position === 'fixed' || s.position === 'absolute') && parseFloat(s.zIndex) >= 100) break;\n blocker = blocker.parentElement;\n }\n if (!blocker || blocker === document.body) return null;\n // \u5173\u95ED\u6309\u94AE\u5019\u9009\uFF1Aclass \u542B close / \u56FE\u6807\u6309\u94AE / \u6587\u672C \u00D7\n const closeBtns = [...blocker.querySelectorAll('a,button,[role=\"button\"],i,span,div')].filter((el) => {\n const cls = (el.className || '').toString();\n const txt = (el.innerText || '').trim();\n const r = el.getBoundingClientRect();\n return r.width > 8 && r.height > 8 &&\n (/close|close-btn|icon-close|dialog-close|del|btn-close/i.test(cls) || txt === '\u00D7' || txt === '\u2715' || txt === 'X' || txt === '\u5173\u95ED');\n });\n const btn = closeBtns[0];\n if (!btn) return null;\n const r = btn.getBoundingClientRect();\n return { x: Math.round(r.left + r.width / 2), y: Math.round(r.top + r.height / 2), cls: (btn.className || '').toString().slice(0, 40) };\n }\n // #16-A \u539F\u5B50\u5316\u906E\u6321\u68C0\u6D4B\uFF1A\u4E0E rect \u91C7\u96C6\u540C\u4E00\u6B21\u6C42\u503C\uFF08\u6D88\u9664\u300C\u91C7\u96C6-\u68C0\u6D4B\u300D\u4E24\u6B21\u6C42\u503C\u95F4\u7684 DOM \u7ADE\u6001\uFF09\u3002\n // \u8FD4\u56DE\u6BCF\u4E2A\u53EF\u89C1\u76EE\u6807\u7684 rect + \u4E2D\u5FC3\u70B9\u547D\u4E2D\u4FE1\u606F + \u5B89\u5168\u70B9\u51FB\u70B9\uFF1A\n // hit=true \u4E2D\u5FC3\u70B9\u547D\u4E2D\u76EE\u6807\u6216\u5176\u975E\u4EA4\u4E92\u540E\u4EE3\uFF08\u5B89\u5168\u70B9\u51FB\u70B9\uFF09\n // hit=false \u4E2D\u5FC3\u70B9\u88AB\u771F\u5B9E\u906E\u6321\uFF08\u6D6E\u52A8\u5C42/\u5F39\u5C42/\u5176\u5B83\u5143\u7D20\u8986\u76D6\uFF09\n // danger=true \u4E2D\u5FC3\u70B9\u843D\u5728\u76EE\u6807\u5185\u7684\u3010\u53EF\u4EA4\u4E92\u5B50\u5143\u7D20\u3011\uFF08\u94FE\u63A5/\u6309\u94AE\u7B49\uFF09\u4E0A\u2014\u2014\u70B9\u51FB\u4F1A\u89E6\u53D1\u5B50\u5143\u7D20\u800C\u975E\u76EE\u6807\n // safe \u77E9\u5F62\u5185\u626B\u63CF\u5230\u7684\u7B2C\u4E00\u4E2A\u300C\u5C5E\u4E8E\u76EE\u6807\u4E14\u975E\u4EA4\u4E92\u5B50\u5143\u7D20\u300D\u7684\u70B9\uFF08\u4E2D\u5FC3\u88AB\u76D6/\u5371\u9669\u65F6\u6362\u70B9\u7528\uFF09\uFF1B\n // \u65E0\u5B89\u5168\u70B9\u5219 null\uFF08\u6574\u4E2A\u76EE\u6807\u88AB\u76D6 \u2192 \u5F15\u64CE\u4FA7 ESC \u5173\u95ED\u6D6E\u52A8\u5C42\u91CD\u8BD5\uFF09\n // top=null\uFF08elementFromPoint \u65E0\u7ED3\u679C\uFF0C\u5982\u5143\u7D20\u90E8\u5206\u5728\u89C6\u53E3\u5916/\u6E32\u67D3\u8FB9\u7F18\uFF09\u2192 \u4E0D\u89C6\u4E3A\u906E\u6321\uFF0Chit=true\u3002\n function clickableRects(selector) {\n const d = window.__rpaDom;\n const nodes = d && d.getBySels ? d.getBySels(selector) : [];\n const vw = innerWidth || document.documentElement.clientWidth;\n const vh = innerHeight || document.documentElement.clientHeight;\n const out = [];\n const INTERACTIVE = 'a,button,[role=\"button\"],[role=\"link\"],input,select,textarea,[onclick],label';\n const isSafeHit = (el, top) => {\n if (!top) return false;\n if (top === el) return true;\n if (el.contains(top)) {\n return !(top.closest && top.closest(INTERACTIVE) === top);\n }\n return false;\n };\n for (let i = 0; i < nodes.length; i++) {\n const el = nodes[i];\n const t = el.getBoundingClientRect();\n const fullW = Math.max(1, t.width);\n const fullH = Math.max(1, t.height);\n const x = Math.max(t.left, 0);\n const y = Math.max(t.top, 0);\n const w = Math.max(0, Math.min(t.right, vw) - x);\n const h = Math.max(0, Math.min(t.bottom, vh) - y);\n if (w <= 0 || h <= 0) continue;\n // #16-A \u53EF\u89C1\u6BD4\u4F8B\uFF1A\u89C6\u53E3\u5185\u9762\u79EF / \u603B\u9762\u79EF\u3002\u7A84\u89C6\u53E3\u4E0B\u5927\u90E8\u5206\u5728\u89C6\u53E3\u5916\u7684\u5143\u7D20\n // \uFF08\u5982\u9876\u90E8\u5BFC\u822A\u53F3\u4FA7\u7684\u641C\u7D22\u6309\u94AE\uFF09\u70B9\u51FB\u5750\u6807\u4F1A\u843D\u5728\u89C6\u53E3\u5916/\u8FB9\u7F18 \u2192 CDP input \u6302\u8D77\u6216\u70B9\u7A7A\u3002\n // \u53EF\u89C1\u6BD4\u4F8B < 50% \u2192 \u6807\u8BB0 partiallyVisible\uFF0C\u5F15\u64CE\u4FA7\u62A5\u300C\u76EE\u6807\u5927\u90E8\u5206\u5728\u89C6\u53E3\u5916\u300D\u800C\u975E\u5C1D\u8BD5\u70B9\u51FB\u3002\n const visibleRatio = (w * h) / (fullW * fullH);\n const partiallyVisible = visibleRatio < 0.5;\n if (partiallyVisible) {\n out.push({\n x: Math.round(x), y: Math.round(y), w: Math.round(w), h: Math.round(h),\n hit: false, danger: false, safe: null, partiallyVisible: true, top: null, target: describeEl(el),\n });\n continue;\n }\n const cx = Math.round(x + w / 2);\n const cy = Math.round(y + h / 2);\n const top = document.elementFromPoint(cx, cy);\n if (!top) {\n // elementFromPoint \u65E0\u7ED3\u679C\uFF1A\u53EF\u89C1\u6BD4\u4F8B\u8DB3\u591F\uFF08\u226550%\uFF09\u4F46\u53D6\u4E0D\u5230\u9876\u5C42\u5143\u7D20 \u2192 \u6E32\u67D3\u8FB9\u754C\u5BB9\u5FCD\uFF0C\u4E0D\u963B\u585E\n out.push({\n x: Math.round(x), y: Math.round(y), w: Math.round(w), h: Math.round(h),\n hit: true, danger: false, safe: null, partiallyVisible: false, top: null, target: describeEl(el),\n });\n continue;\n }\n const belongs = top === el || el.contains(top);\n const isInteractiveChild = top !== el && el.contains(top) && top.closest && top.closest(INTERACTIVE) === top;\n // \u4E2D\u5FC3\u88AB\u76D6/\u5371\u9669 \u2192 \u626B\u63CF\u77E9\u5F62\u5185\u90E8\u627E\u5B89\u5168\u70B9\uFF085\u00D75 \u7F51\u683C\uFF0C\u907F\u5F00\u4EA4\u4E92\u5B50\u5143\u7D20\u4E0E\u906E\u6321\uFF09\n let safe = null;\n if (!(belongs && !isInteractiveChild)) {\n for (let gy = 1; gy <= 4 && !safe; gy++) {\n for (let gx = 1; gx <= 4 && !safe; gx++) {\n const sx = Math.round(x + (w * gx) / 5);\n const sy = Math.round(y + (h * gy) / 5);\n const st = document.elementFromPoint(sx, sy);\n if (st && isSafeHit(el, st)) safe = { x: sx, y: sy };\n }\n }\n }\n out.push({\n x: Math.round(x), y: Math.round(y), w: Math.round(w), h: Math.round(h),\n hit: belongs && !isInteractiveChild,\n danger: isInteractiveChild,\n safe,\n partiallyVisible: false,\n top: describeEl(top),\n target: describeEl(el),\n });\n }\n return out;\n }\n // #16-A \u76EE\u6807\u77E9\u5F62\u5185\u627E\u5B89\u5168\u70B9\u51FB\u70B9\uFF1A\u7F51\u683C\u91C7\u6837\uFF085\u00D75\uFF09\uFF0C\u8FD4\u56DE\u7B2C\u4E00\u4E2A\u300C\u5C5E\u4E8E\u76EE\u6807\u4E14\u4E0D\u5728\u53EF\u4EA4\u4E92\u5B50\u5143\u7D20\u4E0A\u300D\u7684\u70B9\u3002\n // \u5927\u5BB9\u5668\u5361\u7247\uFF08\u5982 job-card-wrap\uFF09\u4E2D\u5FC3\u5E38\u843D\u5728\u516C\u53F8\u94FE\u63A5/\u804C\u4F4D\u8BE6\u60C5\u94FE\u63A5\u4E0A\uFF0C\u76F4\u63A5\u70B9\u4F1A\u8BEF\u8DF3\u8F6C\uFF1B\n // \u5B89\u5168\u70B9\u907F\u5F00\u4EA4\u4E92\u5B50\u5143\u7D20\uFF08a/button/[onclick]\uFF09\uFF0C\u53EA\u843D\u5728\u6587\u672C/\u7A7A\u767D\u533A\u57DF\uFF08\u70B9\u51FB\u4ECD\u7531\u7236\u7EA7\u4E8B\u4EF6\u5192\u6CE1\u5904\u7406\uFF09\u3002\n function safePointIn(selector, index) {\n const el = pick(selector, index);\n if (!el) return null;\n const t = el.getBoundingClientRect();\n const INTERACTIVE = 'a,button,[role=\"button\"],[role=\"link\"],input,select,textarea,[onclick],label';\n for (let gy = 2; gy <= 3; gy++) {\n for (let gx = 2; gx <= 3; gx++) {\n const cx = Math.round(t.left + (t.width * gx) / 5);\n const cy = Math.round(t.top + (t.height * gy) / 5);\n const top = document.elementFromPoint(cx, cy);\n if (!top) continue;\n if (top === el || el.contains(top)) {\n const bad = top !== el && top.closest && top.closest(INTERACTIVE) === top;\n if (!bad) return { x: cx, y: cy };\n }\n }\n }\n // \u515C\u5E95\uFF1A\u4E2D\u5FC3\u70B9\uFF08\u53EF\u80FD\u5371\u9669\uFF0C\u7531\u5F15\u64CE\u4FA7\u518D\u5224\u65AD\uFF09\n return { x: Math.round(t.left + t.width / 2), y: Math.round(t.top + t.height / 2) };\n }\n return { getBySels, scrollIntoView, textOf, focusInput, pageText, numberedElements, describeEl, describeHit, hitTestAt, clickableRects, safePointIn, identifyRegion, findDialogClose };\n})();\ntrue\n";
|
|
20
|
+
export declare const DOM_SOURCE = "\nwindow.__rpaDom = (function() {\n function isVisible(el) {\n if (!el || el.nodeType !== 1) return false;\n const s = window.getComputedStyle(el);\n if (s.display === 'none' || s.visibility === 'hidden' || parseFloat(s.opacity) === 0) return false;\n const r = el.getBoundingClientRect();\n return r.width > 0 && r.height > 0;\n }\n function byText(text) {\n const needle = String(text).trim();\n const visible = Array.from(document.querySelectorAll('body *')).filter(isVisible);\n const leaf = visible.filter(el => el.children.length === 0);\n const exact = leaf.filter(el => (el.textContent || '').trim() === needle);\n if (exact.length > 0) return exact;\n return leaf.filter(el => (el.textContent || '').includes(needle));\n }\n function safeQuery(sel) {\n try {\n return Array.from(document.querySelectorAll(sel)).filter(isVisible);\n } catch (e) {\n return [];\n }\n }\n // T014 P0-3\uFF1A\u533A\u57DF\u89C4\u5219\u547D\u4E2D\u5224\u5B9A\uFF08\u5355\u4E00\u5B9E\u73B0\u2014\u2014identifyRegion \u4E0E region: \u8868\u8FBE\u5F0F\u5171\u7528\uFF0C\u9632\u903B\u8F91\u6F02\u79FB\uFF09\n function ruleHits(rule, el) {\n const m = rule.match || {};\n let hit = true;\n if (m.textContains && !((el.innerText || '').includes(m.textContains))) hit = false;\n if (m.textContainsAll && Array.isArray(m.textContainsAll)) {\n const txt = el.innerText || '';\n for (const kw of m.textContainsAll) {\n if (!txt.includes(kw)) { hit = false; break; }\n }\n }\n if (m.hasInput && !el.querySelector('input, textarea')) hit = false;\n if (m.hasButton && !el.querySelector('button, a[role=\"button\"], [role=\"button\"]')) hit = false;\n if (m.ancestorTag && !(el.tagName || '').toLowerCase().includes(m.ancestorTag)) hit = false;\n if (m.ancestorClass && !((el.className || '').toString().includes(m.ancestorClass))) hit = false;\n return hit;\n }\n // T014 P0-1\uFF1A\u5143\u7D20\u6240\u5C5E\u533A\u57DF\u6807\u7B7E\uFF08\u8D70\u5143\u7D20\u81EA\u8EAB\u2192\u7956\u5148\uFF0C\u9996\u4E2A\u89C4\u5219\u547D\u4E2D\u5373\u8FD4\u56DE\uFF1B\u65E0 \u2192 null\uFF09\n function regionLabelOf(el, rules) {\n let cur = el;\n for (let d = 0; d < 5 && cur; d++) {\n for (const r of rules) {\n if (ruleHits(r, cur)) return { label: r.label, ruleId: r.id };\n }\n cur = cur.parentElement;\n }\n return null;\n }\n // T014 P0-3\uFF1A\u533A\u57DF\u4E3B\u5BB9\u5668\u89E3\u6790\uFF08region:<\u6807\u7B7E> \u4E0E region:<\u6807\u7B7E> >> <n> \u5171\u7528\uFF09\uFF1A\n // self-match \u5019\u9009\uFF08\u9762\u79EF < \u89C6\u53E3\uFF09\u53D6\u300C\u6700\u6DF1\u4E14\u6587\u672C \u2265100 \u5B57\u300D\uFF1B\u65E0 \u2192 \u6700\u5927\u9762\u79EF\uFF1B\u65E0\u5019\u9009 \u2192 null\u3002\n function regionContainerOf(label, rules) {\n const vw = innerWidth || document.documentElement.clientWidth || 1000;\n const vh = innerHeight || document.documentElement.clientHeight || 800;\n const vpArea = vw * vh;\n const matches = [];\n for (const el of Array.from(document.querySelectorAll('body *')).filter(isVisible)) {\n if (!(el.innerText || '').trim()) continue;\n for (const r of rules) {\n if (r.label === label && ruleHits(r, el)) {\n const rect = el.getBoundingClientRect();\n const area = rect.width * rect.height;\n if (area > 0 && area < vpArea) matches.push({ el, area });\n break;\n }\n }\n }\n if (matches.length === 0) return null;\n const depthOf = (el) => {\n let d = 0;\n let cur = el.parentElement;\n while (cur) { d++; cur = cur.parentElement; }\n return d;\n };\n const substantial = matches.filter((m) => (m.el.innerText || '').trim().length >= 100);\n let best = null;\n if (substantial.length > 0) {\n for (const m of substantial) {\n if (!best || depthOf(m.el) > depthOf(best.el)) best = m;\n }\n } else {\n for (const m of matches) {\n if (!best || m.area > best.area) best = m;\n }\n }\n return best ? best.el : null;\n }\n function getBySels(selector) {\n // A3 \u6570\u7EC4\u94FE\uFF08#17 \u4FEE\u590D\uFF09\uFF1A\u4F9D\u6B21\u5C1D\u8BD5\uFF0C\u7B2C\u4E00\u4E2A\u547D\u4E2D\u7684\u8FD4\u56DE\n if (Array.isArray(selector)) {\n for (const s of selector) {\n const nodes = getBySels(s);\n if (nodes.length > 0) return nodes;\n }\n return [];\n }\n const sel = String(selector);\n if (sel.startsWith('text:')) return byText(sel.slice(5));\n // T014 P0-3\uFF1Aregion:<\u6807\u7B7E>[ >> <n>] \u2014\u2014 \u5F15\u64CE\u5185\u90E8\u533A\u57DF\u5BFB\u5740\u8868\u8FBE\u5F0F\uFF08\u4E0D\u5F00\u653E\u7ED9\u7528\u6237\u5199 flow\uFF09\u3002\n // \u89C4\u5219\u7531 setRegionRules \u6CE8\u5165\uFF08\u968F\u6D41\u7A0B\u5305\u5206\u53D1\uFF0C\u5F15\u64CE\u96F6\u4E1A\u52A1\u903B\u8F91\uFF1BT014 \u9636\u6BB5 2 \u4EF2\u88C1\uFF1A\u6CE8\u5165\u89C4\u5219\n // \u5DF2\u6309\u6863\u6848 seen \u7F6E\u4FE1\u5EA6\u91CD\u6392\uFF0C\u540C\u4E00\u5143\u7D20\u547D\u4E2D\u591A\u6761\u89C4\u5219\u65F6\u9AD8\u7F6E\u4FE1\u533A\u57DF\u4F18\u5148\uFF09\u3002\n // \u4E3B\u5BB9\u5668\u89E3\u6790\uFF1Aself-match \u5019\u9009\uFF08\u9762\u79EF < \u89C6\u53E3\uFF09\u4E2D\uFF0C\u53D6\u300C\u6700\u6DF1\u4E14\u6587\u672C \u2265100 \u5B57\u300D\u7684\u5019\u9009\u2014\u2014\n // \u8BE6\u60C5\u533A\u6587\u672C\u94FE container \u2283 box \u2283 body \u2283 h3\uFF0C\u6700\u6DF1\u5B9E\u8D28\u5019\u9009 = \u5185\u5BB9\u5BB9\u5668 body\n // \uFF08\u6DF1\u4E8E\u542B\u8FB9\u680F/\u6C42\u804C\u610F\u5411\u7684\u5305\u88F9\u5C42 container\uFF0C\u89C1 USAGE-ISSUES #16 \u8FB9\u680F\u6C61\u67D3\uFF09\uFF1B\n // \u65E0 \u2265100 \u5B57\u5019\u9009\uFF08\u77ED\u6587\u672C\u533A\u57DF\u5982\u641C\u7D22\u8868\u5355\uFF09\u2192 \u56DE\u9000\u6700\u5927\u9762\u79EF\u3002\n // 'region:<\u6807\u7B7E> >>' = \u533A\u57DF\u5185\u5168\u90E8\u53EF\u89C1\u53EF\u4EA4\u4E92\u5143\u7D20\uFF08#N \u7F16\u53F7\u679A\u4E3E\u7684\u533A\u57DF\u5185\u7248\uFF1B\n // executor \u7528 index \u53C2\u6570\u9009\u7B2C i \u4E2A \u2192 \u8D8A\u754C\u8D70 index_out_of_range \u5217\u8868\u8017\u5C3D\u8BED\u4E49\uFF09\uFF1B\n // 'region:<\u6807\u7B7E> >> <n>' = \u533A\u57DF\u5185\u7B2C n \u4E2A\uFF080 \u8D77\uFF09\u53EF\u4EA4\u4E92\u5143\u7D20\uFF08\u5F15\u64CE\u5185\u90E8\u5177\u4F53\u5BFB\u5740\uFF09\u3002\n if (sel.startsWith('region:')) {\n const rest = sel.slice(7).trim();\n const hasArrow = rest.includes('>>');\n const parts = rest.split('>>');\n const label = parts[0].trim();\n // \u65E0 >> \u2192 \u533A\u57DF\u4E3B\u5BB9\u5668\uFF1B\u6709 >> \u4E14\u7D22\u5F15\u7A7A \u2192 \u533A\u57DF\u5185\u5168\u90E8\u53EF\u4EA4\u4E92\u5143\u7D20\uFF1B\u6709 >> \u4E14\u7D22\u5F15 n \u2192 \u7B2C n \u4E2A\n const indexPart = hasArrow ? parts[1].trim() : undefined;\n const rules = (window.__rpaDom && window.__rpaDom._regionRules) || [];\n const container = regionContainerOf(label, rules);\n if (!container) return [];\n if (indexPart === undefined) return [container];\n const tags = 'a,button,input,select,textarea,[role=\"button\"],[role=\"link\"],[contenteditable=\"true\"]';\n const nodes = Array.from(container.querySelectorAll(tags)).filter(isVisible);\n if (indexPart === '') return nodes;\n const n = Number(indexPart);\n return Number.isInteger(n) && n >= 0 && n < nodes.length ? [nodes[n]] : [];\n }\n // #16-B \u7F16\u53F7\u5BFB\u5740\uFF08\u865A\u62DF\u7A7A\u95F4\u89C6\u89C9\uFF09\uFF1A#N = numberedElements \u5217\u8868\u7B2C N \u4E2A\u53EF\u89C1\u4EA4\u4E92\u5143\u7D20\uFF081 \u8D77\uFF09\u3002\n // \u5FEB\u7167\u7F16\u53F7\u4E0E\u5BFB\u5740\u5171\u7528\u540C\u4E00\u679A\u4E3E \u2192 Agent/\u65E5\u5FD7\u53EF\u300C\u770B\u7F16\u53F7\u76F4\u63A5\u70B9\u300D\uFF1B\u7F16\u53F7\u968F DOM \u53D8\u5316\uFF0C\u64CD\u4F5C\u65F6\u73B0\u53D6\u3002\n const numMatch = sel.match(/^#(d+)$/);\n if (numMatch) {\n const idx = Number(numMatch[1]) - 1;\n const tags = 'a,button,input,select,textarea,[role=\"button\"],[role=\"link\"],[contenteditable=\"true\"]';\n const nodes = Array.from(document.querySelectorAll(tags)).filter(isVisible);\n return idx >= 0 && idx < nodes.length ? [nodes[idx]] : [];\n }\n // \u6DF7\u5408\u4E32\uFF08CSS \u4E0E text: \u9017\u53F7\u5206\u9694\uFF0C\u5982 \"a.search-btn, text:\u641C\u7D22\"\uFF09\uFF1A\u9010\u6BB5\u5C1D\u8BD5\u5408\u5E76\u53BB\u91CD\n // \uFF08#17 \u4FEE\u590D\uFF1Atext: \u6BB5\u4E0D\u80FD\u518D\u6DF7\u8FDB querySelectorAll\u2014\u2014\u6574\u4F53\u4F1A SyntaxError \u5931\u6548\uFF09\n const parts = sel.split(',').map((p) => p.trim()).filter(Boolean);\n if (parts.length > 1 || parts.some((p) => p.startsWith('text:'))) {\n const out = [];\n for (const p of parts) {\n const nodes = p.startsWith('text:') ? byText(p.slice(5)) : safeQuery(p);\n for (const n of nodes) {\n if (isVisible(n) && !out.includes(n)) out.push(n);\n }\n }\n return out;\n }\n return safeQuery(sel);\n }\n function pick(selector, index) {\n const nodes = getBySels(selector);\n return nodes[index] || nodes[0] || null;\n }\n function scrollIntoView(selector, index) {\n const el = pick(selector, index);\n if (!el) return false;\n el.scrollIntoView({ block: 'center', inline: 'center' });\n return true;\n }\n // T012 P1-2\uFF1A\u8F7B\u91CF\u62DF\u4EBA\u6EDA\u52A8\u2014\u2014\u5206\u6BB5\u6E10\u8FDB\u6EDA\u52A8\uFF082~5 \u6BB5\uFF0C\u6BB5\u95F4 30~120ms \u968F\u673A\u8282\u594F\uFF09\uFF0C\n // \u7EC8\u4F4D\u4E0E scrollIntoView \u4E00\u81F4\uFF08block:center\uFF09\u2014\u2014\u53EA\u6539\u8282\u594F\u4E0D\u6539\u843D\u70B9\uFF0C\u529F\u80FD\u4E0D\u56DE\u5F52\u3002\n // \u8FD4\u56DE Promise\uFF08evaluate awaitPromise:true \u751F\u6548\uFF09\uFF1B\u627E\u4E0D\u5230\u5143\u7D20 \u2192 false\uFF08\u540C\u6B65\u503C\u517C\u5BB9\uFF09\u3002\n function scrollIntoViewRhythm(selector, index) {\n const el = pick(selector, index);\n if (!el) return false;\n const steps = 2 + Math.floor(Math.random() * 4);\n const vh = innerHeight || document.documentElement.clientHeight || 600;\n const rect = el.getBoundingClientRect();\n const curTop = pageYOffset || document.documentElement.scrollTop || 0;\n const targetY = curTop + rect.top - (vh - Math.min(rect.height, vh)) / 2;\n const dy = targetY - curTop;\n return (async function () {\n for (let i = 1; i <= steps; i++) {\n const t = i / steps;\n const ease = t * t * (3 - 2 * t);\n window.scrollTo(0, Math.round(curTop + dy * ease));\n await new Promise(function (r) { setTimeout(r, 30 + Math.random() * 90); });\n }\n el.scrollIntoView({ block: 'center', inline: 'center' });\n return true;\n })();\n }\n function textOf(selector, index) {\n const el = pick(selector, index);\n return el ? (el.innerText || el.textContent || '') : '';\n }\n function focusInput(selector, index) {\n const el = pick(selector, index);\n if (!el) return false;\n if (typeof el.select === 'function') el.select();\n el.focus();\n return true;\n }\n function pageText() {\n let t = document.body ? document.body.innerText : '';\n const inputs = Array.from(document.querySelectorAll('input, textarea, [contenteditable=\"true\"]'));\n for (const el of inputs) {\n const v = el.value || el.textContent || '';\n if (v) t += '\\n' + v;\n }\n return t;\n }\n function numberedElements(maxN) {\n const tags = 'a,button,input,select,textarea,[role=\"button\"],[role=\"link\"],[contenteditable=\"true\"]';\n const nodes = Array.from(document.querySelectorAll(tags)).filter(isVisible);\n const cap = maxN || 50;\n return nodes.slice(0, cap).map(el => ({\n tag: el.tagName.toLowerCase(),\n text: (el.innerText || el.value || el.textContent || '').trim().slice(0, 40)\n }));\n }\n // #16-A \u865A\u62DF\u7A7A\u95F4\u89C6\u89C9\uFF1A\u5143\u7D20\u8BED\u4E49\u63CF\u8FF0\uFF08\u906E\u6321\u68C0\u6D4B/\u65E5\u5FD7\u7528\uFF09\n function describeEl(el) {\n if (!el || el.nodeType !== 1) return null;\n const t = (el.innerText || el.value || el.textContent || '').trim();\n return {\n tag: el.tagName.toLowerCase(),\n cls: (el.className || '').toString().slice(0, 60),\n text: t.slice(0, 30),\n role: el.getAttribute && el.getAttribute('role'),\n };\n }\n // P3 \u8BED\u4E49\u951A\u70B9\uFF1A\u547D\u4E2D\u5143\u7D20\u6307\u7EB9\uFF08\u52A8\u4F5C\u6210\u529F\u65F6\u91C7\u96C6\uFF0C\u4F9B\u9009\u62E9\u5668\u5931\u6548\u65F6\u751F\u6210\u66FF\u4EE3\u5B9A\u4F4D\uFF09\u3002\n // \u53EA\u6709\u53EF\u91CD\u5B9A\u4F4D\u7684\u8BED\u4E49\u7279\u5F81\uFF08tag/\u6587\u672C\u622A\u65AD/placeholder/aria-label/name\uFF09\u2014\u2014\u65E0\u5750\u6807\u65E0\u539F\u6587\u3002\n // \u63A9\u7801\u524D\u79FB\uFF08\u94C1\u5F8B\uFF09\uFF1A\u8868\u5355\u5143\u7D20\uFF08input/textarea/select\uFF09\u7EDD\u4E0D\u53D6 value\u2014\u2014\u90A3\u662F\u7528\u6237\u8F93\u5165\u503C\n // \uFF08\u641C\u7D22\u8BCD/\u7535\u8BDD\u7B49\u654F\u611F\u539F\u6587\uFF09\uFF0C\u843D\u76D8\u5373\u8FDD\u89C4\uFF1B\u8868\u5355\u951A\u70B9\u53EA\u7528 placeholder/name/aria-label\u3002\n // T014 P1-4\uFF1A\u9644\u52A0\u6240\u5728\u533A\u57DF\u6807\u7B7E\uFF08region\uFF09\u2014\u2014\u951A\u70B9\u53EA\u5728\u540C\u4E00\u533A\u57DF\u590D\u7528\uFF08\u89C4\u5219\u7531 setRegionRules \u6CE8\u5165\uFF09\u3002\n function describeHit(selector, index) {\n const el = pick(selector, index);\n if (!el) return null;\n const tag = el.tagName.toLowerCase();\n const isForm = tag === 'input' || tag === 'textarea' || tag === 'select';\n const t = isForm ? '' : (el.innerText || el.textContent || '').trim().slice(0, 30);\n const get = (k) => { try { return el.getAttribute(k) || undefined; } catch (e) { return undefined; } };\n const rules = (window.__rpaDom && window.__rpaDom._regionRules) || [];\n const regionHit = rules.length > 0 ? regionLabelOf(el, rules) : null;\n return {\n tag,\n ...(t ? { text: t } : {}),\n ...(get('placeholder') ? { placeholder: get('placeholder') } : {}),\n ...(get('aria-label') ? { ariaLabel: get('aria-label') } : {}),\n ...(get('name') ? { name: get('name') } : {}),\n ...(regionHit ? { region: regionHit.label } : {}),\n };\n }\n // #16-B/P2 \u533A\u57DF\u8BC6\u522B\uFF1A\u76EE\u6807\u5143\u7D20\u5411\u4E0A\u904D\u5386\u7956\u5148\uFF08\u22645 \u5C42\uFF09\uFF0C\u7528\u884C\u4E3A\u89C4\u5219\u5339\u914D\u51FA\u533A\u57DF\u6807\u7B7E\u3002\n // \u89C4\u5219\uFF08behavior.rules.json \u968F\u6D41\u7A0B\u5305\u5206\u53D1\uFF0C\u5F15\u64CE\u96F6\u4E1A\u52A1\u903B\u8F91\uFF09\uFF1A\u5168\u90E8 match \u6761\u4EF6\u6EE1\u8DB3\u624D\u547D\u4E2D\u3002\n // \u8FD4\u56DE { label, ruleId, ancestry }\u2014\u2014ancestry \u4E3A\u7956\u5148\u94FE\u6458\u8981\uFF08\u6297\u6539\u7248\u91CD\u5B9A\u4F4D\u7528\uFF09\u3002\n // T014\uFF1A\u6761\u4EF6\u5224\u5B9A\u7EDF\u4E00\u8D70 ruleHits\uFF08\u4E0E region: \u8868\u8FBE\u5F0F\u5171\u7528\uFF0C\u9632\u6F02\u79FB\uFF09\u3002\n function identifyRegion(selector, index, rulesJson) {\n const target = pick(selector, index);\n if (!target) return null;\n let rules = [];\n try { rules = JSON.parse(rulesJson || '[]'); } catch (e) { rules = []; }\n const ancestry = [];\n let el = target;\n for (let d = 0; d < 5 && el; d++) {\n if (el !== target && el.nodeType === 1) {\n const cls = (el.className || '').toString().slice(0, 40);\n ancestry.push(el.tagName.toLowerCase() + (cls ? '.' + cls : ''));\n }\n // \u7528\u89C4\u5219\u5339\u914D\u5F53\u524D\u7956\u5148\n for (const r of rules) {\n if (ruleHits(r, el)) {\n return { label: r.label, ruleId: r.id, ancestry: ancestry.slice(0, 3) };\n }\n }\n el = el.parentElement;\n }\n return { label: undefined, ruleId: undefined, ancestry: ancestry.slice(0, 3) };\n }\n // T014 P0-3\uFF1A\u6CE8\u5165\u533A\u57DF\u89C4\u5219\uFF08behavior.rules.json \u968F\u6D41\u7A0B\u5305\u5206\u53D1\uFF1Bregion: \u8868\u8FBE\u5F0F\u4E0E describeHit\n // \u533A\u57DF\u6807\u6CE8\u5171\u7528\uFF1B\u5F15\u64CE\u96F6\u4E1A\u52A1\u903B\u8F91\u2014\u2014\u53EA\u5B58\u89C4\u5219 JSON\uFF09\u3002\u5E42\u7B49\uFF1A\u91CD\u590D\u6CE8\u5165\u8986\u76D6\u3002\n function setRegionRules(rulesJson) {\n let rules = [];\n try { rules = JSON.parse(rulesJson || '[]'); } catch (e) { rules = []; }\n window.__rpaDom._regionRules = rules;\n return true;\n }\n // #16-A \u70B9\u51FB\u524D\u906E\u6321\u68C0\u6D4B\uFF1A\u8FD4\u56DE (x,y) \u5904\u6700\u9876\u5C42\u5143\u7D20 + \u8BE5\u5143\u7D20\u662F\u5426\u5C5E\u4E8E\u76EE\u6807\uFF08\u76EE\u6807\u6216\u5176\u540E\u4EE3\uFF09\n function hitTestAt(selector, index, x, y) {\n const target = pick(selector, index);\n if (!target) return { found: false };\n const top = document.elementFromPoint(x, y);\n if (!top) return { found: true, hit: false, top: null };\n const hit = top === target || target.contains(top);\n return {\n found: true,\n hit,\n top: describeEl(top),\n target: describeEl(target),\n };\n }\n // #16-A \u5BF9\u8BDD\u6846\u5173\u95ED\uFF1A\u906E\u6321\u8005\uFF08dialog-layer \u7B49\u906E\u7F69\uFF09\u5185\u7684\u5173\u95ED\u6309\u94AE\u5750\u6807\uFF08\u4F9B\u5F15\u64CE\u70B9\u51FB\u5173\u95ED\u540E\u91CD\u8BD5\uFF09\n function findDialogClose(selector, index) {\n const target = pick(selector, index);\n if (!target) return null;\n const cx = Math.round(target.getBoundingClientRect().left + target.getBoundingClientRect().width / 2);\n const cy = Math.round(target.getBoundingClientRect().top + target.getBoundingClientRect().height / 2);\n const top = document.elementFromPoint(cx, cy);\n if (!top) return null;\n // \u627E\u6700\u9876\u5C42\u5143\u7D20\u4F5C\u4E3A\u906E\u6321\u8005\uFF08dialog-layer/\u906E\u7F69\uFF09\uFF0C\u5728\u5176\u5185\u627E\u5173\u95ED\u6309\u94AE\n let blocker = top;\n while (blocker && blocker !== document.body) {\n const s = getComputedStyle(blocker);\n if ((s.position === 'fixed' || s.position === 'absolute') && parseFloat(s.zIndex) >= 100) break;\n blocker = blocker.parentElement;\n }\n if (!blocker || blocker === document.body) return null;\n // \u5173\u95ED\u6309\u94AE\u641C\u7D22\u8303\u56F4\uFF1A\u906E\u6321\u8005\u81EA\u8EAB + \u5176\u5BF9\u8BDD\u6846\u7956\u5148\uFF08T014 \u9636\u6BB5 2 \u5B9E\u8DD1\u53D1\u73B0\uFF1A\u6B8B\u7559\u5BF9\u8BDD\u6846\u7684\n // .dialog-layer \u906E\u7F69\u4E0B\u5173\u95ED\u6309\u94AE\u5728 dialog-wrap \u5185\u2014\u2014v-leave \u5361\u4F4F\u7684\u5BF9\u8BDD\u6846\u906E\u6321\u6574\u9875\u65F6\uFF0C\n // \u5F15\u64CE\u70B9\u51FB\u5176\u5173\u95ED\u6309\u94AE\u515C\u5E95\u5173\u95ED\uFF0C\u91CD\u8BD5\u76EE\u6807\u70B9\u51FB\uFF09\u3002\n let scope = blocker;\n let walk = blocker.parentElement;\n while (walk && walk !== document.body) {\n if (/(dialog|layer|mask|modal|popup|panel)/i.test((walk.className || '').toString()) && walk.getBoundingClientRect().width > 0) {\n scope = walk;\n }\n walk = walk.parentElement;\n }\n // \u5173\u95ED\u6309\u94AE\u5019\u9009\uFF1Aclass \u542B close / \u56FE\u6807\u6309\u94AE / \u6587\u672C \u00D7\n const closeBtns = [...scope.querySelectorAll('a,button,[role=\"button\"],i,span,div')].filter((el) => {\n const cls = (el.className || '').toString();\n const txt = (el.innerText || '').trim();\n const r = el.getBoundingClientRect();\n return r.width > 8 && r.height > 8 &&\n (/close|close-btn|icon-close|dialog-close|del|btn-close/i.test(cls) || txt === '\u00D7' || txt === '\u2715' || txt === 'X' || txt === '\u5173\u95ED');\n });\n const btn = closeBtns[0];\n if (!btn) return null;\n const r = btn.getBoundingClientRect();\n return { x: Math.round(r.left + r.width / 2), y: Math.round(r.top + r.height / 2), cls: (btn.className || '').toString().slice(0, 40) };\n }\n // #16-A \u539F\u5B50\u5316\u906E\u6321\u68C0\u6D4B\uFF1A\u4E0E rect \u91C7\u96C6\u540C\u4E00\u6B21\u6C42\u503C\uFF08\u6D88\u9664\u300C\u91C7\u96C6-\u68C0\u6D4B\u300D\u4E24\u6B21\u6C42\u503C\u95F4\u7684 DOM \u7ADE\u6001\uFF09\u3002\n // \u8FD4\u56DE\u6BCF\u4E2A\u53EF\u89C1\u76EE\u6807\u7684 rect + \u4E2D\u5FC3\u70B9\u547D\u4E2D\u4FE1\u606F + \u5B89\u5168\u70B9\u51FB\u70B9\uFF1A\n // hit=true \u4E2D\u5FC3\u70B9\u547D\u4E2D\u76EE\u6807\u6216\u5176\u975E\u4EA4\u4E92\u540E\u4EE3\uFF08\u5B89\u5168\u70B9\u51FB\u70B9\uFF09\n // hit=false \u4E2D\u5FC3\u70B9\u88AB\u771F\u5B9E\u906E\u6321\uFF08\u6D6E\u52A8\u5C42/\u5F39\u5C42/\u5176\u5B83\u5143\u7D20\u8986\u76D6\uFF09\n // danger=true \u4E2D\u5FC3\u70B9\u843D\u5728\u76EE\u6807\u5185\u7684\u3010\u53EF\u4EA4\u4E92\u5B50\u5143\u7D20\u3011\uFF08\u94FE\u63A5/\u6309\u94AE\u7B49\uFF09\u4E0A\u2014\u2014\u70B9\u51FB\u4F1A\u89E6\u53D1\u5B50\u5143\u7D20\u800C\u975E\u76EE\u6807\n // safe \u77E9\u5F62\u5185\u626B\u63CF\u5230\u7684\u7B2C\u4E00\u4E2A\u300C\u5C5E\u4E8E\u76EE\u6807\u4E14\u975E\u4EA4\u4E92\u5B50\u5143\u7D20\u300D\u7684\u70B9\uFF08\u4E2D\u5FC3\u88AB\u76D6/\u5371\u9669\u65F6\u6362\u70B9\u7528\uFF09\uFF1B\n // \u65E0\u5B89\u5168\u70B9\u5219 null\uFF08\u6574\u4E2A\u76EE\u6807\u88AB\u76D6 \u2192 \u5F15\u64CE\u4FA7 ESC \u5173\u95ED\u6D6E\u52A8\u5C42\u91CD\u8BD5\uFF09\n // top=null\uFF08elementFromPoint \u65E0\u7ED3\u679C\uFF0C\u5982\u5143\u7D20\u90E8\u5206\u5728\u89C6\u53E3\u5916/\u6E32\u67D3\u8FB9\u7F18\uFF09\u2192 \u4E0D\u89C6\u4E3A\u906E\u6321\uFF0Chit=true\u3002\n function clickableRects(selector) {\n const d = window.__rpaDom;\n const nodes = d && d.getBySels ? d.getBySels(selector) : [];\n const vw = innerWidth || document.documentElement.clientWidth;\n const vh = innerHeight || document.documentElement.clientHeight;\n const out = [];\n const INTERACTIVE = 'a,button,[role=\"button\"],[role=\"link\"],input,select,textarea,[onclick],label';\n const isSafeHit = (el, top) => {\n if (!top) return false;\n if (top === el) return true;\n if (el.contains(top)) {\n return !(top.closest && top.closest(INTERACTIVE) === top);\n }\n return false;\n };\n for (let i = 0; i < nodes.length; i++) {\n const el = nodes[i];\n const t = el.getBoundingClientRect();\n const fullW = Math.max(1, t.width);\n const fullH = Math.max(1, t.height);\n const x = Math.max(t.left, 0);\n const y = Math.max(t.top, 0);\n const w = Math.max(0, Math.min(t.right, vw) - x);\n const h = Math.max(0, Math.min(t.bottom, vh) - y);\n if (w <= 0 || h <= 0) continue;\n // #16-A \u53EF\u89C1\u6BD4\u4F8B\uFF1A\u89C6\u53E3\u5185\u9762\u79EF / \u603B\u9762\u79EF\u3002\u7A84\u89C6\u53E3\u4E0B\u5927\u90E8\u5206\u5728\u89C6\u53E3\u5916\u7684\u5143\u7D20\n // \uFF08\u5982\u9876\u90E8\u5BFC\u822A\u53F3\u4FA7\u7684\u641C\u7D22\u6309\u94AE\uFF09\u70B9\u51FB\u5750\u6807\u4F1A\u843D\u5728\u89C6\u53E3\u5916/\u8FB9\u7F18 \u2192 CDP input \u6302\u8D77\u6216\u70B9\u7A7A\u3002\n // \u53EF\u89C1\u6BD4\u4F8B < 50% \u2192 \u6807\u8BB0 partiallyVisible\uFF0C\u5F15\u64CE\u4FA7\u62A5\u300C\u76EE\u6807\u5927\u90E8\u5206\u5728\u89C6\u53E3\u5916\u300D\u800C\u975E\u5C1D\u8BD5\u70B9\u51FB\u3002\n const visibleRatio = (w * h) / (fullW * fullH);\n const partiallyVisible = visibleRatio < 0.5;\n if (partiallyVisible) {\n out.push({\n x: Math.round(x), y: Math.round(y), w: Math.round(w), h: Math.round(h),\n hit: false, danger: false, safe: null, partiallyVisible: true, top: null, target: describeEl(el),\n });\n continue;\n }\n const cx = Math.round(x + w / 2);\n const cy = Math.round(y + h / 2);\n const top = document.elementFromPoint(cx, cy);\n if (!top) {\n // elementFromPoint \u65E0\u7ED3\u679C\uFF1A\u53EF\u89C1\u6BD4\u4F8B\u8DB3\u591F\uFF08\u226550%\uFF09\u4F46\u53D6\u4E0D\u5230\u9876\u5C42\u5143\u7D20 \u2192 \u6E32\u67D3\u8FB9\u754C\u5BB9\u5FCD\uFF0C\u4E0D\u963B\u585E\n out.push({\n x: Math.round(x), y: Math.round(y), w: Math.round(w), h: Math.round(h),\n hit: true, danger: false, safe: null, partiallyVisible: false, top: null, target: describeEl(el),\n });\n continue;\n }\n // T014 \u9636\u6BB5 2 \u5B9E\u8DD1\u53D1\u73B0\uFF1A\u6B8B\u7559\u5BF9\u8BDD\u6846 mask\uFF08visibility:hidden / opacity:0 \u7684\u5168\u5C4F\u906E\u7F69\u2014\u2014\n // zhipin \u57CE\u5E02\u9009\u62E9\u5668\u5173\u95ED\u540E .dialog-layer \u7559\u5728 DOM\uFF09\u4F1A\u88AB elementFromPoint \u547D\u4E2D\uFF0C\u4F46\u771F\u5B9E\n // \u6307\u9488\u4E8B\u4EF6\u7A7F\u900F\uFF08\u4E0D\u53EF\u89C1\u5143\u7D20\u4E0D\u53C2\u4E0E\u547D\u4E2D\u6D4B\u8BD5\uFF09\u2192 \u4E0D\u6784\u6210\u771F\u5B9E\u906E\u6321\uFF0C\u89C6\u4E3A hit\uFF08\u9632 ESC \u6B7B\u5FAA\u73AF\uFF09\u3002\n const topStyle = getComputedStyle(top);\n if (topStyle.visibility === 'hidden' || parseFloat(topStyle.opacity) === 0) {\n out.push({\n x: Math.round(x), y: Math.round(y), w: Math.round(w), h: Math.round(h),\n hit: true, danger: false, safe: null, partiallyVisible: false, top: null, target: describeEl(el),\n });\n continue;\n }\n const belongs = top === el || el.contains(top);\n const isInteractiveChild = top !== el && el.contains(top) && top.closest && top.closest(INTERACTIVE) === top;\n // \u4E2D\u5FC3\u88AB\u76D6/\u5371\u9669 \u2192 \u626B\u63CF\u77E9\u5F62\u5185\u90E8\u627E\u5B89\u5168\u70B9\uFF085\u00D75 \u7F51\u683C\uFF0C\u907F\u5F00\u4EA4\u4E92\u5B50\u5143\u7D20\u4E0E\u906E\u6321\uFF09\n let safe = null;\n if (!(belongs && !isInteractiveChild)) {\n for (let gy = 1; gy <= 4 && !safe; gy++) {\n for (let gx = 1; gx <= 4 && !safe; gx++) {\n const sx = Math.round(x + (w * gx) / 5);\n const sy = Math.round(y + (h * gy) / 5);\n const st = document.elementFromPoint(sx, sy);\n if (st && isSafeHit(el, st)) safe = { x: sx, y: sy };\n }\n }\n }\n out.push({\n x: Math.round(x), y: Math.round(y), w: Math.round(w), h: Math.round(h),\n hit: belongs && !isInteractiveChild,\n danger: isInteractiveChild,\n safe,\n partiallyVisible: false,\n top: describeEl(top),\n target: describeEl(el),\n });\n }\n return out;\n }\n // #16-A \u76EE\u6807\u77E9\u5F62\u5185\u627E\u5B89\u5168\u70B9\u51FB\u70B9\uFF1A\u7F51\u683C\u91C7\u6837\uFF085\u00D75\uFF09\uFF0C\u8FD4\u56DE\u7B2C\u4E00\u4E2A\u300C\u5C5E\u4E8E\u76EE\u6807\u4E14\u4E0D\u5728\u53EF\u4EA4\u4E92\u5B50\u5143\u7D20\u4E0A\u300D\u7684\u70B9\u3002\n // \u5927\u5BB9\u5668\u5361\u7247\uFF08\u5982 job-card-wrap\uFF09\u4E2D\u5FC3\u5E38\u843D\u5728\u516C\u53F8\u94FE\u63A5/\u804C\u4F4D\u8BE6\u60C5\u94FE\u63A5\u4E0A\uFF0C\u76F4\u63A5\u70B9\u4F1A\u8BEF\u8DF3\u8F6C\uFF1B\n // \u5B89\u5168\u70B9\u907F\u5F00\u4EA4\u4E92\u5B50\u5143\u7D20\uFF08a/button/[onclick]\uFF09\uFF0C\u53EA\u843D\u5728\u6587\u672C/\u7A7A\u767D\u533A\u57DF\uFF08\u70B9\u51FB\u4ECD\u7531\u7236\u7EA7\u4E8B\u4EF6\u5192\u6CE1\u5904\u7406\uFF09\u3002\n function safePointIn(selector, index) {\n const el = pick(selector, index);\n if (!el) return null;\n const t = el.getBoundingClientRect();\n const INTERACTIVE = 'a,button,[role=\"button\"],[role=\"link\"],input,select,textarea,[onclick],label';\n for (let gy = 2; gy <= 3; gy++) {\n for (let gx = 2; gx <= 3; gx++) {\n const cx = Math.round(t.left + (t.width * gx) / 5);\n const cy = Math.round(t.top + (t.height * gy) / 5);\n const top = document.elementFromPoint(cx, cy);\n if (!top) continue;\n if (top === el || el.contains(top)) {\n const bad = top !== el && top.closest && top.closest(INTERACTIVE) === top;\n if (!bad) return { x: cx, y: cy };\n }\n }\n }\n // \u515C\u5E95\uFF1A\u4E2D\u5FC3\u70B9\uFF08\u53EF\u80FD\u5371\u9669\uFF0C\u7531\u5F15\u64CE\u4FA7\u518D\u5224\u65AD\uFF09\n return { x: Math.round(t.left + t.width / 2), y: Math.round(t.top + t.height / 2) };\n }\n return { getBySels, scrollIntoView, scrollIntoViewRhythm, textOf, focusInput, pageText, numberedElements, describeEl, describeHit, hitTestAt, clickableRects, safePointIn, identifyRegion, findDialogClose, setRegionRules, _domVersion: 3 };\n})();\ntrue\n";
|
|
21
|
+
/**
|
|
22
|
+
* T007 P0-1:input 事件补派发(insertText 改 DOM value 不触发 input 事件——React 受控组件
|
|
23
|
+
* onChange 同步 state 需要;BATCH-D:导出为 const,mock 回归可直接求值,避免测试复制漂移)。
|
|
24
|
+
*/
|
|
25
|
+
export declare const INPUT_EVENT_DISPATCH = "(() => {\n const el = document.activeElement;\n if (el && typeof el.dispatchEvent === 'function') {\n el.dispatchEvent(new Event('input', { bubbles: true }));\n }\n})()";
|
|
21
26
|
/** ENV_CHECK 检测注入脚本(A1;页面端一次 evaluate 返回全部信号) */
|
|
22
27
|
export declare function envCheckSource(envCheck: EnvCheckConfig): string;
|
|
23
28
|
/** ENV_CHECK 配置(flow.json 顶层可选字段;平台可配置,引擎不硬编码) */
|
|
@@ -44,9 +49,13 @@ export declare class CdpPage implements PageOps {
|
|
|
44
49
|
private human?;
|
|
45
50
|
/** ENV_CHECK 配置(flow.json 顶层 envCheck;缺省不检测) */
|
|
46
51
|
private envCheck?;
|
|
52
|
+
/** T014 P0-3:区域规则 JSON(behavior.rules.json 的 regionRules 数组;region: 表达式/区域标注用) */
|
|
53
|
+
private regionRulesJson;
|
|
47
54
|
constructor(port: number, sim: SimProfile);
|
|
48
55
|
/** 设置 ENV_CHECK 配置(A1;流程加载时注入) */
|
|
49
56
|
setEnvCheck(config: EnvCheckConfig | undefined): void;
|
|
57
|
+
/** T014 P0-3:设置区域规则(behavior.rules.json 的 regionRules 数组;region: 表达式与 describeHit 区域标注用) */
|
|
58
|
+
setRegionRules(rulesJson: string): void;
|
|
50
59
|
/** 确保浏览器调试实例在跑(端口不可连则拉起 Edge;edgePath 空则直接报错) */
|
|
51
60
|
ensureBrowser(edgePath: string, userDataDir: string): Promise<void>;
|
|
52
61
|
/**
|
|
@@ -71,6 +80,8 @@ export declare class CdpPage implements PageOps {
|
|
|
71
80
|
private currentClient;
|
|
72
81
|
/** 注入 DOM 辅助(幂等:页面已有 __rpaDom 则跳过) */
|
|
73
82
|
private ensureDom;
|
|
83
|
+
/** 注入城市对话框 DOM 辅助(T012 P0-1;幂等:已有 cityLabelPos 则跳过) */
|
|
84
|
+
private ensureCityDom;
|
|
74
85
|
/** JS 字符串字面量(selector 插值安全) */
|
|
75
86
|
private jsStr;
|
|
76
87
|
/** ENV_CHECK 检测(A1 D-V2:每步执行前;命中 → 抛 verify 类别错误 + category=env_check) */
|
|
@@ -78,6 +89,14 @@ export declare class CdpPage implements PageOps {
|
|
|
78
89
|
/** 观察通道 DOM 快照(契约口径:文本快照摘要 + 编号元素) */
|
|
79
90
|
snapshotDom(): Promise<DomSnapshot>;
|
|
80
91
|
navigate(url: string): Promise<void>;
|
|
92
|
+
/**
|
|
93
|
+
* T012 P0-1:地区切换「界面操作优先」——zhipin 城市选择器 UI 路径,URL 参数仅兜底。
|
|
94
|
+
* 返回 true = UI 切换成功(或已是目标城市);false = 调用方回退 URL 导航。
|
|
95
|
+
* 导航可靠性优先:任何一步失败/超时都返回 false,绝不因拟人引入新失败。
|
|
96
|
+
* 仅当目标 URL 为 zhipin 且带 city 参数时尝试;其余直接返回 false(行为不变)。
|
|
97
|
+
* 决策流在 city-switch.runCitySwitchUi(接口驱动,mock 可确定性测试——BATCH-D)。
|
|
98
|
+
*/
|
|
99
|
+
private trySwitchCityViaUi;
|
|
81
100
|
click(selector: string, index: number): Promise<void>;
|
|
82
101
|
/** P2 区域识别:目标元素所在区域的语义描述(DOM_SOURCE.identifyRegion;旁路,失败返回 null) */
|
|
83
102
|
identifyRegion(selector: string, index: number, rulesJson: string): Promise<{
|
|
@@ -108,9 +127,9 @@ export declare class CdpPage implements PageOps {
|
|
|
108
127
|
}
|
|
109
128
|
/**
|
|
110
129
|
* T005 P0-2:路由命中登录页特征(host+path;平台无关特征词:/login /passport /captcha /web/user/ 等)。
|
|
111
|
-
*
|
|
130
|
+
* T014 P0-2:定义上移至 region.ts 统一实现(登录区双重特征),此处再导出保兼容(测试/调用方不变)。
|
|
112
131
|
*/
|
|
113
|
-
export
|
|
132
|
+
export { isLoginRoute } from '../region.js';
|
|
114
133
|
/**
|
|
115
134
|
* T005 P1-3:报错文案选择器消毒——selector 含占位符形态({...} 未替换变量 / <...> 掩码残留)时不泄漏
|
|
116
135
|
* 原文,显示友好描述(避免用户看到「点击 <keyword> 后页面跳转」这类占位符泄漏)。
|
package/dist/cdp/page.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { ExecError as ExecErr } from '../executor.js';
|
|
2
|
-
import { Human, IndexOutOfRangeError } from '../human.js';
|
|
2
|
+
import { Human, IndexOutOfRangeError, point } from '../human.js';
|
|
3
3
|
import { snapshotFromWeb } from '../export.js';
|
|
4
4
|
import { CdpClient, CdpInputSink, closeTarget, listTargets, openTarget } from './client.js';
|
|
5
|
+
import { CITY_SWITCH_SOURCE, ZHIPIN_CITY_NAMES, cityCodeOfUrl, runCitySwitchUi } from './city-switch.js';
|
|
6
|
+
import { isLoginRoute } from '../region.js';
|
|
5
7
|
/**
|
|
6
8
|
* CDP PageOps 适配层 —— executor 的 PageOps 接真实浏览器(移植自 Rust `cdp_page.rs`)。
|
|
7
9
|
*
|
|
@@ -41,6 +43,72 @@ window.__rpaDom = (function() {
|
|
|
41
43
|
return [];
|
|
42
44
|
}
|
|
43
45
|
}
|
|
46
|
+
// T014 P0-3:区域规则命中判定(单一实现——identifyRegion 与 region: 表达式共用,防逻辑漂移)
|
|
47
|
+
function ruleHits(rule, el) {
|
|
48
|
+
const m = rule.match || {};
|
|
49
|
+
let hit = true;
|
|
50
|
+
if (m.textContains && !((el.innerText || '').includes(m.textContains))) hit = false;
|
|
51
|
+
if (m.textContainsAll && Array.isArray(m.textContainsAll)) {
|
|
52
|
+
const txt = el.innerText || '';
|
|
53
|
+
for (const kw of m.textContainsAll) {
|
|
54
|
+
if (!txt.includes(kw)) { hit = false; break; }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (m.hasInput && !el.querySelector('input, textarea')) hit = false;
|
|
58
|
+
if (m.hasButton && !el.querySelector('button, a[role="button"], [role="button"]')) hit = false;
|
|
59
|
+
if (m.ancestorTag && !(el.tagName || '').toLowerCase().includes(m.ancestorTag)) hit = false;
|
|
60
|
+
if (m.ancestorClass && !((el.className || '').toString().includes(m.ancestorClass))) hit = false;
|
|
61
|
+
return hit;
|
|
62
|
+
}
|
|
63
|
+
// T014 P0-1:元素所属区域标签(走元素自身→祖先,首个规则命中即返回;无 → null)
|
|
64
|
+
function regionLabelOf(el, rules) {
|
|
65
|
+
let cur = el;
|
|
66
|
+
for (let d = 0; d < 5 && cur; d++) {
|
|
67
|
+
for (const r of rules) {
|
|
68
|
+
if (ruleHits(r, cur)) return { label: r.label, ruleId: r.id };
|
|
69
|
+
}
|
|
70
|
+
cur = cur.parentElement;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
// T014 P0-3:区域主容器解析(region:<标签> 与 region:<标签> >> <n> 共用):
|
|
75
|
+
// self-match 候选(面积 < 视口)取「最深且文本 ≥100 字」;无 → 最大面积;无候选 → null。
|
|
76
|
+
function regionContainerOf(label, rules) {
|
|
77
|
+
const vw = innerWidth || document.documentElement.clientWidth || 1000;
|
|
78
|
+
const vh = innerHeight || document.documentElement.clientHeight || 800;
|
|
79
|
+
const vpArea = vw * vh;
|
|
80
|
+
const matches = [];
|
|
81
|
+
for (const el of Array.from(document.querySelectorAll('body *')).filter(isVisible)) {
|
|
82
|
+
if (!(el.innerText || '').trim()) continue;
|
|
83
|
+
for (const r of rules) {
|
|
84
|
+
if (r.label === label && ruleHits(r, el)) {
|
|
85
|
+
const rect = el.getBoundingClientRect();
|
|
86
|
+
const area = rect.width * rect.height;
|
|
87
|
+
if (area > 0 && area < vpArea) matches.push({ el, area });
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (matches.length === 0) return null;
|
|
93
|
+
const depthOf = (el) => {
|
|
94
|
+
let d = 0;
|
|
95
|
+
let cur = el.parentElement;
|
|
96
|
+
while (cur) { d++; cur = cur.parentElement; }
|
|
97
|
+
return d;
|
|
98
|
+
};
|
|
99
|
+
const substantial = matches.filter((m) => (m.el.innerText || '').trim().length >= 100);
|
|
100
|
+
let best = null;
|
|
101
|
+
if (substantial.length > 0) {
|
|
102
|
+
for (const m of substantial) {
|
|
103
|
+
if (!best || depthOf(m.el) > depthOf(best.el)) best = m;
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
for (const m of matches) {
|
|
107
|
+
if (!best || m.area > best.area) best = m;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return best ? best.el : null;
|
|
111
|
+
}
|
|
44
112
|
function getBySels(selector) {
|
|
45
113
|
// A3 数组链(#17 修复):依次尝试,第一个命中的返回
|
|
46
114
|
if (Array.isArray(selector)) {
|
|
@@ -52,6 +120,33 @@ window.__rpaDom = (function() {
|
|
|
52
120
|
}
|
|
53
121
|
const sel = String(selector);
|
|
54
122
|
if (sel.startsWith('text:')) return byText(sel.slice(5));
|
|
123
|
+
// T014 P0-3:region:<标签>[ >> <n>] —— 引擎内部区域寻址表达式(不开放给用户写 flow)。
|
|
124
|
+
// 规则由 setRegionRules 注入(随流程包分发,引擎零业务逻辑;T014 阶段 2 仲裁:注入规则
|
|
125
|
+
// 已按档案 seen 置信度重排,同一元素命中多条规则时高置信区域优先)。
|
|
126
|
+
// 主容器解析:self-match 候选(面积 < 视口)中,取「最深且文本 ≥100 字」的候选——
|
|
127
|
+
// 详情区文本链 container ⊃ box ⊃ body ⊃ h3,最深实质候选 = 内容容器 body
|
|
128
|
+
// (深于含边栏/求职意向的包裹层 container,见 USAGE-ISSUES #16 边栏污染);
|
|
129
|
+
// 无 ≥100 字候选(短文本区域如搜索表单)→ 回退最大面积。
|
|
130
|
+
// 'region:<标签> >>' = 区域内全部可见可交互元素(#N 编号枚举的区域内版;
|
|
131
|
+
// executor 用 index 参数选第 i 个 → 越界走 index_out_of_range 列表耗尽语义);
|
|
132
|
+
// 'region:<标签> >> <n>' = 区域内第 n 个(0 起)可交互元素(引擎内部具体寻址)。
|
|
133
|
+
if (sel.startsWith('region:')) {
|
|
134
|
+
const rest = sel.slice(7).trim();
|
|
135
|
+
const hasArrow = rest.includes('>>');
|
|
136
|
+
const parts = rest.split('>>');
|
|
137
|
+
const label = parts[0].trim();
|
|
138
|
+
// 无 >> → 区域主容器;有 >> 且索引空 → 区域内全部可交互元素;有 >> 且索引 n → 第 n 个
|
|
139
|
+
const indexPart = hasArrow ? parts[1].trim() : undefined;
|
|
140
|
+
const rules = (window.__rpaDom && window.__rpaDom._regionRules) || [];
|
|
141
|
+
const container = regionContainerOf(label, rules);
|
|
142
|
+
if (!container) return [];
|
|
143
|
+
if (indexPart === undefined) return [container];
|
|
144
|
+
const tags = 'a,button,input,select,textarea,[role="button"],[role="link"],[contenteditable="true"]';
|
|
145
|
+
const nodes = Array.from(container.querySelectorAll(tags)).filter(isVisible);
|
|
146
|
+
if (indexPart === '') return nodes;
|
|
147
|
+
const n = Number(indexPart);
|
|
148
|
+
return Number.isInteger(n) && n >= 0 && n < nodes.length ? [nodes[n]] : [];
|
|
149
|
+
}
|
|
55
150
|
// #16-B 编号寻址(虚拟空间视觉):#N = numberedElements 列表第 N 个可见交互元素(1 起)。
|
|
56
151
|
// 快照编号与寻址共用同一枚举 → Agent/日志可「看编号直接点」;编号随 DOM 变化,操作时现取。
|
|
57
152
|
const numMatch = sel.match(/^#(\d+)$/);
|
|
@@ -86,6 +181,29 @@ window.__rpaDom = (function() {
|
|
|
86
181
|
el.scrollIntoView({ block: 'center', inline: 'center' });
|
|
87
182
|
return true;
|
|
88
183
|
}
|
|
184
|
+
// T012 P1-2:轻量拟人滚动——分段渐进滚动(2~5 段,段间 30~120ms 随机节奏),
|
|
185
|
+
// 终位与 scrollIntoView 一致(block:center)——只改节奏不改落点,功能不回归。
|
|
186
|
+
// 返回 Promise(evaluate awaitPromise:true 生效);找不到元素 → false(同步值兼容)。
|
|
187
|
+
function scrollIntoViewRhythm(selector, index) {
|
|
188
|
+
const el = pick(selector, index);
|
|
189
|
+
if (!el) return false;
|
|
190
|
+
const steps = 2 + Math.floor(Math.random() * 4);
|
|
191
|
+
const vh = innerHeight || document.documentElement.clientHeight || 600;
|
|
192
|
+
const rect = el.getBoundingClientRect();
|
|
193
|
+
const curTop = pageYOffset || document.documentElement.scrollTop || 0;
|
|
194
|
+
const targetY = curTop + rect.top - (vh - Math.min(rect.height, vh)) / 2;
|
|
195
|
+
const dy = targetY - curTop;
|
|
196
|
+
return (async function () {
|
|
197
|
+
for (let i = 1; i <= steps; i++) {
|
|
198
|
+
const t = i / steps;
|
|
199
|
+
const ease = t * t * (3 - 2 * t);
|
|
200
|
+
window.scrollTo(0, Math.round(curTop + dy * ease));
|
|
201
|
+
await new Promise(function (r) { setTimeout(r, 30 + Math.random() * 90); });
|
|
202
|
+
}
|
|
203
|
+
el.scrollIntoView({ block: 'center', inline: 'center' });
|
|
204
|
+
return true;
|
|
205
|
+
})();
|
|
206
|
+
}
|
|
89
207
|
function textOf(selector, index) {
|
|
90
208
|
const el = pick(selector, index);
|
|
91
209
|
return el ? (el.innerText || el.textContent || '') : '';
|
|
@@ -130,6 +248,7 @@ window.__rpaDom = (function() {
|
|
|
130
248
|
// 只有可重定位的语义特征(tag/文本截断/placeholder/aria-label/name)——无坐标无原文。
|
|
131
249
|
// 掩码前移(铁律):表单元素(input/textarea/select)绝不取 value——那是用户输入值
|
|
132
250
|
// (搜索词/电话等敏感原文),落盘即违规;表单锚点只用 placeholder/name/aria-label。
|
|
251
|
+
// T014 P1-4:附加所在区域标签(region)——锚点只在同一区域复用(规则由 setRegionRules 注入)。
|
|
133
252
|
function describeHit(selector, index) {
|
|
134
253
|
const el = pick(selector, index);
|
|
135
254
|
if (!el) return null;
|
|
@@ -137,17 +256,21 @@ window.__rpaDom = (function() {
|
|
|
137
256
|
const isForm = tag === 'input' || tag === 'textarea' || tag === 'select';
|
|
138
257
|
const t = isForm ? '' : (el.innerText || el.textContent || '').trim().slice(0, 30);
|
|
139
258
|
const get = (k) => { try { return el.getAttribute(k) || undefined; } catch (e) { return undefined; } };
|
|
259
|
+
const rules = (window.__rpaDom && window.__rpaDom._regionRules) || [];
|
|
260
|
+
const regionHit = rules.length > 0 ? regionLabelOf(el, rules) : null;
|
|
140
261
|
return {
|
|
141
262
|
tag,
|
|
142
263
|
...(t ? { text: t } : {}),
|
|
143
264
|
...(get('placeholder') ? { placeholder: get('placeholder') } : {}),
|
|
144
265
|
...(get('aria-label') ? { ariaLabel: get('aria-label') } : {}),
|
|
145
266
|
...(get('name') ? { name: get('name') } : {}),
|
|
267
|
+
...(regionHit ? { region: regionHit.label } : {}),
|
|
146
268
|
};
|
|
147
269
|
}
|
|
148
270
|
// #16-B/P2 区域识别:目标元素向上遍历祖先(≤5 层),用行为规则匹配出区域标签。
|
|
149
271
|
// 规则(behavior.rules.json 随流程包分发,引擎零业务逻辑):全部 match 条件满足才命中。
|
|
150
272
|
// 返回 { label, ruleId, ancestry }——ancestry 为祖先链摘要(抗改版重定位用)。
|
|
273
|
+
// T014:条件判定统一走 ruleHits(与 region: 表达式共用,防漂移)。
|
|
151
274
|
function identifyRegion(selector, index, rulesJson) {
|
|
152
275
|
const target = pick(selector, index);
|
|
153
276
|
if (!target) return null;
|
|
@@ -162,20 +285,7 @@ window.__rpaDom = (function() {
|
|
|
162
285
|
}
|
|
163
286
|
// 用规则匹配当前祖先
|
|
164
287
|
for (const r of rules) {
|
|
165
|
-
|
|
166
|
-
let hit = true;
|
|
167
|
-
if (m.textContains && !((el.innerText || '').includes(m.textContains))) hit = false;
|
|
168
|
-
if (m.textContainsAll && Array.isArray(m.textContainsAll)) {
|
|
169
|
-
const txt = el.innerText || '';
|
|
170
|
-
for (const kw of m.textContainsAll) {
|
|
171
|
-
if (!txt.includes(kw)) { hit = false; break; }
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
if (m.hasInput && !el.querySelector('input, textarea')) hit = false;
|
|
175
|
-
if (m.hasButton && !el.querySelector('button, a[role="button"], [role="button"]')) hit = false;
|
|
176
|
-
if (m.ancestorTag && !(el.tagName || '').toLowerCase().includes(m.ancestorTag)) hit = false;
|
|
177
|
-
if (m.ancestorClass && !((el.className || '').toString().includes(m.ancestorClass))) hit = false;
|
|
178
|
-
if (hit) {
|
|
288
|
+
if (ruleHits(r, el)) {
|
|
179
289
|
return { label: r.label, ruleId: r.id, ancestry: ancestry.slice(0, 3) };
|
|
180
290
|
}
|
|
181
291
|
}
|
|
@@ -183,6 +293,14 @@ window.__rpaDom = (function() {
|
|
|
183
293
|
}
|
|
184
294
|
return { label: undefined, ruleId: undefined, ancestry: ancestry.slice(0, 3) };
|
|
185
295
|
}
|
|
296
|
+
// T014 P0-3:注入区域规则(behavior.rules.json 随流程包分发;region: 表达式与 describeHit
|
|
297
|
+
// 区域标注共用;引擎零业务逻辑——只存规则 JSON)。幂等:重复注入覆盖。
|
|
298
|
+
function setRegionRules(rulesJson) {
|
|
299
|
+
let rules = [];
|
|
300
|
+
try { rules = JSON.parse(rulesJson || '[]'); } catch (e) { rules = []; }
|
|
301
|
+
window.__rpaDom._regionRules = rules;
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
186
304
|
// #16-A 点击前遮挡检测:返回 (x,y) 处最顶层元素 + 该元素是否属于目标(目标或其后代)
|
|
187
305
|
function hitTestAt(selector, index, x, y) {
|
|
188
306
|
const target = pick(selector, index);
|
|
@@ -213,8 +331,19 @@ window.__rpaDom = (function() {
|
|
|
213
331
|
blocker = blocker.parentElement;
|
|
214
332
|
}
|
|
215
333
|
if (!blocker || blocker === document.body) return null;
|
|
334
|
+
// 关闭按钮搜索范围:遮挡者自身 + 其对话框祖先(T014 阶段 2 实跑发现:残留对话框的
|
|
335
|
+
// .dialog-layer 遮罩下关闭按钮在 dialog-wrap 内——v-leave 卡住的对话框遮挡整页时,
|
|
336
|
+
// 引擎点击其关闭按钮兜底关闭,重试目标点击)。
|
|
337
|
+
let scope = blocker;
|
|
338
|
+
let walk = blocker.parentElement;
|
|
339
|
+
while (walk && walk !== document.body) {
|
|
340
|
+
if (/(dialog|layer|mask|modal|popup|panel)/i.test((walk.className || '').toString()) && walk.getBoundingClientRect().width > 0) {
|
|
341
|
+
scope = walk;
|
|
342
|
+
}
|
|
343
|
+
walk = walk.parentElement;
|
|
344
|
+
}
|
|
216
345
|
// 关闭按钮候选:class 含 close / 图标按钮 / 文本 ×
|
|
217
|
-
const closeBtns = [...
|
|
346
|
+
const closeBtns = [...scope.querySelectorAll('a,button,[role="button"],i,span,div')].filter((el) => {
|
|
218
347
|
const cls = (el.className || '').toString();
|
|
219
348
|
const txt = (el.innerText || '').trim();
|
|
220
349
|
const r = el.getBoundingClientRect();
|
|
@@ -282,6 +411,17 @@ window.__rpaDom = (function() {
|
|
|
282
411
|
});
|
|
283
412
|
continue;
|
|
284
413
|
}
|
|
414
|
+
// T014 阶段 2 实跑发现:残留对话框 mask(visibility:hidden / opacity:0 的全屏遮罩——
|
|
415
|
+
// zhipin 城市选择器关闭后 .dialog-layer 留在 DOM)会被 elementFromPoint 命中,但真实
|
|
416
|
+
// 指针事件穿透(不可见元素不参与命中测试)→ 不构成真实遮挡,视为 hit(防 ESC 死循环)。
|
|
417
|
+
const topStyle = getComputedStyle(top);
|
|
418
|
+
if (topStyle.visibility === 'hidden' || parseFloat(topStyle.opacity) === 0) {
|
|
419
|
+
out.push({
|
|
420
|
+
x: Math.round(x), y: Math.round(y), w: Math.round(w), h: Math.round(h),
|
|
421
|
+
hit: true, danger: false, safe: null, partiallyVisible: false, top: null, target: describeEl(el),
|
|
422
|
+
});
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
285
425
|
const belongs = top === el || el.contains(top);
|
|
286
426
|
const isInteractiveChild = top !== el && el.contains(top) && top.closest && top.closest(INTERACTIVE) === top;
|
|
287
427
|
// 中心被盖/危险 → 扫描矩形内部找安全点(5×5 网格,避开交互子元素与遮挡)
|
|
@@ -331,10 +471,20 @@ window.__rpaDom = (function() {
|
|
|
331
471
|
// 兜底:中心点(可能危险,由引擎侧再判断)
|
|
332
472
|
return { x: Math.round(t.left + t.width / 2), y: Math.round(t.top + t.height / 2) };
|
|
333
473
|
}
|
|
334
|
-
return { getBySels, scrollIntoView, textOf, focusInput, pageText, numberedElements, describeEl, describeHit, hitTestAt, clickableRects, safePointIn, identifyRegion, findDialogClose };
|
|
474
|
+
return { getBySels, scrollIntoView, scrollIntoViewRhythm, textOf, focusInput, pageText, numberedElements, describeEl, describeHit, hitTestAt, clickableRects, safePointIn, identifyRegion, findDialogClose, setRegionRules, _domVersion: 3 };
|
|
335
475
|
})();
|
|
336
476
|
true
|
|
337
477
|
`;
|
|
478
|
+
/**
|
|
479
|
+
* T007 P0-1:input 事件补派发(insertText 改 DOM value 不触发 input 事件——React 受控组件
|
|
480
|
+
* onChange 同步 state 需要;BATCH-D:导出为 const,mock 回归可直接求值,避免测试复制漂移)。
|
|
481
|
+
*/
|
|
482
|
+
export const INPUT_EVENT_DISPATCH = `(() => {
|
|
483
|
+
const el = document.activeElement;
|
|
484
|
+
if (el && typeof el.dispatchEvent === 'function') {
|
|
485
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
486
|
+
}
|
|
487
|
+
})()`;
|
|
338
488
|
/** ENV_CHECK 检测注入脚本(A1;页面端一次 evaluate 返回全部信号) */
|
|
339
489
|
export function envCheckSource(envCheck) {
|
|
340
490
|
const verifyUrls = (envCheck.verifyUrls ?? []).map((u) => JSON.stringify(u));
|
|
@@ -373,6 +523,8 @@ export class CdpPage {
|
|
|
373
523
|
human;
|
|
374
524
|
/** ENV_CHECK 配置(flow.json 顶层 envCheck;缺省不检测) */
|
|
375
525
|
envCheck;
|
|
526
|
+
/** T014 P0-3:区域规则 JSON(behavior.rules.json 的 regionRules 数组;region: 表达式/区域标注用) */
|
|
527
|
+
regionRulesJson = '';
|
|
376
528
|
constructor(port, sim) {
|
|
377
529
|
this.port = port;
|
|
378
530
|
this.sim = sim;
|
|
@@ -381,6 +533,10 @@ export class CdpPage {
|
|
|
381
533
|
setEnvCheck(config) {
|
|
382
534
|
this.envCheck = config;
|
|
383
535
|
}
|
|
536
|
+
/** T014 P0-3:设置区域规则(behavior.rules.json 的 regionRules 数组;region: 表达式与 describeHit 区域标注用) */
|
|
537
|
+
setRegionRules(rulesJson) {
|
|
538
|
+
this.regionRulesJson = rulesJson;
|
|
539
|
+
}
|
|
384
540
|
/** 确保浏览器调试实例在跑(端口不可连则拉起 Edge;edgePath 空则直接报错) */
|
|
385
541
|
async ensureBrowser(edgePath, userDataDir) {
|
|
386
542
|
// 检查调试端口是否可连(/json/list);不可连且给了 edgePath → 拉起 Edge
|
|
@@ -430,6 +586,11 @@ export class CdpPage {
|
|
|
430
586
|
}) ?? undefined;
|
|
431
587
|
const t = reuse ?? (await openTarget(url, this.port));
|
|
432
588
|
const client = await CdpClient.connect(t.wsUrl);
|
|
589
|
+
// 提前绑定 human/targetId(T012 P0-1:复用标签同路由的城市切换需要拟人点击层)
|
|
590
|
+
this.targetId = t.id;
|
|
591
|
+
this.openedThisRun = !reuse;
|
|
592
|
+
this.human = new Human(new CdpInputSink(client), this.sim);
|
|
593
|
+
let needNav = false;
|
|
433
594
|
if (reuse) {
|
|
434
595
|
// 复用标签:host+path 相同则跳过导航(忽略 query——zhipin 会带 _security_check 等参数,
|
|
435
596
|
// 精确比较会触发重新导航 → 安全校验重定向 → 页面刷新丢失状态)
|
|
@@ -443,9 +604,19 @@ export class CdpPage {
|
|
|
443
604
|
}
|
|
444
605
|
})();
|
|
445
606
|
if (!sameRoute) {
|
|
446
|
-
|
|
607
|
+
needNav = true;
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
// T012 P0-1:地区切换界面优先——同路由但城市不同 → 城市选择器 UI 操作;
|
|
611
|
+
// UI 失败必回退 URL 导航(导航可靠性优先,不引入新失败)
|
|
612
|
+
if (!(await this.trySwitchCityViaUi(url))) {
|
|
613
|
+
needNav = true;
|
|
614
|
+
}
|
|
447
615
|
}
|
|
448
616
|
}
|
|
617
|
+
if (needNav) {
|
|
618
|
+
await client.send('Page.navigate', { url });
|
|
619
|
+
}
|
|
449
620
|
// 等待页面 readyState=complete(不强制成功,verify/wait_selector 兜底)
|
|
450
621
|
const deadline = Date.now() + OPEN_READY_TIMEOUT_MS;
|
|
451
622
|
for (;;) {
|
|
@@ -454,9 +625,6 @@ export class CdpPage {
|
|
|
454
625
|
break;
|
|
455
626
|
await sleep(200);
|
|
456
627
|
}
|
|
457
|
-
this.targetId = t.id;
|
|
458
|
-
this.openedThisRun = !reuse;
|
|
459
|
-
this.human = new Human(new CdpInputSink(client), this.sim);
|
|
460
628
|
// #16-A 窄视口自动最大化:检测视口宽度,< MIN_VIEWPORT_WIDTH 则最大化窗口
|
|
461
629
|
// (真实窗口操作 Browser.setWindowBounds,非设备模拟——反检测安全,用户确认方案)
|
|
462
630
|
await this.ensureViewportWidth(client);
|
|
@@ -536,11 +704,23 @@ export class CdpPage {
|
|
|
536
704
|
const client = this.currentClient();
|
|
537
705
|
// 版本感知:#16-A 引入 clickableRects;持久 tab 可能残留旧版 __rpaDom(引擎升级后复用),
|
|
538
706
|
// 仅查存在性会漏注入 → clickableRects 调用失败。检查关键 API,缺失则重新注入。
|
|
539
|
-
// P3:describeHit
|
|
540
|
-
|
|
707
|
+
// P3:describeHit(语义锚点指纹采集)也纳入版本检查;T012 P1-2:scrollIntoViewRhythm;
|
|
708
|
+
// T014 P0-3:setRegionRules + _domVersion(region: 表达式/describeHit 区域标注;版本号防旧注入残留)。
|
|
709
|
+
const has = await client.evaluate('!!(window.__rpaDom && window.__rpaDom.clickableRects && window.__rpaDom.getBySels && window.__rpaDom.identifyRegion && window.__rpaDom.describeHit && window.__rpaDom.scrollIntoViewRhythm && window.__rpaDom.setRegionRules && window.__rpaDom._domVersion >= 3)').catch(() => false);
|
|
710
|
+
if (!has)
|
|
711
|
+
await client.evaluate(DOM_SOURCE);
|
|
712
|
+
// T014 P0-3:注入区域规则(flow 专属;每次 ensureDom 重注入保证与当前 flow 一致——复用标签跨流程场景)
|
|
713
|
+
if (this.regionRulesJson.length > 0) {
|
|
714
|
+
await client.evaluate(`window.__rpaDom.setRegionRules(${this.jsStr(this.regionRulesJson)})`).catch(() => undefined);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
/** 注入城市对话框 DOM 辅助(T012 P0-1;幂等:已有 cityLabelPos 则跳过) */
|
|
718
|
+
async ensureCityDom() {
|
|
719
|
+
const client = this.currentClient();
|
|
720
|
+
const has = await client.evaluate('!!(window.__rpaDom && window.__rpaDom.cityLabelPos && window.__rpaDom.cityGroupItem)').catch(() => false);
|
|
541
721
|
if (has)
|
|
542
722
|
return;
|
|
543
|
-
await client.evaluate(
|
|
723
|
+
await client.evaluate(CITY_SWITCH_SOURCE);
|
|
544
724
|
}
|
|
545
725
|
/** JS 字符串字面量(selector 插值安全) */
|
|
546
726
|
jsStr(s) {
|
|
@@ -579,13 +759,56 @@ export class CdpPage {
|
|
|
579
759
|
}
|
|
580
760
|
// ---- PageOps ----
|
|
581
761
|
async navigate(url) {
|
|
762
|
+
// T012 P0-1:轻量拟人——导航前随机预延迟(人类「决定」后出发;不引入新失败)
|
|
763
|
+
await sleep(300 + Math.floor(Math.random() * 900));
|
|
582
764
|
if (!this.targetId) {
|
|
583
765
|
await this.open(url);
|
|
584
766
|
return;
|
|
585
767
|
}
|
|
586
768
|
const client = this.currentClient();
|
|
769
|
+
// T012 P0-1:地区切换界面操作优先(zhipin 城市选择器);失败回退 URL 参数导航
|
|
770
|
+
if (await this.trySwitchCityViaUi(url)) {
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
587
773
|
await client.send('Page.navigate', { url });
|
|
588
774
|
}
|
|
775
|
+
/**
|
|
776
|
+
* T012 P0-1:地区切换「界面操作优先」——zhipin 城市选择器 UI 路径,URL 参数仅兜底。
|
|
777
|
+
* 返回 true = UI 切换成功(或已是目标城市);false = 调用方回退 URL 导航。
|
|
778
|
+
* 导航可靠性优先:任何一步失败/超时都返回 false,绝不因拟人引入新失败。
|
|
779
|
+
* 仅当目标 URL 为 zhipin 且带 city 参数时尝试;其余直接返回 false(行为不变)。
|
|
780
|
+
* 决策流在 city-switch.runCitySwitchUi(接口驱动,mock 可确定性测试——BATCH-D)。
|
|
781
|
+
*/
|
|
782
|
+
async trySwitchCityViaUi(url) {
|
|
783
|
+
const code = cityCodeOfUrl(url);
|
|
784
|
+
if (!code)
|
|
785
|
+
return false;
|
|
786
|
+
const name = ZHIPIN_CITY_NAMES[code];
|
|
787
|
+
if (!name)
|
|
788
|
+
return false;
|
|
789
|
+
const human = this.human;
|
|
790
|
+
if (!human)
|
|
791
|
+
return false;
|
|
792
|
+
try {
|
|
793
|
+
const client = this.currentClient();
|
|
794
|
+
// 真实点击需要标签页前台(CDP Input 只派发到聚焦页)
|
|
795
|
+
await client.send('Page.bringToFront', {}).catch(() => undefined);
|
|
796
|
+
await this.ensureCityDom();
|
|
797
|
+
const page = {
|
|
798
|
+
evaluate: (expr) => client.evaluate(expr),
|
|
799
|
+
clickAt: async (x, y) => {
|
|
800
|
+
await human.mouseClick(point(x, y));
|
|
801
|
+
},
|
|
802
|
+
currentUrl: async () => String(await client.evaluate('location.href').catch(() => '')),
|
|
803
|
+
sleep: async (ms) => sleep(ms),
|
|
804
|
+
log: (msg) => console.log(`[flow-rpa] ${msg}`),
|
|
805
|
+
};
|
|
806
|
+
return await runCitySwitchUi(code, name, page);
|
|
807
|
+
}
|
|
808
|
+
catch {
|
|
809
|
+
return false; // UI 路径失败静默回退 URL(导航可靠性优先)
|
|
810
|
+
}
|
|
811
|
+
}
|
|
589
812
|
async click(selector, index) {
|
|
590
813
|
const human = this.human;
|
|
591
814
|
if (!human)
|
|
@@ -599,8 +822,8 @@ export class CdpPage {
|
|
|
599
822
|
const beforeRoute = this.routeKey(beforeUrl);
|
|
600
823
|
// T010 P2-5:记录点击前 tab id 集合(新标签误点检测基线)
|
|
601
824
|
const beforeTabIds = new Set((await listTargets(this.port).catch(() => [])).map((t) => t.id));
|
|
602
|
-
// 滚动到目标可见(index 指定的第 i
|
|
603
|
-
await client.evaluate(`window.__rpaDom.
|
|
825
|
+
// 滚动到目标可见(index 指定的第 i 个)——T012 P1-2:轻量拟人滚动节奏(分段渐进)
|
|
826
|
+
await client.evaluate(`window.__rpaDom.scrollIntoViewRhythm(${this.jsStr(selector)}, ${index})`);
|
|
604
827
|
// 拟人点击:index 语义(D-L2)——点第 i 个元素;越界 = 列表耗尽 → category=index_out_of_range
|
|
605
828
|
let clicked = 0;
|
|
606
829
|
try {
|
|
@@ -698,20 +921,15 @@ export class CdpPage {
|
|
|
698
921
|
await this.ensureDom();
|
|
699
922
|
await this.checkEnv();
|
|
700
923
|
const client = this.currentClient();
|
|
701
|
-
//
|
|
702
|
-
await client.evaluate(`window.__rpaDom.
|
|
924
|
+
// 滚动到可见(T012 P1-2:轻量拟人滚动节奏)+ 聚焦前小延迟
|
|
925
|
+
await client.evaluate(`window.__rpaDom.scrollIntoViewRhythm(${this.jsStr(selector)}, 0)`);
|
|
926
|
+
await sleep(60 + Math.floor(Math.random() * 140));
|
|
927
|
+
await client.evaluate(`window.__rpaDom.focusInput(${this.jsStr(selector)}, 0)`);
|
|
703
928
|
await human.keyboardInput(value, {});
|
|
704
929
|
// T007 P0-1(重开)真因修复:insertText(中文路径)直接改 DOM value 但**不触发 input 事件**——
|
|
705
930
|
// React 受控组件靠 onChange 同步 state,事件缺失 → React state 仍空 → 搜索提交空值(搜"产品经理"得默认结果)。
|
|
706
931
|
// 补派发一次 input 事件让 React 读到最新 DOM value(全文),与 DOM value 断言一起构成真实输入验证。
|
|
707
|
-
await client
|
|
708
|
-
.evaluate(`(() => {
|
|
709
|
-
const el = document.activeElement;
|
|
710
|
-
if (el && typeof el.dispatchEvent === 'function') {
|
|
711
|
-
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
712
|
-
}
|
|
713
|
-
})()`)
|
|
714
|
-
.catch(() => undefined);
|
|
932
|
+
await client.evaluate(INPUT_EVENT_DISPATCH).catch(() => undefined);
|
|
715
933
|
// T006 P0-1:输入后读 input.value 真实断言(防静默失败——中文变问号/输入被拒时立即报错而非假成功)
|
|
716
934
|
try {
|
|
717
935
|
const actual = String(await client.evaluate(`(() => {
|
|
@@ -875,13 +1093,9 @@ export class CdpPage {
|
|
|
875
1093
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
876
1094
|
/**
|
|
877
1095
|
* T005 P0-2:路由命中登录页特征(host+path;平台无关特征词:/login /passport /captcha /web/user/ 等)。
|
|
878
|
-
*
|
|
1096
|
+
* T014 P0-2:定义上移至 region.ts 统一实现(登录区双重特征),此处再导出保兼容(测试/调用方不变)。
|
|
879
1097
|
*/
|
|
880
|
-
export
|
|
881
|
-
const slash = route.indexOf('/');
|
|
882
|
-
const path = slash >= 0 ? route.slice(slash) : route;
|
|
883
|
-
return /\/login|\/passport|\/captcha|\/web\/user\//i.test(path);
|
|
884
|
-
}
|
|
1098
|
+
export { isLoginRoute } from '../region.js';
|
|
885
1099
|
/**
|
|
886
1100
|
* T005 P1-3:报错文案选择器消毒——selector 含占位符形态({...} 未替换变量 / <...> 掩码残留)时不泄漏
|
|
887
1101
|
* 原文,显示友好描述(避免用户看到「点击 <keyword> 后页面跳转」这类占位符泄漏)。
|