add browser skill

This commit is contained in:
cexll
2026-01-08 11:33:19 +08:00
parent 40e2d00d35
commit bdf62d0f1c
9 changed files with 419 additions and 0 deletions

62
skills/browser/scripts/eval.cjs Executable file
View File

@@ -0,0 +1,62 @@
#!/usr/bin/env node
// Execute JavaScript in the active browser tab
const http = require('http');
const WebSocket = require('ws');
const code = process.argv[2];
if (!code) {
console.error('Usage: eval.js <javascript-expression>');
process.exit(1);
}
async function getTargets() {
return new Promise((resolve, reject) => {
http.get('http://localhost:9222/json', res => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => resolve(JSON.parse(data)));
}).on('error', reject);
});
}
(async () => {
try {
const targets = await getTargets();
const page = targets.find(t => t.type === 'page');
if (!page) throw new Error('No active page found');
const ws = new WebSocket(page.webSocketDebuggerUrl);
ws.on('open', () => {
ws.send(JSON.stringify({
id: 1,
method: 'Runtime.evaluate',
params: {
expression: code,
returnByValue: true,
awaitPromise: true
}
}));
});
ws.on('message', data => {
const msg = JSON.parse(data);
if (msg.id === 1) {
ws.close();
if (msg.result.exceptionDetails) {
console.error('Error:', msg.result.exceptionDetails.text);
process.exit(1);
}
console.log(JSON.stringify(msg.result.result.value ?? msg.result.result));
}
});
ws.on('error', e => {
console.error('WebSocket error:', e.message);
process.exit(1);
});
} catch (e) {
console.error('Error:', e.message);
process.exit(1);
}
})();

70
skills/browser/scripts/nav.cjs Executable file
View File

@@ -0,0 +1,70 @@
#!/usr/bin/env node
// Navigate to URL in current or new tab
const http = require('http');
const url = process.argv[2];
const newTab = process.argv.includes('--new');
if (!url) {
console.error('Usage: nav.js <url> [--new]');
process.exit(1);
}
async function getTargets() {
return new Promise((resolve, reject) => {
http.get('http://localhost:9222/json', res => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => resolve(JSON.parse(data)));
}).on('error', reject);
});
}
async function createTab(url) {
return new Promise((resolve, reject) => {
http.get(`http://localhost:9222/json/new?${encodeURIComponent(url)}`, res => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => resolve(JSON.parse(data)));
}).on('error', reject);
});
}
async function navigate(targetId, url) {
const WebSocket = require('ws');
const targets = await getTargets();
const target = targets.find(t => t.id === targetId);
return new Promise((resolve, reject) => {
const ws = new WebSocket(target.webSocketDebuggerUrl);
ws.on('open', () => {
ws.send(JSON.stringify({ id: 1, method: 'Page.navigate', params: { url } }));
});
ws.on('message', data => {
const msg = JSON.parse(data);
if (msg.id === 1) {
ws.close();
resolve(msg.result);
}
});
ws.on('error', reject);
});
}
(async () => {
try {
if (newTab) {
const tab = await createTab(url);
console.log(JSON.stringify({ action: 'created', tabId: tab.id, url }));
} else {
const targets = await getTargets();
const page = targets.find(t => t.type === 'page');
if (!page) throw new Error('No active page found');
await navigate(page.id, url);
console.log(JSON.stringify({ action: 'navigated', tabId: page.id, url }));
}
} catch (e) {
console.error('Error:', e.message);
process.exit(1);
}
})();

87
skills/browser/scripts/pick.cjs Executable file
View File

@@ -0,0 +1,87 @@
#!/usr/bin/env node
// Visual element picker - click to select DOM nodes
const http = require('http');
const WebSocket = require('ws');
const hint = process.argv[2] || 'Click an element to select it';
async function getTargets() {
return new Promise((resolve, reject) => {
http.get('http://localhost:9222/json', res => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => resolve(JSON.parse(data)));
}).on('error', reject);
});
}
const pickerScript = `
(function(hint) {
return new Promise(resolve => {
const overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;z-index:999999;cursor:crosshair;';
const label = document.createElement('div');
label.textContent = hint;
label.style.cssText = 'position:fixed;top:10px;left:50%;transform:translateX(-50%);background:#333;color:#fff;padding:8px 16px;border-radius:4px;z-index:1000000;font:14px sans-serif;';
document.body.appendChild(overlay);
document.body.appendChild(label);
overlay.onclick = e => {
overlay.remove();
label.remove();
const el = document.elementFromPoint(e.clientX, e.clientY);
if (!el) return resolve(null);
const rect = el.getBoundingClientRect();
resolve({
tag: el.tagName.toLowerCase(),
id: el.id || null,
classes: [...el.classList],
text: el.textContent?.slice(0, 100)?.trim() || null,
href: el.href || null,
selector: el.id ? '#' + el.id : el.className ? el.tagName.toLowerCase() + '.' + [...el.classList].join('.') : el.tagName.toLowerCase(),
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }
});
};
});
})`;
(async () => {
try {
const targets = await getTargets();
const page = targets.find(t => t.type === 'page');
if (!page) throw new Error('No active page found');
const ws = new WebSocket(page.webSocketDebuggerUrl);
ws.on('open', () => {
ws.send(JSON.stringify({
id: 1,
method: 'Runtime.evaluate',
params: {
expression: `${pickerScript}(${JSON.stringify(hint)})`,
returnByValue: true,
awaitPromise: true
}
}));
});
ws.on('message', data => {
const msg = JSON.parse(data);
if (msg.id === 1) {
ws.close();
console.log(JSON.stringify(msg.result.result.value, null, 2));
}
});
ws.on('error', e => {
console.error('WebSocket error:', e.message);
process.exit(1);
});
} catch (e) {
console.error('Error:', e.message);
process.exit(1);
}
})();

View File

@@ -0,0 +1,54 @@
#!/usr/bin/env node
// Capture screenshot of the active browser tab
const http = require('http');
const WebSocket = require('ws');
const fs = require('fs');
const path = require('path');
const os = require('os');
async function getTargets() {
return new Promise((resolve, reject) => {
http.get('http://localhost:9222/json', res => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => resolve(JSON.parse(data)));
}).on('error', reject);
});
}
(async () => {
try {
const targets = await getTargets();
const page = targets.find(t => t.type === 'page');
if (!page) throw new Error('No active page found');
const ws = new WebSocket(page.webSocketDebuggerUrl);
ws.on('open', () => {
ws.send(JSON.stringify({
id: 1,
method: 'Page.captureScreenshot',
params: { format: 'png' }
}));
});
ws.on('message', data => {
const msg = JSON.parse(data);
if (msg.id === 1) {
ws.close();
const filename = `screenshot-${Date.now()}.png`;
const filepath = path.join(os.tmpdir(), filename);
fs.writeFileSync(filepath, Buffer.from(msg.result.data, 'base64'));
console.log(JSON.stringify({ path: filepath, filename }));
}
});
ws.on('error', e => {
console.error('WebSocket error:', e.message);
process.exit(1);
});
} catch (e) {
console.error('Error:', e.message);
process.exit(1);
}
})();

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env node
// Launch Chrome with remote debugging on port 9222
const { execSync, spawn } = require('child_process');
const path = require('path');
const os = require('os');
const useProfile = process.argv.includes('--profile');
const port = 9222;
// Find Chrome executable
const chromePaths = {
darwin: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
linux: '/usr/bin/google-chrome',
win32: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
};
const chromePath = chromePaths[process.platform];
// Build args
const args = [
`--remote-debugging-port=${port}`,
'--no-first-run',
'--no-default-browser-check'
];
if (useProfile) {
const profileDir = path.join(os.homedir(), '.chrome-debug-profile');
args.push(`--user-data-dir=${profileDir}`);
} else {
args.push(`--user-data-dir=${path.join(os.tmpdir(), 'chrome-debug-' + Date.now())}`);
}
console.log(`Starting Chrome on port ${port}${useProfile ? ' (with profile)' : ''}...`);
const chrome = spawn(chromePath, args, { detached: true, stdio: 'ignore' });
chrome.unref();
console.log(`Chrome launched (PID: ${chrome.pid})`);