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,352 @@
1
+ /**
2
+ * Aria snapshot - 生成 playwright ariaSnapshot() 兼容的 YAML 无障碍树。
3
+ *
4
+ * role 用 aria-query 的 getRole,accessible name 用 dom-accessibility-api,
5
+ * 尽量接近 Chromium 原生无障碍树。
6
+ *
7
+ * 同时维护 ref(e1, e2...)→ Element 映射,供 *Ref 动作按 ref 定位。
8
+ */
9
+ import { getRole, computeAccessibleName } from 'dom-accessibility-api'
10
+
11
+ /** 全局 ref → Element 映射。每次 snapshot 重建。 */
12
+ const refToElement = new Map<string, Element>()
13
+ let refSeq = 0
14
+
15
+ export function resetRefs(): void {
16
+ // 清理上一次的 DOM 属性,防止旧 ref 残留污染新映射(重扫后 refSeq 从头计)
17
+ document
18
+ .querySelectorAll('[data-bee-ref]')
19
+ .forEach((el) => el.removeAttribute('data-bee-ref'))
20
+ refToElement.clear()
21
+ refSeq = 0
22
+ }
23
+
24
+ export function getElementByRef(ref: string): Element | undefined {
25
+ const cached = refToElement.get(ref)
26
+ if (cached && document.contains(cached)) return cached
27
+ // fallback:DOM 属性。MAIN world 写入、或缓存元素 detach 后重附着的兜底。
28
+ // data-bee-ref 跨 world 共享,是插件能用的最接近 playwright backendDOMNodeId 的机制。
29
+ const byAttr = document.querySelector(`[data-bee-ref="${ref}"]`)
30
+ if (byAttr) {
31
+ refToElement.set(ref, byAttr)
32
+ return byAttr
33
+ }
34
+ return undefined
35
+ }
36
+
37
+ // ── 节点数据结构 ──────────────────────────────────────────
38
+
39
+ interface SnapshotNode {
40
+ role: string
41
+ name?: string
42
+ attributes?: string[]
43
+ ref: string
44
+ value?: string
45
+ children?: SnapshotNode[]
46
+ }
47
+
48
+ // ── 属性提取(对齐 playwright 的 [attr=value] 集合)──────
49
+
50
+ const HEADING_LEVELS: Record<string, number> = {
51
+ H1: 1, H2: 2, H3: 3, H4: 4, H5: 5, H6: 6,
52
+ }
53
+
54
+ /** 提取 playwright snapshot 暴露的属性:checked/disabled/expanded/invalid/level/pressed/selected */
55
+ function extractAttributes(el: Element, role: string | null): string[] {
56
+ const attrs: string[] = []
57
+
58
+ const disabled =
59
+ (el as HTMLElement).hasAttribute?.('disabled') ||
60
+ el.getAttribute('aria-disabled') === 'true'
61
+ if (disabled) attrs.push('disabled')
62
+
63
+ const ariaChecked = el.getAttribute('aria-checked')
64
+ if (role === 'checkbox' || role === 'radio' || role === 'menuitemcheckbox' || role === 'menuitemradio' || role === 'switch' || role === 'option' || ariaChecked != null) {
65
+ if (ariaChecked === 'true' || ((el as HTMLInputElement).checked === true && ariaChecked == null)) {
66
+ attrs.push('checked')
67
+ } else if (ariaChecked === 'mixed') {
68
+ attrs.push('checked=mixed')
69
+ }
70
+ }
71
+
72
+ const ariaExpanded = el.getAttribute('aria-expanded')
73
+ if (ariaExpanded != null) {
74
+ attrs.push(ariaExpanded === 'true' ? 'expanded' : 'expanded=false')
75
+ }
76
+
77
+ const ariaPressed = el.getAttribute('aria-pressed')
78
+ if (ariaPressed != null) {
79
+ attrs.push(ariaPressed === 'true' ? 'pressed' : 'pressed=false')
80
+ }
81
+
82
+ const ariaSelected = el.getAttribute('aria-selected')
83
+ if (ariaSelected === 'true') attrs.push('selected')
84
+
85
+ const ariaInvalid = el.getAttribute('aria-invalid')
86
+ if (ariaInvalid === 'true' || ariaInvalid === '') attrs.push('invalid')
87
+ else if (ariaInvalid === 'grammar') attrs.push('invalid=grammar')
88
+ else if (ariaInvalid === 'spelling') attrs.push('invalid=spelling')
89
+
90
+ if (role === 'heading') {
91
+ let level = Number(el.getAttribute('aria-level'))
92
+ if (!Number.isFinite(level) || level < 1) {
93
+ level = HEADING_LEVELS[el.tagName] ?? 0
94
+ }
95
+ if (level > 0) attrs.push(`level=${level}`)
96
+ }
97
+
98
+ return attrs
99
+ }
100
+
101
+ // ── 可见性 / 隐藏判断 ────────────────────────────────────
102
+
103
+ function isHidden(el: Element): boolean {
104
+ if (el.getAttribute('aria-hidden') === 'true') return true
105
+ const style = (el as HTMLElement).style
106
+ if (style?.display === 'none' || style?.visibility === 'hidden') return true
107
+ if (el.hasAttribute('hidden')) return true
108
+ return false
109
+ }
110
+
111
+ // role 为这些时,其文本内容作为子节点(容器),否则作为叶子
112
+ const CONTAINER_ROLES = new Set([
113
+ 'application', 'article', 'banner', 'complementary', 'contentinfo',
114
+ 'dialog', 'document', 'feed', 'figure', 'form', 'group', 'main',
115
+ 'navigation', 'region', 'search', 'section', 'table', 'rowgroup',
116
+ 'row', 'cell', 'columnheader', 'rowheader', 'list', 'listitem',
117
+ 'menu', 'menubar', 'toolbar', 'tablist', 'tabpanel', 'tree',
118
+ 'treegrid', 'treegroup', 'radiogroup', 'grid', 'combobox',
119
+ ])
120
+
121
+ // 叶子 role:不递归子节点,文本直接作为 name 或 value
122
+ const LEAF_ROLES = new Set([
123
+ 'button', 'link', 'textbox', 'checkbox', 'radio', 'switch',
124
+ 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option',
125
+ 'tab', 'heading', 'separator', 'slider', 'spinbutton', 'progressbar',
126
+ 'img', 'alert', 'status', 'note', 'tooltip', 'scrollbar',
127
+ ])
128
+
129
+ /** 可交互 role:interactiveOnly 模式只保留这些 */
130
+ const INTERACTIVE_ROLES = new Set([
131
+ 'button', 'link', 'textbox', 'checkbox', 'radio', 'switch',
132
+ 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'tab',
133
+ 'combobox', 'menu', 'menubar', 'listbox', 'treeitem', 'tree',
134
+ 'slider', 'spinbutton', 'searchbox', 'gridcell', 'row',
135
+ 'toolbar', 'tablist',
136
+ ])
137
+
138
+ interface BuildCtx {
139
+ depth?: number
140
+ interactiveOnly?: boolean
141
+ }
142
+
143
+ /** 判断元素是否“可交互”(用于 interactiveOnly 过滤) */
144
+ function isInteractive(el: Element, role: string | null): boolean {
145
+ if (role && INTERACTIVE_ROLES.has(role)) return true
146
+ if ((el as HTMLElement).onclick != null) return true
147
+ if ((el as HTMLElement).style?.cursor === 'pointer') return true
148
+ const tag = el.tagName
149
+ if (tag === 'A' || tag === 'BUTTON' || tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA') return true
150
+ return false
151
+ }
152
+
153
+ /** 元素的"直接文本"(用于无 role 但有文本的 text 节点)。超长截断,防止 inline script/style 喜炸。 */
154
+ function directText(el: Element): string {
155
+ let text = ''
156
+ for (const node of Array.from(el.childNodes)) {
157
+ if (node.nodeType === Node.TEXT_NODE) {
158
+ text += (node.textContent || '') + ' '
159
+ }
160
+ }
161
+ text = text.replace(/\s+/g, ' ').trim()
162
+ if (text.length > 200) text = text.slice(0, 200) + '...'
163
+ return text
164
+ }
165
+
166
+ function nextRef(el: Element): string {
167
+ const ref = `e${++refSeq}`
168
+ refToElement.set(ref, el)
169
+ // 打 DOM 属性:让 MAIN world 的 evaluate 也能用 aria-ref=eN 定位同一元素,
170
+ // 对齐 playwright ref 的“上下文无关”特性。
171
+ try {
172
+ el.setAttribute('data-bee-ref', ref)
173
+ } catch {
174
+ // 忽略只读 / SVG 等无法 setAttribute 的元素
175
+ }
176
+ return ref
177
+ }
178
+
179
+ // ── 树构建 ────────────────────────────────────────────────
180
+
181
+ /** 这些标签不产生无障碍信息,playwright snapshot 永不输出 */
182
+ const IGNORED_TAGS = new Set([
183
+ 'SCRIPT', 'STYLE', 'NOSCRIPT', 'TEMPLATE', 'LINK', 'META',
184
+ 'TITLE', 'BASE', 'HEAD', 'COL', 'COLGROUP',
185
+ ])
186
+
187
+ function buildNode(el: Element, ctx: BuildCtx, level = 0): SnapshotNode | null {
188
+ if (isHidden(el)) return null
189
+ if (IGNORED_TAGS.has(el.tagName)) return null
190
+ const atMaxDepth = ctx.depth != null && level >= ctx.depth
191
+
192
+ const role = getRole(el as HTMLElement) as string | null
193
+ const name = computeAccessibleName(el).trim() || undefined
194
+
195
+ // 有 role 的情况
196
+ if (role) {
197
+ const attributes = extractAttributes(el, role)
198
+ const ref = nextRef(el)
199
+
200
+ // textbox:value 作为叶子值
201
+ if (role === 'textbox') {
202
+ const value = (el as HTMLInputElement).value || ''
203
+ return { role, name, attributes: attributes.length ? attributes : undefined, ref, value: value || undefined }
204
+ }
205
+
206
+ // 叶子 role
207
+ if (LEAF_ROLES.has(role)) {
208
+ return { role, name, attributes: attributes.length ? attributes : undefined, ref }
209
+ }
210
+
211
+ // 容器 role 或其它:递归子元素
212
+ let children: SnapshotNode[] = []
213
+ if (!atMaxDepth) {
214
+ for (const child of Array.from(el.children)) {
215
+ const node = buildNode(child, ctx, level + 1)
216
+ if (node) children.push(node)
217
+ }
218
+ }
219
+ // 容器内还可能有直接文本
220
+ if (children.length === 0 && name) {
221
+ // 纯命名容器,作为叶子
222
+ return { role, name, attributes: attributes.length ? attributes : undefined, ref }
223
+ }
224
+ return { role, name, attributes: attributes.length ? attributes : undefined, ref, children: children.length ? children : undefined }
225
+ }
226
+
227
+ // 无 role:可能是有文本的纯容器(div/p/span 等)
228
+ const text = directText(el)
229
+ if (!text) {
230
+ // 无文本容器:递归子元素,把它们“提升”
231
+ const lifted: SnapshotNode[] = []
232
+ if (!atMaxDepth) {
233
+ for (const child of Array.from(el.children)) {
234
+ const node = buildNode(child, ctx, level + 1)
235
+ if (node) lifted.push(node)
236
+ }
237
+ }
238
+ if (lifted.length === 1) return lifted[0]
239
+ if (lifted.length > 1) {
240
+ return { role: 'text', ref: nextRef(el), children: lifted }
241
+ }
242
+ return null
243
+ }
244
+
245
+ // 有文本、无 role:生成 text 节点
246
+ return { role: 'text', ref: nextRef(el), name: text }
247
+ }
248
+
249
+ // ── YAML 序列化(对齐 playwright aria snapshot 格式)────
250
+
251
+ function quoteIfNeeded(name: string): string {
252
+ // playwright: 精确值用双引号;正则用 /.../;这里 snapshot 输出总是精确值
253
+ if (!name) return ''
254
+ let n = name
255
+ if (n.length > 200) n = n.slice(0, 200) + '...'
256
+ return ` "${n.replace(/"/g, '\\"')}"`
257
+ }
258
+
259
+ function serializeNode(node: SnapshotNode, indent: number): string {
260
+ const pad = ' '.repeat(indent)
261
+ const parts: string[] = []
262
+
263
+ // 头:- role "name" [attrs] [ref]
264
+ let line = `${pad}- ${node.role}`
265
+ if (node.name) line += quoteIfNeeded(node.name)
266
+ if (node.attributes?.length) {
267
+ for (const a of node.attributes) line += ` [${a}]`
268
+ }
269
+ line += ` [ref=${node.ref}]`
270
+
271
+ const hasChildren = node.children && node.children.length > 0
272
+ const hasValue = node.value !== undefined
273
+
274
+ if (hasChildren) {
275
+ line += ':'
276
+ parts.push(line)
277
+ for (const child of node.children!) {
278
+ parts.push(serializeNode(child, indent + 1))
279
+ }
280
+ } else if (hasValue) {
281
+ line += `: ${node.value}`
282
+ parts.push(line)
283
+ } else {
284
+ parts.push(line)
285
+ }
286
+ return parts.join('\n')
287
+ }
288
+
289
+ // ── 主入口 ────────────────────────────────────────────────
290
+
291
+ export interface SnapshotOptions {
292
+ /** 根元素 selector,不传则整个 body */
293
+ rootSelector?: string
294
+ /** 限制深度 */
295
+ depth?: number
296
+ /** 只保留可交互元素(button/link/textbox 等) */
297
+ interactiveOnly?: boolean
298
+ }
299
+
300
+ /** 生成的完整 yaml 与其分块(按行 + 字数切,不在行中间断)。同一份 snapshot 的分块共享 ref 映射。 */
301
+ export interface SnapshotResult {
302
+ yaml: string
303
+ refs: number
304
+ }
305
+
306
+ /** 生成完整 snapshot。分块由调用方(content)缓存后按 chunkId 切。 */
307
+ export function ariaSnapshot(opts: SnapshotOptions = {}): SnapshotResult {
308
+ resetRefs()
309
+
310
+ const root: Element = opts.rootSelector
311
+ ? document.querySelector(opts.rootSelector) || document.body
312
+ : document.body
313
+
314
+ const ctx: BuildCtx = { depth: opts.depth, interactiveOnly: opts.interactiveOnly }
315
+
316
+ const children: SnapshotNode[] = []
317
+ for (const child of Array.from(root.children)) {
318
+ const node = buildNode(child, ctx, 0)
319
+ if (node) children.push(node)
320
+ }
321
+
322
+ let yaml: string
323
+ if (opts.interactiveOnly) {
324
+ // interactiveOnly:从完整树中提取可交互叶子,保留路径但压缩
325
+ const interactive: SnapshotNode[] = []
326
+ for (const c of children) collectInteractive(c, interactive)
327
+ yaml = interactive.map((n) => serializeNode(n, 0)).join('\n')
328
+ } else if (children.length === 1) {
329
+ yaml = serializeNode(children[0], 0)
330
+ } else if (children.length > 1) {
331
+ yaml = children.map((c) => serializeNode(c, 0)).join('\n')
332
+ } else {
333
+ yaml = ''
334
+ }
335
+
336
+ return { yaml, refs: refSeq }
337
+ }
338
+
339
+ /** 递归收集可交互节点(interactiveOnly 用)。纯展示节点跳过,保留可交互的。 */
340
+ function collectInteractive(node: SnapshotNode, out: SnapshotNode[]): void {
341
+ if (isInteractiveRole(node.role)) {
342
+ out.push(node)
343
+ return
344
+ }
345
+ if (node.children) {
346
+ for (const c of node.children) collectInteractive(c, out)
347
+ }
348
+ }
349
+
350
+ function isInteractiveRole(role: string): boolean {
351
+ return INTERACTIVE_ROLES.has(role) || role === 'textbox'
352
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "webplater",
3
+ "version": "0.0.1",
4
+ "description": "Browser-agent extension — Playwright alternative layer for web automation",
5
+ "private": true,
6
+ "type": "module",
7
+ "packageManager": "pnpm@9.15.0",
8
+ "engines": {
9
+ "node": ">=22.0.0"
10
+ },
11
+ "scripts": {
12
+ "dev": "wxt",
13
+ "dev:firefox": "wxt -b firefox",
14
+ "build": "wxt build",
15
+ "build:firefox": "wxt build -b firefox",
16
+ "zip": "wxt zip",
17
+ "zip:firefox": "wxt zip -b firefox",
18
+ "publish": "node scripts/publish-extension.mjs",
19
+ "compile": "tsc --noEmit",
20
+ "postinstall": "wxt prepare"
21
+ },
22
+ "dependencies": {
23
+ "dom-accessibility-api": "^0.7.0"
24
+ },
25
+ "devDependencies": {
26
+ "typescript": "^5.7.2",
27
+ "wxt": "^0.19.24"
28
+ }
29
+ }