huhaa-myskills 0.2.13 → 0.3.4

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 (121) hide show
  1. package/README.md +9 -375
  2. package/bin/huhaa-myskills.mjs +194 -12
  3. package/{scripts → build}/verify.mjs +3 -1
  4. package/docs/Frontend-Spec.md +1149 -0
  5. package/docs/assets/layout-wireframe.png +0 -0
  6. package/docs/assets/theme-reference.png +0 -0
  7. package/docs/scan_rules.md +172 -0
  8. package/package.json +33 -9
  9. package/packages/scanner/src/adapters/directory-skill.mjs +239 -0
  10. package/packages/scanner/src/adapters/skill-adapter.mjs +345 -0
  11. package/packages/scanner/src/icon/brand-map.mjs +84 -0
  12. package/packages/scanner/src/icon/icon-extractor.mjs +246 -0
  13. package/packages/scanner/src/index.mjs +107 -82
  14. package/packages/scanner/src/types.d.ts +18 -0
  15. package/packages/scanner/test/icon.test.mjs +61 -0
  16. package/packages/scanner/test/scanner.test.mjs +37 -41
  17. package/packages/scanner/test/skill-adapter.test.mjs +70 -0
  18. package/packages/scanner/tests/directory-skill.test.mjs +93 -0
  19. package/packages/scanner/tests/scanner-integration.test.mjs +139 -0
  20. package/packages/server/src/index.mjs +164 -12
  21. package/packages/server/src/labels.mjs +62 -0
  22. package/packages/server/test/server.test.mjs +1 -1
  23. package/packages/web/README.md +33 -2
  24. package/packages/web/dist/assets/index-BCY2cRaO.js +175 -0
  25. package/packages/web/dist/assets/index-DALKjJPi.css +1 -0
  26. package/packages/web/dist/favicon-16x16.png +0 -0
  27. package/packages/web/dist/favicon-192x192.png +0 -0
  28. package/packages/web/dist/favicon-32x32.png +0 -0
  29. package/packages/web/dist/favicon-512x512.png +0 -0
  30. package/packages/web/dist/favicon-source.svg +5 -0
  31. package/packages/web/dist/index.html +23 -9
  32. package/packages/web/index.html +22 -8
  33. package/packages/web/package-lock.json +2989 -0
  34. package/packages/web/package.json +21 -8
  35. package/packages/web/postcss.config.js +6 -0
  36. package/packages/web/public/favicon-16x16.png +0 -0
  37. package/packages/web/public/favicon-192x192.png +0 -0
  38. package/packages/web/public/favicon-32x32.png +0 -0
  39. package/packages/web/public/favicon-512x512.png +0 -0
  40. package/packages/web/public/favicon-source.svg +5 -0
  41. package/packages/web/src/App.test.ts +40 -0
  42. package/packages/web/src/App.tsx +177 -0
  43. package/packages/web/src/components/ComingSoon.tsx +14 -0
  44. package/packages/web/src/components/layout/Sidebar.test.tsx +74 -0
  45. package/packages/web/src/components/layout/Sidebar.tsx +114 -0
  46. package/packages/web/src/components/layout/Topbar.tsx +74 -0
  47. package/packages/web/src/components/ui/ActionButton.tsx +48 -0
  48. package/packages/web/src/components/ui/button.tsx +48 -0
  49. package/packages/web/src/components/ui/card.tsx +45 -0
  50. package/packages/web/src/components/views/DashboardView.tsx +122 -0
  51. package/packages/web/src/components/views/OtherSkillsView.test.tsx +126 -0
  52. package/packages/web/src/components/views/OtherSkillsView.tsx +306 -0
  53. package/packages/web/src/components/views/SettingsView.tsx +95 -0
  54. package/packages/web/src/components/views/SkillDetail.test.tsx +57 -0
  55. package/packages/web/src/components/views/SkillDetail.tsx +81 -0
  56. package/packages/web/src/components/views/SkillsView.test.ts +28 -0
  57. package/packages/web/src/components/views/SkillsView.tsx +226 -0
  58. package/packages/web/src/hooks/QUICK_REFERENCE.md +165 -0
  59. package/packages/web/src/hooks/README.md +378 -0
  60. package/packages/web/src/hooks/__tests__/useOtherSkills.test.ts +422 -0
  61. package/packages/web/src/hooks/__tests__/useSkillIcons.test.ts +272 -0
  62. package/packages/web/src/hooks/useLiveReload.ts +20 -0
  63. package/packages/web/src/hooks/useOtherSkills.ts +387 -0
  64. package/packages/web/src/hooks/useSkillIcons.ts +248 -0
  65. package/packages/web/src/hooks/useTheme.ts +36 -0
  66. package/packages/web/src/index.css +161 -0
  67. package/packages/web/src/lib/api.test.ts +47 -0
  68. package/packages/web/src/lib/api.ts +77 -0
  69. package/packages/web/src/lib/cn.ts +7 -0
  70. package/packages/web/src/lib/editors.test.ts +30 -0
  71. package/packages/web/src/lib/editors.ts +70 -0
  72. package/packages/web/src/lib/markdown.test.ts +27 -0
  73. package/packages/web/src/lib/markdown.ts +14 -0
  74. package/packages/web/src/main.tsx +13 -0
  75. package/packages/web/src/types/index.ts +62 -0
  76. package/packages/web/src/types/other-skill.ts +88 -0
  77. package/packages/web/src/types/skill.ts +136 -0
  78. package/packages/web/src/types.ts +47 -0
  79. package/packages/web/src/vite-env.d.ts +1 -0
  80. package/packages/web/tailwind.config.ts +66 -0
  81. package/packages/web/tsconfig.json +28 -0
  82. package/packages/web/vite.config.ts +36 -0
  83. package/packages/web/vitest.config.ts +21 -0
  84. package/packages/web/vitest.setup.ts +1 -0
  85. package/scripts/activate-data-sources.sh +223 -0
  86. package/scripts/brand-completion.py +586 -0
  87. package/scripts/init-nextjs-project.sh +421 -0
  88. package/scripts/install-and-sync.sh +209 -0
  89. package/scripts/start-dev.sh +72 -0
  90. package/docs/GUIDE.md +0 -190
  91. package/docs/PLAN.md +0 -359
  92. package/docs/RULES.md +0 -329
  93. package/docs/RUNBOOK-myskills.md +0 -258
  94. package/docs/SYNC-SKILLS.md +0 -259
  95. package/docs/releases/README.md +0 -47
  96. package/docs/releases/v0.1.0.md +0 -256
  97. package/docs/releases/v0.1.1.md +0 -297
  98. package/docs/releases/v0.1.2.md +0 -207
  99. package/docs/releases/v0.1.3.md +0 -269
  100. package/docs/todo/v0.1.2.md +0 -98
  101. package/docs/todo/v0.1.3.md +0 -40
  102. package/docs/todo/v0.1.4.md +0 -36
  103. package/docs/todo/v0.1.5.md +0 -41
  104. package/docs/todo/v0.1.6.md +0 -43
  105. package/docs/todo/v0.1.7.md +0 -38
  106. package/docs/todo/v0.1.8.md +0 -36
  107. package/docs/todo/v0.1.9.md +0 -54
  108. package/packages/web/dist/assets/index-BPepSb3q.js +0 -36
  109. package/packages/web/dist/assets/index-CIGkgT5w.css +0 -1
  110. package/packages/web/src/App.vue +0 -483
  111. package/packages/web/src/components/AppTree.vue +0 -230
  112. package/packages/web/src/components/DirectoryTree.vue +0 -263
  113. package/packages/web/src/components/SkillTree.vue +0 -208
  114. package/packages/web/src/lib/api.js +0 -43
  115. package/packages/web/src/main.js +0 -6
  116. package/packages/web/src/stores/i18n.js +0 -161
  117. package/packages/web/src/stores/skills.js +0 -287
  118. package/packages/web/src/styles.css +0 -365
  119. package/packages/web/vite.config.js +0 -17
  120. /package/{scripts → build}/build-web.mjs +0 -0
  121. /package/{scripts → build}/prepare-publish.mjs +0 -0
@@ -0,0 +1,586 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 品牌自动识别和补全脚本 (v2 - 增强版)
4
+ 扫描所有 Hermes 技能,基于技能名称、描述和类别自动识别品牌
5
+ """
6
+
7
+ import os
8
+ import re
9
+ import json
10
+ import sys
11
+ from pathlib import Path
12
+ from collections import defaultdict
13
+ from datetime import datetime
14
+
15
+ # 增强的品牌识别规则
16
+ BRAND_RULES = {
17
+ 'GitHub': {
18
+ 'patterns': [r'\bgithub\b', r'git-hub', r'gh-action', r'release-automation', r'github-pr'],
19
+ 'aliases': ['github']
20
+ },
21
+ 'Docker': {
22
+ 'patterns': [r'\bdocker\b', r'docker-compose', r'dockerfile', r'containerization'],
23
+ 'aliases': ['docker']
24
+ },
25
+ 'Kubernetes': {
26
+ 'patterns': [r'\bk8s\b', r'kubernetes', r'kubes', r'helm', r'k8s-private'],
27
+ 'aliases': ['kubernetes', 'k8s']
28
+ },
29
+ 'AWS': {
30
+ 'patterns': [r'\baws\b', r'amazon', r'ec2', r's3', r'lambda', r'cloudformation'],
31
+ 'aliases': ['aws']
32
+ },
33
+ 'Linux': {
34
+ 'patterns': [r'\blinux\b', r'ubuntu', r'debian', r'centos', r'rhel', r'fedora', r'gnu/linux', r'bash', r'systemd'],
35
+ 'aliases': ['linux', 'ubuntu', 'debian', 'centos']
36
+ },
37
+ 'Python': {
38
+ 'patterns': [r'\bpython\b', r'py\d', r'\bpy\b', r'django', r'flask', r'fastapi', r'pip', r'peft', r'jupyter'],
39
+ 'aliases': ['python', 'py']
40
+ },
41
+ 'Node.js': {
42
+ 'patterns': [r'node\.?js', r'nodejs', r'npm\b', r'express', r'vite'],
43
+ 'aliases': ['nodejs', 'node', 'npm']
44
+ },
45
+ 'Go': {
46
+ 'patterns': [r'\bgo\b(?!ogle)', r'golang', r'goroutine'],
47
+ 'aliases': ['go', 'golang']
48
+ },
49
+ 'Rust': {
50
+ 'patterns': [r'(\brust\b|cargo|\.rs\b)(?![a-z])'],
51
+ 'aliases': ['rust', 'cargo']
52
+ },
53
+ 'Java': {
54
+ 'patterns': [r'\bjava\b', r'jvm', r'spring', r'maven'],
55
+ 'aliases': ['java', 'jvm']
56
+ },
57
+ 'OpenAI': {
58
+ 'patterns': [r'\bopenai\b', r'chatgpt', r'gpt-\d', r'davinci', r'whisper'],
59
+ 'aliases': ['openai', 'gpt', 'chatgpt', 'whisper']
60
+ },
61
+ 'Anthropic': {
62
+ 'patterns': [r'\banthrop\w+', r'\bclaude\b', r'custom-anthropic'],
63
+ 'aliases': ['anthropic', 'claude']
64
+ },
65
+ 'Google': {
66
+ 'patterns': [r'\bgoogle\b', r'gemini', r'bard', r'vertex', r'colab', r'youtube'],
67
+ 'aliases': ['google', 'gemini']
68
+ },
69
+ 'Meta': {
70
+ 'patterns': [r'\bmeta\b', r'\bllama\b', r'facebook', r'llama-cpp', r'gguf'],
71
+ 'aliases': ['meta', 'llama']
72
+ },
73
+ 'TensorFlow': {
74
+ 'patterns': [r'tensorflow', r'keras\b'],
75
+ 'aliases': ['tensorflow']
76
+ },
77
+ 'PyTorch': {
78
+ 'patterns': [r'pytorch', r'\btorch\b', r'fsdp'],
79
+ 'aliases': ['pytorch', 'torch']
80
+ },
81
+ 'Apple': {
82
+ 'patterns': [r'\bapple\b', r'macos', r'ios\b', r'swift', r'xcode', r'reminders', r'imessage', r'findmy'],
83
+ 'aliases': ['apple', 'ios', 'macos', 'swift']
84
+ },
85
+ 'React': {
86
+ 'patterns': [r'\breact\b', r'reactjs', r'jsx', r'next\.js', r'react-router', r'react-multi'],
87
+ 'aliases': ['react', 'reactjs']
88
+ },
89
+ 'Vue': {
90
+ 'patterns': [r'\bvue\b', r'vuejs', r'nuxt', r'vue3-', r'vue-vite', r'pinia'],
91
+ 'aliases': ['vue', 'vuejs']
92
+ },
93
+ 'Svelte': {
94
+ 'patterns': [r'\bsvelte\b', r'astro-vercel'],
95
+ 'aliases': ['svelte']
96
+ },
97
+ 'TypeScript': {
98
+ 'patterns': [r'typescript', r'\.ts\b', r'tsc'],
99
+ 'aliases': ['typescript', 'ts']
100
+ },
101
+ 'Terraform': {
102
+ 'patterns': [r'terraform', r'\.tf\b'],
103
+ 'aliases': ['terraform']
104
+ },
105
+ 'Ansible': {
106
+ 'patterns': [r'\bansible\b'],
107
+ 'aliases': ['ansible']
108
+ },
109
+ 'Jenkins': {
110
+ 'patterns': [r'\bjenkins\b', r'ci/cd', r'continuous-integration'],
111
+ 'aliases': ['jenkins']
112
+ },
113
+ 'GitLab': {
114
+ 'patterns': [r'\bgitlab\b'],
115
+ 'aliases': ['gitlab']
116
+ },
117
+ 'Figma': {
118
+ 'patterns': [r'\bfigma\b'],
119
+ 'aliases': ['figma']
120
+ },
121
+ 'Slack': {
122
+ 'patterns': [r'\bslack\b'],
123
+ 'aliases': ['slack']
124
+ },
125
+ 'Notion': {
126
+ 'patterns': [r'\bnotion\b'],
127
+ 'aliases': ['notion']
128
+ },
129
+ 'Jira': {
130
+ 'patterns': [r'\bjira\b', r'atlassian'],
131
+ 'aliases': ['jira']
132
+ },
133
+ 'PostgreSQL': {
134
+ 'patterns': [r'postgres', r'postgresql', r'psql'],
135
+ 'aliases': ['postgres', 'postgresql']
136
+ },
137
+ 'MySQL': {
138
+ 'patterns': [r'\bmysql\b'],
139
+ 'aliases': ['mysql', 'sql']
140
+ },
141
+ 'MongoDB': {
142
+ 'patterns': [r'mongodb', r'mongo\b'],
143
+ 'aliases': ['mongodb', 'mongo']
144
+ },
145
+ 'Redis': {
146
+ 'patterns': [r'\bredis\b'],
147
+ 'aliases': ['redis']
148
+ },
149
+ 'Elasticsearch': {
150
+ 'patterns': [r'elasticsearch', r'elk'],
151
+ 'aliases': ['elasticsearch']
152
+ },
153
+ 'Prometheus': {
154
+ 'patterns': [r'prometheus'],
155
+ 'aliases': ['prometheus']
156
+ },
157
+ 'Grafana': {
158
+ 'patterns': [r'grafana'],
159
+ 'aliases': ['grafana']
160
+ },
161
+ 'Hermes': {
162
+ 'patterns': [r'\bhermes\b', r'hermes-agent', r'huhaa-myskills'],
163
+ 'aliases': ['hermes']
164
+ },
165
+ 'Hugging Face': {
166
+ 'patterns': [r'huggingface', r'hugging.face', r'huggingface-hub'],
167
+ 'aliases': ['huggingface']
168
+ },
169
+ 'ComfyUI': {
170
+ 'patterns': [r'comfyui', r'comfy'],
171
+ 'aliases': ['comfyui', 'comfy']
172
+ },
173
+ 'Suno': {
174
+ 'patterns': [r'suno\b', r'heartmula', r'music.generation', r'audiocraft'],
175
+ 'aliases': ['suno', 'heartmula']
176
+ },
177
+ 'Midjourney': {
178
+ 'patterns': [r'midjourney'],
179
+ 'aliases': ['midjourney']
180
+ },
181
+ 'Stable Diffusion': {
182
+ 'patterns': [r'stable.diffusion', r'diffusion(?!s)', r'axolotl'],
183
+ 'aliases': ['stable-diffusion']
184
+ },
185
+ 'dspy': {
186
+ 'patterns': [r'\bdspy\b'],
187
+ 'aliases': ['dspy']
188
+ },
189
+ 'LangChain': {
190
+ 'patterns': [r'langchain'],
191
+ 'aliases': ['langchain']
192
+ },
193
+ 'LlamaIndex': {
194
+ 'patterns': [r'llamaindex', r'gpt-index'],
195
+ 'aliases': ['llamaindex']
196
+ },
197
+ 'Modal': {
198
+ 'patterns': [r'\bmodal\b'],
199
+ 'aliases': ['modal']
200
+ },
201
+ 'Jupyter': {
202
+ 'patterns': [r'jupyter', r'ipython'],
203
+ 'aliases': ['jupyter']
204
+ },
205
+ 'Obsidian': {
206
+ 'patterns': [r'obsidian\b'],
207
+ 'aliases': ['obsidian']
208
+ },
209
+ 'Excalidraw': {
210
+ 'patterns': [r'excalidraw'],
211
+ 'aliases': ['excalidraw']
212
+ },
213
+ 'Manim': {
214
+ 'patterns': [r'manim\b'],
215
+ 'aliases': ['manim']
216
+ },
217
+ 'Blender': {
218
+ 'patterns': [r'blender'],
219
+ 'aliases': ['blender']
220
+ },
221
+ 'GIMP': {
222
+ 'patterns': [r'\bgimp\b'],
223
+ 'aliases': ['gimp']
224
+ },
225
+ 'Krita': {
226
+ 'patterns': [r'krita\b'],
227
+ 'aliases': ['krita']
228
+ },
229
+ 'P5.js': {
230
+ 'patterns': [r'p5js', r'p5\.js'],
231
+ 'aliases': ['p5js']
232
+ },
233
+ 'Unity': {
234
+ 'patterns': [r'\bunity\b', r'gamedev'],
235
+ 'aliases': ['unity']
236
+ },
237
+ 'Godot': {
238
+ 'patterns': [r'\bgodot\b'],
239
+ 'aliases': ['godot']
240
+ },
241
+ 'Unreal': {
242
+ 'patterns': [r'unreal\b', r'ue\d'],
243
+ 'aliases': ['unreal']
244
+ },
245
+ 'Minecraft': {
246
+ 'patterns': [r'minecraft', r'mcporter'],
247
+ 'aliases': ['minecraft']
248
+ },
249
+ 'OpenStreetMap': {
250
+ 'patterns': [r'openstreetmap', r'osrm', r'maps'],
251
+ 'aliases': ['osm', 'openstreetmap']
252
+ },
253
+ 'RSS': {
254
+ 'patterns': [r'rss\b', r'feed', r'atom\b', r'blogwatch'],
255
+ 'aliases': ['rss', 'feed']
256
+ },
257
+ 'Email': {
258
+ 'patterns': [r'\bemail\b', r'smtp\b', r'imap\b', r'himalaya', r'powerpoint'],
259
+ 'aliases': ['email', 'smtp']
260
+ },
261
+ 'Telegram': {
262
+ 'patterns': [r'telegram', r'dingtalk'],
263
+ 'aliases': ['telegram', 'dingtalk']
264
+ },
265
+ 'OpenHue': {
266
+ 'patterns': [r'hue\b', r'openhue'],
267
+ 'aliases': ['hue', 'openhue']
268
+ },
269
+ 'Airtable': {
270
+ 'patterns': [r'airtable'],
271
+ 'aliases': ['airtable']
272
+ },
273
+ 'TouchDesigner': {
274
+ 'patterns': [r'touchdesigner'],
275
+ 'aliases': ['touchdesigner']
276
+ },
277
+ 'Ollama': {
278
+ 'patterns': [r'ollama'],
279
+ 'aliases': ['ollama']
280
+ },
281
+ 'HuHaa': {
282
+ 'patterns': [r'huhaa', r'myskills'],
283
+ 'aliases': ['huhaa']
284
+ },
285
+ 'X': {
286
+ 'patterns': [r'xitter', r'xurl'],
287
+ 'aliases': ['twitter', 'x']
288
+ },
289
+ 'Discord': {
290
+ 'patterns': [r'discord', r'team.communication'],
291
+ 'aliases': ['discord']
292
+ },
293
+ 'Segment Anything': {
294
+ 'patterns': [r'segment.anything', r'sam\b'],
295
+ 'aliases': ['sam', 'segment-anything']
296
+ },
297
+ 'CLIP': {
298
+ 'patterns': [r'\bclip\b(?!py)'],
299
+ 'aliases': ['clip']
300
+ },
301
+ 'VLLM': {
302
+ 'patterns': [r'vllm'],
303
+ 'aliases': ['vllm']
304
+ },
305
+ 'TRL': {
306
+ 'patterns': [r'trl\b', r'trl-fine-tuning'],
307
+ 'aliases': ['trl']
308
+ },
309
+ 'PEFT': {
310
+ 'patterns': [r'\bpeft\b'],
311
+ 'aliases': ['peft']
312
+ },
313
+ 'Unsloth': {
314
+ 'patterns': [r'unsloth'],
315
+ 'aliases': ['unsloth']
316
+ },
317
+ 'Weights & Biases': {
318
+ 'patterns': [r'weights.and.biases', r'wandb'],
319
+ 'aliases': ['wandb']
320
+ },
321
+ 'Guidance': {
322
+ 'patterns': [r'\bguidance\b(?!-?)'],
323
+ 'aliases': ['guidance']
324
+ },
325
+ 'Outlines': {
326
+ 'patterns': [r'\boutlines\b'],
327
+ 'aliases': ['outlines']
328
+ },
329
+ 'MCP': {
330
+ 'patterns': [r'\bmcp\b', r'native-mcp'],
331
+ 'aliases': ['mcp']
332
+ },
333
+ 'Proxy': {
334
+ 'patterns': [r'proxy', r'clash', r'sing-box', r'hysteria', r'webhook'],
335
+ 'aliases': ['proxy']
336
+ },
337
+ 'Development': {
338
+ 'patterns': [r'code-review', r'debugg', r'test-driven', r'phased-engineering', r'systematic'],
339
+ 'aliases': ['development']
340
+ },
341
+ 'Design': {
342
+ 'patterns': [r'design-md', r'architecture-diagram', r'pretext', r'web-design', r'svg'],
343
+ 'aliases': ['design']
344
+ },
345
+ 'Creativity': {
346
+ 'patterns': [r'creative-ideation', r'humanizer', r'ascii-art', r'ascii-video', r'baoyu-infographic'],
347
+ 'aliases': ['creative']
348
+ },
349
+ 'OCR': {
350
+ 'patterns': [r'ocr\b', r'nano-pdf', r'pdf'],
351
+ 'aliases': ['ocr']
352
+ },
353
+ }
354
+
355
+ # 基于类别的默认品牌
356
+ CATEGORY_BRAND_MAP = {
357
+ 'devops': 'Docker',
358
+ 'software-development': 'Development',
359
+ 'creative': 'Design',
360
+ 'gaming': 'Unity',
361
+ 'media': 'Google',
362
+ 'research': 'Google',
363
+ 'productivity': 'Notion',
364
+ 'mlops': 'PyTorch',
365
+ 'models': 'Hugging Face',
366
+ 'training': 'PyTorch',
367
+ 'evaluation': 'Weights & Biases',
368
+ 'inference': 'LlamaIndex',
369
+ 'red-teaming': 'Anthropic',
370
+ }
371
+
372
+ def clean_text(text):
373
+ """清理文本用于匹配"""
374
+ if not text:
375
+ return ""
376
+ return re.sub(r'[^a-z0-9\s\-_.]', '', text.lower())
377
+
378
+ def identify_brand(skill_name, skill_description="", category=""):
379
+ """基于技能名称、描述和类别识别品牌"""
380
+ text_to_search = clean_text(f"{skill_name} {skill_description}")
381
+
382
+ matched_brands = []
383
+ for brand, rule in BRAND_RULES.items():
384
+ for pattern in rule['patterns']:
385
+ try:
386
+ if re.search(pattern, text_to_search, re.IGNORECASE):
387
+ matched_brands.append(brand)
388
+ break
389
+ except re.error:
390
+ pass
391
+
392
+ # 优先返回第一个匹配的品牌
393
+ if matched_brands:
394
+ return matched_brands[0]
395
+
396
+ # 如果没有匹配,使用类别默认品牌
397
+ if category and category in CATEGORY_BRAND_MAP:
398
+ return CATEGORY_BRAND_MAP[category]
399
+
400
+ return None
401
+
402
+ def read_skill_metadata(skill_path):
403
+ """读取 SKILL.md 文件的 frontmatter"""
404
+ try:
405
+ with open(skill_path, 'r', encoding='utf-8') as f:
406
+ content = f.read()
407
+
408
+ match = re.match(r'^---\n(.*?)\n---\n', content, re.DOTALL)
409
+ if match:
410
+ fm_text = match.group(1)
411
+ metadata = {}
412
+ for line in fm_text.split('\n'):
413
+ if ':' in line and not line.startswith(' '):
414
+ key, val = line.split(':', 1)
415
+ key = key.strip()
416
+ val = val.strip()
417
+ if val.startswith('"') and val.endswith('"'):
418
+ val = val[1:-1]
419
+ elif val.startswith("'") and val.endswith("'"):
420
+ val = val[1:-1]
421
+ metadata[key] = val
422
+ return metadata
423
+ except:
424
+ pass
425
+
426
+ return {}
427
+
428
+ def update_skill_metadata(skill_path, brand):
429
+ """更新 SKILL.md 的 frontmatter 添加 brand 字段"""
430
+ try:
431
+ with open(skill_path, 'r', encoding='utf-8') as f:
432
+ content = f.read()
433
+
434
+ match = re.match(r'^(---\n.*?\n---\n)', content, re.DOTALL)
435
+ if match:
436
+ frontmatter = match.group(1)
437
+ rest = content[len(frontmatter):]
438
+
439
+ if 'brand:' not in frontmatter:
440
+ fm_lines = frontmatter.rstrip('-\n').rstrip() + f'\nbrand: {brand}\n---\n'
441
+ new_content = fm_lines + rest
442
+
443
+ with open(skill_path, 'w', encoding='utf-8') as f:
444
+ f.write(new_content)
445
+ return True
446
+ except:
447
+ pass
448
+
449
+ return False
450
+
451
+ def scan_skills(skills_dir):
452
+ """扫描所有技能并识别品牌"""
453
+ results = {
454
+ 'total': 0,
455
+ 'with_brand': 0,
456
+ 'newly_added': 0,
457
+ 'brands': defaultdict(int),
458
+ 'skills_by_brand': defaultdict(list),
459
+ 'failures': []
460
+ }
461
+
462
+ for root, dirs, files in os.walk(skills_dir):
463
+ dirs[:] = [d for d in dirs if not d.startswith('.')]
464
+
465
+ if 'SKILL.md' in files:
466
+ skill_path = os.path.join(root, 'SKILL.md')
467
+ skill_name = os.path.basename(root)
468
+ category = os.path.basename(os.path.dirname(root))
469
+
470
+ results['total'] += 1
471
+
472
+ try:
473
+ metadata = read_skill_metadata(skill_path)
474
+
475
+ if 'brand' in metadata:
476
+ results['with_brand'] += 1
477
+ brand = metadata['brand']
478
+ else:
479
+ description = metadata.get('description', '')
480
+ brand = identify_brand(skill_name, description, category)
481
+
482
+ if brand:
483
+ if update_skill_metadata(skill_path, brand):
484
+ results['newly_added'] += 1
485
+ else:
486
+ results['failures'].append({
487
+ 'skill': skill_name,
488
+ 'reason': 'Failed to update metadata'
489
+ })
490
+ continue
491
+ else:
492
+ continue
493
+
494
+ if brand:
495
+ results['brands'][brand] += 1
496
+ results['skills_by_brand'][brand].append(skill_name)
497
+
498
+ except Exception as e:
499
+ results['failures'].append({
500
+ 'skill': skill_name,
501
+ 'reason': str(e)
502
+ })
503
+
504
+ return results
505
+
506
+ def generate_report(results, output_file=None):
507
+ """生成补全报告"""
508
+ report = []
509
+ report.append("=" * 70)
510
+ report.append("品牌补全报告 (Brand Completion Report)")
511
+ report.append("=" * 70)
512
+ report.append(f"\n生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
513
+
514
+ report.append("📊 补全统计:\n")
515
+ report.append(f" 总技能数: {results['total']:3d} 个")
516
+ report.append(f" 已有品牌标记: {results['with_brand']:3d} 个")
517
+ report.append(f" 新增品牌标记: {results['newly_added']:3d} 个")
518
+
519
+ before_percentage = (results['with_brand'] / results['total'] * 100) if results['total'] > 0 else 0
520
+ after_percentage = ((results['with_brand'] + results['newly_added']) / results['total'] * 100) if results['total'] > 0 else 0
521
+
522
+ report.append(f"\n 补全前: {results['with_brand']}/{results['total']} ({before_percentage:.1f}%)")
523
+ report.append(f" 补全后: {results['with_brand'] + results['newly_added']}/{results['total']} ({after_percentage:.1f}%)")
524
+ report.append(f" 提升: +{results['newly_added']} 个 ({after_percentage - before_percentage:+.1f}%)\n")
525
+
526
+ if results['brands']:
527
+ report.append("🏷️ Top 25 品牌分布:\n")
528
+
529
+ sorted_brands = sorted(results['brands'].items(), key=lambda x: x[1], reverse=True)
530
+ for i, (brand, count) in enumerate(sorted_brands[:25], 1):
531
+ report.append(f" {i:2d}. {brand:25s} : {count:3d} 个")
532
+
533
+ report.append(f"\n 总计: {len(results['brands'])} 个不同品牌\n")
534
+
535
+ if results['failures']:
536
+ report.append(f"❌ 失败: {len(results['failures'])} 项\n")
537
+ for fail in results['failures'][:10]:
538
+ report.append(f" - {fail['skill']}: {fail['reason']}")
539
+ if len(results['failures']) > 10:
540
+ report.append(f" ... 还有 {len(results['failures']) - 10} 项")
541
+ report.append()
542
+
543
+ report.append("=" * 70)
544
+
545
+ report_text = '\n'.join(report)
546
+ print(report_text)
547
+
548
+ if output_file:
549
+ with open(output_file, 'w', encoding='utf-8') as f:
550
+ f.write(report_text)
551
+
552
+ return report_text
553
+
554
+ def main():
555
+ skills_dir = os.path.expanduser('~/.hermes/skills')
556
+
557
+ if not os.path.exists(skills_dir):
558
+ print(f"❌ 技能目录不存在: {skills_dir}")
559
+ sys.exit(1)
560
+
561
+ print(f"📁 正在扫描技能目录: {skills_dir}")
562
+ print("⏳ 这可能需要几秒钟...\n")
563
+
564
+ results = scan_skills(skills_dir)
565
+
566
+ output_file = os.path.expanduser('~/Project/HuHaa-MySkills/BRANDING_COMPLETION_REPORT.txt')
567
+ generate_report(results, output_file)
568
+
569
+ json_output = os.path.expanduser('~/Project/HuHaa-MySkills/branding-results.json')
570
+ json_data = {
571
+ 'timestamp': datetime.now().isoformat(),
572
+ 'total': results['total'],
573
+ 'with_brand': results['with_brand'],
574
+ 'newly_added': results['newly_added'],
575
+ 'brands': dict(results['brands']),
576
+ 'skills_by_brand': {k: sorted(v) for k, v in results['skills_by_brand'].items()},
577
+ 'failures': results['failures']
578
+ }
579
+ with open(json_output, 'w', encoding='utf-8') as f:
580
+ json.dump(json_data, f, indent=2, ensure_ascii=False)
581
+
582
+ print(f"\n✅ 报告已保存到: {output_file}")
583
+ print(f"✅ JSON 数据已保存到: {json_output}")
584
+
585
+ if __name__ == '__main__':
586
+ main()