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
@@ -1,43 +0,0 @@
1
- export async function getJson(url) {
2
- let res;
3
- try {
4
- res = await fetch(url, { headers: { accept: 'application/json' } });
5
- } catch (e) {
6
- throw new Error(`GET ${url} failed: ${e.message || e}`);
7
- }
8
-
9
- const text = await res.text();
10
- let data = null;
11
- try {
12
- data = text ? JSON.parse(text) : null;
13
- } catch {
14
- throw new Error(`GET ${url} returned non-JSON (${res.status}): ${text.slice(0, 160)}`);
15
- }
16
-
17
- if (!res.ok) throw new Error(data?.error || `GET ${url} failed: ${res.status} ${res.statusText}`);
18
- return data;
19
- }
20
-
21
- export async function postJson(url, body) {
22
- let res;
23
- try {
24
- res = await fetch(url, {
25
- method: 'POST',
26
- headers: { 'content-type': 'application/json', accept: 'application/json' },
27
- body: JSON.stringify(body),
28
- });
29
- } catch (e) {
30
- throw new Error(`POST ${url} failed: ${e.message || e}`);
31
- }
32
-
33
- const text = await res.text();
34
- let data = null;
35
- try {
36
- data = text ? JSON.parse(text) : null;
37
- } catch {
38
- throw new Error(`POST ${url} returned non-JSON (${res.status}): ${text.slice(0, 160)}`);
39
- }
40
-
41
- if (!res.ok) throw new Error(data?.error || `POST ${url} failed: ${res.status} ${res.statusText}`);
42
- return data;
43
- }
@@ -1,6 +0,0 @@
1
- import { createApp } from 'vue';
2
- import { createPinia } from 'pinia';
3
- import App from './App.vue';
4
- import './styles.css';
5
-
6
- createApp(App).use(createPinia()).mount('#app');
@@ -1,161 +0,0 @@
1
- import { defineStore } from 'pinia';
2
-
3
- const messages = {
4
- zh: {
5
- appSubtitle: '本地技能中枢',
6
- skillsLoaded: '已加载技能',
7
- list: '列表',
8
- source: '来源',
9
- editor: '所属编辑器',
10
- kind: '类型',
11
- product: '产品',
12
- category: '分类',
13
- brand: '品牌',
14
- filters: '筛选',
15
- active: '已启用',
16
- selectedEditor: '当前编辑器',
17
- frontSelect: '已选前置',
18
- rootKind: '归属地',
19
- relativePath: '相对路径',
20
- fileSize: '文件大小',
21
- redacted: '已脱敏',
22
- parseError: '解析错误',
23
- sort: '排序',
24
- sortDefault: '默认',
25
- sortName: '名称',
26
- sortUpdated: '最近更新',
27
- sortKind: '类型',
28
- sortSource: '来源',
29
- sortProduct: '产品',
30
- all: '全部',
31
- searchPlaceholder: '搜索名称 / 描述 / 编辑器 / 路径...',
32
- searchHint: '支持 kind:mcp / source:mcp-config / product:xxx 等组合搜索',
33
- clear: '清空',
34
- noFilters: '无筛选',
35
- items: '条',
36
- uncategorized: '未分类',
37
- copyPath: '复制路径',
38
- copyDir: '复制目录',
39
- copyRel: '复制相对路径',
40
- copyPrompt: '复制调用提示',
41
- copyName: '复制名称',
42
- copyRaw: '复制全文',
43
- openCursor: '用 Cursor 打开',
44
- reveal: '在 Finder 中显示',
45
- usage: '使用',
46
- howToUse: '怎么调用',
47
- appliesTo: '适用场景',
48
- quickFacts: '快速信息',
49
- params: '参数',
50
- required: '必填',
51
- links: '链接',
52
- path: '路径',
53
- triggers: '触发词',
54
- updated: '更新于',
55
- loading: '正在加载技能...',
56
- reload: '重新扫描',
57
- reloading: '正在重新扫描...',
58
- reloadDone: '已重新扫描',
59
- lastReload: '最后扫描',
60
- liveConnected: '自动刷新已连接',
61
- copied: '已复制',
62
- opened: '已打开',
63
- bytes: '字节',
64
- language: '语言',
65
- chinese: '中文',
66
- english: 'English',
67
- },
68
- en: {
69
- appSubtitle: 'local skill hub',
70
- skillsLoaded: 'skills loaded',
71
- list: 'List',
72
- source: 'Source',
73
- editor: 'Editor',
74
- kind: 'Kind',
75
- product: 'Product',
76
- category: 'Category',
77
- brand: 'Brand',
78
- filters: 'Filters',
79
- active: 'active',
80
- selectedEditor: 'Selected editor',
81
- frontSelect: 'selected first',
82
- rootKind: 'Root',
83
- relativePath: 'Relative path',
84
- fileSize: 'File size',
85
- redacted: 'redacted',
86
- parseError: 'parse error',
87
- sort: 'Sort',
88
- sortDefault: 'Default',
89
- sortName: 'Name',
90
- sortUpdated: 'Recently updated',
91
- sortKind: 'Kind',
92
- sortSource: 'Source',
93
- sortProduct: 'Product',
94
- all: 'All',
95
- searchPlaceholder: 'Search name / description / editor / path...',
96
- searchHint: 'Supports kind:mcp / source:mcp-config / product:xxx',
97
- clear: 'Clear',
98
- noFilters: 'no filters',
99
- items: 'items',
100
- uncategorized: 'uncategorized',
101
- copyPath: 'Copy path',
102
- copyDir: 'Copy folder',
103
- copyRel: 'Copy relative path',
104
- copyPrompt: 'Copy prompt',
105
- copyName: 'Copy name',
106
- copyRaw: 'Copy raw',
107
- openCursor: 'Open Cursor',
108
- reveal: 'Reveal',
109
- usage: 'Usage',
110
- howToUse: 'How to use',
111
- appliesTo: 'Applies to',
112
- quickFacts: 'Quick facts',
113
- params: 'Params',
114
- required: 'required',
115
- links: 'Links',
116
- path: 'path',
117
- triggers: 'triggers',
118
- updated: 'updated',
119
- loading: 'Loading skills...',
120
- reload: 'Reload',
121
- reloading: 'Reloading...',
122
- reloadDone: 'Reloaded',
123
- lastReload: 'Last reload',
124
- liveConnected: 'live reload connected',
125
- copied: 'Copied',
126
- opened: 'Opened',
127
- bytes: 'bytes',
128
- language: 'Language',
129
- chinese: '中文',
130
- english: 'English',
131
- },
132
- };
133
-
134
- export const useI18nStore = defineStore('i18n', {
135
- state: () => ({
136
- locale: localStorage.getItem('huhaa-locale') || 'zh',
137
- }),
138
- getters: {
139
- t: (state) => (key) => messages[state.locale]?.[key] || messages.zh[key] || key,
140
-
141
- // Get localized skill property (name, description, category)
142
- skillText: (state) => (skill, field) => {
143
- if (!skill) return '';
144
- if (state.locale === 'zh') {
145
- const zhField = `${field}_zh`;
146
- if (skill.i18n?.[zhField]) return skill.i18n[zhField];
147
- }
148
- return skill[field] || '';
149
- },
150
- },
151
- actions: {
152
- setLocale(locale) {
153
- this.locale = locale === 'en' ? 'en' : 'zh';
154
- localStorage.setItem('huhaa-locale', this.locale);
155
- document.documentElement.lang = this.locale === 'zh' ? 'zh-CN' : 'en';
156
- },
157
- toggle() {
158
- this.setLocale(this.locale === 'zh' ? 'en' : 'zh');
159
- },
160
- },
161
- });
@@ -1,287 +0,0 @@
1
- import { defineStore } from 'pinia';
2
- import Fuse from 'fuse.js';
3
- import { getJson, postJson } from '../lib/api.js';
4
-
5
- export const useSkillsStore = defineStore('skills', {
6
- state: () => ({
7
- loading: false,
8
- error: '',
9
- skills: [],
10
- stats: null,
11
- selectedId: '',
12
- selectedDetail: null,
13
- reloadState: null,
14
- reloadConnected: false,
15
- reloadTimer: null,
16
- query: '',
17
- sortKey: 'default',
18
- filters: {
19
- editor: '',
20
- kind: '',
21
- source: '',
22
- product: '',
23
- brand: '',
24
- },
25
- notice: '',
26
- }),
27
-
28
- getters: {
29
- sources(state) {
30
- return Object.keys(state.stats?.bySource || {}).sort();
31
- },
32
- editors(state) {
33
- return Object.entries(state.stats?.byEditor || state.stats?.bySource || {})
34
- .sort((a, b) => {
35
- if (state.filters.editor && a[0] === state.filters.editor) return -1;
36
- if (state.filters.editor && b[0] === state.filters.editor) return 1;
37
- return a[0].localeCompare(b[0], 'zh-CN');
38
- })
39
- .map(([name, count]) => ({ name, count }));
40
- },
41
- kinds(state) {
42
- return countOptions(state.skills, x => x.kind);
43
- },
44
- filterSources(state) {
45
- return countOptions(state.skills, x => x.source);
46
- },
47
- products(state) {
48
- return countOptions(state.skills, x => x.product);
49
- },
50
- brands(state) {
51
- return countOptions(state.skills, x => x.brand);
52
- },
53
- activeFilterCount(state) {
54
- return Object.values(state.filters).filter(Boolean).length + (state.query.trim() ? 1 : 0);
55
- },
56
- filtered(state) {
57
- let arr = state.skills;
58
- const { editor, kind, source, product, brand } = state.filters;
59
- if (editor) arr = arr.filter(x => (x.editor || x.source) === editor);
60
- if (kind) arr = arr.filter(x => x.kind === kind);
61
- if (source) arr = arr.filter(x => x.source === source);
62
- if (product) arr = arr.filter(x => x.product === product);
63
- if (brand) arr = arr.filter(x => x.brand === brand);
64
-
65
- const parsed = parseQuery(state.query);
66
- arr = applyStructuredFilters(arr, parsed.filters);
67
- const q = parsed.text;
68
- if (!q) return sortItems(arr, state.sortKey);
69
- const fuse = new Fuse(arr, {
70
- threshold: 0.20,
71
- ignoreLocation: true,
72
- minMatchCharLength: 2,
73
- keys: [
74
- { name: 'name', weight: 0.50 },
75
- { name: 'description', weight: 0.25 },
76
- { name: 'category', weight: 0.10 },
77
- { name: 'editor', weight: 0.08 },
78
- { name: 'product', weight: 0.04 },
79
- { name: 'brand', weight: 0.02 },
80
- { name: 'triggers', weight: 0.01 },
81
- ],
82
- });
83
- return sortItems(fuse.search(q).map(r => r.item), state.sortKey);
84
- },
85
- selected(state) {
86
- return state.selectedDetail || state.skills.find(x => x.id === state.selectedId) || null;
87
- },
88
- },
89
-
90
- actions: {
91
- async load() {
92
- this.loading = true;
93
- this.error = '';
94
- try {
95
- const [skills, stats, reloadState] = await Promise.all([
96
- getJson('/api/skills'),
97
- getJson('/api/stats'),
98
- getJson('/api/reload-state'),
99
- ]);
100
- this.applySnapshot(skills, stats);
101
- this.reloadState = reloadState;
102
- if (!this.selectedId && skills.length) {
103
- this.selectedId = skills[0].id;
104
- await this.loadDetail(skills[0].id);
105
- }
106
- this.connectEvents();
107
- } catch (e) {
108
- this.error = e.message;
109
- } finally {
110
- this.loading = false;
111
- }
112
- },
113
- applySnapshot(skills, stats) {
114
- this.skills = skills;
115
- this.stats = stats;
116
- },
117
- async refreshSnapshot() {
118
- const currentId = this.selectedId;
119
- const [skills, stats, reloadState] = await Promise.all([
120
- getJson('/api/skills'),
121
- getJson('/api/stats'),
122
- getJson('/api/reload-state'),
123
- ]);
124
- this.applySnapshot(skills, stats);
125
- this.reloadState = reloadState;
126
- if (currentId && skills.some(x => x.id === currentId)) {
127
- this.selectedId = currentId;
128
- await this.loadDetail(currentId);
129
- } else if (skills.length) {
130
- this.selectedId = skills[0].id;
131
- await this.loadDetail(skills[0].id);
132
- } else {
133
- this.selectedId = '';
134
- this.selectedDetail = null;
135
- }
136
- },
137
- async loadDetail(id) {
138
- this.selectedId = id;
139
- this.error = '';
140
- try {
141
- this.selectedDetail = await getJson(`/api/skills/${encodeURIComponent(id)}`);
142
- } catch (e) {
143
- this.selectedDetail = this.skills.find(x => x.id === id) || null;
144
- this.error = e.message;
145
- }
146
- },
147
- async reload(labels = {}) {
148
- this.error = '';
149
- this.reloadState = { ...(this.reloadState || {}), scanning: true, lastError: '' };
150
- try {
151
- this.reloadState = await postJson('/api/reload', {});
152
- await this.refreshSnapshot();
153
- this.flash(labels.done || 'Reloaded');
154
- } catch (e) {
155
- this.error = e.message;
156
- this.reloadState = { ...(this.reloadState || {}), scanning: false, lastError: e.message };
157
- }
158
- },
159
- connectEvents() {
160
- if (this.reloadConnected || typeof EventSource === 'undefined') return;
161
- const es = new EventSource('/api/events');
162
- this.reloadConnected = true;
163
- es.addEventListener('hello', ev => {
164
- this.reloadState = JSON.parse(ev.data);
165
- });
166
- es.addEventListener('reload-start', ev => {
167
- this.reloadState = JSON.parse(ev.data);
168
- });
169
- es.addEventListener('reload-done', ev => {
170
- this.reloadState = JSON.parse(ev.data);
171
- clearTimeout(this.reloadTimer);
172
- this.reloadTimer = setTimeout(() => {
173
- this.refreshSnapshot().catch(e => { this.error = e.message; });
174
- }, 150);
175
- });
176
- es.addEventListener('reload-error', ev => {
177
- this.reloadState = JSON.parse(ev.data);
178
- this.error = this.reloadState.lastError || 'reload failed';
179
- });
180
- es.onerror = () => {
181
- this.reloadConnected = false;
182
- es.close();
183
- setTimeout(() => this.connectEvents(), 1500);
184
- };
185
- },
186
- setFilter(key, value) {
187
- this.filters[key] = this.filters[key] === value ? '' : value;
188
- },
189
- clearFilters() {
190
- this.query = '';
191
- this.filters = { editor: '', kind: '', source: '', product: '', brand: '' };
192
- },
193
- async copySelected(what, labels = {}) {
194
- if (!this.selectedId) return;
195
- const r = await postJson('/api/copy', { id: this.selectedId, what });
196
- this.flash(`${labels.copied || 'Copied'} ${what} (${r.bytes} ${labels.bytes || 'bytes'})`);
197
- },
198
- async openSelected(withWhat, labels = {}) {
199
- if (!this.selectedId) return;
200
- await postJson('/api/open', { id: this.selectedId, with: withWhat });
201
- this.flash(`${labels.opened || 'Opened'} ${withWhat}`);
202
- },
203
- flash(msg) {
204
- this.notice = msg;
205
- setTimeout(() => {
206
- if (this.notice === msg) this.notice = '';
207
- }, 1600);
208
- },
209
- },
210
- });
211
-
212
- function countOptions(items, pick) {
213
- const counts = new Map();
214
- for (const it of items) {
215
- const value = pick(it);
216
- if (!value) continue;
217
- counts.set(value, (counts.get(value) || 0) + 1);
218
- }
219
- return [...counts.entries()]
220
- .sort((a, b) => a[0].localeCompare(b[0], 'zh-CN'))
221
- .map(([name, count]) => ({ name, count }));
222
- }
223
-
224
- function parseQuery(input) {
225
- const filters = {};
226
- const textParts = [];
227
- const re = /(editor|kind|source|product|brand|trigger):("[^"]+"|'[^']+'|\S+)/gi;
228
- let last = 0;
229
- let match;
230
- while ((match = re.exec(input || ''))) {
231
- const before = input.slice(last, match.index).trim();
232
- if (before) textParts.push(before);
233
- filters[match[1].toLowerCase()] = match[2].replace(/^['"]|['"]$/g, '');
234
- last = re.lastIndex;
235
- }
236
- const rest = (input || '').slice(last).trim();
237
- if (rest) textParts.push(rest);
238
- return { text: textParts.join(' ').trim(), filters };
239
- }
240
-
241
- function applyStructuredFilters(items, filters) {
242
- let arr = items;
243
- for (const [key, value] of Object.entries(filters || {})) {
244
- const needle = String(value || '').toLowerCase();
245
- if (!needle) continue;
246
- arr = arr.filter(it => {
247
- const field = fieldValue(it, key);
248
- if (key === 'trigger') {
249
- // Handle triggers as array or comma-separated string
250
- const triggers = Array.isArray(field)
251
- ? field
252
- : (field || '').split(',').map(s => s.trim());
253
- return triggers.some(t => t.toLowerCase() === needle);
254
- }
255
- return String(field || '').toLowerCase() === needle;
256
- });
257
- }
258
- return arr;
259
- }
260
-
261
- function fieldValue(item, key) {
262
- if (key === 'editor') return item.editor || item.source;
263
- if (key === 'trigger') return item.triggers || [];
264
- return item[key];
265
- }
266
-
267
- function sortItems(items, sortKey) {
268
- const arr = [...items];
269
- const text = v => String(v || '').toLowerCase();
270
- const time = v => Date.parse(v || '') || 0;
271
-
272
- switch (sortKey) {
273
- case 'name':
274
- return arr.sort((a, b) => text(a.name).localeCompare(text(b.name), 'zh-CN'));
275
- case 'updated':
276
- return arr.sort((a, b) => time(b.updatedAt) - time(a.updatedAt));
277
- case 'kind':
278
- return arr.sort((a, b) => `${text(a.kind)}\u0000${text(a.name)}`.localeCompare(`${text(b.kind)}\u0000${text(b.name)}`, 'zh-CN'));
279
- case 'source':
280
- return arr.sort((a, b) => `${text(a.source)}\u0000${text(a.name)}`.localeCompare(`${text(b.source)}\u0000${text(b.name)}`, 'zh-CN'));
281
- case 'product':
282
- return arr.sort((a, b) => `${text(a.product)}\u0000${text(a.name)}`.localeCompare(`${text(b.product)}\u0000${text(b.name)}`, 'zh-CN'));
283
- case 'default':
284
- default:
285
- return arr.sort((a, b) => `${text(a.editor || a.source)}\u0000${text(a.name)}`.localeCompare(`${text(b.editor || b.source)}\u0000${text(b.name)}`, 'zh-CN'));
286
- }
287
- }