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,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
|
+
export const 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, '&')
|
|
34
|
+
.replace(/"/g, '"')
|
|
35
|
+
.replace(/</g, '<')
|
|
36
|
+
.replace(/>/g, '>');
|
|
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
|
+
})();`
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// -*- coding: utf-8 -*-
|
|
2
|
+
//
|
|
3
|
+
// capture-service.cjs — CDP 链路捕获模式管理。
|
|
4
|
+
// 完整复刻自 bee/packages/app/browser-op/main/capture-service.ts。
|
|
5
|
+
//
|
|
6
|
+
// 职责:
|
|
7
|
+
// - 通过 CDP browser endpoint WebSocket 连接 Chrome
|
|
8
|
+
// - 向所有 page target 注入 captureInjectJS(Page.addScriptToEvaluateOnNewDocument)
|
|
9
|
+
// - 新 target / 页面导航时自动重新注入
|
|
10
|
+
// - disable 时通过 Runtime.evaluate 关闭页面内标志位
|
|
11
|
+
//
|
|
12
|
+
// 注意:这是 CDP 链路专用。插件链路的捕获走 extension/server.cjs 的 callPlugin('enableCapture', ...)
|
|
13
|
+
|
|
14
|
+
'use strict'
|
|
15
|
+
|
|
16
|
+
const WebSocket = require('ws')
|
|
17
|
+
const { captureInjectJS } = require('./capture-inject.js')
|
|
18
|
+
|
|
19
|
+
class CaptureService {
|
|
20
|
+
constructor () {
|
|
21
|
+
this.ws = null
|
|
22
|
+
this.msgId = 0
|
|
23
|
+
this.pending = new Map()
|
|
24
|
+
this.enabled = false
|
|
25
|
+
/** targetId → sessionId (from Target.attachToTarget) */
|
|
26
|
+
this.sessions = new Map()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async enable (port) {
|
|
30
|
+
if (this.enabled) return
|
|
31
|
+
|
|
32
|
+
const resp = await fetch(`http://127.0.0.1:${port}/json/version`, {
|
|
33
|
+
signal: AbortSignal.timeout(3000)
|
|
34
|
+
})
|
|
35
|
+
const version = await resp.json()
|
|
36
|
+
const wsUrl = version.webSocketDebuggerUrl
|
|
37
|
+
if (!wsUrl) throw new Error('CDP WebSocket URL not found')
|
|
38
|
+
|
|
39
|
+
this.ws = new WebSocket(wsUrl)
|
|
40
|
+
|
|
41
|
+
await new Promise((resolve, reject) => {
|
|
42
|
+
this.ws.once('open', resolve)
|
|
43
|
+
this.ws.once('error', reject)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
this.ws.on('message', (raw) => {
|
|
47
|
+
const msg = JSON.parse(raw.toString())
|
|
48
|
+
if (msg.id && this.pending.has(msg.id)) {
|
|
49
|
+
const p = this.pending.get(msg.id)
|
|
50
|
+
this.pending.delete(msg.id)
|
|
51
|
+
if (msg.error) p.reject(new Error(msg.error.message || 'CDP error'))
|
|
52
|
+
else p.resolve(msg.result)
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
// 新 target 自动注入
|
|
56
|
+
if (msg.method === 'Target.targetCreated') {
|
|
57
|
+
const info = msg.params && msg.params.targetInfo
|
|
58
|
+
if (info && info.type === 'page' && info.targetId) {
|
|
59
|
+
this.injectToTarget(info.targetId).catch(() => {})
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// 页面刷新/导航 → 重新注入当前页面
|
|
63
|
+
if (msg.method === 'Page.frameNavigated' && msg.sessionId) {
|
|
64
|
+
this.reinjectToSession(msg.sessionId).catch(() => {})
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
await this.send('Target.setDiscoverTargets', { discover: true })
|
|
69
|
+
await this.injectToAllTargets()
|
|
70
|
+
|
|
71
|
+
this.enabled = true
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async disable () {
|
|
75
|
+
if (!this.enabled || !this.ws) return
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const targets = await this.send('Target.getTargets')
|
|
79
|
+
for (const t of targets.targetInfos) {
|
|
80
|
+
if (t.type !== 'page') continue
|
|
81
|
+
const sessionId = this.sessions.get(t.targetId)
|
|
82
|
+
if (!sessionId) continue
|
|
83
|
+
try {
|
|
84
|
+
await this.sendWithSession(sessionId, 'Runtime.evaluate', {
|
|
85
|
+
expression: 'window.__bee_capture_enabled = false; window.__bee_capture_installed = false;'
|
|
86
|
+
})
|
|
87
|
+
} catch { /* page may have closed */ }
|
|
88
|
+
}
|
|
89
|
+
// 断开所有 target 连接
|
|
90
|
+
for (const [, sessionId] of this.sessions) {
|
|
91
|
+
try { await this.send('Target.detachFromTarget', { sessionId }) } catch {}
|
|
92
|
+
}
|
|
93
|
+
} finally {
|
|
94
|
+
// 无论上面是否出错,必须重置状态,否则 enable 的守卫永久拦截
|
|
95
|
+
this.sessions.clear()
|
|
96
|
+
this.pending.clear()
|
|
97
|
+
try { this.ws.close() } catch {}
|
|
98
|
+
this.ws = null
|
|
99
|
+
this.enabled = false
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
isEnabled () {
|
|
104
|
+
return this.enabled
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async injectToAllTargets () {
|
|
108
|
+
const targets = await this.send('Target.getTargets')
|
|
109
|
+
for (const t of targets.targetInfos) {
|
|
110
|
+
if (t.type !== 'page') continue
|
|
111
|
+
try { await this.injectToTarget(t.targetId) } catch { /* skip */ }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async injectToTarget (targetId) {
|
|
116
|
+
// 先 attach 获取真实 sessionId(flat 模式)
|
|
117
|
+
const attachResult = await this.send('Target.attachToTarget', {
|
|
118
|
+
targetId,
|
|
119
|
+
flatten: true
|
|
120
|
+
})
|
|
121
|
+
const sessionId = attachResult.sessionId
|
|
122
|
+
this.sessions.set(targetId, sessionId)
|
|
123
|
+
|
|
124
|
+
// 启用 Page 域事件(监听 frameNavigated 用于页面刷新重新注入)
|
|
125
|
+
await this.sendWithSession(sessionId, 'Page.enable')
|
|
126
|
+
|
|
127
|
+
// 向未来新建 frame 注入
|
|
128
|
+
await this.sendWithSession(sessionId, 'Page.addScriptToEvaluateOnNewDocument', {
|
|
129
|
+
source: captureInjectJS
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// 立即注入当前已打开的页面
|
|
133
|
+
await this.sendWithSession(sessionId, 'Runtime.evaluate', {
|
|
134
|
+
expression: captureInjectJS
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
reinjectToSession (sessionId) {
|
|
139
|
+
return this.sendWithSession(sessionId, 'Runtime.evaluate', {
|
|
140
|
+
expression: captureInjectJS
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
sendWithSession (sessionId, method, params) {
|
|
145
|
+
return this._send(method, params, sessionId)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
send (method, params) {
|
|
149
|
+
return this._send(method, params, undefined)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
_send (method, params, sessionId) {
|
|
153
|
+
const id = ++this.msgId
|
|
154
|
+
return new Promise((resolve, reject) => {
|
|
155
|
+
const timer = setTimeout(() => {
|
|
156
|
+
if (this.pending.has(id)) {
|
|
157
|
+
this.pending.delete(id)
|
|
158
|
+
reject(new Error(`CDP timeout: ${method}${sessionId ? ' (session)' : ''}`))
|
|
159
|
+
}
|
|
160
|
+
}, 5000)
|
|
161
|
+
this.pending.set(id, {
|
|
162
|
+
resolve: (v) => { clearTimeout(timer); resolve(v) },
|
|
163
|
+
reject: (e) => { clearTimeout(timer); reject(e) }
|
|
164
|
+
})
|
|
165
|
+
const msg = { id, method, params }
|
|
166
|
+
if (sessionId) msg.sessionId = sessionId
|
|
167
|
+
this.ws.send(JSON.stringify(msg))
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = { CaptureService }
|