@topmindspace/topmind-skills 4.14.0 → 4.15.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/evals/evals.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "skill_name": "topmind",
3
3
  "daily_entry": "topmind",
4
- "version": "4.14.0",
4
+ "version": "4.15.0",
5
5
  "evals": [
6
6
  {
7
7
  "id": 1,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topmindspace/topmind-skills",
3
- "version": "4.14.0",
3
+ "version": "4.15.0",
4
4
  "description": "topmind Agent Skills pack — portable markdown skills for capture, organize, write, memory, maintain, loop.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -65,8 +65,10 @@ Host 能力映射:
65
65
  | 能力 | Desktop | UTR | 纯 Host Agent |
66
66
  |------|---------|-----|---------------|
67
67
  | 提取 | `add_todo` · `todo_maintain` | `memory.add-todo` | 读活动窗口 → 识别行动语 → 写 checklist |
68
- | 维护 | `toggle_todo` · `maintainTodos` | `memory.toggle-todo` | 对照周期本与 todo.md 做 diff 建议 |
68
+ | 维护 | `toggle_todo` · `update_todo` · `set_todo_due` · `delete_todo` · `maintainTodos` | `memory.toggle-todo` | 对照周期本与 todo.md 做 diff 建议 |
69
69
  | 列表 | `list_todos` · Todo 面板 | `memory.list-todos` | 读 `memory/todo.md` 活跃段 |
70
+ | 动态查询 | `list_recent_stream` | `workspace-read.list-recent-captures` | 读最近周期本日段 |
71
+ | 记忆查询 | `list_recent_memories` | `memory.list-profile` 等 | 读 `memory/` 活跃段(勿整库倾倒) |
70
72
  | 分类/专题建议 | `topic_classify`(确认后 `create_topic`) | 文件工具 + role 路由 | 按 role + 内容性质给最多 3 候选 |
71
73
  | 记忆建议 | `memory_organize`(append/update/retire 建议) | `memory.append-profile` 等(确认后) | 识别稳定事实 → L1 候选 |
72
74
 
@@ -74,6 +74,9 @@ word_count: 1200
74
74
  - URL 类型提示(GitHub README / GitHub 原文 / X / 网页)
75
75
  - Inbox:按 `source_type` 筛选「网页/摘录」
76
76
  - AI `fetch_url` 支持 `maxLen` + `render`;Obsidian `fetch_url` 对 GitHub md 自动走 raw
77
+ - AI `capture_url`:一键抓取并入库(带 source URL + 标题);Desktop 与 Obsidian 对齐(`forceInbox` / `forceAtom`)
78
+ - AI `web_search`:无需 Key 的网络搜索短列表(域名打分 + 同域去重);命中后 `fetch_url` / `capture_url`
79
+ - 工具失败会返回**一条**具体恢复步骤(如 edit 失败 → 先 read_file 刷 contentHash)
77
80
 
78
81
  ## 不要做的事
79
82
 
@@ -9,6 +9,8 @@
9
9
  |--------|--------------|------|
10
10
  | 总结要点 | organize vs memory | 默认 → `topmind-organize`。仅「更新我的情况 / 加到专题记忆 / 沉淀到记忆」→ memory |
11
11
  | 沉淀 | capture vs memory | 默认 → `topmind-capture`。仅「沉淀成稳定结论 / 更新我的情况」→ memory |
12
+ | **记一下 / Note it** | capture 主入口 | **完整捕获**(笔记 / 链接 / 附件)→ `topmind-capture` |
13
+ | **记下 / Log it** | capture(追加) | **写入当前周期本**(动态追加,同文件续写)→ `topmind-capture`,默认平铺到当前周期;不弹分类访谈 |
12
14
  | 记一下要做 X / 别忘了 X | capture vs todo | → `topmind-capture`(材料落盘)+ 待办写入/建议 `memory/todo.md`(见 `auto-suggest.md`) |
13
15
  | 待办 / 有什么要做的 | router 直达 | 读 `memory/todo.md` 活跃项;不新建概念、不进 organize |
14
16
  | X 做完了 / 这条不用了 | todo vs organize | 清单项 → 勾掉/归档 `memory/todo.md`;整周事项状态 → organize「整理本周」 |
package/topmind/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: topmind
3
- version: 4.14.0
3
+ version: 4.15.0
4
4
  description: >-
5
5
  topmind 总入口与多意图路由(类别/专题/笔记/待办/交付)。Use when 用户说 topmind、意图模糊、
6
6
  待办/有什么要做的/todo list、或需要收→整→写 分步。单意图明确时直接用 topmind-capture|organize|write|memory|maintain|loop|weread|x|ledger|wechat。
@@ -139,7 +139,7 @@ Connector:[`references/connector-resolution.md`](references/connector-resoluti
139
139
  主路径:host 文件工具 + project-model-brief。Skills pack **不依赖** Pi / `pi-agent-core`;不要编造 bash / shell。宿主若有唯一片段替换,中段改稿优先用它。
140
140
  降级:[`../shared/capability-degradation.md`](../shared/capability-degradation.md)。
141
141
 
142
- UTR 可选(MCP primary+danger 共 26;注册表 35 = 8 域 / 35 命令,见 TOOLS.md):`list-categories` · `list-topics` · `inspect-topic` · `list-topic-files` · `list-inbox` · `create-topic` · `capture-note` · `save-output` · `contract.validate` · `contract.reseed` · `memory.promote` · `memory.digest` · `memory.append-profile` · `memory.append-topic` · `memory.retire-profile` · `memory.update-profile` · `memory.compact-history` · `memory.restore-profile` · `memory.list-todos` · `memory.add-todo` · `memory.toggle-todo` · `doctor-workspace` · `plan-inbox-routing` · `archive-topic` · `archive-stream-year` · `restore-safety-receipt`。
142
+ UTR 可选(MCP primary+danger 共 29;注册表 38 = 8 域 / 38 命令,见 TOOLS.md):`list-categories` · `list-topics` · `inspect-topic` · `list-topic-files` · `list-inbox` · `create-topic` · `capture-note` · `save-output` · `contract.validate` · `contract.reseed` · `memory.promote` · `memory.digest` · `memory.append-profile` · `memory.append-topic` · `memory.retire-profile` · `memory.update-profile` · `memory.compact-history` · `memory.restore-profile` · `memory.list-todos` · `memory.add-todo` · `memory.toggle-todo` · `memory.update-todo` · `memory.set-todo-due` · `memory.delete-todo` · `doctor-workspace` · `plan-inbox-routing` · `archive-topic` · `archive-stream-year` · `restore-safety-receipt`。
143
143
 
144
144
  字段始终独立 **`category` + `topic`**(真实目录名,非写死编号)。
145
145
 
@@ -1,12 +1,12 @@
1
1
  # Multi-Intent Resolution
2
2
 
3
- 当用户请求包含多个意图时,按以下矩阵裁决(与 `shared/trigger-disambiguation.md` 一致):
3
+ 当用户请求包含多个意图时,按以下矩阵裁决(与 `../../shared/trigger-disambiguation.md` 一致):
4
4
 
5
5
  | 意图组合 | 裁决 | 说明 |
6
6
  |---------|------|------|
7
7
  | capture + organize | 先 capture,回执中建议 organize | 先确保材料落地 |
8
8
  | capture + memory | 先 capture,回执中建议 memory | 先存笔记再更新我的情况 / 周期反思 |
9
- | capture + todo | 先 capture;显式行动语同轮写 `memory/todo.md`,隐含则 L1 建议 | 见 `shared/auto-suggest.md` |
9
+ | capture + todo | 先 capture;显式行动语同轮写 `memory/todo.md`,隐含则 L1 建议 | 见 `../../shared/auto-suggest.md` |
10
10
  | organize + write | 先 organize 提炼,再 write 输出 | 分两步 |
11
11
  | organize + memory | 先 organize 候选,再 memory 落盘 | organize 产出列表 |
12
12
  | organize + todo | 整理本周后 L1 待办 diff 建议 | 不自动批量改清单 |
@@ -1,9 +1,10 @@
1
1
  ---
2
2
  name: topmind-capture
3
- version: 4.14.0
3
+ version: 4.15.0
4
4
  description: >-
5
- 把链接、摘录、随手记收进动态周期本、Inbox 或专题;行动语自动提取/建议待办。Use when 记一下、收进、
6
- 剪藏、保存链接、记一下要做/别忘了、capture、save URL/idea。
5
+ 把链接、摘录、随手记收进动态周期本、Inbox 或专题;行动语自动提取/建议待办。Use when 记一下、
6
+ 收进、剪藏、保存链接、记一下要做/别忘了、capture、save URL/idea、Note it。
7
+ 记下/Log it 是动态主区「写入当前周期本」——路由仍进本 skill,但不作 frontmatter 触发词(见 shared/trigger-disambiguation.md)。
7
8
  Do NOT use for 整理本周、出稿、写我的情况、doctor/loop、微信读书、X.
8
9
  action_category: capture
9
10
  triggers:
@@ -30,6 +31,7 @@ triggers:
30
31
  - capture
31
32
  - note
32
33
  - save
34
+ - "Note it"
33
35
  tags: [capture, inbox, note, material]
34
36
  entrypoint: false
35
37
  compatibility: topmind workspace. Host file tools or optional UTR capture-note.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: topmind-ledger
3
- version: 4.14.0
3
+ version: 4.15.0
4
4
  description: >-
5
5
  通用记账到记忆平面(默认个人/自己账本,用户自建账本与分类)。Use when 记账、记一笔、花了、存入、查看账单、账户余额。
6
6
  Do NOT use for 记一下到动态、待办、微信读书、发推、Feishu/lark-cli.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: topmind-loop
3
- version: 4.14.0
3
+ version: 4.15.0
4
4
  description: >-
5
5
  可中断的工作区语义巡检(走专题/Inbox,.loop 断点续跑)。Use when 跑一遍 loop、巡检、整体体检、复盘、
6
6
  继续 loop、audit。Do NOT use for 快速 doctor(→maintain)、单专题整理、捕获、写作、仅记忆.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: topmind-maintain
3
- version: 4.14.0
3
+ version: 4.15.0
4
4
  description: >-
5
5
  确定性体检/清理/结构修复/回执恢复。Use when 快速体检、诊断、doctor、清理、修复、workspace check。
6
6
  Do NOT use for 整体巡检 loop、内容整理、捕获、写作、记忆.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: topmind-memory
3
- version: 4.14.0
3
+ version: 4.15.0
4
4
  description: >-
5
5
  更新「我的情况」或周期反思。Use when 记住这个、更新我的情况、加到专题记忆、沉淀结论。
6
6
  Do NOT use for 捕获、仅总结(→organize)、整理本周正文、出稿、开/并专题、doctor/loop.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: topmind-organize
3
- version: 4.14.0
3
+ version: 4.15.0
4
4
  description: >-
5
5
  整理本周动态、专题内整理/研究/分析/路由 Inbox。Use when 整理本周、整理、分析、研究、对比、总结要点、organize、summarize。
6
6
  Do NOT use for 首次捕获、最终出稿、仅写我的情况、快速 doctor、全库 loop.
package/topmind-pack.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "label": "topmind",
5
5
  "description": "topmind skill pack: single daily entry topmind + progressive disclosure (shared/ + references/), Agent Skills open-standard aligned, capture-first template-driven categories, optional connectors topmind-weread and topmind-x, optional topmind-ledger (记账), optional write sub-skill topmind-wechat. None of the optional skills is an extra user concept. File-system-first content truth; UTR optional.",
6
6
  "daily_entry": "topmind",
7
- "version": "4.14.0",
7
+ "version": "4.15.0",
8
8
  "updated": "2026-09-28",
9
9
  "license": "MIT",
10
10
  "locales": {
@@ -382,7 +382,10 @@
382
382
  "restore-profile",
383
383
  "list-todos",
384
384
  "add-todo",
385
- "toggle-todo"
385
+ "toggle-todo",
386
+ "update-todo",
387
+ "set-todo-due",
388
+ "delete-todo"
386
389
  ],
387
390
  "lifecycle": [
388
391
  "scan"
@@ -391,8 +394,8 @@
391
394
  "rebuild"
392
395
  ]
393
396
  },
394
- "command_count": 35,
395
- "mcp_default_count": 26,
397
+ "command_count": 38,
398
+ "mcp_default_count": 29,
396
399
  "command_exposure": {
397
400
  "primary": [
398
401
  "workspace-read.list-categories",
@@ -416,7 +419,10 @@
416
419
  "memory.restore-profile",
417
420
  "memory.list-todos",
418
421
  "memory.add-todo",
419
- "memory.toggle-todo"
422
+ "memory.toggle-todo",
423
+ "memory.update-todo",
424
+ "memory.set-todo-due",
425
+ "memory.delete-todo"
420
426
  ],
421
427
  "danger": [
422
428
  "workspace-maintain.archive-topic",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: topmind-wechat
3
- version: 4.14.0
3
+ version: 4.15.0
4
4
  description: >-
5
5
  公众号文章全生命周期子技能(write 族):交付包、审校改写、质量三关、状态同步、微信内联排版与发布清单。
6
6
  支持 forward(底稿→公众号)、reverse(选题原创→回推)、站外拉取(在线精选站→reverse+pending)三条路径;
@@ -49,14 +49,15 @@ compatibility: topmind workspace. Writes via UTR workspace-write / Desktop Works
49
49
  | `scripts/push-to-topstream.py` | 可选回推:公众号稿降级为纯 Markdown notes |
50
50
  | `scripts/lint-wechat.py` | 排版体检 + 自动修复(中英文间距、段长、AI 腔…) |
51
51
  | `scripts/md2wechat.py` | Markdown → 全内联 HTML;**必加 `--embed-images`** |
52
- | `scripts/scan_ai_flavor.py` | 中文去 AI 味扫描(与 `qu-aiwei-zh` 同源) |
52
+ | `scripts/scan_ai_flavor.py` | 中文去 AI 味扫描 —— **转发器**,实现在 canonical `qu-aiwei-zh`(`--which` 可查解析结果;本目录不含实现,避免副本漂移) |
53
+ | `scripts/audit-provenance.py` | **改稿保真审计**:双向逐段溯源(本稿→源稿 / 源稿→本稿),抓悄悄扩写与被落下的段。有源稿时定稿必跑 |
53
54
 
54
55
  ```bash
55
56
  # 路径解析:CLI --base/--workspace → env TOPMIND_WECHAT_BASE / TOPMIND_WORKSPACE / TOPSTREAM_ROOT → 惯例
56
57
  export TOPMIND_WORKSPACE=/path/to/workspace # 推荐
57
58
  python3 scripts/new-article.py --slug demo --title "标题" --direction reverse
58
59
  python3 scripts/lint-wechat.py --input <包>/公众号稿.md --fix
59
- python3 scripts/scan_ai_flavor.py <包>/公众号稿.md # 目标 ≥85
60
+ python3 scripts/scan_ai_flavor.py <包>/公众号稿.md # 总分与作者姿态分都 ≥85
60
61
  python3 scripts/md2wechat.py --input <包>/公众号稿.md --out-dir <包> --slug demo \
61
62
  --asset-root <素材根> --embed-images
62
63
  python3 scripts/sync-status.py --set 定稿 <包> --apply
@@ -147,6 +148,13 @@ python3 scripts/sync-status.py --set 定稿 <包> --apply
147
148
  - 改稿续写:正文已有数字**回源重核**(上一轮文本最不可信)
148
149
  - 多口径(主轮/复跑)显式拆开;表格从数据源生成,禁止手抄
149
150
  - 外部工具改过的稿:**先核数字再动文字**;「比值对但绝对值错」= 全段重核
151
+ - **有源稿时(`reverse` / 站外拉取)跑一次双向溯源审计**,别靠「我记得只改了三处」:
152
+
153
+ ```bash
154
+ python3 scripts/audit-provenance.py --draft <包>/公众号稿.md --source <包>/源稿-*.md
155
+ ```
156
+
157
+ 正向查「本稿有、源稿找不到」(扩写藏在这里),反向查「源稿有、本稿落下」。**退出码 1 不是失败,是「有待判定项」**——每一项要么归入已声明的修正/增补,要么就是不该有的改动;逐条在包内 README 声明后即收口。源稿里后加的块(勘误等)用 `--skip '^>\s*\[!'` 排除。
150
158
 
151
159
  ### 关 2 · 逻辑
152
160
 
@@ -156,13 +164,16 @@ python3 scripts/sync-status.py --set 定稿 <包> --apply
156
164
  ### 关 3 · 文字(去 AI 味)
157
165
 
158
166
  ```bash
159
- python3 scripts/scan_ai_flavor.py <包>/公众号稿.md # ≥85(人话)
167
+ python3 scripts/scan_ai_flavor.py <包>/公众号稿.md # 总分 ≥85(人话)
160
168
  ```
161
169
 
170
+ - **同时看「作者姿态分」(E 类,与总分独立)**,目标同样 ≥85。实测两者会严重背离:同一篇 AI 完整版总分 92 / 姿态 21,手改发布版 100 / 100——**总分只差 8 分,姿态差 79 分**,只看总分会被骗
171
+ - ⚠️ 这里调的是 canonical `qu-aiwei-zh` 的脚本(本目录的 `scripts/scan_ai_flavor.py` 是转发器)。**若输出里没有「作者姿态层」一节,说明拿到的是 E 类之前的旧版本**,分数不可信——先 `--which` 确认实际执行的是哪一份
162
172
  - 删套话/黑话/工程圈行话;降调段末加粗金句
163
173
  - **满分 ≠ 有人味**:再查「段末金句癖 / 节奏过分整齐 / 没有场景与我 / 报告体标注」
164
174
  - **口语化 ≠ 有人味**:删社交垫词(元叙述、空转过渡、姿态句)
165
175
  - 判据:**这句话删掉之后,读者少知道了什么?**
176
+ - **姿态分只对文章有效**:清单 / 词表 / 说明书不适用(检测器会把被引用的反例当真命中),脚本命中 ≥70% 落在表格行时会提示「疑似清单文档」,看到提示先确认再改
166
177
 
167
178
  详见 [`references/writing-quality.md`](references/writing-quality.md)。
168
179
 
@@ -60,21 +60,32 @@ python3 scripts/sync-status.py --set 定稿 <包> --apply
60
60
 
61
61
  | 输出 | 口径 | 用途 |
62
62
  |------|------|------|
63
- | frontmatter `word_count` | 纯中文 `\[一-鿿\]` | 映射真源 |
63
+ | frontmatter `word_count` | 正文**全文**的 `[\u4e00-\u9fff]` 计数 —— **frontmatter 之后的一切都算,含图注、也含图片路径里的中文** | 映射真源 |
64
64
  | lint「字数」 | 视觉长度(中 1 + 英数 ×0.6,URL 不计) | 阅读时长 / 段长 |
65
65
  | md2wechat「正文字数」 | 渲染后字符 | 仅展示 |
66
66
 
67
+ ⚠️ `word_count` **只能由 `sync-status.py --apply` 回写,禁止手算**。它的口径与直觉不同:一篇 9 图稿,光 `images/NN-中文名.png` 这一串路名就贡献 40+ 字。自制口径(如「剔掉图注与路径」)会得出一个更小、更像「正文字数」的值,写进 frontmatter 后会被 `sync-mapping.py` 判为漂移 —— 实测一篇 1,027 vs 1,450,而错值同时落进 `topic.md` 总表与包内 README,**一个错值三处落盘**。
68
+
67
69
  ## 定稿验收命令串
68
70
 
69
71
  ```bash
70
- python3 scripts/scan_ai_flavor.py <包>/公众号稿.md # ≥85
72
+ python3 scripts/scan_ai_flavor.py <包>/公众号稿.md # 总分 ≥85 且「作者姿态分」≥85
71
73
  python3 scripts/lint-wechat.py --input <包>/公众号稿.md
74
+ python3 scripts/audit-provenance.py --draft <包>/公众号稿.md --source <包>/源稿-*.md
72
75
  python3 scripts/sync-mapping.py --no-topstream
73
76
  python3 scripts/sync-status.py
74
77
  python3 scripts/md2wechat.py --input <包>/公众号稿.md --out-dir <包> \
75
78
  --slug <slug> --asset-root <素材根> --embed-images
76
79
  ```
77
80
 
81
+ 第 3 行只在**有源稿**时跑(`reverse` / 站外拉取;`forward` 的底稿在 `notes/` 也一样适用)。退出码 **0 = 双向干净**、**1 = 有待判定项**、2 = 参数错。**1 不等于稿子有问题**——它只是把「本稿有源稿找不到」和「源稿有本稿落下」两类段摆出来,由人逐条判:归入已声明的修正/增补,或改回去。判定完把结论写进包内 `README.md`,这一关才算收口。
82
+
83
+ 三个易踩的点:
84
+
85
+ - **阈值别乱调**:默认 `0.72`。调高会把正常的改写(换词、调序)判成新增,调低会漏掉真扩写。
86
+ - **源稿后加的块要 `--skip`**:若源稿顶部有本包后补的勘误/说明块(`> [!note] …`),它在本稿侧不存在,会恒定报一条反向待判定。用 `--skip '^>\s*\[!'` 排除,别去改稿子。
87
+ - **图注不算遗漏**:脚本反向侧额外建了一个「去掉 `▲ 图 N` 前缀」的候选池,图注编号不会被误判为漏段。
88
+
78
89
  ## 收尾
79
90
 
80
91
  1. 更新专题 `topic.md` 总表(派生视图)
@@ -44,7 +44,7 @@
44
44
  |---|---|---|
45
45
  | 中文长文 / 公众号改稿 | `humanizer-zh` + `qu-aiwei-zh` | 先立边界,再扫描定位(默认组合) |
46
46
  | 只要分数和命中清单 | `qu-aiwei-zh` | 跑脚本,不改稿 |
47
- | 随笔要「加声音」 | `qu-aiwei-zh` + `references/pan-style.md` | 用表达方式加,不编故事 |
47
+ | 随笔要「加声音」 | `qu-aiwei-zh` + `qu-aiwei-zh/references/pan-style.md` | 用表达方式加,不编故事 |
48
48
  | 英文文本 | `humanizer` | `humanizer-zh` 只针对中文 |
49
49
  | 清单 / 词表 / 说明书 | 都不用 E 类姿态分 | 这类文体靠列举反例,检测器会把反例当真命中 |
50
50
 
@@ -70,12 +70,20 @@
70
70
 
71
71
  ### 第二步:扫描定位(`qu-aiwei-zh`)
72
72
 
73
- 目标:`qu-aiwei-zh` 扫描 **≥ 85 分**。
73
+ 目标:`qu-aiwei-zh` 扫描 **总分 ≥ 85**,且**「作者姿态分」(E 类)也 ≥ 85**。两个分数独立,都要看。
74
74
 
75
75
  ```bash
76
+ python3 scripts/scan_ai_flavor.py <稿>.md
77
+ # 或直连 canonical(同一个脚本)
76
78
  python3 ~/.workbuddy/skills/qu-aiwei-zh/scripts/scan_ai_flavor.py <稿>.md
77
79
  ```
78
80
 
81
+ `scripts/scan_ai_flavor.py` 是**转发器**,实现在 canonical `qu-aiwei-zh`,两条命令等价(`--which` 可查解析到哪一份)。
82
+
83
+ ⚠️ **认准「作者姿态层」这一节**:姿态分是后加的维度,早期版本没有。若输出里没有这一节,说明调用到了旧版脚本,**分数不可信**。实测同一篇稿:AI 完整版总分 92 / 姿态 21,手改发布版 100 / 100 —— 总分只差 8 分,姿态差 79 分,**只看总分会被骗**。
84
+
85
+ ⚠️ **姿态分只对文章有效**:清单 / 词表 / 说明书不适用——这类文体靠列举反例工作,检测器会把被引用的例子当成真命中。脚本命中 ≥70% 落在表格行时会提示「疑似清单文档」,看到提示先确认文体再改。
86
+
79
87
  ### 删套话
80
88
 
81
89
  值得注意 / 值得记录 / 值得单独强调 / 本质上 / 事实上 / 意味着 / 由此可见 / 不难看出 / 换句话说 / 换言之 / 殊途同归 / 显而易见 / 众所周知 / 更进一步说
@@ -114,7 +122,7 @@ python3 ~/.workbuddy/skills/qu-aiwei-zh/scripts/scan_ai_flavor.py <稿>.md
114
122
 
115
123
  有「我」、有具体场景、有具体数字与人名、有不确定性表达(「其实」「不过」「还在想」)、句子长短交替、有真实细节而不是「很多人」「大多数」。
116
124
 
117
- ⚠️ 这一段说的是**表达方式**。原文没有第一人称、没有数字时,不许靠这一条去补——那正好踩中 `humanizer-zh` 的优先级第 1 条。加声音的手段是句长节奏、设问、括号自注(见 `pan-style.md`),配合删掉元叙述和教学腔。`humanizer-zh` 里写着同一句话:「用户要求增强个人风格时,用表达方式实现,不编故事。」
125
+ ⚠️ 这一段说的是**表达方式**。原文没有第一人称、没有数字时,不许靠这一条去补——那正好踩中 `humanizer-zh` 的优先级第 1 条。加声音的手段是句长节奏、设问、括号自注(见 `qu-aiwei-zh/references/pan-style.md`),配合删掉元叙述和教学腔。`humanizer-zh` 里写着同一句话:「用户要求增强个人风格时,用表达方式实现,不编故事。」
118
126
 
119
127
  英文文本才走 `humanizer`:它的取向是 **Add soul**(主动注入个性),与中文改稿的**保真优先**方向相反,别把它的思路套到中文稿上。
120
128
 
@@ -0,0 +1,366 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """audit-provenance.py — 双向逐段溯源审计(改稿保真度的机械化检查)。
4
+
5
+ ## 它解决什么问题
6
+
7
+ 把一篇成稿改成另一种形态(X 长文 → 公众号、底稿 → 交付稿)时,最容易出的不是错别字,
8
+ 而是**悄悄扩写**和**悄悄漏掉**。人眼逐段比对一篇几千字的稿子既慢又不可靠。
9
+
10
+ 本脚本做**两个方向**的机械化检查:
11
+
12
+ - **正向(本稿 → 源稿)**:`--draft` 的每一段,能否在 `--source` 里找到?找不到的即
13
+ 「疑似新增」—— 扩写就藏在这里
14
+ - **反向(源稿 → 本稿)**:`--source` 的每一段,有没有在本稿里落下?找不到的即「疑似遗漏」
15
+
16
+ ## 为什么不用 diff
17
+
18
+ 形态转换必然改变排版:断段、标题层级、`++` 标记层、图注加 `▲ 图 N` 编号。
19
+ 逐行 diff 会被这些噪声淹没,真正的问题(多出/少掉的事实句)反而看不见。
20
+
21
+ 所以这里**先归一化到「纯文字内容」再比对**,并显式放过几类合法差异:
22
+
23
+ | 分类 | 含义 |
24
+ |------|------|
25
+ | `identical` | 归一化后完全相同 |
26
+ | `rewritten` | 同段但有文字改动(图注编号、标点、已声明的事实修正都落这里) |
27
+ | `merged` | 本稿把源稿的**几段并成了一段** |
28
+ | `split` | 源稿的一段被本稿**拆成了几段** |
29
+ | `caption` | 仅图注加了 `▲ 图 N:` 前缀,正文一字未改 |
30
+ | `tail` | 结尾 CTA / 签名 / 「首发于公众号」类附加句 |
31
+ | `unexplained` | **其余全部**,需要人工判定 —— 这是唯一会被报警的一类 |
32
+
33
+ ⚠️ **`merged` / `split` 两类是必须的**:源稿常把「正文末句 + 图注」挤在同一段,本稿拆开是
34
+ 正常排版。没有这两类,几乎每张图都会报一次假阳性;而只做段落级匹配则会把所有拆段都误报。
35
+ 所以候选池同时建两级窗口:**段窗口**(1~3 段合并)与**句窗口**(1~3 句合并,长度下限 12 字)。
36
+
37
+ ## 已知陷阱(踩过)
38
+
39
+ 不要用 `^---.*?^---` 配 `re.S` 剥 frontmatter —— 正文里常有 `---` 分隔线,
40
+ 那个正则会连带把中间正文吃掉,导致源稿被误判成「只剩几段」。
41
+ 本脚本只剥**文件头**的 frontmatter:`^---\\n.*?\\n---\\n` + `count=1`。
42
+
43
+ 源稿里后加的块(勘误、HTML 注释)默认会进待判定,用 `--skip` 排除:
44
+
45
+ --skip '^>\\s*\\[!'
46
+
47
+ ## 用法
48
+
49
+ python3 scripts/audit-provenance.py --draft <包>/公众号稿.md --source <包>/源稿-X长文.md
50
+ python3 scripts/audit-provenance.py --draft a.md --source b.md --skip '^>\\s*\\[!' --json
51
+ python3 scripts/audit-provenance.py --draft a.md --source b.md --threshold 0.8 --verbose
52
+
53
+ 退出码:0 = 无待判定项;1 = 有 `unexplained`(需人工看);2 = 参数/文件错误。
54
+ """
55
+
56
+ from __future__ import annotations
57
+
58
+ import argparse
59
+ import json
60
+ import re
61
+ import sys
62
+ from difflib import SequenceMatcher
63
+
64
+ MAX_SPAN = 3 # 允许 1~MAX_SPAN 个相邻单位合并成一个窗口
65
+ MIN_SENT_WIN = 12 # 句级窗口归一化后的最小长度,避免短句偶然命中
66
+
67
+ # 元信息块(来源 / 抓取记录)——两侧都跳过
68
+ META_RE = re.compile(
69
+ r"^>\s*(来源|实标题|标题|摘要|作者|站点|提取|约字数|说明|发布|原始)\s*[::]"
70
+ )
71
+ IMG_ONLY_RE = re.compile(r"^\s*(!\[[^\]]*\]\([^)]*\)|!\[\[[^\]]*\]\])\s*$")
72
+ HTML_COMMENT_RE = re.compile(r"^\s*<!--.*?-->\s*$", re.S)
73
+ CAPTION_PREFIX_RE = re.compile(r"^▲\s*图\s*\d+\s*[::\-–—]?\s*")
74
+ SENT_RE = re.compile(r"[^。!?;!?;]*[。!?;!?;]|[^。!?;!?;]+")
75
+
76
+ TAIL_HINTS = ("欢迎转发", "点个关注", "评论区", "不迷路", "首发于", "转载请注明",
77
+ "如果这篇", "如果对你有用")
78
+
79
+ _TYPO = {
80
+ "\u201c": '"', "\u201d": '"', "\u2018": "'", "\u2019": "'",
81
+ "\u2014": "-", "\u2013": "-", "\u2026": "...",
82
+ }
83
+
84
+
85
+ # --------------------------------------------------------------------------- #
86
+ # 切段与归一化
87
+ # --------------------------------------------------------------------------- #
88
+
89
+ def strip_fm(text: str) -> str:
90
+ """只剥文件头的 frontmatter。正文里的 --- 必须保留。"""
91
+ if text.startswith("---\n"):
92
+ m = re.match(r"^---\n.*?\n---\n", text, re.S | re.M)
93
+ if m:
94
+ return text[m.end():]
95
+ return text
96
+
97
+
98
+ def blocks(text: str, skips):
99
+ """按空行切段;丢掉分隔线、纯图片、元信息块、HTML 注释块、--skip 命中的块。
100
+
101
+ 代码块内部不切分,避免把一段代码拆散。
102
+ """
103
+ body = strip_fm(text).replace("\r\n", "\n")
104
+ out, buf, in_code = [], [], False
105
+ for line in body.split("\n"):
106
+ if line.strip().startswith("```"):
107
+ in_code = not in_code
108
+ buf.append(line)
109
+ continue
110
+ if in_code:
111
+ buf.append(line)
112
+ continue
113
+ if line.strip() == "":
114
+ if buf:
115
+ out.append("\n".join(buf))
116
+ buf = []
117
+ else:
118
+ buf.append(line)
119
+ if buf:
120
+ out.append("\n".join(buf))
121
+
122
+ keep = []
123
+ for b in out:
124
+ s = b.strip()
125
+ if not s or re.fullmatch(r"[-*_]{3,}", s) or IMG_ONLY_RE.match(s):
126
+ continue
127
+ if META_RE.match(s) or HTML_COMMENT_RE.match(s):
128
+ continue
129
+ if any(rx.search(s) for rx in skips):
130
+ continue
131
+ keep.append(s)
132
+ return keep
133
+
134
+
135
+ def normalize(block: str) -> str:
136
+ """把一段 markdown 归一化成「可比较的纯文字内容」。"""
137
+ t = block
138
+ t = re.sub(r"^\s*>\s?", "", t, flags=re.M)
139
+ t = re.sub(r"^\s*#{1,6}\s*", "", t, flags=re.M)
140
+ t = re.sub(r"^\s*[-*+]\s+", "", t, flags=re.M)
141
+ t = re.sub(r"^\s*\d+[.)]\s+", "", t, flags=re.M)
142
+ t = re.sub(r"!\[[^\]]*\]\([^)]*\)", "", t)
143
+ t = re.sub(r"!\[\[[^\]]*\]\]", "", t)
144
+ t = re.sub(r"\[([^\]]*)\]\([^)]*\)", r"\1", t)
145
+ t = re.sub(r"`{1,3}", "", t)
146
+ for a in ("**", "__", "==", "++", "~~"):
147
+ t = t.replace(a, "")
148
+ for a, b in _TYPO.items():
149
+ t = t.replace(a, b)
150
+ return re.sub(r"\s+", "", t)
151
+
152
+
153
+ def block_windows(norms):
154
+ """段级窗口:(起始段, 结束段, 合并文本)。"""
155
+ n = len(norms)
156
+ for i in range(n):
157
+ acc = ""
158
+ for k in range(MAX_SPAN):
159
+ if i + k >= n:
160
+ break
161
+ acc += norms[i + k]
162
+ yield i, i + k, acc
163
+
164
+
165
+ def sent_windows(norms):
166
+ """句级窗口:(所属段, 所属段, 合并文本)。用于识别「源稿一段被拆开」。"""
167
+ for i, blk in enumerate(norms):
168
+ sents = [s for s in SENT_RE.findall(blk) if s]
169
+ for a in range(len(sents)):
170
+ acc = ""
171
+ for k in range(MAX_SPAN):
172
+ if a + k >= len(sents):
173
+ break
174
+ acc += sents[a + k]
175
+ if len(acc) >= MIN_SENT_WIN:
176
+ yield i, i, acc
177
+
178
+
179
+ def best(needle, win_list):
180
+ """返回 (最高相似度, 窗口起, 窗口止, 次高相似度)。"""
181
+ if not needle or not win_list:
182
+ return 0.0, -1, -1, 0.0
183
+ scored = []
184
+ for a, b, txt in win_list:
185
+ scored.append((SequenceMatcher(None, needle, txt).ratio(), a, b))
186
+ scored.sort(key=lambda x: x[0], reverse=True)
187
+ top = scored[0]
188
+ second = scored[1][0] if len(scored) > 1 else 0.0
189
+ return top[0], top[1], top[2], second
190
+
191
+
192
+ def is_tail(text: str) -> bool:
193
+ return any(h in text for h in TAIL_HINTS)
194
+
195
+
196
+ def snippet(text: str, n: int = 44) -> str:
197
+ one = re.sub(r"\s+", " ", text).strip()
198
+ return one if len(one) <= n else one[:n] + "…"
199
+
200
+
201
+ # --------------------------------------------------------------------------- #
202
+ # 审计
203
+ # --------------------------------------------------------------------------- #
204
+
205
+ def audit(draft_path, source_path, threshold, skips):
206
+ dblocks = blocks(open(draft_path, encoding="utf-8").read(), skips)
207
+ sblocks = blocks(open(source_path, encoding="utf-8").read(), skips)
208
+ dn = [normalize(b) for b in dblocks]
209
+ sn = [normalize(b) for b in sblocks]
210
+
211
+ s_blk = list(block_windows(sn))
212
+ s_sent = list(sent_windows(sn))
213
+ # 本稿侧额外把「去掉 ▲ 图 N 前缀」的版本算作候选,图注不算遗漏
214
+ dn_plus = list(dn) + [normalize(CAPTION_PREFIX_RE.sub("", b.strip()))
215
+ for b in dblocks if CAPTION_PREFIX_RE.match(b.strip())]
216
+ dn_plus = [x for x in dn_plus if x]
217
+ d_blk = list(block_windows(dn_plus))
218
+ d_sent = list(sent_windows(dn_plus))
219
+
220
+ def judge(r_blk, r_sent, span_blk, span_sent):
221
+ """按「先段级、后句级」判定分类。"""
222
+ if r_blk >= threshold:
223
+ if r_blk >= 0.995:
224
+ return "identical", r_blk, span_blk
225
+ if span_blk[1] > span_blk[0]:
226
+ return "merged", r_blk, span_blk
227
+ return "rewritten", r_blk, span_blk
228
+ if r_sent >= threshold:
229
+ return "split", r_sent, span_sent
230
+ return "unexplained", max(r_blk, r_sent), span_blk
231
+
232
+ forward = []
233
+ for i, d in enumerate(dblocks):
234
+ if not dn[i]:
235
+ continue
236
+ rb, ab1, ab2, sb = best(dn[i], s_blk)
237
+ rs, as1, as2, ss = best(dn[i], s_sent)
238
+ capped = bool(CAPTION_PREFIX_RE.match(d.strip()))
239
+ r_nocap = -1.0
240
+ if capped:
241
+ alt = normalize(CAPTION_PREFIX_RE.sub("", d.strip()))
242
+ if alt:
243
+ r_nocap, *_ = best(alt, s_blk)
244
+ top = max(rb, rs, r_nocap)
245
+
246
+ if top >= threshold:
247
+ kind, _, span = judge(rb, rs, (ab1, ab2), (as1, as2))
248
+ if capped and r_nocap > max(rb, rs):
249
+ kind = "caption"
250
+ forward.append({"i": i, "kind": kind, "ratio": round(top, 3),
251
+ "source_span": list(span), "text": snippet(d)})
252
+ elif is_tail(d):
253
+ forward.append({"i": i, "kind": "tail", "ratio": round(top, 3),
254
+ "source_span": [-1, -1], "text": snippet(d)})
255
+ else:
256
+ forward.append({"i": i, "kind": "unexplained", "ratio": round(top, 3),
257
+ "second": round(max(sb, ss), 3), "source_span": [ab1, ab2],
258
+ "text": snippet(d)})
259
+
260
+ backward = []
261
+ for i, s in enumerate(sblocks):
262
+ if not sn[i]:
263
+ continue
264
+ rb, ab1, ab2, sb = best(sn[i], d_blk)
265
+ rs, as1, as2, ss = best(sn[i], d_sent)
266
+ kind, ratio, span = judge(rb, rs, (ab1, ab2), (as1, as2))
267
+ if kind == "identical":
268
+ kind = "found"
269
+ rec = {"i": i, "kind": kind, "ratio": round(ratio, 3),
270
+ "draft_span": list(span), "text": snippet(s)}
271
+ if kind == "unexplained":
272
+ rec["second"] = round(max(sb, ss), 3)
273
+ backward.append(rec)
274
+
275
+ return {"draft": draft_path, "source": source_path, "threshold": threshold,
276
+ "draft_blocks": len(dblocks), "source_blocks": len(sblocks),
277
+ "forward": forward, "backward": backward}
278
+
279
+
280
+ def report(res) -> int:
281
+ f, b = res["forward"], res["backward"]
282
+ f_un = [x for x in f if x["kind"] == "unexplained"]
283
+ b_un = [x for x in b if x["kind"] == "unexplained"]
284
+
285
+ print("溯源审计:%s ← %s" % (res["draft"], res["source"]))
286
+ print("阈值 %.2f · 本稿 %d 段 · 源稿 %d 段" %
287
+ (res["threshold"], res["draft_blocks"], res["source_blocks"]))
288
+ print("-" * 78)
289
+
290
+ def tally(rows):
291
+ d = {}
292
+ for r in rows:
293
+ d[r["kind"]] = d.get(r["kind"], 0) + 1
294
+ return d
295
+
296
+ print("正向(本稿 → 源稿):", " ".join("%s %d" % kv for kv in sorted(tally(f).items())))
297
+ print("反向(源稿 → 本稿):", " ".join("%s %d" % kv for kv in sorted(tally(b).items())))
298
+
299
+ if f_un:
300
+ print("\n⚠ 正向待判定(本稿有、源稿找不到 —— 扩写藏在这里):")
301
+ for r in f_un:
302
+ print(" 段%-4d 相似度 %.2f(次高 %.2f) 「%s」" %
303
+ (r["i"], r["ratio"], r.get("second", 0), r["text"]))
304
+ if b_un:
305
+ print("\n⚠ 反向待判定(源稿有、本稿落下了):")
306
+ for r in b_un:
307
+ print(" 段%-4d 相似度 %.2f(次高 %.2f) 「%s」" %
308
+ (r["i"], r["ratio"], r.get("second", 0), r["text"]))
309
+
310
+ total = len(f_un) + len(b_un)
311
+ print("-" * 78)
312
+ if total == 0:
313
+ print("✓ 无待判定项:本稿相对源稿只剩排版差异、图注编号与结尾附加句")
314
+ return 0
315
+ print("✗ %d 项待人工判定(正向新增 %d · 反向遗漏 %d)" % (total, len(f_un), len(b_un)))
316
+ print(" 判定口径:每一项要么归入「已声明的事实修正 / 已声明的增补」,")
317
+ print(" 要么就是不该出现的改动 —— 见技能 SKILL.md「改稿底线」。")
318
+ print(" 源稿里后加的块(勘误等)用 --skip '^>\\s*\\[!' 排除。")
319
+ return 1
320
+
321
+
322
+ def main():
323
+ ap = argparse.ArgumentParser(
324
+ description="双向逐段溯源审计:查改稿有没有悄悄扩写或漏掉")
325
+ ap.add_argument("--draft", required=True, help="派生稿(如 公众号稿.md)")
326
+ ap.add_argument("--source", required=True, help="源稿(如 源稿-X长文.md)")
327
+ ap.add_argument("--threshold", type=float, default=0.72,
328
+ help="相似度阈值,低于它即待判定(默认 0.72)")
329
+ ap.add_argument("--skip", action="append", default=[],
330
+ help="正则:命中的段直接排除(可重复,两侧都生效)")
331
+ ap.add_argument("--json", action="store_true", help="输出 JSON")
332
+ ap.add_argument("--verbose", action="store_true", help="打印逐段明细")
333
+ args = ap.parse_args()
334
+
335
+ for p in (args.draft, args.source):
336
+ try:
337
+ open(p, encoding="utf-8").close()
338
+ except OSError as e:
339
+ print("✗ 读不到文件:%s(%s)" % (p, e), file=sys.stderr)
340
+ return 2
341
+
342
+ try:
343
+ skips = [re.compile(rx) for rx in args.skip]
344
+ except re.error as e:
345
+ print("✗ --skip 正则非法:%s" % e, file=sys.stderr)
346
+ return 2
347
+
348
+ res = audit(args.draft, args.source, args.threshold, skips)
349
+
350
+ if args.verbose:
351
+ print("逐段明细:")
352
+ for tag, rows in (("正", res["forward"]), ("反", res["backward"])):
353
+ for r in rows:
354
+ print(" %s 段%-4d %-11s %.2f %s" %
355
+ (tag, r["i"], r["kind"], r["ratio"], r["text"]))
356
+ print("-" * 78)
357
+
358
+ if args.json:
359
+ print(json.dumps(res, ensure_ascii=False, indent=2))
360
+ return 0 if not any(x["kind"] == "unexplained"
361
+ for x in res["forward"] + res["backward"]) else 1
362
+ return report(res)
363
+
364
+
365
+ if __name__ == "__main__":
366
+ sys.exit(main())
@@ -1,464 +1,101 @@
1
1
  #!/usr/bin/env python3
2
- """
3
- scan_ai_flavor.py — 中文 AI 味检测脚本
4
-
5
- 用法:
6
- python3 scan_ai_flavor.py <file> # 输出 Markdown 报告
7
- python3 scan_ai_flavor.py --json <file> # 输出 JSON
8
- python3 scan_ai_flavor.py -o report.md <file>
9
- cat article.md | python3 scan_ai_flavor.py - # 管道
10
-
11
- 设计目标:纯 stdlib,无第三方依赖,能在普通 Mac / Linux 上直接跑。
12
- """
13
- from __future__ import annotations
14
-
15
- import argparse
16
- import json
17
- import re
18
- import sys
19
- from collections import Counter
20
- from dataclasses import dataclass, field, asdict
21
- from pathlib import Path
22
- from typing import Iterable
2
+ # -*- coding: utf-8 -*-
3
+ """scan_ai_flavor.py — 转发器:调用 qu-aiwei-zh 的 canonical 检测脚本。
23
4
 
5
+ **本文件不含检测实现。** 原因见下。
24
6
 
25
- # --------------------------------------------------------------------------- #
26
- # 模式定义:每条 (类别代码, 类别名, 正则, 单次扣分, 严重度, 开头末尾加权, 备注)
27
- # --------------------------------------------------------------------------- #
7
+ ## 为什么是转发器而不是自带副本
28
8
 
9
+ topmind-wechat 曾自带一份 463 行的实现副本,它是 `qu-aiwei-zh` 在加入「E 类 · 作者姿态层」
10
+ **之前**的旧快照 —— 不输出姿态参考分,也识别不了姿态类 AI 痕迹。照 SKILL.md 跑它,会拿到一个
11
+ **没有姿态维度的「100 分」**:看着干净,其实少了一整个维度。
29
12
 
30
- @dataclass
31
- class Pattern:
32
- code: str # e.g. "A1"
33
- name: str # e.g. "开头套话"
34
- regex: str # 正则字符串
35
- penalty: int # 单次扣分(绝对值)
36
- severity: str # high / mid / low
37
- weighted: str = "" # "head" | "tail" | ""(加权区段)
38
- note: str = ""
13
+ 两版实测差异:旧副本 0 处「姿态」相关代码,canonical 20 处。
39
14
 
40
- def compiled(self) -> re.Pattern:
41
- return re.compile(self.regex)
15
+ 副本必然再次漂移(一边改了另一边不知道),所以这里只保留入口,实现归 canonical 一处。
42
16
 
17
+ ## canonical 查找顺序(命中即用)
43
18
 
44
- # 头部加权的模式(命中位置 ≤ 200 字 权重 ×1.5)
45
- HEAD_PATTERNS: list[Pattern] = [
46
- Pattern("A1", "开头套话", r"在当今(社会|时代)", 8, "high", "head"),
47
- Pattern("A1", "开头套话", r"随着[^,。]{2,30}的(发展|日益|不断)", 8, "high", "head"),
48
- Pattern("A1", "开头套话", r"众所周知[,,]", 7, "high", "head"),
49
- Pattern("A1", "开头套话", r"毋庸置疑[,,]|不可否认[,,]", 7, "high", "head"),
50
- Pattern("A1", "开头套话", r"近年来[^,。]{0,15}日益", 6, "high", "head"),
51
- Pattern("A1", "开头套话", r"伴随着[^,。]{2,30}的(快速|迅猛)", 8, "high", "head"),
52
- Pattern("A1", "开头套话", r"在[^,。]{2,20}的背景下", 5, "mid", "head"),
53
- Pattern("A1", "开头套话", r"一直以来[^,。]{2,20}都", 4, "low", "head"),
54
- Pattern("A10", "模板口水", r"大家好[,,]", 6, "high", "head"),
55
- Pattern("A10", "模板口水", r"本文将从[^,。]{0,30}个方面", 7, "high", "head"),
56
- Pattern("A10", "模板口水", r"希望本文能够", 5, "mid", "head"),
57
- Pattern("A10", "模板口水", r"如(有不当|有疏漏|有错误)[,,]", 4, "low", "head"),
58
- ]
59
-
60
- # 全文模式
61
- BODY_PATTERNS: list[Pattern] = [
62
- # A2 转折连接
63
- Pattern("A2", "转折连接", r"此外[,,]", 2, "low"),
64
- Pattern("A2", "转折连接", r"与此同时[,,]", 2, "low"),
65
- Pattern("A2", "转折连接", r"不仅如此[,,]", 2, "low"),
66
- Pattern("A2", "转折连接", r"综上所述[,,]", 4, "mid"),
67
- Pattern("A2", "转折连接", r"总而言之[,,]", 4, "mid"),
68
- Pattern("A2", "转折连接", r"一方面[^,。]{1,15}[,,另一方面]", 3, "low"),
69
- Pattern("A2", "转折连接", r"既[^,。]{1,12}又[^,。]{1,12}还", 3, "low"),
70
- Pattern("A2", "转折连接", r"无可厚非", 4, "mid"),
71
- Pattern("A2", "转折连接", r"瑕不掩瑜", 4, "mid"),
19
+ 1. `$QU_AIWEI_SCAN` 显式指定(测试与非标准安装用)
20
+ 2. `~/.workbuddy/skills/qu-aiwei-zh/scripts/scan_ai_flavor.py`
21
+ 3. `~/.agents/skills/qu-aiwei-zh/scripts/scan_ai_flavor.py`
22
+ 4. `~/.claude/skills/qu-aiwei-zh/scripts/scan_ai_flavor.py`
23
+ 5. `~/.codex/skills/qu-aiwei-zh/scripts/scan_ai_flavor.py`
72
24
 
73
- # A3 形容词/官腔
74
- Pattern("A3", "官腔形容词", r"至关重要", 5, "high"),
75
- Pattern("A3", "官腔形容词", r"举足轻重", 5, "high"),
76
- Pattern("A3", "官腔形容词", r"不可或缺", 4, "mid"),
77
- Pattern("A3", "官腔形容词", r"深入(探讨|分析|研究)", 5, "high"),
78
- Pattern("A3", "官腔形容词", r"高度重视", 5, "high"),
79
- Pattern("A3", "官腔形容词", r"深入推进", 5, "high"),
80
- Pattern("A3", "官腔形容词", r"扎实开展", 5, "high"),
81
- Pattern("A3", "官腔形容词", r"积极探索", 4, "mid"),
82
- Pattern("A3", "官腔形容词", r"有效落实", 4, "mid"),
83
- Pattern("A3", "官腔形容词", r"持续优化", 4, "mid"),
84
- Pattern("A3", "官腔形容词", r"凝聚共识", 5, "high"),
85
- Pattern("A3", "官腔形容词", r"汇聚合力", 5, "high"),
86
- Pattern("A3", "官腔形容词", r"协同推进", 4, "mid"),
87
- Pattern("A3", "官腔形容词", r"全方位(提升|推进|优化)", 4, "mid"),
88
- Pattern("A3", "官腔形容词", r"多角度(分析|审视)", 4, "mid"),
89
- Pattern("A3", "官腔形容词", r"立体化", 3, "low"),
90
- # 注:数字化/智能化 是领域术语,不作为 AI 味标志,跳过
25
+ ## 行为
91
26
 
92
- # A5 排比/三段式
93
- Pattern("A5", "排比三段式", r"这不仅[^,。]{2,20}[,,][^,。]{2,15}也[^,。]{2,15}[,,更]?", 6, "high"),
94
- Pattern("A5", "排比三段式", r"从[^,。]{2,15}到[^,。]{2,15}[,,从][^,。]{2,15}到", 4, "mid"),
27
+ - 参数原样透传(位置参数 `<file>` / `--json` / `-o` / `-` 管道都可用),stdin / stdout / stderr 继承
28
+ - 退出码取 canonical 的退出码
29
+ - `--which` 只打印解析到的 canonical 路径并退出 0(转发器自有开关,canonical 无此参数)
30
+ - **找不到 canonical 时报错退出(exit 3),不静默降级** —— 宁可这一步跑不了,
31
+ 也不要给出一个没有姿态分的分数
95
32
 
96
- # A6 营销体
97
- Pattern("A6", "营销黑话", r"赋能", 5, "high"),
98
- Pattern("A6", "营销黑话", r"加持", 5, "high"),
99
- Pattern("A6", "营销黑话", r"引爆", 5, "high"),
100
- Pattern("A6", "营销黑话", r"破圈", 5, "high"),
101
- Pattern("A6", "营销黑话", r"一站式", 4, "mid"),
102
- Pattern("A6", "营销黑话", r"全链路", 4, "mid"),
103
- Pattern("A6", "营销黑话", r"闭环", 4, "mid"),
104
- Pattern("A6", "营销黑话", r"抓手", 5, "high"),
105
- Pattern("A6", "营销黑话", r"底层逻辑", 5, "high"),
106
- Pattern("A6", "营销黑话", r"顶层设计", 5, "high"),
107
- Pattern("A6", "营销黑话", r"(打造|构建|重塑)[^,。]{2,15}(方案|生态|体系|平台)", 3, "low"),
108
- Pattern("A6", "营销黑话", r"拉通|对齐|复盘", 3, "low"),
109
- # 工程圈行话:用户明确点过——「踩坑」这类词读起来像圈内口令,不像人在说话
110
- Pattern("A6", "工程圈行话", r"踩坑|避坑|干货|踩出来的坑|踩过的坑", 3, "low"),
33
+ ## 用法
111
34
 
112
- # A7 模糊归因
113
- Pattern("A7", "模糊归因", r"专家指出", 5, "high"),
114
- Pattern("A7", "模糊归因", r"业内人士表示", 5, "high"),
115
- Pattern("A7", "模糊归因", r"据相关(研究|调查|数据)表明", 5, "high"),
116
- Pattern("A7", "模糊归因", r"大量数据表明", 5, "high"),
117
- Pattern("A7", "模糊归因", r"有学者认为", 4, "mid"),
118
- Pattern("A7", "模糊归因", r"专家(普遍|一致)认为", 4, "mid"),
119
- Pattern("A7", "模糊归因", r"大家普遍认为", 3, "low"),
35
+ python3 scripts/scan_ai_flavor.py <包>/公众号稿.md # 目标 ≥85
36
+ python3 scripts/scan_ai_flavor.py --which
37
+ """
120
38
 
121
- # A8 伪深度
122
- Pattern("A8", "伪深度", r"(是|为)[^,。]{2,15}的(体现|证明|象征)", 5, "high"),
123
- Pattern("A8", "伪深度", r"彰显了[^,。]{2,20}(意义|价值|精神)", 5, "high"),
124
- Pattern("A8", "伪深度", r"凸显了[^,。]{2,20}(意义|价值)", 5, "high"),
125
- Pattern("A8", "伪深度", r"反映了更深(层次)的", 4, "mid"),
39
+ from __future__ import annotations
126
40
 
127
- # A9 假大空成语
128
- Pattern("A9", "假大空成语", r"博大精深", 4, "mid"),
129
- Pattern("A9", "假大空成语", r"源远流长", 4, "mid"),
130
- Pattern("A9", "假大空成语", r"历久弥新", 4, "mid"),
131
- Pattern("A9", "假大空成语", r"与时俱进", 3, "low"),
132
- Pattern("A9", "假大空成语", r"开拓创新", 3, "low"),
133
- Pattern("A9", "假大空成语", r"锐意进取", 3, "low"),
134
- Pattern("A9", "假大空成语", r"举世(瞩目|无双)", 4, "mid"),
41
+ import os
42
+ import sys
135
43
 
136
- # A10 模板口水
137
- Pattern("A10", "模板口水", r"感谢(您的)?(观看|阅读|支持)", 5, "high"),
138
- Pattern("A10", "模板口水", r"以上就是[^,。]{2,30}(全部)?内容", 5, "high"),
139
- Pattern("A10", "模板口水", r"欢迎(指正|交流|留言)", 4, "mid"),
140
- ]
44
+ REL = os.path.join("qu-aiwei-zh", "scripts", "scan_ai_flavor.py")
141
45
 
142
- # 尾部加权
143
- TAIL_PATTERNS: list[Pattern] = [
144
- Pattern("A4", "结尾升华", r"让我们(共同|一起)", 8, "high", "tail"),
145
- Pattern("A4", "结尾升华", r"未来可期", 6, "high", "tail"),
146
- Pattern("A4", "结尾升华", r"任重道远", 6, "high", "tail"),
147
- Pattern("A4", "结尾升华", r"砥砺前行|扬帆远航", 5, "high", "tail"),
148
- Pattern("A4", "结尾升华", r"在[^,。]{2,20}的道路上(不断)?前行", 6, "high", "tail"),
149
- Pattern("A4", "结尾升华", r"为[^,。]{2,15}贡献(自己)?的?力量", 6, "high", "tail"),
150
- Pattern("A4", "结尾升华", r"谱写[^,。]{2,15}新篇章", 6, "high", "tail"),
151
- Pattern("A4", "结尾升华", r"展望未来", 4, "mid", "tail"),
46
+ CANDIDATES = [
47
+ os.path.join("~", ".workbuddy", "skills", REL),
48
+ os.path.join("~", ".agents", "skills", REL),
49
+ os.path.join("~", ".claude", "skills", REL),
50
+ os.path.join("~", ".codex", "skills", REL),
152
51
  ]
153
52
 
154
- ALL_PATTERNS = HEAD_PATTERNS + BODY_PATTERNS + TAIL_PATTERNS
155
-
156
-
157
- # --------------------------------------------------------------------------- #
158
- # 数据结构
159
- # --------------------------------------------------------------------------- #
160
-
161
-
162
- @dataclass
163
- class Hit:
164
- code: str
165
- name: str
166
- pattern: str
167
- text: str
168
- line: int
169
- column: int
170
- severity: str
171
- penalty: int
172
- raw_penalty: int # 加权前
173
-
174
-
175
- @dataclass
176
- class Report:
177
- score: int
178
- level: str
179
- level_emoji: str
180
- hits: list[Hit] = field(default_factory=list)
181
- lexical_deduction: int = 0
182
- structural_deduction: int = 0
183
- stats: dict = field(default_factory=dict)
184
- suggestions: list[str] = field(default_factory=list)
185
-
186
-
187
- # --------------------------------------------------------------------------- #
188
- # 扫描逻辑
189
- # --------------------------------------------------------------------------- #
190
-
191
-
192
- def scan_text(text: str) -> Report:
193
- lines = text.splitlines()
194
- total_chars = len(text)
195
- total_lines = len(lines)
196
- head_chars = "".join(lines[: max(1, total_lines // 5)]) # 前 20%
197
- tail_chars = "".join(lines[-max(1, total_lines // 5):]) # 后 20%
198
-
199
- hits: list[Hit] = []
200
- seen_in_text: dict[tuple[str, str], int] = {} # (pattern, exact_text) → 第 N 次出现
201
-
202
- for p in ALL_PATTERNS:
203
- regex = p.compiled()
204
- for m in regex.finditer(text):
205
- # 计算行号/列号
206
- start = m.start()
207
- line_no = text.count("\n", 0, start) + 1
208
- col_no = start - (text.rfind("\n", 0, start) + 1) + 1
209
-
210
- # 计算加权:位置决定
211
- region = "body"
212
- if p.weighted == "head" and start < len(head_chars):
213
- region = "head"
214
- elif p.weighted == "tail" and start >= len(text) - len(tail_chars):
215
- region = "tail"
216
-
217
- raw = p.penalty
218
- if region != "body":
219
- raw = int(raw * 1.5)
220
-
221
- # 重复衰减:同一模式第 N 次出现
222
- key = (p.regex, m.group(0))
223
- n = seen_in_text.get(key, 0) + 1
224
- seen_in_text[key] = n
225
- multiplier = min(n, 3) # 最多 3 次
53
+ MISSING = """\
54
+ ✗ 找不到 qu-aiwei-zh 的 canonical 检测脚本,无法执行「文字关」。
226
55
 
227
- final = raw * multiplier
56
+ 本目录不再自带实现 —— 曾经那份是 E 类姿态检测之前的旧快照,会漏掉姿态维度。
57
+ 已查找:
58
+ {paths}
228
59
 
229
- hits.append(
230
- Hit(
231
- code=p.code,
232
- name=p.name,
233
- pattern=p.regex,
234
- text=m.group(0),
235
- line=line_no,
236
- column=col_no,
237
- severity=p.severity,
238
- penalty=final,
239
- raw_penalty=raw,
240
- )
241
- )
242
-
243
- # 结构扣分
244
- structural = 0
245
- # 重复的段首加粗标签:**结论**:/ **踩坑提醒**:/ **注意**: …
246
- # 这是模板化写作最典型的痕迹,词面词表抓不到,只能按"同一标签重复出现"判定。
247
- # 判据取 ≥3 次:用两次属于正常强调,用三次以上就是在套模板。
248
- lead_labels = re.findall(r"^\s*\*\*([^*\n]{1,10})\*\*\s*[::]", text, re.M)
249
- repeated_labels: list[str] = []
250
- if lead_labels:
251
- counts = Counter(lead_labels)
252
- repeated_labels = [k for k, v in counts.items() if v >= 3]
253
- if total_lines >= 5:
254
- # 破折号过多
255
- dash_count = text.count("——")
256
- if dash_count >= max(3, total_chars // 200):
257
- structural -= 5
258
- # 感叹号过多
259
- if text.count("!!!") >= 1 or text.count("!!") >= 3:
260
- structural -= 3
261
- # 缺乏具体细节(无任何数字)
262
- if not re.search(r"\d", text):
263
- structural -= 12
264
- # 缺乏个性化声音(无第一人称)
265
- if not re.search(r"[我我们]", text):
266
- structural -= 8
267
- # 标题 emoji 滥用
268
- emoji_count = len(re.findall(r"[\U0001F300-\U0001FAFF\U00002600-\U000027BF]", text))
269
- if emoji_count >= 3:
270
- structural -= min(9, (emoji_count - 2) * 3)
271
- # 段首加粗标签模板化
272
- if repeated_labels:
273
- structural -= min(12, sum(lead_labels.count(k) for k in repeated_labels) * 2)
274
-
275
- # 词面扣分汇总
276
- # 注意:structural 累加时是负数(structural -= n),必须取绝对值再相加。
277
- # 若直接相加,lexical + structural 会变成"加分",结构扣分整条维度静默失效。
278
- lexical = sum(h.penalty for h in hits)
279
- structural = abs(structural)
280
- total_deduction = lexical + structural
281
- score = max(0, min(100, 100 - total_deduction))
282
-
283
- level, level_emoji = grade(score)
284
-
285
- # 生成建议(Top 3)
286
- suggestions = build_suggestions(hits, score)
287
- if repeated_labels:
288
- tips = "、".join(f"**{k}**:×{lead_labels.count(k)}" for k in repeated_labels)
289
- suggestions.insert(0, f"段首加粗标签模板化 — {tips},同一标签重复 3 次以上就是在套模板,改成直陈句")
290
-
291
- return Report(
292
- score=score,
293
- level=level,
294
- level_emoji=level_emoji,
295
- hits=hits,
296
- lexical_deduction=lexical,
297
- structural_deduction=structural,
298
- stats={
299
- "total_chars": total_chars,
300
- "total_lines": total_lines,
301
- "ai_pattern_count": len(hits),
302
- "repeated_lead_labels": repeated_labels,
303
- },
304
- suggestions=suggestions,
305
- )
306
-
307
-
308
- def grade(score: int) -> tuple[str, str]:
309
- if score >= 85:
310
- return "人话", "🟢"
311
- if score >= 70:
312
- return "微 AI 味", "🟡"
313
- if score >= 50:
314
- return "中度 AI 味", "🟠"
315
- if score >= 30:
316
- return "重度 AI 味", "🔴"
317
- return "完全 AI 体", "⚫"
318
-
319
-
320
- def build_suggestions(hits: list[Hit], score: int) -> list[str]:
321
- """按类别聚类,给出 3 条优先建议。"""
322
- by_code: dict[str, list[Hit]] = {}
323
- for h in hits:
324
- by_code.setdefault(h.code, []).append(h)
325
-
326
- # 优先级:A4>A3>A6>A7>A1>A8>A10>A5>A9
327
- priority = ["A4", "A3", "A6", "A7", "A1", "A8", "A10", "A5", "A9"]
328
- tips_map = {
329
- "A1": "开头套话 — 删掉'在当今社会'等模板开头,直接说事实",
330
- "A2": "转折词 — 用句号断开,靠上下文自然过渡",
331
- "A3": "官腔形容词 — 换成大白话或具体数字",
332
- "A4": "结尾升华 — 删掉'让我们共同'等空话,给具体计划",
333
- "A5": "排比三段式 — 砍到两项或四项,别硬凑三段",
334
- "A6": "营销黑话 — 改为具体动词,如'做了一个工具'",
335
- "A7": "模糊归因 — 给具体名字/出处/数字",
336
- "A8": "伪深度 — 删除'是 XX 的体现/证明'句式",
337
- "A9": "假大空成语 — 用白话翻译一遍",
338
- "A10": "模板口水 — 删掉口水句,直接进入正文",
339
- }
340
-
341
- suggestions = []
342
- for code in priority:
343
- if code in by_code:
344
- n = len(by_code[code])
345
- suggestions.append(f"{tips_map[code]}(命中 {n} 处)")
346
- if len(suggestions) >= 3:
347
- break
348
-
349
- if not suggestions:
350
- suggestions.append("未发现明显 AI 模式,但建议人工通读检查结构/个性化声音")
351
-
352
- return suggestions
353
-
354
-
355
- # --------------------------------------------------------------------------- #
356
- # 输出格式
357
- # --------------------------------------------------------------------------- #
358
-
359
-
360
- def render_markdown(report: Report, source: str) -> str:
361
- md: list[str] = []
362
- md.append(f"## AI 味检测报告\n")
363
- md.append(f"**文件:** `{source}` ")
364
- md.append(f"**总分:** **{report.score}** / 100 {report.level_emoji} {report.level}\n")
365
-
366
- md.append("### 分项评分")
367
- md.append(f"- 词面命中扣分:**-{report.lexical_deduction}**")
368
- md.append(f"- 结构问题扣分:**{report.structural_deduction}**")
369
- md.append(f"- 总计:**-{report.lexical_deduction + abs(report.structural_deduction)}** → {report.score} 分\n")
370
-
371
- md.append("### 基础统计")
372
- md.append(f"- 字符数:{report.stats.get('total_chars', 0)}")
373
- md.append(f"- 行数:{report.stats.get('total_lines', 0)}")
374
- md.append(f"- AI 模式命中数:**{report.stats.get('ai_pattern_count', 0)}**")
375
- reps = report.stats.get("repeated_lead_labels") or []
376
- if reps:
377
- md.append(f"- 重复段首加粗标签:**{'、'.join(reps)}**")
378
- md.append("")
379
-
380
- if report.hits:
381
- md.append("### 命中位置(按严重度排序)")
382
- md.append("| # | 类别 | 命中内容 | 位置 | 严重度 | 扣分 |")
383
- md.append("|---|---|---|---|---|---|")
384
- sorted_hits = sorted(report.hits, key=lambda h: -h.penalty)
385
- for i, h in enumerate(sorted_hits[:30], 1):
386
- preview = h.text if len(h.text) <= 25 else h.text[:25] + "…"
387
- md.append(f"| {i} | {h.code} {h.name} | `{preview}` | 第 {h.line} 行 | {h.severity} | -{h.penalty} |")
388
- if len(report.hits) > 30:
389
- md.append(f"\n*(还有 {len(report.hits) - 30} 处命中未显示)*\n")
390
- else:
391
- md.append("### 命中位置")
392
- md.append("无命中 🎉\n")
393
-
394
- md.append("### Top 3 优先改写建议")
395
- for s in report.suggestions:
396
- md.append(f"1. {s}")
397
- md.append("")
398
-
399
- return "\n".join(md)
400
-
401
-
402
- def render_json(report: Report) -> str:
403
- return json.dumps(
404
- {
405
- "score": report.score,
406
- "level": report.level,
407
- "level_emoji": report.level_emoji,
408
- "deductions": {
409
- "lexical": report.lexical_deduction,
410
- "structural": report.structural_deduction,
411
- },
412
- "stats": report.stats,
413
- "hits": [asdict(h) for h in report.hits],
414
- "suggestions": report.suggestions,
415
- },
416
- ensure_ascii=False,
417
- indent=2,
418
- )
419
-
420
-
421
- # --------------------------------------------------------------------------- #
422
- # CLI
423
- # --------------------------------------------------------------------------- #
60
+ 解决:安装 qu-aiwei-zh 技能,或用环境变量显式指定:
61
+ export QU_AIWEI_SCAN=/path/to/qu-aiwei-zh/scripts/scan_ai_flavor.py
62
+ """
424
63
 
425
64
 
426
- def main(argv: list[str]) -> int:
427
- parser = argparse.ArgumentParser(
428
- description="中文 AI 味检测器 — 给文章打 AI 味分数"
429
- )
430
- parser.add_argument("input", help="输入文件路径,或 '-' 表示 stdin")
431
- parser.add_argument("--json", action="store_true", help="输出 JSON 格式")
432
- parser.add_argument("-o", "--output", help="输出文件(默认 stdout)")
433
- args = parser.parse_args(argv)
65
+ def resolve():
66
+ """返回 canonical 脚本的绝对路径;找不到返回 None。"""
67
+ env = os.environ.get("QU_AIWEI_SCAN")
68
+ if env:
69
+ p = os.path.abspath(os.path.expanduser(env))
70
+ return p if os.path.isfile(p) else None
71
+ for c in CANDIDATES:
72
+ p = os.path.abspath(os.path.expanduser(c))
73
+ if os.path.isfile(p):
74
+ return p
75
+ return None
434
76
 
435
- # 读取输入
436
- if args.input == "-":
437
- text = sys.stdin.read()
438
- source = "<stdin>"
439
- else:
440
- p = Path(args.input)
441
- if not p.exists():
442
- print(f"❌ 文件不存在: {args.input}", file=sys.stderr)
443
- return 1
444
- text = p.read_text(encoding="utf-8")
445
- source = str(p)
446
77
 
447
- report = scan_text(text)
78
+ def main():
79
+ args = sys.argv[1:]
448
80
 
449
- if args.json:
450
- out = render_json(report)
451
- else:
452
- out = render_markdown(report, source)
81
+ if args and args[0] == "--which":
82
+ target = resolve()
83
+ if target:
84
+ print(target)
85
+ return 0
86
+ print("✗ 未解析到 canonical 脚本;用 QU_AIWEI_SCAN 指定或安装 qu-aiwei-zh",
87
+ file=sys.stderr)
88
+ return 3
453
89
 
454
- if args.output:
455
- Path(args.output).write_text(out, encoding="utf-8")
456
- print(f"✅ 已写入 {args.output}({report.score} 分 / {report.level})")
457
- else:
458
- print(out)
90
+ target = resolve()
91
+ if not target:
92
+ shown = [os.path.abspath(os.path.expanduser(c)) for c in CANDIDATES]
93
+ sys.stderr.write(MISSING.format(paths="\n".join(" " + p for p in shown)))
94
+ return 3
459
95
 
460
- return 0
96
+ os.execv(sys.executable, [sys.executable, target] + args)
97
+ return 0 # 不会到达
461
98
 
462
99
 
463
100
  if __name__ == "__main__":
464
- sys.exit(main(sys.argv[1:]))
101
+ sys.exit(main())
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: topmind-weread
3
- version: 4.14.0
3
+ version: 4.15.0
4
4
  description: >-
5
5
  同步微信读书划线/想法/统计到专题。Use when 微信读书、划线同步、weread、读书笔记。
6
6
  Do NOT use for 普通 URL 捕获、仅整理、出稿、X.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: topmind-write
3
- version: 4.14.0
3
+ version: 4.15.0
4
4
  description: >-
5
5
  写作/润色/出稿到专题或交付层(role:delivery)。Use when 写、起草、续写、润色、出稿、交付、write、draft、deliver。
6
6
  Do NOT use for 仅捕获、仅结构整理、只写记忆、doctor/loop、社交连接器.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: topmind-x
3
- version: 4.14.0
3
+ version: 4.15.0
4
4
  description: >-
5
5
  X/Twitter:归档/搜索(Bearer)、发帖(xurl)、Agent 可用官方 MCP。Use when 发推、推特、twitter、x.com。
6
6
  Do NOT use for 微信读书、非 X 捕获、doctor、长文交付.