#!/usr/bin/env python3 """拼一张 JCAI one-pager 骨架(html-one-pager 技能 · 第 3 步「装壳」)。 从仓库源文件机械抽取三段静态模板字符串 —— PRD_CSS / PRD_SPRITE(views/prd-style.ts) 与 BIND_CSS(views/report.ts)—— 填进 assets/skeleton.html 的占位符,产出一份 浏览器直接可开的 standalone 骨架。 为什么必须脚本抽:PRD_CSS 约 30KB,手搬必静默截断,截断后样式半死、极难肉眼定位。 抽取优先级:--activity 指定的活动自带 views/prd-style.ts 就抽它自己的 (A1/A2 各持一份且已各自演化,错族的版面「像又不像」);缺了才落回 A1(quote-intake)。 用法: python3 assemble.py --out draft.html python3 assemble.py --out draft.html --activity quote-pricing \ --title 核价单 --kicker "统一报价 · A4 成本定价 · 过程成果" --badge file-check """ import argparse import re import sys from pathlib import Path SKILL_DIR = Path(__file__).resolve().parent.parent # .../.claude/skills/html-one-pager REPO = SKILL_DIR.parents[2] # 仓库根 FALLBACK = 'quote-intake' def extract(path, name): if not path.is_file(): return None m = re.search(r'(?:export\s+)?const\s+' + name + r'\s*=\s*`([\s\S]*?)`;', path.read_text(encoding='utf-8')) return m.group(1) if m else None def pick(activity, filename, name): tried = [] order = ([activity] if activity and activity != FALLBACK else []) + [FALLBACK] for act in order: p = REPO / 'activities' / act / 'src' / 'views' / filename tried.append(str(p)) got = extract(p, name) if got is not None: return got, act sys.exit('抽不到 %s(试过:%s)。仓库根按脚本位置推得 %s;不对就检查技能目录层级,' '或 grep -rn "const %s" activities/ 找真实位置。' % (name, ' | '.join(tried), REPO, name)) def main(): ap = argparse.ArgumentParser(description='拼 JCAI one-pager standalone 骨架') ap.add_argument('--out', required=True, help='输出 HTML 路径') ap.add_argument('--activity', default='', help='目标活动 id(activities/);它自带 prd-style.ts 就优先抽它的') ap.add_argument('--title', default='工作单标题') ap.add_argument('--kicker', default='统一报价 · 活动名 · 过程成果') ap.add_argument('--ref', default='REF-2026-000') ap.add_argument('--state', default='在办 · 示例状态灯') ap.add_argument('--badge', default='file-text', help='题头图标名(精灵 symbol 名,不带 i- 前缀;19 枚清单见 block-catalog.md)') a = ap.parse_args() css, css_src = pick(a.activity, 'prd-style.ts', 'PRD_CSS') sprite, sprite_src = pick(a.activity, 'prd-style.ts', 'PRD_SPRITE') bind, bind_src = pick(a.activity, 'report.ts', 'BIND_CSS') if len(css) < 15000: print('⚠ PRD_CSS 只有 %d 字节(常态约 30KB)—— 抽取源可能不完整,继续但请人工核一眼' % len(css)) skeleton = (SKILL_DIR / 'assets' / 'skeleton.html').read_text(encoding='utf-8') out = (skeleton .replace('/*@PRD_CSS@*/', css) .replace('/*@BIND_CSS@*/', bind) .replace('', sprite) .replace('@TITLE@', a.title) .replace('@KICKER@', a.kicker) .replace('@REF@', a.ref) .replace('@STATE@', a.state) .replace('@BADGE@', a.badge)) dest = Path(a.out).expanduser() dest.parent.mkdir(parents=True, exist_ok=True) dest.write_text(out, encoding='utf-8') print('已产出 %s(%.1f KB)' % (dest, len(out.encode('utf-8')) / 1024)) print(' PRD_CSS %d 字节 ← %s · BIND_CSS %d 字节 ← %s · 精灵 %d 枚 symbol ← %s' % (len(css), css_src, len(bind), bind_src, sprite.count('