product-planning(同步 26 改 65 新)
- 目录约定:docs/pm/<项目>/ ⇒ <工作区>/项目/<产品slug>/ 为产品根,下分 产品规划/ 与 产品原型/{designs,pages}
- ④段骨架六节 ⇒ 七节(新增「原型地址」;后两节改「页面导航」「原型状态取值」)
- ③段新增真机组件库;结案「源站只有暗色主题」,tokens.css(亮色)标为不可用于生产
- ③段改版规则反转为「一个产品只用一套原型、只留当前态」;②段新增 产品规划模板.md(表头唯一定义处)
- scripts:产品根定位重写(含 .registry/current-project 指针),新增 check_terms.py 与 selftest_product_root.py
session-mechanism(同步 3 改 2 新)
- references/06-技能优化规程.md 新增 §5「要求落在生成时看得见的面上」,总表 16 ⇒ 17 条;SKILL.md 同步
- scripts/collabctl.py 取回 _find_board() 候选定位(旧版写死 scripts/board.py,而看板已搬进 boards/会话看板/ ⇒ 旧版本就是坏的)
element-picker(新装)
- 按用户口径作「独立功能加入 product-planning」,非独立全局技能
- 修掉它自身三处路径漂移(技能根写法 ⇒ 同目录;README 两处旧落点;剪贴板示例文案与代码对齐)
工具归拢到技能级 assets/
- diagram-design / design-capture / element-picker / video-capture 从各段 assets/ 移到技能包 assets/
- 新增红线:本包有两处 assets/(技能级工具箱 vs 段内资产),引工具一律写「技能级 assets/<工具名>/」
- 路径口径由两类改三类(技能级工具箱 / 本段自带资产 / 同根技能)
不入库(磁盘保留)
- design-system-tiaoyue 整个目录(用户 2026-10-16 令):git rm -r --cached + .gitignore,理由=14 MB 且可 CDP 重采;
已在 ③段 SKILL.md 注明「取仓后这一层是空的属正常,用自带套需另取或重采」
- element-picker 的运行期存档 marks/(服务自己会建目录)
126 lines
4.4 KiB
JavaScript
126 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}`);
|
|
});
|