@topmindspace/tms-skills 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/LICENSE +21 -0
- package/README.md +102 -0
- package/bin/tms-skills.js +160 -0
- package/package.json +48 -0
- package/top-ppt-html/README.md +254 -0
- package/top-ppt-html/SKILL.md +111 -0
- package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.html +3926 -0
- package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.model.json +168 -0
- package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.html +3926 -0
- package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.model.json +168 -0
- package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.html +4325 -0
- package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.model.json +321 -0
- package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.html +4325 -0
- package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.model.json +321 -0
- package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.html +4325 -0
- package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.model.json +321 -0
- package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.html +5527 -0
- package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.model.json +914 -0
- package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.html +5527 -0
- package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.model.json +914 -0
- package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.html +5527 -0
- package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.model.json +914 -0
- package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.html +5527 -0
- package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.model.json +914 -0
- package/top-ppt-html/assets/pptx-export.js +1944 -0
- package/top-ppt-html/assets/style-gallery.html +589 -0
- package/top-ppt-html/assets/templates/architecture.html +3728 -0
- package/top-ppt-html/assets/templates/engine.css +840 -0
- package/top-ppt-html/assets/templates/presentation.html +3738 -0
- package/top-ppt-html/assets/templates/research.html +4017 -0
- package/top-ppt-html/assets/templates/ui.js +520 -0
- package/top-ppt-html/assets/theme-overview-architecture.png +0 -0
- package/top-ppt-html/assets/theme-overview-presentation.png +0 -0
- package/top-ppt-html/assets/theme-overview-research.png +0 -0
- package/top-ppt-html/assets/theme-overview.png +0 -0
- package/top-ppt-html/evals/prompts.csv +15 -0
- package/top-ppt-html/evals/rubric.schema.json +25 -0
- package/top-ppt-html/evals/run_evals.py +220 -0
- package/top-ppt-html/evals/trace.example.json +16 -0
- package/top-ppt-html/package.json +35 -0
- package/top-ppt-html/references/charts-basic.md +624 -0
- package/top-ppt-html/references/charts-discipline.md +108 -0
- package/top-ppt-html/references/charts-extended.md +482 -0
- package/top-ppt-html/references/charts.md +28 -0
- package/top-ppt-html/references/components-atoms.md +624 -0
- package/top-ppt-html/references/components.md +30 -0
- package/top-ppt-html/references/content-rules.md +490 -0
- package/top-ppt-html/references/design-system-engine.md +235 -0
- package/top-ppt-html/references/design-system.md +478 -0
- package/top-ppt-html/references/failure-modes.md +214 -0
- package/top-ppt-html/references/high-fidelity.md +127 -0
- package/top-ppt-html/references/icons.md +397 -0
- package/top-ppt-html/references/industry-benchmark.md +105 -0
- package/top-ppt-html/references/infographics-stats.md +308 -0
- package/top-ppt-html/references/infographics-structure.md +226 -0
- package/top-ppt-html/references/infographics.md +43 -0
- package/top-ppt-html/references/layout-grammar.md +315 -0
- package/top-ppt-html/references/layouts-architecture.md +108 -0
- package/top-ppt-html/references/layouts-combo.md +600 -0
- package/top-ppt-html/references/layouts-research.md +160 -0
- package/top-ppt-html/references/modes.md +254 -0
- package/top-ppt-html/references/outline-design.md +275 -0
- package/top-ppt-html/references/playbook.md +266 -0
- package/top-ppt-html/references/pptx-export.md +209 -0
- package/top-ppt-html/references/reform-plan.md +252 -0
- package/top-ppt-html/references/styles.md +370 -0
- package/top-ppt-html/references/tech-design.md +138 -0
- package/top-ppt-html/scripts/audit_css.py +109 -0
- package/top-ppt-html/scripts/audit_docs.py +176 -0
- package/top-ppt-html/scripts/audit_skill.py +220 -0
- package/top-ppt-html/scripts/audit_styles.py +351 -0
- package/top-ppt-html/scripts/build_examples.py +2276 -0
- package/top-ppt-html/scripts/build_pptx.js +2380 -0
- package/top-ppt-html/scripts/capture_theme_overview.js +78 -0
- package/top-ppt-html/scripts/checks_html.py +127 -0
- package/top-ppt-html/scripts/cross_verify.py +294 -0
- package/top-ppt-html/scripts/env_probe.py +158 -0
- package/top-ppt-html/scripts/extract_model.py +210 -0
- package/top-ppt-html/scripts/extract_snippet.py +374 -0
- package/top-ppt-html/scripts/gen_channel_a.js +214 -0
- package/top-ppt-html/scripts/layout-constants.json +3377 -0
- package/top-ppt-html/scripts/layout_slots.json +830 -0
- package/top-ppt-html/scripts/lib_layout_regions.js +412 -0
- package/top-ppt-html/scripts/measure_height.py +178 -0
- package/top-ppt-html/scripts/model-schema.json +547 -0
- package/top-ppt-html/scripts/negative_tests.py +307 -0
- package/top-ppt-html/scripts/package_skill.py +291 -0
- package/top-ppt-html/scripts/prepare_images.py +341 -0
- package/top-ppt-html/scripts/probe_image_export.py +188 -0
- package/top-ppt-html/scripts/quality_gate.py +301 -0
- package/top-ppt-html/scripts/regression.py +308 -0
- package/top-ppt-html/scripts/render_compare.py +275 -0
- package/top-ppt-html/scripts/render_from_model.py +698 -0
- package/top-ppt-html/scripts/scaffold_report.py +1054 -0
- package/top-ppt-html/scripts/section-file-map.json +105 -0
- package/top-ppt-html/scripts/sync_runtime.py +662 -0
- package/top-ppt-html/scripts/validate_pptx.py +1510 -0
- package/top-ppt-html/scripts/validate_report.py +1456 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
TopPPT HTML· 从 HTML 报告抽取 PPTX 内容模型
|
|
5
|
+
用法:
|
|
6
|
+
python extract_model.py <报告.html> [输出.json]
|
|
7
|
+
|
|
8
|
+
从报告的 window.REPORT_MODEL(JSON)抽取内容模型,供 build_pptx.js --model= 使用。
|
|
9
|
+
保证 PPTX 与页面同源——模型即唯一事实源,不再两处维护。
|
|
10
|
+
校验双端同源:页型字段与必填约束读 scripts/model-schema.json(与浏览器端 validateModel
|
|
11
|
+
消费同一份 schema,经 sync_runtime.py 注入 assets/pptx-export.js,杜绝漂移)。
|
|
12
|
+
含模型-正文一致性抽查(标题/agenda 条数/页数粗对齐),不一致打印警告(硬门禁在 validate_report.py)。
|
|
13
|
+
"""
|
|
14
|
+
import sys
|
|
15
|
+
import re
|
|
16
|
+
import json
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
# Windows GBK 控制台兜底:强制 UTF-8 输出
|
|
20
|
+
try:
|
|
21
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
22
|
+
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
|
|
23
|
+
except Exception:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
SCHEMA_PATH = Path(__file__).resolve().parent / 'model-schema.json'
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def load_schema():
|
|
30
|
+
"""DSL schema 单源(model-schema.json);与浏览器端 validateModel 同一份定义。"""
|
|
31
|
+
try:
|
|
32
|
+
return json.loads(SCHEMA_PATH.read_text(encoding='utf-8'))
|
|
33
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
34
|
+
print(f"警告: 无法读取 schema 单源 {SCHEMA_PATH}: {e}")
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def schema_get(obj, path):
|
|
39
|
+
cur = obj
|
|
40
|
+
for k in path.split('.'):
|
|
41
|
+
if cur is None or not isinstance(cur, dict):
|
|
42
|
+
return None
|
|
43
|
+
cur = cur.get(k)
|
|
44
|
+
return cur
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def schema_field_ok(sec, spec):
|
|
48
|
+
"""spec = 'a.b:array' / 'a.b:str' / 'a.b'(真值检查)/ 'anyof:a|b:c'(任一满足)。"""
|
|
49
|
+
if spec.startswith('anyof:'):
|
|
50
|
+
return any(schema_field_ok(sec, alt) for alt in spec[len('anyof:'):].split('|') if alt)
|
|
51
|
+
m = re.match(r'^(.*?)(?::(array|str))?$', spec)
|
|
52
|
+
path, kind = m.group(1), m.group(2)
|
|
53
|
+
v = schema_get(sec, path)
|
|
54
|
+
if kind == 'array':
|
|
55
|
+
return isinstance(v, list) and len(v) > 0
|
|
56
|
+
if kind == 'str':
|
|
57
|
+
return isinstance(v, str) and bool(v.strip())
|
|
58
|
+
return bool(v)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def validate_against_schema(model, schema):
|
|
62
|
+
"""按 schema 单源校验模型;返回 (缺失列表, 警告列表)。与浏览器端 validateModel 同语义。"""
|
|
63
|
+
missing, warnings = [], []
|
|
64
|
+
if not schema:
|
|
65
|
+
return missing, warnings
|
|
66
|
+
ms = schema.get('model') or {}
|
|
67
|
+
for spec in ms.get('required', []):
|
|
68
|
+
if not schema_field_ok(model, spec):
|
|
69
|
+
missing.append(f"{spec}(顶层必填)")
|
|
70
|
+
mode = model.get('mode') or 'presentation'
|
|
71
|
+
ag = model.get('agenda') or []
|
|
72
|
+
ag_min = (ms.get('agendaMin') or {}).get(mode, 0)
|
|
73
|
+
if ag_min > 0 and len(ag) < ag_min:
|
|
74
|
+
missing.append(f"agenda(大纲 ≥{ag_min} 条)")
|
|
75
|
+
secs = model.get('sections') or []
|
|
76
|
+
if not isinstance(secs, list) or len(secs) < ms.get('sectionsMin', 1):
|
|
77
|
+
missing.append("sections(章节页 ≥1)")
|
|
78
|
+
page_types = schema.get('pageTypes') or {}
|
|
79
|
+
for i, sec in enumerate(secs if isinstance(secs, list) else []):
|
|
80
|
+
t = (sec.get('type') if isinstance(sec, dict) else None) or 'points'
|
|
81
|
+
defn = page_types.get(t)
|
|
82
|
+
if not defn:
|
|
83
|
+
missing.append(f"sections[{i}].type={t!r}(未知页型)")
|
|
84
|
+
continue
|
|
85
|
+
if not (isinstance(sec, dict) and sec.get('title')):
|
|
86
|
+
missing.append(f"sections[{i}].title(第 {i + 1} 章标题)")
|
|
87
|
+
for spec in defn.get('required', []):
|
|
88
|
+
if not schema_field_ok(sec, spec):
|
|
89
|
+
missing.append(f"sections[{i}].{spec}({defn.get('label', t)}必填)")
|
|
90
|
+
if defn.get('modes') and mode not in defn['modes']:
|
|
91
|
+
warnings.append(f"sections[{i}] 页型 {t!r} 适用于 {'/'.join(defn['modes'])},当前 mode={mode!r}")
|
|
92
|
+
if schema.get('modes') and model.get('mode') and model['mode'] not in schema['modes']:
|
|
93
|
+
warnings.append(f"mode={model['mode']!r} 未知,按 presentation 处理")
|
|
94
|
+
if secs and len(secs) < ms.get('sectionsRecommended', 3):
|
|
95
|
+
warnings.append(f"章节页仅 {len(secs)} 页,正式报告建议 ≥3")
|
|
96
|
+
if len(ag) > ms.get('agendaComfortMax', 16):
|
|
97
|
+
warnings.append(f"agenda {len(ag)} 条超出单页舒适上限,建议拆分")
|
|
98
|
+
return missing, warnings
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def main():
|
|
102
|
+
if len(sys.argv) < 2:
|
|
103
|
+
print(__doc__)
|
|
104
|
+
return 2
|
|
105
|
+
path = Path(sys.argv[1])
|
|
106
|
+
if not path.exists():
|
|
107
|
+
print(f"文件不存在: {path}")
|
|
108
|
+
return 2
|
|
109
|
+
txt = path.read_text(encoding='utf-8')
|
|
110
|
+
|
|
111
|
+
m = re.search(r'window\.REPORT_MODEL\s*=\s*(\{[\s\S]*?\})\s*;', txt)
|
|
112
|
+
if not m:
|
|
113
|
+
print("未找到 window.REPORT_MODEL —— 报告未内嵌内容模型。")
|
|
114
|
+
print("请按 references/pptx-export.md 在报告 <script> 中补齐模型后再抽取。")
|
|
115
|
+
return 1
|
|
116
|
+
raw = m.group(1)
|
|
117
|
+
try:
|
|
118
|
+
model = json.loads(raw)
|
|
119
|
+
except json.JSONDecodeError as e:
|
|
120
|
+
print(f"REPORT_MODEL 不是合法 JSON: {e}")
|
|
121
|
+
print("注意:模型必须是严格 JSON(双引号、无尾逗号、无注释)。")
|
|
122
|
+
return 1
|
|
123
|
+
|
|
124
|
+
# 模型字符串字段净化:禁止把 HTML 标签当纯文本写入(cite 只允许 [n])。
|
|
125
|
+
# 泄漏标签会在 PPTX/预览里原样露出 <a class="cite"…>,属交付硬缺陷。
|
|
126
|
+
stripped = {'count': 0, 'samples': []}
|
|
127
|
+
|
|
128
|
+
def _scrub_assign(obj, path=''):
|
|
129
|
+
if isinstance(obj, dict):
|
|
130
|
+
for k in list(obj.keys()):
|
|
131
|
+
v = obj[k]
|
|
132
|
+
p = f'{path}.{k}' if path else k
|
|
133
|
+
if isinstance(v, str) and re.search(r'</?[a-zA-Z][^>]*>', v):
|
|
134
|
+
obj[k] = re.sub(r'\s+', ' ', re.sub(r'</?[a-zA-Z][^>]*>', '', v)).strip()
|
|
135
|
+
stripped['count'] += 1
|
|
136
|
+
if len(stripped['samples']) < 5:
|
|
137
|
+
stripped['samples'].append(p)
|
|
138
|
+
else:
|
|
139
|
+
_scrub_assign(v, p)
|
|
140
|
+
elif isinstance(obj, list):
|
|
141
|
+
for i, v in enumerate(obj):
|
|
142
|
+
p = f'{path}[{i}]'
|
|
143
|
+
if isinstance(v, str) and re.search(r'</?[a-zA-Z][^>]*>', v):
|
|
144
|
+
obj[i] = re.sub(r'\s+', ' ', re.sub(r'</?[a-zA-Z][^>]*>', '', v)).strip()
|
|
145
|
+
stripped['count'] += 1
|
|
146
|
+
if len(stripped['samples']) < 5:
|
|
147
|
+
stripped['samples'].append(p)
|
|
148
|
+
else:
|
|
149
|
+
_scrub_assign(v, p)
|
|
150
|
+
|
|
151
|
+
_scrub_assign(model)
|
|
152
|
+
if stripped['count']:
|
|
153
|
+
print(f"[净化] 剥离 {stripped['count']} 处模型字段中的 HTML 标签: {stripped['samples']}")
|
|
154
|
+
print(" 注意:引用在模型里写 [n] 纯文本;HTML 正文才用 <a class=\"cite\">。禁止把标签写进模型。")
|
|
155
|
+
|
|
156
|
+
# 风格/模式/主题兜底:模型未写时取页面 data-style / data-mode / data-theme
|
|
157
|
+
if not model.get('style'):
|
|
158
|
+
sm = re.search(r'<html[^>]*data-style="([^"]+)"', txt)
|
|
159
|
+
if sm:
|
|
160
|
+
model['style'] = sm.group(1)
|
|
161
|
+
if not model.get('mode'):
|
|
162
|
+
mm = re.search(r'<html[^>]*data-mode="([^"]+)"', txt)
|
|
163
|
+
if mm:
|
|
164
|
+
model['mode'] = mm.group(1)
|
|
165
|
+
if not model.get('theme'):
|
|
166
|
+
tm = re.search(r'<html[^>]*data-theme="([^"]+)"', txt)
|
|
167
|
+
if tm:
|
|
168
|
+
model['theme'] = tm.group(1)
|
|
169
|
+
|
|
170
|
+
# schema 单源校验(与浏览器端 validateModel 同一份 model-schema.json)
|
|
171
|
+
schema = load_schema()
|
|
172
|
+
missing, schema_warn = validate_against_schema(model, schema)
|
|
173
|
+
if missing:
|
|
174
|
+
print(f"模型缺字段 {len(missing)} 项(导出的 PPTX 相应页面会为空或跳过):")
|
|
175
|
+
for msg in missing:
|
|
176
|
+
print(f" - {msg}")
|
|
177
|
+
for msg in schema_warn:
|
|
178
|
+
print(f"[schema 警告] {msg}")
|
|
179
|
+
|
|
180
|
+
# 模型-正文一致性抽查(标题 / agenda 条数 / 页数粗对齐)
|
|
181
|
+
issues = []
|
|
182
|
+
plain = re.sub(r'<[^>]+>', ' ', txt)
|
|
183
|
+
secs = model.get('sections') or []
|
|
184
|
+
miss_titles = [str(s.get('title'))[:14] for s in secs
|
|
185
|
+
if s.get('title') and str(s.get('title')) not in plain]
|
|
186
|
+
if miss_titles:
|
|
187
|
+
issues.append(f"{len(miss_titles)} 个章节标题未在正文出现: {miss_titles[:3]}")
|
|
188
|
+
n_ag = len(model.get('agenda') or [])
|
|
189
|
+
n_html_ag = txt.count('class="agenda__a"')
|
|
190
|
+
if n_ag and n_html_ag and n_ag != n_html_ag:
|
|
191
|
+
issues.append(f"agenda 条数不一致: model={n_ag} 正文={n_html_ag}")
|
|
192
|
+
n_bands = len(re.findall(r'<section class="band', txt))
|
|
193
|
+
if secs and not (len(secs) + 3 <= n_bands <= len(secs) + 7):
|
|
194
|
+
issues.append(f"页数粗不匹配: 正文 {n_bands} 页 vs 模型 {len(secs)}+3~7")
|
|
195
|
+
for msg in issues:
|
|
196
|
+
print(f"[一致性警告] {msg}")
|
|
197
|
+
|
|
198
|
+
out = Path(sys.argv[2]) if len(sys.argv) > 2 else path.with_suffix('.model.json')
|
|
199
|
+
out.write_text(json.dumps(model, ensure_ascii=False, indent=2), encoding='utf-8')
|
|
200
|
+
n_sec = len(model.get('sections', []))
|
|
201
|
+
print(f"已抽取: {out} (agenda {len(model.get('agenda', []))} 条 · sections {n_sec} 页 · style={model.get('style', 'business-blue')} · theme={model.get('theme', 'light')})")
|
|
202
|
+
if missing:
|
|
203
|
+
print("注意:模型不完整,正式交付前应回 AI 对话补全(页面预览模态含可复制提示词)。")
|
|
204
|
+
print("下一步:")
|
|
205
|
+
print(f' NODE_PATH=<pptxgenjs 所在 node_modules> <node> scripts/build_pptx.js "报告.pptx" --model="{out}"')
|
|
206
|
+
return 0
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
if __name__ == '__main__':
|
|
210
|
+
sys.exit(main())
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""TopPPT HTML · L2 节级片段抽取器(减少整读 components.md / charts.md 的上下文成本)
|
|
4
|
+
|
|
5
|
+
用法:
|
|
6
|
+
python scripts/extract_snippet.py --list
|
|
7
|
+
python scripts/extract_snippet.py --chart waterfall
|
|
8
|
+
python scripts/extract_snippet.py --page-type exhibit
|
|
9
|
+
python scripts/extract_snippet.py --file components.md --section 46
|
|
10
|
+
python scripts/extract_snippet.py --task research-evidence
|
|
11
|
+
python scripts/extract_snippet.py --task pptx-export
|
|
12
|
+
|
|
13
|
+
设计:
|
|
14
|
+
智能体按「任务 → 只读相关节」取代码,而不是整文件读入 70KB+ 规范。
|
|
15
|
+
节边界用标题行识别;--task 走内置路由表(与 playbook.md §十 同源)。
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import json
|
|
21
|
+
import re
|
|
22
|
+
import sys
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
27
|
+
except Exception:
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
31
|
+
REF = ROOT / 'references'
|
|
32
|
+
LC = json.loads((ROOT / 'scripts' / 'layout-constants.json').read_text(encoding='utf-8'))
|
|
33
|
+
# §编号 → 物理文件(components/charts 拆分后逻辑路由;单源 scripts/section-file-map.json)
|
|
34
|
+
_SECTION_MAP_PATH = ROOT / 'scripts' / 'section-file-map.json'
|
|
35
|
+
try:
|
|
36
|
+
SECTION_FILE_MAP = json.loads(_SECTION_MAP_PATH.read_text(encoding='utf-8'))
|
|
37
|
+
except (OSError, json.JSONDecodeError):
|
|
38
|
+
SECTION_FILE_MAP = {}
|
|
39
|
+
|
|
40
|
+
# 任务 → 建议只读的文件与节(与 references/playbook.md §十 保持一致)
|
|
41
|
+
# 节号三种形态均可寻址:阿拉伯(46 / 46b / 8-1)、中文(三 / 一-b)、标题关键词(精导 / 速查)
|
|
42
|
+
TASK_ROUTES = {
|
|
43
|
+
'research-evidence': {
|
|
44
|
+
'desc': 'research 证据页 / Exhibit / 密表',
|
|
45
|
+
'reads': [
|
|
46
|
+
('components.md', ['36d', '46', '46c']),
|
|
47
|
+
('playbook.md', ['三', '四']),
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
'presentation-combo': {
|
|
51
|
+
'desc': '演示组合版式与卡片',
|
|
52
|
+
'reads': [
|
|
53
|
+
('components.md', ['39', '40', '41', '42', '46', '46b', '46c']),
|
|
54
|
+
('playbook.md', ['三', '四']),
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
'architecture-diagram': {
|
|
58
|
+
'desc': '架构/泳道/分层',
|
|
59
|
+
'reads': [
|
|
60
|
+
('components.md', ['37', '38', '38b']),
|
|
61
|
+
('infographics.md', ['78', '79', '80']),
|
|
62
|
+
('playbook.md', ['三', '七']),
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
'chart-pick': {
|
|
66
|
+
'desc': '选图与取图表代码',
|
|
67
|
+
'reads': [
|
|
68
|
+
('playbook.md', ['五']),
|
|
69
|
+
('charts.md', ['16']),
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
'content-rules': {
|
|
73
|
+
'desc': '密度/字数/去AI味/细节保全',
|
|
74
|
+
'reads': [
|
|
75
|
+
('content-rules.md', ['四', '一']),
|
|
76
|
+
('playbook.md', ['六']),
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
'layout-grammar': {
|
|
80
|
+
'desc': '布局骨架 P1–P12 / 元素排版 / 组合与留白',
|
|
81
|
+
'reads': [
|
|
82
|
+
('layout-grammar.md', ['〇', '二', '三', '四']),
|
|
83
|
+
('playbook.md', ['〇']),
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
'model-render': {
|
|
87
|
+
'desc': '模型驱动生成 / 单写路径',
|
|
88
|
+
'reads': [
|
|
89
|
+
('pptx-export.md', ['内容模型']),
|
|
90
|
+
('content-rules.md', ['二-b']),
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
'pptx-export': {
|
|
94
|
+
'desc': 'PPTX 精导与页型字段',
|
|
95
|
+
'reads': [
|
|
96
|
+
('pptx-export.md', ['精导', '内容模型']),
|
|
97
|
+
('playbook.md', ['九']),
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
'high-fidelity': {
|
|
101
|
+
'desc': '深度高保真/锚点/manifest',
|
|
102
|
+
'reads': [
|
|
103
|
+
('high-fidelity.md', ['三', '四']),
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
'image-layout': {
|
|
107
|
+
'desc': '素材图片六版式与配图占位',
|
|
108
|
+
'reads': [
|
|
109
|
+
('components.md', ['11c']),
|
|
110
|
+
('playbook.md', ['三']),
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
'style-theme': {
|
|
114
|
+
'desc': '配色/亮暗/风格选型',
|
|
115
|
+
'reads': [
|
|
116
|
+
('styles.md', ['快速选型', '新增']),
|
|
117
|
+
('design-system.md', ['1a', '1b', '9']),
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
'icons': {
|
|
121
|
+
'desc': '图标语义速查与使用准则',
|
|
122
|
+
'reads': [
|
|
123
|
+
('icons.md', ['速查', '使用准则']),
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# 节号:阿拉伯(8 / 8b / 8-1)或中文(一 / 一-b / 一-续 / 十),须带 .、. 分隔或后随空白;
|
|
129
|
+
# 无节号的二级标题也入节表(num='',用标题关键词寻址——modes/icons/styles 等中文标题文件)
|
|
130
|
+
_SEC_NUM = r'(?:\d+(?:[a-z]|-\d+)?|[一二三四五六七八九十]{1,3}(?:-[a-z0-9续]+)?)'
|
|
131
|
+
HEADING_RE = re.compile(
|
|
132
|
+
r'^(#{2,4})[ \t]+(?:§)?(%s)[.、.]?[ \t]*(.*)$' % _SEC_NUM, re.M)
|
|
133
|
+
HEADING_UNNUM_RE = re.compile(r'^(##)[ \t]+(.+)$', re.M)
|
|
134
|
+
# 任意级别标题行(extract_chart 定位代码节边界用——### 代码节不能再被 ## 回退错层)
|
|
135
|
+
_HEADING_ANY = re.compile(r'\n#{2,4}[ \t]')
|
|
136
|
+
_CODE_FENCE = re.compile(r'```[\s\S]*?```')
|
|
137
|
+
|
|
138
|
+
_TEXT_CACHE: dict[str, str] = {}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _read(path: Path) -> str:
|
|
142
|
+
"""带缓存的读取(extract_section/_sections/extract_chart 共用,防同文件重复 IO)。"""
|
|
143
|
+
key = str(path)
|
|
144
|
+
if key not in _TEXT_CACHE:
|
|
145
|
+
_TEXT_CACHE[key] = path.read_text(encoding='utf-8')
|
|
146
|
+
return _TEXT_CACHE[key]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _fence_spans(text: str) -> list[tuple[int, int]]:
|
|
150
|
+
"""代码围栏 ``` 区间(供 extract_chart 优先在代码内定位 data-chart)。"""
|
|
151
|
+
return [(m.start(), m.end()) for m in _CODE_FENCE.finditer(text)]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _sections(path: Path) -> list[tuple[str, str, int, int]]:
|
|
155
|
+
"""返回 [(编号, 标题, start, end), ...]
|
|
156
|
+
|
|
157
|
+
编号节(## ~ ####)与无编号二级标题共同构成节边界;
|
|
158
|
+
无编号节 num='',仅供 extract_section 的标题关键词兜底命中。
|
|
159
|
+
"""
|
|
160
|
+
text = _read(path)
|
|
161
|
+
marks: list[tuple[str, str, int]] = []
|
|
162
|
+
num_starts: set[int] = set()
|
|
163
|
+
for m in HEADING_RE.finditer(text):
|
|
164
|
+
marks.append((m.group(2), m.group(3).strip(), m.start()))
|
|
165
|
+
num_starts.add(m.start())
|
|
166
|
+
for m in HEADING_UNNUM_RE.finditer(text):
|
|
167
|
+
if m.start() not in num_starts: # 已被编号正则命中则跳过
|
|
168
|
+
marks.append(('', m.group(2).strip(), m.start()))
|
|
169
|
+
marks.sort(key=lambda t: t[2])
|
|
170
|
+
out = []
|
|
171
|
+
for i, (num, title, start) in enumerate(marks):
|
|
172
|
+
end = marks[i + 1][2] if i + 1 < len(marks) else len(text)
|
|
173
|
+
out.append((num, title, start, end))
|
|
174
|
+
return out
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def resolve_physical(filename: str, section: str | None = None) -> str:
|
|
178
|
+
"""逻辑文件名 → 物理文件名(components/charts 拆分后路由)。"""
|
|
179
|
+
if not section:
|
|
180
|
+
return filename
|
|
181
|
+
mapping = SECTION_FILE_MAP.get(filename) or {}
|
|
182
|
+
return mapping.get(str(section), filename)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def extract_section(filename: str, section: str, max_chars: int = 12000) -> str:
|
|
186
|
+
physical = resolve_physical(filename, section)
|
|
187
|
+
path = REF / physical
|
|
188
|
+
if not path.exists():
|
|
189
|
+
path = REF / filename
|
|
190
|
+
if not path.exists():
|
|
191
|
+
return f'错误:{path} 不存在'
|
|
192
|
+
secs = _sections(path)
|
|
193
|
+
# ① 精确节号优先;② 无精确命中才允许前缀(并提示,防敲错节号静默取错码);③ 标题关键词兜底
|
|
194
|
+
hits = [s for s in secs if s[0] == section]
|
|
195
|
+
note = ''
|
|
196
|
+
if not hits:
|
|
197
|
+
prefix = [s for s in secs if s[0].startswith(section)]
|
|
198
|
+
if prefix:
|
|
199
|
+
hits = prefix[:1]
|
|
200
|
+
note = (f'\n/* 提示:§{section} 无精确节号,已按前缀命中 §{hits[0][0]} '
|
|
201
|
+
f'({hits[0][1][:24]});若非本意请用精确节号 */')
|
|
202
|
+
if not hits:
|
|
203
|
+
hits = [s for s in secs if section in s[1]]
|
|
204
|
+
if not hits:
|
|
205
|
+
seen: list[str] = []
|
|
206
|
+
for s in secs:
|
|
207
|
+
label = s[0] or (s[1][:10] + '…' if len(s[1]) > 10 else s[1])
|
|
208
|
+
if label not in seen:
|
|
209
|
+
seen.append(label)
|
|
210
|
+
sample = '、'.join(seen[:12])
|
|
211
|
+
return f'错误:{filename} 未找到 §{section}。可用节号/标题样例: {sample}'
|
|
212
|
+
num, title, start, end = hits[0]
|
|
213
|
+
body = _read(path)[start:end].rstrip()
|
|
214
|
+
if len(body) > max_chars:
|
|
215
|
+
body = body[:max_chars] + f'\n…(截断,全文见 references/{physical})'
|
|
216
|
+
return f'/* references/{physical} §{num} {title} */\n\n{body}{note}'
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def verify_routes() -> list[str]:
|
|
220
|
+
"""遍历 TASK_ROUTES,返回不可解析/空路由问题清单(audit_docs ⑥ 与 regression ⑦-b 共用同一实现)。"""
|
|
221
|
+
bad: list[str] = []
|
|
222
|
+
for task, r in TASK_ROUTES.items():
|
|
223
|
+
for fn, secs in r['reads']:
|
|
224
|
+
if not secs:
|
|
225
|
+
bad.append(f'{task}:{fn} 空路由')
|
|
226
|
+
continue
|
|
227
|
+
for sec in secs:
|
|
228
|
+
if extract_section(fn, sec, max_chars=200).startswith('错误'):
|
|
229
|
+
bad.append(f'{task}:{fn} §{sec}')
|
|
230
|
+
return bad
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def extract_chart(chart_type: str, max_chars: int = 14000) -> str:
|
|
234
|
+
chart_type = (chart_type or '').strip().lower()
|
|
235
|
+
reg = ((LC.get('charts') or {}).get('registry') or {}).get(chart_type)
|
|
236
|
+
if not reg:
|
|
237
|
+
known = sorted((LC.get('charts') or {}).get('registry') or {})
|
|
238
|
+
return f'错误:未登记图表 {chart_type!r}。可用: {", ".join(known)}'
|
|
239
|
+
info_types = set(((LC.get('charts') or {}).get('scaffold') or {}).get('infoTypes') or {})
|
|
240
|
+
key = f'data-chart="{chart_type}"'
|
|
241
|
+
candidates = (
|
|
242
|
+
[REF / 'infographics-stats.md', REF / 'infographics-structure.md', REF / 'infographics.md']
|
|
243
|
+
if chart_type in info_types
|
|
244
|
+
else [REF / 'charts-basic.md', REF / 'charts-extended.md',
|
|
245
|
+
REF / 'charts-discipline.md', REF / 'charts.md']
|
|
246
|
+
)
|
|
247
|
+
path = next((p for p in candidates if p.exists() and key in _read(p)), None)
|
|
248
|
+
if path is None:
|
|
249
|
+
names = ', '.join(p.name for p in candidates if p.exists())
|
|
250
|
+
return f'错误:未找到 {key}(检索: {names})'
|
|
251
|
+
text = _read(path)
|
|
252
|
+
# 优先取**代码围栏内**的 data-chart 出现位置——概述散文里的首个出现不是代码节
|
|
253
|
+
fences = _fence_spans(text)
|
|
254
|
+
idx = -1
|
|
255
|
+
pos = text.find(key)
|
|
256
|
+
while pos >= 0:
|
|
257
|
+
if any(a <= pos < b for a, b in fences):
|
|
258
|
+
idx = pos
|
|
259
|
+
break
|
|
260
|
+
pos = text.find(key, pos + 1)
|
|
261
|
+
if idx < 0:
|
|
262
|
+
idx = text.find(key) # 无围栏命中时退回首现
|
|
263
|
+
# 回退到该代码块前最近的标题(任意级别——### 代码节不能被 ## 回退错层)
|
|
264
|
+
prev_head = None
|
|
265
|
+
for m in _HEADING_ANY.finditer(text, 0, idx + 1):
|
|
266
|
+
prev_head = m
|
|
267
|
+
head = prev_head.start() if prev_head else max(0, idx - 200)
|
|
268
|
+
# 前进到下一个任意级别标题或文件尾
|
|
269
|
+
nxt = _HEADING_ANY.search(text, idx)
|
|
270
|
+
end = nxt.start() if nxt else len(text)
|
|
271
|
+
body = text[head:end].rstrip()
|
|
272
|
+
if len(body) > max_chars:
|
|
273
|
+
body = body[:max_chars] + f'\n…(截断,全文见 {path.name})'
|
|
274
|
+
meta = (f'/* chart={chart_type} · pptx={reg.get("pptx")} · '
|
|
275
|
+
f'dataTable={reg.get("dataTable")} · 来源 references/{path.name} */')
|
|
276
|
+
return f'{meta}\n\n{body}'
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def extract_page_type(page_type: str, max_chars: int = 10000) -> str:
|
|
280
|
+
ms = json.loads((ROOT / 'scripts' / 'model-schema.json').read_text(encoding='utf-8'))
|
|
281
|
+
pages = ms.get('pages') or ms.get('pageTypes') or ms
|
|
282
|
+
# schema 结构:可能是 {pages: {type: {...}}} 或顶层
|
|
283
|
+
entry = None
|
|
284
|
+
if isinstance(pages, dict) and page_type in pages:
|
|
285
|
+
entry = pages[page_type]
|
|
286
|
+
elif isinstance(ms.get('sections'), dict) and page_type in ms['sections']:
|
|
287
|
+
entry = ms['sections'][page_type]
|
|
288
|
+
# 兜底:递归找 type
|
|
289
|
+
if entry is None:
|
|
290
|
+
def find(obj):
|
|
291
|
+
if isinstance(obj, dict):
|
|
292
|
+
if obj.get('type') == page_type or page_type in obj and isinstance(obj[page_type], dict):
|
|
293
|
+
return obj.get(page_type, obj)
|
|
294
|
+
for v in obj.values():
|
|
295
|
+
r = find(v)
|
|
296
|
+
if r is not None:
|
|
297
|
+
return r
|
|
298
|
+
return None
|
|
299
|
+
entry = find(ms)
|
|
300
|
+
if entry is None:
|
|
301
|
+
return f'错误:model-schema.json 未找到页型 {page_type!r}'
|
|
302
|
+
# 选型表提示
|
|
303
|
+
hint = extract_section('components.md', '46', max_chars=4000)
|
|
304
|
+
schema_txt = json.dumps({page_type: entry}, ensure_ascii=False, indent=2)
|
|
305
|
+
return (f'/* pageType={page_type} · schema 字段(scripts/model-schema.json) */\n'
|
|
306
|
+
f'{schema_txt}\n\n'
|
|
307
|
+
f'/* 选型表摘要(components.md §46,完整表见原文) */\n'
|
|
308
|
+
f'{hint}')
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def main() -> int:
|
|
312
|
+
ap = argparse.ArgumentParser(description='L2 节级片段抽取')
|
|
313
|
+
ap.add_argument('--list', action='store_true', help='列出任务路由与已登记图表')
|
|
314
|
+
ap.add_argument('--task', help='任务路由名(research-evidence / chart-pick / …)')
|
|
315
|
+
ap.add_argument('--chart', help='图表类型,从 charts.md / infographics.md 抽代码节')
|
|
316
|
+
ap.add_argument('--page-type', dest='page_type', help='页型名,输出 schema 字段 + 选型提示')
|
|
317
|
+
ap.add_argument('--file', help='references 下文件名,如 components.md')
|
|
318
|
+
ap.add_argument('--section', help='节编号,如 46 / 11c / 36d')
|
|
319
|
+
ap.add_argument('--max-chars', type=int, default=12000)
|
|
320
|
+
args = ap.parse_args()
|
|
321
|
+
|
|
322
|
+
if args.list:
|
|
323
|
+
print('任务路由:')
|
|
324
|
+
for name, r in TASK_ROUTES.items():
|
|
325
|
+
print(f' {name:22} {r["desc"]}')
|
|
326
|
+
for fn, secs in r['reads']:
|
|
327
|
+
sec = ('§' + ' §'.join(secs)) if secs else '(全文按需)'
|
|
328
|
+
print(f' · references/{fn} {sec}')
|
|
329
|
+
print('\n已登记图表:')
|
|
330
|
+
reg = (LC.get('charts') or {}).get('registry') or {}
|
|
331
|
+
for t, meta in sorted(reg.items()):
|
|
332
|
+
if t.startswith('$') or not isinstance(meta, dict):
|
|
333
|
+
continue
|
|
334
|
+
print(f' {t:14} pptx={meta.get("pptx")} dataTable={meta.get("dataTable")}')
|
|
335
|
+
print('\n用法示例:')
|
|
336
|
+
print(' python scripts/extract_snippet.py --task research-evidence')
|
|
337
|
+
print(' python scripts/extract_snippet.py --chart waterfall')
|
|
338
|
+
print(' python scripts/extract_snippet.py --page-type exhibit')
|
|
339
|
+
print(' python scripts/extract_snippet.py --file components.md --section 46c')
|
|
340
|
+
return 0
|
|
341
|
+
|
|
342
|
+
if args.task:
|
|
343
|
+
r = TASK_ROUTES.get(args.task)
|
|
344
|
+
if not r:
|
|
345
|
+
print(f'错误:未知任务 {args.task!r}。用 --list 查看。')
|
|
346
|
+
return 2
|
|
347
|
+
print(f'# 任务:{args.task} — {r["desc"]}\n')
|
|
348
|
+
print('## 建议只读(读完即停,不预读下一份)\n')
|
|
349
|
+
for fn, secs in r['reads']:
|
|
350
|
+
sec = ('§' + '、'.join(secs)) if secs else '全文按需'
|
|
351
|
+
print(f'- references/{fn} — {sec}')
|
|
352
|
+
print('\n## 片段\n')
|
|
353
|
+
for fn, secs in r['reads']:
|
|
354
|
+
for sec in secs[:2]: # 每文件最多抽 2 节,防上下文爆
|
|
355
|
+
print(extract_section(fn, sec, args.max_chars))
|
|
356
|
+
print('\n---\n')
|
|
357
|
+
return 0
|
|
358
|
+
|
|
359
|
+
if args.chart:
|
|
360
|
+
print(extract_chart(args.chart, args.max_chars))
|
|
361
|
+
return 0
|
|
362
|
+
if args.page_type:
|
|
363
|
+
print(extract_page_type(args.page_type, args.max_chars))
|
|
364
|
+
return 0
|
|
365
|
+
if args.file and args.section:
|
|
366
|
+
print(extract_section(args.file, args.section, args.max_chars))
|
|
367
|
+
return 0
|
|
368
|
+
|
|
369
|
+
ap.print_help()
|
|
370
|
+
return 2
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
if __name__ == '__main__':
|
|
374
|
+
sys.exit(main())
|