@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.
- package/CHANGELOG.md +931 -0
- package/LICENSE +21 -0
- package/README.en.md +124 -0
- package/README.md +124 -0
- package/bin/topmind-writing-skills.js +388 -0
- package/package.json +56 -0
- package/topmind-briefs/CHANGELOG.md +22 -0
- package/topmind-briefs/README.en.md +41 -0
- package/topmind-briefs/README.md +37 -0
- package/topmind-briefs/SKILL.md +101 -0
- package/topmind-briefs/dist/topmind-briefs.manifest.json +21 -0
- package/topmind-briefs/dist/topmind-briefs.zip +0 -0
- package/topmind-briefs/package-lock.json +13 -0
- package/topmind-briefs/package.json +1 -0
- package/topmind-briefs/references/examples.md +131 -0
- package/topmind-briefs/references/platform-specs.md +63 -0
- package/topmind-briefs/references/style-guide.md +60 -0
- package/topmind-briefs/scripts/audit_css.py +39 -0
- package/topmind-briefs/scripts/audit_docs.py +80 -0
- package/topmind-briefs/scripts/audit_skill.py +60 -0
- package/topmind-briefs/scripts/audit_styles.py +54 -0
- package/topmind-briefs/scripts/negative_tests.py +88 -0
- package/topmind-briefs/scripts/package_skill.py +188 -0
- package/topmind-cover/README.en.md +54 -0
- package/topmind-cover/README.md +54 -0
- package/topmind-cover/SKILL.md +134 -0
- package/topmind-cover/assets/examples/README.md +34 -0
- package/topmind-cover/assets/examples/bg-blur-alt.png +0 -0
- package/topmind-cover/assets/examples/bg-blur-wechat.png +0 -0
- package/topmind-cover/assets/examples/bg-blur.png +0 -0
- package/topmind-cover/assets/examples/big-type-wechat.png +0 -0
- package/topmind-cover/assets/examples/big-type.png +0 -0
- package/topmind-cover/assets/examples/brand-launch-wechat.png +0 -0
- package/topmind-cover/assets/examples/brand-launch.png +0 -0
- package/topmind-cover/assets/examples/gan-huo-wechat.png +0 -0
- package/topmind-cover/assets/examples/gan-huo.png +0 -0
- package/topmind-cover/assets/examples/ip-fun-wechat.png +0 -0
- package/topmind-cover/assets/examples/ip-fun.png +0 -0
- package/topmind-cover/assets/examples/magazine-wechat.png +0 -0
- package/topmind-cover/assets/examples/magazine.png +0 -0
- package/topmind-cover/assets/examples/minimal-wechat.png +0 -0
- package/topmind-cover/assets/examples/minimal.png +0 -0
- package/topmind-cover/assets/examples/news-flash-wechat.png +0 -0
- package/topmind-cover/assets/examples/news-flash.png +0 -0
- package/topmind-cover/assets/examples/overview.png +0 -0
- package/topmind-cover/assets/examples/paper-collage-alt.png +0 -0
- package/topmind-cover/assets/examples/paper-collage-wechat.png +0 -0
- package/topmind-cover/assets/examples/paper-collage.png +0 -0
- package/topmind-cover/assets/examples/tutorial-steps-wechat.png +0 -0
- package/topmind-cover/assets/examples/tutorial-steps.png +0 -0
- package/topmind-cover/assets/examples/white-clean-alt-wechat.png +0 -0
- package/topmind-cover/assets/examples/white-clean-alt.png +0 -0
- package/topmind-cover/assets/examples/white-clean-wechat.png +0 -0
- package/topmind-cover/assets/examples/white-clean.png +0 -0
- package/topmind-cover/package-lock.json +16 -0
- package/topmind-cover/package.json +16 -0
- package/topmind-cover/references/cover-prompts.md +436 -0
- package/topmind-cover/references/cover-styles.md +496 -0
- package/topmind-cover/references/impact-language.md +94 -0
- package/topmind-cover/references/ref-white-clean-1.png +0 -0
- package/topmind-cover/references/ref-white-clean-2.png +0 -0
- package/topmind-cover/scripts/audit_css.py +39 -0
- package/topmind-cover/scripts/audit_docs.py +80 -0
- package/topmind-cover/scripts/audit_skill.py +60 -0
- package/topmind-cover/scripts/audit_styles.py +54 -0
- package/topmind-cover/scripts/crop-cover.py +74 -0
- package/topmind-cover/scripts/negative_tests.py +71 -0
- package/topmind-cover/scripts/package_skill.py +188 -0
- package/topmind-wechat-post/README.en.md +111 -0
- package/topmind-wechat-post/README.md +111 -0
- package/topmind-wechat-post/SKILL.md +251 -0
- package/topmind-wechat-post/assets/themes/amber-review.json +35 -0
- package/topmind-wechat-post/assets/themes/graphite.json +35 -0
- package/topmind-wechat-post/assets/themes/minimal-ink.json +35 -0
- package/topmind-wechat-post/assets/themes/newsprint.json +35 -0
- package/topmind-wechat-post/assets/themes/tech-blue.json +35 -0
- package/topmind-wechat-post/package-lock.json +16 -0
- package/topmind-wechat-post/package.json +16 -0
- package/topmind-wechat-post/references/design.md +50 -0
- package/topmind-wechat-post/references/element-spec.md +163 -0
- package/topmind-wechat-post/references/known-pits.md +85 -0
- package/topmind-wechat-post/references/theme-map.md +139 -0
- package/topmind-wechat-post/references/typography-rules.md +62 -0
- package/topmind-wechat-post/references/wechat-constraints.md +111 -0
- package/topmind-wechat-post/references/workflow.md +102 -0
- package/topmind-wechat-post/references/writing-quality.md +105 -0
- package/topmind-wechat-post/scripts/audit_css.py +39 -0
- package/topmind-wechat-post/scripts/audit_docs.py +80 -0
- package/topmind-wechat-post/scripts/audit_skill.py +60 -0
- package/topmind-wechat-post/scripts/audit_styles.py +54 -0
- package/topmind-wechat-post/scripts/lint-wechat.py +446 -0
- package/topmind-wechat-post/scripts/md2wechat.py +1792 -0
- package/topmind-wechat-post/scripts/negative_tests.py +171 -0
- package/topmind-wechat-post/scripts/new-article.py +183 -0
- package/topmind-wechat-post/scripts/package_skill.py +188 -0
- package/topmind-wechat-post/scripts/push-to-topstream.py +226 -0
- package/topmind-wechat-post/scripts/scan_ai_flavor.py +464 -0
- package/topmind-wechat-post/scripts/sync-mapping.py +220 -0
- package/topmind-wechat-post/scripts/sync-status.py +187 -0
- package/topmind-x-article/README.en.md +109 -0
- package/topmind-x-article/README.md +104 -0
- package/topmind-x-article/SKILL.md +156 -0
- package/topmind-x-article/package-lock.json +16 -0
- package/topmind-x-article/package.json +16 -0
- package/topmind-x-article/references/publish-checklist.md +31 -0
- package/topmind-x-article/references/x-format.md +27 -0
- package/topmind-x-article/references/x-html-format.md +85 -0
- package/topmind-x-article/scripts/audit_css.py +39 -0
- package/topmind-x-article/scripts/audit_docs.py +80 -0
- package/topmind-x-article/scripts/audit_skill.py +60 -0
- package/topmind-x-article/scripts/audit_styles.py +54 -0
- package/topmind-x-article/scripts/md2x-html.py +522 -0
- package/topmind-x-article/scripts/md2x.py +323 -0
- package/topmind-x-article/scripts/negative_tests.py +91 -0
- package/topmind-x-article/scripts/package_skill.py +188 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# 公众号改稿规则(Stage 1 执行标准)
|
|
2
|
+
|
|
3
|
+
从技术博客 Markdown 到公众号稿,改的是**阅读节奏**,不是技术事实。事实、数据、链接一律不动。
|
|
4
|
+
|
|
5
|
+
## 必须做
|
|
6
|
+
|
|
7
|
+
1. **开头 150 字内给钩子**:结论、反差或悬念,不要背景铺垫。原文首段若是长背景,压缩或后移。
|
|
8
|
+
2. **段落切分**:单段 ≤ 110 字(约 4 行手机屏)。长段按语义断点拆开,不要机械等分。
|
|
9
|
+
3. **小标题不手写序号**:写稿时保持二级标题为纯文本,序号由排版层按 `h2_style` 自动生成(规则见 `element-spec.md`「章节编号」)。主题换编号样式时无需手改,读者仍能感知进度。
|
|
10
|
+
4. **列表项 ≤ 70 字**:超长列表项改成「加粗小标题 + 短说明」的段落组。
|
|
11
|
+
5. **重点加粗**:`加粗`是全篇最强的视觉锚点(锚点层),只留给结论句和关键数据,**全文 ≤5 处**(`lint-wechat.py` 会判);其余关键词标记改用 `++下划线++`(标记层,每段 1–3 个)。层级纪律见 `element-spec.md`「视觉层级」。
|
|
12
|
+
6. **长句拆短**:单句 > 45 字且有逗号堆叠时,拆成两句。
|
|
13
|
+
7. **图注统一**:`*▲ 图 N:说明*`,图注要说出「看什么」,不要只重复图片名。
|
|
14
|
+
8. **外链处理**:保留可点性无意义,改为「来源:XXX(URL)」的灰色小字或直接写机构名。
|
|
15
|
+
9. **结尾三段式**:一句总结结论 → 一句抛出问题/邀请讨论 → 引导关注语。
|
|
16
|
+
|
|
17
|
+
## 不要做
|
|
18
|
+
|
|
19
|
+
- 不改技术事实、不改数据、不改评测结论
|
|
20
|
+
- 不删代码块(公众号读者需要可复制的配置/命令)
|
|
21
|
+
- 不把「我」改成「笔者」「作者」等腔调词,第一人称是这类文章的信任来源
|
|
22
|
+
- 不加「随着...的发展」「在当今时代」这类 AI 腔 / 八股开场
|
|
23
|
+
- 不加 emoji 堆砌的小标题
|
|
24
|
+
- 不为了凑字数注水,公众号长文 3000-6000 字是合理区间
|
|
25
|
+
- 中英文/数字之间忘了空格没关系(lint --fix 会补),但**别手动在代码和链接里加**
|
|
26
|
+
|
|
27
|
+
## 容器语法使用时机(v2)
|
|
28
|
+
|
|
29
|
+
| 时机 | 用法 |
|
|
30
|
+
|---|---|
|
|
31
|
+
| 一句话核心观点、想让它从版面上跳出来 | `::: pull` 金句容器(大字居中,前后留白) |
|
|
32
|
+
| 关键数据对比(如「62.7% vs 99.9%」) | `::: stat` 数据卡(大数字 + 注释) |
|
|
33
|
+
| 补充但不打断主线的信息 | `::: note` 灰色提示框 |
|
|
34
|
+
| 操作建议、经验窍门 | `::: tip` 绿色提示框 |
|
|
35
|
+
| 容易踩的坑、官方风险提示 | `::: warn` 橙色 / `::: danger` 红色 |
|
|
36
|
+
| 观点交锋、问答式段落(A: B:) | `::: dialogue` 对话容器 |
|
|
37
|
+
| 版本号、状态标签、关键词打标 | `[!新特性]` 行内徽章 |
|
|
38
|
+
| 全文最重要的 1-3 句 | `==高亮==`(节制使用) |
|
|
39
|
+
|
|
40
|
+
原则:**每 800 字最多 1 个容器**,容器是调味料不是主菜;连续两种容器相邻会显得杂乱。
|
|
41
|
+
|
|
42
|
+
## 去 AI 腔检查点
|
|
43
|
+
|
|
44
|
+
- 排比三连句("不是 A,不是 B,而是 C")出现超过 2 次 → 改写
|
|
45
|
+
- 「值得注意的是」「综上所述」「不难看出」→ 删
|
|
46
|
+
- 每段都以「首先/其次/最后」开头 → 打散
|
|
47
|
+
- 形容词堆叠("极其轻巧稳定的架构")→ 留一个
|
|
48
|
+
|
|
49
|
+
## 排版参数基线(调研印证的通用规范)
|
|
50
|
+
|
|
51
|
+
- 正文 16px、行距 1.75、字间距 0、左对齐(不用 justify,会被格式检测判为非标准值)
|
|
52
|
+
- 段间距 22px 代替首行缩进(手机端惯例)
|
|
53
|
+
- 正文色 #333(纯黑 #000 在白底上过刺眼),辅助文字 #888
|
|
54
|
+
- 代码块深底浅字(浅灰底代码块在部分安卓机型上对比度不足)
|
|
55
|
+
|
|
56
|
+
## 交付前自检
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
python3 scripts/lint-wechat.py --input 公众号稿.md --fix
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
错误项(长段 > 200 字 / 图片缺失/过大)必须清零;警告项逐条判断是否接受。
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# 微信公众号编辑器硬约束
|
|
2
|
+
|
|
3
|
+
所有排版决策的依据。违反任一条,粘贴后的效果就会与预览不一致。
|
|
4
|
+
|
|
5
|
+
> **先厘清一个常见误解**:`::: stat`、`::: pull`、`==高亮==`、`[!徽章]` 这些**不是公众号语法**。
|
|
6
|
+
> 公众号后台是富文本编辑器,**根本不认 Markdown**。它们是我们自研的「中间标记」,
|
|
7
|
+
> 由 `md2wechat.py` 转成内联样式 HTML 后才粘进后台。所以判断「公众号支不支持」,
|
|
8
|
+
> 看的是**转换产物**(`section/p/span/div` + 内联样式),而不是 `:::` 本身。
|
|
9
|
+
> 本文件约束的就是「转换产物」必须遵守的规则。
|
|
10
|
+
|
|
11
|
+
## 1. 只保留内联样式
|
|
12
|
+
|
|
13
|
+
- 剥离 `<style>` 标签、`<link>`、`class` 属性、`id` 属性
|
|
14
|
+
- 剥离 `<script>`,所有交互失效
|
|
15
|
+
- **不用 `<div>`**:新版编辑器底层是 ProseMirror,白名单里没有 div。
|
|
16
|
+
div 包裹的块会连同它的背景、内边距、圆角**被整段吞掉**,只剩文字,
|
|
17
|
+
表面看就是「整篇没 CSS」。这是「粘贴后样式全丢」的头号元凶,比渐变被吃更隐蔽。
|
|
18
|
+
**一律用 `<section>`**(标签级红线清单见 `element-spec.md`)。
|
|
19
|
+
- **结论**:HTML 里每一个样式都必须写成 `style="..."`,不许出现 class 依赖
|
|
20
|
+
|
|
21
|
+
## 2. 外链
|
|
22
|
+
|
|
23
|
+
- 正文 `<a href>` 指向非白名单域名时,链接不可点(未认证订阅号尤其如此)
|
|
24
|
+
- 「阅读原文」是唯一官方外链入口
|
|
25
|
+
- **结论**:外链降级为「灰色小字 URL 文本」,读者手动复制;重要链接放「阅读原文」
|
|
26
|
+
|
|
27
|
+
## 3. 图片
|
|
28
|
+
|
|
29
|
+
- 本地相对路径 / `file://` 一律失效
|
|
30
|
+
- 远程图片在粘贴进入编辑器时,微信**多数情况下会自动抓取**转存素材库,但不保证(防盗链、content-type 不对、超时都会失败)
|
|
31
|
+
- **宽度用 `max-width:100%;height:auto`,绝不用 `width:100%`**:
|
|
32
|
+
正文容器约 677px,`width:100%` 会把 400px 的小截图强行拉满变糊,竖图更糟。
|
|
33
|
+
`max-width` 让大图缩到容器宽、小图保持原尺寸并居中。
|
|
34
|
+
- `WebP` 在正文图里不被支持(需转 PNG/GIF);`SVG` 完全不支持(需光栅化)
|
|
35
|
+
- **结论**:默认走「导出图片包 + 手动上传」;用 CDN 外链时必须提供上传清单兜底
|
|
36
|
+
|
|
37
|
+
## 4. 代码与特殊语法
|
|
38
|
+
|
|
39
|
+
- 无代码块语法,**不要用 `<pre>`**:pre 不自动换行,窄屏横向溢出,且会被格式检测标为风险项
|
|
40
|
+
- 用 `<section style="white-space:pre-wrap;word-break:break-all;">` + 等宽字体 + 深色底模拟
|
|
41
|
+
- **已发布文章里不可能有一键复制按钮**(2026-09-29 核实):公众号正文剥离 JS,
|
|
42
|
+
第三方工具的"复制代码"按钮只活在它们自己的预览页,进不了正文;编辑器原生代码块
|
|
43
|
+
也没有读者侧复制入口。``` 围栏渲染为高亮代码区(语言标签 + pre-wrap 自动折行),
|
|
44
|
+
读者靠长按/全选手动复制。需要分发"复制即用"的提示词时,走评论区置顶、附件、
|
|
45
|
+
或小程序文档等站外链路,**不要在正文里承诺一键复制**
|
|
46
|
+
- 不支持 mermaid、LaTeX、SVG 内联(部分 SVG 会被剥离)
|
|
47
|
+
- `<iframe>`、`<video>` 会被剥离,视频只能用公众号自带的腾讯视频/视频号组件
|
|
48
|
+
- **结论**:mermaid / 公式必须预渲染成 PNG 再插图
|
|
49
|
+
|
|
50
|
+
## 5. 字体与颜色
|
|
51
|
+
|
|
52
|
+
- 不允许自定义 font-family 之外的特殊字体,系统字体最稳
|
|
53
|
+
- 正文字号 15-16px 为佳,行高 1.7-1.8,段间距 20-24px
|
|
54
|
+
- `text-align` **只允许 `left` / `center` / `right`**。`justify` 会被格式检测判为「非标准值」,
|
|
55
|
+
且部分安卓机型出现难看字间距 → 正文统一 `left`,标题与图注用 `center`
|
|
56
|
+
|
|
57
|
+
## 6. 表格
|
|
58
|
+
|
|
59
|
+
- 支持,但**列数 > 4 时在手机端必然横向溢出**
|
|
60
|
+
- 表头**不要** `white-space:nowrap`,它会强制撑宽导致溢出;改用 `word-break:break-word`
|
|
61
|
+
- **不要用 `table-layout:fixed`**(本条修正了此前的错误建议):
|
|
62
|
+
`fixed` 会让各列宽度**均分**,5 列表格在 677px 正文宽里每列只有 135px,
|
|
63
|
+
中文会被压成竖条。除非逐列显式设置百分比宽度,否则它只会让小屏更糟。
|
|
64
|
+
正确做法是让列宽按内容自适应(`table-layout:auto`,即不写)。
|
|
65
|
+
- **不用 `<thead>` / `<tbody>` / `<colgroup>`**:公众号不支持表格分组标签,
|
|
66
|
+
表头行直接作为首个 `<tr>` 输出
|
|
67
|
+
- **结论**(与 `md2wechat.py` 的实现一致):
|
|
68
|
+
- 列数 ≤ 3 → 保留 `<table>`,`width:100%` + 内容自适应
|
|
69
|
+
- 列数 ≥ 4 → **转卡片化**,每行一张卡(首列作标题,其余「列名 + 值」成对),
|
|
70
|
+
彻底摆脱横向溢出。这是唯一稳的解法。
|
|
71
|
+
|
|
72
|
+
## 7. 格式检测红线(实测触发告警的项)
|
|
73
|
+
|
|
74
|
+
微信侧/第三方格式检测会逐段校验内联样式,以下写法会被告警:
|
|
75
|
+
|
|
76
|
+
| 写法 | 告警 | 正确做法 |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `text-align:justify` | 文字对齐异常·非标准值 | `text-align:left` |
|
|
79
|
+
| `max-width:677px` 等 px 宽度 | 不同屏幕下宽度差异 | `max-width:100%` |
|
|
80
|
+
| `<div>` 包裹 | 块被整段吞掉(样式全丢) | `<section>` |
|
|
81
|
+
| `<pre>` + `white-space:pre` | pre 标签·移动端溢出 | `<section>` + `pre-wrap` + `break-all` |
|
|
82
|
+
| `<h1>` 标签 | 标题语义映射,字号被覆盖 | `<p>` + 内联样式(20px/700/居中) |
|
|
83
|
+
| `<figure>` / `<figcaption>` | 粘贴时剥离包裹,图注错位 | `<section>` + `<p>` + 独立图注 |
|
|
84
|
+
| `<thead>` / `<tbody>` / `<colgroup>` | 表格分组标签不被支持 | 表头行直接作为首个 `<tr>` |
|
|
85
|
+
| `table-layout:fixed` | 列宽均分,小屏中文压成竖条 | 不写(按内容自适应)或逐列显式设宽 |
|
|
86
|
+
| `display:flex` / `grid` | 布局属性被吞,内容堆叠 | 用 `<section>` + `padding` 模拟 |
|
|
87
|
+
| `display:inline-block` | 非常规 display 值,支持不稳定 | 尽量用行内 `<span>` + padding |
|
|
88
|
+
| `float` / `position` | 定位失效,元素错位 | 不用定位,用块级流式布局 |
|
|
89
|
+
| `linear-gradient` | 渐变被静默丢弃,退化成无背景 | 纯色 `background` |
|
|
90
|
+
| `box-shadow` | 阴影被丢弃 | 不用阴影;层次靠底色 + 左边框 |
|
|
91
|
+
| `rgba()` / `hsla()` / `opacity` | 透明度不支持 | 纯色十六进制 |
|
|
92
|
+
| `-webkit-*` 私有属性 | 非标准属性 | 不用 |
|
|
93
|
+
| 表头 `white-space:nowrap` | 内容溢出 | `word-break:break-word` |
|
|
94
|
+
|
|
95
|
+
**圆角(`border-radius`)是「优雅降级」例外**:可能被丢弃,丢了只是变直角,
|
|
96
|
+
不破坏结构与可读性。可以用,但**不要依赖圆角做视觉区分**——卡片之间的区别必须
|
|
97
|
+
同时有底色或左边框,否则圆角一丢就分不清了。
|
|
98
|
+
|
|
99
|
+
**三样最可靠的手段**:纯色 `background` + `border-left` + 字号/颜色。
|
|
100
|
+
它们能覆盖 90% 的排版需求,且不会静默失效。
|
|
101
|
+
|
|
102
|
+
**本地预览页的宽度约束**要写进 `<head><style>`(粘进公众号时会被剥离),
|
|
103
|
+
正文内联样式始终保持百分比宽度——否则预览好看、粘贴后告警。
|
|
104
|
+
|
|
105
|
+
## 8. 粘贴方式
|
|
106
|
+
|
|
107
|
+
- 从浏览器复制渲染后的富文本,粘贴进编辑器可保留内联样式
|
|
108
|
+
- 直接粘贴 HTML 源码会被当成纯文本
|
|
109
|
+
- 不要用脚本直接注入剪贴板(`execCommand('copy')` 在预览页里可用,
|
|
110
|
+
但外部程序注入的 CF_HTML 微信识别率低,会退回纯文本)
|
|
111
|
+
- 推荐:本地 HTML 预览页 → 点「复制正文」或全选正文 → 复制 → 粘贴
|
|
@@ -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,105 @@
|
|
|
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` | 用表达方式加,不编故事 |
|
|
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
|
+
**不要误伤专业用法**:「口径」(官方口径/社区口径)、「对齐」(alignment)、「收敛」(数学语义)都是正常的。
|
|
84
|
+
|
|
85
|
+
**降调段末金句**:删完套话却满篇加粗金句,等于从「AI 体」变成「新闻通稿」——同样没人味。
|
|
86
|
+
|
|
87
|
+
**保留的东西**:有信息量的「不是 A 而是 B」反转保留;三段排比内容扎实可留,硬凑的砍到两项或四项。
|
|
88
|
+
|
|
89
|
+
**扫描器的两个误报(别照着改坏)**:「排比三段式」误报数据陈述(`从 15 个精简到 2 个,准确率从 80% 升到…`、`从发布、被审计、更正到上线` 是时间/数据变化,**保留**);「复盘」误报专有名词(「37signals 退出云复盘」是专名不改,只有「我们来复盘一下」这种动词套话才改)。**先看语义是不是真排比/真套话,再决定删不删**。
|
|
90
|
+
|
|
91
|
+
### 加人味(只加表达,不加事实)
|
|
92
|
+
|
|
93
|
+
有「我」、有具体场景、有具体数字与人名、有不确定性表达(「其实」「不过」「还在想」)、句子长短交替、有真实细节而不是「很多人」「大多数」。手段是句长节奏、设问、括号自注,配合删掉元叙述和教学腔。
|
|
94
|
+
|
|
95
|
+
原文没有第一人称、没有数字时,不许靠这一条去补——那正好踩中 `humanizer-zh` 的优先级第 1 条。英文文本才走 `humanizer`:它的取向是 **Add soul**(主动注入个性),与中文改稿的**保真优先**方向相反,别套过来。
|
|
96
|
+
|
|
97
|
+
### 工具纪律
|
|
98
|
+
|
|
99
|
+
**并行编辑同一文件会互相覆盖**——同一文件的多处修改必须走**单个串行脚本**,改完立刻复核关键字段是否落盘。
|
|
100
|
+
|
|
101
|
+
## 四、改稿红线
|
|
102
|
+
|
|
103
|
+
不改技术事实、数据、评测结论、代码块内容。改稿改的是阅读节奏与表达,不是重写。
|
|
104
|
+
|
|
105
|
+
外部工具改过的稿子(用户先拿别的模型改过一版再交回来):**先核数字,再动文字,顺序不能反**。实测一轮揪出 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,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()
|