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,64 @@
1
+ // rpc-client.cjs — browserd 的 HTTP IPC client
2
+ //
3
+ // client 层: 业务脚本通过这个模块和 browserd 通信。
4
+ // 所有方法返回 Promise, 错误时 throw。
5
+
6
+ 'use strict'
7
+
8
+ const IPC_PORT = 17322
9
+ const IPC_HOST = '127.0.0.1'
10
+
11
+ let _reqId = 0
12
+
13
+ /** 发一个 JSON-RPC 请求到 browserd, 返回 result 或 throw error */
14
+ function rpc (method, params = {}, timeoutMs = 60000) {
15
+ return new Promise((resolve, reject) => {
16
+ const id = ++_reqId
17
+ const body = JSON.stringify({ id, method, params })
18
+ const http = require('node:http')
19
+
20
+ const req = http.request(
21
+ { hostname: IPC_HOST, port: IPC_PORT, path: '/rpc', method: 'POST',
22
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
23
+ timeout: timeoutMs },
24
+ (res) => {
25
+ let data = ''
26
+ res.on('data', (c) => { data += c })
27
+ res.on('end', () => {
28
+ try {
29
+ const json = JSON.parse(data)
30
+ if (json.ok) resolve(json.result)
31
+ else reject(new Error(json.error || 'rpc failed'))
32
+ } catch (e) {
33
+ reject(new Error(`rpc bad response: ${data.slice(0, 200)}`))
34
+ }
35
+ })
36
+ }
37
+ )
38
+ req.on('timeout', () => { req.destroy(); reject(new Error(`rpc timeout: ${method} (${timeoutMs}ms)`)) })
39
+ req.on('error', (e) => reject(e))
40
+ req.write(body)
41
+ req.end()
42
+ })
43
+ }
44
+
45
+ /** GET /health, 返回 health 对象或 null(连不上) */
46
+ function health (timeoutMs = 2000) {
47
+ return new Promise((resolve) => {
48
+ const http = require('node:http')
49
+ const req = http.get(
50
+ { hostname: IPC_HOST, port: IPC_PORT, path: '/health', timeout: timeoutMs },
51
+ (res) => {
52
+ let data = ''
53
+ res.on('data', (c) => { data += c })
54
+ res.on('end', () => {
55
+ try { resolve(JSON.parse(data)) } catch { resolve(null) }
56
+ })
57
+ }
58
+ )
59
+ req.on('timeout', () => { req.destroy(); resolve(null) })
60
+ req.on('error', () => resolve(null))
61
+ })
62
+ }
63
+
64
+ module.exports = { rpc, health, IPC_PORT, IPC_HOST }
@@ -0,0 +1,51 @@
1
+ // state.cjs — browserd 后端状态文件管理
2
+ //
3
+ // 记录 browserd 进程信息, 供 client 发现和诊断。
4
+ // 文件位置: <userDataDir>/.browserd.json
5
+
6
+ 'use strict'
7
+
8
+ const fs = require('node:fs')
9
+ const path = require('node:path')
10
+
11
+ const STATE_FILENAME = '.browserd.json'
12
+
13
+ function statePath (userDataDir) {
14
+ return path.join(userDataDir, STATE_FILENAME)
15
+ }
16
+
17
+ function read (userDataDir) {
18
+ try {
19
+ const p = statePath(userDataDir)
20
+ if (!fs.existsSync(p)) return null
21
+ return JSON.parse(fs.readFileSync(p, 'utf8'))
22
+ } catch {
23
+ return null
24
+ }
25
+ }
26
+
27
+ function write (userDataDir, info) {
28
+ try {
29
+ const p = statePath(userDataDir)
30
+ fs.mkdirSync(path.dirname(p), { recursive: true })
31
+ fs.writeFileSync(p, JSON.stringify({
32
+ pid: info.pid || process.pid,
33
+ ipcPort: info.ipcPort || 17322,
34
+ extensionWsPort: info.extensionWsPort || 17321,
35
+ sessionId: info.sessionId || null,
36
+ profile: info.profile || 'default',
37
+ mode: info.mode || null,
38
+ chromePid: info.chromePid || null,
39
+ updatedAt: Date.now()
40
+ }, null, 2))
41
+ } catch { /* ignore */ }
42
+ }
43
+
44
+ function clear (userDataDir) {
45
+ try {
46
+ const p = statePath(userDataDir)
47
+ if (fs.existsSync(p)) fs.unlinkSync(p)
48
+ } catch { /* ignore */ }
49
+ }
50
+
51
+ module.exports = { STATE_FILENAME, statePath, read, write, clear }
@@ -0,0 +1,426 @@
1
+ /**
2
+ * Capture Inject JS — 注入到浏览器页面的捕获脚本
3
+ *
4
+ * 行为:
5
+ * - 鼠标悬浮:蓝色高亮框
6
+ * - Ctrl+C:复制当前高亮元素信息到剪贴板(BeeCapture 格式)
7
+ *
8
+ * 剪贴板写入三层(PRD §5):
9
+ * - text/plain: label(如 "DOM: button.submit")
10
+ * - text/html: <a data-bee-capture="..." data-bee-source="browser" ...>label</a>
11
+ * - web application/vnd.bee.capture+json: 完整 BeeCapture JSON(可选增强,try-catch)
12
+ *
13
+ * 注入方式:
14
+ * 1. Page.addScriptToEvaluateOnNewDocument — 新页面自动执行
15
+ * 2. Runtime.evaluate — 当前已加载页面立即执行
16
+ *
17
+ * installed 标志位在安装成功后设置,防止 addEventListener 重复注册。
18
+ *
19
+ * 注意:此脚本是独立字符串,运行在第三方网页上下文,不能 import @shared/bee-capture。
20
+ * BeeCapture 编码逻辑在此内联实现,与 src/shared/bee-capture.ts 的 encodeBeeCaptureHtml 对齐。
21
+ */
22
+
23
+ module.exports.captureInjectJS = `(function() {
24
+ if (window.__bee_capture_installed) return;
25
+ window.__bee_capture_enabled = true;
26
+
27
+ // ─── BeeCapture 编码(与 @shared/bee-capture.ts 对齐)──────────
28
+
29
+ var BEE_MIME = 'web application/vnd.bee.capture+json';
30
+
31
+ function escapeHtml(s) {
32
+ return String(s)
33
+ .replace(/&/g, '&amp;')
34
+ .replace(/"/g, '&quot;')
35
+ .replace(/</g, '&lt;')
36
+ .replace(/>/g, '&gt;');
37
+ }
38
+
39
+ /**
40
+ * 构造 label:DOM: {tag}{#id}{.class1.class2}
41
+ * 单行,无换行(chat 渲染要求)。
42
+ */
43
+ function buildLabel(el) {
44
+ // 优先用元素文本内容(更有可读性),截断到 40 字符
45
+ var text = (el.textContent || '').trim().replace(/\\s+/g, ' ').slice(0, 40);
46
+ if (text) return text;
47
+ // 无文本时降级为 tag + id/class
48
+ var tag = el.tagName ? el.tagName.toLowerCase() : 'element';
49
+ var parts = [tag];
50
+ if (el.id) parts.push('#' + el.id);
51
+ if (el.className && typeof el.className === 'string') {
52
+ var classes = el.className.split(/\\s+/).filter(Boolean);
53
+ if (classes.length) parts.push('.' + classes.join('.'));
54
+ }
55
+ return 'DOM: ' + parts.join('');
56
+ }
57
+
58
+ /**
59
+ * 生成稳定的 CSS selector(nth-child 路径,不依赖随机 class/id)。
60
+ * 从元素向上遍历到 body,每级用 tag:nth-child(index)。
61
+ * 这是在页面结构不变时的确定性定位依据,AI 可直接 page.locator()。
62
+ */
63
+ function buildStableSelector(el) {
64
+ var path = [];
65
+ var node = el;
66
+ var MAX_DEPTH = 15;
67
+ var depth = 0;
68
+ while (node && node.nodeType === 1 && node.tagName && depth < MAX_DEPTH) {
69
+ var tag = node.tagName.toLowerCase();
70
+ // html/body 作为起点,不用 nth-child
71
+ if (tag === 'html') break;
72
+ if (tag === 'body') { path.unshift('body'); break; }
73
+
74
+ // 计算 nth-child index(在父元素的子元素中的位置,元素节点)
75
+ var parent = node.parentElement;
76
+ if (!parent) { path.unshift(tag); break; }
77
+ var index = 1;
78
+ var sibling = parent.firstElementChild;
79
+ while (sibling && sibling !== node) {
80
+ sibling = sibling.nextElementSibling;
81
+ index++;
82
+ }
83
+ // 如果父元素有稳定的 id,直接用 #id 作为终点(更短且稳定)
84
+ if (parent.id && /^[A-Za-z][\w-]*$/.test(parent.id)) {
85
+ path.unshift(tag + ':nth-child(' + index + ')');
86
+ path.unshift('#' + parent.id);
87
+ break;
88
+ }
89
+ path.unshift(tag + ':nth-child(' + index + ')');
90
+ node = parent;
91
+ depth++;
92
+ }
93
+ return path.join(' > ');
94
+ }
95
+
96
+ /**
97
+ * 提取 snapshot 友好的无障碍信息(让 AI 能在 snapshot 树里快速匹配)。
98
+ */
99
+ function buildA11yInfo(el) {
100
+ var role = el.getAttribute('role') || '';
101
+ var ariaLabel = el.getAttribute('aria-label') || '';
102
+ var ariaLabelledBy = el.getAttribute('aria-labelledby') || '';
103
+ var ariaDescribedBy = el.getAttribute('aria-describedby') || '';
104
+
105
+ // heading level
106
+ var headingLevel = 0;
107
+ var tagLower = (el.tagName || '').toLowerCase();
108
+ var hMatch = tagLower.match(/^(\d)$/);
109
+ if (hMatch) headingLevel = parseInt(hMatch[1], 10);
110
+ if (!headingLevel && role === 'heading') {
111
+ var ariaLevel = el.getAttribute('aria-level');
112
+ if (ariaLevel) headingLevel = parseInt(ariaLevel, 10);
113
+ }
114
+
115
+ // 隐式 role 推断:显式 role 优先,否则从 tag/type/href 推断
116
+ if (!role) {
117
+ role = inferImplicitRole(el, tagLower);
118
+ }
119
+
120
+ // 无障碍名称:优先 aria-label,否则 textContent 前 40
121
+ var name = ariaLabel || ((el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 40));
122
+
123
+ return {
124
+ role: role,
125
+ name: name,
126
+ ariaLabel: ariaLabel,
127
+ ariaLabelledBy: ariaLabelledBy,
128
+ ariaDescribedBy: ariaDescribedBy,
129
+ headingLevel: headingLevel || undefined,
130
+ };
131
+ }
132
+
133
+ /**
134
+ * 从 tag/type/href 推断隐式 ARIA role。
135
+ * 与浏览器无障碍树映射一致,让 snapshot 能匹配。
136
+ */
137
+ function inferImplicitRole(el, tagLower) {
138
+ // 有 href 的 a → link
139
+ if (tagLower === 'a' && el.getAttribute('href')) return 'link';
140
+ // 无 href 的 a → generic(现代浏览器)
141
+ if (tagLower === 'a') return 'generic';
142
+ // 直接映射
143
+ var directMap = {
144
+ button: 'button',
145
+ nav: 'navigation',
146
+ main: 'main',
147
+ header: 'banner',
148
+ footer: 'contentinfo',
149
+ aside: 'complementary',
150
+ form: 'form',
151
+ search: 'search',
152
+ ul: 'list',
153
+ ol: 'list',
154
+ li: 'listitem',
155
+ table: 'table',
156
+ tr: 'row',
157
+ td: 'cell',
158
+ th: 'columnheader',
159
+ img: 'image',
160
+ figure: 'figure',
161
+ dialog: 'dialog',
162
+ };
163
+ if (directMap[tagLower]) return directMap[tagLower];
164
+ // h1-h6 → heading
165
+ if (/^h[1-6]$/.test(tagLower)) return 'heading';
166
+ // input 根据类型
167
+ if (tagLower === 'input') {
168
+ var inputType = (el.getAttribute('type') || 'text').toLowerCase();
169
+ if (inputType === 'button' || inputType === 'submit' || inputType === 'reset') return 'button';
170
+ if (inputType === 'checkbox') return 'checkbox';
171
+ if (inputType === 'radio') return 'radio';
172
+ if (inputType === 'range') return 'slider';
173
+ if (inputType === 'search') return 'searchbox';
174
+ return 'textbox';
175
+ }
176
+ if (tagLower === 'textarea') return 'textbox';
177
+ if (tagLower === 'select') return 'listbox';
178
+ // 无隐式 role
179
+ return '';
180
+ }
181
+
182
+ /**
183
+ * 提取最近的有语义的祖先上下文(让 AI 能区分同级相似元素)。
184
+ * 向上查找最近的有 role / heading / id / nav / main / section / 语义 class 的祖先。
185
+ */
186
+ function buildParentContext(el) {
187
+ var contexts = [];
188
+ var node = el.parentElement;
189
+ var depth = 0;
190
+ // 语义化 class 关键词(命中其一即视为有语义)
191
+ var semClassRe = /(?:^|[\s_-])(nav|menu|header|footer|sidebar|content|main|body|article|post|list|item|card|product|detail|search|login|user|cart|banner|hero|modal|dialog|popup|tab|panel|section|category|breadcrumb|pagination|comment|reply|form|register|checkout|profile|setting)(?:[\s_-]|$)/i;
192
+ while (node && depth < 8) {
193
+ var role = node.getAttribute && node.getAttribute('role');
194
+ var nodeTag = (node.tagName || '').toLowerCase();
195
+ var nodeClass = (typeof node.className === 'string' ? node.className : '');
196
+ var nodeText = (node.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 30);
197
+ var hasSemanticClass = semClassRe.test(nodeClass);
198
+ var isSemantic = role || /^h[1-6]$/.test(nodeTag) ||
199
+ ['nav', 'main', 'header', 'footer', 'section', 'article', 'aside', 'form', 'dialog', 'ul', 'ol', 'table'].indexOf(nodeTag) >= 0 ||
200
+ node.id || hasSemanticClass;
201
+ if (isSemantic) {
202
+ contexts.push({
203
+ tag: nodeTag,
204
+ role: role || inferImplicitRole(node, nodeTag),
205
+ id: node.id || '',
206
+ className: nodeClass,
207
+ text: nodeText,
208
+ });
209
+ if (contexts.length >= 3) break; // 最多取 3 级上下文
210
+ }
211
+ node = node.parentElement;
212
+ depth++;
213
+ }
214
+ return contexts;
215
+ }
216
+
217
+ /**
218
+ * 构造 BeeCapture 对象(source='browser', kind='dom-element')。
219
+ */
220
+ function buildCapture(el) {
221
+ var tag = el.tagName ? el.tagName.toLowerCase() : '';
222
+ var attrs = {};
223
+ if (el.attributes) {
224
+ for (var i = 0; i < el.attributes.length; i++) {
225
+ var a = el.attributes[i];
226
+ attrs[a.name] = a.value;
227
+ }
228
+ }
229
+ var selector = el.id ? '#' + el.id : tag;
230
+ if (!el.id && el.className && typeof el.className === 'string') {
231
+ var classes = el.className.split(/\\s+/).filter(Boolean);
232
+ if (classes.length) selector += '.' + classes.join('.');
233
+ }
234
+ var text = (el.textContent || '').trim().slice(0, 200);
235
+ return {
236
+ v: 1,
237
+ source: 'browser',
238
+ kind: 'dom-element',
239
+ label: buildLabel(el),
240
+ preview: text ? text.slice(0, 80) : undefined,
241
+ data: {
242
+ tag: tag,
243
+ id: el.id || '',
244
+ className: typeof el.className === 'string' ? el.className : '',
245
+ text: text,
246
+ selector: selector,
247
+ // 稳定的确定性定位依据(nth-child 路径),AI 可直接 page.locator() 使用
248
+ stableSelector: buildStableSelector(el),
249
+ attributes: attrs,
250
+ a11y: buildA11yInfo(el),
251
+ parentContext: buildParentContext(el),
252
+ url: location.href,
253
+ title: document.title,
254
+ },
255
+ };
256
+ }
257
+
258
+ /**
259
+ * 编码为 text/html 的 <a data-bee-*> 片段(与 encodeBeeCaptureHtml 对齐)。
260
+ */
261
+ function encodeCaptureHtml(capture, refId) {
262
+ var dataStr = encodeURIComponent(JSON.stringify(capture.data));
263
+ var previewAttr = capture.preview
264
+ ? ' data-bee-preview="' + escapeHtml(capture.preview) + '"'
265
+ : '';
266
+ return '<a data-bee-capture="' + escapeHtml(refId) + '"' +
267
+ ' data-bee-source="' + escapeHtml(capture.source) + '"' +
268
+ ' data-bee-kind="' + escapeHtml(capture.kind) + '"' +
269
+ ' data-bee-label="' + escapeHtml(capture.label) + '"' +
270
+ ' data-bee-data="' + escapeHtml(dataStr) + '"' +
271
+ previewAttr + '>' + escapeHtml(capture.label) + '</a>';
272
+ }
273
+
274
+ /**
275
+ * 写三层剪贴板。
276
+ * 优先 navigator.clipboard.write(含 text/html + 自定义 MIME + text/plain),
277
+ * 失败降级 execCommand('copy')(同步选区拷贝,非安全上下文下最可靠),
278
+ * 再失败降级 navigator.clipboard.writeText(仅文本)。
279
+ */
280
+ function writeCaptureToClipboard(capture) {
281
+ var refId = 'browser_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8);
282
+ var html = encodeCaptureHtml(capture, refId);
283
+ var plainText = JSON.stringify(capture, null, 2);
284
+ var jsonStr = JSON.stringify(capture);
285
+
286
+ // 优先:Clipboard API 写多格式(text/html + text/plain + 自定义 MIME)
287
+ if (navigator.clipboard && navigator.clipboard.write && window.ClipboardItem) {
288
+ try {
289
+ var items = {
290
+ 'text/html': new Blob([html], { type: 'text/html' }),
291
+ 'text/plain': new Blob([plainText], { type: 'text/plain' }),
292
+ };
293
+ // 自定义 MIME 需要安全上下文,可能被拒,单独 try
294
+ try {
295
+ items[BEE_MIME] = new Blob([jsonStr], { type: BEE_MIME });
296
+ } catch (e) { /* custom MIME not allowed, skip */ }
297
+ return navigator.clipboard.write([new ClipboardItem(items)]).then(function() {
298
+ return 'clipboard.write';
299
+ }, function() {
300
+ // write 失败(权限/安全上下文)→ execCommand 兜底
301
+ return execCommandCopy(html);
302
+ }).catch(function() {
303
+ return execCommandCopy(html);
304
+ });
305
+ } catch (e) {
306
+ // ClipboardItem 构造失败 → execCommand 兜底
307
+ }
308
+ }
309
+
310
+ // execCommand 兜底:创建临时元素 + 选区 + execCommand('copy')
311
+ var result = execCommandCopy(html);
312
+ if (result) return Promise.resolve('execCommand');
313
+
314
+ // 最终降级:writeText 写 html 字符串(chat handlePaste 兜底解析 text/plain)
315
+ if (navigator.clipboard && navigator.clipboard.writeText) {
316
+ return navigator.clipboard.writeText(html);
317
+ }
318
+
319
+ return Promise.reject(new Error('Clipboard API not available'));
320
+ }
321
+
322
+ /**
323
+ * execCommand('copy') 同步兜底:创建临时可见元素承载 html,选区选中后执行 copy。
324
+ * 利用浏览器原生的 text/html 写入机制(同用户手动复制)。非安全上下文下最可靠。
325
+ * 返回 true 成功 / false 失败。
326
+ */
327
+ function execCommandCopy(html) {
328
+ try {
329
+ var container = document.createElement('div');
330
+ container.setAttribute('contenteditable', 'true');
331
+ container.style.cssText = 'position:fixed;left:-9999px;top:0;opacity:0;';
332
+ container.innerHTML = html;
333
+ document.body.appendChild(container);
334
+ var range = document.createRange();
335
+ range.selectNodeContents(container);
336
+ var sel = window.getSelection();
337
+ sel.removeAllRanges();
338
+ sel.addRange(range);
339
+ var ok = document.execCommand('copy');
340
+ sel.removeAllRanges();
341
+ document.body.removeChild(container);
342
+ return ok;
343
+ } catch (e) {
344
+ return false;
345
+ }
346
+ }
347
+
348
+ // ─── 捕获 UI(高亮 + Ctrl+C)─────────────────────────────────
349
+
350
+ function install() {
351
+ if (window.__bee_capture_installed) return;
352
+
353
+ var highlight = document.createElement('div');
354
+ highlight.id = '__bee_highlight';
355
+ highlight.style.cssText = 'position:fixed;pointer-events:none;z-index:2147483647;border:2px solid #3b82f6;background:rgba(59,130,246,0.1);display:none;transition:all 0.08s ease;';
356
+ document.documentElement.appendChild(highlight);
357
+
358
+ var currentTarget = null;
359
+
360
+ document.addEventListener('mouseover', function(e) {
361
+ if (!window.__bee_capture_enabled) return;
362
+ if (e.target === highlight) return;
363
+ currentTarget = e.target;
364
+ var r = e.target.getBoundingClientRect();
365
+ highlight.style.left = r.left + 'px';
366
+ highlight.style.top = r.top + 'px';
367
+ highlight.style.width = r.width + 'px';
368
+ highlight.style.height = r.height + 'px';
369
+ highlight.style.display = 'block';
370
+ }, true);
371
+
372
+ document.addEventListener('mouseout', function(e) {
373
+ if (e.target === currentTarget) {
374
+ highlight.style.display = 'none';
375
+ currentTarget = null;
376
+ }
377
+ }, true);
378
+
379
+ document.addEventListener('keydown', function(e) {
380
+ if (!window.__bee_capture_enabled) return;
381
+ if ((e.ctrlKey || e.metaKey) && e.key === 'c') {
382
+ console.log('[BEE-CAPTURE] Ctrl+C detected, currentTarget=', currentTarget, 'activeElement=', document.activeElement);
383
+ var el = currentTarget || document.activeElement;
384
+ if (!el || el === document.body || el === document.documentElement) {
385
+ console.log('[BEE-CAPTURE] no valid element, abort');
386
+ return;
387
+ }
388
+ var capture = buildCapture(el);
389
+ if (!capture) {
390
+ console.log('[BEE-CAPTURE] buildCapture returned null, abort');
391
+ return;
392
+ }
393
+ console.log('[BEE-CAPTURE] capture built, label=', capture.label);
394
+ e.preventDefault();
395
+ e.stopPropagation();
396
+ writeCaptureToClipboard(capture).then(function(method) {
397
+ console.log('[BEE-CAPTURE] clipboard write SUCCESS via', method);
398
+ highlight.style.border = '2px solid #22c55e';
399
+ highlight.style.background = 'rgba(34,197,94,0.2)';
400
+ setTimeout(function() {
401
+ highlight.style.border = '2px solid #3b82f6';
402
+ highlight.style.background = 'rgba(59,130,246,0.1)';
403
+ }, 300);
404
+ }).catch(function(err) {
405
+ console.log('[BEE-CAPTURE] clipboard write FAILED:', err && err.message ? err.message : err);
406
+ });
407
+ }
408
+ }, true);
409
+
410
+ window.addEventListener('scroll', function() {
411
+ highlight.style.display = 'none';
412
+ currentTarget = null;
413
+ }, true);
414
+
415
+ window.__bee_capture_installed = true;
416
+ }
417
+
418
+ // DOM 已就绪:立即安装
419
+ if (document.documentElement && document.body) {
420
+ install();
421
+ return;
422
+ }
423
+
424
+ // DOM 未就绪(addScriptToEvaluateOnNewDocument 场景):等 DOMContentLoaded
425
+ document.addEventListener('DOMContentLoaded', install, { once: true });
426
+ })();`