#!/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}`); });