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.
Files changed (117) hide show
  1. package/README.md +213 -0
  2. package/browser-op/backend/browserd.cjs +1004 -0
  3. package/browser-op/backend/rpc-client.cjs +64 -0
  4. package/browser-op/backend/state.cjs +51 -0
  5. package/browser-op/cdp/capture-inject.js +426 -0
  6. package/browser-op/cdp/capture-inject.ts +426 -0
  7. package/browser-op/cdp/capture-service.cjs +172 -0
  8. package/browser-op/cdp/chrome-launcher.cjs +370 -0
  9. package/browser-op/cdp/chrome-path.cjs +57 -0
  10. package/browser-op/cdp/state.cjs +89 -0
  11. package/browser-op/extension/extension-detect.cjs +228 -0
  12. package/browser-op/extension/server.cjs +197 -0
  13. package/browser-op/extension/service.cjs +228 -0
  14. package/browser-op/extension/state.cjs +78 -0
  15. package/browser-op/index.cjs +389 -0
  16. package/browser-op/package.json +17 -0
  17. package/browser-op/py/behavior.py +138 -0
  18. package/browser-op/py/browser.py +340 -0
  19. package/browser-op/py/captcha.py +115 -0
  20. package/browser-op/py/crawler.py +125 -0
  21. package/browser-op/py/examples/01_open_and_probe.py +48 -0
  22. package/browser-op/py/examples/02_reuse_and_probe.py +66 -0
  23. package/browser-op/py/examples/03_interact.py +66 -0
  24. package/browser-op/py/find.py +150 -0
  25. package/browser-op/py/honeypot.py +73 -0
  26. package/browser-op/py/humanize.py +392 -0
  27. package/browser-op/py/image.py +186 -0
  28. package/browser-op/py/interact.py +193 -0
  29. package/browser-op/py/markdown.py +38 -0
  30. package/browser-op/py/pyproject.toml +32 -0
  31. package/browser-op/py/ready.py +208 -0
  32. package/browser-op/py/scroll.py +180 -0
  33. package/browser-op/py/upload.py +103 -0
  34. package/browser-op/py/visual_target.py +47 -0
  35. package/browser-op/py/visualize.py +91 -0
  36. package/browser-op/state.cjs +63 -0
  37. package/browser-op/web/behavior.js +153 -0
  38. package/browser-op/web/browser.js +231 -0
  39. package/browser-op/web/captcha.js +85 -0
  40. package/browser-op/web/crawler.js +109 -0
  41. package/browser-op/web/find.js +147 -0
  42. package/browser-op/web/honeypot.js +68 -0
  43. package/browser-op/web/humanize.js +522 -0
  44. package/browser-op/web/image.js +177 -0
  45. package/browser-op/web/interact.js +169 -0
  46. package/browser-op/web/markdown.js +80 -0
  47. package/browser-op/web/ready.js +295 -0
  48. package/browser-op/web/scroll.js +167 -0
  49. package/browser-op/web/upload.js +116 -0
  50. package/browser-op/web/visual-runtime.inject.cjs +6 -0
  51. package/browser-op/webplater/.env.example +7 -0
  52. package/browser-op/webplater/ARCHITECTURE.md +102 -0
  53. package/browser-op/webplater/dist/chrome-mv3/assets/popup-BUZEUmsx.css +1 -0
  54. package/browser-op/webplater/dist/chrome-mv3/background.js +2 -0
  55. package/browser-op/webplater/dist/chrome-mv3/capture.js +310 -0
  56. package/browser-op/webplater/dist/chrome-mv3/chunks/_virtual_wxt-html-plugins-DPbbfBKe.js +1 -0
  57. package/browser-op/webplater/dist/chrome-mv3/chunks/offscreen-CFXYw9Mo.js +1 -0
  58. package/browser-op/webplater/dist/chrome-mv3/chunks/popup-C-lpxZZO.js +1 -0
  59. package/browser-op/webplater/dist/chrome-mv3/content-scripts/content.js +7 -0
  60. package/browser-op/webplater/dist/chrome-mv3/manifest.json +1 -0
  61. package/browser-op/webplater/dist/chrome-mv3/offscreen.html +16 -0
  62. package/browser-op/webplater/dist/chrome-mv3/popup.html +31 -0
  63. package/browser-op/webplater/entrypoints/background.ts +938 -0
  64. package/browser-op/webplater/entrypoints/content.ts +1150 -0
  65. package/browser-op/webplater/entrypoints/offscreen/index.html +15 -0
  66. package/browser-op/webplater/entrypoints/offscreen/main.ts +161 -0
  67. package/browser-op/webplater/entrypoints/popup/index.html +29 -0
  68. package/browser-op/webplater/entrypoints/popup/main.ts +61 -0
  69. package/browser-op/webplater/entrypoints/popup/style.css +100 -0
  70. package/browser-op/webplater/lib/snapshot.ts +352 -0
  71. package/browser-op/webplater/package.json +29 -0
  72. package/browser-op/webplater/pnpm-lock.yaml +3411 -0
  73. package/browser-op/webplater/public/capture.js +310 -0
  74. package/browser-op/webplater/scripts/publish-extension.mjs +176 -0
  75. package/browser-op/webplater/tsconfig.json +19 -0
  76. package/browser-op/webplater/wxt.config.ts +34 -0
  77. package/dist/actions.md +102 -0
  78. package/dist/cli.d.ts +2 -0
  79. package/dist/cli.d.ts.map +1 -0
  80. package/dist/cli.js +278 -0
  81. package/dist/cli.js.map +1 -0
  82. package/dist/client.d.ts +94 -0
  83. package/dist/client.d.ts.map +1 -0
  84. package/dist/client.js +277 -0
  85. package/dist/client.js.map +1 -0
  86. package/dist/config.d.ts +61 -0
  87. package/dist/config.d.ts.map +1 -0
  88. package/dist/config.js +119 -0
  89. package/dist/config.js.map +1 -0
  90. package/dist/protocol.d.ts +195 -0
  91. package/dist/protocol.d.ts.map +1 -0
  92. package/dist/protocol.js +11 -0
  93. package/dist/protocol.js.map +1 -0
  94. package/dist/server.d.ts +66 -0
  95. package/dist/server.d.ts.map +1 -0
  96. package/dist/server.js +259 -0
  97. package/dist/server.js.map +1 -0
  98. package/package.json +78 -0
  99. package/schemas/browser.clearCookies.schema.json +13 -0
  100. package/schemas/browser.close.schema.json +9 -0
  101. package/schemas/browser.getCookies.schema.json +13 -0
  102. package/schemas/browser.getDownload.schema.json +15 -0
  103. package/schemas/browser.health.schema.json +9 -0
  104. package/schemas/browser.listDownloads.schema.json +16 -0
  105. package/schemas/browser.listTabs.schema.json +9 -0
  106. package/schemas/browser.newTab.schema.json +15 -0
  107. package/schemas/browser.open.schema.json +15 -0
  108. package/schemas/browser.operate.schema.json +15 -0
  109. package/schemas/browser.reuseTab.schema.json +15 -0
  110. package/schemas/browser.setCookies.schema.json +15 -0
  111. package/schemas/browser.waitFor.schema.json +15 -0
  112. package/schemas/browser.waitForDownload.schema.json +15 -0
  113. package/skills/browser/SKILL.md +110 -0
  114. package/skills/browser/references/collect.md +163 -0
  115. package/skills/browser/references/high-risk.md +161 -0
  116. package/skills/browser/references/operate-actions.md +92 -0
  117. 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 }