@topmindspace/topmind-writing-skills 0.7.1

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 (115) hide show
  1. package/CHANGELOG.md +931 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +124 -0
  4. package/README.md +124 -0
  5. package/bin/topmind-writing-skills.js +388 -0
  6. package/package.json +56 -0
  7. package/topmind-briefs/CHANGELOG.md +22 -0
  8. package/topmind-briefs/README.en.md +41 -0
  9. package/topmind-briefs/README.md +37 -0
  10. package/topmind-briefs/SKILL.md +101 -0
  11. package/topmind-briefs/dist/topmind-briefs.manifest.json +21 -0
  12. package/topmind-briefs/dist/topmind-briefs.zip +0 -0
  13. package/topmind-briefs/package-lock.json +13 -0
  14. package/topmind-briefs/package.json +1 -0
  15. package/topmind-briefs/references/examples.md +131 -0
  16. package/topmind-briefs/references/platform-specs.md +63 -0
  17. package/topmind-briefs/references/style-guide.md +60 -0
  18. package/topmind-briefs/scripts/audit_css.py +39 -0
  19. package/topmind-briefs/scripts/audit_docs.py +80 -0
  20. package/topmind-briefs/scripts/audit_skill.py +60 -0
  21. package/topmind-briefs/scripts/audit_styles.py +54 -0
  22. package/topmind-briefs/scripts/negative_tests.py +88 -0
  23. package/topmind-briefs/scripts/package_skill.py +188 -0
  24. package/topmind-cover/README.en.md +54 -0
  25. package/topmind-cover/README.md +54 -0
  26. package/topmind-cover/SKILL.md +134 -0
  27. package/topmind-cover/assets/examples/README.md +34 -0
  28. package/topmind-cover/assets/examples/bg-blur-alt.png +0 -0
  29. package/topmind-cover/assets/examples/bg-blur-wechat.png +0 -0
  30. package/topmind-cover/assets/examples/bg-blur.png +0 -0
  31. package/topmind-cover/assets/examples/big-type-wechat.png +0 -0
  32. package/topmind-cover/assets/examples/big-type.png +0 -0
  33. package/topmind-cover/assets/examples/brand-launch-wechat.png +0 -0
  34. package/topmind-cover/assets/examples/brand-launch.png +0 -0
  35. package/topmind-cover/assets/examples/gan-huo-wechat.png +0 -0
  36. package/topmind-cover/assets/examples/gan-huo.png +0 -0
  37. package/topmind-cover/assets/examples/ip-fun-wechat.png +0 -0
  38. package/topmind-cover/assets/examples/ip-fun.png +0 -0
  39. package/topmind-cover/assets/examples/magazine-wechat.png +0 -0
  40. package/topmind-cover/assets/examples/magazine.png +0 -0
  41. package/topmind-cover/assets/examples/minimal-wechat.png +0 -0
  42. package/topmind-cover/assets/examples/minimal.png +0 -0
  43. package/topmind-cover/assets/examples/news-flash-wechat.png +0 -0
  44. package/topmind-cover/assets/examples/news-flash.png +0 -0
  45. package/topmind-cover/assets/examples/overview.png +0 -0
  46. package/topmind-cover/assets/examples/paper-collage-alt.png +0 -0
  47. package/topmind-cover/assets/examples/paper-collage-wechat.png +0 -0
  48. package/topmind-cover/assets/examples/paper-collage.png +0 -0
  49. package/topmind-cover/assets/examples/tutorial-steps-wechat.png +0 -0
  50. package/topmind-cover/assets/examples/tutorial-steps.png +0 -0
  51. package/topmind-cover/assets/examples/white-clean-alt-wechat.png +0 -0
  52. package/topmind-cover/assets/examples/white-clean-alt.png +0 -0
  53. package/topmind-cover/assets/examples/white-clean-wechat.png +0 -0
  54. package/topmind-cover/assets/examples/white-clean.png +0 -0
  55. package/topmind-cover/package-lock.json +16 -0
  56. package/topmind-cover/package.json +16 -0
  57. package/topmind-cover/references/cover-prompts.md +436 -0
  58. package/topmind-cover/references/cover-styles.md +496 -0
  59. package/topmind-cover/references/impact-language.md +94 -0
  60. package/topmind-cover/references/ref-white-clean-1.png +0 -0
  61. package/topmind-cover/references/ref-white-clean-2.png +0 -0
  62. package/topmind-cover/scripts/audit_css.py +39 -0
  63. package/topmind-cover/scripts/audit_docs.py +80 -0
  64. package/topmind-cover/scripts/audit_skill.py +60 -0
  65. package/topmind-cover/scripts/audit_styles.py +54 -0
  66. package/topmind-cover/scripts/crop-cover.py +74 -0
  67. package/topmind-cover/scripts/negative_tests.py +71 -0
  68. package/topmind-cover/scripts/package_skill.py +188 -0
  69. package/topmind-wechat-post/README.en.md +111 -0
  70. package/topmind-wechat-post/README.md +111 -0
  71. package/topmind-wechat-post/SKILL.md +251 -0
  72. package/topmind-wechat-post/assets/themes/amber-review.json +35 -0
  73. package/topmind-wechat-post/assets/themes/graphite.json +35 -0
  74. package/topmind-wechat-post/assets/themes/minimal-ink.json +35 -0
  75. package/topmind-wechat-post/assets/themes/newsprint.json +35 -0
  76. package/topmind-wechat-post/assets/themes/tech-blue.json +35 -0
  77. package/topmind-wechat-post/package-lock.json +16 -0
  78. package/topmind-wechat-post/package.json +16 -0
  79. package/topmind-wechat-post/references/design.md +50 -0
  80. package/topmind-wechat-post/references/element-spec.md +163 -0
  81. package/topmind-wechat-post/references/known-pits.md +85 -0
  82. package/topmind-wechat-post/references/theme-map.md +139 -0
  83. package/topmind-wechat-post/references/typography-rules.md +62 -0
  84. package/topmind-wechat-post/references/wechat-constraints.md +111 -0
  85. package/topmind-wechat-post/references/workflow.md +102 -0
  86. package/topmind-wechat-post/references/writing-quality.md +105 -0
  87. package/topmind-wechat-post/scripts/audit_css.py +39 -0
  88. package/topmind-wechat-post/scripts/audit_docs.py +80 -0
  89. package/topmind-wechat-post/scripts/audit_skill.py +60 -0
  90. package/topmind-wechat-post/scripts/audit_styles.py +54 -0
  91. package/topmind-wechat-post/scripts/lint-wechat.py +446 -0
  92. package/topmind-wechat-post/scripts/md2wechat.py +1792 -0
  93. package/topmind-wechat-post/scripts/negative_tests.py +171 -0
  94. package/topmind-wechat-post/scripts/new-article.py +183 -0
  95. package/topmind-wechat-post/scripts/package_skill.py +188 -0
  96. package/topmind-wechat-post/scripts/push-to-topstream.py +226 -0
  97. package/topmind-wechat-post/scripts/scan_ai_flavor.py +464 -0
  98. package/topmind-wechat-post/scripts/sync-mapping.py +220 -0
  99. package/topmind-wechat-post/scripts/sync-status.py +187 -0
  100. package/topmind-x-article/README.en.md +109 -0
  101. package/topmind-x-article/README.md +104 -0
  102. package/topmind-x-article/SKILL.md +156 -0
  103. package/topmind-x-article/package-lock.json +16 -0
  104. package/topmind-x-article/package.json +16 -0
  105. package/topmind-x-article/references/publish-checklist.md +31 -0
  106. package/topmind-x-article/references/x-format.md +27 -0
  107. package/topmind-x-article/references/x-html-format.md +85 -0
  108. package/topmind-x-article/scripts/audit_css.py +39 -0
  109. package/topmind-x-article/scripts/audit_docs.py +80 -0
  110. package/topmind-x-article/scripts/audit_skill.py +60 -0
  111. package/topmind-x-article/scripts/audit_styles.py +54 -0
  112. package/topmind-x-article/scripts/md2x-html.py +522 -0
  113. package/topmind-x-article/scripts/md2x.py +323 -0
  114. package/topmind-x-article/scripts/negative_tests.py +91 -0
  115. package/topmind-x-article/scripts/package_skill.py +188 -0
@@ -0,0 +1,94 @@
1
+ # 冲击力设计语言(抽象原则清单)
2
+
3
+ > 用途:给封面 prompt 编写者的"敢用力"依据。与 `cover-styles.md` 的原创铁律配合使用——
4
+ > 本文件只给可迁移的抽象原则,不给具体版式;任何原则落地时,版式与配色不得与第三方封面构成实质相似。
5
+ > 具体版式红线见 `docs/audit-2026-09-29-round5.md` 第一节差异审查表。
6
+
7
+ ## 一、字体:字是第一眼
8
+
9
+ 1. **标题字高目标冲击 1/3(空间允许时),底线 ≥1/4。**——为什么有效:封面在信息流里先以缩略图出现,字小等于没写标题;1/3 是"一眼即主题"的经验阈值,空间不够时才退到 1/4。
10
+ 2. **全图只给一处强调**(一个关键词或一个数字),用字号/色彩/描边等手法与其它文字拉开断层式差距。——为什么有效:处处强调等于没有强调,单点强调才能形成视觉锚。
11
+ 3. **字色与底色明度差拉到最大**(深底配浅字、浅底配深字)。——为什么有效:对比是可读性的前提,也是缩略图下抓住注意力的最廉价手段。
12
+ 4. **数字优先于文字做视觉钩子。**——为什么有效:数字比文字更快被大脑处理,且自带"具体、可验证"的信任感,3 秒内信息密度最高。
13
+ 5. **字体性格与内容情绪一致**(硬朗/书法/圆润/纤细各有脾气)。——为什么有效:字形本身就是情绪,字形与内容违和会直接削弱信任。
14
+
15
+ ## 二、色彩:0.3 秒的情绪预告
16
+
17
+ 6. **一主色 + 一强调色;强调色只出现在一处或一类元素上。**——为什么有效:色彩记忆点来自克制,第二种强调色出现画面就"吵"了。
18
+ 7. **主色与底色/字色形成明确对立**(明暗对立或冷暖对立二选一)。——为什么有效:对立产生张力,张力产生注视;没对比的画面在信息流里直接隐身。
19
+ 8. **底色明度走极端**(极浅或极深),避开中间灰。——为什么有效:极端的明度选择让画面有"立场",中间灰最安全也最容易被滑过。
20
+ 9. **情绪色与题材匹配**(科技/活力/阅读感各有对应的色彩情绪)。——为什么有效:读者在读字之前,先被颜色"说服"一半。
21
+
22
+ ## 三、构图:先有动线,再摆元素
23
+
24
+ 10. **先定一条主阅读动线,再摆元素;全图只允许一条。**——为什么有效:眼睛需要被引导,没有动线的画面信息是散的,3 秒内形不成记忆。
25
+ 11. **文字区与视觉主体分区明确、互不遮挡。**——为什么有效:字被挡=信息丢失;分区让大脑一次只处理一类信息,认知负荷最低。
26
+ 12. **留白是设计元素,不是"没画完"。**——为什么有效:留白制造对比、托出主体;满版无呼吸的画面在小图下糊成一团。
27
+ 13. **每张图必须有一个记忆点元素**(形象、数字、符号、质感四选一)。——为什么有效:人记不住"一张好看的图",只记得住一个具体的"东西"。
28
+
29
+ ## 四、信息层级:3 秒能读到什么
30
+
31
+ 14. **3 秒测试:遮住细节,3 秒内能说出"这是什么、为什么值得点"。**——为什么有效:封面的唯一 KPI 是点击率,3 秒读不懂=设计失败。
32
+ 15. **信息分层、各司其职:钩子(为什么停)→ 主题(这是什么)→ 背书(凭什么信)。**——为什么有效:对应读者"被吸引→懂主题→信可信"的决策链,缺一层就断一环。
33
+ 16. **背书层只给"点击理由"**(数据、筛选口径、作者身份三选一)。——为什么有效:标题负责"是什么",背书负责"凭什么信你",分工不清两头都弱。
34
+ 17. **元素总数 ≤5。**——为什么有效:每多一个元素,视觉权重就被稀释一分;克制是冲击力的另一面。
35
+
36
+ ## 五、质感:不单调的来源
37
+
38
+ 18. **纯色底必须配一种背景质感**(纸纹/噪点/光影渐变/细微纹理四选一)。——为什么有效:AI 生成的纯色底最容易显"塑料感",一层细微质感立刻拉开与模板图的差距。
39
+ 19. **质感只做背景层,不进文字区。**——为什么有效:质感是氛围组,进了文字区就是噪音;克制使用才"高级"。
40
+ 20. **保留一处手工感/真实感细节**(笔触、纸纹、真实小物件、自然光影四选一)。——为什么有效:AI 图最缺的是"人味",一个不完美的细节让画面可信度上升。
41
+
42
+ ## 六、保守陷阱清单(前几轮样张犯过的错)
43
+
44
+ | # | 保守错误 | 纠正方向 |
45
+ |---|---|---|
46
+ | 1 | 标题太小(字高 <1/5 画面) | 先定标题字高目标 1/3(底线 ≥1/4),再摆其他元素;标题是视觉重心,不是配角 |
47
+ | 2 | 对比太弱(灰底灰字、强调色饱和度不足) | 字色底色明度差拉满;强调色敢用高饱和,但只用一处 |
48
+ | 3 | 画面太平(元素平均用力、无重心、无动线) | 先定一条主阅读动线和一个最大元素,其他全部让路 |
49
+ | 4 | 元素太多太碎(>5 个、装饰无节制) | 每加一个元素就问"删掉它会损失什么",答不上来就删 |
50
+ | 5 | 数字不敢放大(数据藏在角落小字里) | 真实数字是天然钩子,值得给最大字号或最跳的颜色(只给一处) |
51
+ | 6 | 质感不敢加(纯色底一片死平) | 至少加一层纸纹/噪点/光影,放在背景层 |
52
+ | 7 | 情绪不敢给(断言、感叹全磨平) | 在标题或背书层保留一处情绪支点(断言/感叹/反问三选一),只一处 |
53
+ | 8 | 配色求稳(全用安全中性色) | 主色敢选有情绪的颜色;要克制的是"颜色数量",不是"颜色性格" |
54
+
55
+ ## 七、文字装饰系统(第九轮新增:文字排印与装饰)
56
+
57
+ **总原则:中文正确性永远优先于花样。**图像模型对中文大字的渲染能力有限,任何装饰不得导致错字、乱码、笔画粘连。装饰是"放大记忆点"的手段,不是"堆花样"的目的——每张图选 2~3 种装饰,用到就够,多一种就问自己一次"删掉它会损失什么"(参见第四条规则)。
58
+
59
+ ### 7.1 六种可靠装饰(按优先级从高到低)
60
+
61
+ | # | 装饰 | 效果 | 风险 | 适用风格 | 提示词写法(具体可执行) |
62
+ |---|---|---|---|---|---|
63
+ | 1 | 关键词变色高亮 | 1~2 个关键词换强调色,成为全图视觉锚点;3 秒内眼睛先被"有色"字抓住 | 一句话超 3 色会吵;低对比配色(浅灰底白字)直接隐身 | gan-huo、news-flash、tutorial-steps、magazine、white-clean、bg-blur、paper-collage | 把"翻车"两字换成朱红 #E6392B,其余字米白 #FAF6EC;整句只用这两种颜色 |
64
+ | 2 | 字号对比 | 关键词放大 1.3~1.6 倍,信息层级瞬间拉开,钩子词拥有最大视觉权重 | 放大超 1.6 倍易挤占构图、导致换行断裂;数字放大优先于文字放大 | big-type、gan-huo、news-flash、magazine、bg-blur | 标题中"暴跌"二字字号放大到其余字的 1.5 倍,其余字保持原字号,不换行 |
65
+ | 3 | 文字底部色块/描边 | 色块衬底:关键词下垫一块实色矩形,街头海报感,远距离可读;描边:深底浅字时勾勒字形,字更"跳" | 色块过厚会吞掉字下半部;浅色字+浅色色块对比不足;描边过粗导致笔画粘连 | brand-launch(色块)、big-type(描边)、ip-fun(色块)、white-clean(色块/马克笔横条) | "暴跌"两字下方垫一块朱红 #E6392B 实色矩形,矩形高度为字高的 1.2 倍,字为米白压在色块上;描边写法:米白字加 2px 深灰描边,描边宽度不超过笔画宽度的 1/5 |
66
+ | 4 | 轻微阴影/立体 | 字从底色上"浮"起来,增加纵深;轻微立体(1~2 层错位)有质感 | 三重叠加(描边+阴影+立体)必糊;阴影过重显脏;深色底+深色阴影等于没加 | big-type、brand-launch、ip-fun | 标题字加一层向右下 45° 的柔和投影,投影颜色 #000000 不透明度 30%,偏移距离为字高的 1/10;立体写法:字后方加一层同字形、朱红 #E6392B、向右下错位 4px 的色块字,只错位一层 |
67
+ | 5 | 手绘下划线/圈注/箭头 | 手绘感=人味(参见第 20 条),划线给关键词"划重点",圈注给数据"画圈强调",箭头给动线"指路" | 线条过粗会压住字;圈注套住整句等于没强调;箭头超过 2 个画面变流程图 | gan-huo(圈注)、news-flash(箭头)、tutorial-steps(下划线/箭头)、white-clean(马克笔横条)、paper-collage(圈注) | 在"翻车"两字下方画一条手绘感波浪下划线,朱红 #E6392B,线宽为笔画宽度的 1/2,略带抖动不压字;圈注写法:用朱红马克笔笔触把"23.2%"圈起来,圈略大于字、不压笔画;箭头写法:从标题指向配图的红色手绘箭头,一条,线条粗细均匀 |
68
+ | 6 | 适度旋转 | 印章/标签/角标类小元素倾斜 ≤8°,打破呆板,制造"贴上去"的真实感 | 大标题旋转必乱码;竖排+旋转组合必翻车;旋转超 8° 显廉价 | 仅用于印章/标签/角标小元素(如"实测""必看"角标),不用于大标题 | 右上角"实测"角标整体顺时针旋转 6°,字不转、整体转;大标题保持水平,一个字都不许倾斜 |
69
+
70
+ ### 7.2 八风格装饰指定(每风格 2~3 种必用)
71
+
72
+ | 风格 | 必用装饰 | 理由 |
73
+ |---|---|---|
74
+ | gan-huo(干货) | ①关键词变色高亮 ②色块衬底 ③手绘圈注 | 干货风靠"划重点"建立信任:变色定锚点、色块给海报感、圈注圈住关键数据,三件套=把"值得收藏"写在脸上 |
75
+ | big-type(大字报) | ②字号对比 ③描边 ④轻微立体 | 大字报的全部冲击力来自字本身:字号对比拉层级、描边让字跳、轻微立体给纵深,三者都作用于字形、不引入外部元素,字即画面 |
76
+ | brand-launch(品牌发布) | ③色块衬底 ④轻微阴影 | 品牌发布要"贵气+仪式感":色块衬底是发布会主视觉的经典语言,轻微阴影给字高级的浮起感,克制不用手绘元素以免掉价 |
77
+ | tutorial-steps(教程步骤) | ①关键词变色高亮 ⑤手绘下划线/箭头 | 教程风需要"指路感":变色标出每步关键词,下划线/箭头串起步骤动线,读者一眼知道先看哪、后看哪 |
78
+ | ip-fun(IP 趣味) | ③色块衬底 ④轻微立体 | IP 趣味风要"潮玩感":色块衬底像贴纸,轻微立体像徽章凸起,两者叠加出潮玩包装的质感,忌用严肃的手绘批注 |
79
+ | news-flash(新闻快讯) | ①关键词变色高亮 ②字号对比 ⑤手绘箭头 | 快讯风=突发感:变色+放大双管齐下让爆点词在信息流里"炸"出来,一条箭头指向配图、制造"正在发生"的现场感 |
80
+ | minimal(极简) | 只用一点睛色块(③的克制版) | 极简的冲击力来自克制本身:只允许一处色块衬底点睛,多一种装饰就破功;变色、旋转、手绘一律不用 |
81
+ | magazine(杂志风) | ①关键词变色高亮 ②字号对比 | 杂志封面语言就是"大标题+关键词强调":变色给情绪、字号对比给层级,两种都作用于标题排印本身,不加外部装饰以保编辑感 |
82
+ | white-clean(白色清新) | ①关键词变色高亮 ③色块衬底/马克笔横条 ⑤手绘下划线 | 清新风靠"撞色+横条"制造亲和的干货感:变色定锚点、马克笔横条给"划重点"的人味;每次只用其中 2~3 种,小装饰(星星/感叹号)至多 2 处 |
83
+ | bg-blur(背景虚化) | ①关键词变色高亮 ②字号对比 | 虚化风的冲击力全在"虚实对比"上:标题只用变色+放大两种装饰,背景光斑不抢戏;标题必须压清晰区 |
84
+ | paper-collage(纸感拼贴) | ①关键词变色高亮 ⑤手绘圈注 | 拼贴风要"手作感":变色给标题关键词,圈注只圈最小标签一处;纸片本身已是装饰,标题装饰 2 种封顶 |
85
+
86
+ ### 7.3 装饰禁令清单(Worker B:写入各风格"绝不"清单)
87
+
88
+ 1. **禁透视变形大标题**——大标题做透视/扭曲/弧形排列,中文必乱码;要动感用轻微立体代替。
89
+ 2. **禁一句话超 3 种颜色**——标题内颜色种类(含字色、色块色、描边色)不得超过 3 种,超了画面就"吵"。
90
+ 3. **禁描边+阴影+立体三重叠加**——三种效果最多同时用一种(big-type 可用描边+轻微立体两种,但不得再加阴影)。
91
+ 4. **禁竖排+旋转组合**——竖排文字不得再叠加任何旋转;旋转只允许用于印章/标签类小元素,且 ≤8°,大标题永远水平。
92
+ 5. **禁色块吞字**——色块衬底高度不得超过字高的 1.3 倍,色块与字色必须明暗对立(深底浅字或浅底深字),对比不足宁可不用。
93
+ 6. **禁装饰压笔画**——手绘下划线/圈注/箭头不得压住文字笔画;圈注只圈 1~2 个词或一个数字,圈住整句等于没强调。
94
+ 7. **禁装饰超 3 种**——单张图装饰种类不得超过 7.2 表中该风格指定的数量(minimal 只许 1 种);多一种就问"删掉它会损失什么"。
@@ -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,80 @@
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
+ # 反引号代码引用中的路径可达(审计 轻微-15 门禁盲区:之前只查 [text](path) 形式,
52
+ # `references/pan-style.md` 这类反引号引用曾漏网)。抓取反引号内的 references/*.md
53
+ # 与 scripts/* 路径并校验存在;glob/占位符写法(* ? < > |)跳过。
54
+ backtick_path_re = re.compile(r"(references|scripts)/([\w.\-/]+)")
55
+ line_suffix_re = re.compile(r":\d+(?:-\d+)?$")
56
+ backtick_bad = 0
57
+ for md in md_files:
58
+ text = md.read_text(encoding="utf-8")
59
+ rel = md.relative_to(ROOT).as_posix()
60
+ for span in set(re.findall(r"`([^`]+)`", text)):
61
+ for kind, p in backtick_path_re.findall(span):
62
+ p = line_suffix_re.sub("", p)
63
+ if not p or any(c in p for c in "*?<>|"):
64
+ continue
65
+ target = md.parent / kind / p
66
+ if not target.exists():
67
+ target = ROOT / kind / p
68
+ if not target.exists():
69
+ backtick_bad += 1
70
+ check(False, f"{rel} 反引号引用不可达: `{kind}/{p}`")
71
+ check(backtick_bad == 0, f"反引号内 references/scripts 引用可达(扫描 {len(md_files)} 个 md 文件)")
72
+
73
+ if errors:
74
+ print(f"\n[audit_docs] {NAME}: {len(errors)} 项失败")
75
+ sys.exit(1)
76
+ print(f"\n[audit_docs] {NAME}: 全部通过")
77
+
78
+
79
+ if __name__ == "__main__":
80
+ 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()
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env python3
2
+ """封面图裁剪:从 5:2 主图中央裁出公众号 2.35:1 版,并统一落盘命名。
3
+
4
+ 用法:
5
+ python3 crop-cover.py <主图路径> --out-dir <包>/images/
6
+
7
+ 产出:
8
+ <out-dir>/00-封面.png 1500x600 (X / 公众号共用主文件)
9
+ <out-dir>/00-封面-公众号.png 900x383 (公众号封面大图,中央裁剪)
10
+ """
11
+ import argparse
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ try:
16
+ from PIL import Image
17
+ except ImportError:
18
+ sys.exit("需要 Pillow:pip install pillow")
19
+
20
+ TARGETS = {
21
+ "00-封面.png": (1500, 600),
22
+ "00-封面-公众号.png": (900, 383),
23
+ }
24
+
25
+
26
+ def main() -> None:
27
+ ap = argparse.ArgumentParser()
28
+ ap.add_argument("src", help="5:2 主图路径")
29
+ ap.add_argument("--out-dir", required=True)
30
+ args = ap.parse_args()
31
+
32
+ src = Path(args.src)
33
+ out_dir = Path(args.out_dir)
34
+ try:
35
+ out_dir.mkdir(parents=True, exist_ok=True)
36
+ except OSError as e:
37
+ sys.exit(f"错误:无法创建输出目录 {out_dir}({e})")
38
+
39
+ if not src.is_file():
40
+ sys.exit(f"错误:找不到输入图片 {src}")
41
+ try:
42
+ img = Image.open(src).convert("RGB")
43
+ except Exception as e:
44
+ sys.exit(f"错误:无法读取图片 {src}({e})")
45
+ w, h = img.size
46
+
47
+ # 先按 5:2 中央裁一块基准
48
+ base_ratio = 5 / 2
49
+ if w / h > base_ratio:
50
+ cw, ch = int(h * base_ratio), h
51
+ else:
52
+ cw, ch = w, int(w / base_ratio)
53
+ x0, y0 = (w - cw) // 2, (h - ch) // 2
54
+ base = img.crop((x0, y0, x0 + cw, y0 + ch))
55
+
56
+ try:
57
+ for name, (tw, th) in TARGETS.items():
58
+ ratio = tw / th
59
+ bw, bh = base.size
60
+ if bw / bh > ratio:
61
+ cw2, ch2 = int(bh * ratio), bh
62
+ else:
63
+ cw2, ch2 = bw, int(bw / ratio)
64
+ x1, y1 = (bw - cw2) // 2, (bh - ch2) // 2
65
+ out = base.crop((x1, y1, x1 + cw2, y1 + ch2)).resize((tw, th), Image.LANCZOS)
66
+ dest = out_dir / name
67
+ out.save(dest)
68
+ print(f"OK {dest} ({tw}x{th})")
69
+ except OSError as e:
70
+ sys.exit(f"错误:写入封面图失败({e})")
71
+
72
+
73
+ if __name__ == "__main__":
74
+ main()
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """topmind-cover 异常输入测试:crop-cover.py 错误路径干净(任一失败 → 退出码 1)。"""
4
+ from __future__ import annotations
5
+
6
+ import subprocess
7
+ import sys
8
+ import tempfile
9
+ from pathlib import Path
10
+
11
+ try:
12
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
13
+ except Exception:
14
+ pass
15
+
16
+ ROOT = Path(__file__).resolve().parent.parent
17
+ CROP = ROOT / "scripts" / "crop-cover.py"
18
+ PY = sys.executable
19
+ fails: list[str] = []
20
+
21
+
22
+ def run(args: list[str]) -> subprocess.CompletedProcess:
23
+ return subprocess.run([PY, str(CROP), *args], capture_output=True, text=True, timeout=60)
24
+
25
+
26
+ def check(name: str, cond: bool, detail: str = "") -> None:
27
+ print((" ✓ " if cond else " ✗ ") + name + (f" — {detail}" if detail and not cond else ""))
28
+ if not cond:
29
+ fails.append(name)
30
+
31
+
32
+ def main() -> None:
33
+ print("[negative_tests] topmind-cover")
34
+ with tempfile.TemporaryDirectory() as td:
35
+ td = Path(td)
36
+
37
+ # 1. 缺失输入 → 非零退出,无 Traceback
38
+ r = run([str(td / "nope.png"), "--out-dir", str(td / "o1")])
39
+ check("缺失输入干净报错", r.returncode != 0 and "Traceback" not in r.stderr, r.stderr[:100])
40
+
41
+ # 2. 非图片文件 → 非零退出,无 Traceback
42
+ (td / "notimg.txt").write_text("hello", encoding="utf-8")
43
+ r = run([str(td / "notimg.txt"), "--out-dir", str(td / "o2")])
44
+ check("非图片干净报错", r.returncode != 0 and "Traceback" not in r.stderr, r.stderr[:100])
45
+
46
+ # 3. 正常图片产出双尺寸
47
+ try:
48
+ from PIL import Image
49
+ img = Image.new("RGB", (1600, 900), (20, 30, 60))
50
+ img.save(td / "ok.png")
51
+ r = run([str(td / "ok.png"), "--out-dir", str(td / "o3")])
52
+ got_a = (td / "o3" / "00-封面.png").exists()
53
+ got_b = (td / "o3" / "00-封面-公众号.png").exists()
54
+ check("正常图片产出双尺寸", r.returncode == 0 and got_a and got_b, r.stderr[:100])
55
+ if got_a:
56
+ w, h = Image.open(td / "o3" / "00-封面.png").size
57
+ check("主图 1500×600", (w, h) == (1500, 600), f"实 {w}×{h}")
58
+ if got_b:
59
+ w, h = Image.open(td / "o3" / "00-封面-公众号.png").size
60
+ check("公众号图 900×383", (w, h) == (900, 383), f"实 {w}×{h}")
61
+ except ImportError:
62
+ print(" - 跳过图片生成测试(无 Pillow)")
63
+
64
+ if fails:
65
+ print(f"\n[negative_tests] {len(fails)} 项失败")
66
+ sys.exit(1)
67
+ print("\n[negative_tests] 全部通过")
68
+
69
+
70
+ if __name__ == "__main__":
71
+ main()