技能库同步:另一台机的 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:
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); });
|
||||
Reference in new issue
Block a user