技能库同步:另一台机的 2026-10-16 包 + element-picker 入包 + 工具归拢到技能级 assets/

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/(服务自己会建目录)
This commit is contained in:
admin committed 2026-10-16 23:13:33 +08:00
1 parent 749c0e3d5d
commit 8160f91559
336 files changed
+12821 -6621

No files matched your search

@@ -0,0 +1,328 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
design-capture · 设计系统模板生成器(③段工具)
做什么:把抓取 raw JSON(capture_design_system.mjs 的产出)转成一套「可选样式」模板包,
与 skills/.../assets/design-systems/design-system-tiaoyue/ 同构,可直接被 D1 定调选用。
输出(默认写入 --out 目录):
SKILL.md 规范总纲(含七语义色、排版、几何、组件用法、生成步骤)
assets/tokens.css 亮色令牌 :root{}
assets/tokens-dark.css 暗色令牌(仅当提供 --dark-raw 时生成)
assets/brand.json 品牌画像(色/字/几何/来源)
references/components.md 组件族人类可读目录
references/library.json 机器可读全量(原始 data)
references/design-system.html 抓取出的设计系统原始 HTML
references/capture-notes.md 抓取记录与已知缺口
依赖:仅 Python 3 标准库。
用法:
python build_style_package.py --raw raw.json --out ./design-system-xxx [--name "跳跃视界"] [--dark-raw dark.json]
"""
import argparse
import json
import os
import re
import sys
from datetime import datetime, timezone
ROLE_TOKEN = {
"background": "--background",
"surface": "--surface",
"foreground": "--foreground",
"muted": "--muted",
"faint": "--faint",
"border": "--border",
"accent": "--accent",
"accent-secondary": "--accent-secondary",
"highlight": "--accent-2",
}
# 角色 → 常用别名(同义归并,便于生成器/原型直接引用)
ROLE_ALIAS = {
"background": "--c-bg",
"surface": "--c-surface",
"foreground": "--c-text",
"muted": "--c-text-2",
"border": "--c-border",
"accent": "--c-accent",
}
def digits_only(s):
return re.sub(r"[^0-9a-zA-Z]+", "-", str(s or "")).strip("-").lower()
def slugify(name):
s = digits_only(name)
return s or "captured-style"
def hex_to_rgb(h):
h = str(h or "").strip().lstrip("#")
if len(h) == 3:
h = "".join(c * 2 for c in h)
if len(h) != 6:
return None
try:
return tuple(int(h[i:i + 2], 16) for i in (0, 2, 4))
except ValueError:
return None
def rel_luminance(hex_color):
rgb = hex_to_rgb(hex_color)
if not rgb:
return 0.5
def f(c):
c = c / 255.0
return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4
r, g, b = (f(c) for c in rgb)
return 0.2126 * r + 0.7152 * g + 0.0722 * b
def contrast_text(hex_color):
return "#FFFFFF" if rel_luminance(hex_color) < 0.5 else "#1A1A18"
def tokens_from_colors(colors, layout):
"""colors: [{role, hex}] → ordered token lines."""
lines = []
slots = []
for c in colors or []:
role = (c or {}).get("role")
hexv = (c or {}).get("hex")
if role and hexv:
slots.append((role, hexv))
out = {}
for role, hexv in slots:
tok = ROLE_TOKEN.get(role, "--" + digits_only(role))
out.setdefault(tok, hexv)
# 兜底:确保核心角色存在
out.setdefault("--background", "#FFFFFF")
out.setdefault("--surface", "#FFFFFF")
out.setdefault("--foreground", "#1A1A18")
out.setdefault("--muted", "#57564F")
out.setdefault("--border", "#E7E5DC")
out.setdefault("--accent", "#C96442")
out["--on-accent"] = contrast_text(out["--accent"])
out.setdefault("--ok", "#3D7A4F")
out.setdefault("--err", "#B4453A")
if layout and layout.get("radius") and layout["radius"] != "—":
out["--radius"] = layout["radius"]
# 别名
for role, tok in ROLE_ALIAS.items():
src = ROLE_TOKEN.get(role)
if src and src in out:
out[("alias:" + tok)] = out[src]
return out
def write_tokens(path, tokens):
alias = [(k.split(":", 1)[1], v) for k, v in tokens.items() if k.startswith("alias:")]
base = [(k, v) for k, v in tokens.items() if not k.startswith("alias:")]
with open(path, "w", encoding="utf-8") as f:
f.write(":root{\n")
f.write(" /* primitive / 语义令牌(由 design-capture 从源站实测生成) */\n")
for k, v in base:
f.write(f" {k}:{v};\n")
if alias:
f.write(" /* 常用别名 */\n")
for k, v in alias:
f.write(f" {k}:{v};\n")
f.write("}\n")
def md_components(components, typography):
lines = ["# 组件目录(抓取)", ""]
order = ["button", "input", "card", "nav"]
labels = {"button": "按钮", "input": "输入框", "card": "卡片", "nav": "导航/头部"}
for key in order:
prof = (components or {}).get(key) or {}
lines.append(f"## {labels.get(key, key)}(`{key}`)")
if not prof:
lines.append("- (未抓到该组件样本)")
lines.append("")
continue
for pk, pv in prof.items():
if pv in (None, "", "none"):
continue
lines.append(f"- `{pk}`: {pv}")
lines.append("")
fams = (typography or {}).get("families") or []
specs = (typography or {}).get("specs") or []
lines.append("## 排版")
lines.append(f"- 字体族:{', '.join(fams) if fams else '(未抓到)'}")
for s in specs:
if not isinstance(s, dict):
continue
lines.append(
f"- `{s.get('selector','')}` → {s.get('family','')} / {s.get('weight','')} / {s.get('size','')} / lh {s.get('lineHeight','')}"
)
lines.append("")
return "\n".join(lines)
def md_capture_notes(meta, data, has_dark):
content = (data or {}).get("content") or {}
lines = [
"# 抓取记录(design-capture)",
"",
f"- 源站:{meta.get('requestedUrl') or content.get('url') or '—'}",
f"- 抓取时间:{meta.get('capturedAt') or '—'}",
f"- 工具:design-capture v{meta.get('toolVersion','1.0')}(抽取脚本搬运自 open-design clipper,Apache-2.0)",
f"- CDP 端口:{meta.get('cdpPort','—')}",
f"- 页面标题:{content.get('title','—')}",
"",
"## 已知缺口(如实登记,⛔ 不要当已覆盖)",
"",
f"- 暗色令牌:{'已随 --dark-raw 一并生成' if has_dark else '未抓取(源站亮色为准;要暗色需另跑带 dark 的采集)'}",
"- 组件为「页面代表性样本」而非全站枚举:抓取只取每类一个最优样本(button/input/card/nav)。",
"- 动态类名/运行时样式:源站若用 Vue scoped 或运行时 JS 决定样式,快照只反映抓取时刻。",
"- 跨域样式表受限:跨域 CSS 若禁止读取,其规则不会进入组件/字体统计。",
"",
]
return "\n".join(lines)
def build_skill_md(name, data, tokens):
content = (data or {}).get("content") or {}
brand = (data or {}).get("brand") or {}
layout = (data or {}).get("layout") or {}
colors = brand.get("colors") or []
color_rows = "\n".join(
f"- `{c.get('role','')}` = `{c.get('hex','')}`" for c in colors if isinstance(c, dict)
)
slug = slugify(name)
desc = (
f"{name} 的设计规范与组件库 —— 按此规范生成「{name} 风格」的界面时使用。"
f"含语义色令牌、排版、几何,以及从源站实测提取的组件样本。"
f"当用户说「按 {name} 的风格做一页」「用 {slug} 的设计系统生成」时触发。"
)
return f"""---
name: design-system-{slug}
description: {desc}
agent_created: true
source: design-capture(源站实测,脚本搬运自 open-design clipper · Apache-2.0)
---
# {name} 设计规范
从 `{content.get('url') or brand.get('sourceUrl') or '源站'}` **真实页面实测提取**的设计系统 + 组件样本,用于生成同风格界面。
**核心纪律**:本规范每个值都来自**实测**,⛔ 不要凭印象改色值/圆角/命名,⛔ 不要用通用默认值顶替。
## 一、覆盖范围
- **设计令牌**:语义色 + 排版 + 几何 → `assets/tokens.css`(亮色)
{('| **`assets/tokens-dark.css`**(暗色,随 --dark-raw 生成)' if False else '')}
- **机器可读全量**:`references/library.json`(原始结构化 `data`)
- **人可读组件目录**:`references/components.md`
- **设计系统原始产出**:`references/design-system.html`
- **抓取记录与缺口**:`references/capture-notes.md`
## 二、语义色(实测值)
{color_rows or '- (未抓到色板)'}
- `--on-accent` = `{tokens.get('--on-accent','')}`(强调色上的文字,按对比度自动取黑/白)
## 三、几何与排版
- 圆角:`{layout.get('radius','—')}`
- 姿态:{(';'.join(layout.get('postureRules') or []) or '—')}
- 排版:见 `references/components.md`
## 四、组件怎么用
优先级:① 先用 `references/components.md` 里的样本组件 → ② 没有才自创,且沿用同一套令牌。
组件来自源站代表性样本(button/input/card/nav),非全站枚举。
## 五、生成一个页面的步骤
1. 读 `assets/tokens.css`,把 `:root` 令牌整段纳入(⛔ 不要手改 hex)。
2. 读 `references/components.md`,挑出该页面需要的组件。
3. 按源站命名/形态复用;状态用语义修饰。
4. 自查:色值是否全部来自令牌?圆角/组件形态是否一致?
## 六、已知缺口
见 `references/capture-notes.md`。
"""
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--raw", required=True, help="capture_design_system.mjs 产出的 raw JSON")
ap.add_argument("--out", required=True, help="输出目录(将生成为一套 design-system-<slug>/)")
ap.add_argument("--name", default="", help="样式名(缺省取页面标题)")
ap.add_argument("--dark-raw", default="", help="可选:另一份暗色采集的 raw JSON")
args = ap.parse_args()
with open(args.raw, "r", encoding="utf-8") as f:
raw = json.load(f)
data = raw.get("data") or {}
content = data.get("content") or {}
brand = data.get("brand") or {}
name = args.name or content.get("title") or brand.get("name") or "Captured Style"
slug = slugify(name)
out = args.out
os.makedirs(os.path.join(out, "assets"), exist_ok=True)
os.makedirs(os.path.join(out, "references"), exist_ok=True)
tokens = tokens_from_colors(brand.get("colors"), data.get("layout"))
write_tokens(os.path.join(out, "assets", "tokens.css"), tokens)
# brand.json
brand_json = {
"name": name,
"tagline": brand.get("tagline") or content.get("description") or "",
"sourceUrl": brand.get("sourceUrl") or content.get("url") or raw.get("url") or "",
"accent": brand.get("accent") or tokens.get("--accent"),
"colors": brand.get("colors") or [],
"typography": data.get("typography") or {},
"layout": data.get("layout") or {},
}
with open(os.path.join(out, "assets", "brand.json"), "w", encoding="utf-8") as f:
json.dump(brand_json, f, ensure_ascii=False, indent=2)
# 暗色(可选)
has_dark = False
if args.dark_raw and os.path.exists(args.dark_raw):
with open(args.dark_raw, "r", encoding="utf-8") as f:
dark_raw = json.load(f)
dark_data = dark_raw.get("data") or {}
dark_brand = dark_data.get("brand") or {}
dark_tokens = tokens_from_colors(dark_brand.get("colors"), dark_data.get("layout"))
write_tokens(os.path.join(out, "assets", "tokens-dark.css"), dark_tokens)
has_dark = True
# references
with open(os.path.join(out, "references", "design-system.html"), "w", encoding="utf-8") as f:
f.write(raw.get("html") or "")
with open(os.path.join(out, "references", "library.json"), "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
with open(os.path.join(out, "references", "components.md"), "w", encoding="utf-8") as f:
f.write(md_components(data.get("components"), data.get("typography")))
with open(os.path.join(out, "references", "capture-notes.md"), "w", encoding="utf-8") as f:
f.write(md_capture_notes(raw.get("_meta") or {}, data, has_dark))
# SKILL.md
with open(os.path.join(out, "SKILL.md"), "w", encoding="utf-8") as f:
f.write(build_skill_md(name, data, tokens))
print(json.dumps({
"ok": True,
"package": os.path.abspath(out),
"name": name,
"slug": slug,
"colors": len(brand.get("colors") or []),
"hasDark": has_dark,
}, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
@@ -0,0 +1,163 @@
#!/usr/bin/env node
/**
* design-capture · 独立「抓取网站设计系统」驱动(①③段工具)
*
* 做什么:连本机 Chrome(CDP)→ 打开目标 URL → 注入 vendor/brand-capture.js →
* 调 window.__odBrandCapture() → 落一份 raw JSON(含结构化 data + 设计系统 html)。
* 依赖:本机 Chrome(已开 --remote-debugging-port)+ Node ≥ 21(内置 fetch + 全局 WebSocket)。
* 边界:⛔ 不依赖 open-design daemon;抽取脚本搬运自 open-design clipper(Apache-2.0,见 ../ATTRIBUTION.md)。
*
* 用法:
* node capture_design_system.mjs --url https://example.com --out raw.json [--ports 9222,9223,9333] [--settle 3500]
* 若没有可连的 Chrome,先起一个:
* chrome.exe --remote-debugging-port=9333 --user-data-dir=<临时目录> about:blank
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = path.dirname(fileURLToPath(import.meta.url));
const VENDOR = path.join(HERE, '..', 'vendor', 'brand-capture.js');
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
function parseArgs(argv) {
const a = {};
for (let i = 0; i < argv.length; i++) {
const t = argv[i];
if (!t.startsWith('--')) continue;
const k = t.slice(2);
const next = argv[i + 1];
a[k] = next && !next.startsWith('--') ? argv[++i] : true;
}
return a;
}
const args = parseArgs(process.argv.slice(2));
const TARGET_URL = args.url;
const OUT = args.out || 'design-capture-raw.json';
const PORTS = String(args.ports || '9222,9223,9224,9333').split(',').map((s) => s.trim()).filter(Boolean);
const SETTLE = Number(args.settle || 3500);
if (!TARGET_URL) {
console.error('用法:node capture_design_system.mjs --url <网址> --out <raw.json> [--ports 9222,9223] [--settle 3500]');
process.exit(2);
}
async function probePorts() {
for (const p of PORTS) {
for (const host of ['127.0.0.1', '[::1]']) {
try {
const r = await fetch(`http://${host}:${p}/json/version`, { signal: AbortSignal.timeout(800) });
if (r.ok) return { port: p, host };
} catch {}
}
}
return null;
}
async function openTab(host, port, url) {
for (const method of ['PUT', 'GET']) {
try {
const r = await fetch(`http://${host}:${port}/json/new?${encodeURIComponent(url)}`, { method });
if (r.ok) return await r.json();
} catch {}
}
throw new Error('无法新建标签页(/json/new 失败)');
}
class CDP {
constructor(wsUrl) {
this.wsUrl = wsUrl;
this.id = 0;
this.pending = new Map();
}
connect() {
return new Promise((resolve, reject) => {
this.ws = new WebSocket(this.wsUrl);
this.ws.addEventListener('open', () => resolve());
this.ws.addEventListener('error', (e) => reject(new Error('WebSocket 连接失败:' + (e.message || e.type))));
this.ws.addEventListener('message', (ev) => {
let msg;
try { msg = JSON.parse(ev.data); } catch { return; }
if (msg.id && this.pending.has(msg.id)) {
const { resolve: res, reject: rej } = this.pending.get(msg.id);
this.pending.delete(msg.id);
msg.error ? rej(new Error(msg.error.message)) : res(msg.result);
}
});
});
}
send(method, params = {}) {
const id = ++this.id;
return new Promise((resolve, reject) => {
this.pending.set(id, { resolve, reject });
this.ws.send(JSON.stringify({ id, method, params }));
setTimeout(() => {
if (this.pending.has(id)) { this.pending.delete(id); reject(new Error(`CDP 超时:${method}`)); }
}, 30000);
});
}
async eval(expression, returnByValue = true) {
const r = await this.send('Runtime.evaluate', { expression, returnByValue, awaitPromise: true });
if (r.exceptionDetails) {
throw new Error('页面脚本异常:' + (r.exceptionDetails.exception?.description || r.exceptionDetails.text));
}
return r.result ? r.result.value : undefined;
}
close() { try { this.ws.close(); } catch {} }
}
async function main() {
const target = await probePorts();
if (!target) {
console.error('未发现可用的 Chrome 远程调试端口(试过 ' + PORTS.join('/') + ')。');
console.error('请先启动:chrome.exe --remote-debugging-port=9333 --user-data-dir=<临时目录> about:blank');
process.exit(3);
}
const tab = await openTab(target.host, target.port, TARGET_URL);
const cdp = new CDP(tab.webSocketDebuggerUrl);
await cdp.connect();
await cdp.send('Page.enable');
await cdp.send('Runtime.enable');
const deadline = Date.now() + 45000;
let ready = '';
while (Date.now() < deadline) {
try { ready = await cdp.eval('document.readyState'); } catch {}
if (ready === 'complete') break;
await sleep(500);
}
await sleep(SETTLE);
const src = fs.readFileSync(VENDOR, 'utf8');
await cdp.eval(src, false);
const kind = await cdp.eval('typeof window.__odBrandCapture');
if (kind !== 'function') throw new Error('注入后未找到 window.__odBrandCapture(页面可能禁止脚本)');
const cap = await cdp.eval('window.__odBrandCapture({})');
if (!cap || !cap.html) throw new Error('抓取失败:未返回 html');
cap._meta = {
tool: 'design-capture',
toolVersion: '1.0',
capturedAt: new Date().toISOString(),
requestedUrl: TARGET_URL,
cdpPort: target.port,
};
fs.writeFileSync(OUT, JSON.stringify(cap));
console.log(JSON.stringify({
ok: true,
out: path.resolve(OUT),
title: cap.title,
url: cap.url,
summary: cap.summary,
htmlBytes: cap.html.length,
hasStructuredData: !!cap.data,
}, null, 2));
cdp.close();
try { await fetch(`http://${target.host}:${target.port}/json/close/${tab.id}`); } catch {}
}
main().catch((e) => { console.error('抓取失败:' + e.message); process.exit(1); });