#!/usr/bin/env node
/**
* ui-picker-server.js —— 极简「页面元素拾取」服务(零依赖)
*
* 干什么:给浏览器里的拾取器提供一个落脚点 ——
* 页面点选元素后,把「选择器 / 网页 / 文本 / class / 关键样式 / HTML 片段」
* 追加写进本技能目录下的 marks/_ui-marks.json,供 AI 读取后精确改代码。
*
* 起法: node ui-picker-server.js [端口] (在本目录下执行,默认 8890)
* 页面:
* 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(`页面引入: `);
console.log(`标记落盘: ${OUT_FILE}`);
});