mooncat-browser 0.1.0 → 0.2.1
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/browser-op/backend/browserd.cjs +4 -1
- package/browser-op/extension/server.cjs +3 -2
- package/browser-op/extension/service.cjs +7 -2
- package/browser-op/index.cjs +14 -17
- package/browser-op/webplater/dist/chrome-mv3/chunks/offscreen-JoFB8k4y.js +1 -0
- package/browser-op/webplater/dist/chrome-mv3/manifest.json +1 -1
- package/browser-op/webplater/dist/chrome-mv3/offscreen.html +1 -1
- package/browser-op/webplater/entrypoints/offscreen/main.ts +4 -1
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +363 -85
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +63 -42
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +69 -73
- package/dist/config.js.map +1 -1
- package/dist/instance.d.ts +57 -0
- package/dist/instance.d.ts.map +1 -0
- package/dist/instance.js +128 -0
- package/dist/instance.js.map +1 -0
- package/dist/prepare-extension.d.ts +37 -0
- package/dist/prepare-extension.d.ts.map +1 -0
- package/dist/prepare-extension.js +144 -0
- package/dist/prepare-extension.js.map +1 -0
- package/dist/server.d.ts +40 -23
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +51 -35
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- package/skills/browser/SKILL.md +37 -14
- package/skills/browser/references/collect.md +4 -2
- package/skills/browser/references/high-risk.md +27 -14
- package/skills/browser/references/probing.md +6 -4
- package/browser-op/webplater/dist/chrome-mv3/chunks/offscreen-CFXYw9Mo.js +0 -1
package/skills/browser/SKILL.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: browser
|
|
3
|
-
description: "Drive the user's local Chrome via
|
|
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
4
|
---
|
|
5
5
|
|
|
6
6
|
# browser
|
|
7
7
|
|
|
8
|
-
通过
|
|
8
|
+
通过 `mooncat-browser` 的 `BrowserClient` 驱动**用户本地 Chrome**(用户看得见的浏览器,
|
|
9
9
|
不是无头集群),做页面探查、采集、表单自动化、截图。
|
|
10
10
|
|
|
11
11
|
`BrowserClient` 是薄 HTTP RPC client,连接常驻的 **browserd 服务**(`mooncat-browser start`
|
|
@@ -31,31 +31,54 @@ description: "Drive the user's local Chrome via @mooncat/browser (BrowserClient
|
|
|
31
31
|
|
|
32
32
|
## 怎么用
|
|
33
33
|
|
|
34
|
+
> **前置**:mooncat-browser 是实例化、项目级配置的独立包。**不写全局 appData**,
|
|
35
|
+
> 必须先 `init` 生成项目配置,再 `start`。找不到配置会 fail fast。
|
|
36
|
+
>
|
|
37
|
+
> **实例(instance)= 一个 Chrome 进程边界**:一组端口 + 一个 profile + 一套 state/logs。
|
|
38
|
+
> **不是 task**——task 是调用方(workspace/agent)的概念,本包不规定、不关心。
|
|
39
|
+
> 一个项目**默认就一个 `default` 实例**(够用);只有需要**同时跑两个独立 Chrome**
|
|
40
|
+
> 时才加实例(各自独立 profile,互不串登录态/cookie)。
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# 一次性:在项目目录初始化(生成 config/browser.json + .browser/default/ 数据目录)
|
|
44
|
+
npx mooncat-browser init # 默认建 default 实例,绝大多数项目就这一个
|
|
45
|
+
|
|
46
|
+
# 启动实例(常驻,另一个终端)
|
|
47
|
+
npx mooncat-browser start default
|
|
48
|
+
# 高危平台走 extension 路:先 prepare 扩展目录 + 手动 load unpacked(见 high-risk.md)
|
|
49
|
+
npx mooncat-browser start default --route-mode extension
|
|
50
|
+
# open 时显式指定路由(高危平台必走 extension)
|
|
51
|
+
npx mooncat-browser open default --url https://example.com --route-mode extension
|
|
52
|
+
|
|
53
|
+
# 同时跑第二个独立 Chrome(高级,按需):init 时给名,各自独立 profile
|
|
54
|
+
npx mooncat-browser init shop-b # 加 shop-b 实例(独立 profile)
|
|
55
|
+
```
|
|
56
|
+
|
|
34
57
|
```ts
|
|
35
|
-
import { BrowserClient } from "
|
|
58
|
+
import { BrowserClient } from "mooncat-browser";
|
|
36
59
|
|
|
37
|
-
//
|
|
60
|
+
// 端口要和实例配置里 rpcPort 一致(默认 17322,多实例各自不同)
|
|
61
|
+
// 不确定时看 config/browser.json,或用 mooncat-browser list 查
|
|
38
62
|
const browser = new BrowserClient({ baseUrl: "http://127.0.0.1:17322" });
|
|
39
63
|
|
|
40
|
-
// 探测 browserd 是否就绪
|
|
41
64
|
const h = await browser.health();
|
|
42
65
|
if (!h?.browserOpen) await browser.open({ headless: false });
|
|
43
66
|
|
|
44
|
-
// 打开页面 (newTab 返回 TabInfo, operate 传 tab.pageHandle)
|
|
45
67
|
const tab = await browser.newTab({ url: "https://example.com" });
|
|
46
68
|
|
|
47
|
-
// 所有页面动作走 operate (action
|
|
69
|
+
// 所有页面动作走 operate (action 清单见 references/operate-actions)
|
|
48
70
|
const snap = await browser.operate({ pageHandle: tab.pageHandle, action: "snapshot" });
|
|
49
71
|
await browser.operate({ pageHandle: tab.pageHandle, action: "click", params: { selector: "#login" } });
|
|
50
72
|
```
|
|
51
73
|
|
|
52
|
-
>
|
|
53
|
-
> 没启动时 `health()` 返回 null
|
|
54
|
-
>
|
|
74
|
+
> 服务必须先启动:`mooncat-browser start <name>`(另一个终端,常驻)。
|
|
75
|
+
> 没启动时 `health()` 返回 null,`operate` 会连接拒绝报错。
|
|
76
|
+
> 多实例:每个实例独立 profile/端口/state,`mooncat-browser list` 查所有实例状态,
|
|
77
|
+
> `mooncat-browser stop <name>` / `status <name>` 按实例名管理。
|
|
55
78
|
|
|
56
|
-
**操作清单不在本 skill 列**——`operate` 的 40+ 个 action
|
|
57
|
-
|
|
58
|
-
`browser-op/backend/browserd.cjs` 的 `rpcOperate
|
|
79
|
+
**操作清单不在本 skill 列**——`operate` 的 40+ 个 action(导航/交互/读取/等待/存储/截图)
|
|
80
|
+
是公开契约,查包内 `README.md` 的 "operate 的 action 清单" 或
|
|
81
|
+
`browser-op/backend/browserd.cjs` 的 `rpcOperate`(始终权威)。
|
|
59
82
|
|
|
60
83
|
## 步进式 co-work 的标准节奏
|
|
61
84
|
|
|
@@ -100,7 +123,7 @@ await browser.operate({ pageHandle: tab.pageHandle, action: "click", params: { s
|
|
|
100
123
|
|
|
101
124
|
> 如果你在一个更大的系统里(有自己的通知渠道,如飞书/Slack/webhook),
|
|
102
125
|
> 把"报告用户"换成你的通知机制即可。本 skill 只规定"必须可见化 + 叫人"的纪律,
|
|
103
|
-
>
|
|
126
|
+
> 不规定具体通知后端——`mooncat-browser` 是独立工具包,不带任何通知能力。
|
|
104
127
|
|
|
105
128
|
## 参考
|
|
106
129
|
|
|
@@ -14,12 +14,14 @@
|
|
|
14
14
|
## 标准采集 step 模板
|
|
15
15
|
|
|
16
16
|
```ts
|
|
17
|
-
import { BrowserClient } from "
|
|
17
|
+
import { BrowserClient } from "mooncat-browser";
|
|
18
18
|
|
|
19
19
|
const SITE = "example.com";
|
|
20
|
+
// 端口要和实例配置 rpcPort 一致(默认 17322,多实例各自不同)
|
|
20
21
|
const browser = new BrowserClient({ baseUrl: "http://127.0.0.1:17322" });
|
|
21
22
|
|
|
22
23
|
// 1. 复用已有 tab (不要盲目 newTab)
|
|
24
|
+
// 高危平台:open({ routeMode: "extension" }),见 high-risk.md
|
|
23
25
|
await browser.open({ headless: false });
|
|
24
26
|
const tabs = await browser.listTabs();
|
|
25
27
|
let tab = tabs.find((t) => t.url?.includes(SITE));
|
|
@@ -147,7 +149,7 @@ await browser.operate({ pageHandle: page, action: "waitForSelector", params: { s
|
|
|
147
149
|
## 中间产物落盘
|
|
148
150
|
|
|
149
151
|
采集每一步的中间产物(snapshot / 截图 / 抓取的数据)建议落盘到本地目录,便于审计 + 给用户看。
|
|
150
|
-
用 Node 原生 `fs`
|
|
152
|
+
用 Node 原生 `fs` 写即可(`mooncat-browser` 不提供文件落盘能力,那是你应用层的职责):
|
|
151
153
|
|
|
152
154
|
```ts
|
|
153
155
|
import { writeFileSync } from "node:fs";
|
|
@@ -21,21 +21,34 @@ CDP 路由通过 `--remote-debugging-port` 暴露调试端口,**强风控脚
|
|
|
21
21
|
**高危平台必须走 extension 路由**:`open({ routeMode: "extension" })`。
|
|
22
22
|
若 profile 没装扩展,extension 路会失败——此时**停下来报告用户**,不要降级到 CDP 凑合。
|
|
23
23
|
|
|
24
|
-
## WebPlater
|
|
24
|
+
## WebPlater 扩展安装(每个实例 profile 一次性)
|
|
25
25
|
|
|
26
|
-
extension 路由依赖 WebPlater
|
|
27
|
-
|
|
26
|
+
extension 路由依赖 WebPlater 扩展。**扩展装在具体实例的 Chrome profile 里**——
|
|
27
|
+
每个实例有独立 profile(`config/browser.json` 里的 `profileDir`),换实例就要重装。
|
|
28
28
|
|
|
29
|
-
|
|
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`。
|
|
29
|
+
多实例下,**每个实例生成自己的扩展目录**(写死该实例的 WS 端口),扩展不共享:
|
|
36
30
|
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
```bash
|
|
32
|
+
# 1. 为实例生成 unpacked 扩展目录(copy 包内模板 + 替换 WS 端口占位符)
|
|
33
|
+
npx mooncat-browser prepare-extension <实例名>
|
|
34
|
+
# 产物在 <实例 extensionDir>(默认 .browser/<实例名>/extension)
|
|
35
|
+
# 里面 offscreen 代码已写死连 ws://127.0.0.1:<该实例 extensionPort>
|
|
36
|
+
|
|
37
|
+
# 2. 启动实例(会弹出用该实例 profile 的 Chrome)
|
|
38
|
+
npx mooncat-browser start <实例名> --route-mode extension
|
|
39
|
+
|
|
40
|
+
# 3. 在弹出的 Chrome 里手动 load unpacked:
|
|
41
|
+
# chrome://extensions → 开「开发者模式」→「加载已解压的扩展程序」
|
|
42
|
+
# 选择:<实例 extensionDir>(校验路径下有 manifest.json)
|
|
43
|
+
# profile 会永久记住这次安装,以后 start 不用再 load
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`prepare-extension` 会打印实例 extensionDir 的绝对路径——**把这个路径告诉用户**,
|
|
47
|
+
不要让用户自己找。校验路径下有 `manifest.json` 即正确。
|
|
48
|
+
|
|
49
|
+
> 注意:extension 路不能 headless(Chrome 限制)。高危平台本就要看得见浏览器,不冲突。
|
|
50
|
+
> 改了实例 extensionPort 或升级了包(扩展 build 变了),要重 `prepare-extension` 并在
|
|
51
|
+
> Chrome 里重新 load unpacked。
|
|
39
52
|
|
|
40
53
|
## 装了扩展后必须重启浏览器
|
|
41
54
|
|
|
@@ -124,7 +137,7 @@ if (blocked) {
|
|
|
124
137
|
writeFileSync(imgPath, Buffer.from(shot.dataUrl.split(",")[1], "base64"));
|
|
125
138
|
}
|
|
126
139
|
|
|
127
|
-
// 2. 报告用户叫人来处理 (用你应用层的通知机制;
|
|
140
|
+
// 2. 报告用户叫人来处理 (用你应用层的通知机制; mooncat-browser 不带通知能力)
|
|
128
141
|
// 内容: 拦截类型 + 截图路径 + "请打开浏览器完成验证, 会话将在登录态恢复后继续"
|
|
129
142
|
reportToUser({ type: "登录拦截", 拦截类型, screenshot: imgPath });
|
|
130
143
|
|
|
@@ -134,7 +147,7 @@ if (blocked) {
|
|
|
134
147
|
```
|
|
135
148
|
|
|
136
149
|
> 上例的 `reportToUser` 是占位——你的应用层用什么通知(飞书/Slack/webhook/console 打印 +
|
|
137
|
-
>
|
|
150
|
+
> 等待)就用什么。`mooncat-browser` 是独立工具包,**不带任何通知能力**,本 skill 只规定
|
|
138
151
|
> "必须可见化 + 叫人"的纪律,不规定通知后端。
|
|
139
152
|
|
|
140
153
|
**关键心态**:这些拦截"不能搞定"是**正常预期**,不是失败。职责是**第一时间发现并叫人**,
|
|
@@ -35,7 +35,8 @@ iframe/portal/shadow 里的元素。正确做法是上面 5 类系统性枚举,
|
|
|
35
35
|
### listFrames + 逐 frame eval 范式
|
|
36
36
|
|
|
37
37
|
```ts
|
|
38
|
-
import { BrowserClient } from "
|
|
38
|
+
import { BrowserClient } from "mooncat-browser";
|
|
39
|
+
// 端口 = 实例 rpcPort(默认 17322,多实例各自不同,看 config/browser.json)
|
|
39
40
|
const browser = new BrowserClient({ baseUrl: "http://127.0.0.1:17322" });
|
|
40
41
|
|
|
41
42
|
const tab = await browser.newTab({ url: "https://example.com" });
|
|
@@ -238,14 +239,14 @@ Chrome 有**自动下载保护**:点击下载后,文件可能被浏览器**
|
|
|
238
239
|
能把探查过程结构化留痕:事件流(events.ndjson)+ 三态结果(pass/fail/skip)+ 产物附件(截图/数据)。
|
|
239
240
|
它和 BrowserClient 是**天然搭档**:browser 做探查动作,cateye-probe 记录反应。
|
|
240
241
|
|
|
241
|
-
> cateye-probe 是**可选推荐**,不是
|
|
242
|
+
> cateye-probe 是**可选推荐**,不是 `mooncat-browser` 的依赖。不需要它也能探查(手动 fs 落盘
|
|
242
243
|
> 即可);需要可回溯 trace 时推荐搭配。两者都是独立包,互不耦合。
|
|
243
244
|
|
|
244
245
|
### 分工
|
|
245
246
|
|
|
246
247
|
| | 职责 | 来源 |
|
|
247
248
|
|---|---|---|
|
|
248
|
-
| browser | 决定做什么探查(导航/交互/读取),执行动作 |
|
|
249
|
+
| browser | 决定做什么探查(导航/交互/读取),执行动作 | `mooncat-browser` 的 `BrowserClient` |
|
|
249
250
|
| cateye-probe | 把过程留痕(事件流/结果/产物) | 独立 npm 包 `cateye-probe`(`import { probe } from "cateye-probe/sdk"`) |
|
|
250
251
|
|
|
251
252
|
### 组合范式骨架
|
|
@@ -253,7 +254,7 @@ Chrome 有**自动下载保护**:点击下载后,文件可能被浏览器**
|
|
|
253
254
|
一个探查脚本里:browser 做动作,probe 在关键节点写痕迹。每个 step 问一个问题、留一段 trace。
|
|
254
255
|
|
|
255
256
|
```ts
|
|
256
|
-
import { BrowserClient } from "
|
|
257
|
+
import { BrowserClient } from "mooncat-browser";
|
|
257
258
|
import { probe } from "cateye-probe/sdk"; // 可选搭配, 独立包
|
|
258
259
|
import { writeFileSync } from "node:fs";
|
|
259
260
|
|
|
@@ -261,6 +262,7 @@ const browser = new BrowserClient({ baseUrl: "http://127.0.0.1:17322" });
|
|
|
261
262
|
|
|
262
263
|
const done = probe.section("采集订单"); // 结构化段落
|
|
263
264
|
|
|
265
|
+
// 高危平台:open({ routeMode: "extension" }),见 high-risk.md
|
|
264
266
|
await browser.open({ headless: false });
|
|
265
267
|
const tab = await browser.newTab({ url: "https://example.com/orders" });
|
|
266
268
|
const page = tab.pageHandle;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import"./_virtual_wxt-html-plugins-DPbbfBKe.js";const l="ws://127.0.0.1:17321",p=1e3,E=2e4;let e=null,s=null,c=null;function d(){(e==null?void 0:e.readyState)===WebSocket.CONNECTING||(e==null?void 0:e.readyState)===WebSocket.OPEN||(e=new WebSocket(l),e.addEventListener("open",()=>{s&&(clearTimeout(s),s=null),console.log("[WebPlater/offscreen] connected to host",l),i({type:"hello",name:"bee-web-plugin"}),y()}),e.addEventListener("message",async t=>{let n;try{n=JSON.parse(String(t.data))}catch{console.error("[WebPlater/offscreen] invalid JSON from host",t.data);return}if(!(!n||n.type!=="command"||typeof n.id!="number"))try{const r=await S(n.id,n.method,n.params??{});u(n.id,{ok:!0,result:r})}catch(r){u(n.id,{ok:!1,error:r instanceof Error?r.message:String(r)})}}),e.addEventListener("close",()=>{console.log("[WebPlater/offscreen] disconnected, reconnecting..."),e=null,f(),b()}),e.addEventListener("error",()=>{e==null||e.close()}))}function i(t){(e==null?void 0:e.readyState)===WebSocket.OPEN&&e.send(JSON.stringify(t))}function u(t,n){const r={id:t,type:"response",...n};i(r)}function S(t,n,r){return new Promise((m,a)=>{chrome.runtime.sendMessage({source:"offscreen-command",id:t,method:n,params:r},g=>{if(chrome.runtime.lastError){a(new Error(chrome.runtime.lastError.message));return}const o=g;if(o&&typeof o=="object"&&o.ok===!1){a(new Error(o.error??"background error"));return}m(o==null?void 0:o.result)})})}function y(){f(),c=setInterval(()=>{i({type:"ping",time:Date.now()})},E)}function f(){c&&(clearInterval(c),c=null)}function b(){s||(s=setTimeout(()=>{s=null,d()},p))}d();
|