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.
- package/README.md +9 -375
- package/bin/huhaa-myskills.mjs +194 -12
- package/{scripts → build}/verify.mjs +3 -1
- package/docs/Frontend-Spec.md +1149 -0
- package/docs/assets/layout-wireframe.png +0 -0
- package/docs/assets/theme-reference.png +0 -0
- package/docs/scan_rules.md +172 -0
- package/package.json +33 -9
- package/packages/scanner/src/adapters/directory-skill.mjs +239 -0
- package/packages/scanner/src/adapters/skill-adapter.mjs +345 -0
- package/packages/scanner/src/icon/brand-map.mjs +84 -0
- package/packages/scanner/src/icon/icon-extractor.mjs +246 -0
- package/packages/scanner/src/index.mjs +107 -82
- package/packages/scanner/src/types.d.ts +18 -0
- package/packages/scanner/test/icon.test.mjs +61 -0
- package/packages/scanner/test/scanner.test.mjs +37 -41
- package/packages/scanner/test/skill-adapter.test.mjs +70 -0
- package/packages/scanner/tests/directory-skill.test.mjs +93 -0
- package/packages/scanner/tests/scanner-integration.test.mjs +139 -0
- package/packages/server/src/index.mjs +164 -12
- package/packages/server/src/labels.mjs +62 -0
- package/packages/server/test/server.test.mjs +1 -1
- package/packages/web/README.md +33 -2
- package/packages/web/dist/assets/index-BCY2cRaO.js +175 -0
- package/packages/web/dist/assets/index-DALKjJPi.css +1 -0
- package/packages/web/dist/favicon-16x16.png +0 -0
- package/packages/web/dist/favicon-192x192.png +0 -0
- package/packages/web/dist/favicon-32x32.png +0 -0
- package/packages/web/dist/favicon-512x512.png +0 -0
- package/packages/web/dist/favicon-source.svg +5 -0
- package/packages/web/dist/index.html +23 -9
- package/packages/web/index.html +22 -8
- package/packages/web/package-lock.json +2989 -0
- package/packages/web/package.json +21 -8
- package/packages/web/postcss.config.js +6 -0
- package/packages/web/public/favicon-16x16.png +0 -0
- package/packages/web/public/favicon-192x192.png +0 -0
- package/packages/web/public/favicon-32x32.png +0 -0
- package/packages/web/public/favicon-512x512.png +0 -0
- package/packages/web/public/favicon-source.svg +5 -0
- package/packages/web/src/App.test.ts +40 -0
- package/packages/web/src/App.tsx +177 -0
- package/packages/web/src/components/ComingSoon.tsx +14 -0
- package/packages/web/src/components/layout/Sidebar.test.tsx +74 -0
- package/packages/web/src/components/layout/Sidebar.tsx +114 -0
- package/packages/web/src/components/layout/Topbar.tsx +74 -0
- package/packages/web/src/components/ui/ActionButton.tsx +48 -0
- package/packages/web/src/components/ui/button.tsx +48 -0
- package/packages/web/src/components/ui/card.tsx +45 -0
- package/packages/web/src/components/views/DashboardView.tsx +122 -0
- package/packages/web/src/components/views/OtherSkillsView.test.tsx +126 -0
- package/packages/web/src/components/views/OtherSkillsView.tsx +306 -0
- package/packages/web/src/components/views/SettingsView.tsx +95 -0
- package/packages/web/src/components/views/SkillDetail.test.tsx +57 -0
- package/packages/web/src/components/views/SkillDetail.tsx +81 -0
- package/packages/web/src/components/views/SkillsView.test.ts +28 -0
- package/packages/web/src/components/views/SkillsView.tsx +226 -0
- package/packages/web/src/hooks/QUICK_REFERENCE.md +165 -0
- package/packages/web/src/hooks/README.md +378 -0
- package/packages/web/src/hooks/__tests__/useOtherSkills.test.ts +422 -0
- package/packages/web/src/hooks/__tests__/useSkillIcons.test.ts +272 -0
- package/packages/web/src/hooks/useLiveReload.ts +20 -0
- package/packages/web/src/hooks/useOtherSkills.ts +387 -0
- package/packages/web/src/hooks/useSkillIcons.ts +248 -0
- package/packages/web/src/hooks/useTheme.ts +36 -0
- package/packages/web/src/index.css +161 -0
- package/packages/web/src/lib/api.test.ts +47 -0
- package/packages/web/src/lib/api.ts +77 -0
- package/packages/web/src/lib/cn.ts +7 -0
- package/packages/web/src/lib/editors.test.ts +30 -0
- package/packages/web/src/lib/editors.ts +70 -0
- package/packages/web/src/lib/markdown.test.ts +27 -0
- package/packages/web/src/lib/markdown.ts +14 -0
- package/packages/web/src/main.tsx +13 -0
- package/packages/web/src/types/index.ts +62 -0
- package/packages/web/src/types/other-skill.ts +88 -0
- package/packages/web/src/types/skill.ts +136 -0
- package/packages/web/src/types.ts +47 -0
- package/packages/web/src/vite-env.d.ts +1 -0
- package/packages/web/tailwind.config.ts +66 -0
- package/packages/web/tsconfig.json +28 -0
- package/packages/web/vite.config.ts +36 -0
- package/packages/web/vitest.config.ts +21 -0
- package/packages/web/vitest.setup.ts +1 -0
- package/scripts/activate-data-sources.sh +223 -0
- package/scripts/brand-completion.py +586 -0
- package/scripts/init-nextjs-project.sh +421 -0
- package/scripts/install-and-sync.sh +209 -0
- package/scripts/start-dev.sh +72 -0
- package/docs/GUIDE.md +0 -190
- package/docs/PLAN.md +0 -359
- package/docs/RULES.md +0 -329
- package/docs/RUNBOOK-myskills.md +0 -258
- package/docs/SYNC-SKILLS.md +0 -259
- package/docs/releases/README.md +0 -47
- package/docs/releases/v0.1.0.md +0 -256
- package/docs/releases/v0.1.1.md +0 -297
- package/docs/releases/v0.1.2.md +0 -207
- package/docs/releases/v0.1.3.md +0 -269
- package/docs/todo/v0.1.2.md +0 -98
- package/docs/todo/v0.1.3.md +0 -40
- package/docs/todo/v0.1.4.md +0 -36
- package/docs/todo/v0.1.5.md +0 -41
- package/docs/todo/v0.1.6.md +0 -43
- package/docs/todo/v0.1.7.md +0 -38
- package/docs/todo/v0.1.8.md +0 -36
- package/docs/todo/v0.1.9.md +0 -54
- package/packages/web/dist/assets/index-BPepSb3q.js +0 -36
- package/packages/web/dist/assets/index-CIGkgT5w.css +0 -1
- package/packages/web/src/App.vue +0 -483
- package/packages/web/src/components/AppTree.vue +0 -230
- package/packages/web/src/components/DirectoryTree.vue +0 -263
- package/packages/web/src/components/SkillTree.vue +0 -208
- package/packages/web/src/lib/api.js +0 -43
- package/packages/web/src/main.js +0 -6
- package/packages/web/src/stores/i18n.js +0 -161
- package/packages/web/src/stores/skills.js +0 -287
- package/packages/web/src/styles.css +0 -365
- package/packages/web/vite.config.js +0 -17
- /package/{scripts → build}/build-web.mjs +0 -0
- /package/{scripts → build}/prepare-publish.mjs +0 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
/* ─────────────────────────── 设计 token ───────────────────────────
|
|
6
|
+
颜色为 HSL 通道值,配合 tailwind.config.ts 的 hsl(var(--x)) 使用。
|
|
7
|
+
取自 docs/Frontend-Theme-Design.md v2.0(Cockpit Tools 风格柔和浅色)。 */
|
|
8
|
+
:root {
|
|
9
|
+
/* Light Mode(默认) */
|
|
10
|
+
--color-background: 220 20% 98%;
|
|
11
|
+
--color-foreground: 220 9% 12%;
|
|
12
|
+
--color-card: 0 0% 100%;
|
|
13
|
+
--color-card-foreground: 220 9% 12%;
|
|
14
|
+
--color-sidebar: 210 20% 99%;
|
|
15
|
+
--color-border: 220 16% 94%;
|
|
16
|
+
--color-input: 220 13% 96%;
|
|
17
|
+
--color-ring: 215 100% 46%;
|
|
18
|
+
--color-primary: 215 100% 46%;
|
|
19
|
+
--color-primary-foreground: 0 0% 100%;
|
|
20
|
+
--color-primary-soft: 215 100% 96%;
|
|
21
|
+
--color-secondary: 217 33% 17%;
|
|
22
|
+
--color-secondary-foreground: 210 40% 96%;
|
|
23
|
+
--color-destructive: 0 84% 60%;
|
|
24
|
+
--color-destructive-foreground: 0 0% 100%;
|
|
25
|
+
--color-muted: 220 13% 95%;
|
|
26
|
+
--color-muted-foreground: 217 11% 45%;
|
|
27
|
+
--color-accent: 38 92% 50%;
|
|
28
|
+
--color-accent-foreground: 220 9% 12%;
|
|
29
|
+
|
|
30
|
+
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
|
31
|
+
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
|
32
|
+
--font-mono: 'SF Mono', 'JetBrains Mono', 'Fira Code', Menlo, Consolas, monospace;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.dark {
|
|
36
|
+
--color-background: 222 47% 11%;
|
|
37
|
+
--color-foreground: 210 40% 96%;
|
|
38
|
+
--color-card: 217 33% 14%;
|
|
39
|
+
--color-card-foreground: 210 40% 96%;
|
|
40
|
+
--color-sidebar: 222 33% 13%;
|
|
41
|
+
--color-border: 217 33% 20%;
|
|
42
|
+
--color-input: 217 33% 17%;
|
|
43
|
+
--color-ring: 215 100% 46%;
|
|
44
|
+
--color-primary: 215 100% 46%;
|
|
45
|
+
--color-primary-foreground: 0 0% 100%;
|
|
46
|
+
--color-primary-soft: 217 40% 20%;
|
|
47
|
+
--color-secondary: 210 40% 96%;
|
|
48
|
+
--color-secondary-foreground: 217 33% 17%;
|
|
49
|
+
--color-destructive: 0 84% 60%;
|
|
50
|
+
--color-destructive-foreground: 0 0% 100%;
|
|
51
|
+
--color-muted: 217 33% 17%;
|
|
52
|
+
--color-muted-foreground: 215 14% 65%;
|
|
53
|
+
--color-accent: 38 92% 50%;
|
|
54
|
+
--color-accent-foreground: 220 9% 12%;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@layer base {
|
|
58
|
+
* {
|
|
59
|
+
@apply border-border;
|
|
60
|
+
}
|
|
61
|
+
html,
|
|
62
|
+
body,
|
|
63
|
+
#app {
|
|
64
|
+
height: 100%;
|
|
65
|
+
}
|
|
66
|
+
body {
|
|
67
|
+
@apply bg-background text-foreground;
|
|
68
|
+
font-family: var(--font-sans);
|
|
69
|
+
-webkit-font-smoothing: antialiased;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* ─────────────────── 三区外壳布局(topbar 横跨)───────────────────
|
|
74
|
+
类名 sidebar / topbar / detail / main-pane 同时被 build/verify.mjs
|
|
75
|
+
的 CSS 冒烟断言引用,重命名需同步修改 verify。
|
|
76
|
+
布局:顶部模块标签栏横跨 → 左侧 editor 图标导航 + 右侧内容区。 */
|
|
77
|
+
@layer components {
|
|
78
|
+
.app-shell {
|
|
79
|
+
@apply grid h-full;
|
|
80
|
+
grid-template-columns: 240px 1fr;
|
|
81
|
+
grid-template-rows: 56px 1fr;
|
|
82
|
+
grid-template-areas:
|
|
83
|
+
'topbar topbar'
|
|
84
|
+
'sidebar main';
|
|
85
|
+
}
|
|
86
|
+
.topbar {
|
|
87
|
+
grid-area: topbar;
|
|
88
|
+
@apply flex items-center gap-4 border-b border-border bg-card px-4;
|
|
89
|
+
}
|
|
90
|
+
.sidebar {
|
|
91
|
+
grid-area: sidebar;
|
|
92
|
+
@apply flex flex-col gap-1 overflow-y-auto border-r border-border p-3;
|
|
93
|
+
background-color: hsl(var(--color-sidebar));
|
|
94
|
+
}
|
|
95
|
+
.main-pane {
|
|
96
|
+
grid-area: main;
|
|
97
|
+
@apply overflow-y-auto p-6;
|
|
98
|
+
}
|
|
99
|
+
.detail {
|
|
100
|
+
@apply rounded-lg border border-border bg-card p-6 text-card-foreground;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* 点阵画布纹理(仅内容区) */
|
|
104
|
+
.canvas-dotted {
|
|
105
|
+
background-color: hsl(var(--color-background));
|
|
106
|
+
background-image: radial-gradient(hsl(var(--color-border)) 1px, transparent 1px);
|
|
107
|
+
background-size: 20px 20px;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* markdown 正文(详情面板 raw 渲染) */
|
|
111
|
+
.markdown-body {
|
|
112
|
+
@apply text-body-sm leading-relaxed text-card-foreground;
|
|
113
|
+
}
|
|
114
|
+
.markdown-body h1 {
|
|
115
|
+
@apply mb-2 mt-5 text-h4 font-semibold;
|
|
116
|
+
}
|
|
117
|
+
.markdown-body h2 {
|
|
118
|
+
@apply mb-2 mt-5 text-body font-semibold;
|
|
119
|
+
}
|
|
120
|
+
.markdown-body h3,
|
|
121
|
+
.markdown-body h4 {
|
|
122
|
+
@apply mb-1 mt-4 text-body-sm font-semibold;
|
|
123
|
+
}
|
|
124
|
+
.markdown-body p {
|
|
125
|
+
@apply my-2;
|
|
126
|
+
}
|
|
127
|
+
.markdown-body ul {
|
|
128
|
+
@apply my-2 list-disc pl-5;
|
|
129
|
+
}
|
|
130
|
+
.markdown-body ol {
|
|
131
|
+
@apply my-2 list-decimal pl-5;
|
|
132
|
+
}
|
|
133
|
+
.markdown-body li {
|
|
134
|
+
@apply my-1;
|
|
135
|
+
}
|
|
136
|
+
.markdown-body a {
|
|
137
|
+
@apply text-primary underline;
|
|
138
|
+
}
|
|
139
|
+
.markdown-body code {
|
|
140
|
+
@apply rounded-sm bg-muted px-1 py-0.5 font-mono text-caption;
|
|
141
|
+
}
|
|
142
|
+
.markdown-body pre {
|
|
143
|
+
@apply my-3 overflow-x-auto rounded-md bg-muted p-3;
|
|
144
|
+
}
|
|
145
|
+
.markdown-body pre code {
|
|
146
|
+
@apply bg-transparent p-0 text-caption;
|
|
147
|
+
}
|
|
148
|
+
.markdown-body blockquote {
|
|
149
|
+
@apply my-2 border-l-2 border-border pl-3 text-muted-foreground;
|
|
150
|
+
}
|
|
151
|
+
.markdown-body hr {
|
|
152
|
+
@apply my-4 border-border;
|
|
153
|
+
}
|
|
154
|
+
.markdown-body table {
|
|
155
|
+
@apply my-3 w-full border-collapse text-caption;
|
|
156
|
+
}
|
|
157
|
+
.markdown-body th,
|
|
158
|
+
.markdown-body td {
|
|
159
|
+
@apply border border-border px-2 py-1 text-left;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest'
|
|
2
|
+
import { copy, open } from './api'
|
|
3
|
+
|
|
4
|
+
interface FakeRes {
|
|
5
|
+
ok: boolean
|
|
6
|
+
json: () => Promise<unknown>
|
|
7
|
+
}
|
|
8
|
+
function stubFetch(res: FakeRes) {
|
|
9
|
+
const fn = vi.fn((_url: string, _init?: RequestInit) => Promise.resolve(res as unknown as Response))
|
|
10
|
+
vi.stubGlobal('fetch', fn)
|
|
11
|
+
return fn
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
afterEach(() => vi.unstubAllGlobals())
|
|
15
|
+
|
|
16
|
+
describe('api copy/open 契约(C5 回归守卫)', () => {
|
|
17
|
+
it('copy 透传 what(含后端支持的 raw/prompt),POST /api/copy', async () => {
|
|
18
|
+
const fn = stubFetch({ ok: true, json: async () => ({ ok: true, what: 'raw', bytes: 12 }) })
|
|
19
|
+
const r = await copy('id-1', 'raw')
|
|
20
|
+
expect(r.ok).toBe(true)
|
|
21
|
+
const [url, init] = fn.mock.calls[0]
|
|
22
|
+
expect(url).toBe('/api/copy')
|
|
23
|
+
expect(JSON.parse(init!.body as string)).toEqual({ id: 'id-1', what: 'raw' })
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('copy 默认 what=path', async () => {
|
|
27
|
+
const fn = stubFetch({ ok: true, json: async () => ({ ok: true }) })
|
|
28
|
+
await copy('id-2')
|
|
29
|
+
const [, init] = fn.mock.calls[0]
|
|
30
|
+
expect(JSON.parse(init!.body as string).what).toBe('path')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('open 透传 with,POST /api/open', async () => {
|
|
34
|
+
const fn = stubFetch({ ok: true, json: async () => ({ ok: true, with: 'finder' }) })
|
|
35
|
+
await open('id-3', 'finder')
|
|
36
|
+
const [url, init] = fn.mock.calls[0]
|
|
37
|
+
expect(url).toBe('/api/open')
|
|
38
|
+
expect(JSON.parse(init!.body as string)).toEqual({ id: 'id-3', with: 'finder' })
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('非 2xx 时仍读到 {ok:false,error}(不抛错吞掉 body)', async () => {
|
|
42
|
+
stubFetch({ ok: false, json: async () => ({ ok: false, error: "invalid 'what'" }) })
|
|
43
|
+
const r = await copy('id-4', 'path')
|
|
44
|
+
expect(r.ok).toBe(false)
|
|
45
|
+
expect(r.error).toMatch(/invalid/)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { SkillItem, Stats } from '@/types'
|
|
2
|
+
|
|
3
|
+
// 生产环境前端与 Fastify 同源(11520);dev 模式由 vite proxy 转发 /api。
|
|
4
|
+
const API_BASE = '/api'
|
|
5
|
+
|
|
6
|
+
async function get<T>(path: string): Promise<T> {
|
|
7
|
+
const res = await fetch(`${API_BASE}${path}`)
|
|
8
|
+
if (!res.ok) throw new Error(`GET ${path} 失败: ${res.status}`)
|
|
9
|
+
return res.json() as Promise<T>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function post<T>(path: string, body?: unknown): Promise<T> {
|
|
13
|
+
const res = await fetch(`${API_BASE}${path}`, {
|
|
14
|
+
method: 'POST',
|
|
15
|
+
headers: { 'Content-Type': 'application/json' },
|
|
16
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
17
|
+
})
|
|
18
|
+
if (!res.ok) throw new Error(`POST ${path} 失败: ${res.status}`)
|
|
19
|
+
return res.json() as Promise<T>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 容忍非 2xx 的 POST:copy/open 在非法输入时返回 {ok:false,error}(4xx/5xx),需读取该 body */
|
|
23
|
+
async function postTolerant<T>(path: string, body?: unknown): Promise<T> {
|
|
24
|
+
const res = await fetch(`${API_BASE}${path}`, {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: { 'Content-Type': 'application/json' },
|
|
27
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
28
|
+
})
|
|
29
|
+
return res.json() as Promise<T>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 列表:返回不含 raw 的 SkillItem[] */
|
|
33
|
+
export function fetchSkills(): Promise<SkillItem[]> {
|
|
34
|
+
return get<SkillItem[]>('/skills')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 详情:返回含完整 raw 的 SkillItem */
|
|
38
|
+
export function fetchSkillDetail(id: string): Promise<SkillItem> {
|
|
39
|
+
return get<SkillItem>(`/skills/${encodeURIComponent(id)}`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 统计聚合 */
|
|
43
|
+
export function fetchStats(): Promise<Stats> {
|
|
44
|
+
return get<Stats>('/stats')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 手动触发重新扫描 */
|
|
48
|
+
export function reload(): Promise<{ ok: boolean; items: number }> {
|
|
49
|
+
return post('/reload')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 后端 pickCopyText 支持的 what 取值(packages/server/src/index.mjs:352) */
|
|
53
|
+
export type CopyWhat = 'path' | 'dir' | 'rel' | 'name' | 'raw' | 'prompt'
|
|
54
|
+
export type OpenWith = 'default' | 'cursor' | 'finder'
|
|
55
|
+
|
|
56
|
+
export interface CopyResult {
|
|
57
|
+
ok: boolean
|
|
58
|
+
bytes?: number
|
|
59
|
+
what?: string
|
|
60
|
+
error?: string
|
|
61
|
+
}
|
|
62
|
+
export interface OpenResult {
|
|
63
|
+
ok: boolean
|
|
64
|
+
opened?: string
|
|
65
|
+
with?: string
|
|
66
|
+
error?: string
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 复制技能内容 / 路径到剪贴板(由后端执行) */
|
|
70
|
+
export function copy(id: string, what: CopyWhat = 'path'): Promise<CopyResult> {
|
|
71
|
+
return postTolerant<CopyResult>('/copy', { id, what })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 用指定编辑器 / 访达打开技能文件 */
|
|
75
|
+
export function open(id: string, withApp: OpenWith = 'default'): Promise<OpenResult> {
|
|
76
|
+
return postTolerant<OpenResult>('/open', { id, with: withApp })
|
|
77
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { getEditorMeta, isNoneEditor, editorLabel } from '@/lib/editors'
|
|
3
|
+
|
|
4
|
+
describe('editors 映射(D2 主轴 = editor)', () => {
|
|
5
|
+
it('按关键字归一着色(Claude/Codex/Hermes/Cursor)', () => {
|
|
6
|
+
expect(getEditorMeta('Claude Code').color).toBe('#D97757')
|
|
7
|
+
expect(getEditorMeta('codex').color).toBe('#10A37F')
|
|
8
|
+
expect(getEditorMeta('Hermes Agent').color).toBe('#4A5FC7')
|
|
9
|
+
expect(getEditorMeta('Cursor').color).toBe('#1F2937')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('label 透传原值,(none) → 未分类', () => {
|
|
13
|
+
expect(getEditorMeta('Claude Code').label).toBe('Claude Code')
|
|
14
|
+
expect(getEditorMeta('(none)').label).toBe('未分类')
|
|
15
|
+
expect(editorLabel('Cursor')).toBe('Cursor')
|
|
16
|
+
expect(editorLabel('(none)')).toBe('未分类')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('未知 editor 走兜底色(C6)', () => {
|
|
20
|
+
const meta = getEditorMeta('SomeNewTool 9000')
|
|
21
|
+
expect(meta.color).toBe('#6B7280')
|
|
22
|
+
expect(meta.icon).toBeTruthy()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('isNoneEditor 识别空/(none)', () => {
|
|
26
|
+
expect(isNoneEditor('(none)')).toBe(true)
|
|
27
|
+
expect(isNoneEditor('')).toBe(true)
|
|
28
|
+
expect(isNoneEditor('Claude Code')).toBe(false)
|
|
29
|
+
})
|
|
30
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Sparkles,
|
|
3
|
+
Terminal,
|
|
4
|
+
Bot,
|
|
5
|
+
Code2,
|
|
6
|
+
Wind,
|
|
7
|
+
Ghost,
|
|
8
|
+
Cpu,
|
|
9
|
+
Boxes,
|
|
10
|
+
type LucideIcon,
|
|
11
|
+
} from 'lucide-react'
|
|
12
|
+
import type { SkillItem } from '@/types'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* editor 元数据映射(D2:导航/筛选主轴 = editor,非 brand)。
|
|
16
|
+
* editor 取值来自 Stats.byEditor 的 key(it.editor || it.source || '(none)'),
|
|
17
|
+
* 形如 "Claude Code" / "Hermes Agent" / "Cursor" / "codex" / "(none)"。
|
|
18
|
+
*
|
|
19
|
+
* 按关键字归一着色取图标;未知/缺失走兜底(C6:(none) 由调用方过滤或标注)。
|
|
20
|
+
*/
|
|
21
|
+
export interface EditorMeta {
|
|
22
|
+
label: string
|
|
23
|
+
/** 品牌色(hex),用于图标前景 + 半透明底 */
|
|
24
|
+
color: string
|
|
25
|
+
icon: LucideIcon
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface Rule {
|
|
29
|
+
test: RegExp
|
|
30
|
+
color: string
|
|
31
|
+
icon: LucideIcon
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 关键字优先匹配(顺序敏感:更具体的放前面)
|
|
35
|
+
const RULES: Rule[] = [
|
|
36
|
+
{ test: /claude|anthropic/i, color: '#D97757', icon: Sparkles },
|
|
37
|
+
{ test: /codex|openai/i, color: '#10A37F', icon: Cpu },
|
|
38
|
+
{ test: /hermes/i, color: '#4A5FC7', icon: Bot },
|
|
39
|
+
{ test: /cursor/i, color: '#1F2937', icon: Code2 },
|
|
40
|
+
{ test: /windsurf/i, color: '#10B6C4', icon: Wind },
|
|
41
|
+
{ test: /kiro/i, color: '#8B5CF6', icon: Ghost },
|
|
42
|
+
{ test: /copilot|github/i, color: '#24292F', icon: Terminal },
|
|
43
|
+
{ test: /gemini/i, color: '#4285F4', icon: Sparkles },
|
|
44
|
+
{ test: /mcp/i, color: '#2563EB', icon: Boxes },
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
const FALLBACK: Omit<EditorMeta, 'label'> = { color: '#6B7280', icon: Boxes }
|
|
48
|
+
|
|
49
|
+
/** 是否为「无归属」桶(C6)——调用方据此过滤或标注「未分类」。 */
|
|
50
|
+
export function isNoneEditor(key: string): boolean {
|
|
51
|
+
return !key || key === '(none)' || key.toLowerCase() === 'none'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 单条目的 editor 归属 key,与 server buildStats 口径一致(it.editor || it.source || '(none)')。 */
|
|
55
|
+
export function itemEditorKey(it: SkillItem): string {
|
|
56
|
+
return it.editor || it.source || '(none)'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 把原始 editor key 转为展示用 label((none) → 未分类)。 */
|
|
60
|
+
export function editorLabel(key: string): string {
|
|
61
|
+
return isNoneEditor(key) ? '未分类' : key
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getEditorMeta(key: string): EditorMeta {
|
|
65
|
+
const label = editorLabel(key)
|
|
66
|
+
for (const r of RULES) {
|
|
67
|
+
if (r.test.test(key)) return { label, color: r.color, icon: r.icon }
|
|
68
|
+
}
|
|
69
|
+
return { label, ...FALLBACK }
|
|
70
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { renderMarkdown } from './markdown'
|
|
3
|
+
|
|
4
|
+
describe('renderMarkdown', () => {
|
|
5
|
+
it('渲染标题与段落', () => {
|
|
6
|
+
expect(renderMarkdown('# Hi')).toContain('<h1>Hi</h1>')
|
|
7
|
+
expect(renderMarkdown('hello')).toContain('<p>hello</p>')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('围栏代码块带 language- 前缀', () => {
|
|
11
|
+
const html = renderMarkdown('```js\nconst a = 1\n```')
|
|
12
|
+
expect(html).toContain('<pre>')
|
|
13
|
+
expect(html).toContain('language-js')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('转义内联 HTML(防 XSS,html:false)', () => {
|
|
17
|
+
const html = renderMarkdown('<script>alert(1)</script>')
|
|
18
|
+
expect(html).not.toContain('<script>')
|
|
19
|
+
expect(html).toContain('<script>')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('空/undefined 输入返回空串', () => {
|
|
23
|
+
expect(renderMarkdown('')).toBe('')
|
|
24
|
+
expect(renderMarkdown(undefined)).toBe('')
|
|
25
|
+
expect(renderMarkdown(null)).toBe('')
|
|
26
|
+
})
|
|
27
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import MarkdownIt from 'markdown-it'
|
|
2
|
+
|
|
3
|
+
// html:false → 源文本里的内联 HTML 被转义(防 XSS);只输出 markdown-it 生成的安全标签。
|
|
4
|
+
// 代码块输出 <pre><code class="language-xxx">,由 index.css 的 .markdown-body 着色。
|
|
5
|
+
const md = new MarkdownIt({
|
|
6
|
+
html: false,
|
|
7
|
+
linkify: true,
|
|
8
|
+
breaks: false,
|
|
9
|
+
langPrefix: 'language-',
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
export function renderMarkdown(src: string | undefined | null): string {
|
|
13
|
+
return md.render(src ?? '')
|
|
14
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { StrictMode } from 'react'
|
|
2
|
+
import { createRoot } from 'react-dom/client'
|
|
3
|
+
import App from '@/App'
|
|
4
|
+
import '@/index.css'
|
|
5
|
+
|
|
6
|
+
const container = document.getElementById('app')
|
|
7
|
+
if (!container) throw new Error('挂载节点 #app 不存在')
|
|
8
|
+
|
|
9
|
+
createRoot(container).render(
|
|
10
|
+
<StrictMode>
|
|
11
|
+
<App />
|
|
12
|
+
</StrictMode>
|
|
13
|
+
)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// 前端镜像后端 IR(packages/scanner/src/types.d.ts 的 SkillItem)。
|
|
2
|
+
// 二者需保持同步:后端字段变更时同步此文件。
|
|
3
|
+
|
|
4
|
+
export type SkillKind =
|
|
5
|
+
| 'skill'
|
|
6
|
+
| 'plugin'
|
|
7
|
+
| 'mcp'
|
|
8
|
+
| 'mcp-tool'
|
|
9
|
+
| 'runbook'
|
|
10
|
+
| 'instruction'
|
|
11
|
+
| 'config'
|
|
12
|
+
| 'doc'
|
|
13
|
+
| 'agent-rule'
|
|
14
|
+
|
|
15
|
+
export interface SkillItem {
|
|
16
|
+
id: string
|
|
17
|
+
kind: SkillKind
|
|
18
|
+
source: string
|
|
19
|
+
name: string
|
|
20
|
+
title?: string
|
|
21
|
+
description?: string
|
|
22
|
+
category?: string
|
|
23
|
+
editor?: string
|
|
24
|
+
brand?: string
|
|
25
|
+
product?: string
|
|
26
|
+
triggers?: string[]
|
|
27
|
+
tags?: string[]
|
|
28
|
+
paths: {
|
|
29
|
+
abs: string
|
|
30
|
+
rel?: string
|
|
31
|
+
rootKind: 'home' | 'project' | 'icloud'
|
|
32
|
+
}
|
|
33
|
+
preview: string
|
|
34
|
+
/** /api/skills 列表接口会剥离 raw,仅 /api/skills/:id 返回 */
|
|
35
|
+
raw?: string
|
|
36
|
+
links?: { label: string; url: string }[]
|
|
37
|
+
updatedAt: string
|
|
38
|
+
parseError?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface Stats {
|
|
42
|
+
total: number
|
|
43
|
+
bySource: Record<string, number>
|
|
44
|
+
byEditor: Record<string, number>
|
|
45
|
+
byKind: Record<string, number>
|
|
46
|
+
byCategory: Record<string, number>
|
|
47
|
+
byBrand: Record<string, number>
|
|
48
|
+
labels?: Record<string, unknown>
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// OtherSkill 类型导出 - 前端 Hook 改造后的新类型
|
|
52
|
+
export type {
|
|
53
|
+
OtherSkill,
|
|
54
|
+
OtherSkillGroup,
|
|
55
|
+
OtherSkillsOptions,
|
|
56
|
+
} from './other-skill'
|
|
57
|
+
export {
|
|
58
|
+
OtherSkillCategory,
|
|
59
|
+
SortBy,
|
|
60
|
+
SortOrder,
|
|
61
|
+
GroupBy,
|
|
62
|
+
} from './other-skill'
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 其它技能分类枚举 - 支持按分类、品牌、来源分组
|
|
3
|
+
*/
|
|
4
|
+
export enum OtherSkillCategory {
|
|
5
|
+
COMMAND = 'command', // CLI 命令
|
|
6
|
+
EDITOR = 'editor', // 编辑器集成
|
|
7
|
+
TOOL = 'tool', // 工具/插件
|
|
8
|
+
CLOUD = 'cloud', // 云服务
|
|
9
|
+
AI = 'ai', // AI 能力
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 排序顺序
|
|
14
|
+
*/
|
|
15
|
+
export enum SortOrder {
|
|
16
|
+
ASC = 'asc',
|
|
17
|
+
DESC = 'desc',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 排序字段
|
|
22
|
+
*/
|
|
23
|
+
export enum SortBy {
|
|
24
|
+
NAME = 'name',
|
|
25
|
+
UPDATED = 'updatedAt',
|
|
26
|
+
CATEGORY = 'category',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 分组方式
|
|
31
|
+
*/
|
|
32
|
+
export enum GroupBy {
|
|
33
|
+
CATEGORY = 'category',
|
|
34
|
+
BRAND = 'brand',
|
|
35
|
+
SOURCE = 'source',
|
|
36
|
+
NONE = 'none',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 其它技能项 - 匹配真实 SKILL.md 数据结构
|
|
41
|
+
*/
|
|
42
|
+
export interface OtherSkill {
|
|
43
|
+
id: string
|
|
44
|
+
name: string
|
|
45
|
+
title?: string // 人类可读标题
|
|
46
|
+
category?: string | string[] // 语义分类标签(可多个)
|
|
47
|
+
brand?: string // 品牌标识(用于 icon)
|
|
48
|
+
source?: string // 来源(hermes、claude-code、cursor 等)
|
|
49
|
+
description?: string // 简短描述
|
|
50
|
+
icon?: string // 自定义 icon(emoji 或字符)
|
|
51
|
+
iconUrl?: string // 真实应用图标 URL(如 /api/icons/cursor?size=64)
|
|
52
|
+
iconFallback?: string // 图标加载失败时的 emoji 兜底
|
|
53
|
+
tags?: string[] // 搜索标签
|
|
54
|
+
docs?: string // 文档链接
|
|
55
|
+
links?: Array<{ // 相关链接
|
|
56
|
+
label: string
|
|
57
|
+
url: string
|
|
58
|
+
}>
|
|
59
|
+
examples?: string[] // 使用示例
|
|
60
|
+
updatedAt?: string // ISO 8601 时间戳
|
|
61
|
+
parseError?: string // 解析错误(如有)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 其它技能分组(支持多种分组方式)
|
|
66
|
+
*/
|
|
67
|
+
export interface OtherSkillGroup {
|
|
68
|
+
groupKey: string // 分组键(category、brand 等)
|
|
69
|
+
label: string // 显示标签
|
|
70
|
+
icon: string // 分组 icon
|
|
71
|
+
items: OtherSkill[] // 该分组中的技能项
|
|
72
|
+
count: number // 项数统计
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 搜索和过滤选项
|
|
77
|
+
*/
|
|
78
|
+
export interface OtherSkillsOptions {
|
|
79
|
+
query?: string // 搜索词
|
|
80
|
+
sortBy?: SortBy // 排序字段
|
|
81
|
+
sortOrder?: SortOrder // 排序方向
|
|
82
|
+
groupBy?: GroupBy // 分组方式
|
|
83
|
+
filterBrand?: string // 按品牌过滤
|
|
84
|
+
filterCategory?: string // 按分类过滤
|
|
85
|
+
filterSource?: string // 按来源过滤
|
|
86
|
+
limit?: number // 限制结果数
|
|
87
|
+
offset?: number // 分页偏移
|
|
88
|
+
}
|