@topmindspace/tms-skills 0.1.19 → 0.2.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.en.md +5 -2
  3. package/README.md +4 -1
  4. package/package.json +5 -2
  5. package/topmind-cover/README.md +40 -0
  6. package/topmind-cover/SKILL.md +66 -0
  7. package/topmind-cover/package-lock.json +16 -0
  8. package/topmind-cover/package.json +16 -0
  9. package/topmind-cover/references/cover-styles.md +40 -0
  10. package/topmind-cover/scripts/audit_css.py +39 -0
  11. package/topmind-cover/scripts/audit_docs.py +58 -0
  12. package/topmind-cover/scripts/audit_skill.py +60 -0
  13. package/topmind-cover/scripts/audit_styles.py +54 -0
  14. package/topmind-cover/scripts/crop-cover.py +69 -0
  15. package/topmind-cover/scripts/negative_tests.py +71 -0
  16. package/topmind-cover/scripts/package_skill.py +188 -0
  17. package/topmind-wechat-post/README.md +49 -0
  18. package/topmind-wechat-post/SKILL.md +208 -0
  19. package/topmind-wechat-post/assets/themes/amber-review.json +35 -0
  20. package/topmind-wechat-post/assets/themes/graphite.json +35 -0
  21. package/topmind-wechat-post/assets/themes/minimal-ink.json +35 -0
  22. package/topmind-wechat-post/assets/themes/newsprint.json +35 -0
  23. package/topmind-wechat-post/assets/themes/tech-blue.json +35 -0
  24. package/topmind-wechat-post/package-lock.json +16 -0
  25. package/topmind-wechat-post/package.json +16 -0
  26. package/topmind-wechat-post/references/design.md +50 -0
  27. package/topmind-wechat-post/references/element-spec.md +162 -0
  28. package/topmind-wechat-post/references/known-pits.md +86 -0
  29. package/topmind-wechat-post/references/theme-map.md +139 -0
  30. package/topmind-wechat-post/references/typography-rules.md +62 -0
  31. package/topmind-wechat-post/references/wechat-constraints.md +106 -0
  32. package/topmind-wechat-post/references/workflow.md +102 -0
  33. package/topmind-wechat-post/references/writing-quality.md +129 -0
  34. package/topmind-wechat-post/scripts/audit_css.py +39 -0
  35. package/topmind-wechat-post/scripts/audit_docs.py +58 -0
  36. package/topmind-wechat-post/scripts/audit_skill.py +60 -0
  37. package/topmind-wechat-post/scripts/audit_styles.py +54 -0
  38. package/topmind-wechat-post/scripts/lint-wechat.py +446 -0
  39. package/topmind-wechat-post/scripts/md2wechat.py +1602 -0
  40. package/topmind-wechat-post/scripts/negative_tests.py +65 -0
  41. package/topmind-wechat-post/scripts/new-article.py +183 -0
  42. package/topmind-wechat-post/scripts/package_skill.py +188 -0
  43. package/topmind-wechat-post/scripts/push-to-topstream.py +226 -0
  44. package/topmind-wechat-post/scripts/scan_ai_flavor.py +464 -0
  45. package/topmind-wechat-post/scripts/sync-mapping.py +220 -0
  46. package/topmind-wechat-post/scripts/sync-status.py +187 -0
  47. package/topmind-x-article/README.md +40 -0
  48. package/topmind-x-article/SKILL.md +62 -0
  49. package/topmind-x-article/package-lock.json +16 -0
  50. package/topmind-x-article/package.json +16 -0
  51. package/topmind-x-article/references/publish-checklist.md +24 -0
  52. package/topmind-x-article/references/x-format.md +24 -0
  53. package/topmind-x-article/scripts/audit_css.py +39 -0
  54. package/topmind-x-article/scripts/audit_docs.py +58 -0
  55. package/topmind-x-article/scripts/audit_skill.py +60 -0
  56. package/topmind-x-article/scripts/audit_styles.py +54 -0
  57. package/topmind-x-article/scripts/md2x.py +121 -0
  58. package/topmind-x-article/scripts/negative_tests.py +74 -0
  59. package/topmind-x-article/scripts/package_skill.py +188 -0
@@ -0,0 +1,102 @@
1
+ # 工作流 · 交付包 · 状态机
2
+
3
+ ## 生命周期
4
+
5
+ ```text
6
+ forward: 底稿 notes/*.md → 审校改写 → 质量三关 → 定稿 → 排版 → 发布
7
+ reverse: 选题包 → 调研/素材 → 多轮改稿 → 质量三关 → 定稿 → 排版 → 发布 →(可选)回推
8
+ 站外拉取 (ext): 在线精选站/转载整合 → 创作/整合 → 质量三关 → 定稿 → 排版 → 发布 →(可选)回推
9
+ ```
10
+
11
+ `direction` 写在 frontmatter:`forward | reverse`。**站外拉取也用 `reverse` + `target_file: pending`**(站外源不进 `notes/`,`forward` 会因 `source_file` 校验失败)。
12
+
13
+ ## 建包
14
+
15
+ ```bash
16
+ export TOPMIND_WORKSPACE=/path/to/ws
17
+ python3 scripts/new-article.py --slug <中文或英文短名> --title "中文标题" \
18
+ --direction reverse \
19
+ [--base <包根>] [--date YYYY-MM-DD] [--images a.png b.png] \
20
+ [--source-file notes/xxx.md] # forward
21
+ ```
22
+
23
+ 产出目录 `YYYY-MM-DD-<中文短名>/`,骨架 `公众号稿.md`。
24
+
25
+ ### 站外拉取取源注意
26
+
27
+ - Next.js 站点正文常在 RSC 载荷里:优先探 `GET /api/notes/<id>` 读 `item.body`,别只 `curl` HTML。
28
+ - 图片可能在 `/api/uploads/<hash>`;下载时记录 hash → 本地名映射。
29
+ - 同图可能双 hash:`md5` 去重;截图带页面残留时按面板边框裁切。
30
+ - 与源差异、图片处理、口径边界写进包内 `README.md`;一手来源抓不到必须写明。
31
+
32
+ ## 反向选题包(可选,长文建议)
33
+
34
+ ```text
35
+ YYYY-MM-DD-选题策划/
36
+ ├── 选题策划.md
37
+ ├── T1-<题>-调研与框架.md
38
+ ├── 素材总库.md
39
+ ├── 图表与配图方案.md
40
+ └── 发布前待办.md
41
+ ```
42
+
43
+ 多轮调研**每轮回头找反证**。
44
+
45
+ ## 状态机
46
+
47
+ | status | 目录名 | 含义 |
48
+ |--------|--------|------|
49
+ | 草稿 | `YYYY-MM-DD-名` | 写作中 |
50
+ | 定稿 | `…-released` | 可发 |
51
+ | 已发布 | `…-released` | 已上线(作者手动改) |
52
+
53
+ ```bash
54
+ python3 scripts/sync-status.py # dry-run
55
+ python3 scripts/sync-status.py --apply # 刷字数 + 纠目录名
56
+ python3 scripts/sync-status.py --set 定稿 <包> --apply
57
+ ```
58
+
59
+ ## 字数口径(三处别混)
60
+
61
+ | 输出 | 口径 | 用途 |
62
+ |------|------|------|
63
+ | frontmatter `word_count` | 纯中文 `\[一-鿿\]` | 映射真源 |
64
+ | lint「字数」 | 视觉长度(中 1 + 英数 ×0.6,URL 不计) | 阅读时长 / 段长 |
65
+ | md2wechat「正文字数」 | 渲染后字符 | 仅展示 |
66
+
67
+ ## 定稿验收命令串
68
+
69
+ ```bash
70
+ python3 scripts/scan_ai_flavor.py <包>/公众号稿.md # ≥85
71
+ python3 scripts/lint-wechat.py --input <包>/公众号稿.md
72
+ python3 scripts/sync-mapping.py --no-topstream
73
+ python3 scripts/sync-status.py
74
+ python3 scripts/md2wechat.py --input <包>/公众号稿.md --out-dir <包> \
75
+ --slug <slug> --asset-root <素材根> --embed-images
76
+ ```
77
+
78
+ ## 收尾
79
+
80
+ 1. 更新专题 `topic.md` 总表(派生视图)
81
+ 2. reverse 且要回推:**务必带 `--assets`**,否则 GitHub 上 `images/` 全裂:
82
+
83
+ ```bash
84
+ python3 scripts/push-to-topstream.py <包> --target notes/xxx.md --assets # dry-run
85
+ python3 scripts/push-to-topstream.py <包> --target notes/xxx.md --assets --apply \
86
+ --asset-names "00-封面.jpg=01-cover.jpg,…" # 仓库约定 ASCII 图名
87
+ ```
88
+
89
+ 仓库图路径约定:`notes/<slug>.md` 引用 `../assets/images/<slug>/NN-name.png`。
90
+ 脚本只降级转换 + 写 notes + 搬图;**不自动改** README 索引 / `docs/公众号映射.md` / frontmatter(防索引漂移)。手工完成后再把 `target_file` 改实际路径。
91
+ 3. 提醒:外链只能进「阅读原文」;图已 base64 内嵌,占位符按清单补传
92
+ 4. 需要进交付层时:`save-output` 拷贝终稿到 `88-交付/`(`YYYY-MM-DD-描述.ext`)
93
+
94
+ ## Desktop
95
+
96
+ | 步骤 | Desktop「公众号创作」 |
97
+ |------|----------------------|
98
+ | 建包/列表 | 扫工作区包根 |
99
+ | 改稿 | 写闸回执 |
100
+ | 三关 | AI 味 + lint + 事实勾选 |
101
+ | 排版 | 主题预览 + 调脚本导出 |
102
+ | 状态 | 调 `sync-status` |
@@ -0,0 +1,129 @@
1
+ # 写作质量参考
2
+
3
+ 定稿前三关的可执行清单,以及这几轮改稿里真实踩过的坑。
4
+
5
+ ## 一、事实关
6
+
7
+ ### 检查动作
8
+
9
+ 1. 列出全文所有承重数字(会被引用的、支撑结论的),逐条回溯一手来源
10
+ 2. 一手来源优先级:厂商定价页/官方博客/官方公告 > arXiv 原文 > 政府与监管机构网站 > 标准组织官网 > 一手媒体报道
11
+ 3. 标注口径:厂商自报标「厂商口径」;社区转述标「据报道」;预测类写清「这是预测,不是已发生的事实」
12
+ 4. 无一手来源的传闻**直接删**,不要为了完整留着
13
+
14
+ ### 高频错误
15
+
16
+ - **跨版本混用**:同一指标的不同年度版本数字混着用(IBM 2025 版 97% vs 2026 版 92%)
17
+ - **单位错误**:「人均代码 4450→7839 行」实际是每月不是每年
18
+ - **口径换算**:「3MB → 64MB 提高二十多倍」——前后计量方式不同(压缩后 MB vs 未压缩 MiB),不能换算
19
+ - **州别/主体错位**:「佛蒙特约 600 家数据经纪商」实际是加州 500-600、佛蒙特 283-467
20
+ - **把预测写成事实**:Gartner「2027 年底前 40% 项目被取消」是 2025-06 发布的**预测**
21
+
22
+ ### 核查员也会误判,必须自己复核
23
+
24
+ 出现「查不到/有反证」时,先确认它搜对了地方:
25
+
26
+ - Mem0 的 issue #4573 被误判为不存在——核查员搜的是 `anthropics/claude-code`,实际在 `mem0ai/mem0`
27
+ - ChatGPT 项目记忆 2025-08 被误判——`project-only memory` 与 `Projects` 功能是两回事
28
+ - Claude Cowork 记忆合并 2026-08-25 被误判——Cowork 产品 2026-01 发布,但记忆合并是 8 月
29
+
30
+ **可硬核实的数字自己动手取**:GitHub star 用 `curl https://api.github.com/repos/{owner}/{repo}` 取 `stargazers_count`,比搜来的区间值准。
31
+
32
+ ## 二、逻辑关
33
+
34
+ - **每个单边结论旁配反方证据**。只写一方,文章就只剩卖方叙事
35
+ - **结构前后一致**:结论前置的体例要贯穿全篇;短清单里的数字必须与正文同值
36
+ - **区分「预测」与「已发生」**、「厂商自报」与「第三方复测」、「相关」与「因果」
37
+ - **核心论断先找反证**:提出一个判断时,先搜「反对这个判断的证据」。曾因此把「harness 不重要」改写成更站得住的版本——找到了「只改 harness 不动模型就让准确率涨 13.7 个百分点」的硬反证
38
+
39
+ ## 三、文字关(去 AI 味)
40
+
41
+ 分两件事做,顺序不能反:**先立保真边界,再动词**。
42
+
43
+ | 场景 | 用哪个技能 | 做什么 |
44
+ |---|---|---|
45
+ | 中文长文 / 公众号改稿 | `humanizer-zh` + `qu-aiwei-zh` | 先立边界,再扫描定位(默认组合) |
46
+ | 只要分数和命中清单 | `qu-aiwei-zh` | 跑脚本,不改稿 |
47
+ | 随笔要「加声音」 | `qu-aiwei-zh` + `references/pan-style.md` | 用表达方式加,不编故事 |
48
+ | 英文文本 | `humanizer` | `humanizer-zh` 只针对中文 |
49
+ | 清单 / 词表 / 说明书 | 都不用 E 类姿态分 | 这类文体靠列举反例,检测器会把反例当真命中 |
50
+
51
+ ### 第一步:保真边界(`humanizer-zh`)
52
+
53
+ `qu-aiwei-zh` 管「像不像人写的」,`humanizer-zh` 管「改完意思有没有变」。31 条检查点每条都带「保留」边界,**是检查线索不是词语黑名单**——命中不等于要改,回上下文判断是否空泛、重复、歧义或与文体不符。
54
+
55
+ 动词前先钉住这些:
56
+
57
+ - 不新增原文没有的事实、数字、人名、日期、来源、经历、实现细节
58
+ - 不丢失否定、比较对象、范围、条件、时间、完成状态、归因
59
+ - 「可能」不改成「确定」,「计划」不改成「已经完成」,「相关」不改成「因果」
60
+ - 三项列表 / 四字格 / 破折号 / 被动句按作用判断,不确定作用就保留
61
+
62
+ ### ⚠️ 分数不够时不许补事实
63
+
64
+ `qu-aiwei-zh` 的结构维度会因「全文无数字」扣 12 分、「全文无第一人称」扣 8 分。**这两条是诊断,不是待办**:
65
+
66
+ - 原文确实没有数字 → 分数就停在那儿,**不许为了拿分编一个数**
67
+ - 原文是不需要第一人称的客观报道 → **不许硬塞「我」**
68
+
69
+ 抬分只有三条路:删套话、改表达、调节奏。补数字只有两条路:一手来源已在手,或用户提供。够不着就不补。
70
+
71
+ ### 第二步:扫描定位(`qu-aiwei-zh`)
72
+
73
+ 目标:`qu-aiwei-zh` 扫描 **≥ 85 分**。
74
+
75
+ ```bash
76
+ python3 scripts/scan_ai_flavor.py <稿>.md # 技能自带,与 qu-aiwei-zh 同源
77
+ ```
78
+
79
+ ### 删套话
80
+
81
+ 值得注意 / 值得记录 / 值得单独强调 / 本质上 / 事实上 / 意味着 / 由此可见 / 不难看出 / 换句话说 / 换言之 / 殊途同归 / 显而易见 / 众所周知 / 更进一步说
82
+
83
+ 改成直述。例:「最值得个人记住的其实是复盘里的一个细节」→「对个人最有参考价值的其实是复盘里的一个细节」→ 更好的是直接说细节是什么。
84
+
85
+ ### 删黑话
86
+
87
+ 护城河、生态位、闭环、打法、颗粒度、赋能、沉淀、赛道、倒逼、组合拳、卡位、底层逻辑
88
+
89
+ 换法:护城河→优势;闭环→这一轮/整条链;打法→做法;赛道→方向;沉淀→积累/形成。
90
+
91
+ **不要误伤专业用法**:「口径」(官方口径/社区口径)、「对齐」(alignment)、「收敛」(数学语义)都是正常的。
92
+
93
+ ### 降调段末金句
94
+
95
+ `**能搬走的数据,才是真正属于你的数据。**` → `能搬走的数据,才是真正属于你的数据。`
96
+
97
+ 删完套话却满篇加粗金句,等于从「AI 体」变成「新闻通稿」——同样没人味。
98
+
99
+ ### 保留的东西
100
+
101
+ - **「不是 A 而是 B」多数要保留**:「你要的是同事,不是神」「沙箱不是边界,是概率」都是有信息量的反转,只删纯修辞的重复
102
+ - **三段排比若内容扎实可留**,硬凑的才砍到两项或四项
103
+
104
+ ### 扫描器的两个误报(别照着改坏)
105
+
106
+ `qu-aiwei-zh` 会把两类东西误判成 AI 味,改之前先自己看一遍原文:
107
+
108
+ - **「排比三段式」误报数据陈述**:`从 15 个精简到 2 个,准确率从 80% 升到…`、`从发布、被审计、更正到上线` 是时间/数据变化,不是修辞排比,**保留**。
109
+ - **「复盘」误报专有名词**:「37signals 退出云复盘」里的「复盘」是标题/专名,不改;只有「我们来复盘一下」这种动词套话才改。
110
+
111
+ 判断优先级:**先看语义是不是真排比/真套话,再决定删不删**,不要按扫描器命中的字面动手。
112
+
113
+ ### 加人味(只加表达,不加事实)
114
+
115
+ 有「我」、有具体场景、有具体数字与人名、有不确定性表达(「其实」「不过」「还在想」)、句子长短交替、有真实细节而不是「很多人」「大多数」。
116
+
117
+ ⚠️ 这一段说的是**表达方式**。原文没有第一人称、没有数字时,不许靠这一条去补——那正好踩中 `humanizer-zh` 的优先级第 1 条。加声音的手段是句长节奏、设问、括号自注(见 `pan-style.md`),配合删掉元叙述和教学腔。`humanizer-zh` 里写着同一句话:「用户要求增强个人风格时,用表达方式实现,不编故事。」
118
+
119
+ 英文文本才走 `humanizer`:它的取向是 **Add soul**(主动注入个性),与中文改稿的**保真优先**方向相反,别把它的思路套到中文稿上。
120
+
121
+ ### 工具纪律
122
+
123
+ **并行编辑同一文件会互相覆盖**——同一文件的多处修改必须走**单个串行脚本**,改完立刻复核关键字段是否落盘(曾丢过 8 处,含 star 数与日期两处关键修正)。
124
+
125
+ ## 四、改稿红线
126
+
127
+ 不改技术事实、数据、评测结论、代码块内容。改稿改的是阅读节奏与表达,不是重写。
128
+
129
+ 外部工具改过的稿子(用户先拿别的模型改过一版再交回来):**先核数字,再动文字,顺序不能反**。实测一轮揪出 7 处错值,其中 5 处是「比值对但绝对值错」——比值正确会让错误绝对值显得可信。判据:出现比值对而绝对值错,说明写的人是从别的口径倒推,该段所有数字都要重核。
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """CSS 审计:本组技能无 CSS 资产;若未来加入 .css 文件,则做括号配平基础检查。"""
4
+ from __future__ import annotations
5
+
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ try:
10
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
11
+ except Exception:
12
+ pass
13
+
14
+ ROOT = Path(__file__).resolve().parent.parent
15
+ NAME = ROOT.name
16
+ errors: list[str] = []
17
+
18
+
19
+ def main() -> None:
20
+ print(f"[audit_css] {NAME}")
21
+ css_files = [p for p in ROOT.rglob("*.css") if ".git" not in p.parts and "dist" not in p.parts]
22
+ if not css_files:
23
+ print(" ✓ 无 CSS 资产,跳过")
24
+ print(f"\n[audit_css] {NAME}: 通过")
25
+ return
26
+ for css in css_files:
27
+ text = css.read_text(encoding="utf-8")
28
+ balanced = text.count("{") == text.count("}")
29
+ print((" ✓ " if balanced else " ✗ ") + f"{css.name} 括号配平")
30
+ if not balanced:
31
+ errors.append(css.name)
32
+ if errors:
33
+ print(f"\n[audit_css] {NAME}: {len(errors)} 项失败")
34
+ sys.exit(1)
35
+ print(f"\n[audit_css] {NAME}: 全部通过")
36
+
37
+
38
+ if __name__ == "__main__":
39
+ main()
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """文档审计:references 完整性与内部链接可达性(任一不过 → 退出码 1)。"""
4
+ from __future__ import annotations
5
+
6
+ import re
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ try:
11
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
12
+ except Exception:
13
+ pass
14
+
15
+ ROOT = Path(__file__).resolve().parent.parent
16
+ NAME = ROOT.name
17
+ errors: list[str] = []
18
+
19
+
20
+ def check(cond: bool, msg: str) -> None:
21
+ print((" ✓ " if cond else " ✗ ") + msg)
22
+ if not cond:
23
+ errors.append(msg)
24
+
25
+
26
+ def main() -> None:
27
+ print(f"[audit_docs] {NAME}")
28
+ md_files = [p for p in ROOT.rglob("*.md") if ".git" not in p.parts and "dist" not in p.parts]
29
+ check(len(md_files) >= 2, f"md 文档 ≥2(实 {len(md_files)},含 SKILL.md/README.md)")
30
+
31
+ refdir = ROOT / "references"
32
+ if refdir.is_dir():
33
+ refs = sorted(refdir.glob("*.md"))
34
+ check(len(refs) >= 1, f"references/*.md ≥1(实 {len(refs)})")
35
+ for md in refs:
36
+ check(md.stat().st_size > 0, f"references/{md.name} 非空")
37
+
38
+ for md in md_files:
39
+ text = md.read_text(encoding="utf-8")
40
+ rel = md.relative_to(ROOT).as_posix()
41
+ for link in set(re.findall(r"\]\(([^)]+)\)", text)):
42
+ if link.startswith(("http://", "https://", "#", "mailto:")):
43
+ continue
44
+ target = (md.parent / link.split("#")[0]).resolve()
45
+ if link and not target.exists():
46
+ # 允许指向脚本/资源等非 md 文件的相对链接缺失检查放宽到 md
47
+ if target.suffix == ".md":
48
+ check(False, f"{rel} 悬空链接: {link}")
49
+ check(True, "内部 md 链接可达")
50
+
51
+ if errors:
52
+ print(f"\n[audit_docs] {NAME}: {len(errors)} 项失败")
53
+ sys.exit(1)
54
+ print(f"\n[audit_docs] {NAME}: 全部通过")
55
+
56
+
57
+ if __name__ == "__main__":
58
+ main()
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """技能入口审计:SKILL.md 结构完整性(任一不过 → 退出码 1)。"""
4
+ from __future__ import annotations
5
+
6
+ import re
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ try:
11
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
12
+ except Exception:
13
+ pass
14
+
15
+ ROOT = Path(__file__).resolve().parent.parent
16
+ NAME = ROOT.name
17
+ errors: list[str] = []
18
+
19
+
20
+ def check(cond: bool, msg: str) -> None:
21
+ print((" ✓ " if cond else " ✗ ") + msg)
22
+ if not cond:
23
+ errors.append(msg)
24
+
25
+
26
+ def main() -> None:
27
+ print(f"[audit_skill] {NAME}")
28
+ text = (ROOT / "SKILL.md").read_text(encoding="utf-8")
29
+
30
+ m = re.search(r"^triggers:\s*\n((?: - .+\n?)+)", text, re.M)
31
+ triggers = re.findall(r"^ - (.+)$", m.group(1), re.M) if m else []
32
+ check(len(triggers) >= 1, f"triggers ≥1(实 {len(triggers)})")
33
+
34
+ check("When NOT to use" in text or "何时不用" in text, "含 When NOT to use / 何时不用")
35
+
36
+ has_workflow = bool(re.search(r"工作流|Workflow|用法|Usage", text))
37
+ check(has_workflow, "含工作流/用法说明")
38
+
39
+ fm = re.match(r"^---\r?\n([\s\S]*?)\r?\n---", text)
40
+ desc = ""
41
+ if fm:
42
+ dm = re.search(r'^description:\s*"([^"]*)"\s*$', fm.group(1), re.M)
43
+ if dm:
44
+ desc = dm.group(1).strip()
45
+ else:
46
+ dm = re.search(r"^description:\s*>\-\s*\n((?:[ \t]+.*\n?)+)", fm.group(1), re.M)
47
+ if dm:
48
+ desc = " ".join(l.strip() for l in dm.group(1).splitlines())
49
+ check("Use when" in desc or "用" in desc, "description 含使用场景")
50
+ check("Do NOT use" in desc or "不用" in desc, "description 含不适用场景")
51
+ check(len(desc) <= 1024, f"description ≤1024(实 {len(desc)})")
52
+
53
+ if errors:
54
+ print(f"\n[audit_skill] {NAME}: {len(errors)} 项失败")
55
+ sys.exit(1)
56
+ print(f"\n[audit_skill] {NAME}: 全部通过")
57
+
58
+
59
+ if __name__ == "__main__":
60
+ main()
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """样式资产审计:主题/风格资源有效性(任一不过 → 退出码 1)。
4
+
5
+ 本组技能无 CSS 样式表:若存在 assets/themes/*.json 则校验 JSON 可解析;
6
+ references 风格文档非空(已由 audit_docs 覆盖,此处做存在性断言)。
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ try:
15
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
16
+ except Exception:
17
+ pass
18
+
19
+ ROOT = Path(__file__).resolve().parent.parent
20
+ NAME = ROOT.name
21
+ errors: list[str] = []
22
+
23
+
24
+ def check(cond: bool, msg: str) -> None:
25
+ print((" ✓ " if cond else " ✗ ") + msg)
26
+ if not cond:
27
+ errors.append(msg)
28
+
29
+
30
+ def main() -> None:
31
+ print(f"[audit_styles] {NAME}")
32
+ themes = sorted((ROOT / "assets" / "themes").glob("*.json")) if (ROOT / "assets" / "themes").is_dir() else []
33
+ if themes:
34
+ for t in themes:
35
+ try:
36
+ json.loads(t.read_text(encoding="utf-8"))
37
+ check(True, f"assets/themes/{t.name} JSON 可解析")
38
+ except (OSError, json.JSONDecodeError) as e:
39
+ check(False, f"assets/themes/{t.name} 解析失败: {e}")
40
+ else:
41
+ check(True, "无主题 JSON 资产(本技能不依赖主题文件)")
42
+
43
+ refdir = ROOT / "references"
44
+ has_style_doc = refdir.is_dir() and any(refdir.glob("*.md"))
45
+ check(has_style_doc, "references 风格/格式文档存在")
46
+
47
+ if errors:
48
+ print(f"\n[audit_styles] {NAME}: {len(errors)} 项失败")
49
+ sys.exit(1)
50
+ print(f"\n[audit_styles] {NAME}: 全部通过")
51
+
52
+
53
+ if __name__ == "__main__":
54
+ main()