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,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://mooncat/sdk/browser.getCookies.schema.json",
|
|
4
|
+
"title": "browser.getCookies",
|
|
5
|
+
"description": "Cookie 操作",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"filter": {
|
|
9
|
+
"type": null
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"required": []
|
|
13
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://mooncat/sdk/browser.getDownload.schema.json",
|
|
4
|
+
"title": "browser.getDownload",
|
|
5
|
+
"description": "查单个下载状态",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"id": {
|
|
9
|
+
"type": "number"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"required": [
|
|
13
|
+
"id"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://mooncat/sdk/browser.listDownloads.schema.json",
|
|
4
|
+
"title": "browser.listDownloads",
|
|
5
|
+
"description": "列出最近下载 (chrome.downloads.search 实时查)",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"limit": {
|
|
9
|
+
"type": null,
|
|
10
|
+
"description": "[Type Gap: 未类型化]"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"required": [
|
|
14
|
+
"limit"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://mooncat/sdk/browser.listTabs.schema.json",
|
|
4
|
+
"title": "browser.listTabs",
|
|
5
|
+
"description": "列出标签页 (返回 TabInfo[], 每项 pageHandle 必有 pageId; 规范化 browserd wrapper)。",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {},
|
|
8
|
+
"required": []
|
|
9
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://mooncat/sdk/browser.newTab.schema.json",
|
|
4
|
+
"title": "browser.newTab",
|
|
5
|
+
"description": "新建标签页",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"options": {
|
|
9
|
+
"type": null
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"required": [
|
|
13
|
+
"options"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://mooncat/sdk/browser.open.schema.json",
|
|
4
|
+
"title": "browser.open",
|
|
5
|
+
"description": "打开浏览器 (宿主 Chrome, 双路由自动选择 extension/cdp)",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"options": {
|
|
9
|
+
"type": null
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"required": [
|
|
13
|
+
"options"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://mooncat/sdk/browser.operate.schema.json",
|
|
4
|
+
"title": "browser.operate",
|
|
5
|
+
"description": "在页面上执行动作 (click/fill/goto/snapshot/evaluate/...)",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"params": {
|
|
9
|
+
"type": null
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"required": [
|
|
13
|
+
"params"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://mooncat/sdk/browser.reuseTab.schema.json",
|
|
4
|
+
"title": "browser.reuseTab",
|
|
5
|
+
"description": "复用 tab: 按 urlMatch 找已开的 tab, 有则切过去复用, 无则 newTab(url)。",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"options": {
|
|
9
|
+
"type": null
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"required": [
|
|
13
|
+
"options"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://mooncat/sdk/browser.setCookies.schema.json",
|
|
4
|
+
"title": "browser.setCookies",
|
|
5
|
+
"description": "browser.setCookies input",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"cookies": {
|
|
9
|
+
"type": null
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"required": [
|
|
13
|
+
"cookies"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://mooncat/sdk/browser.waitFor.schema.json",
|
|
4
|
+
"title": "browser.waitFor",
|
|
5
|
+
"description": "通用等待: selector 或 text 出现。延迟轮询 + 刷新重试 + maxRefresh 次后仍未出现返回 ok:false。\nframeId 指定时在对应 iframe 内检测; 刷新整页 (location.reload) 后 frameId 可能变, 重新 listFrames。",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"options": {
|
|
9
|
+
"type": null
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"required": [
|
|
13
|
+
"options"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://mooncat/sdk/browser.waitForDownload.schema.json",
|
|
4
|
+
"title": "browser.waitForDownload",
|
|
5
|
+
"description": "轮询等下载完成 (基于 chrome.downloads 原生状态)。\n返回 { download, reason }: reason=complete 成功, timeout 超时。",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"options": {
|
|
9
|
+
"type": null
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"required": [
|
|
13
|
+
"options"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: browser
|
|
3
|
+
description: "Drive the user's local Chrome via @mooncat/browser (BrowserClient -> browserd RPC) for page probing, data collection, form automation, screenshots. Long-session co-work model (open once, reuse handle across turns), never judge route, stepwise with artifacts on disk. Use when the user wants to probe/collect/automate a web page, mentions browser automation, sycm/taobao scraping, or form filling. Independent toolkit - no mooncat distribution / workspace / PM2 dependency."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# browser
|
|
7
|
+
|
|
8
|
+
通过 `@mooncat/browser` 的 `BrowserClient` 驱动**用户本地 Chrome**(用户看得见的浏览器,
|
|
9
|
+
不是无头集群),做页面探查、采集、表单自动化、截图。
|
|
10
|
+
|
|
11
|
+
`BrowserClient` 是薄 HTTP RPC client,连接常驻的 **browserd 服务**(`mooncat-browser start`
|
|
12
|
+
拉起)。browserd 才是真正持有 Chrome 的进程,client 只发指令。
|
|
13
|
+
|
|
14
|
+
**这不是批处理脚本,是 co-work**——和用户一起,看着同一个打开的浏览器,一步一步推进。
|
|
15
|
+
|
|
16
|
+
## 三个铁律
|
|
17
|
+
|
|
18
|
+
1. **handle 是纯数据,绝不判断路由。** 拿到 `pageHandle` 就直接用,所有页面动作走
|
|
19
|
+
`browser.operate({ pageHandle, action, params })`。绝不写
|
|
20
|
+
`if (ph.mode === 'cdp') {...}`——路由(extension/CDP)是 browserd 内部的事。
|
|
21
|
+
handle 不可变,没有"切当前 tab"的操作,要操作哪个 page 就拿它的 pageHandle。
|
|
22
|
+
|
|
23
|
+
2. **会话长生命周期,用户满意前不释放。** `browser.open()` 一次,跨多步复用
|
|
24
|
+
handle。**绝不**每步都 open/close——启动 Chrome 慢,反复开关丢登录态/cookie。
|
|
25
|
+
只在用户说"完成"或要换完全不同的浏览器实例时才 `browser.close()`。`open()` 是幂等的:
|
|
26
|
+
浏览器还活着就复用当前 handle,被关了才重启。
|
|
27
|
+
|
|
28
|
+
3. **步进式 co-work,一步一确认。** 一次一步,每步落盘中间产物(snapshot/截图/数据),
|
|
29
|
+
给用户看、等反馈。**绝不**把所有操作塞进一个调用——任何中间步失败(弹窗/验证码/加载慢)
|
|
30
|
+
都会让已改的状态不一致,且用户根本没看到过程。
|
|
31
|
+
|
|
32
|
+
## 怎么用
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { BrowserClient } from "@mooncat/browser";
|
|
36
|
+
|
|
37
|
+
// 连接到已启动的服务(端口要和 mooncat-browser start --port 一致,默认 17322)
|
|
38
|
+
const browser = new BrowserClient({ baseUrl: "http://127.0.0.1:17322" });
|
|
39
|
+
|
|
40
|
+
// 探测 browserd 是否就绪
|
|
41
|
+
const h = await browser.health();
|
|
42
|
+
if (!h?.browserOpen) await browser.open({ headless: false });
|
|
43
|
+
|
|
44
|
+
// 打开页面 (newTab 返回 TabInfo, operate 传 tab.pageHandle)
|
|
45
|
+
const tab = await browser.newTab({ url: "https://example.com" });
|
|
46
|
+
|
|
47
|
+
// 所有页面动作走 operate (action 清单见包内 references/operate-actions 或 README)
|
|
48
|
+
const snap = await browser.operate({ pageHandle: tab.pageHandle, action: "snapshot" });
|
|
49
|
+
await browser.operate({ pageHandle: tab.pageHandle, action: "click", params: { selector: "#login" } });
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
> 服务必须先启动:`mooncat-browser start --port 17322`(另一个终端,常驻)。
|
|
53
|
+
> 没启动时 `health()` 返回 null,`operate` 会连接拒绝报错。CLI 一键开页验证:
|
|
54
|
+
> `mooncat-browser open --url https://example.com`。
|
|
55
|
+
|
|
56
|
+
**操作清单不在本 skill 列**——`operate` 的 40+ 个 action(导航/交互/读取/等待/存储/截图)
|
|
57
|
+
是公开契约,查包内 `README.md` 的 "operate 的 action 清单" 或
|
|
58
|
+
`browser-op/backend/browserd.cjs` 的 `rpcOperate`(始终权威)。
|
|
59
|
+
|
|
60
|
+
## 步进式 co-work 的标准节奏
|
|
61
|
+
|
|
62
|
+
用户:"帮我把这个网站我的订单都导出来。"
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
[回合1] 探查: open + newTab + status → "看到登录页,我先探查,你手动登录后告诉我?"
|
|
66
|
+
[回合2] 用户:登录好了 → 进订单页: waitForSelector + snapshot 落盘 → "看到50条订单,要导出吗?"
|
|
67
|
+
[回合3] 用户:导出 → 点导出: click + 截图确认 → "导出按钮点了,等下载"
|
|
68
|
+
[回合4] 用户:好了 → 收尾: 用户满意才 close
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
全程 browser 只 open 一次(幂等复用),跨越所有回合,最后一回合才 close。
|
|
72
|
+
**每一步的中间产物落盘**(snapshot yaml / 截图 / 抓取的数据),便于审计 + 给用户看。
|
|
73
|
+
|
|
74
|
+
## 路由不泄漏
|
|
75
|
+
|
|
76
|
+
`operate` 的 action 在 extension/CDP 两条路由上行为统一(少数例外见 high-risk.md 的 ext 列)。
|
|
77
|
+
**绝不**:
|
|
78
|
+
|
|
79
|
+
- 绝不 `if (ph.mode === 'cdp') { ph.page.click() }` —— 路由泄漏
|
|
80
|
+
- 绝不直接调 browserd 内部模块 —— 那是 BrowserClient 背后的实现
|
|
81
|
+
- 绝不修改 handle —— `ph.tabId = xxx` 是错的
|
|
82
|
+
|
|
83
|
+
路由相关(如换 profile、切 extension↔cdp)只能 `close()` 后重新 `open()`,不能中途换。
|
|
84
|
+
|
|
85
|
+
## 出错时怎么办
|
|
86
|
+
|
|
87
|
+
**STOP。报告具体错误。等用户指示。** 绝不强杀 Chrome 进程(`taskkill` 丢登录态、留僵尸端口、
|
|
88
|
+
用户未保存表单丢失),绝不自动重试轰炸。`open()` 失败时看返回的 notice/error。
|
|
89
|
+
|
|
90
|
+
句柄过期(用户手滑关了浏览器、CDP 断)时不要推倒重来——重新 `open()` attach 即可(便宜),
|
|
91
|
+
不要 close+relaunch。
|
|
92
|
+
|
|
93
|
+
## 失败可见化(通用纪律,不依赖任何通知框架)
|
|
94
|
+
|
|
95
|
+
任何一步失败(登录态丢失 / selector 失效 / 下载超时 / 风控拦截 / RPC 错),统一动作:
|
|
96
|
+
|
|
97
|
+
1. **截图取证**(`screenshot`,extension 模式先 `activate`)——落盘到本地路径。
|
|
98
|
+
2. **报告用户**:错误 + 截图路径 + 判断(验证码?登录失效?被限流?DOM 变了?)。
|
|
99
|
+
3. **STOP**——绝不吞错静默跳过,绝不重试轰炸(高危平台重试会加速封号)。
|
|
100
|
+
|
|
101
|
+
> 如果你在一个更大的系统里(有自己的通知渠道,如飞书/Slack/webhook),
|
|
102
|
+
> 把"报告用户"换成你的通知机制即可。本 skill 只规定"必须可见化 + 叫人"的纪律,
|
|
103
|
+
> 不规定具体通知后端——`@mooncat/browser` 是独立工具包,不带任何通知能力。
|
|
104
|
+
|
|
105
|
+
## 参考
|
|
106
|
+
|
|
107
|
+
- [references/collect.md](references/collect.md) — **★标准采集工作流**:三件套范式(复用 tab / 清弹窗 / 等就绪),SPA 采集必读
|
|
108
|
+
- [references/probing.md](references/probing.md) — **★探查方法论**:frame 意识(元素查不到怎么系统性枚举)/ 同名消歧(位置 context)/ SOP 截图识图纪律
|
|
109
|
+
- [references/high-risk.md](references/high-risk.md) — 高危平台专题(淘宝/京东/银行):扩展路由 + 验证码 + 拟人化
|
|
110
|
+
- [references/operate-actions.md](references/operate-actions.md) — operate 的 40+ 个 action 速查(按导航/交互/读取/等待/存储/截图分组)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# 标准采集工作流
|
|
2
|
+
|
|
3
|
+
采集类任务的固定范式。三个动作串起来,解决 SPA 场景三大坑:
|
|
4
|
+
**重复打开同页堆积 tab、弹窗挡住主体、主体没渲染就采**。
|
|
5
|
+
|
|
6
|
+
## 三件套(缺一不可)
|
|
7
|
+
|
|
8
|
+
| 动作 | 解决的坑 | 怎么做 |
|
|
9
|
+
|---|---|---|
|
|
10
|
+
| 复用已有 tab | 重复打开同一页面,堆积 tab | `browser.listTabs()` 找同 host 的 tab,有就复用,没有才 newTab |
|
|
11
|
+
| 清弹窗 | 网页内弹窗挡住主体,snapshot/点击失效 | `evaluate` 关闭 modal/popup,或点关闭按钮 |
|
|
12
|
+
| 等就绪 | SPA 加载慢,采集时主体还没渲染 | `waitForSelector` 等业务节点出现 |
|
|
13
|
+
|
|
14
|
+
## 标准采集 step 模板
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { BrowserClient } from "@mooncat/browser";
|
|
18
|
+
|
|
19
|
+
const SITE = "example.com";
|
|
20
|
+
const browser = new BrowserClient({ baseUrl: "http://127.0.0.1:17322" });
|
|
21
|
+
|
|
22
|
+
// 1. 复用已有 tab (不要盲目 newTab)
|
|
23
|
+
await browser.open({ headless: false });
|
|
24
|
+
const tabs = await browser.listTabs();
|
|
25
|
+
let tab = tabs.find((t) => t.url?.includes(SITE));
|
|
26
|
+
if (!tab) tab = await browser.newTab({ url: `https://${SITE}/orders` });
|
|
27
|
+
// newTab()/listTabs() 统一返回 TabInfo, operate 传 tab.pageHandle
|
|
28
|
+
const page = tab.pageHandle;
|
|
29
|
+
|
|
30
|
+
// 2. 清弹窗 (网页内 modal/popup, 会挡住主体)
|
|
31
|
+
await browser.operate({
|
|
32
|
+
pageHandle: page, action: "evaluate",
|
|
33
|
+
params: { source: "() => { document.querySelectorAll('.modal-close,.popup-close').forEach(b=>b.click()); return document.querySelectorAll('[class*=modal]').length }" },
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// 3. 等就绪 (业务节点出现才算能采)
|
|
37
|
+
await browser.operate({
|
|
38
|
+
pageHandle: page, action: "waitForSelector",
|
|
39
|
+
params: { selector: "#orders-table", timeout: 30000 },
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// 4. 采集 (此时主体已就绪, 没弹窗)
|
|
43
|
+
const data = await browser.operate({
|
|
44
|
+
pageHandle: page, action: "evaluate",
|
|
45
|
+
params: { source: "() => Array.from(document.querySelectorAll('.order')).map(e=>e.innerText)" },
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 顺序不能乱
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
复用 tab (listTabs 找)
|
|
53
|
+
↓
|
|
54
|
+
清弹窗 (弹窗可能挡住就绪判定元素)
|
|
55
|
+
↓
|
|
56
|
+
等就绪 (主体渲染完成)
|
|
57
|
+
↓
|
|
58
|
+
采集
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**清弹窗在等就绪前**:弹窗挡住时,`waitForSelector` 可能抓不到就绪元素(遮罩盖住),
|
|
62
|
+
导致误判没就绪。先清弹窗,再判就绪。
|
|
63
|
+
|
|
64
|
+
**不能跳过等就绪**:SPA 主体是 JS 动态渲染的,`goto`/`newTab` 返回时主体大概率没出来。
|
|
65
|
+
直接采会采到空。
|
|
66
|
+
|
|
67
|
+
## 弹窗类型区分(只有两类)
|
|
68
|
+
|
|
69
|
+
所有弹窗本质只有模态 / 非模态两类, 处理逻辑完全不同, 看点空白能不能关:
|
|
70
|
+
|
|
71
|
+
| 弹窗类型 | 特征 | 处理 |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| **模态弹窗** | 有遮罩(mask/overlay), 点空白**不能**关, 阻塞主体 | 找弹窗内**关闭按钮点掉**(×/关闭/我知道了/确定) |
|
|
74
|
+
| **非模态弹窗** | 无遮罩 或 点空白/遮罩区**能**关, 不阻塞 | **点空白处关**(点 body/遮罩外侧) 或直接隐藏 |
|
|
75
|
+
| 浏览器原生 dialog | alert/confirm/prompt | `setDialogHandler` action 注册处理器 |
|
|
76
|
+
|
|
77
|
+
国内站点引导弹窗("我知道了/开始使用/领取优惠")常见, 模态的按文本点关闭:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
await browser.operate({
|
|
81
|
+
pageHandle: page, action: "evaluate",
|
|
82
|
+
params: { source: "() => { const btns=[...document.querySelectorAll('button,span,a')]; const t=btns.find(b=>/我知道了|确定|关闭|开始使用/.test(b.textContent||'')); if(t){t.click();return true} return false }" },
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**优先判定"点空白能不能关", 不看弹窗叫什么名字。** 模态点空白没用必须找按钮;
|
|
87
|
+
非模态点空白就关。混淆会点不到或点错。
|
|
88
|
+
|
|
89
|
+
### 通用清弹窗范式(开页第一件事, 关键页操作前必走一次)
|
|
90
|
+
|
|
91
|
+
开页后第一件事是清弹窗, 然后才等就绪/采数据/点控件。万相台/生意参谋/淘宝系站点尤其多
|
|
92
|
+
新手引导热点遮罩(**文本为空的半透明遮罩**, 不存盘只看 DOM 极隐蔽)。
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
// 通用清弹窗: 模态找按钮点, 非模态点空白关。一次扫两类。
|
|
96
|
+
// 返回关了几个, 供日志。每个关键页操作前必走一次。
|
|
97
|
+
async function clearPopups(page) {
|
|
98
|
+
return await browser.operate({
|
|
99
|
+
pageHandle: page, action: "evaluate",
|
|
100
|
+
params: { source: `() => {
|
|
101
|
+
let cleared = 0;
|
|
102
|
+
// 1. 模态弹窗: 有遮罩且有按钮的, 点按钮(优先 ×/关闭/我知道了/确定)
|
|
103
|
+
const modals = [...document.querySelectorAll('[class*=modal],[class*=dialog],[class*=popup],[role=dialog],[class*=mask],[class*=overlay],[class*=guide-hotspot],[class*=hotspot]')]
|
|
104
|
+
.filter(e => e.offsetParent);
|
|
105
|
+
for (const m of modals) {
|
|
106
|
+
const btn = [...m.querySelectorAll('button,span,a,i,div,[role=button]')]
|
|
107
|
+
.find(b => b.offsetParent && /×|✕|关闭|我知道了|知道了|确定|不再提示|跳过|开始使用|下次再说/.test((b.textContent||'').trim()));
|
|
108
|
+
if (btn) { btn.click(); cleared++; }
|
|
109
|
+
}
|
|
110
|
+
// 2. 通用兜底: 全局找 ×/关闭/我知道了 按钮
|
|
111
|
+
const gBtns = [...document.querySelectorAll('button,span,a,i,[role=button]')]
|
|
112
|
+
.filter(e => e.offsetParent && /×|✕|关闭|我知道了|知道了|确定|跳过/.test((e.textContent||'').trim().slice(0,8)));
|
|
113
|
+
for (const b of gBtns.slice(0,3)) { b.click(); cleared++; }
|
|
114
|
+
// 3. 非模态弹窗: 点空白处关(点 body 顶层, 触发 clickoutside)
|
|
115
|
+
const masksNoBtn = modals.filter(m => ![...m.querySelectorAll('*')].some(b => b.offsetParent && /×|关闭|我知道了|确定/.test((b.textContent||''))));
|
|
116
|
+
if (masksNoBtn.length) { document.body.click(); cleared++; }
|
|
117
|
+
return { cleared, remainModal: modals.length };
|
|
118
|
+
}` },
|
|
119
|
+
}).catch(() => ({ cleared: 0 }));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 每个关键页操作前的标准顺序: goto → 清弹窗 → 等就绪 → 操作
|
|
123
|
+
await browser.operate({ pageHandle: page, action: "goto", params: { url } });
|
|
124
|
+
await sleep(2500);
|
|
125
|
+
await clearPopups(page); // ← 开页第一件事, 清弹窗
|
|
126
|
+
await browser.operate({ pageHandle: page, action: "waitForSelector", params: { selector: targetEl, timeout: 15000 } });
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**为什么清弹窗在等就绪前**: 弹窗遮罩盖住主体时, waitForSelector 可能抽不到就绪元素,
|
|
130
|
+
误判没就绪; 点控件会点到遮罩上。先清干净再判就绪。
|
|
131
|
+
|
|
132
|
+
## 等就绪的判定源
|
|
133
|
+
|
|
134
|
+
`waitForSelector` 等业务元素出现即就绪。判定源选择:
|
|
135
|
+
|
|
136
|
+
- **selector**:业务关键节点(如 `#orders-table`、`.data-panel`)出现即就绪
|
|
137
|
+
- **兜底**:用 `waitForLoadState`(state=networkidle)等网络空闲
|
|
138
|
+
|
|
139
|
+
没就绪的处理:等待超时后**停下报告用户**,不要采垃圾数据。SPA 主体没出来就采,
|
|
140
|
+
采到的是空壳,浪费整轮。
|
|
141
|
+
|
|
142
|
+
## 何时不用这套
|
|
143
|
+
|
|
144
|
+
- **简单静态页**(无弹窗、主体在 DOMContentLoaded 就绪):直接 `waitForSelector` + 采集。
|
|
145
|
+
- **纯读取场景**(status/snapshot/screenshot):本身不改状态,但若页面有弹窗挡住,仍建议先清弹窗。
|
|
146
|
+
|
|
147
|
+
## 中间产物落盘
|
|
148
|
+
|
|
149
|
+
采集每一步的中间产物(snapshot / 截图 / 抓取的数据)建议落盘到本地目录,便于审计 + 给用户看。
|
|
150
|
+
用 Node 原生 `fs` 写即可(`@mooncat/browser` 不提供文件落盘能力,那是你应用层的职责):
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import { writeFileSync } from "node:fs";
|
|
154
|
+
|
|
155
|
+
const snap = await browser.operate({ pageHandle: page, action: "snapshot" });
|
|
156
|
+
writeFileSync("./trace/orders-snapshot.yaml", snap.yaml);
|
|
157
|
+
|
|
158
|
+
const shot = await browser.operate({ pageHandle: page, action: "screenshot" });
|
|
159
|
+
if (shot?.dataUrl) writeFileSync("./trace/orders.png", Buffer.from(shot.dataUrl.split(",")[1], "base64"));
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
> 如果你在更大的系统里(有自己的 trace/artifacts 体系,如 mooncat 的 var/probe + cateye-probe),
|
|
163
|
+
> 用那套体系替代这里的手动 fs 即可。本 skill 只规定"中间产物要落盘"的纪律。
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# 高危平台注意事项
|
|
2
|
+
|
|
3
|
+
高危平台(淘宝/京东/生意参谋/银行/小红书等强风控站点)的探查/采集约束。
|
|
4
|
+
|
|
5
|
+
## 核心约束:CDP 端口会被风控识别
|
|
6
|
+
|
|
7
|
+
CDP 路由通过 `--remote-debugging-port` 暴露调试端口,**强风控脚本会检测这个端口**
|
|
8
|
+
(以及 webdriver 标记)。在淘宝/京东/银行这类站点走 CDP 路由,大概率被判定为自动化,
|
|
9
|
+
触发验证码、限流、甚至封号。
|
|
10
|
+
|
|
11
|
+
## 路由选择
|
|
12
|
+
|
|
13
|
+
`browser.open({ routeMode })` 的取值:
|
|
14
|
+
|
|
15
|
+
| routeMode | 行为 | 适用 |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `auto`(默认)| 自动检测 profile 是否装 WebPlater 扩展,装了走 extension,没装走 CDP | 通用 |
|
|
18
|
+
| `extension` | 强制走扩展路(无 CDP 端口,无 webdriver 标记) | **高危平台必选** |
|
|
19
|
+
| `cdp` | 强制走 CDP(暴露调试端口) | 静态页/调试/非风控站点 |
|
|
20
|
+
|
|
21
|
+
**高危平台必须走 extension 路由**:`open({ routeMode: "extension" })`。
|
|
22
|
+
若 profile 没装扩展,extension 路会失败——此时**停下来报告用户**,不要降级到 CDP 凑合。
|
|
23
|
+
|
|
24
|
+
## WebPlater 扩展安装(每个 profile 一次性)
|
|
25
|
+
|
|
26
|
+
extension 路由依赖 WebPlater 扩展。**扩展装在具体 Chrome profile 里**——换 profile 就要重装。
|
|
27
|
+
首次使用高危平台前,用户需在目标 profile 装 WebPlater:
|
|
28
|
+
|
|
29
|
+
1. `mooncat-browser install-extension` 拿到扩展目录绝对路径(包内 `browser-op/webplater/dist/chrome-mv3/`)。
|
|
30
|
+
2. `mooncat-browser start --port 17322` 启动服务,`browser.open({ routeMode: "auto" })` 启动 Chrome,
|
|
31
|
+
拿到 profile 的 user-data-dir(默认 `%LOCALAPPDATA%\mooncat-browser\profile`,或 `--profile` 指定)。
|
|
32
|
+
3. 用户在该 Chrome 访问 `chrome://extensions`,开「开发者模式」。
|
|
33
|
+
4. 点「加载已解压的扩展程序」,选第 1 步拿到的扩展目录(校验路径下有 `manifest.json`)。
|
|
34
|
+
5. 等几秒 background service worker 连上 WS server。
|
|
35
|
+
6. 重新 `open`(routeMode=auto),看返回的 `mode` 是否为 `extension`。
|
|
36
|
+
|
|
37
|
+
**必须先算好扩展绝对路径再告诉用户**,不要让用户自己找。`mooncat-browser install-extension`
|
|
38
|
+
会打印路径。校验路径下有 `manifest.json`。
|
|
39
|
+
|
|
40
|
+
## 装了扩展后必须重启浏览器
|
|
41
|
+
|
|
42
|
+
扩展装上后,`open()` 返回的 `mode` 可能仍是 `cdp`(因为 open 时 routeMode 已锁定)。
|
|
43
|
+
**必须 `browser.close()` 再重新 `open()`**,mode 才会切到 extension。不能中途静默切换。
|
|
44
|
+
|
|
45
|
+
## 探查动作的拟人化
|
|
46
|
+
|
|
47
|
+
⚠️ **重要**:BrowserClient(薄 RPC 到 browserd)**不暴露 humanize/riskLevel 选项**。
|
|
48
|
+
裸 `operate` 的 click/fill 是机器特征(瞬点、无轨迹),高危平台即使走 extension 路由,
|
|
49
|
+
风控仍可能通过交互节奏识别。
|
|
50
|
+
|
|
51
|
+
高危平台探查时:
|
|
52
|
+
|
|
53
|
+
- **优先只读操作**(status/snapshot/innerText/screenshot)——只读不触发风控交互检测。
|
|
54
|
+
- **交互操作(click/fill)谨慎用**:必须交互时,操作间加 `waitForTimeout` 随机延迟,
|
|
55
|
+
降低节奏特征。
|
|
56
|
+
- **触发验证码立即停下**:检测到验证码(页面出现验证码元素/被重定向到验证页),
|
|
57
|
+
**STOP,报告用户**,不要尝试自动过验证码。
|
|
58
|
+
|
|
59
|
+
### probe 长会话分阶段(高危平台是安全红线,不只是效率)
|
|
60
|
+
|
|
61
|
+
探查天然分阶段——开一次页面 + 导航到位后,**复用同一个 tab**继续探下一个问题,不重开。
|
|
62
|
+
**严禁每个小问题都重新 newTab + 导航**。
|
|
63
|
+
|
|
64
|
+
**为什么在高危平台这是红线,不只是效率问题**:
|
|
65
|
+
- 每开一次 newTab + 导航 + 等 SPA = 一次完整页面加载请求。反复重开 = 高频异常访问。
|
|
66
|
+
- 生意参谋/天猫/京东等强风控站点,**重开页面越多,风控触发越快**——推向验证码/限流/封号。
|
|
67
|
+
- 探查阶段本就高频操作,再叠加反复重开,等于主动触雷。
|
|
68
|
+
|
|
69
|
+
→ **同一个页面一旦打开,在会话内反复复用,用多次 evaluate 探不同问题,绝不为每个小问题
|
|
70
|
+
重新 newTab+导航。** 探查动作只读优先(status/snapshot/screenshot),减少交互请求;
|
|
71
|
+
必须交互时控制频率。
|
|
72
|
+
|
|
73
|
+
复用方式: `browser.listTabs()` 按 url 匹配找到已开的 tab,拿它的 `pageHandle` 继续操作。
|
|
74
|
+
|
|
75
|
+
## 验证码检测
|
|
76
|
+
|
|
77
|
+
操作后用只读 snapshot/innerText 检测是否被验证码拦截。**注意:iframe 内的拦截要从对应 frameId 读**
|
|
78
|
+
(淘宝/阿里统一登录在 havanalogin iframe,顶层 document 读不到)。
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// 顶层页面文本
|
|
82
|
+
const topText = await browser.operate({
|
|
83
|
+
pageHandle: tab.pageHandle, action: "innerText",
|
|
84
|
+
params: { selector: "body" },
|
|
85
|
+
});
|
|
86
|
+
// 登录 iframe 内文本 (跨域, 必须用 frameId eval)
|
|
87
|
+
const iframeText = await browser.operate({
|
|
88
|
+
pageHandle: tab.pageHandle, action: "evaluate",
|
|
89
|
+
params: { frameId: loginFrameId, source: "() => document.body ? document.body.innerText : ''" },
|
|
90
|
+
});
|
|
91
|
+
const combined = String(topText.value) + String(iframeText?.value || iframeText || "");
|
|
92
|
+
if (/验证码|滑动验证|请完成验证|向右滑动|captcha|拖动|安全验证/i.test(combined)) {
|
|
93
|
+
// STOP: 截图 + 报告用户被验证码拦截, 不自动过
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
命中后**绝不**继续操作,绝不尝试自动识别/绕过验证码。停下来让用户手动过。
|
|
98
|
+
|
|
99
|
+
## 不可自动化的拦截类型(检测到即报告用户,绝不对抗)
|
|
100
|
+
|
|
101
|
+
高危平台登录链路有一类拦截是**根本无法自动化**的(自动过 = 对抗风控 = 封号风险)。
|
|
102
|
+
这些不是 bug,是平台风控设计,唯一正确动作是**检测 → 报告用户 → 暂停等人工**。
|
|
103
|
+
|
|
104
|
+
| 拦截类型 | 识别特征(页面文本) | 能否自动 |
|
|
105
|
+
|---|---|---|
|
|
106
|
+
| **滑块验证** | `向右滑动验证` / `拖动滑块` / `滑动验证` | ❌ 不能。轨迹是风控核心,机器滑动必被识破 |
|
|
107
|
+
| **手机/短信验证码** | `短信验证码` / `获取验证码` / `手机验证` | ❌ 不能。要人手机收码 |
|
|
108
|
+
| **图形验证码** | `输入图中字符` / `看不清` / 图形验证 | ❌ 不能。OCR 过码违反平台规则 |
|
|
109
|
+
| **人脸/实名** | `人脸识别` / `实名认证` | ❌ 不能。生物特征,只能人 |
|
|
110
|
+
| **风险提醒** | `检测到风险` / `异常登录` / `环境异常` | ❌ 不能。需人工按提示操作 |
|
|
111
|
+
|
|
112
|
+
**统一处理范式**(无论哪种拦截,用截图 + 报告代替通知框架):
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
// 检测到拦截 (滑块/验证码/风险提醒任一)
|
|
116
|
+
if (blocked) {
|
|
117
|
+
// 1. 截图取证 (extension 模式先 activate, 否则 screenshot 报错)
|
|
118
|
+
await browser.operate({ pageHandle: tab.pageHandle, action: "activate" }).catch(() => {});
|
|
119
|
+
const shot = await browser.operate({ pageHandle: tab.pageHandle, action: "screenshot" });
|
|
120
|
+
let imgPath = null;
|
|
121
|
+
if (shot?.dataUrl) {
|
|
122
|
+
const { writeFileSync } = await import("node:fs");
|
|
123
|
+
imgPath = "./trace/login-blocked.png";
|
|
124
|
+
writeFileSync(imgPath, Buffer.from(shot.dataUrl.split(",")[1], "base64"));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 2. 报告用户叫人来处理 (用你应用层的通知机制; @mooncat/browser 不带通知能力)
|
|
128
|
+
// 内容: 拦截类型 + 截图路径 + "请打开浏览器完成验证, 会话将在登录态恢复后继续"
|
|
129
|
+
reportToUser({ type: "登录拦截", 拦截类型, screenshot: imgPath });
|
|
130
|
+
|
|
131
|
+
// 3. STOP — 不重试, 不对抗, 等人。会话在此挂起, 下次重新检测登录态
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
> 上例的 `reportToUser` 是占位——你的应用层用什么通知(飞书/Slack/webhook/console 打印 +
|
|
137
|
+
> 等待)就用什么。`@mooncat/browser` 是独立工具包,**不带任何通知能力**,本 skill 只规定
|
|
138
|
+
> "必须可见化 + 叫人"的纪律,不规定通知后端。
|
|
139
|
+
|
|
140
|
+
**关键心态**:这些拦截"不能搞定"是**正常预期**,不是失败。职责是**第一时间发现并叫人**,
|
|
141
|
+
不是去破解。把"无法自动过"当问题去解,就是在对抗风控,违反铁律。
|
|
142
|
+
|
|
143
|
+
### 登录态复用(避免每次都撞拦截)
|
|
144
|
+
|
|
145
|
+
既然登录拦截无法自动,正确架构是 **不碰登录填表,只做登录态检测**:
|
|
146
|
+
- 启动 → `browser.open()` + 访问目标页 → 读 url/snapshot 判断是否已登录
|
|
147
|
+
- 已登录(cookie 在 profile 里)→ 直接进业务步骤
|
|
148
|
+
- 未登录/被拦 → 报告用户叫人,会话挂起
|
|
149
|
+
- 人工登录一次后(过滑块/验证码),cookie 留在 profile,后续会话复用,不再撞拦截
|
|
150
|
+
|
|
151
|
+
这条"登录态复用"是高危平台的标准模式。
|
|
152
|
+
|
|
153
|
+
## 出错纪律(高危平台更严)
|
|
154
|
+
|
|
155
|
+
1. **STOP**,不要重试轰炸。
|
|
156
|
+
2. 截图记录当前状态(`screenshot` 到产物)。
|
|
157
|
+
3. 报告用户:错误 + 截图 + 判断(验证码?登录失效?被限流?)。
|
|
158
|
+
4. 等用户决定下一步。
|
|
159
|
+
|
|
160
|
+
**绝不**:强杀 Chrome(`taskkill`)、清 cookie 重来、自动切换 profile——这些在高危平台都会
|
|
161
|
+
放大风险(清 cookie 可能触发异地登录风控,切 profile 丢登录态)。
|