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,310 @@
1
+ /**
2
+ * 捕获脚本本体(webplater 自带,等价 bee 的 capture-inject.ts captureInjectJS)。
3
+ *
4
+ * 行为:鼠标悬浮蓝色高亮 + Ctrl+C 复制 BeeCapture 到剪贴板。
5
+ * 由 background 用 chrome.scripting.registerContentScripts(world:'MAIN',
6
+ * runAt:'document_start') 注册,等价 CDP Page.addScriptToEvaluateOnNewDocument
7
+ * ——每个文档加载(导航/刷新/新 tab)都自动执行,document_start 即注入,持久存在。
8
+ *
9
+ * installed/enabled 标志位做幂等与开关控制。enabled 由 background 控制
10
+ * (开启捕获时 background 再注入一次把 enabled 置 true)。
11
+ *
12
+ * ⚠️ 此脚本是 bee/packages/.../capture-inject.ts 的 captureInjectJS 的独立副本。
13
+ * webplater 与 bee 非 monorepo,逻辑需手动保持一致(两处改动要同步)。
14
+ */
15
+ (function () {
16
+ if (window.__bee_capture_installed) return;
17
+
18
+ // enabled 状态存 chrome.storage.local(background enable/disable 写入)。
19
+ // 脚本跑在 ISOLATED world,可直接读 storage + 监听变化,所有 tab(含新 tab)状态一致。
20
+ var enabled = false;
21
+ try { chrome.storage.local.get(['bee_capture_enabled'], function (r) {
22
+ enabled = !!r && r.bee_capture_enabled;
23
+ }); } catch (e) {}
24
+ try { chrome.storage.onChanged.addListener(function (changes, area) {
25
+ if (area === 'local' && changes.bee_capture_enabled) {
26
+ enabled = !!changes.bee_capture_enabled.newValue;
27
+ }
28
+ }); } catch (e) {}
29
+
30
+ var BEE_MIME = 'web application/vnd.bee.capture+json';
31
+
32
+ function escapeHtml(s) {
33
+ return String(s)
34
+ .replace(/&/g, '&')
35
+ .replace(/"/g, '"')
36
+ .replace(/</g, '&lt;')
37
+ .replace(/>/g, '&gt;');
38
+ }
39
+
40
+ function buildLabel(el) {
41
+ var text = (el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 40);
42
+ if (text) return text;
43
+ var tag = el.tagName ? el.tagName.toLowerCase() : 'element';
44
+ var parts = [tag];
45
+ if (el.id) parts.push('#' + el.id);
46
+ if (el.className && typeof el.className === 'string') {
47
+ var classes = el.className.split(/\s+/).filter(Boolean);
48
+ if (classes.length) parts.push('.' + classes.join('.'));
49
+ }
50
+ return 'DOM: ' + parts.join('');
51
+ }
52
+
53
+ function buildStableSelector(el) {
54
+ var path = [];
55
+ var node = el;
56
+ var MAX_DEPTH = 15;
57
+ var depth = 0;
58
+ while (node && node.nodeType === 1 && node.tagName && depth < MAX_DEPTH) {
59
+ var tag = node.tagName.toLowerCase();
60
+ if (tag === 'html') break;
61
+ if (tag === 'body') { path.unshift('body'); break; }
62
+ var parent = node.parentElement;
63
+ if (!parent) { path.unshift(tag); break; }
64
+ var index = 1;
65
+ var sibling = parent.firstElementChild;
66
+ while (sibling && sibling !== node) {
67
+ sibling = sibling.nextElementSibling;
68
+ index++;
69
+ }
70
+ if (parent.id && /^[A-Za-z][\w-]*$/.test(parent.id)) {
71
+ path.unshift(tag + ':nth-child(' + index + ')');
72
+ path.unshift('#' + parent.id);
73
+ break;
74
+ }
75
+ path.unshift(tag + ':nth-child(' + index + ')');
76
+ node = parent;
77
+ depth++;
78
+ }
79
+ return path.join(' > ');
80
+ }
81
+
82
+ function inferImplicitRole(el, tagLower) {
83
+ if (tagLower === 'a' && el.getAttribute('href')) return 'link';
84
+ if (tagLower === 'a') return 'generic';
85
+ var directMap = {
86
+ button: 'button', nav: 'navigation', main: 'main', header: 'banner',
87
+ footer: 'contentinfo', aside: 'complementary', form: 'form', search: 'search',
88
+ ul: 'list', ol: 'list', li: 'listitem', table: 'table', tr: 'row', td: 'cell',
89
+ th: 'columnheader', img: 'image', figure: 'figure', dialog: 'dialog',
90
+ };
91
+ if (directMap[tagLower]) return directMap[tagLower];
92
+ if (/^h[1-6]$/.test(tagLower)) return 'heading';
93
+ if (tagLower === 'input') {
94
+ var inputType = (el.getAttribute('type') || 'text').toLowerCase();
95
+ if (inputType === 'button' || inputType === 'submit' || inputType === 'reset') return 'button';
96
+ if (inputType === 'checkbox') return 'checkbox';
97
+ if (inputType === 'radio') return 'radio';
98
+ if (inputType === 'range') return 'slider';
99
+ if (inputType === 'search') return 'searchbox';
100
+ return 'textbox';
101
+ }
102
+ if (tagLower === 'textarea') return 'textbox';
103
+ if (tagLower === 'select') return 'listbox';
104
+ return '';
105
+ }
106
+
107
+ function buildA11yInfo(el) {
108
+ var role = el.getAttribute('role') || '';
109
+ var ariaLabel = el.getAttribute('aria-label') || '';
110
+ var ariaLabelledBy = el.getAttribute('aria-labelledby') || '';
111
+ var ariaDescribedBy = el.getAttribute('aria-describedby') || '';
112
+ var headingLevel = 0;
113
+ var tagLower = (el.tagName || '').toLowerCase();
114
+ var hMatch = tagLower.match(/^h(\d)$/);
115
+ if (hMatch) headingLevel = parseInt(hMatch[1], 10);
116
+ if (!headingLevel && role === 'heading') {
117
+ var ariaLevel = el.getAttribute('aria-level');
118
+ if (ariaLevel) headingLevel = parseInt(ariaLevel, 10);
119
+ }
120
+ if (!role) role = inferImplicitRole(el, tagLower);
121
+ var name = ariaLabel || ((el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 40));
122
+ return {
123
+ role: role, name: name, ariaLabel: ariaLabel,
124
+ ariaLabelledBy: ariaLabelledBy, ariaDescribedBy: ariaDescribedBy,
125
+ headingLevel: headingLevel || undefined,
126
+ };
127
+ }
128
+
129
+ function buildParentContext(el) {
130
+ var contexts = [];
131
+ var node = el.parentElement;
132
+ var depth = 0;
133
+ 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;
134
+ while (node && depth < 8) {
135
+ var role = node.getAttribute && node.getAttribute('role');
136
+ var nodeTag = (node.tagName || '').toLowerCase();
137
+ var nodeClass = (typeof node.className === 'string' ? node.className : '');
138
+ var nodeText = (node.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 30);
139
+ var hasSemanticClass = semClassRe.test(nodeClass);
140
+ var isSemantic = role || /^h[1-6]$/.test(nodeTag) ||
141
+ ['nav', 'main', 'header', 'footer', 'section', 'article', 'aside', 'form', 'dialog', 'ul', 'ol', 'table'].indexOf(nodeTag) >= 0 ||
142
+ node.id || hasSemanticClass;
143
+ if (isSemantic) {
144
+ contexts.push({
145
+ tag: nodeTag, role: role || inferImplicitRole(node, nodeTag),
146
+ id: node.id || '', className: nodeClass, text: nodeText,
147
+ });
148
+ if (contexts.length >= 3) break;
149
+ }
150
+ node = node.parentElement;
151
+ depth++;
152
+ }
153
+ return contexts;
154
+ }
155
+
156
+ function buildCapture(el) {
157
+ var tag = el.tagName ? el.tagName.toLowerCase() : '';
158
+ var attrs = {};
159
+ if (el.attributes) {
160
+ for (var i = 0; i < el.attributes.length; i++) {
161
+ var a = el.attributes[i];
162
+ attrs[a.name] = a.value;
163
+ }
164
+ }
165
+ var selector = el.id ? '#' + el.id : tag;
166
+ if (!el.id && el.className && typeof el.className === 'string') {
167
+ var classes = el.className.split(/\s+/).filter(Boolean);
168
+ if (classes.length) selector += '.' + classes.join('.');
169
+ }
170
+ var text = (el.textContent || '').trim().slice(0, 200);
171
+ return {
172
+ v: 1, source: 'browser', kind: 'dom-element',
173
+ label: buildLabel(el),
174
+ preview: text ? text.slice(0, 80) : undefined,
175
+ data: {
176
+ tag: tag, id: el.id || '',
177
+ className: typeof el.className === 'string' ? el.className : '',
178
+ text: text, selector: selector,
179
+ stableSelector: buildStableSelector(el),
180
+ attributes: attrs, a11y: buildA11yInfo(el),
181
+ parentContext: buildParentContext(el),
182
+ url: location.href, title: document.title,
183
+ },
184
+ };
185
+ }
186
+
187
+ function encodeCaptureHtml(capture, refId) {
188
+ var dataStr = encodeURIComponent(JSON.stringify(capture.data));
189
+ var previewAttr = capture.preview
190
+ ? ' data-bee-preview="' + escapeHtml(capture.preview) + '"'
191
+ : '';
192
+ return '<a data-bee-capture="' + escapeHtml(refId) + '"' +
193
+ ' data-bee-source="' + escapeHtml(capture.source) + '"' +
194
+ ' data-bee-kind="' + escapeHtml(capture.kind) + '"' +
195
+ ' data-bee-label="' + escapeHtml(capture.label) + '"' +
196
+ ' data-bee-data="' + escapeHtml(dataStr) + '"' +
197
+ previewAttr + '>' + escapeHtml(capture.label) + '</a>';
198
+ }
199
+
200
+ function execCommandCopy(html) {
201
+ try {
202
+ var container = document.createElement('div');
203
+ container.setAttribute('contenteditable', 'true');
204
+ container.style.cssText = 'position:fixed;left:-9999px;top:0;opacity:0;';
205
+ container.innerHTML = html;
206
+ document.body.appendChild(container);
207
+ var range = document.createRange();
208
+ range.selectNodeContents(container);
209
+ var sel = window.getSelection();
210
+ sel.removeAllRanges();
211
+ sel.addRange(range);
212
+ var ok = document.execCommand('copy');
213
+ sel.removeAllRanges();
214
+ document.body.removeChild(container);
215
+ return ok;
216
+ } catch (e) {
217
+ return false;
218
+ }
219
+ }
220
+
221
+ function writeCaptureToClipboard(capture) {
222
+ var refId = 'browser_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8);
223
+ var html = encodeCaptureHtml(capture, refId);
224
+ var plainText = JSON.stringify(capture, null, 2);
225
+ var jsonStr = JSON.stringify(capture);
226
+ if (navigator.clipboard && navigator.clipboard.write && window.ClipboardItem) {
227
+ try {
228
+ var items = {
229
+ 'text/html': new Blob([html], { type: 'text/html' }),
230
+ 'text/plain': new Blob([plainText], { type: 'text/plain' }),
231
+ };
232
+ try { items[BEE_MIME] = new Blob([jsonStr], { type: BEE_MIME }); } catch (e) { }
233
+ return navigator.clipboard.write([new ClipboardItem(items)]).then(function () {
234
+ return 'clipboard.write';
235
+ }, function () { return execCommandCopy(html); }).catch(function () {
236
+ return execCommandCopy(html);
237
+ });
238
+ } catch (e) { }
239
+ }
240
+ var result = execCommandCopy(html);
241
+ if (result) return Promise.resolve('execCommand');
242
+ if (navigator.clipboard && navigator.clipboard.writeText) {
243
+ return navigator.clipboard.writeText(html);
244
+ }
245
+ return Promise.reject(new Error('Clipboard API not available'));
246
+ }
247
+
248
+ function install() {
249
+ if (window.__bee_capture_installed) return;
250
+
251
+ var highlight = document.createElement('div');
252
+ highlight.id = '__bee_highlight';
253
+ 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;';
254
+ document.documentElement.appendChild(highlight);
255
+
256
+ var currentTarget = null;
257
+
258
+ document.addEventListener('mouseover', function (e) {
259
+ if (!enabled) return;
260
+ if (e.target === highlight) return;
261
+ currentTarget = e.target;
262
+ var r = e.target.getBoundingClientRect();
263
+ highlight.style.left = r.left + 'px';
264
+ highlight.style.top = r.top + 'px';
265
+ highlight.style.width = r.width + 'px';
266
+ highlight.style.height = r.height + 'px';
267
+ highlight.style.display = 'block';
268
+ }, true);
269
+
270
+ document.addEventListener('mouseout', function (e) {
271
+ if (e.target === currentTarget) {
272
+ highlight.style.display = 'none';
273
+ currentTarget = null;
274
+ }
275
+ }, true);
276
+
277
+ document.addEventListener('keydown', function (e) {
278
+ if (!enabled) return;
279
+ if ((e.ctrlKey || e.metaKey) && e.key === 'c') {
280
+ var el = currentTarget || document.activeElement;
281
+ if (!el || el === document.body || el === document.documentElement) return;
282
+ var capture = buildCapture(el);
283
+ if (!capture) return;
284
+ e.preventDefault();
285
+ e.stopPropagation();
286
+ writeCaptureToClipboard(capture).then(function (method) {
287
+ highlight.style.border = '2px solid #22c55e';
288
+ highlight.style.background = 'rgba(34,197,94,0.2)';
289
+ setTimeout(function () {
290
+ highlight.style.border = '2px solid #3b82f6';
291
+ highlight.style.background = 'rgba(59,130,246,0.1)';
292
+ }, 300);
293
+ }).catch(function () { });
294
+ }
295
+ }, true);
296
+
297
+ window.addEventListener('scroll', function () {
298
+ highlight.style.display = 'none';
299
+ currentTarget = null;
300
+ }, true);
301
+
302
+ window.__bee_capture_installed = true;
303
+ }
304
+
305
+ if (document.documentElement && document.body) {
306
+ install();
307
+ return;
308
+ }
309
+ document.addEventListener('DOMContentLoaded', install, { once: true });
310
+ })();
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env node
2
+ // WebPlater 浏览器插件发布脚本。
3
+ // 构建 → wxt zip → 上传到对象存储 → 写版本元信息。
4
+ //
5
+ // 产物形态:zip 包(manifest.json 在根目录),消费方是 flashbox host 的
6
+ // browser feature(load unpacked)。不走 catalog,用"单向最新指针"
7
+ // (webplater-latest.zip + webplater-manifest.json),与 flashbox host-update
8
+ // 的 latest.yml 同构。
9
+ //
10
+ // Usage:
11
+ // node scripts/publish-extension.mjs # 发布当前 wxt build 版本
12
+ // node scripts/publish-extension.mjs --note "修复 popup" # 附 changelog
13
+ // node scripts/publish-extension.mjs --keep-history # 保留历史 zip 不删
14
+ //
15
+ // 上传到 bucket 的 key 布局:
16
+ // browser-extension/webplater-<version>.zip 带版本号的 zip(可追溯)
17
+ // browser-extension/webplater-latest.zip 固定名指针(总是最新,host 拉这个)
18
+ // browser-extension/webplater-manifest.json 版本元信息(version/sha256/size/url)
19
+ import { spawnSync } from 'node:child_process'
20
+ import { existsSync, readFileSync, readdirSync, statSync, rmSync } from 'node:fs'
21
+ import { resolve, dirname, basename } from 'node:path'
22
+ import { fileURLToPath, pathToFileURL } from 'node:url'
23
+ import { createHash } from 'node:crypto'
24
+
25
+ const __filename = fileURLToPath(import.meta.url)
26
+ const __dirname = dirname(__filename)
27
+ const ROOT = resolve(__dirname, '..')
28
+
29
+ // ── 加载 .env.local ──
30
+ const envPath = resolve(ROOT, '.env.local')
31
+ if (existsSync(envPath)) {
32
+ for (const line of readFileSync(envPath, 'utf8').split('\n')) {
33
+ const m = line.match(/^([A-Z_]+)=(.*)$/)
34
+ if (m && process.env[m[1]] === undefined) process.env[m[1]] = m[2]
35
+ }
36
+ }
37
+
38
+ // ── 解析命令行参数 ──
39
+ const notes = []
40
+ let keepHistory = false
41
+ for (let i = 2; i < process.argv.length; i++) {
42
+ const a = process.argv[i]
43
+ if (a === '--note' || a === '-n') {
44
+ notes.push(process.argv[++i])
45
+ } else if (a === '--keep-history') {
46
+ keepHistory = true
47
+ } else if (a === '--help' || a === '-h') {
48
+ console.log(`usage: node scripts/publish-extension.mjs [--note "..." ...] [--keep-history]
49
+
50
+ --note <text> 追加一条 changelog(可多次),写入 webplater-manifest.json
51
+ --keep-history 保留 bucket 上的历史 zip(默认删旧只留最新 + latest 指针)
52
+ `)
53
+ process.exit(0)
54
+ }
55
+ }
56
+
57
+ function run(cmd, args) {
58
+ const r = spawnSync(cmd, args, { cwd: ROOT, stdio: 'inherit', shell: true })
59
+ if (r.status !== 0) {
60
+ console.error(`failed: ${cmd} ${args.join(' ')}`)
61
+ process.exit(r.status ?? 1)
62
+ }
63
+ }
64
+
65
+ // ── 1. wxt zip(含 build)→ dist/webplater-<version>-chrome.zip ──
66
+ console.log('=== wxt build + zip ===')
67
+ run('pnpm', ['run', 'zip'])
68
+
69
+ // ── 2. 找产出的 zip + 读 manifest 拿版本号 ──
70
+ const DIST_DIR = resolve(ROOT, 'dist')
71
+ const zipFiles = readdirSync(DIST_DIR).filter((f) => f.startsWith('webplater-') && f.endsWith('-chrome.zip'))
72
+ if (zipFiles.length === 0) {
73
+ console.error('wxt zip 没产出 zip 文件(期望 dist/webplater-<version>-chrome.zip)')
74
+ process.exit(1)
75
+ }
76
+ // 取最新的(按 mtime)
77
+ let zipFile = zipFiles[0]
78
+ let zipMtime = 0
79
+ for (const f of zipFiles) {
80
+ const mtimeMs = statSync(resolve(DIST_DIR, f)).mtimeMs
81
+ if (mtimeMs > zipMtime) {
82
+ zipMtime = mtimeMs
83
+ zipFile = f
84
+ }
85
+ }
86
+ const ZIP_PATH = resolve(DIST_DIR, zipFile)
87
+
88
+ const manifest = JSON.parse(readFileSync(resolve(DIST_DIR, 'chrome-mv3', 'manifest.json'), 'utf8'))
89
+ const version = manifest.version
90
+ if (!version) {
91
+ console.error('manifest.version 缺失(wxt.config 的 buildVersion 没生效?)')
92
+ process.exit(1)
93
+ }
94
+ // extensionId 由 manifest.key 稳定算出(见 wxt.config 注释),固定值,用于 host 端校验
95
+ const EXTENSION_ID = 'npkapgkjeaaipldb'
96
+ console.log(`version: ${version}`)
97
+ console.log(`extensionId: ${EXTENSION_ID}`)
98
+ console.log(`zip: ${zipFile}`)
99
+
100
+ // ── 3. 加载 miniapp-tools(复用 flashbox 主仓库的工具函数)──
101
+ // 候选路径:webplater 是独立仓库,flashbox 主仓库在同级目录 ../flashbox
102
+ const toolsCandidates = [
103
+ resolve(ROOT, '../flashbox/packages/miniapp-tools/dist/index.js'),
104
+ resolve(ROOT, '../../flashbox/packages/miniapp-tools/dist/index.js'),
105
+ ]
106
+ const toolsPath = toolsCandidates.find((p) => existsSync(p))
107
+ if (!toolsPath) {
108
+ console.error('miniapp-tools 未找到。尝试过:')
109
+ toolsCandidates.forEach((p) => console.error(' ' + p))
110
+ console.error('请确保 flashbox 主仓库已 build miniapp-tools(pnpm --filter @flashbox/miniapp-tools build)')
111
+ process.exit(1)
112
+ }
113
+ const tools = await import(pathToFileURL(toolsPath).href)
114
+ const cfg = tools.s3ConfigFromEnv()
115
+
116
+ // ── 4. 计算 sha256 + size ──
117
+ const zipBuf = readFileSync(ZIP_PATH)
118
+ const sha256 = createHash('sha256').update(zipBuf).digest('hex')
119
+ const size = zipBuf.length
120
+ console.log(`sha256: ${sha256.slice(0, 16)}...`)
121
+ console.log(`size: ${size} bytes`)
122
+
123
+ // ── 5. 上传带版本号 zip ──
124
+ console.log('=== upload versioned zip ===')
125
+ const versionedKey = `browser-extension/webplater-${version}.zip`
126
+ await tools.uploadFile(cfg, versionedKey, ZIP_PATH, 'application/zip')
127
+ console.log(' ' + tools.publicUrl(cfg, versionedKey))
128
+
129
+ // ── 6. 覆盖 latest 指针(固定名,host 总是拉这个)──
130
+ console.log('=== upload latest pointer ===')
131
+ const latestKey = 'browser-extension/webplater-latest.zip'
132
+ await tools.uploadFile(cfg, latestKey, ZIP_PATH, 'application/zip')
133
+ console.log(' ' + tools.publicUrl(cfg, latestKey))
134
+
135
+ // ── 7. 写版本元信息 manifest.json(host 端先拉这个拿 sha256 校验)──
136
+ console.log('=== upload version meta ===')
137
+ const meta = {
138
+ extensionId: EXTENSION_ID,
139
+ version,
140
+ sha256,
141
+ size,
142
+ packageUrl: tools.publicUrl(cfg, versionedKey),
143
+ latestUrl: tools.publicUrl(cfg, latestKey),
144
+ publishedAt: new Date().toISOString(),
145
+ changelog: notes.length > 0 ? notes : undefined,
146
+ }
147
+ const metaKey = 'browser-extension/webplater-manifest.json'
148
+ await tools.uploadText(cfg, metaKey, JSON.stringify(meta, null, 2) + '\n', 'application/json')
149
+ console.log(' ' + tools.publicUrl(cfg, metaKey))
150
+
151
+ // ── 8. 删旧版本 zip(默认只留 latest + 当前版本)──
152
+ if (!keepHistory) {
153
+ const allKeys = await tools.listObjectKeys(cfg, 'browser-extension/')
154
+ const oldKeys = allKeys.filter(
155
+ (k) =>
156
+ k.startsWith('browser-extension/webplater-') &&
157
+ k.endsWith('.zip') &&
158
+ k !== versionedKey &&
159
+ k !== latestKey,
160
+ )
161
+ if (oldKeys.length > 0) {
162
+ console.log(`=== 清理旧版本 zip (${oldKeys.length}) ===`)
163
+ for (const k of oldKeys) console.log(' - ' + k)
164
+ await tools.deleteKeys(cfg, oldKeys)
165
+ } else {
166
+ console.log('=== 无旧版本 zip 需要清理 ===')
167
+ }
168
+ }
169
+
170
+ // ── 9. 清理本地 wxt zip 产物(发布完不再需要本地副本)──
171
+ rmSync(ZIP_PATH, { force: true })
172
+
173
+ console.log(`\n=== publish webplater@${version} OK ===`)
174
+ console.log(`\nhost 端接入(webplater-constants.ts):`)
175
+ console.log(` WEBPLATER_MANIFEST_URL = '${tools.publicUrl(cfg, metaKey)}'`)
176
+ console.log(` 或固定下载地址: ${tools.publicUrl(cfg, latestKey)}`)
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "resolveJsonModule": true,
12
+ "declaration": true,
13
+ "declarationMap": true,
14
+ "sourceMap": true,
15
+ "noEmit": true,
16
+ "types": ["wxt/types"]
17
+ },
18
+ "include": ["entrypoints/**/*.ts", "entrypoints/**/*.tsx"]
19
+ }
@@ -0,0 +1,34 @@
1
+ import { defineConfig } from 'wxt'
2
+
3
+ // 构建版本号:主.次.补丁.YYMMDDHHMM。每次 build 不同,重载后能从 chrome://extensions
4
+ // 卡片的 version 肉眼确认是不是新构建(避免 0.0.1 一成不变无法区分)。
5
+ function buildVersion() {
6
+ const d = new Date()
7
+ const p = (n) => String(n).padStart(2, '0')
8
+ const stamp = `${p(d.getFullYear() % 100)}${p(d.getMonth() + 1)}${p(d.getDate())}${p(d.getHours())}${p(d.getMinutes())}`
9
+ return `0.0.1.${stamp}`
10
+ }
11
+ const VERSION = buildVersion()
12
+
13
+ // https://wxt.dev/api/config.html
14
+ // 通用浏览器自动化扩展,不做任何域名限制。
15
+ export default defineConfig({
16
+ extensionApi: 'chrome',
17
+ outDir: 'dist',
18
+ modules: [],
19
+ manifest: {
20
+ name: 'WebPlater',
21
+ description: 'Generic browser automation bridge — Playwright alternative layer',
22
+ version: VERSION,
23
+ // 固定 extensionId=npkapgkjeaaipldb。
24
+ // key 是 RSA 公钥 DER 的 base64,Chrome 据此稳定算出 id(不再随 load 路径变)。
25
+ // 供 bee 启动前检测插件是否装在本 profile(按固定 id 命中,比 name 匹配稳)。
26
+ // ⚠️ 私钥存仓库 keys/extension-key.pem(本仓库为 PRIVATE)。未来发 Chrome Web Store
27
+ // 或 CRX 签名分发时用私钥;load unpacked 只验公钥算 id,不验签名。
28
+ key:
29
+ 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlljeBdeINKB8ouCR6euqG5iegaoSZY3c/uubW1AJoJzoVNyiB8UPzpL98zfWXW5nMF+KTdGa1Vb19sBACMVxO8gXEbxS/gobfqJHddfe0qZpNDEBrM7lwVJtocPb9lYJK3rHASx2VSkRFRPoymaaNDaeEO/uiu9uCG2dut/V00UvUAaeMDM9bwiWxD8aJ5IINRFIk+UniZsPB0kY3tbcmqayGYsjYpwZmU9RXoLSBZjWa8CfFjTNeyPBtLpkj2groQN/Ytk1RWf5AEwAPIahfz/hn2kDOnL1OSNGyQfswL8GuWJZs9h2bvDQMKwCRUQ/iUuqypNQfbin42rpD1kz9QIDAQAB',
30
+ permissions: ['tabs', 'scripting', 'cookies', 'storage', 'offscreen', 'webNavigation', 'downloads'],
31
+ // 全域名,无任何限制。这是一个通用插件。
32
+ host_permissions: ['<all_urls>'],
33
+ },
34
+ })
@@ -0,0 +1,102 @@
1
+ # operate action 清单
2
+
3
+ `BrowserClient.operate({ pageHandle, action, params })` 的全部 action 取值与参数。
4
+
5
+ > 自动生成,来自 `browser-op/backend/browserd.cjs` 的 `@action` 标注。这是 browser 内核的公开契约,始终最新。
6
+
7
+ ## 约定
8
+
9
+ - **pageHandle**:所有 action 都需要,由 `newTab()` / `listTabs()` 返回的 `tab.pageHandle` 提供。
10
+ - **selector 语法**:CSS / `aria-ref=eN`(来自 snapshot 的 ref)/ `.cls>>nth(n)` / `.cls>>last`。
11
+ - **ext 列**:action 在双路由(CDP / WebPlater 扩展)的可用性。
12
+ - ✓ = CDP 与扩展均支持
13
+ - ✗ = 仅 CDP(扩展路不支持,如 `waitForFunction`,因扩展路 CSP 拦截 eval)
14
+ - 仅 ext = 仅扩展路(如 `setDialogHandler`、`listDownloads`,依赖扩展 API)
15
+
16
+ ### 交互
17
+
18
+ | action | params | returns | description | ext |
19
+ | --- | --- | --- | --- | --- |
20
+ | `clickAt` | `x`:number, `y`:number, `button`?:string=left, `clickCount`?:number=1 | {ok,x,y,route} | 坐标点击(绕过selector,CDP路真实鼠标/扩展路合成事件) | ✓ |
21
+ | `clickByText` | `text`:string, `exact`?:boolean=false, `index`?:number=0, `offsetX`?:number=0, `offsetY`?:number=0 | {ok,text,x,y,match,route} | 定位可见文本后立即点击(原子,防DOM重渲染) | ✓ |
22
+ | `click` | `selector`:string, `x`?:number, `y`?:number | {ok,selector} | 点击元素(或坐标x,y) | ✓ |
23
+ | `fill` | `selector`:string, `value`:string | {ok,selector,value} | 在selector元素填入文本(先清空) | ✓ |
24
+ | `type` | `selector`:string, `value`:string, `delay`?:number=0 | {ok,selector} | 逐字输入(带延迟,模拟键盘) | ✓ |
25
+ | `press` | `selector`:string, `key`:string | {ok,selector,key} | 在元素上按键 | ✓ |
26
+ | `hover` | `selector`:string, `x`?:number, `y`?:number | {ok,selector} | 悬停元素(或坐标) | ✓ |
27
+ | `focus` | `selector`:string | {ok,selector} | 聚焦元素 | ✓ |
28
+ | `check` | `selector`:string | {ok,selector} | 勾选checkbox/radio | ✓ |
29
+ | `uncheck` | `selector`:string | {ok,selector} | 取消勾选 | ✓ |
30
+ | `selectOption` | `selector`:string, `value`:string | {ok,selector,value} | 选择option | ✓ |
31
+ | `dblclick` | `selector`:string | {ok,selector} | 双击元素 | ✓ |
32
+ | `mouseMove` | `x`:number, `y`:number | {ok,x,y} | 移动鼠标到坐标 | ✓ |
33
+ | `clickAt` | `x`:number, `y`:number, `button`?:string=left, `clickCount`?:number=1 | {ok,x,y,route} | 坐标点击(CDP路page.mouse真实鼠标) | ✓ |
34
+ | `clickByText` | `text`:string, `exact`?:boolean=false, `index`?:number=0, `offsetX`?:number=0, `offsetY`?:number=0 | {ok,text,x,y,match,route} | 定位可见文本后立即点击(CDP路原子) | ✓ |
35
+ | `dragTo` | `source`:string, `target`:string | {ok,source,target} | 拖拽source到target(双兼容mouse+HTML5) | ✓ |
36
+
37
+ ### 读取
38
+
39
+ | action | params | returns | description | ext |
40
+ | --- | --- | --- | --- | --- |
41
+ | `locateVisibleText` | `text`:string, `exact`?:boolean=false, `index`?:number=0 | {ok,matches:[{text,x,y,width,height,centerX,centerY,visible}]} | 定位可见文本节点返回bbox(不返回DOM handle) | ✓ |
42
+ | `locateVisibleText` | `text`:string, `exact`?:boolean=false, `index`?:number=0 | {ok,matches:[{text,x,y,width,height,centerX,centerY,visible}]} | 定位可见文本节点返回bbox | ✓ |
43
+ | `innerHTML` | `selector`:string | {ok,selector,value} | 读元素innerHTML | ✓ |
44
+ | `innerText` | `selector`:string | {ok,selector,value} | 读元素innerText | ✓ |
45
+ | `textContent` | `selector`:string | {ok,selector,value} | 读元素textContent | ✓ |
46
+ | `getAttribute` | `selector`:string, `name`:string | {ok,selector,name,value} | 读元素属性 | ✓ |
47
+ | `inputValue` | `selector`:string, `timeout`?:number=10000 | {ok,selector,value} | 读input/select当前值 | ✓ |
48
+ | `boundingBox` | `selector`:string | {ok,selector,x,y,width,height} | 读元素包围盒(坐标+尺寸) | ✓ |
49
+ | `count` | `selector`:string | {ok,selector,count} | 统计selector匹配数 | ✓ |
50
+ | `snapshot` | `depth`?:number, `timeout`?:number=15000 | {ok,yaml,totalChars} | aria无障碍快照(返回yaml) | ✓ |
51
+
52
+ ### 进阶
53
+
54
+ | action | params | returns | description | ext |
55
+ | --- | --- | --- | --- | --- |
56
+ | `operateSequence` | `steps`:array | {ok,results} | 原子序列执行(locate/click/wait在同一页面上下文串行) | ✓ |
57
+ | `setDialogHandler` | `handler`:string | {ok} | 设置JS对话框处理 | 仅 ext |
58
+ | `operateSequence` | `steps`:array | {ok,results} | 原子序列执行 | ✓ |
59
+ | `evaluate` | `source`:string, `args`?:any | 由函数返回值决定 | 执行页面JS函数(返回其结果) | ✓ |
60
+ | `setInputFiles` | `selector`:string, `files`:array, `timeout`?:number=10000 | {ok,selector,count} | 上传文件到file input | ✓ |
61
+
62
+ ### 导航
63
+
64
+ | action | params | returns | description | ext |
65
+ | --- | --- | --- | --- | --- |
66
+ | `goto` | `url`:string | {ok,url} | 导航到URL,等待DOMContentLoaded | ✓ |
67
+ | `goBack` | `timeout`?:number=15000 | {ok,url} | 后退一页 | ✓ |
68
+ | `goForward` | `timeout`?:number=15000 | {ok,url} | 前进一页 | ✓ |
69
+ | `reload` | `timeout`?:number=15000 | {ok,url} | 刷新当前页 | ✓ |
70
+ | `status` | — | {ok,url,title,readyState,textLength} | 读当前页状态(url/title/readyState) | ✓ |
71
+
72
+ ### 等待
73
+
74
+ | action | params | returns | description | ext |
75
+ | --- | --- | --- | --- | --- |
76
+ | `waitForLoadState` | `state`?:string=load, `timeout`?:number=30000 | {ok} | 等待指定加载状态 | ✓ |
77
+ | `waitForSelector` | `selector`:string, `timeout`?:number=30000 | {ok,selector,visible,inViewport} | 等待selector元素可见 | ✓ |
78
+ | `waitForFunction` | `source`:string, `timeout`?:number=30000 | 由函数返回值决定 | 等待页面函数返回truthy | ✗ (extension路CSP拦截eval) |
79
+ | `waitForURL` | `url`:string, `timeout`?:number=30000 | {ok,url} | 等待URL匹配 | ✓ |
80
+ | `waitForTimeout` | `ms`?:number=1000 | {ok,ms} | 固定等待 | ✓ |
81
+
82
+ ### 标签页
83
+
84
+ | action | params | returns | description | ext |
85
+ | --- | --- | --- | --- | --- |
86
+ | `closeTab` | — | {ok} | 关闭当前page | ✓ |
87
+ | `activate` | — | {ok} | 切到最前(仅可视) | ✓ |
88
+
89
+ ### 截图
90
+
91
+ | action | params | returns | description | ext |
92
+ | --- | --- | --- | --- | --- |
93
+ | `screenshot` | — | {ok,format,dataUrl} | 截整页PNG(返回dataUrl) | ✓ |
94
+
95
+ ### 存储
96
+
97
+ | action | params | returns | description | ext |
98
+ | --- | --- | --- | --- | --- |
99
+ | `getLocalStorage` | `keys`?:array | {ok,storage} | 读localStorage(不传keys读全部) | ✓ |
100
+ | `setLocalStorage` | `items`:object | {ok,count} | 写localStorage | ✓ |
101
+ | `removeLocalStorage` | `keys`?:array | {ok,count} | 删localStorage(不传keys删全部) | ✓ |
102
+ | `clearLocalStorage` | — | {ok} | 清空localStorage | ✓ |
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}