huhaa-myskills 0.3.5 → 0.3.6
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/README.md +79 -0
- package/bin/lib/paths.mjs +5 -0
- package/build/verify.mjs +6 -2
- package/docs/Frontend-Spec.md +11 -0
- package/docs/scan_skills_rules.md +381 -0
- package/package.json +2 -2
- package/packages/scanner/src/adapters/scan-tier.mjs +99 -0
- package/packages/scanner/src/adapters/tier1-editor-skills.mjs +240 -0
- package/packages/scanner/src/adapters/tier2-user-skills.mjs +192 -0
- package/packages/scanner/src/adapters/tier3-other-skills.mjs +190 -0
- package/packages/scanner/src/config/editor-tiers.mjs +191 -0
- package/packages/scanner/src/hash/path-hash.mjs +86 -0
- package/packages/scanner/src/icon/brand-map.mjs +110 -30
- package/packages/scanner/src/icon/icon-extractor.mjs +198 -40
- package/packages/scanner/src/index.mjs +82 -16
- package/packages/scanner/src/types.d.ts +10 -0
- package/packages/scanner/test/icon.test.mjs +158 -7
- package/packages/scanner/test/scan-tier.test.mjs +74 -0
- package/packages/scanner/test/scanner.test.mjs +6 -6
- package/packages/scanner/test/tier-editor.test.mjs +67 -0
- package/packages/scanner/verify-tier-scan.mjs +72 -0
- package/packages/server/src/index.mjs +331 -51
- package/packages/server/src/langdetect.mjs +34 -0
- package/packages/server/src/llm-client.mjs +42 -0
- package/packages/server/src/translate-cache.mjs +132 -0
- package/packages/server/src/translator.mjs +11 -29
- package/packages/server/test/langdetect.test.mjs +48 -0
- package/packages/server/test/llm-client.test.mjs +101 -0
- package/packages/server/test/server.test.mjs +21 -11
- package/packages/server/test/translate-cache.test.mjs +96 -0
- package/packages/server/test/translate-raw.test.mjs +113 -0
- package/packages/server/test/translator.test.mjs +30 -0
- package/packages/web/dist/assets/index-BbSpJilK.js +249 -0
- package/packages/web/dist/assets/index-DtwcYsHQ.css +1 -0
- package/packages/web/dist/index.html +2 -2
- package/packages/web/src/App.test.ts +84 -9
- package/packages/web/src/App.tsx +100 -10
- package/packages/web/src/components/Sidebar/SidebarMenu.tsx +83 -0
- package/packages/web/src/components/Sidebar/SkillItem.tsx +44 -0
- package/packages/web/src/components/Sidebar/TierGroup.tsx +143 -0
- package/packages/web/src/components/Sidebar/__tests__/verify-frontend.test.ts +158 -0
- package/packages/web/src/components/layout/Sidebar.test.tsx +225 -8
- package/packages/web/src/components/layout/Sidebar.tsx +135 -53
- package/packages/web/src/components/layout/Topbar.tsx +14 -12
- package/packages/web/src/components/ui/OfficialBrandIcon.tsx +101 -0
- package/packages/web/src/components/views/CliCommandView.test.tsx +328 -0
- package/packages/web/src/components/views/CliCommandView.tsx +588 -0
- package/packages/web/src/components/views/CommandIcon.test.tsx +36 -0
- package/packages/web/src/components/views/CommandIcon.tsx +18 -0
- package/packages/web/src/components/views/DashboardView.test.tsx +165 -0
- package/packages/web/src/components/views/DashboardView.tsx +148 -94
- package/packages/web/src/components/views/EditorPlaceholder.tsx +25 -0
- package/packages/web/src/components/views/OtherSkillsView.tsx +91 -14
- package/packages/web/src/components/views/SettingsView.tsx +30 -0
- package/packages/web/src/components/views/SkillDetail.test.tsx +79 -4
- package/packages/web/src/components/views/SkillDetail.tsx +200 -17
- package/packages/web/src/components/views/SkillsView.icon.test.tsx +67 -0
- package/packages/web/src/components/views/SkillsView.tsx +37 -39
- package/packages/web/src/components/views/cli/SubcommandRelatedItemsPanel.tsx +288 -0
- package/packages/web/src/data/command-help/claude.json +328 -0
- package/packages/web/src/data/command-help/code.json +297 -0
- package/packages/web/src/data/command-help/codex.json +444 -0
- package/packages/web/src/data/command-help/gstack.json +177 -0
- package/packages/web/src/data/command-help/hermes.json +321 -0
- package/packages/web/src/data/commands.json +1468 -0
- package/packages/web/src/hooks/__tests__/{useSkillIcons.test.ts → getSkillIcons.test.ts} +62 -17
- package/packages/web/src/hooks/__tests__/useLiveReload.test.ts +84 -0
- package/packages/web/src/hooks/{useSkillIcons.ts → getSkillIcons.ts} +35 -13
- package/packages/web/src/hooks/useLiveReload.ts +27 -9
- package/packages/web/src/hooks/useOtherSkills.ts +1 -1
- package/packages/web/src/hooks/useSidebarMenu.ts +74 -0
- package/packages/web/src/lib/api.ts +135 -0
- package/packages/web/src/lib/cli-enrichments/gstackList.test.ts +71 -0
- package/packages/web/src/lib/cli-enrichments/gstackList.ts +91 -0
- package/packages/web/src/lib/cli-enrichments/index.ts +45 -0
- package/packages/web/src/lib/cli-enrichments/skillDocParser.test.ts +47 -0
- package/packages/web/src/lib/cli-enrichments/skillDocParser.ts +124 -0
- package/packages/web/src/lib/cli-enrichments/types.ts +52 -0
- package/packages/web/src/lib/commandHelp.test.ts +41 -0
- package/packages/web/src/lib/commandHelp.ts +64 -0
- package/packages/web/src/lib/commands.test.ts +71 -0
- package/packages/web/src/lib/commands.ts +74 -0
- package/packages/web/src/lib/editors.test.ts +5 -1
- package/packages/web/src/lib/editors.ts +19 -9
- package/packages/web/src/lib/i18n.ts +87 -0
- package/packages/web/src/lib/iconBlacklist.ts +37 -0
- package/packages/web/src/lib/idb.ts +64 -0
- package/packages/web/src/types/index.ts +12 -0
- package/packages/web/src/types.ts +75 -0
- package/packages/web/tsconfig.json +2 -2
- package/packages/web/vitest.config.ts +21 -2
- package/scripts/digest-commands.mjs +389 -0
- package/scripts/digest-commands.test.mjs +147 -0
- package/docs/scan_rules.md +0 -172
- package/packages/web/dist/assets/index-BCY2cRaO.js +0 -175
- package/packages/web/dist/assets/index-DALKjJPi.css +0 -1
package/README.md
CHANGED
|
@@ -16,3 +16,82 @@
|
|
|
16
16
|
---
|
|
17
17
|
|
|
18
18
|
**快速开始**:见 docs/Frontend-Spec.md §VI 「快速启动指南」
|
|
19
|
+
|
|
20
|
+
## 🔑 关键技术决策
|
|
21
|
+
|
|
22
|
+
### 后端设计
|
|
23
|
+
- **API 端点**: `/api/other-skills`(列表)、`/api/icons/:brand`(真实图标)
|
|
24
|
+
- **扫描器**: 通用 `skill-adapter.mjs`,支持任意 SKILL.md 位置
|
|
25
|
+
- **文件匹配**: `**/SKILL.md` glob 递归扫描,目录名大小写不敏感、文件名大小写敏感
|
|
26
|
+
- **图标**: macOS 内置命令(`mdfind`/`plutil`/`sips`)提取,无 Swift 依赖;非 macOS 自动降级 emoji
|
|
27
|
+
|
|
28
|
+
### 前端设计
|
|
29
|
+
- **数据获取**: Hook 模式(`useOtherSkills`),支持搜索/排序/分组
|
|
30
|
+
- **图标展示**: `SkillIcon` 组件,真实图标优先、emoji 兜底
|
|
31
|
+
- **UI 布局**: 响应式双列(list + detail)
|
|
32
|
+
- **错误处理**: 自定义 `OtherSkillsError` 类
|
|
33
|
+
|
|
34
|
+
### 数据流
|
|
35
|
+
```
|
|
36
|
+
SKILL.md 文件 (160+)
|
|
37
|
+
↓
|
|
38
|
+
skill-adapter.mjs (扫描 + frontmatter 解析 + icon 字段)
|
|
39
|
+
↓
|
|
40
|
+
/api/other-skills (注入 iconUrl/iconFallback) ──► /api/icons/:brand (按需提取真实图标 PNG + 缓存)
|
|
41
|
+
↓
|
|
42
|
+
useOtherSkills Hook (搜索、过滤、排序、分组)
|
|
43
|
+
↓
|
|
44
|
+
OtherSkillsView + SkillIcon (真实图标 / emoji 降级)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 🚀 快速开始
|
|
50
|
+
|
|
51
|
+
### 开发模式
|
|
52
|
+
```bash
|
|
53
|
+
cd /Users/mac/Project/HuHaa-MySkills
|
|
54
|
+
|
|
55
|
+
# 启动后端服务(默认 11520,占用则自动切换)
|
|
56
|
+
npm start
|
|
57
|
+
|
|
58
|
+
# 前端开发(Vite)
|
|
59
|
+
npm run dev
|
|
60
|
+
|
|
61
|
+
# 点击菜单:其它技能 → 查看真实技能 + 真实工具图标
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 验证
|
|
65
|
+
```bash
|
|
66
|
+
# 列表数据(含 iconUrl 字段)
|
|
67
|
+
curl -s "http://127.0.0.1:11520/api/other-skills?roots=~/.hermes/skills&fileGlob=**/SKILL.md" | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>console.log(JSON.parse(d).skills.length))"
|
|
68
|
+
|
|
69
|
+
# 真实图标(Cursor / VS Code)
|
|
70
|
+
curl -s -o /tmp/i.png "http://127.0.0.1:11520/api/icons/cursor?size=64" && file /tmp/i.png
|
|
71
|
+
|
|
72
|
+
# 后端测试
|
|
73
|
+
npm test # 12/12
|
|
74
|
+
npm run typecheck:web
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 📊 版本历史
|
|
80
|
+
|
|
81
|
+
| 版本 | 日期 | 主要功能 | 状态 |
|
|
82
|
+
|------|------|--------|------|
|
|
83
|
+
| v0.3.2 | - | 基础架构 | ✅ |
|
|
84
|
+
| v0.3.3 | 2026-06-30 | Dashboard + Skills Sources | ✅ |
|
|
85
|
+
| v0.3.4 | 2026-07-01 | Other Skills(真实数据) | ✅ |
|
|
86
|
+
| v0.3.5 | 2026-07-01 | 真实图标(R6) + 扫描性能(R7) · 规则 v3.0 | ✅ 已实现,待发布 |
|
|
87
|
+
| 待定 | - | 桌面模式 / 功能扩展 | 📋 规划中 |
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 🔍 项目规范
|
|
92
|
+
|
|
93
|
+
所有开发须遵循:
|
|
94
|
+
- **CLAUDE.md** — AI 协作规则(全中文、strict mode)
|
|
95
|
+
- **docs/Frontend-Engineering.md** — 技术栈和编码规范
|
|
96
|
+
- **根目录整洁** — 仅保留 README.md、CLAUDE.md、package.json 等核心文件
|
|
97
|
+
- **计划文档** — 统一放 `.hermes/plans/`
|
package/bin/lib/paths.mjs
CHANGED
|
@@ -25,6 +25,11 @@ export function cacheFile() {
|
|
|
25
25
|
return path.join(homeDir(), 'cache.json');
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
// 翻译结果持久化缓存(md5 key → 译文)。purge 删除整个 homeDir 时随之清理。
|
|
29
|
+
export function translateCacheFile() {
|
|
30
|
+
return path.join(homeDir(), 'translate-cache.json');
|
|
31
|
+
}
|
|
32
|
+
|
|
28
33
|
export function stateFile() {
|
|
29
34
|
return path.join(homeDir(), 'state.json');
|
|
30
35
|
}
|
package/build/verify.mjs
CHANGED
|
@@ -10,7 +10,9 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
10
10
|
const repoRoot = path.resolve(__dirname, '..');
|
|
11
11
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'huhaa-verify-'));
|
|
12
12
|
const oldHome = process.env.HUHAA_HOME;
|
|
13
|
+
const oldUserHome = process.env.HOME;
|
|
13
14
|
process.env.HUHAA_HOME = path.join(tempRoot, 'home');
|
|
15
|
+
process.env.HOME = process.env.HUHAA_HOME;
|
|
14
16
|
|
|
15
17
|
const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
|
|
16
18
|
const testFiles = [
|
|
@@ -40,7 +42,7 @@ async function main() {
|
|
|
40
42
|
run('npm', ['run', 'test:web']);
|
|
41
43
|
run('node', ['--test', ...testFiles]);
|
|
42
44
|
|
|
43
|
-
const skillRoot = path.join(
|
|
45
|
+
const skillRoot = path.join(process.env.HOME, '.hermes', 'skills');
|
|
44
46
|
write(path.join(skillRoot, 'demo', 'verify-skill', 'SKILL.md'), `---
|
|
45
47
|
name: verify-skill
|
|
46
48
|
description: Verify smoke skill
|
|
@@ -83,7 +85,7 @@ limits:
|
|
|
83
85
|
|
|
84
86
|
const stats = await fetchJson(`${base}/api/stats`);
|
|
85
87
|
assert.equal(stats.total, 1);
|
|
86
|
-
assert.equal(stats.
|
|
88
|
+
assert.equal(stats.byBrand.hermes, 1);
|
|
87
89
|
|
|
88
90
|
const reload = await fetchJson(`${base}/api/reload`, { method: 'POST' });
|
|
89
91
|
assert.equal(reload.ok, true);
|
|
@@ -111,6 +113,8 @@ limits:
|
|
|
111
113
|
} finally {
|
|
112
114
|
if (oldHome == null) delete process.env.HUHAA_HOME;
|
|
113
115
|
else process.env.HUHAA_HOME = oldHome;
|
|
116
|
+
if (oldUserHome == null) delete process.env.HOME;
|
|
117
|
+
else process.env.HOME = oldUserHome;
|
|
114
118
|
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
115
119
|
}
|
|
116
120
|
}
|
package/docs/Frontend-Spec.md
CHANGED
|
@@ -602,8 +602,19 @@ localStorage.setItem('theme', next)
|
|
|
602
602
|
| 三区布局类(sidebar/topbar/detail/main-pane) | `packages/web/src/index.css`(`@layer components`) |
|
|
603
603
|
| 主题切换 | `packages/web/src/hooks/useTheme.ts` |
|
|
604
604
|
| editor 色映射 | `packages/web/src/lib/editors.ts` |
|
|
605
|
+
| 官方品牌图标(Skill + Command) | `packages/scanner/src/icon/icon-extractor.mjs` + `packages/scanner/src/icon/brand-map.mjs`(后端 `/api/icons/:brand`) |
|
|
606
|
+
| 前端官方图标组件 | `packages/web/src/components/ui/OfficialBrandIcon.tsx` |
|
|
605
607
|
| verify 断言约束 | `build/verify.mjs`(`#app`;CSS 子串 OR 含 `topbar`/`sidebar`/`detail` 任一) |
|
|
606
608
|
|
|
609
|
+
### 官方品牌 icon 资源策略
|
|
610
|
+
|
|
611
|
+
Skill 与 Command 模块统一通过 `/api/icons/:brand` 读取官方 icon,前端不再打包自制品牌 SVG,也不使用 emoji / lucide 品牌图标伪装官方 icon:
|
|
612
|
+
|
|
613
|
+
1. **本机官方 `.app` 图标优先**:后端按 `brand-map.mjs` 中的 Bundle ID / App 名称扫描并提取系统应用图标。
|
|
614
|
+
2. **官方远程 URL 兜底**:本机无图标时,仅从 `brand-map.mjs` 登记的 HTTPS 官方 URL 下载;结果缓存到 `~/.config/huhaa-myskills/icon-cache/`。
|
|
615
|
+
3. **中性占位**:没有本机图标且没有可确认官方远程 URL(如 `hermes` / `gstack`)时,前端显示中性占位。
|
|
616
|
+
4. **禁用联网**:`HUHAA_ICON_REMOTE=0` 时不触发远程下载,但仍可读取已有本地缓存。
|
|
617
|
+
|
|
607
618
|
---
|
|
608
619
|
|
|
609
620
|
# 三、框架说明
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
# 技能扫描规则(Scan Rules)
|
|
2
|
+
|
|
3
|
+
> 本文档定义 HuHaa-MySkills 的技能扫描规范,是稳定的工程规则,供扫描器(`packages/scanner`)与服务端(`packages/server`)遵循。
|
|
4
|
+
> 版本:v4.0 · 上次更新:2026-07-02
|
|
5
|
+
|
|
6
|
+
设计参考开源项目 **Pearcleaner**(macOS 清理工具,Swift/SwiftUI)的「软件扫描逻辑」与「图标获取展示」,在 Node.js + Fastify + React 架构下等价落地。
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## R0 扫描优先级策略 ⭐⭐⭐
|
|
11
|
+
|
|
12
|
+
扫描器按以下**三层优先级**递进扫描技能。优先级高的层级命中的技能路径不能与低优先级层级重复,使用 MD5 哈希值标记技能路径实现去重。
|
|
13
|
+
|
|
14
|
+
### 第1层:编辑器工具全局和项目目录(最高优先级)
|
|
15
|
+
|
|
16
|
+
定位以下20+编辑器工具的技能/工作流目录,按以下优先级尝试:
|
|
17
|
+
|
|
18
|
+
#### 编辑器工具清单
|
|
19
|
+
|
|
20
|
+
| 排序 | 编辑器工具 | 全局路径 | 项目路径 | 品牌标识 |
|
|
21
|
+
|------|-----------|--------|--------|---------|
|
|
22
|
+
| 1 | **Hermes** | `~/.hermes/skills` | `.hermes/skills` | `hermes` |
|
|
23
|
+
| 2 | **Claude** | `~/.claude/skills` | `.claude/skills` | `claude` |
|
|
24
|
+
| 3 | **Cursor** | `~/.cursor/skills` | `.cursor/skills` | `cursor` |
|
|
25
|
+
| 4 | **VSCode** | `~/.vscode/skills` | `.vscode/skills` | `vscode` |
|
|
26
|
+
| 5 | **Codeium** | `~/.codeium/skills` | `.codeium/skills` | `codeium` |
|
|
27
|
+
| 6 | **Windsurf** | `~/.windsurf/skills` | `.windsurf/skills` | `windsurf` |
|
|
28
|
+
| 7 | **Continue** | `~/.continue/skills` | `.continue/skills` | `continue` |
|
|
29
|
+
| 8 | **Tauri** | `~/.tauri/skills` | `.tauri/skills` | `tauri` |
|
|
30
|
+
| 9 | **Trae** | `~/.trae/skills` | `.trae/skills` | `trae` |
|
|
31
|
+
| 10 | **Trae-CN** | `~/.trae-cn/skills` | `.trae-cn/skills` | `trae-cn` |
|
|
32
|
+
| 11 | **QoDer** | `~/.qoder/skills` | `.qoder/skills` | `qoder` |
|
|
33
|
+
| 12 | **Codex** | `~/.codex/skills` | `.codex/skills` | `codex` |
|
|
34
|
+
| 13 | **Vim** | `~/.vim/skills` | `.vim/skills` | `vim` |
|
|
35
|
+
| 14 | **Neovim** | `~/.config/nvim/skills` | `.nvim/skills` | `neovim` |
|
|
36
|
+
| 15 | **Emacs** | `~/.emacs.d/skills` | `.emacs.d/skills` | `emacs` |
|
|
37
|
+
| 16 | **Sublime** | `~/.config/sublime-text/skills` | `.sublime-text/skills` | `sublime` |
|
|
38
|
+
| 17 | **Jetbrains IDE** | `~/.config/JetBrains/skills` | `.jetbrains/skills` | `jetbrains` |
|
|
39
|
+
| 18 | **Nova** | `~/.nova/skills` | `.nova/skills` | `nova` |
|
|
40
|
+
| 19 | **Zed** | `~/.config/zed/skills` | `.zed/skills` | `zed` |
|
|
41
|
+
| 20 | **GitHub Copilot** | `~/.copilot/skills` | `.copilot/skills` | `copilot` |
|
|
42
|
+
| 21 | **Replit** | `~/.replit/skills` | `.replit/skills` | `replit` |
|
|
43
|
+
| 22 | **Glot.io** | `~/.glot/skills` | `.glot/skills` | `glot` |
|
|
44
|
+
|
|
45
|
+
**扫描规则**:
|
|
46
|
+
- 递归匹配 `**/SKILL.md` 或 `**/skill.md`(文件名**不区分大小写**)
|
|
47
|
+
- 目录名称:所有变体允许(`skills` / `Skills` / `SKILLS` 等,**不区分大小写**)
|
|
48
|
+
- **路径去重**:计算每个扫描到的 SKILL.md 的绝对路径 MD5 哈希值,存储在扫描结果中作为 `pathHash`
|
|
49
|
+
|
|
50
|
+
### 第2层:用户根目录技能库(中等优先级)
|
|
51
|
+
|
|
52
|
+
定位用户 HOME 目录下的技能库:
|
|
53
|
+
|
|
54
|
+
- 目录路径:`~/skills` 或任意大小写变体(`~/SKILLS` / `~/Skills` 等)
|
|
55
|
+
- **扫描规则**:递归匹配 `**/SKILL.md` 或 `**/skill.md`(文件名**不区分大小写**)
|
|
56
|
+
- **路径去重**:同样计算 MD5 哈希值,与第1层的 `pathHash` 集合对比,**如果已存在则跳过该技能**
|
|
57
|
+
|
|
58
|
+
### 第3层:其他所有位置(最低优先级)⭐
|
|
59
|
+
|
|
60
|
+
扫描范围外或通配位置,仅在开启专项扫描时激活:
|
|
61
|
+
|
|
62
|
+
- **扫描规则**:**仅** 递归匹配 `**/skill.md`(小写,**精确大小写敏感**)
|
|
63
|
+
- **路径去重**:计算 MD5 哈希值,与第1、2层的 `pathHash` 集合对比,**如果已存在则跳过该技能**
|
|
64
|
+
- **扫描范围**(可配置,默认仅包括标准系统路径):
|
|
65
|
+
- `/opt/` 及其子目录(自定义软件安装位置)
|
|
66
|
+
- `/usr/local/` 及其子目录
|
|
67
|
+
- `~/opt/` 及其子目录
|
|
68
|
+
- 其他用户指定的自定义扫描路径
|
|
69
|
+
|
|
70
|
+
**激活条件**:
|
|
71
|
+
- 通过配置文件或 API 参数 `scanTier3=true` 显式启用
|
|
72
|
+
- 默认不扫描第3层,仅返回第1、2层结果
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## R1 扫描根目录
|
|
77
|
+
|
|
78
|
+
- 支持多个技能根目录:`roots: string[]`,由 R0 的三层优先级策略动态生成。
|
|
79
|
+
- `~` 展开为 `$HOME`。
|
|
80
|
+
- 每个 root 必须存在,否则返回 400。
|
|
81
|
+
- glob 返回精确且无重复的文件列表。
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## R2 目录和文件名大小写规则 ⭐
|
|
86
|
+
|
|
87
|
+
### 目录名称(所有层级都不敏感)
|
|
88
|
+
|
|
89
|
+
- 技能目录可能命名为 `skills` / `Skills` / `SKILLS` / `sKiLLs`,扫描器接受所有变体。
|
|
90
|
+
- 按存在性和优先级返回第一个匹配的目录。
|
|
91
|
+
|
|
92
|
+
### 文件名规则(按层级区分)
|
|
93
|
+
|
|
94
|
+
- **第1层(编辑器工具)& 第2层(用户根目录)**:`SKILL.md` 或 `skill.md` 均匹配(**不区分大小写**)
|
|
95
|
+
- **第3层(其他)**:仅匹配 `skill.md`(小写,**精确大小写敏感**)
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## R3 文件匹配与递归扫描
|
|
100
|
+
|
|
101
|
+
- 使用 `fast-glob` 递归匹配对应层级的文件名规则(见 R2)。
|
|
102
|
+
- 每个文件仅读取一次;多个 root 命中同一文件时按 `filePath` 去重。
|
|
103
|
+
- 扫描采用分块并发(见 R7),但返回列表按路径字典序稳定排序,保证幂等。
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## R4 Frontmatter 解析
|
|
108
|
+
|
|
109
|
+
- frontmatter 必须以 `---` 开始和结束;YAML 无效时返回 400。
|
|
110
|
+
- `name` / `description` 必填,缺失时返回 400。
|
|
111
|
+
- 其余字段缺失时用默认值或 null。
|
|
112
|
+
|
|
113
|
+
支持字段:
|
|
114
|
+
|
|
115
|
+
```yaml
|
|
116
|
+
---
|
|
117
|
+
name: string # 必须
|
|
118
|
+
description: string # 必填
|
|
119
|
+
category?: string
|
|
120
|
+
brand?: string # 图标解析主要依据(如 cursor / vs-code / claude-code)
|
|
121
|
+
source?: string # 图标解析次要依据
|
|
122
|
+
tier?: 'tool' | 'directory' | 'other'
|
|
123
|
+
tags?: string[]
|
|
124
|
+
icon?: string # 可选:显式指定图标,优先级最高
|
|
125
|
+
# - "app:com.todesktop.230313mzl4w4u92"(指定 Bundle ID)
|
|
126
|
+
# - "emoji:🤖"(强制 emoji)
|
|
127
|
+
# - "url:https://..."(远程/本地图标)
|
|
128
|
+
---
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## R5 返回数据格式
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
SkillItem {
|
|
137
|
+
id, name, description, filePath,
|
|
138
|
+
pathHash, // MD5(filePath),用于去重和菜单分层显示
|
|
139
|
+
tier, // 'tier-1' | 'tier-2' | 'tier-3',标记所属层级
|
|
140
|
+
category?, brand?, tier?, tags?,
|
|
141
|
+
iconUrl?, // 真实图标端点,如 "/api/icons/cursor?size=64";无真实图标时为空
|
|
142
|
+
iconFallback?, // emoji 兜底值,如 "🤖"
|
|
143
|
+
createdAt?, updatedAt?
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
ScanResult {
|
|
147
|
+
skills, // 按 tier 分组的技能列表
|
|
148
|
+
total, limit, offset, roots, scannedAt, duration,
|
|
149
|
+
pathHashes, // 已扫描的所有 pathHash 集合,用于去重检测
|
|
150
|
+
tier1Count, tier2Count, tier3Count // 各层级技能数量统计
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
排序:按 tier(1 → 2 → 3)优先,同 tier 内按 category 分组后按 name 字典序。
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## R6 真实工具/编辑器图标获取 ⭐
|
|
159
|
+
|
|
160
|
+
技能列表展示其所属工具/编辑器的**真实应用图标**,对标 Pearcleaner 的 `AppInfoUtils.fetchAppIcon(...)`。
|
|
161
|
+
|
|
162
|
+
### R6.1 品牌 → 应用映射表
|
|
163
|
+
|
|
164
|
+
维护 `brand/source → 候选 Bundle ID + 候选应用名` 的映射表(`packages/scanner/src/icon/brand-map.mjs`),包含 R0 列出的所有22个编辑器工具品牌。无对应 GUI 应用的品牌(如 `hermes`、`mcp`)按 R6.5 回退 emoji。
|
|
165
|
+
|
|
166
|
+
### R6.2 应用定位
|
|
167
|
+
|
|
168
|
+
按优先级定位 `.app`:
|
|
169
|
+
|
|
170
|
+
1. **Bundle ID 精确定位**(最快最准):`mdfind "kMDItemCFBundleIdentifier == '<bundleId>'"`
|
|
171
|
+
2. **应用名扫描**:遍历 `/Applications`、`~/Applications`、`/System/Applications`,`.app` 目录名大小写不敏感匹配
|
|
172
|
+
3. 命中多个时取修改时间最新者
|
|
173
|
+
|
|
174
|
+
### R6.3 图标提取(Node.js 等价 `NSWorkspace.icon(forFile:)`)
|
|
175
|
+
|
|
176
|
+
对定位到的 `<App>.app`:
|
|
177
|
+
|
|
178
|
+
1. 读 `Contents/Info.plist` 的 `CFBundleIconFile`(`plutil -extract CFBundleIconFile raw`)
|
|
179
|
+
2. 拼出 `Contents/Resources/<IconFile>.icns`(缺省兜底:目录下首个 `.icns`,优先含 `icon` 的文件)
|
|
180
|
+
3. 用系统内置 `sips` 转 PNG:
|
|
181
|
+
```bash
|
|
182
|
+
sips -s format png -Z 64 "<icns>" --out "<cachePng>"
|
|
183
|
+
```
|
|
184
|
+
支持尺寸 `32 / 64 / 128` 三档。
|
|
185
|
+
|
|
186
|
+
### R6.4 图标缓存
|
|
187
|
+
|
|
188
|
+
- 缓存目录:`~/.config/huhaa-myskills/icon-cache/`(受 `HUHAA_HOME` / `XDG_CONFIG_HOME` 影响)
|
|
189
|
+
- 文件名:`{appSlug}-{size}.png`
|
|
190
|
+
- **失效策略**:源 `.app` 的 mtime 新于缓存 PNG 时重建
|
|
191
|
+
- 进程内维护 `brand → appPath` / `brand:size → pngPath` 内存索引,避免重复 `sips`
|
|
192
|
+
|
|
193
|
+
### R6.5 图标优先级与降级链
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
1. frontmatter.icon 显式指定(app:/emoji:/url:) —— 最高优先级
|
|
197
|
+
2. 真实应用图标(R6.1–R6.4 成功)
|
|
198
|
+
3. emoji 映射表(BRAND_ICONS / CATEGORY_ICONS) —— 兜底降级
|
|
199
|
+
4. 通用占位图标 —— 最终兜底
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
找不到 app 时静默降级,不报错。
|
|
203
|
+
|
|
204
|
+
### R6.6 图标服务 API
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
GET /api/icons/:brand?size=64
|
|
208
|
+
→ 200 image/png (命中真实图标,cache-control: public, max-age=86400)
|
|
209
|
+
→ 404 JSON { ok:false, fallback:true, brand } (无对应 app,前端回退 emoji)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
`size` 会被 clamp 到 `[32, 128]`,非法值回退 64。
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## R7 左侧菜单栏分层显示 ⭐⭐⭐
|
|
217
|
+
|
|
218
|
+
技能在左侧菜单栏中按 tier 分组显示,每个分组有独立的 icon 和分类标签。
|
|
219
|
+
|
|
220
|
+
### R7.1 菜单栏分组结构
|
|
221
|
+
|
|
222
|
+
前端左侧菜单栏采用**二级导航**结构:
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
左侧菜单栏
|
|
226
|
+
│
|
|
227
|
+
├─ 📦 编辑器工具技能(Tier 1)
|
|
228
|
+
│ ├─ Hermes
|
|
229
|
+
│ ├─ Claude
|
|
230
|
+
│ ├─ Cursor
|
|
231
|
+
│ └─ ... (其他编辑器工具)
|
|
232
|
+
│
|
|
233
|
+
├─ 👤 我的技能库(Tier 2)
|
|
234
|
+
│ ├─ skill-1
|
|
235
|
+
│ ├─ skill-2
|
|
236
|
+
│ └─ ...
|
|
237
|
+
│
|
|
238
|
+
└─ 🔍 其他技能(Tier 3)← 仅在 scanTier3=true 时显示
|
|
239
|
+
├─ skill-x
|
|
240
|
+
├─ skill-y
|
|
241
|
+
└─ ...
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### R7.2 分组 Icon 设计
|
|
245
|
+
|
|
246
|
+
| 分组 | 显示名称 | 建议 Icon | 说明 |
|
|
247
|
+
|-----|---------|---------|------|
|
|
248
|
+
| **Tier 1** | 编辑器工具技能 | `📦` 或 `⚙️` | 代表工具集合、生态环境 |
|
|
249
|
+
| **Tier 2** | 我的技能库 | `👤` 或 `⭐` | 代表个人、用户私有 |
|
|
250
|
+
| **Tier 3** | 其他技能 | `🔍` 或 `🌐` | 代表外部、探索发现 |
|
|
251
|
+
|
|
252
|
+
**优先选择**:使用 emoji(跨平台一致),或从设计系统中选择对应的 SVG icon。
|
|
253
|
+
|
|
254
|
+
### R7.3 展开/折叠行为
|
|
255
|
+
|
|
256
|
+
- **Tier 1 和 Tier 2**:默认展开(因为用户常用)
|
|
257
|
+
- **Tier 3**:默认折叠(降低视觉噪音,仅在需要时展开)
|
|
258
|
+
- 用户可手动点击分组 header 切换展开/折叠状态
|
|
259
|
+
- 展开/折叠状态持久化到本地存储(localStorage)
|
|
260
|
+
|
|
261
|
+
### R7.4 Tier 1 内部结构
|
|
262
|
+
|
|
263
|
+
Tier 1(编辑器工具技能)在菜单栏中进一步按**编辑器品牌分组**:
|
|
264
|
+
|
|
265
|
+
```
|
|
266
|
+
编辑器工具技能(Tier 1)
|
|
267
|
+
├─ 🔷 Hermes
|
|
268
|
+
│ ├─ skill-1
|
|
269
|
+
│ └─ skill-2
|
|
270
|
+
├─ 🔹 Claude
|
|
271
|
+
│ ├─ skill-3
|
|
272
|
+
│ └─ skill-4
|
|
273
|
+
├─ 🔶 Cursor
|
|
274
|
+
│ └─ skill-5
|
|
275
|
+
└─ ... (其他编辑器)
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
每个编辑器品牌使用 **R6 获取的真实应用 icon**(如 Cursor 的真实图标、VS Code 的真实图标)。
|
|
279
|
+
|
|
280
|
+
### R7.5 技能计数 Badge
|
|
281
|
+
|
|
282
|
+
菜单栏分组 header 显示该分组的技能计数:
|
|
283
|
+
|
|
284
|
+
```
|
|
285
|
+
编辑器工具技能(📦 125)
|
|
286
|
+
我的技能库(👤 42)
|
|
287
|
+
其他技能(🔍 18)
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
动态更新,基于 `ScanResult` 的 `tier1Count / tier2Count / tier3Count`。
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## R8 扫描性能优化 ⭐
|
|
295
|
+
|
|
296
|
+
对标 Pearcleaner 的「并发扫描 + 两阶段加载 + Spotlight 元数据」。
|
|
297
|
+
|
|
298
|
+
### R8.1 两阶段加载
|
|
299
|
+
|
|
300
|
+
- **阶段 1(mini,快)**:返回轻量行(`id / name / description / category / brand / tier / iconUrl / iconFallback`),前端立即渲染列表。
|
|
301
|
+
- **阶段 2(full,补全)**:后台补齐 `tags / links / preview / frontmatter 全字段 / 时间戳 / pathHash`,前端合并升级。
|
|
302
|
+
|
|
303
|
+
服务端通过 `stage=mini|full` 区分;前端先取 mini 快速渲染,再后台取 full 升级。图标 `<img loading="lazy">` 惰性加载。
|
|
304
|
+
|
|
305
|
+
### R8.2 并发分块扫描
|
|
306
|
+
|
|
307
|
+
- `fast-glob` 拿到文件列表后,用有界并发池(默认 = CPU 核数,范围 `[4, 16]`)并发解析。
|
|
308
|
+
- 每个 worker 顺序读文件、解析 frontmatter,避免一次性打开过多文件句柄。
|
|
309
|
+
- 结果合并后按路径字典序排序(保证 R3 幂等)。
|
|
310
|
+
|
|
311
|
+
### R8.3 Spotlight 元数据(可选)
|
|
312
|
+
|
|
313
|
+
- 文件时间戳优先用 `mdls -name kMDItemFSCreationDate -name kMDItemFSContentChangeDate`(批量,每批 50)。
|
|
314
|
+
- Spotlight 不可用时回退 `fs.stat` 的 mtime。
|
|
315
|
+
- 默认关闭,通过 `useSpotlight=1` 开启,失败静默回退。
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## R9 路径去重算法 ⭐⭐
|
|
320
|
+
|
|
321
|
+
确保第2、3层的技能不与第1层重复,防止同一技能在多个层级中出现。
|
|
322
|
+
|
|
323
|
+
### R9.1 MD5 哈希计算
|
|
324
|
+
|
|
325
|
+
```javascript
|
|
326
|
+
// 伪代码
|
|
327
|
+
const crypto = require('crypto');
|
|
328
|
+
|
|
329
|
+
function getPathHash(absolutePath) {
|
|
330
|
+
// 规范化路径:展开 ~ 为 $HOME,统一为绝对路径,规范化 / 符号
|
|
331
|
+
const normalized = path.resolve(path.expanduser(absolutePath));
|
|
332
|
+
// 计算 MD5 哈希
|
|
333
|
+
const hash = crypto.createHash('md5').update(normalized).digest('hex');
|
|
334
|
+
return hash;
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### R9.2 去重流程
|
|
339
|
+
|
|
340
|
+
```
|
|
341
|
+
Step 1: 扫描第1层(编辑器工具)
|
|
342
|
+
→ 收集所有 pathHash,存入集合 tier1Hashes
|
|
343
|
+
→ 返回所有第1层技能
|
|
344
|
+
|
|
345
|
+
Step 2: 扫描第2层(用户根目录)
|
|
346
|
+
→ 对每个扫描到的技能,计算 pathHash
|
|
347
|
+
→ 如果 pathHash ∈ tier1Hashes,则跳过(已在第1层存在)
|
|
348
|
+
→ 否则,加入 tier2Hashes,返回该技能
|
|
349
|
+
|
|
350
|
+
Step 3: 扫描第3层(其他,仅当 scanTier3=true)
|
|
351
|
+
→ 对每个扫描到的技能,计算 pathHash
|
|
352
|
+
→ 如果 pathHash ∈ (tier1Hashes ∪ tier2Hashes),则跳过
|
|
353
|
+
→ 否则,返回该技能
|
|
354
|
+
|
|
355
|
+
Return: [tier1Skills, tier2Skills, tier3Skills] + { tier1Hashes, tier2Hashes }
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### R9.3 缓存策略
|
|
359
|
+
|
|
360
|
+
- 扫描结果中包含 `pathHashes: { tier1: Set<string>, tier2: Set<string> }`,方便前端或后续请求快速判断。
|
|
361
|
+
- 内存中维护 LRU 缓存(最多 1000 条),避免重复计算同一路径的哈希值。
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
## 系统依赖与兼容性
|
|
366
|
+
|
|
367
|
+
- 依赖 macOS 内置命令:`sips` / `mdfind` / `plutil` / `mdls`(均无需额外安装)。
|
|
368
|
+
- 非 macOS 环境:图标提取直接按 R6.5 降级为 emoji,扫描功能不受影响。
|
|
369
|
+
- `iconUrl` / `iconFallback` 为可选字段,旧客户端不受影响;无真实图标时行为等同 emoji 模式。
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## 附录:v3.0 规则原文(存档)
|
|
374
|
+
|
|
375
|
+
改版前 v3.0 的 R1–R5 定义,语义已升级为 v4.0:
|
|
376
|
+
|
|
377
|
+
- **R1 扫描根目录**:支持多个技能根目录,`~` 展开为 `$HOME`,每个 root 必须存在(否则 400),glob 返回精确且无重复的文件列表。
|
|
378
|
+
- **R2 目录名称大小写不敏感**:`Skills` / `SKILLS` / `skills` / `sKiLLs` 均可匹配,按优先级返回第一个存在的目录;文件名 `SKILL.md` 大小写敏感。
|
|
379
|
+
- **R3 文件匹配与递归扫描**:`fast-glob` 递归匹配 `**/SKILL.md`,每个文件仅读一次,返回按路径字典序排序。
|
|
380
|
+
- **R4 Frontmatter 解析**:frontmatter 以 `---` 开始和结束;YAML 无效时 400;`name`/`description` 必填。
|
|
381
|
+
- **R5 返回数据格式**:`SkillItem { id, name, description, filePath, category?, brand?, tier?, tags?, createdAt?, updatedAt? }`;`ScanResult { skills, total, limit, offset, roots, scannedAt, duration }`。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huhaa-myskills",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Local skill / plugin / MCP aggregation hub. Browse all your AI agent skills at http://localhost:11520.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"init": "node bin/huhaa-myskills.mjs init",
|
|
40
40
|
"purge": "node bin/huhaa-myskills.mjs purge",
|
|
41
41
|
"uninstall": "node bin/huhaa-myskills.mjs uninstall",
|
|
42
|
-
"dev": "node bin/huhaa-myskills.mjs
|
|
42
|
+
"dev": "node bin/huhaa-myskills.mjs restart && cd packages/web && npm run dev",
|
|
43
43
|
"dev-bg": "bash scripts/start-dev.sh",
|
|
44
44
|
"sync": "node bin/huhaa-myskills.mjs sync",
|
|
45
45
|
"build:web": "node build/build-web.mjs",
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file scan-tier.mjs
|
|
3
|
+
* v4.0 三层优先级扫描器
|
|
4
|
+
* 集成 Tier 1(编辑器工具)+ Tier 2(用户根目录)+ Tier 3(其他)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { scanTier1EditorSkills } from './tier1-editor-skills.mjs';
|
|
8
|
+
import { scanTier2UserSkills } from './tier2-user-skills.mjs';
|
|
9
|
+
import { scanTier3OtherSkills } from './tier3-other-skills.mjs';
|
|
10
|
+
import { pathHashCache } from '../hash/path-hash.mjs';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 三层优先级扫描
|
|
14
|
+
*
|
|
15
|
+
* @param {Object} options
|
|
16
|
+
* @param {boolean} [options.scanTier1=true] - 是否扫描第1层(编辑器工具)
|
|
17
|
+
* @param {boolean} [options.scanTier2=true] - 是否扫描第2层(用户根目录)
|
|
18
|
+
* @param {boolean} [options.scanTier3=false] - 是否扫描第3层(其他位置)
|
|
19
|
+
* @param {string} [options.projectRoot] - 项目根目录(用于扫描 .hermes/skills 等)
|
|
20
|
+
* @param {Object} [options.limits] - { maxFiles, maxFileBytes }
|
|
21
|
+
* @returns {Promise<{items: SkillItem[], stats: Object}>}
|
|
22
|
+
*/
|
|
23
|
+
export async function scanTierSkills(options = {}) {
|
|
24
|
+
const {
|
|
25
|
+
scanTier1 = true,
|
|
26
|
+
scanTier2 = true,
|
|
27
|
+
scanTier3 = false,
|
|
28
|
+
projectRoot = process.cwd(),
|
|
29
|
+
limits = {}
|
|
30
|
+
} = options;
|
|
31
|
+
|
|
32
|
+
const mergedLimits = {
|
|
33
|
+
maxFiles: limits.maxFiles ?? 5000,
|
|
34
|
+
maxFileBytes: limits.maxFileBytes ?? 1024 * 1024,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Step 1: 扫描第1层
|
|
38
|
+
console.log('[scan] Tier 1: scanning editor tools...');
|
|
39
|
+
const tier1Result = await scanTier1EditorSkills({ projectRoot, limits: mergedLimits });
|
|
40
|
+
const tier1Items = tier1Result.items;
|
|
41
|
+
const tier1PathHashes = tier1Result.pathHashes;
|
|
42
|
+
console.log(`[scan] Tier 1: found ${tier1Items.length} skills`);
|
|
43
|
+
|
|
44
|
+
// Step 2: 扫描第2层(排除第1层已有的)
|
|
45
|
+
console.log('[scan] Tier 2: scanning user skills library...');
|
|
46
|
+
const tier2Result = await scanTier2UserSkills({
|
|
47
|
+
tier1PathHashes,
|
|
48
|
+
limits: mergedLimits,
|
|
49
|
+
});
|
|
50
|
+
const tier2Items = tier2Result.items;
|
|
51
|
+
const tier2PathHashes = tier2Result.pathHashes;
|
|
52
|
+
console.log(`[scan] Tier 2: found ${tier2Items.length} skills`);
|
|
53
|
+
|
|
54
|
+
// Step 3: 扫描第3层(如果启用,排除第1、2层已有的)
|
|
55
|
+
let tier3Items = [];
|
|
56
|
+
if (scanTier3) {
|
|
57
|
+
console.log('[scan] Tier 3: scanning other locations...');
|
|
58
|
+
const tier3Result = await scanTier3OtherSkills({
|
|
59
|
+
tier1PathHashes,
|
|
60
|
+
tier2PathHashes,
|
|
61
|
+
limits: mergedLimits,
|
|
62
|
+
});
|
|
63
|
+
tier3Items = tier3Result.items;
|
|
64
|
+
console.log(`[scan] Tier 3: found ${tier3Items.length} skills`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 合并结果
|
|
68
|
+
const allItems = [...tier1Items, ...tier2Items, ...tier3Items];
|
|
69
|
+
|
|
70
|
+
// 按 tier → category → name 排序
|
|
71
|
+
allItems.sort((a, b) => {
|
|
72
|
+
const tierOrder = { 'tier-1': 0, 'tier-2': 1, 'tier-3': 2 };
|
|
73
|
+
const tierCmp = (tierOrder[a.tierId] ?? 99) - (tierOrder[b.tierId] ?? 99);
|
|
74
|
+
if (tierCmp !== 0) return tierCmp;
|
|
75
|
+
|
|
76
|
+
const catCmp = (a.category || '').localeCompare(b.category || '');
|
|
77
|
+
if (catCmp !== 0) return catCmp;
|
|
78
|
+
|
|
79
|
+
return a.name.localeCompare(b.name);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
items: allItems,
|
|
84
|
+
stats: {
|
|
85
|
+
tier1Count: tier1Items.length,
|
|
86
|
+
tier2Count: tier2Items.length,
|
|
87
|
+
tier3Count: tier3Items.length,
|
|
88
|
+
total: allItems.length,
|
|
89
|
+
pathHashes: {
|
|
90
|
+
tier1: Array.from(tier1PathHashes),
|
|
91
|
+
tier2: Array.from(tier2PathHashes),
|
|
92
|
+
},
|
|
93
|
+
cacheSize: pathHashCache.size,
|
|
94
|
+
scannedAt: new Date().toISOString(),
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { pathHashCache };
|