mooncat-browser 0.1.0
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/README.md +213 -0
- package/browser-op/backend/browserd.cjs +1004 -0
- package/browser-op/backend/rpc-client.cjs +64 -0
- package/browser-op/backend/state.cjs +51 -0
- package/browser-op/cdp/capture-inject.js +426 -0
- package/browser-op/cdp/capture-inject.ts +426 -0
- package/browser-op/cdp/capture-service.cjs +172 -0
- package/browser-op/cdp/chrome-launcher.cjs +370 -0
- package/browser-op/cdp/chrome-path.cjs +57 -0
- package/browser-op/cdp/state.cjs +89 -0
- package/browser-op/extension/extension-detect.cjs +228 -0
- package/browser-op/extension/server.cjs +197 -0
- package/browser-op/extension/service.cjs +228 -0
- package/browser-op/extension/state.cjs +78 -0
- package/browser-op/index.cjs +389 -0
- package/browser-op/package.json +17 -0
- package/browser-op/py/behavior.py +138 -0
- package/browser-op/py/browser.py +340 -0
- package/browser-op/py/captcha.py +115 -0
- package/browser-op/py/crawler.py +125 -0
- package/browser-op/py/examples/01_open_and_probe.py +48 -0
- package/browser-op/py/examples/02_reuse_and_probe.py +66 -0
- package/browser-op/py/examples/03_interact.py +66 -0
- package/browser-op/py/find.py +150 -0
- package/browser-op/py/honeypot.py +73 -0
- package/browser-op/py/humanize.py +392 -0
- package/browser-op/py/image.py +186 -0
- package/browser-op/py/interact.py +193 -0
- package/browser-op/py/markdown.py +38 -0
- package/browser-op/py/pyproject.toml +32 -0
- package/browser-op/py/ready.py +208 -0
- package/browser-op/py/scroll.py +180 -0
- package/browser-op/py/upload.py +103 -0
- package/browser-op/py/visual_target.py +47 -0
- package/browser-op/py/visualize.py +91 -0
- package/browser-op/state.cjs +63 -0
- package/browser-op/web/behavior.js +153 -0
- package/browser-op/web/browser.js +231 -0
- package/browser-op/web/captcha.js +85 -0
- package/browser-op/web/crawler.js +109 -0
- package/browser-op/web/find.js +147 -0
- package/browser-op/web/honeypot.js +68 -0
- package/browser-op/web/humanize.js +522 -0
- package/browser-op/web/image.js +177 -0
- package/browser-op/web/interact.js +169 -0
- package/browser-op/web/markdown.js +80 -0
- package/browser-op/web/ready.js +295 -0
- package/browser-op/web/scroll.js +167 -0
- package/browser-op/web/upload.js +116 -0
- package/browser-op/web/visual-runtime.inject.cjs +6 -0
- package/browser-op/webplater/.env.example +7 -0
- package/browser-op/webplater/ARCHITECTURE.md +102 -0
- package/browser-op/webplater/dist/chrome-mv3/assets/popup-BUZEUmsx.css +1 -0
- package/browser-op/webplater/dist/chrome-mv3/background.js +2 -0
- package/browser-op/webplater/dist/chrome-mv3/capture.js +310 -0
- package/browser-op/webplater/dist/chrome-mv3/chunks/_virtual_wxt-html-plugins-DPbbfBKe.js +1 -0
- package/browser-op/webplater/dist/chrome-mv3/chunks/offscreen-CFXYw9Mo.js +1 -0
- package/browser-op/webplater/dist/chrome-mv3/chunks/popup-C-lpxZZO.js +1 -0
- package/browser-op/webplater/dist/chrome-mv3/content-scripts/content.js +7 -0
- package/browser-op/webplater/dist/chrome-mv3/manifest.json +1 -0
- package/browser-op/webplater/dist/chrome-mv3/offscreen.html +16 -0
- package/browser-op/webplater/dist/chrome-mv3/popup.html +31 -0
- package/browser-op/webplater/entrypoints/background.ts +938 -0
- package/browser-op/webplater/entrypoints/content.ts +1150 -0
- package/browser-op/webplater/entrypoints/offscreen/index.html +15 -0
- package/browser-op/webplater/entrypoints/offscreen/main.ts +161 -0
- package/browser-op/webplater/entrypoints/popup/index.html +29 -0
- package/browser-op/webplater/entrypoints/popup/main.ts +61 -0
- package/browser-op/webplater/entrypoints/popup/style.css +100 -0
- package/browser-op/webplater/lib/snapshot.ts +352 -0
- package/browser-op/webplater/package.json +29 -0
- package/browser-op/webplater/pnpm-lock.yaml +3411 -0
- package/browser-op/webplater/public/capture.js +310 -0
- package/browser-op/webplater/scripts/publish-extension.mjs +176 -0
- package/browser-op/webplater/tsconfig.json +19 -0
- package/browser-op/webplater/wxt.config.ts +34 -0
- package/dist/actions.md +102 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +278 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +94 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +277 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +61 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +119 -0
- package/dist/config.js.map +1 -0
- package/dist/protocol.d.ts +195 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +11 -0
- package/dist/protocol.js.map +1 -0
- package/dist/server.d.ts +66 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +259 -0
- package/dist/server.js.map +1 -0
- package/package.json +78 -0
- package/schemas/browser.clearCookies.schema.json +13 -0
- package/schemas/browser.close.schema.json +9 -0
- package/schemas/browser.getCookies.schema.json +13 -0
- package/schemas/browser.getDownload.schema.json +15 -0
- package/schemas/browser.health.schema.json +9 -0
- package/schemas/browser.listDownloads.schema.json +16 -0
- package/schemas/browser.listTabs.schema.json +9 -0
- package/schemas/browser.newTab.schema.json +15 -0
- package/schemas/browser.open.schema.json +15 -0
- package/schemas/browser.operate.schema.json +15 -0
- package/schemas/browser.reuseTab.schema.json +15 -0
- package/schemas/browser.setCookies.schema.json +15 -0
- package/schemas/browser.waitFor.schema.json +15 -0
- package/schemas/browser.waitForDownload.schema.json +15 -0
- package/skills/browser/SKILL.md +110 -0
- package/skills/browser/references/collect.md +163 -0
- package/skills/browser/references/high-risk.md +161 -0
- package/skills/browser/references/operate-actions.md +92 -0
- package/skills/browser/references/probing.md +302 -0
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
// -*- coding: utf-8 -*-
|
|
2
|
+
//
|
|
3
|
+
// web/humanize — 拟人化行为注入(路由无关,全走 operate)
|
|
4
|
+
//
|
|
5
|
+
// 设计:随机触发,不是每次 action 都注入。每个 operate 有概率走 humanize 序列,
|
|
6
|
+
// 概率按 riskLevel 调。避免"每次必注入"本身变成新的固定模式。
|
|
7
|
+
//
|
|
8
|
+
// 嵌入方式:operate 顶部作为隐式中间件,调用方无感:
|
|
9
|
+
// if (ph.humanize && 触发) await humanize.beforeAction(ph, action, params)
|
|
10
|
+
// 执行 action
|
|
11
|
+
// if (ph.humanize && 触发) await humanize.afterAction(ph, action)
|
|
12
|
+
//
|
|
13
|
+
// beforeAction 末尾对有 selector 的 action 必归位(reposition):
|
|
14
|
+
// 平滑滚回目标 + 贝塞尔移到目标中心 + focus。
|
|
15
|
+
// 保证目标 action 命中(不被注入的行为搞砸)。
|
|
16
|
+
//
|
|
17
|
+
// 依赖:./browser(operate)
|
|
18
|
+
|
|
19
|
+
'use strict'
|
|
20
|
+
|
|
21
|
+
// 注:不直接 require('./browser'),避免循环依赖。
|
|
22
|
+
// operate 函数由 createHumanizer 调用时从外部注入,或用懒加载获取。
|
|
23
|
+
let _operate = null
|
|
24
|
+
function getOperate () {
|
|
25
|
+
if (_operate) return _operate
|
|
26
|
+
// 懒加载:第一次调用原子行为时才 require,此时 browser.js 已加载完
|
|
27
|
+
_operate = require('./browser').operate
|
|
28
|
+
return _operate
|
|
29
|
+
}
|
|
30
|
+
let _scroll = null
|
|
31
|
+
function getScroll () {
|
|
32
|
+
// scroll.js 只依赖 browser, 不依赖 humanize, 安全 require
|
|
33
|
+
if (_scroll) return _scroll
|
|
34
|
+
_scroll = require('./scroll')
|
|
35
|
+
return _scroll
|
|
36
|
+
}
|
|
37
|
+
function operate (pageHandle, params) {
|
|
38
|
+
return getOperate()(pageHandle, params)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─── 风险等级配置(影响延迟倍率、序列长度、触发概率、滚动频率)───
|
|
42
|
+
const RISK_LEVELS = {
|
|
43
|
+
low: {
|
|
44
|
+
delayScale: 0.6,
|
|
45
|
+
sequenceLen: [1, 3], // 序列长度范围
|
|
46
|
+
triggerProb: 0.35, // 每个 operate 触发 humanize 的概率
|
|
47
|
+
scrollProb: 0.3, // 序列里含滚动的概率
|
|
48
|
+
wanderProb: 0.25
|
|
49
|
+
},
|
|
50
|
+
medium: {
|
|
51
|
+
delayScale: 1.0,
|
|
52
|
+
sequenceLen: [2, 4],
|
|
53
|
+
triggerProb: 0.55,
|
|
54
|
+
scrollProb: 0.5,
|
|
55
|
+
wanderProb: 0.4
|
|
56
|
+
},
|
|
57
|
+
high: {
|
|
58
|
+
delayScale: 1.8,
|
|
59
|
+
sequenceLen: [3, 6],
|
|
60
|
+
triggerProb: 0.75,
|
|
61
|
+
scrollProb: 0.7,
|
|
62
|
+
wanderProb: 0.6
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 高风控域名(open/newTab 检测到自动切 high)
|
|
67
|
+
const HIGH_RISK_DOMAINS = [
|
|
68
|
+
'taobao.com', 'tmall.com', 'jd.com', '1688.com', 'pinduoduo.com',
|
|
69
|
+
'yangkeduo.com', 'xiaohongshu.com', 'xhslink.com', 'weidian.com',
|
|
70
|
+
'douyin.com', 'kuaishou.com', 'bilibili.com', 'weibo.com',
|
|
71
|
+
'alipay.com', 'bank', 'icbc', 'cmbchina', '12306.cn'
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
// ─── 工具:高斯分布随机数 ───
|
|
75
|
+
function gaussian (mean, std) {
|
|
76
|
+
// Box-Muller
|
|
77
|
+
let u = 0; let v = 0
|
|
78
|
+
while (u === 0) u = Math.random()
|
|
79
|
+
while (v === 0) v = Math.random()
|
|
80
|
+
return mean + std * Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, Math.max(0, ms)))
|
|
84
|
+
const randomBetween = (min, max) => min + Math.random() * (max - min)
|
|
85
|
+
const randomInt = (min, max) => Math.floor(randomBetween(min, max + 1))
|
|
86
|
+
const pick = (arr) => arr[Math.floor(Math.random() * arr.length)]
|
|
87
|
+
|
|
88
|
+
// ─── 贝塞尔曲线点(复用 behavior.js 的逻辑,这里独立实现避免循环依赖)───
|
|
89
|
+
function bezier (p0, p1, p2, p3, t) {
|
|
90
|
+
const u = 1 - t
|
|
91
|
+
return u * u * u * p0 + 3 * u * u * t * p1 + 3 * u * t * t * p2 + t * t * t * p3
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function bezierTrajectory (from, to, steps) {
|
|
95
|
+
const pts = []
|
|
96
|
+
const mx = (from.x + to.x) / 2 + (Math.random() - 0.5) * Math.abs(to.x - from.x) * 0.6
|
|
97
|
+
const my = (from.y + to.y) / 2 + (Math.random() - 0.5) * Math.abs(to.y - from.y) * 0.6
|
|
98
|
+
const c1 = { x: from.x + (mx - from.x) * 0.3, y: from.y + (my - from.y) * 0.3 }
|
|
99
|
+
const c2 = { x: to.x + (mx - to.x) * 0.3, y: to.y + (my - to.y) * 0.3 }
|
|
100
|
+
for (let i = 0; i <= steps; i++) {
|
|
101
|
+
const t = i / steps
|
|
102
|
+
pts.push({
|
|
103
|
+
x: bezier(from.x, c1.x, c2.x, to.x, t) + (Math.random() - 0.5) * 2,
|
|
104
|
+
y: bezier(from.y, c1.y, c2.y, to.y, t) + (Math.random() - 0.5) * 2
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
return pts
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 模块级:每个 pageHandle 记录上次鼠标位置(贝塞尔轨迹的起点)
|
|
111
|
+
const _lastMousePos = new WeakMap()
|
|
112
|
+
function getLastMouse (pageHandle) {
|
|
113
|
+
return _lastMousePos.get(pageHandle) || { x: 200 + Math.random() * 400, y: 200 + Math.random() * 300 }
|
|
114
|
+
}
|
|
115
|
+
function setLastMouse (pageHandle, pos) {
|
|
116
|
+
_lastMousePos.set(pageHandle, pos)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ─── 原子行为(每个独立、可组合、可重复)───
|
|
120
|
+
|
|
121
|
+
// 停顿"阅读"
|
|
122
|
+
async function read (pageHandle, scale) {
|
|
123
|
+
await sleep(gaussian(2500, 1200) * scale)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 短暂走神
|
|
127
|
+
async function idle (pageHandle, scale) {
|
|
128
|
+
await sleep(gaussian(1200, 600) * scale)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 平滑滚动(随机方向、随机距离)
|
|
132
|
+
async function scrollSmooth (pageHandle, scale, direction) {
|
|
133
|
+
const dir = direction || (Math.random() < 0.8 ? 'down' : 'up')
|
|
134
|
+
const pixels = randomInt(120, 480)
|
|
135
|
+
const dy = dir === 'down' ? pixels : -pixels
|
|
136
|
+
// 调用封装的连续滚动 (scrollBy 内部分步不跳变)
|
|
137
|
+
await getScroll().scrollBy(pageHandle, dy).catch(() => {})
|
|
138
|
+
await sleep(gaussian(500, 200) * scale)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 滚到页面某比例位置 (调封装连续滚动, 不跳变)
|
|
142
|
+
async function scrollToRatio (pageHandle, scale) {
|
|
143
|
+
const ratio = pick([0.25, 0.4, 0.55, 0.7, 0.85])
|
|
144
|
+
// 先算出目标 Y, 再用封装的 scrollToY 连续滚
|
|
145
|
+
const targetY = await operate(pageHandle, { _skipHumanize: true,
|
|
146
|
+
action: 'evaluate',
|
|
147
|
+
source: '(r) => Math.max(0, (document.body.scrollHeight - window.innerHeight) * r)',
|
|
148
|
+
args: ratio
|
|
149
|
+
}).catch(() => 0)
|
|
150
|
+
await getScroll().scrollToY(pageHandle, targetY || 0).catch(() => {})
|
|
151
|
+
await sleep(gaussian(800, 300) * scale)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 鼠标贝塞尔漫游到随机点(不和任何元素交互)
|
|
155
|
+
// 鼠标贝塞尔漫游: 随机坐标 → elementFromPoint 找到元素 → 高亮 → 移到元素中心
|
|
156
|
+
// 这是"随机选一个元素并围绕它操作"的拟人行为
|
|
157
|
+
async function mouseWander (pageHandle, scale) {
|
|
158
|
+
const from = getLastMouse(pageHandle)
|
|
159
|
+
// 1. 随机坐标 → 找到该坐标下的元素 (模拟人随意看某处)
|
|
160
|
+
const randX = randomBetween(100, 1100)
|
|
161
|
+
const randY = randomBetween(100, 700)
|
|
162
|
+
const elInfo = await operate(pageHandle, { _skipHumanize: true,
|
|
163
|
+
action: 'evaluate',
|
|
164
|
+
source: '(x, y) => { const el = document.elementFromPoint(x, y); if (!el) return null; const r = el.getBoundingClientRect(); if (window.__beeVR) window.__beeVR.visualMark(el); return { x: Math.round(r.x + r.width/2), y: Math.round(r.y + r.height/2), tag: el.tagName, text: (el.innerText||"").trim().slice(0,20) } }',
|
|
165
|
+
args: [randX, randY]
|
|
166
|
+
}).catch(() => null)
|
|
167
|
+
const target = (elInfo && (elInfo.value || elInfo)) || { x: randX, y: randY }
|
|
168
|
+
// (高亮已在上面同一 evaluate 内对真实 el 调 visualMark, 不再回 Node 侧传坐标)
|
|
169
|
+
// 3. 贝塞尔移到元素中心
|
|
170
|
+
const to = { x: target.x || randX, y: target.y || randY }
|
|
171
|
+
const steps = randomInt(12, 22)
|
|
172
|
+
const pts = bezierTrajectory(from, to, steps)
|
|
173
|
+
for (const pt of pts) {
|
|
174
|
+
await operate(pageHandle, { _skipHumanize: true, action: 'mouseMove', x: Math.round(pt.x), y: Math.round(pt.y) }).catch(() => {})
|
|
175
|
+
await sleep(randomBetween(8, 22))
|
|
176
|
+
}
|
|
177
|
+
setLastMouse(pageHandle, to)
|
|
178
|
+
await sleep(gaussian(300, 150) * scale)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 悬停某个可交互元素(不点击)—— 随机选页面上一个 button/link/input
|
|
182
|
+
async function hoverRandom (pageHandle, scale) {
|
|
183
|
+
// 用 evaluate 找一个随机可交互元素的中心点
|
|
184
|
+
const pos = await operate(pageHandle, { _skipHumanize: true,
|
|
185
|
+
action: 'evaluate',
|
|
186
|
+
source: '() => { const els = document.querySelectorAll("a, button, input, [role=button]"); const arr = [...els].filter(e => { const r = e.getBoundingClientRect(); return r.width > 0 && r.height > 0 && r.top > 0 && r.top < window.innerHeight - 50 }); if (!arr.length) return null; const el = arr[Math.floor(Math.random()*arr.length)]; const r = el.getBoundingClientRect(); if (window.__beeVR) window.__beeVR.visualMark(el); return { x: Math.round(r.x + r.width/2), y: Math.round(r.y + r.height/2) } }'
|
|
187
|
+
}).catch(() => null)
|
|
188
|
+
const target = (pos && (pos.value || pos)) || null
|
|
189
|
+
if (!target || target.x == null) return
|
|
190
|
+
// (高亮已在上面同一 evaluate 内对真实 el 调 visualMark)
|
|
191
|
+
const from = getLastMouse(pageHandle)
|
|
192
|
+
const steps = randomInt(10, 18)
|
|
193
|
+
const pts = bezierTrajectory(from, target, steps)
|
|
194
|
+
for (const pt of pts) {
|
|
195
|
+
await operate(pageHandle, { _skipHumanize: true, action: 'mouseMove', x: Math.round(pt.x), y: Math.round(pt.y) }).catch(() => {})
|
|
196
|
+
await sleep(randomBetween(10, 25))
|
|
197
|
+
}
|
|
198
|
+
setLastMouse(pageHandle, target)
|
|
199
|
+
await sleep(gaussian(400, 200) * scale)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 键盘翻页: 原生 PageDown/Space 不可控 (页面可拦截、距离不定),
|
|
203
|
+
// 改用 ScrollController.scrollBy 模拟 (可控连续滚动)
|
|
204
|
+
async function scrollKey (pageHandle, scale) {
|
|
205
|
+
const delta = randomInt(300, 600) * (Math.random() < 0.85 ? 1 : -1)
|
|
206
|
+
await getScroll().scrollBy(pageHandle, delta).catch(() => {})
|
|
207
|
+
await sleep(gaussian(600, 250) * scale)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 原地鼠标小抖动
|
|
211
|
+
async function mouseJitter (pageHandle, scale) {
|
|
212
|
+
const base = getLastMouse(pageHandle)
|
|
213
|
+
for (let i = 0; i < randomInt(2, 5); i++) {
|
|
214
|
+
await operate(pageHandle, { _skipHumanize: true,
|
|
215
|
+
action: 'mouseMove',
|
|
216
|
+
x: Math.round(base.x + (Math.random() - 0.5) * 20),
|
|
217
|
+
y: Math.round(base.y + (Math.random() - 0.5) * 20)
|
|
218
|
+
}).catch(() => {})
|
|
219
|
+
await sleep(randomBetween(30, 80))
|
|
220
|
+
}
|
|
221
|
+
await sleep(gaussian(200, 100) * scale)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Tab 键跳几下焦点
|
|
225
|
+
async function tabNav (pageHandle, scale) {
|
|
226
|
+
const count = randomInt(1, 3)
|
|
227
|
+
for (let i = 0; i < count; i++) {
|
|
228
|
+
await operate(pageHandle, { _skipHumanize: true, action: 'press', selector: 'body', key: 'Tab' }).catch(() => {})
|
|
229
|
+
await sleep(gaussian(250, 100) * scale)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// noop(什么都不做,制造"空洞")
|
|
234
|
+
async function noop (pageHandle, scale) {
|
|
235
|
+
await sleep(randomBetween(100, 400))
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ─── 原子行为池(按类别,供随机组合器抽取)───
|
|
239
|
+
const ATOMS = {
|
|
240
|
+
// 注意力类
|
|
241
|
+
attention: [
|
|
242
|
+
{ fn: read, weight: 3 },
|
|
243
|
+
{ fn: idle, weight: 1 }
|
|
244
|
+
],
|
|
245
|
+
// 滚动类
|
|
246
|
+
scroll: [
|
|
247
|
+
{ fn: scrollSmooth, weight: 4 },
|
|
248
|
+
{ fn: scrollToRatio, weight: 2 },
|
|
249
|
+
{ fn: scrollKey, weight: 1 }
|
|
250
|
+
],
|
|
251
|
+
// 鼠标类
|
|
252
|
+
mouse: [
|
|
253
|
+
{ fn: mouseWander, weight: 3 },
|
|
254
|
+
{ fn: hoverRandom, weight: 2 },
|
|
255
|
+
{ fn: mouseJitter, weight: 1 }
|
|
256
|
+
],
|
|
257
|
+
// 键盘/导航类
|
|
258
|
+
nav: [
|
|
259
|
+
{ fn: tabNav, weight: 1 }
|
|
260
|
+
],
|
|
261
|
+
// 空洞
|
|
262
|
+
meta: [
|
|
263
|
+
{ fn: noop, weight: 2 }
|
|
264
|
+
]
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 按权重从池里抽一个
|
|
268
|
+
function weightedPick (pool) {
|
|
269
|
+
const total = pool.reduce((s, a) => s + a.weight, 0)
|
|
270
|
+
let r = Math.random() * total
|
|
271
|
+
for (const item of pool) {
|
|
272
|
+
r -= item.weight
|
|
273
|
+
if (r <= 0) return item.fn
|
|
274
|
+
}
|
|
275
|
+
return pool[0].fn
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ─── 随机序列生成器(按约束规则)───
|
|
279
|
+
// 约束:导航后必有 read+scroll;交互前可选瞄准;偶发分心;总长度有界
|
|
280
|
+
function buildSequence (phase, riskCfg) {
|
|
281
|
+
const [minLen, maxLen] = riskCfg.sequenceLen
|
|
282
|
+
const targetLen = randomInt(minLen, maxLen)
|
|
283
|
+
const seq = []
|
|
284
|
+
|
|
285
|
+
if (phase === 'navigate') {
|
|
286
|
+
// 导航后适应序列:必 read + 多次 scroll + 偶发 mouse
|
|
287
|
+
seq.push(read)
|
|
288
|
+
const scrollCount = randomInt(1, 3)
|
|
289
|
+
for (let i = 0; i < scrollCount; i++) seq.push(weightedPick(ATOMS.scroll))
|
|
290
|
+
if (Math.random() < riskCfg.wanderProb) seq.push(weightedPick(ATOMS.mouse))
|
|
291
|
+
// 可能再来一段
|
|
292
|
+
while (seq.length < targetLen && Math.random() < 0.5) {
|
|
293
|
+
seq.push(weightedPick([...ATOMS.attention, ...ATOMS.scroll, ...ATOMS.mouse, ...ATOMS.meta]))
|
|
294
|
+
}
|
|
295
|
+
} else if (phase === 'interact') {
|
|
296
|
+
// 交互前瞄准序列:0-2 个(mouse/hover/idle/noop)
|
|
297
|
+
const aimLen = randomInt(0, Math.min(2, targetLen))
|
|
298
|
+
for (let i = 0; i < aimLen; i++) {
|
|
299
|
+
const pool = Math.random() < 0.6 ? ATOMS.mouse : [...ATOMS.attention, ...ATOMS.meta]
|
|
300
|
+
seq.push(weightedPick(pool))
|
|
301
|
+
}
|
|
302
|
+
} else if (phase === 'after-interact') {
|
|
303
|
+
// 交互后确认序列:0-2 个(read 短 / scroll / noop)
|
|
304
|
+
const len = randomInt(0, Math.min(2, targetLen))
|
|
305
|
+
for (let i = 0; i < len; i++) {
|
|
306
|
+
const pool = [...ATOMS.attention, ...ATOMS.scroll, ...ATOMS.meta]
|
|
307
|
+
seq.push(weightedPick(pool))
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
// 默认:全池随机
|
|
311
|
+
const all = [...ATOMS.attention, ...ATOMS.scroll, ...ATOMS.mouse, ...ATOMS.meta]
|
|
312
|
+
for (let i = 0; i < targetLen; i++) seq.push(weightedPick(all))
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// 偶发分心:10% 概率在随机位置插入 idle
|
|
316
|
+
if (seq.length > 0 && Math.random() < 0.1) {
|
|
317
|
+
seq.splice(randomInt(0, seq.length), 0, idle)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return seq
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ─── 归位:保证目标 action 命中 ───
|
|
324
|
+
async function reposition (pageHandle, selector, scale) {
|
|
325
|
+
if (!selector) return
|
|
326
|
+
// 1. 连续滚到目标元素 (调封装的 scrollToElement, 不跳变)
|
|
327
|
+
await getScroll().scrollToElement(pageHandle, selector, { block: 'center' }).catch(() => {})
|
|
328
|
+
await sleep(gaussian(500, 200) * scale)
|
|
329
|
+
|
|
330
|
+
// 2. 拿目标 bbox,贝塞尔移到中心(带随机偏移)
|
|
331
|
+
const boxR = await operate(pageHandle, { _skipHumanize: true, action: 'boundingBox', selector }).catch(() => null)
|
|
332
|
+
const box = boxR && (boxR.value || boxR)
|
|
333
|
+
if (box && box.width != null) {
|
|
334
|
+
const cx = box.x + box.width * (0.4 + Math.random() * 0.2)
|
|
335
|
+
const cy = box.y + box.height * (0.4 + Math.random() * 0.2)
|
|
336
|
+
const from = getLastMouse(pageHandle)
|
|
337
|
+
const steps = randomInt(8, 16)
|
|
338
|
+
const pts = bezierTrajectory(from, { x: cx, y: cy }, steps)
|
|
339
|
+
for (const pt of pts) {
|
|
340
|
+
await operate(pageHandle, { _skipHumanize: true, action: 'mouseMove', x: Math.round(pt.x), y: Math.round(pt.y) }).catch(() => {})
|
|
341
|
+
await sleep(randomBetween(10, 25))
|
|
342
|
+
}
|
|
343
|
+
setLastMouse(pageHandle, { x: cx, y: cy })
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// 3. focus 目标
|
|
347
|
+
await operate(pageHandle, { _skipHumanize: true, action: 'focus', selector }).catch(() => {})
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ─── 触发判定(随机)───
|
|
351
|
+
function shouldTrigger (riskCfg) {
|
|
352
|
+
return Math.random() < riskCfg.triggerProb
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ─── 主接口:beforeAction / afterAction ───
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* 在 operate 执行 action 之前调用(随机触发)。
|
|
359
|
+
* @param {object} pageHandle
|
|
360
|
+
* @param {string} action
|
|
361
|
+
* @param {object} params operate 的原始 params(用于拿 selector 归位)
|
|
362
|
+
* @param {object} riskCfg RISK_LEVELS[level]
|
|
363
|
+
*/
|
|
364
|
+
async function beforeAction (pageHandle, action, params, riskCfg) {
|
|
365
|
+
const scale = riskCfg.delayScale
|
|
366
|
+
const isNavigate = ['goto', 'goBack', 'goForward', 'reload'].includes(action)
|
|
367
|
+
const isInteract = ['click', 'fill', 'type', 'press', 'hover', 'dblclick', 'check', 'uncheck', 'selectOption', 'dragTo', 'setInputFiles'].includes(action)
|
|
368
|
+
|
|
369
|
+
// 导航类:beforeAction 不注入(导航还没发生),归位交给 afterAction 的适应序列
|
|
370
|
+
if (isNavigate) return
|
|
371
|
+
|
|
372
|
+
// 触发判定
|
|
373
|
+
if (!shouldTrigger(riskCfg)) {
|
|
374
|
+
// 即使不触发完整序列,交互类也要保证最小归位(鼠标到目标)
|
|
375
|
+
if (isInteract && params.selector) {
|
|
376
|
+
await reposition(pageHandle, params.selector, scale)
|
|
377
|
+
}
|
|
378
|
+
return
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// 注入序列
|
|
382
|
+
const seq = buildSequence(isInteract ? 'interact' : 'default', riskCfg)
|
|
383
|
+
for (const atom of seq) {
|
|
384
|
+
await atom(pageHandle, scale)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// 交互类必归位(序列末尾)
|
|
388
|
+
if (isInteract && params.selector) {
|
|
389
|
+
await reposition(pageHandle, params.selector, scale)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* 在 operate 执行 action 之后调用(随机触发)。
|
|
395
|
+
* @param {object} pageHandle
|
|
396
|
+
* @param {string} action
|
|
397
|
+
* @param {object} riskCfg
|
|
398
|
+
*/
|
|
399
|
+
async function afterAction (pageHandle, action, riskCfg) {
|
|
400
|
+
const scale = riskCfg.delayScale
|
|
401
|
+
const isNavigate = ['goto', 'goBack', 'goForward', 'reload'].includes(action)
|
|
402
|
+
|
|
403
|
+
// 导航类:必注入完整适应序列(人到新页面先打量)
|
|
404
|
+
if (isNavigate) {
|
|
405
|
+
const seq = buildSequence('navigate', riskCfg)
|
|
406
|
+
for (const atom of seq) {
|
|
407
|
+
await atom(pageHandle, scale)
|
|
408
|
+
}
|
|
409
|
+
return
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// 其余:随机触发
|
|
413
|
+
if (!shouldTrigger(riskCfg)) return
|
|
414
|
+
const seq = buildSequence('after-interact', riskCfg)
|
|
415
|
+
for (const atom of seq) {
|
|
416
|
+
await atom(pageHandle, scale)
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ─── 工厂:createHumanizer ───
|
|
421
|
+
/**
|
|
422
|
+
* 创建 humanizer 实例。
|
|
423
|
+
* @param {object} options
|
|
424
|
+
* @param {string} [options.riskLevel='medium'] 'low'|'medium'|'high'
|
|
425
|
+
* @returns {{beforeAction, afterAction, riskLevel, riskCfg}}
|
|
426
|
+
*/
|
|
427
|
+
function createHumanizer (options = {}) {
|
|
428
|
+
const level = options.riskLevel || 'medium'
|
|
429
|
+
const riskCfg = RISK_LEVELS[level] || RISK_LEVELS.medium
|
|
430
|
+
// 未知 level 回退到 medium(riskCfg 和 riskLevel 都呑予 medium,不泄漏原值)
|
|
431
|
+
const safeLevel = RISK_LEVELS[level] ? level : 'medium'
|
|
432
|
+
return {
|
|
433
|
+
riskLevel: safeLevel,
|
|
434
|
+
riskCfg,
|
|
435
|
+
beforeAction: (ph, action, params) => beforeAction(ph, action, params, riskCfg),
|
|
436
|
+
afterAction: (ph, action) => afterAction(ph, action, riskCfg)
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ─── 自由调用的拟人接口 (采集/evaluate 场景) ───
|
|
441
|
+
/**
|
|
442
|
+
* 随机拟人行为 + 归位到目标。一个调用搞定采集前的拟人。
|
|
443
|
+
*
|
|
444
|
+
* 自由调用场景 (不依赖 operate 封装): 调用方明确知道目标 (要点击/采集的元素或位置),
|
|
445
|
+
* 传给 humanAct, 它随机注入拟人行为 (滚动/鼠标漫游/阅读停顿) 后, 归位到目标。
|
|
446
|
+
*
|
|
447
|
+
* @param {pageHandle} pageHandle
|
|
448
|
+
* @param {object} [opts]
|
|
449
|
+
* @param {string} [opts.target] 归位目标 (CSS selector), 拟人后滚+移到它
|
|
450
|
+
* @param {{x:number,y:number}} [opts.targetXY] 归位目标 (坐标), 与 target 二选一
|
|
451
|
+
* @param {string} [opts.phase='default'] 'default'|'navigate'|'interact'|'after' 组合策略
|
|
452
|
+
* - default/navigate/interact: 拟人后归位到目标 (动作前用)
|
|
453
|
+
* - after: 拟人后随机收尾, 不归位 (动作已执行完, 收尾拟人)
|
|
454
|
+
* @param {string} [opts.riskLevel='medium'] 风控等级
|
|
455
|
+
* @param {boolean} [opts.reposition=true] 拟人后归位到目标 (保证后续动作命中)
|
|
456
|
+
* @returns {Promise<void>}
|
|
457
|
+
*/
|
|
458
|
+
async function humanAct (pageHandle, opts = {}) {
|
|
459
|
+
const riskCfg = RISK_LEVELS[opts.riskLevel] || RISK_LEVELS.medium
|
|
460
|
+
const scale = riskCfg.delayScale
|
|
461
|
+
const phase = opts.phase || 'default'
|
|
462
|
+
const target = opts.target || null
|
|
463
|
+
const targetXY = opts.targetXY || null
|
|
464
|
+
|
|
465
|
+
// 高亮真实目标 (单源 resolver): 让用户看到拟人行为围绕什么展开
|
|
466
|
+
// target=selector 走 resolver 解析真实元素再标记; targetXY=坐标直接标记
|
|
467
|
+
const interact = require('./interact')
|
|
468
|
+
if (target) {
|
|
469
|
+
try { await interact.highlightSelector(pageHandle, target, { ms: 0 }) } catch {}
|
|
470
|
+
} else if (targetXY) {
|
|
471
|
+
try { await interact.highlight(pageHandle, targetXY, { ms: 0 }) } catch {}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// 1. 随机拟人行为序列 (buildSequence 随机组合原子)
|
|
475
|
+
if (shouldTrigger(riskCfg)) {
|
|
476
|
+
const seqPhase = phase === 'navigate' ? 'navigate' : (phase === 'after' ? 'after-interact' : 'default')
|
|
477
|
+
const seq = buildSequence(seqPhase, riskCfg)
|
|
478
|
+
for (const atom of seq) {
|
|
479
|
+
await atom(pageHandle, scale)
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// 2. 归位到目标 (after 阶段不归位: 动作已执行完, 随机收尾即可)
|
|
484
|
+
if (opts.reposition !== false && phase !== 'after') {
|
|
485
|
+
if (target) {
|
|
486
|
+
await reposition(pageHandle, target, scale)
|
|
487
|
+
} else if (targetXY) {
|
|
488
|
+
// 坐标归位: 贝塞尔移到目标坐标
|
|
489
|
+
const from = getLastMouse(pageHandle)
|
|
490
|
+
const steps = randomInt(8, 16)
|
|
491
|
+
const pts = bezierTrajectory(from, targetXY, steps)
|
|
492
|
+
for (const pt of pts) {
|
|
493
|
+
await operate(pageHandle, { _skipHumanize: true, action: 'mouseMove', x: Math.round(pt.x), y: Math.round(pt.y) }).catch(() => {})
|
|
494
|
+
await sleep(randomBetween(10, 25))
|
|
495
|
+
}
|
|
496
|
+
setLastMouse(pageHandle, targetXY)
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// 检测 url 是否命中高风控域名
|
|
502
|
+
function detectRiskLevel (url) {
|
|
503
|
+
if (!url) return 'medium'
|
|
504
|
+
const lower = url.toLowerCase()
|
|
505
|
+
for (const domain of HIGH_RISK_DOMAINS) {
|
|
506
|
+
if (lower.includes(domain)) return 'high'
|
|
507
|
+
}
|
|
508
|
+
return 'medium'
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
module.exports = {
|
|
512
|
+
createHumanizer,
|
|
513
|
+
humanAct,
|
|
514
|
+
detectRiskLevel,
|
|
515
|
+
RISK_LEVELS,
|
|
516
|
+
HIGH_RISK_DOMAINS,
|
|
517
|
+
// 导出原子行为供测试/自定义组合
|
|
518
|
+
ATOMS,
|
|
519
|
+
// 导出工具
|
|
520
|
+
gaussian,
|
|
521
|
+
bezierTrajectory
|
|
522
|
+
}
|