125 lines
4.4 KiB
JavaScript
125 lines
4.4 KiB
JavaScript
#!/usr/bin/env node
|
|||
|
|
/**
|
||
|
|
* ui-picker-server.js —— 极简「页面元素拾取」服务(零依赖)
|
||
|
|
*
|
||
|
|
* 干什么:给浏览器里的拾取器提供一个落脚点 ——
|
||
|
|
* 页面点选元素后,把「选择器 / 网页 / 文本 / class / 关键样式 / HTML 片段」
|
||
|
|
* 追加写进本技能目录下的 marks/_ui-marks.json,供 AI 读取后精确改代码。
|
||
|
|
*
|
||
|
|
* 起法: node ui-picker-server.js [端口] (在本目录下执行,默认 8890)
|
||
|
|
* 页面: <script src="http://127.0.0.1:8890/picker.js"></script>
|
||
|
|
* URL 带 ?pick 才激活;按 Z 切拾取模式;点元素 → 确认窗 → 「确认」
|
||
|
|
*
|
||
|
|
* 端点:
|
||
|
|
* GET /picker.js 注入脚本
|
||
|
|
* POST /mark 存一条标记(追加写盘)
|
||
|
|
* GET /marks 读回全部标记(JSON)
|
||
|
|
* POST /clear 清空标记
|
||
|
|
* GET /health 存活检查
|
||
|
|
*/
|
||
|
|
const http = require('http');
|
||
|
|
const fs = require('fs');
|
||
|
|
const path = require('path');
|
||
|
|
|
||
|
|
const PORT = parseInt(process.argv[2], 10) || 8890;
|
||
|
|
// 存档跟技能一起放,搬到哪都自洽
|
||
|
|
const OUT_DIR = path.join(__dirname, 'marks');
|
||
|
|
const OUT_FILE = path.join(OUT_DIR, '_ui-marks.json');
|
||
|
|
|
||
|
|
function readMarks() {
|
||
|
|
try {
|
||
|
|
const raw = fs.readFileSync(OUT_FILE, 'utf8');
|
||
|
|
const obj = JSON.parse(raw);
|
||
|
|
return Array.isArray(obj.marks) ? obj : { updated: '', marks: [] };
|
||
|
|
} catch {
|
||
|
|
return { updated: '', marks: [] };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function writeMarks(obj) {
|
||
|
|
fs.mkdirSync(OUT_DIR, { recursive: true });
|
||
|
|
obj.updated = new Date().toISOString();
|
||
|
|
fs.writeFileSync(OUT_FILE, JSON.stringify(obj, null, 2), 'utf8');
|
||
|
|
}
|
||
|
|
|
||
|
|
function send(res, code, body, type) {
|
||
|
|
res.writeHead(code, {
|
||
|
|
'Content-Type': type || 'application/json; charset=utf-8',
|
||
|
|
'Access-Control-Allow-Origin': '*',
|
||
|
|
'Access-Control-Allow-Headers': 'Content-Type',
|
||
|
|
'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
|
||
|
|
'Cache-Control': 'no-store',
|
||
|
|
});
|
||
|
|
res.end(body);
|
||
|
|
}
|
||
|
|
|
||
|
|
function readBody(req) {
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
let buf = '';
|
||
|
|
req.on('data', (c) => {
|
||
|
|
buf += c;
|
||
|
|
if (buf.length > 8 * 1024 * 1024) reject(new Error('body too large'));
|
||
|
|
});
|
||
|
|
req.on('end', () => resolve(buf));
|
||
|
|
req.on('error', reject);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
const server = http.createServer(async (req, res) => {
|
||
|
|
const url = new URL(req.url, `http://127.0.0.1:${PORT}`);
|
||
|
|
|
||
|
|
if (req.method === 'OPTIONS') return send(res, 204, '');
|
||
|
|
|
||
|
|
if (req.method === 'GET' && url.pathname === '/health') {
|
||
|
|
return send(res, 200, JSON.stringify({ ok: true, skill: __dirname, file: OUT_FILE, count: readMarks().marks.length }));
|
||
|
|
}
|
||
|
|
|
||
|
|
if (req.method === 'GET' && url.pathname === '/picker.js') {
|
||
|
|
return send(res, 200, fs.readFileSync(path.join(__dirname, 'picker.js'), 'utf8'), 'application/javascript; charset=utf-8');
|
||
|
|
}
|
||
|
|
|
||
|
|
if (req.method === 'GET' && url.pathname === '/marks') {
|
||
|
|
return send(res, 200, fs.readFileSync(OUT_FILE, 'utf8').toString());
|
||
|
|
}
|
||
|
|
|
||
|
|
if (req.method === 'POST' && url.pathname === '/mark') {
|
||
|
|
try {
|
||
|
|
const body = JSON.parse(await readBody(req));
|
||
|
|
const store = readMarks();
|
||
|
|
const item = {
|
||
|
|
id: store.marks.length + 1,
|
||
|
|
at: new Date().toISOString(),
|
||
|
|
page: body.page || '',
|
||
|
|
note: body.note || '',
|
||
|
|
selector: body.selector || '',
|
||
|
|
breadcrumb: body.breadcrumb || '',
|
||
|
|
tag: body.tag || '',
|
||
|
|
classes: body.classes || [],
|
||
|
|
text: body.text || '',
|
||
|
|
attrs: body.attrs || {},
|
||
|
|
rect: body.rect || null,
|
||
|
|
styles: body.styles || {},
|
||
|
|
html: body.html || '',
|
||
|
|
};
|
||
|
|
store.marks.push(item);
|
||
|
|
writeMarks(store);
|
||
|
|
console.log(`[mark #${item.id}] ${item.selector} ← ${item.note}`);
|
||
|
|
return send(res, 200, JSON.stringify({ ok: true, id: item.id, total: store.marks.length }));
|
||
|
|
} catch (e) {
|
||
|
|
return send(res, 400, JSON.stringify({ ok: false, error: String(e.message || e) }));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (req.method === 'POST' && url.pathname === '/clear') {
|
||
|
|
writeMarks({ updated: '', marks: [] });
|
||
|
|
return send(res, 200, JSON.stringify({ ok: true, total: 0 }));
|
||
|
|
}
|
||
|
|
|
||
|
|
return send(res, 404, JSON.stringify({ ok: false, error: 'not found' }));
|
||
|
|
});
|
||
|
|
|
||
|
|
server.listen(PORT, '127.0.0.1', () => {
|
||
|
|
console.log(`元素拾取服务已启动: http://127.0.0.1:${PORT}`);
|
||
|
|
console.log(`页面引入: <script src="http://127.0.0.1:${PORT}/picker.js"></script>`);
|
||
|
|
console.log(`标记落盘: ${OUT_FILE}`);
|
||
|
|
});
|