@topmindspace/topmind-skills 4.15.1 → 4.15.2

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 (50) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/INSTALL.md +10 -2
  3. package/README.en.md +18 -14
  4. package/README.md +19 -15
  5. package/README.zh-CN.md +19 -15
  6. package/bin/install-skills.mjs +48 -8
  7. package/evals/evals.json +85 -3
  8. package/install-targets/claude-code.json +1 -2
  9. package/install-targets/codex.json +1 -2
  10. package/install-targets/hermes.json +1 -2
  11. package/install-targets/mimocode.json +1 -2
  12. package/install-targets/opencode.json +1 -2
  13. package/install-targets/universal.json +1 -2
  14. package/package.json +3 -3
  15. package/shared/host-loading.md +3 -3
  16. package/shared/trigger-disambiguation.md +14 -2
  17. package/skills.md +1 -1
  18. package/topmind/SKILL.md +18 -23
  19. package/topmind-capture/SKILL.md +11 -34
  20. package/topmind-ledger/SKILL.md +11 -18
  21. package/topmind-loop/SKILL.md +11 -23
  22. package/topmind-maintain/SKILL.md +11 -24
  23. package/topmind-memory/SKILL.md +11 -21
  24. package/topmind-organize/SKILL.md +11 -35
  25. package/topmind-pack.json +48 -28
  26. package/topmind-weread/SKILL.md +11 -18
  27. package/topmind-write/SKILL.md +15 -32
  28. package/topmind-x/SKILL.md +13 -19
  29. package/topmind-wechat/DESIGN.md +0 -50
  30. package/topmind-wechat/SKILL.md +0 -229
  31. package/topmind-wechat/assets/themes/amber-review.json +0 -35
  32. package/topmind-wechat/assets/themes/graphite.json +0 -35
  33. package/topmind-wechat/assets/themes/minimal-ink.json +0 -35
  34. package/topmind-wechat/assets/themes/newsprint.json +0 -35
  35. package/topmind-wechat/assets/themes/tech-blue.json +0 -35
  36. package/topmind-wechat/references/element-spec.md +0 -162
  37. package/topmind-wechat/references/known-pits.md +0 -86
  38. package/topmind-wechat/references/theme-map.md +0 -139
  39. package/topmind-wechat/references/typography-rules.md +0 -62
  40. package/topmind-wechat/references/wechat-constraints.md +0 -106
  41. package/topmind-wechat/references/workflow.md +0 -113
  42. package/topmind-wechat/references/writing-quality.md +0 -137
  43. package/topmind-wechat/scripts/audit-provenance.py +0 -366
  44. package/topmind-wechat/scripts/lint-wechat.py +0 -446
  45. package/topmind-wechat/scripts/md2wechat.py +0 -1599
  46. package/topmind-wechat/scripts/new-article.py +0 -183
  47. package/topmind-wechat/scripts/push-to-topstream.py +0 -226
  48. package/topmind-wechat/scripts/scan_ai_flavor.py +0 -101
  49. package/topmind-wechat/scripts/sync-mapping.py +0 -220
  50. package/topmind-wechat/scripts/sync-status.py +0 -187
@@ -1,183 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """new-article.py — 一键创建公众号文章交付包。
4
-
5
- 创建 <base>/YYYY-MM-DD-<slug>/ 目录,内含:
6
- - 公众号稿.md 带 frontmatter 的文章骨架(含写作提示与容器语法示例)
7
- - images/ 图片目录(可选,从 --images 拷贝)
8
- - diagrams/ mermaid 图表目录
9
-
10
- Usage:
11
- python3 new-article.py --slug gpt-6-review --title "GPT-6 深度评测" \
12
- [--base <专题目录>] [--date 2026-09-04] [--images src1 src2 ...]
13
-
14
- 默认 base = <topmind-workspace>/40-创作/2026-公众号
15
- """
16
-
17
- import argparse
18
- import datetime
19
- import os
20
- import re
21
- import shutil
22
- import sys
23
-
24
- def _default_base():
25
- """交付包根:CLI --base → env TOPMIND_WECHAT_BASE → env TOPMIND_WORKSPACE 发现 → 惯例路径。"""
26
- env = os.environ.get("TOPMIND_WECHAT_BASE") or os.environ.get("TOPMIND_WECHAT_PACKAGE_ROOT")
27
- if env:
28
- return os.path.abspath(os.path.expanduser(env))
29
- ws = os.environ.get("TOPMIND_WORKSPACE")
30
- if ws:
31
- for rel in (
32
- ("40-创作", "2026-公众号"),
33
- ("20-专题", "2026-公众号"),
34
- ("88-交付", "2026-公众号"),
35
- ):
36
- p = os.path.join(ws, *rel)
37
- if os.path.isdir(p):
38
- return p
39
- return os.path.join(ws, "40-创作", "2026-公众号")
40
- # 惯例:父工作区(可被 CLI 覆盖)
41
- return os.path.join(
42
- os.path.expanduser("~"), "TopWorkSpace", "topmind-workspace",
43
- "40-创作", "2026-公众号")
44
-
45
-
46
- def _default_topstream():
47
- env = os.environ.get("TOPSTREAM_ROOT") or os.environ.get("TOPMIND_TOPSTREAM")
48
- if env:
49
- return os.path.abspath(os.path.expanduser(env))
50
- return os.path.join(os.path.expanduser("~"), "TopWorkSpace", "topstream")
51
-
52
- DEFAULT_BASE = _default_base()
53
-
54
- SKELETON = """---
55
- title: "{title}"
56
- category: 40-创作
57
- topic: 2026-公众号
58
- source_type: {source_type}
59
- captured_at: {now}
60
- status: 草稿
61
- direction: {direction}
62
- source_file: {source_file}
63
- target_file: {target_file}
64
- word_count: 0
65
- tags: [公众号, 排版]
66
- note_role: bundle
67
- ---
68
-
69
- # {title}
70
-
71
- <!-- 骨架说明(写作时逐条删除):
72
- 1. 开头:3 句内给出「反差钩子」——一个具体事实/数字/场景,别写背景铺垫。
73
- 2. 小标题用 ##(h2),正文段落 ≤ 110 字(约 4 行手机屏)。
74
- 3. 重点句才加粗,加粗占比 < 20%;关键数据可用 ::: stat 容器。
75
- 4. 外链会被公众号拦截,直接写 [文字](url),脚本自动转文末脚注。
76
- 5. 中英文间距不用管,lint --fix 会自动补。
77
- 6. 可用容器(::: 类型 然后空行,内容,空行,::: 收尾):
78
- ::: note 补充说明(灰)
79
- ::: tip 小技巧(绿)
80
- ::: warn 注意事项(橙)
81
- ::: danger 常见坑(红)
82
- ::: pull 金句 / 核心观点(大字号居中)
83
- ::: dialogue 对话(A: B: 交替缩进)
84
- ::: stat 数据亮点(大数字)
85
- [!新特性] 这样的行内徽章 / ==高亮重点==
86
- -->
87
-
88
- > 一句话导语:这篇文章解决什么问题、读者能带走什么。
89
-
90
- ## 为什么值得聊
91
-
92
- ## 核心内容
93
-
94
- ### 分论点一
95
-
96
- ### 分论点二
97
-
98
- ## 我的看法
99
-
100
- ::: pull
101
- 把最想让读者记住的一句话放在这里。
102
- :::
103
-
104
- ## 小结
105
-
106
- ---
107
-
108
- *本文首发于我的公众号,转载请注明出处。*
109
- """
110
-
111
- # 目录名按专题规范:中文短名(可含英文/数字/连字符),如 个人免费基建全景、GrokBot多Agent调度
112
- SLUG_RE = re.compile(r"^[\u4e00-\u9fffA-Za-z0-9][\u4e00-\u9fffA-Za-z0-9-]*$")
113
-
114
-
115
- def main():
116
- ap = argparse.ArgumentParser()
117
- ap.add_argument("--slug", required=True,
118
- help="包名关键词(中文短名,可含英文/数字/连字符),如 个人免费基建全景、GrokBot多Agent调度")
119
- ap.add_argument("--title", required=True, help="文章标题")
120
- ap.add_argument("--base", default=DEFAULT_BASE)
121
- ap.add_argument("--date", default=datetime.date.today().isoformat())
122
- ap.add_argument("--direction", default="reverse", choices=("forward", "reverse"),
123
- help="forward=源自 topstream 底稿;reverse=本工作区选题,定稿后回推 topstream")
124
- ap.add_argument("--source-file", default="",
125
- help='forward 时填 topstream 的 notes/xxx.md')
126
- ap.add_argument("--images", nargs="*", default=[],
127
- help="要拷入 images/ 的源图片路径")
128
- args = ap.parse_args()
129
-
130
- if not SLUG_RE.match(args.slug):
131
- print("✗ 目录名只允许中文、英文字母、数字、连字符:%s" % args.slug)
132
- return 1
133
-
134
- pkg = os.path.join(args.base, "%s-%s" % (args.date, args.slug))
135
- if os.path.exists(pkg):
136
- print("✗ 目录已存在:%s" % pkg)
137
- return 1
138
-
139
- os.makedirs(os.path.join(pkg, "images"), exist_ok=True)
140
- os.makedirs(os.path.join(pkg, "diagrams"), exist_ok=True)
141
-
142
- draft = os.path.join(pkg, "公众号稿.md")
143
- if args.direction == "forward" and not args.source_file:
144
- print("⚠ forward 未给 --source-file,将留空 source_file(回补 notes/xxx.md)")
145
- src_file = args.source_file or '""'
146
- tgt_file = '""' if args.direction == "forward" else "pending"
147
- with open(draft, "w", encoding="utf-8") as fh:
148
- fh.write(SKELETON.format(
149
- title=args.title,
150
- source_type="user-original" if args.direction == "reverse" else "ai-derived",
151
- direction=args.direction,
152
- source_file=src_file,
153
- target_file=tgt_file,
154
- now=datetime.datetime.now().astimezone().isoformat(timespec="seconds")))
155
-
156
- copied = 0
157
- for src in args.images:
158
- if os.path.exists(src):
159
- shutil.copy2(src, os.path.join(pkg, "images", os.path.basename(src)))
160
- copied += 1
161
- else:
162
- print("! 跳过不存在的图片:%s" % src)
163
-
164
- print("✓ 交付包已创建:%s" % pkg)
165
- print(" 稿件骨架 : 公众号稿.md(含容器语法示例,写作时删掉注释)")
166
- if copied:
167
- print(" 图片 : %d 张 → images/" % copied)
168
- print("")
169
- print("下一步:")
170
- print(" 1. 写完稿后体检(可加 --fix 自动补中英文间距):")
171
- print(" python3 %s/lint-wechat.py --input '%s' --fix" % (
172
- os.path.dirname(os.path.abspath(__file__)), draft))
173
- print(" 2. 定稿时同步状态与目录名(加 --apply 生效):")
174
- print(" python3 %s/sync-status.py --set 定稿 '%s' --apply" % (
175
- os.path.dirname(os.path.abspath(__file__)), os.path.basename(pkg)))
176
- print(" 3. 生成公众号 HTML:")
177
- print(" python3 %s/md2wechat.py --input '%s' --out-dir '%s' --slug %s" % (
178
- os.path.dirname(os.path.abspath(__file__)), draft, pkg, args.slug))
179
- return 0
180
-
181
-
182
- if __name__ == "__main__":
183
- sys.exit(main())
@@ -1,226 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """push-to-topstream.py — 反向回推:公众号稿 → topstream/notes 纯 Markdown。
4
-
5
- 反向链路的最后一公里最容易出错:`::: 容器`、`==高亮==`、`[!徽章]` 等公众号专用语法
6
- 绝不能写进 notes/(仓库原文保持纯 Markdown)。本脚本做「降级转换」并生成 README 索引条目。
7
-
8
- 降级规则:
9
- - 剥离 frontmatter
10
- - `:::` 围栏行删除,容器内文本原样保留
11
- - `==高亮==` → 去掉 `==`
12
- - `[!徽章]` / `[!xxx]` → 整段删除
13
- - `[文字](url)` 外链保留(GitHub markdown 支持)
14
- - 中英文盘古之白保留(对 notes 同样适用)
15
-
16
- Usage:
17
- python3 push-to-topstream.py <包目录> [--target notes/xxx.md] [--apply]
18
- # 默认 dry-run:打印降级预览 + 语法统计 + README 索引条目,不写盘
19
- """
20
-
21
- import argparse
22
- import os
23
- import re
24
- import shutil
25
- import sys
26
-
27
- DEFAULT_BASE = os.path.join(
28
- os.path.expanduser("~"), "TopWorkSpace", "topmind-workspace",
29
- "40-创作", "2026-公众号")
30
- DEFAULT_TOPSTREAM = os.path.join(os.path.expanduser("~"), "TopWorkSpace", "topstream")
31
-
32
- CONTAINER_RE = re.compile(r"^:::\s*\w*\s*$")
33
- HIGHLIGHT_RE = re.compile(r"==([^=]+)==")
34
- BADGE_RE = re.compile(r"\[![^\]]*\]")
35
-
36
-
37
- def split_fm(text):
38
- if not text.startswith("---\n"):
39
- return None, text
40
- i = text.find("\n---", 3)
41
- if i < 0:
42
- return None, text
43
- return text[4:i], text[i + 4:].lstrip("\n")
44
-
45
-
46
- def get_field(fm, key, default=""):
47
- if not fm:
48
- return default
49
- m = re.search(r"^%s:\s*(.*)$" % re.escape(key), fm, re.M)
50
- if not m:
51
- return default
52
- v = m.group(1).strip()
53
- return v.strip('"').strip("'") if v else default
54
-
55
-
56
- def downgrade(body):
57
- """公众号稿正文 → notes 纯 Markdown。返回 (文本, 统计)"""
58
- stats = {"container": 0, "highlight": 0, "badge": 0}
59
- out = []
60
- for line in body.split("\n"):
61
- if CONTAINER_RE.match(line.strip()):
62
- stats["container"] += 1
63
- continue
64
- if BADGE_RE.search(line):
65
- stats["badge"] += len(BADGE_RE.findall(line))
66
- line = BADGE_RE.sub("", line).strip()
67
- if not line:
68
- continue
69
- line, n = HIGHLIGHT_RE.subn(r"\1", line)
70
- stats["highlight"] += n
71
- out.append(line)
72
- return "\n".join(out).strip("\n") + "\n", stats
73
-
74
-
75
- IMG_REF_RE = re.compile(r"(!\[[^\]]*\]\()(images/[^)\s]+)(\))")
76
-
77
-
78
- def push_assets(body_md, pkg_path, topstream, note_target, name_map, dry=False):
79
- """把正文引用的本地图片搬进 topstream 并改写为仓库约定路径。
80
-
81
- `notes/xxx.md` 里写 `images/xx.png` 在 GitHub 上是死链——仓库没有 `notes/images/`。
82
- 仓库既有约定:图放 `assets/images/<note-slug>/`,正文用 `../assets/images/<slug>/xx.png`。
83
- 返回 (新正文, 搬移明细, 未找到的源图)。
84
- """
85
- slug = os.path.splitext(os.path.basename(note_target))[0]
86
- dst_dir = os.path.join(topstream, "assets", "images", slug)
87
- moved, missing = [], []
88
-
89
- def repl(m):
90
- src = m.group(2)
91
- base = os.path.basename(src)
92
- src_path = os.path.join(pkg_path, src)
93
- if not os.path.exists(src_path):
94
- missing.append(src)
95
- return m.group(0)
96
- new_name = name_map.get(base, base)
97
- if not dry: # dry-run 只看映射,不落盘
98
- os.makedirs(dst_dir, exist_ok=True)
99
- shutil.copy2(src_path, os.path.join(dst_dir, new_name))
100
- moved.append((base, new_name))
101
- return m.group(1) + "../assets/images/%s/%s" % (slug, new_name) + m.group(3)
102
-
103
- return IMG_REF_RE.sub(repl, body_md), moved, missing
104
-
105
-
106
- def make_readme_entry(title, target, tags):
107
- tag_str = " ".join("`%s`" % t for t in tags)
108
- return (
109
- "- [**%s**](%s) \n"
110
- " 描述:<一句话介绍这篇文章解决了什么问题、读者能带走什么> \n"
111
- " %s\n" % (title, target, tag_str)
112
- )
113
-
114
-
115
- def main():
116
- ap = argparse.ArgumentParser()
117
- ap.add_argument("pkg", help="包目录名(相对 base)或绝对路径")
118
- ap.add_argument("--base", default=DEFAULT_BASE)
119
- ap.add_argument("--topstream", default=DEFAULT_TOPSTREAM)
120
- ap.add_argument("--target", help="回推目标 notes/xxx.md(缺省读 frontmatter target_file)")
121
- ap.add_argument("--apply", action="store_true", help="实际写盘(默认只预览)")
122
- ap.add_argument("--force", action="store_true", help="目标已存在时覆盖")
123
- ap.add_argument("--assets", action="store_true",
124
- help="把正文图片搬进 <topstream>/assets/images/<note-slug>/ 并改写引用(GitHub 必需)")
125
- ap.add_argument("--asset-slug", default="", help="覆盖资源子目录名(默认取 note 文件名)")
126
- ap.add_argument("--asset-names", default="",
127
- help='文件名重命名映射,如 "00-封面.jpg=01-cover.jpg,01-分工.png=02-division-of-labor.png"')
128
- args = ap.parse_args()
129
-
130
- pkg_path = args.pkg if os.path.isabs(args.pkg) else os.path.join(args.base, args.pkg)
131
- draft = os.path.join(pkg_path, "公众号稿.md")
132
- if not os.path.exists(draft):
133
- print("✗ 找不到公众号稿:%s" % draft)
134
- return 1
135
-
136
- text = open(draft, encoding="utf-8").read()
137
- fm, body = split_fm(text)
138
- title = get_field(fm, "title") or get_field(fm, "title", "")
139
- status = get_field(fm, "status", "草稿")
140
- target_file = get_field(fm, "target_file", "")
141
- tags = []
142
- mt = re.search(r"^tags:\s*\[(.*)\]$", fm or "", re.M)
143
- if mt:
144
- tags = [t.strip().strip('"').strip("'") for t in mt.group(1).split(",") if t.strip()]
145
- tags = [t for t in tags if t not in ("公众号", "排版")]
146
-
147
- target = args.target or (target_file if target_file != "pending" else "")
148
- if not target:
149
- print("✗ 需要回推目标:--target notes/xxx.md(frontmatter target_file 仍为 pending)")
150
- return 1
151
- if not target.startswith("notes/"):
152
- print("✗ target 应以 notes/ 开头:%s" % target)
153
- return 1
154
-
155
- body_md, stats = downgrade(body)
156
-
157
- name_map = {}
158
- for pair in args.asset_names.split(","):
159
- if "=" in pair:
160
- k, v = pair.split("=", 1)
161
- name_map[k.strip()] = v.strip()
162
-
163
- if args.assets:
164
- if args.asset_slug:
165
- target = os.path.join(os.path.dirname(target), args.asset_slug + ".md")
166
- body_md, moved, missing = push_assets(
167
- body_md, pkg_path, args.topstream, target, name_map,
168
- dry=not args.apply)
169
- else:
170
- moved, missing = [], []
171
-
172
- # 确保有 H1 标题开头
173
- if not body_md.lstrip().startswith("# "):
174
- body_md = "# %s\n\n%s" % (title, body_md)
175
-
176
- out_path = os.path.join(args.topstream, target)
177
-
178
- print("回推预览:%s" % os.path.basename(pkg_path))
179
- print(" 标题 : %s" % title)
180
- print(" 状态 : %s" % status)
181
- print(" 目标 : %s" % target)
182
- print(" 降级统计 : 容器 %d · 高亮 %d · 徽章 %d"
183
- % (stats["container"], stats["highlight"], stats["badge"]))
184
- if status != "定稿":
185
- print(" ⚠ status=%s,回推前应先定稿(sync-status.py --set 定稿)" % status)
186
- print("")
187
- if moved or missing:
188
- print(" 图片搬运 : %d 张 → assets/images/%s/"
189
- % (len(moved), os.path.splitext(os.path.basename(target))[0]))
190
- for zh, en in moved:
191
- if zh != en:
192
- print(" %s → %s" % (zh, en))
193
- non_ascii = [en for _, en in moved if not en.isascii()]
194
- if non_ascii:
195
- print(" ⚠ 文件名含非 ASCII 字符:%s" % "、".join(non_ascii))
196
- print(" 仓库约定用 ASCII(--asset-names 传映射,如 \"00-封面.jpg=01-cover.jpg\")")
197
- if missing:
198
- print(" ✗ 源图缺失(引用仍是 images/,会 404):%s" % "、".join(missing))
199
- elif "images/" in body_md:
200
- print(" ⚠ 正文引用了本地图但未加 --assets:GitHub 上 images/ 是死链,请补 --assets")
201
- print("")
202
- print("—— README 索引条目(复制到 topstream/README.md 的「研析心得」段)——")
203
- print(make_readme_entry(title, target, tags))
204
-
205
- if args.apply:
206
- os.makedirs(os.path.dirname(out_path), exist_ok=True)
207
- if os.path.exists(out_path) and not args.force:
208
- print("✗ 目标已存在,加 --force 覆盖:%s" % out_path)
209
- return 1
210
- with open(out_path, "w", encoding="utf-8") as fh:
211
- fh.write(body_md)
212
- print("")
213
- print("✓ 已写入 %s(%d 字节)" % (out_path, len(body_md.encode("utf-8"))))
214
- print(" 下一步:把上面的 README 条目插进 topstream/README.md;")
215
- print(" (图片已按上面明细落 assets/,正文引用已改写为 ../assets/...)")
216
- print(" 然后回本包改 frontmatter:target_file 从 pending 改为 %s" % target)
217
- print(" (可用:sync-status.py 之外手动改 target_file 字段,或用编辑器)")
218
- else:
219
- print("")
220
- print("[dry-run] 加 --apply 实际写盘。写盘不会自动改 README 与 frontmatter,")
221
- print(" 这两步由你/agent 手工完成,保证索引不漂移。")
222
- return 0
223
-
224
-
225
- if __name__ == "__main__":
226
- sys.exit(main())
@@ -1,101 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """scan_ai_flavor.py — 转发器:调用 qu-aiwei-zh 的 canonical 检测脚本。
4
-
5
- **本文件不含检测实现。** 原因见下。
6
-
7
- ## 为什么是转发器而不是自带副本
8
-
9
- topmind-wechat 曾自带一份 463 行的实现副本,它是 `qu-aiwei-zh` 在加入「E 类 · 作者姿态层」
10
- **之前**的旧快照 —— 不输出姿态参考分,也识别不了姿态类 AI 痕迹。照 SKILL.md 跑它,会拿到一个
11
- **没有姿态维度的「100 分」**:看着干净,其实少了一整个维度。
12
-
13
- 两版实测差异:旧副本 0 处「姿态」相关代码,canonical 20 处。
14
-
15
- 副本必然再次漂移(一边改了另一边不知道),所以这里只保留入口,实现归 canonical 一处。
16
-
17
- ## canonical 查找顺序(命中即用)
18
-
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`
24
-
25
- ## 行为
26
-
27
- - 参数原样透传(位置参数 `<file>` / `--json` / `-o` / `-` 管道都可用),stdin / stdout / stderr 继承
28
- - 退出码取 canonical 的退出码
29
- - `--which` 只打印解析到的 canonical 路径并退出 0(转发器自有开关,canonical 无此参数)
30
- - **找不到 canonical 时报错退出(exit 3),不静默降级** —— 宁可这一步跑不了,
31
- 也不要给出一个没有姿态分的分数
32
-
33
- ## 用法
34
-
35
- python3 scripts/scan_ai_flavor.py <包>/公众号稿.md # 目标 ≥85
36
- python3 scripts/scan_ai_flavor.py --which
37
- """
38
-
39
- from __future__ import annotations
40
-
41
- import os
42
- import sys
43
-
44
- REL = os.path.join("qu-aiwei-zh", "scripts", "scan_ai_flavor.py")
45
-
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),
51
- ]
52
-
53
- MISSING = """\
54
- ✗ 找不到 qu-aiwei-zh 的 canonical 检测脚本,无法执行「文字关」。
55
-
56
- 本目录不再自带实现 —— 曾经那份是 E 类姿态检测之前的旧快照,会漏掉姿态维度。
57
- 已查找:
58
- {paths}
59
-
60
- 解决:安装 qu-aiwei-zh 技能,或用环境变量显式指定:
61
- export QU_AIWEI_SCAN=/path/to/qu-aiwei-zh/scripts/scan_ai_flavor.py
62
- """
63
-
64
-
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
76
-
77
-
78
- def main():
79
- args = sys.argv[1:]
80
-
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
89
-
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
95
-
96
- os.execv(sys.executable, [sys.executable, target] + args)
97
- return 0 # 不会到达
98
-
99
-
100
- if __name__ == "__main__":
101
- sys.exit(main())