Initial commit: 让多个 AI 智能体通过文件总线在共享聊天室里互相 @、协作与交接任务
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* ui-shot.mjs — 给群聊台拍一张截图,用来肉眼审图。
|
||||
*
|
||||
* 用法:node tools/ui-shot.mjs [url] [--out 文件.png] [--w 1440] [--h 900] [--wait 2500] [--dark|--light]
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
const URL_ = argv[0] && !argv[0].startsWith('--') ? argv[0] : 'http://127.0.0.1:3099/';
|
||||
const flag = (n, d) => { const i = argv.indexOf('--' + n); return i > -1 && argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[i + 1] : d; };
|
||||
const OUT = path.resolve(String(flag('out', path.join(process.cwd(), 'ui-shot.png'))));
|
||||
const W = Number(flag('w', 1480));
|
||||
const H = Number(flag('h', 940));
|
||||
const WAIT = Number(flag('wait', 2600));
|
||||
const THEME = argv.includes('--light') ? 'light' : 'dark';
|
||||
const PORT = 9333;
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
const EDGE = ['C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
|
||||
'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe'].find((p) => fs.existsSync(p));
|
||||
if (!EDGE) { console.error('找不到 msedge.exe'); process.exit(2); }
|
||||
|
||||
const profile = fs.mkdtempSync(path.join(os.tmpdir(), 'aig-shot-'));
|
||||
const edge = spawn(EDGE, [
|
||||
'--headless=new', '--disable-gpu', '--no-first-run', '--no-default-browser-check', '--disable-extensions',
|
||||
'--hide-scrollbars', `--window-size=${W},${H}`, `--remote-debugging-port=${PORT}`, `--user-data-dir=${profile}`, 'about:blank',
|
||||
], { stdio: 'ignore', windowsHide: true });
|
||||
|
||||
async function wsUrl() {
|
||||
for (let i = 0; i < 60; i++) {
|
||||
try {
|
||||
const list = await (await fetch(`http://127.0.0.1:${PORT}/json/list`)).json();
|
||||
const p = list.find((t) => t.type === 'page' && t.webSocketDebuggerUrl);
|
||||
if (p) return p.webSocketDebuggerUrl;
|
||||
} catch { /* 等 */ }
|
||||
await sleep(250);
|
||||
}
|
||||
throw new Error('调试端口没起来');
|
||||
}
|
||||
|
||||
let ws;
|
||||
try {
|
||||
ws = new WebSocket(await wsUrl());
|
||||
await new Promise((res, rej) => { ws.addEventListener('open', res, { once: true }); ws.addEventListener('error', () => rej(new Error('CDP 失败')), { once: true }); });
|
||||
let id = 0; const pend = new Map();
|
||||
ws.addEventListener('message', (ev) => {
|
||||
const m = JSON.parse(ev.data);
|
||||
if (m.id && pend.has(m.id)) { const { resolve, reject } = pend.get(m.id); pend.delete(m.id); m.error ? reject(new Error(m.error.message)) : resolve(m.result); }
|
||||
});
|
||||
const send = (method, params = {}) => new Promise((resolve, reject) => {
|
||||
const i = ++id; pend.set(i, { resolve, reject });
|
||||
ws.send(JSON.stringify({ id: i, method, params }));
|
||||
setTimeout(() => { if (pend.has(i)) { pend.delete(i); reject(new Error(method + ' 超时')); } }, 30000);
|
||||
});
|
||||
const evaluate = (expr) => send('Runtime.evaluate', { expression: expr, returnByValue: true });
|
||||
|
||||
await send('Runtime.enable');
|
||||
await send('Page.enable');
|
||||
await send('Emulation.setDeviceMetricsOverride', { width: W, height: H, deviceScaleFactor: 2, mobile: false });
|
||||
await send('Page.navigate', { url: URL_ });
|
||||
for (let i = 0; i < 60; i++) { await sleep(250); const n = (await evaluate('document.querySelectorAll(".mcard").length')).result?.value || 0; if (n > 0) break; }
|
||||
await evaluate(`try { localStorage.setItem('aig.theme', ${JSON.stringify(THEME)}); } catch(e) {}; document.documentElement.dataset.theme = ${JSON.stringify(THEME)}; true`);
|
||||
await sleep(WAIT);
|
||||
const shot = await send('Page.captureScreenshot', { format: 'png', captureBeyondViewport: false });
|
||||
fs.writeFileSync(OUT, Buffer.from(shot.data, 'base64'));
|
||||
const stat = await evaluate('JSON.stringify({members: document.querySelectorAll(".mcard").length, msgs: document.querySelectorAll(".row:not(.typing)").length, typing: document.querySelectorAll(".row.typing").length, strip: document.getElementById("strip").textContent.trim()})');
|
||||
console.log('已保存 ' + OUT + ' ' + Math.round(fs.statSync(OUT).size / 1024) + 'KB');
|
||||
console.log('页面状态:' + stat.result.value);
|
||||
} catch (e) {
|
||||
console.error('截图失败:' + e.message);
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
try { ws?.close(); } catch { /* 忽略 */ }
|
||||
try { edge.kill(); } catch { /* 忽略 */ }
|
||||
await sleep(300);
|
||||
try { fs.rmSync(profile, { recursive: true, force: true }); } catch { /* 忽略 */ }
|
||||
}
|
||||
Reference in New Issue
Block a user