Files
MuyueWorkspace/extension/src/lib/page-rpc.js
Augustin 9f9f2bd2c6
Some checks failed
Beta Release / beta (push) Failing after 48s
feat(extension): browser extension for Chrome/Edge/Firefox + CI + v0.8.0
Adds a WXT-based browser extension that replaces manual JS snippet
injection for AI-driven browser testing. The extension auto-connects
to the Muyue server via WebSocket on every page, using the exact
same protocol as the existing snippet — zero backend changes needed.

- Chrome/Edge (MV3) + Firefox (MV2) from single codebase via WXT
- Content script: auto-connect WS, console capture, URL tracking, RPC
- Background service worker: token management, screenshots, badge
- Popup + side panel with server status, sessions, URL config
- CI workflows: build extension, attach .zip to releases
- Makefile targets: ext, ext-chrome, ext-firefox, ext-zip
- Version bumped to 0.8.0

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-27 16:50:04 +02:00

114 lines
3.0 KiB
JavaScript

let lastList = [];
function safeText(el) {
let t = (el.innerText || el.textContent || '').trim();
if (t.length > 80) t = t.slice(0, 80) + '…';
return t;
}
function describe(el) {
let sel = el.id ? '#' + el.id : el.tagName.toLowerCase();
if (!el.id && el.className && typeof el.className === 'string') {
sel += '.' + el.className.trim().split(/\s+/).slice(0, 2).join('.');
}
const label = el.getAttribute('aria-label') || el.getAttribute('title') || el.getAttribute('name') || '';
return {
tag: el.tagName.toLowerCase(),
selector: sel,
text: safeText(el),
label,
type: el.getAttribute('type') || '',
disabled: !!el.disabled,
};
}
export function listClickables() {
const els = Array.from(
document.querySelectorAll(
'button, a[href], input[type=submit], input[type=button], [role=button], [onclick]'
)
);
lastList = els.filter((e) => {
const r = e.getBoundingClientRect();
return r.width > 0 && r.height > 0;
});
return lastList.map((el, i) => {
const d = describe(el);
d.index = i;
return d;
});
}
export function clickElement(params) {
let el;
if (params.selector) el = document.querySelector(params.selector);
else if (typeof params.index === 'number') el = lastList[params.index];
if (!el) return { ok: false, error: 'element not found' };
if (el.disabled) return { ok: false, error: 'element is disabled' };
try {
el.scrollIntoView({ block: 'center' });
el.click();
return { ok: true };
} catch (e) {
return { ok: false, error: String(e) };
}
}
export function typeText(params) {
let el;
if (params.selector) el = document.querySelector(params.selector);
else if (typeof params.index === 'number') el = lastList[params.index];
if (!el) return { ok: false, error: 'element not found' };
const proto = Object.getPrototypeOf(el);
const setter = Object.getOwnPropertyDescriptor(proto, 'value');
try {
if (setter && setter.set) setter.set.call(el, params.text || '');
else el.value = params.text || '';
} catch {
el.value = params.text || '';
}
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
return { ok: true };
}
export function evalExpr(params) {
try {
const r = (0, eval)(params.expr);
return { ok: true, value: serialize(r) };
} catch (e) {
return { ok: false, error: String(e) };
}
}
export function currentUrl() {
return { url: location.href, title: document.title };
}
function serialize(v) {
if (v === undefined) return 'undefined';
try {
return JSON.parse(JSON.stringify(v));
} catch {
return String(v);
}
}
export function dispatch(msg) {
const p = msg.params || {};
switch (msg.action) {
case 'list_clickables':
return listClickables();
case 'click':
return clickElement(p);
case 'eval':
return evalExpr(p);
case 'current_url':
return currentUrl();
case 'type':
return typeText(p);
default:
return { ok: false, error: 'unknown action: ' + msg.action };
}
}