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,228 @@
|
|
|
1
|
+
// -*- coding: utf-8 -*-
|
|
2
|
+
//
|
|
3
|
+
// extension-detect.cjs — 启动前检测:扫浏览器 profile,判断 WebPlater 扩展是否装好且未禁用。
|
|
4
|
+
// 完整复刻自 bee/packages/app/browser-op/main/web/extension-detect.ts。
|
|
5
|
+
//
|
|
6
|
+
// 用途:双路由 open() 决策走插件通道还是 CDP 通道。
|
|
7
|
+
// - 扩展装好且未禁用 → 走插件通道(无 CDP 端口,WS 通信)
|
|
8
|
+
// - 否则 → 走 CDP 通道(CdpBrowserLauncher,带调试端口)
|
|
9
|
+
//
|
|
10
|
+
// 判据(基于真实 Chrome profile 数据结构):
|
|
11
|
+
// 扩展设置存在 `Secure Preferences`(不是 `Preferences`)的
|
|
12
|
+
// `extensions.settings.<id>` 下,字段:
|
|
13
|
+
// - manifest.name : 扩展名(webplater 的 manifest 无 key,id 随 load 路径变,
|
|
14
|
+
// 故按 name=="WebPlater" 匹配,不按固定 id)
|
|
15
|
+
// - disable_reasons : 数组(非空 = 被禁用),如 [8192]
|
|
16
|
+
// - path : 相对 profile 的扩展目录,形如 "<id>\\<ver>_0"
|
|
17
|
+
// - state : 多数扩展无此字段,不可靠,不作为判据
|
|
18
|
+
//
|
|
19
|
+
// 扩展文件存在 `<profile>/Extensions/<id>/<ver>/manifest.json`。
|
|
20
|
+
|
|
21
|
+
'use strict'
|
|
22
|
+
|
|
23
|
+
const { existsSync, readFileSync, readdirSync, statSync } = require('node:fs')
|
|
24
|
+
const { join } = require('node:path')
|
|
25
|
+
|
|
26
|
+
/** webplater 扩展名(manifest.name),作为 fallback 匹配依据 */
|
|
27
|
+
const WEBPLATER_EXTENSION_NAME = 'WebPlater'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* webplater 固定 extensionId(manifest.key 决定,不随 load 路径变)。
|
|
31
|
+
* 由 webplater 的 wxt.config.ts manifest.key 按 Chrome 算法稳定算出。
|
|
32
|
+
* 这是主判据——比 name 匹配稳(name 可能多语言/__MSG__/被改)。
|
|
33
|
+
* 与 webplater 仓库的 manifest.key 必须保持一致,任一方变动需同步。
|
|
34
|
+
*/
|
|
35
|
+
const WEBPLATER_EXTENSION_ID = 'npkapgkjeaaipldb'
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @typedef {Object} ExtensionDetectResult
|
|
39
|
+
* @property {boolean} installed 是否装了(无论禁用与否)
|
|
40
|
+
* @property {string|null} profileDir 找到扩展的 profile 子目录绝对路径,未装为 null
|
|
41
|
+
* @property {string|null} extensionId 扩展 id
|
|
42
|
+
* @property {string|null} name Secure Preferences 里的 manifest.name
|
|
43
|
+
* @property {boolean} disabled 是否被禁用(disable_reasons 非空数组)
|
|
44
|
+
* @property {boolean} usable 是否既装了又未禁用 — open() 路由判据
|
|
45
|
+
* @property {boolean} hasFiles Extensions 目录下是否有 manifest.json 文件
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/** Preferences 文件优先级:Secure Preferences 先(扩展真实落点),Preferences 兜底 */
|
|
49
|
+
const PREF_FILES = ['Secure Preferences', 'Preferences']
|
|
50
|
+
|
|
51
|
+
function readJson (path) {
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(readFileSync(path, 'utf8'))
|
|
54
|
+
} catch {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 扫描 user-data-dir 下所有可能的 profile 子目录(Default/Profile 1/...)。
|
|
61
|
+
* 返回找到的扩展检测结果(第一个含 WebPlater 的 profile)。
|
|
62
|
+
*
|
|
63
|
+
* 参数是 Chrome 的 user-data-dir (如 .../profiles/default), 不是 profileRoot
|
|
64
|
+
* (.../profiles)。扩展在 user-data-dir/Default/Secure Preferences 下。
|
|
65
|
+
* 同时兼容传入目录本身就是 profile 的情况 (load unpacked 到默认 profile)。
|
|
66
|
+
* @param {string} userDataDir
|
|
67
|
+
* @returns {ExtensionDetectResult}
|
|
68
|
+
*/
|
|
69
|
+
function detectWebplaterExtension (userDataDir) {
|
|
70
|
+
if (!userDataDir || !existsSync(userDataDir)) {
|
|
71
|
+
return emptyResult()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 候选 profile 子目录:Default / Profile 1 / Profile 2 / ...
|
|
75
|
+
const profileDirs = listProfileDirs(userDataDir)
|
|
76
|
+
|
|
77
|
+
for (const dir of profileDirs) {
|
|
78
|
+
const result = scanProfile(dir)
|
|
79
|
+
if (result.installed) return result
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return emptyResult()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function listProfileDirs (userDataDir) {
|
|
86
|
+
const dirs = []
|
|
87
|
+
// 1. 传入目录本身可能就是一个 profile (直接有 Secure Preferences)。
|
|
88
|
+
// 某些 load unpacked 场景扩展落在默认 profile, 不在 Default 子目录。
|
|
89
|
+
if (hasAnyPrefFile(userDataDir)) dirs.push(userDataDir)
|
|
90
|
+
|
|
91
|
+
// 2. Chromium 标准: userDataDir/Default + userDataDir/Profile N
|
|
92
|
+
const defaultDir = join(userDataDir, 'Default')
|
|
93
|
+
if (existsSync(defaultDir) && statSync(defaultDir).isDirectory()) dirs.push(defaultDir)
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
for (const name of readdirSync(userDataDir)) {
|
|
97
|
+
if (/^Profile \d+$/.test(name)) {
|
|
98
|
+
const full = join(userDataDir, name)
|
|
99
|
+
if (statSync(full).isDirectory()) dirs.push(full)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} catch { /* ignore */ }
|
|
103
|
+
|
|
104
|
+
return dirs
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function hasAnyPrefFile (dir) {
|
|
108
|
+
return PREF_FILES.some((f) => existsSync(join(dir, f)))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function scanProfile (profileDir) {
|
|
112
|
+
for (const prefFile of PREF_FILES) {
|
|
113
|
+
const prefPath = join(profileDir, prefFile)
|
|
114
|
+
if (!existsSync(prefPath)) continue
|
|
115
|
+
|
|
116
|
+
const prefs = readJson(prefPath)
|
|
117
|
+
if (!prefs || typeof prefs !== 'object') continue
|
|
118
|
+
|
|
119
|
+
const settings = prefs.extensions && prefs.extensions.settings
|
|
120
|
+
if (!settings || typeof settings !== 'object') continue
|
|
121
|
+
|
|
122
|
+
// 遍历所有扩展设置, 用 resolveExtensionInfo 读真实 name (对齐 bee)
|
|
123
|
+
// load unpacked (location=4) 时 setting.manifest 常缺失, name 要从
|
|
124
|
+
// setting.path 指向的 manifest.json 读 — 这是 WebPlater 的实际情况
|
|
125
|
+
for (const [id, entry] of Object.entries(settings)) {
|
|
126
|
+
if (!entry) continue
|
|
127
|
+
const info = resolveExtensionInfo(entry, profileDir)
|
|
128
|
+
// 主判据: 固定 extensionId 命中, 或 name == WebPlater
|
|
129
|
+
const matched = id === WEBPLATER_EXTENSION_ID ||
|
|
130
|
+
info?.name === WEBPLATER_EXTENSION_NAME ||
|
|
131
|
+
(entry.manifest && entry.manifest.name === WEBPLATER_EXTENSION_NAME)
|
|
132
|
+
if (!matched) continue
|
|
133
|
+
|
|
134
|
+
const disabled = Array.isArray(entry.disable_reasons) && entry.disable_reasons.length > 0
|
|
135
|
+
const manifestDir = info?.manifestDir || profileDir
|
|
136
|
+
const hasFiles = existsSync(join(manifestDir, 'manifest.json')) ||
|
|
137
|
+
checkExtensionFiles(profileDir, id, entry)
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
installed: true,
|
|
141
|
+
profileDir,
|
|
142
|
+
extensionId: id,
|
|
143
|
+
name: info?.name || (entry.manifest && entry.manifest.name) || WEBPLATER_EXTENSION_NAME,
|
|
144
|
+
disabled,
|
|
145
|
+
usable: !disabled && hasFiles,
|
|
146
|
+
hasFiles
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return emptyResult()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 从 setting 解析扩展的真实 name 与 manifest.json 所在目录 (对齐 bee)。
|
|
156
|
+
* Chrome 存储两种形态:
|
|
157
|
+
* - 商店安装 (location=3): setting.manifest.name 有值, 文件在
|
|
158
|
+
* <profile>/Extensions/<id>/<ver>/manifest.json
|
|
159
|
+
* - load unpacked (location=4): setting.manifest 常缺失, path 是**绝对路径**
|
|
160
|
+
* 指向扩展根目录, manifest.json 在该根目录下
|
|
161
|
+
* 返回 { name, manifestDir } 或 null。
|
|
162
|
+
*/
|
|
163
|
+
function resolveExtensionInfo (setting, profileDir) {
|
|
164
|
+
if (setting.path) {
|
|
165
|
+
const root = existsSync(setting.path) ? setting.path : join(profileDir, setting.path)
|
|
166
|
+
const manifestPath = join(root, 'manifest.json')
|
|
167
|
+
if (existsSync(manifestPath)) {
|
|
168
|
+
const m = readJson(manifestPath)
|
|
169
|
+
if (m && m.name) return { name: m.name, manifestDir: root }
|
|
170
|
+
}
|
|
171
|
+
// path 可能是 <id>\<ver>_0 这种商店子目录形式: manifest 在下一层
|
|
172
|
+
if (existsSync(root)) {
|
|
173
|
+
try {
|
|
174
|
+
for (const v of readdirSync(root)) {
|
|
175
|
+
const mp = join(root, v, 'manifest.json')
|
|
176
|
+
if (existsSync(mp)) {
|
|
177
|
+
const m = readJson(mp)
|
|
178
|
+
if (m && m.name) return { name: m.name, manifestDir: join(root, v) }
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch { /* ignore */ }
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// fallback: setting.manifest.name (部分安装方式 Chrome 会内联 manifest)
|
|
185
|
+
if (setting.manifest && setting.manifest.name) {
|
|
186
|
+
return { name: setting.manifest.name, manifestDir: profileDir }
|
|
187
|
+
}
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function checkExtensionFiles (profileDir, extensionId, entry) {
|
|
192
|
+
const extBase = join(profileDir, 'Extensions', extensionId)
|
|
193
|
+
if (!existsSync(extBase)) return false
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
for (const ver of readdirSync(extBase)) {
|
|
197
|
+
const manifest = join(extBase, ver, 'manifest.json')
|
|
198
|
+
if (existsSync(manifest)) return true
|
|
199
|
+
}
|
|
200
|
+
} catch { /* ignore */ }
|
|
201
|
+
|
|
202
|
+
// fallback: entry.path 给出的相对路径
|
|
203
|
+
if (entry.path) {
|
|
204
|
+
const byPath = join(profileDir, entry.path, 'manifest.json')
|
|
205
|
+
if (existsSync(byPath)) return true
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return false
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function emptyResult () {
|
|
212
|
+
return {
|
|
213
|
+
installed: false,
|
|
214
|
+
profileDir: null,
|
|
215
|
+
extensionId: null,
|
|
216
|
+
name: null,
|
|
217
|
+
disabled: false,
|
|
218
|
+
usable: false,
|
|
219
|
+
hasFiles: false
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
module.exports = {
|
|
224
|
+
detectWebplaterExtension,
|
|
225
|
+
WEBPLATER_EXTENSION_ID,
|
|
226
|
+
WEBPLATER_EXTENSION_NAME,
|
|
227
|
+
PREF_FILES
|
|
228
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// -*- coding: utf-8 -*-
|
|
2
|
+
//
|
|
3
|
+
// server.cjs — 本地 WebSocket 主机,供 WebPlater 扩展连接。
|
|
4
|
+
// 完整复刻自 bee/packages/app/browser-op/main/web/server.ts。
|
|
5
|
+
//
|
|
6
|
+
// 协议:
|
|
7
|
+
// - 监听 127.0.0.1:17321
|
|
8
|
+
// - 扩展 background service worker 连入
|
|
9
|
+
// - 主机通过 callPlugin() 下发命令,扩展通过 response 回包
|
|
10
|
+
// - 扩展断开时,所有挂起 callPlugin 立即 reject(不等超时)
|
|
11
|
+
// - 扩展周期发 hello/ping 保活,主机据此判 isPluginAlive
|
|
12
|
+
|
|
13
|
+
'use strict'
|
|
14
|
+
|
|
15
|
+
const { WebSocketServer, WebSocket } = require('ws')
|
|
16
|
+
|
|
17
|
+
const DEFAULT_HOST = '127.0.0.1'
|
|
18
|
+
const DEFAULT_PORT = 17321
|
|
19
|
+
const COMMAND_TIMEOUT_MS = 15000
|
|
20
|
+
// 长耗时命令放宽:evaluate/waitForFunction 可能长计算或等异步(interact 人机输入最长 120s)
|
|
21
|
+
const COMMAND_LONG_TIMEOUT_MS = 300000
|
|
22
|
+
const LONG_TIMEOUT_METHODS = new Set(['evaluate', 'waitForFunction'])
|
|
23
|
+
|
|
24
|
+
function defaultLog () {
|
|
25
|
+
if (process.env.BROWSER_OP_DEBUG) {
|
|
26
|
+
console.error.apply(console, arguments)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class WebPluginServer {
|
|
31
|
+
constructor (info = defaultLog) {
|
|
32
|
+
this.info = info
|
|
33
|
+
this.wss = null
|
|
34
|
+
this.socket = null
|
|
35
|
+
this.seq = 1
|
|
36
|
+
this.pending = new Map()
|
|
37
|
+
/** 最近一次收到扩展 hello/ping 的时间戳。alive 判据 = Date.now() - lastSeenAt < 阈值 */
|
|
38
|
+
this.lastSeenAt = 0
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get isConnected () {
|
|
42
|
+
return this.socket !== null && this.socket.readyState === WebSocket.OPEN
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 插件心跳是否存活。
|
|
47
|
+
* 扩展每 20s 发一次 ping,阈值放宽到 45s 容忍抖动。
|
|
48
|
+
* 双路由 open() 决策:alive → 走插件通道;否则 → CDP。
|
|
49
|
+
*/
|
|
50
|
+
isPluginAlive (thresholdMs = 45000) {
|
|
51
|
+
return this.lastSeenAt > 0 && Date.now() - this.lastSeenAt < thresholdMs
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async start (host = DEFAULT_HOST, port = DEFAULT_PORT) {
|
|
55
|
+
if (this.wss) return true
|
|
56
|
+
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
this.wss = new WebSocketServer({ host, port })
|
|
59
|
+
|
|
60
|
+
this.wss.on('error', (err) => {
|
|
61
|
+
if (err.message.includes('EADDRINUSE')) {
|
|
62
|
+
// 必须失败: 17321 被占说明已有 browserd 在管插件, 不能两个同时管
|
|
63
|
+
this.info('[web] port %d already in use — EADDRINUSE', port)
|
|
64
|
+
this.wss = null
|
|
65
|
+
reject(new Error(`EADDRINUSE: port ${port} already in use (another browserd owns it)`))
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
this.info('[web] server error: %s', err.message)
|
|
69
|
+
this.wss = null
|
|
70
|
+
reject(err)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
this.wss.on('listening', () => {
|
|
74
|
+
this.info('[web] server listening on ws://%s:%d', host, port)
|
|
75
|
+
resolve(true)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
this.wss.on('connection', (socket) => {
|
|
79
|
+
if (this.socket && this.socket !== socket) {
|
|
80
|
+
this.info('[web] new connection replaced previous socket')
|
|
81
|
+
this.socket.terminate()
|
|
82
|
+
}
|
|
83
|
+
this.socket = socket
|
|
84
|
+
this.info('[web] extension connected')
|
|
85
|
+
|
|
86
|
+
socket.on('message', (raw) => this.handleMessage(raw))
|
|
87
|
+
socket.on('close', () => {
|
|
88
|
+
if (this.socket === socket) {
|
|
89
|
+
this.socket = null
|
|
90
|
+
this.info('[web] extension disconnected')
|
|
91
|
+
this.rejectAllPending('extension disconnected')
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
socket.on('error', (err) => {
|
|
95
|
+
this.info('[web] socket error: %s', err.message)
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async stop () {
|
|
102
|
+
this.rejectAllPending('server stopping')
|
|
103
|
+
if (this.socket) {
|
|
104
|
+
this.socket.terminate()
|
|
105
|
+
this.socket = null
|
|
106
|
+
}
|
|
107
|
+
if (!this.wss) return
|
|
108
|
+
|
|
109
|
+
const wss = this.wss
|
|
110
|
+
this.wss = null
|
|
111
|
+
return new Promise((resolve) => {
|
|
112
|
+
wss.close(() => resolve())
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** 向扩展下发一条命令并等待回包 */
|
|
117
|
+
async callPlugin (method, params) {
|
|
118
|
+
if (!this.isConnected) {
|
|
119
|
+
throw new Error('Plugin not connected')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const id = this.seq++
|
|
123
|
+
const command = { id, type: 'command', method, params }
|
|
124
|
+
const limit = LONG_TIMEOUT_METHODS.has(method) ? COMMAND_LONG_TIMEOUT_MS : COMMAND_TIMEOUT_MS
|
|
125
|
+
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
const timeout = setTimeout(() => {
|
|
128
|
+
this.pending.delete(id)
|
|
129
|
+
reject(new Error(`Plugin command timeout: ${method}`))
|
|
130
|
+
}, limit)
|
|
131
|
+
|
|
132
|
+
this.pending.set(id, {
|
|
133
|
+
resolve: (value) => {
|
|
134
|
+
clearTimeout(timeout)
|
|
135
|
+
resolve(value)
|
|
136
|
+
},
|
|
137
|
+
reject: (err) => {
|
|
138
|
+
clearTimeout(timeout)
|
|
139
|
+
reject(err)
|
|
140
|
+
},
|
|
141
|
+
timeout
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
this.socket.send(JSON.stringify(command))
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
handleMessage (raw) {
|
|
149
|
+
let msg
|
|
150
|
+
try {
|
|
151
|
+
const text = Buffer.isBuffer(raw)
|
|
152
|
+
? raw.toString()
|
|
153
|
+
: Array.isArray(raw)
|
|
154
|
+
? Buffer.concat(raw).toString()
|
|
155
|
+
: Buffer.from(raw).toString()
|
|
156
|
+
msg = JSON.parse(text)
|
|
157
|
+
} catch {
|
|
158
|
+
this.info('[web] invalid JSON from extension')
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!msg || typeof msg !== 'object') return
|
|
163
|
+
|
|
164
|
+
if (msg.type === 'hello' || msg.type === 'ping') {
|
|
165
|
+
// 记录心跳时间(hello 首连 + ping 周期保活)
|
|
166
|
+
this.lastSeenAt = Date.now()
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (msg.type === 'response') {
|
|
171
|
+
const item = this.pending.get(msg.id)
|
|
172
|
+
if (!item) return
|
|
173
|
+
|
|
174
|
+
this.pending.delete(msg.id)
|
|
175
|
+
if (msg.ok) {
|
|
176
|
+
item.resolve(msg.result)
|
|
177
|
+
} else {
|
|
178
|
+
item.reject(new Error(msg.error || 'Plugin error'))
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** 拒绝所有挂起命令(扩展断开 / server 关闭时调用) */
|
|
184
|
+
rejectAllPending (reason) {
|
|
185
|
+
for (const [, item] of this.pending) {
|
|
186
|
+
clearTimeout(item.timeout)
|
|
187
|
+
item.reject(new Error(reason))
|
|
188
|
+
}
|
|
189
|
+
this.pending.clear()
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = {
|
|
194
|
+
WebPluginServer,
|
|
195
|
+
DEFAULT_HOST,
|
|
196
|
+
DEFAULT_PORT
|
|
197
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// -*- coding: utf-8 -*-
|
|
2
|
+
//
|
|
3
|
+
// service.cjs — 插件路 WebService 业务门面。
|
|
4
|
+
// 完整复刻自 bee/packages/app/browser-op/main/web/service.ts(去 DI、纯 JS)。
|
|
5
|
+
//
|
|
6
|
+
// 职责:转发所有命令到 WebPlater 扩展(callPlugin)+ 维护 WS server + 检测/心跳。
|
|
7
|
+
// 浏览器启停由 index.cjs 的 BrowserOp 类负责(双路由统一)。
|
|
8
|
+
//
|
|
9
|
+
// API 全集(与扩展协议对齐,参数顺序与 TS 原版一致):
|
|
10
|
+
// 生命周期: startServer / stopServer
|
|
11
|
+
// 检测: isConnected / isPluginAlive / checkRoute
|
|
12
|
+
// tab: listTabs / switchTab / closeTab / newTab
|
|
13
|
+
// 导航: goto / goBack / goForward / reload / waitForLoadState / waitForURL
|
|
14
|
+
// 交互: click / dblclick / fill / type / press / hover / mouseMove / focus /
|
|
15
|
+
// check / uncheck / selectOption / dragTo
|
|
16
|
+
// 等待: waitForSelector / waitForFunction / waitForTimeout
|
|
17
|
+
// 读取: innerHTML / innerText / textContent / getAttribute / inputValue /
|
|
18
|
+
// boundingBox / count / status / snapshot
|
|
19
|
+
// 存储: getLocalStorage / setLocalStorage / removeLocalStorage / clearLocalStorage
|
|
20
|
+
// 截图: screenshot
|
|
21
|
+
// cookie: getCookies / setCookies / clearCookies
|
|
22
|
+
// dialog: setDialogHandler
|
|
23
|
+
// 捕获: enableCapture / disableCapture
|
|
24
|
+
// 评估: evaluate
|
|
25
|
+
|
|
26
|
+
'use strict'
|
|
27
|
+
|
|
28
|
+
const { WebPluginServer } = require('./server.cjs')
|
|
29
|
+
const { detectWebplaterExtension } = require('./extension-detect.cjs')
|
|
30
|
+
|
|
31
|
+
const BROWSER_PROFILES_DEFAULT = 'browser/profiles'
|
|
32
|
+
|
|
33
|
+
class WebService {
|
|
34
|
+
constructor (deps = {}) {
|
|
35
|
+
this.info = deps.info || (() => {})
|
|
36
|
+
this.errorLog = deps.errorLog || (() => {})
|
|
37
|
+
/** profileRoot:WebPlater 扩展安装位置。由调用方注入(index.cjs 注入 userDataDir 的父目录) */
|
|
38
|
+
this.profileRoot = deps.profileRoot || null
|
|
39
|
+
this.server = new WebPluginServer(this.info)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
setProfileRoot (root) {
|
|
43
|
+
this.profileRoot = root
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── 生命周期 ──────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
async startServer () {
|
|
49
|
+
await this.server.start()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async stopServer () {
|
|
53
|
+
await this.server.stop()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 注:浏览器启动/关闭由 index.cjs 的 BrowserOp 类负责。
|
|
57
|
+
// 本 service 只维护 WS server(插件通信)+ 转发命令 + 检测/心跳。
|
|
58
|
+
|
|
59
|
+
// ── 扩展能力 ───────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
get isConnected () {
|
|
62
|
+
return this.server.isConnected
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 插件心跳是否存活 */
|
|
66
|
+
isPluginAlive (thresholdMs) {
|
|
67
|
+
return this.server.isPluginAlive(thresholdMs)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 捕获模式:把 captureInjectJS 字符串下发给插件,插件注入所有 tab + 监听新 tab */
|
|
71
|
+
enableCapture (script) {
|
|
72
|
+
return this.server.callPlugin('enableCapture', { script })
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 捕获模式关闭:插件移除注入 + 关标志位 */
|
|
76
|
+
disableCapture () {
|
|
77
|
+
return this.server.callPlugin('disableCapture', {})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 双路由可用性检测(open 前调用)。
|
|
82
|
+
* 两层判据:启动前扫 profile 看装没装;启动后看心跳。
|
|
83
|
+
* 返回 mode 决策 + 给用户的提示信息。
|
|
84
|
+
*/
|
|
85
|
+
checkRoute () {
|
|
86
|
+
const profileRoot = this.profileRoot
|
|
87
|
+
const detect = profileRoot ? detectWebplaterExtension(profileRoot) : null
|
|
88
|
+
const alive = this.server.isPluginAlive()
|
|
89
|
+
|
|
90
|
+
if (!detect || !detect.installed) {
|
|
91
|
+
this.info('[web] route=cdp (webplater not installed in profile)')
|
|
92
|
+
return {
|
|
93
|
+
mode: 'cdp',
|
|
94
|
+
installed: false,
|
|
95
|
+
disabled: false,
|
|
96
|
+
alive: false,
|
|
97
|
+
notice: {
|
|
98
|
+
level: 'warn',
|
|
99
|
+
message: '未检测到 WebPlater 插件,将使用 CDP 模式。对于淘宝/京东等高危平台,CDP 可能触发风控,建议安装 WebPlater 插件。'
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (detect.disabled) {
|
|
105
|
+
this.info('[web] route=cdp (webplater disabled in profile)')
|
|
106
|
+
return {
|
|
107
|
+
mode: 'cdp',
|
|
108
|
+
installed: true,
|
|
109
|
+
disabled: true,
|
|
110
|
+
alive: false,
|
|
111
|
+
notice: {
|
|
112
|
+
level: 'warn',
|
|
113
|
+
message: 'WebPlater 插件已安装但被禁用,将使用 CDP 模式。请在浏览器扩展页启用 WebPlater。'
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!alive) {
|
|
119
|
+
this.info('[web] route=cdp (webplater installed but no heartbeat)')
|
|
120
|
+
return {
|
|
121
|
+
mode: 'cdp',
|
|
122
|
+
installed: true,
|
|
123
|
+
disabled: false,
|
|
124
|
+
alive: false,
|
|
125
|
+
notice: {
|
|
126
|
+
level: 'error',
|
|
127
|
+
message: 'WebPlater 插件已安装但未响应(心跳缺失)。请重装插件或检查浏览器是否已启动该 profile。'
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this.info('[web] route=extension (webplater alive)')
|
|
133
|
+
return { mode: 'extension', installed: true, disabled: false, alive: true, notice: null }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── tab ──
|
|
137
|
+
|
|
138
|
+
listTabs () { return this.server.callPlugin('listTabs') }
|
|
139
|
+
switchTab (tabId) { return this.server.callPlugin('switchTab', { tabId }) }
|
|
140
|
+
reuseTab (url, urlMatch) { return this.server.callPlugin('reuseTab', { url: url || '', ...(urlMatch ? { urlMatch: urlMatch.source } : {}) }) }
|
|
141
|
+
closeTab (tabId) { return this.server.callPlugin('closeTab', { tabId }) }
|
|
142
|
+
newTab (url) { return this.server.callPlugin('newTab', { url: url || '' }) }
|
|
143
|
+
|
|
144
|
+
// ── 下载 (chrome.downloads API, 原生监听不轮询目录) ──
|
|
145
|
+
listDownloads (limit) { return this.server.callPlugin('listDownloads', { limit: limit || 20 }) }
|
|
146
|
+
getDownload (id) { return this.server.callPlugin('getDownload', { id }) }
|
|
147
|
+
waitForDownload (opts) { return this.server.callPlugin('waitForDownload', opts || {}) }
|
|
148
|
+
|
|
149
|
+
// ── 导航 ──
|
|
150
|
+
|
|
151
|
+
goto (url, tabId) { return this.server.callPlugin('goto', { url, tabId }) }
|
|
152
|
+
goBack (tabId) { return this.server.callPlugin('goBack', { tabId }) }
|
|
153
|
+
goForward (tabId) { return this.server.callPlugin('goForward', { tabId }) }
|
|
154
|
+
reload (tabId) { return this.server.callPlugin('reload', { tabId }) }
|
|
155
|
+
waitForLoadState (state, tabId) { return this.server.callPlugin('waitForLoadState', { state, tabId }) }
|
|
156
|
+
waitForURL (url, timeout, tabId) { return this.server.callPlugin('waitForURL', { url, timeout, tabId }) }
|
|
157
|
+
|
|
158
|
+
status (tabId) { return this.server.callPlugin('status', { tabId }) }
|
|
159
|
+
|
|
160
|
+
// ── 交互 ──
|
|
161
|
+
|
|
162
|
+
click (selector, x, y, tabId) { return this.server.callPlugin('click', { selector, x, y, tabId }) }
|
|
163
|
+
dblclick (selector, tabId) { return this.server.callPlugin('dblclick', { selector, tabId }) }
|
|
164
|
+
fill (selector, value, tabId) { return this.server.callPlugin('fill', { selector, value, tabId }) }
|
|
165
|
+
type (selector, value, delay, tabId) { return this.server.callPlugin('type', { selector, value, delay, tabId }) }
|
|
166
|
+
press (selector, key, tabId) { return this.server.callPlugin('press', { selector, key, tabId }) }
|
|
167
|
+
hover (selector, x, y, tabId) { return this.server.callPlugin('hover', { selector, x, y, tabId }) }
|
|
168
|
+
mouseMove (x, y, tabId) { return this.server.callPlugin('mouseMove', { x, y, tabId }) }
|
|
169
|
+
focus (selector, tabId) { return this.server.callPlugin('focus', { selector, tabId }) }
|
|
170
|
+
check (selector, tabId) { return this.server.callPlugin('check', { selector, tabId }) }
|
|
171
|
+
uncheck (selector, tabId) { return this.server.callPlugin('uncheck', { selector, tabId }) }
|
|
172
|
+
selectOption (selector, value, tabId) { return this.server.callPlugin('selectOption', { selector, value, tabId }) }
|
|
173
|
+
setInputFiles (selector, files, tabId) { return this.server.callPlugin('setInputFiles', { selector, files, tabId }) }
|
|
174
|
+
dragTo (source, target, tabId) { return this.server.callPlugin('dragTo', { source, target, tabId }) }
|
|
175
|
+
|
|
176
|
+
// ── 坐标/文本点击 + 用户视角定位 (扩展路=合成事件, 不走 CDP/debugger) ──
|
|
177
|
+
// clickAt: 坐标点击 (extension 路 content script 合成事件; CDP 路 page.mouse)
|
|
178
|
+
clickAt (x, y, opts, tabId) { return this.server.callPlugin('clickAt', { x, y, ...opts, tabId }) }
|
|
179
|
+
// locateVisibleText: 定位可见文本返回 bbox
|
|
180
|
+
locateVisibleText (text, exact, index, tabId) { return this.server.callPlugin('locateVisibleText', { text, exact, index, tabId }) }
|
|
181
|
+
// clickByText: 定位文本后立即点击 (content script 原子)
|
|
182
|
+
clickByText (p, tabId) { return this.server.callPlugin('clickByText', { ...p, tabId }) }
|
|
183
|
+
// operateSequence: 原子序列执行
|
|
184
|
+
operateSequence (steps, tabId) { return this.server.callPlugin('operateSequence', { steps, tabId }) }
|
|
185
|
+
|
|
186
|
+
// ── 等待 ──
|
|
187
|
+
|
|
188
|
+
waitForSelector (selector, timeout, tabId) { return this.server.callPlugin('waitForSelector', { selector, timeout, tabId }) }
|
|
189
|
+
waitForFunction (source, timeout, args, tabId) { return this.server.callPlugin('waitForFunction', { source, timeout, args, tabId }) }
|
|
190
|
+
waitForTimeout (ms, tabId) { return this.server.callPlugin('waitForTimeout', { ms, tabId }) }
|
|
191
|
+
|
|
192
|
+
// ── 读取 ──
|
|
193
|
+
|
|
194
|
+
evaluate (source, args, tabId, frameId) { return this.server.callPlugin('evaluate', { source, args, tabId, frameId }) }
|
|
195
|
+
listFrames (tabId) { return this.server.callPlugin('listFrames', { tabId }) }
|
|
196
|
+
innerHTML (selector, tabId) { return this.server.callPlugin('innerHTML', { selector, tabId }) }
|
|
197
|
+
innerText (selector, tabId) { return this.server.callPlugin('innerText', { selector, tabId }) }
|
|
198
|
+
textContent (selector, tabId) { return this.server.callPlugin('textContent', { selector, tabId }) }
|
|
199
|
+
getAttribute (selector, name, tabId) { return this.server.callPlugin('getAttribute', { selector, name, tabId }) }
|
|
200
|
+
inputValue (selector, tabId) { return this.server.callPlugin('inputValue', { selector, tabId }) }
|
|
201
|
+
boundingBox (selector, tabId) { return this.server.callPlugin('boundingBox', { selector, tabId }) }
|
|
202
|
+
count (selector, tabId) { return this.server.callPlugin('count', { selector, tabId }) }
|
|
203
|
+
|
|
204
|
+
snapshot (options, tabId) { return this.server.callPlugin('snapshot', { ...options, tabId }) }
|
|
205
|
+
|
|
206
|
+
// ── 存储 ──
|
|
207
|
+
|
|
208
|
+
getLocalStorage (keys, tabId) { return this.server.callPlugin('getLocalStorage', { keys, tabId }) }
|
|
209
|
+
setLocalStorage (items, tabId) { return this.server.callPlugin('setLocalStorage', { items, tabId }) }
|
|
210
|
+
removeLocalStorage (keys, tabId) { return this.server.callPlugin('removeLocalStorage', { keys, tabId }) }
|
|
211
|
+
clearLocalStorage (tabId) { return this.server.callPlugin('clearLocalStorage', { tabId }) }
|
|
212
|
+
|
|
213
|
+
// ── 截图 ──
|
|
214
|
+
|
|
215
|
+
screenshot (tabId) { return this.server.callPlugin('screenshot', { tabId }) }
|
|
216
|
+
|
|
217
|
+
// ── cookie ──
|
|
218
|
+
|
|
219
|
+
getCookies (filter) { return this.server.callPlugin('getCookies', filter || {}) }
|
|
220
|
+
setCookies (params) { return this.server.callPlugin('setCookies', params || { cookies: [] }) }
|
|
221
|
+
clearCookies (filter) { return this.server.callPlugin('clearCookies', filter || {}) }
|
|
222
|
+
|
|
223
|
+
// ── dialog ──
|
|
224
|
+
|
|
225
|
+
setDialogHandler (handlerSource, tabId) { return this.server.callPlugin('setDialogHandler', { handler: handlerSource, tabId }) }
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
module.exports = { WebService, BROWSER_PROFILES_DEFAULT }
|