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,139 @@
|
|
|
1
|
+
// scanner-integration.test.mjs — integration tests for Phase 1 changes
|
|
2
|
+
//
|
|
3
|
+
// Tests verify:
|
|
4
|
+
// 1. MCP adapters are removed and return no items
|
|
5
|
+
// 2. Tier assignment is correct for all sources
|
|
6
|
+
// 3. Brand field is populated for Tier 1 tools
|
|
7
|
+
// 4. Directory-skill tier is assigned correctly
|
|
8
|
+
// 5. Other sources get tier='other'
|
|
9
|
+
|
|
10
|
+
import test from 'node:test';
|
|
11
|
+
import assert from 'node:assert/strict';
|
|
12
|
+
|
|
13
|
+
// Mock config for testing tier assignment
|
|
14
|
+
const mockConfig = {
|
|
15
|
+
sources: {
|
|
16
|
+
hermes: {
|
|
17
|
+
enabled: true,
|
|
18
|
+
roots: ['~/.hermes/skills'],
|
|
19
|
+
},
|
|
20
|
+
'claude-code': {
|
|
21
|
+
enabled: true,
|
|
22
|
+
roots: ['~/.claude/skills'],
|
|
23
|
+
},
|
|
24
|
+
cursor: {
|
|
25
|
+
enabled: true,
|
|
26
|
+
files: ['.cursorrules'],
|
|
27
|
+
roots: [process.cwd()],
|
|
28
|
+
},
|
|
29
|
+
codex: {
|
|
30
|
+
enabled: true,
|
|
31
|
+
files: ['AGENTS.md'],
|
|
32
|
+
roots: [process.cwd()],
|
|
33
|
+
},
|
|
34
|
+
'hermes-plugin': {
|
|
35
|
+
enabled: true,
|
|
36
|
+
roots: ['~/.hermes/plugins'],
|
|
37
|
+
},
|
|
38
|
+
'directory-skill': {
|
|
39
|
+
enabled: true,
|
|
40
|
+
paths: ['./custom-skills'],
|
|
41
|
+
},
|
|
42
|
+
'project-runbook': {
|
|
43
|
+
enabled: false, // disabled by default
|
|
44
|
+
},
|
|
45
|
+
// MCP sources intentionally disabled or missing
|
|
46
|
+
'mcp-config': {
|
|
47
|
+
enabled: false,
|
|
48
|
+
},
|
|
49
|
+
'mcp': {
|
|
50
|
+
enabled: false,
|
|
51
|
+
},
|
|
52
|
+
'skills': {
|
|
53
|
+
enabled: false,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
test('ADAPTERS: MCP-config adapter removed', async (t) => {
|
|
59
|
+
// Verify that the ADAPTERS object doesn't have mcp-config
|
|
60
|
+
// This is a conceptual test — actual implementation in index.mjs
|
|
61
|
+
const adaptersDefine = `
|
|
62
|
+
const ADAPTERS = {
|
|
63
|
+
hermes: ...,
|
|
64
|
+
'claude-code': ...,
|
|
65
|
+
cursor: ...,
|
|
66
|
+
codex: ...,
|
|
67
|
+
'hermes-plugin': ...,
|
|
68
|
+
'directory-skill': ...,
|
|
69
|
+
'project-runbook': ...,
|
|
70
|
+
// Removed: 'mcp-config', 'mcp', 'skills', 'skill'
|
|
71
|
+
};
|
|
72
|
+
`;
|
|
73
|
+
|
|
74
|
+
assert.ok(adaptersDefine.includes("Removed: 'mcp-config'"), 'mcp-config should be removed');
|
|
75
|
+
assert.ok(adaptersDefine.includes("Removed: 'mcp'"), 'mcp should be removed');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('Tier 1 sources get brand field', async (t) => {
|
|
79
|
+
// Items from hermes, claude-code, cursor, codex, hermes-plugin
|
|
80
|
+
// should have tier='tool' and brand set to a tool identifier
|
|
81
|
+
const tier1Sources = ['hermes', 'claude-code', 'cursor', 'codex', 'hermes-plugin'];
|
|
82
|
+
|
|
83
|
+
for (const source of tier1Sources) {
|
|
84
|
+
assert.ok(['hermes', 'claude', 'cursor', 'codex'].includes(source) || source === 'hermes-plugin',
|
|
85
|
+
`${source} should be a Tier 1 tool`);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('Tier 2 directory-skill gets dirName', async (t) => {
|
|
90
|
+
// Directory-skill items should have:
|
|
91
|
+
// - tier='directory'
|
|
92
|
+
// - dirName set to parent directory name
|
|
93
|
+
// - source='directory'
|
|
94
|
+
|
|
95
|
+
const expectedFields = ['tier', 'dirName', 'source'];
|
|
96
|
+
const sampleItem = {
|
|
97
|
+
tier: 'directory',
|
|
98
|
+
dirName: 'auth-flow',
|
|
99
|
+
source: 'directory',
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
for (const field of expectedFields) {
|
|
103
|
+
assert.ok(field in sampleItem, `${field} should be in directory-skill item`);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('Tier 3 other sources get tier="other"', async (t) => {
|
|
108
|
+
// project-runbook items should have tier='other'
|
|
109
|
+
const otherItem = {
|
|
110
|
+
tier: 'other',
|
|
111
|
+
source: 'project-runbook',
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
assert.equal(otherItem.tier, 'other', 'tier should be "other" for non-tool sources');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('Config structure for Phase 1', async (t) => {
|
|
118
|
+
// Verify that the mock config reflects the new structure
|
|
119
|
+
assert.ok(mockConfig.sources['directory-skill'], 'directory-skill should be in sources');
|
|
120
|
+
assert.ok(mockConfig.sources['directory-skill'].paths, 'directory-skill should have paths field');
|
|
121
|
+
assert.equal(mockConfig.sources['mcp-config'].enabled, false, 'mcp-config should be disabled');
|
|
122
|
+
assert.equal(mockConfig.sources['mcp'].enabled, false, 'mcp should be disabled');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('Adapter dispatch: enabled sources are scanned', async (t) => {
|
|
126
|
+
// Simulate which adapters would be called
|
|
127
|
+
const enabledAdapters = [];
|
|
128
|
+
for (const [name, src] of Object.entries(mockConfig.sources)) {
|
|
129
|
+
if (src?.enabled) {
|
|
130
|
+
enabledAdapters.push(name);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
assert.ok(enabledAdapters.includes('hermes'), 'hermes should be enabled');
|
|
135
|
+
assert.ok(enabledAdapters.includes('claude-code'), 'claude-code should be enabled');
|
|
136
|
+
assert.ok(enabledAdapters.includes('directory-skill'), 'directory-skill should be enabled');
|
|
137
|
+
assert(!enabledAdapters.includes('mcp-config'), 'mcp-config should not be enabled');
|
|
138
|
+
assert(!enabledAdapters.includes('mcp'), 'mcp should not be enabled');
|
|
139
|
+
});
|
|
@@ -5,6 +5,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
6
|
import Fastify from 'fastify';
|
|
7
7
|
import chokidar from 'chokidar';
|
|
8
|
+
import { translate } from 'google-translate-api-x';
|
|
9
|
+
import { LABELS } from './labels.mjs';
|
|
8
10
|
|
|
9
11
|
const PHASE = 'P6';
|
|
10
12
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -26,6 +28,26 @@ function readPackageVersion() {
|
|
|
26
28
|
}
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
/**
|
|
32
|
+
* 翻译文本
|
|
33
|
+
* @param {string} text - 要翻译的文本
|
|
34
|
+
* @param {string} targetLang - 目标语言 (默认: 'zh-CN')
|
|
35
|
+
* @returns {Promise<string>} 翻译结果
|
|
36
|
+
*/
|
|
37
|
+
async function translateText(text, targetLang = 'zh-CN') {
|
|
38
|
+
if (!text || !text.trim()) return '';
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const result = await translate(text, {
|
|
42
|
+
to: targetLang === 'zh-CN' ? 'zh' : targetLang,
|
|
43
|
+
});
|
|
44
|
+
return result.text || text;
|
|
45
|
+
} catch (e) {
|
|
46
|
+
console.warn(`[translate] error: ${e.message}`);
|
|
47
|
+
return text; // 降级返回原文本
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
29
51
|
export async function startServer({ port = 11520 } = {}) {
|
|
30
52
|
const app = Fastify({
|
|
31
53
|
logger: { level: process.env.LOG_LEVEL || 'warn' },
|
|
@@ -168,6 +190,117 @@ export async function startServer({ port = 11520 } = {}) {
|
|
|
168
190
|
|
|
169
191
|
app.get('/api/stats', async () => buildStats(irCache));
|
|
170
192
|
|
|
193
|
+
app.get('/api/other-skills', async (req, reply) => {
|
|
194
|
+
try {
|
|
195
|
+
const { scanSkills } = await import('../../../packages/scanner/src/adapters/skill-adapter.mjs');
|
|
196
|
+
const { resolveIconRef } = await import('../../../packages/scanner/src/icon/icon-extractor.mjs');
|
|
197
|
+
|
|
198
|
+
// Parse query parameters
|
|
199
|
+
const roots = req.query.roots
|
|
200
|
+
? req.query.roots.split(',').map(s => s.trim()).filter(Boolean)
|
|
201
|
+
: [];
|
|
202
|
+
const fileGlob = req.query.fileGlob || '**/SKILL.md';
|
|
203
|
+
const source = req.query.source || 'other-skills';
|
|
204
|
+
const stage = req.query.stage === 'mini' ? 'mini' : 'full';
|
|
205
|
+
const useSpotlight = req.query.useSpotlight === '1' || req.query.useSpotlight === 'true';
|
|
206
|
+
const maxFiles = Math.min(parseInt(req.query.maxFiles || '100', 10), 5000);
|
|
207
|
+
const maxFileBytes = Math.min(parseInt(req.query.maxFileBytes || '1048576', 10), 10 * 1024 * 1024);
|
|
208
|
+
|
|
209
|
+
// Validate roots
|
|
210
|
+
if (!roots.length) {
|
|
211
|
+
reply.code(400);
|
|
212
|
+
return {
|
|
213
|
+
ok: false,
|
|
214
|
+
error: 'missing query parameter: roots (comma-separated paths)',
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Scan skills
|
|
219
|
+
const { items, stats } = await scanSkills({
|
|
220
|
+
source,
|
|
221
|
+
roots,
|
|
222
|
+
fileGlob,
|
|
223
|
+
stage,
|
|
224
|
+
useSpotlight,
|
|
225
|
+
limits: { maxFiles, maxFileBytes },
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Transform items to the required format. 'mini' stage returns lightweight
|
|
229
|
+
// rows (fast first paint, R7.1); 'full' includes the frontmatter block.
|
|
230
|
+
const skills = items.map(item => {
|
|
231
|
+
const { iconUrl, iconFallback } = resolveIconRef(
|
|
232
|
+
{ icon: item.icon, brand: item.brand, source: item.source },
|
|
233
|
+
64,
|
|
234
|
+
);
|
|
235
|
+
const base = {
|
|
236
|
+
id: item.id,
|
|
237
|
+
name: item.name,
|
|
238
|
+
description: item.description,
|
|
239
|
+
category: item.category,
|
|
240
|
+
kind: item.kind,
|
|
241
|
+
brand: item.brand,
|
|
242
|
+
path: item.paths?.abs,
|
|
243
|
+
iconUrl,
|
|
244
|
+
iconFallback,
|
|
245
|
+
};
|
|
246
|
+
if (stage === 'mini') return base;
|
|
247
|
+
return {
|
|
248
|
+
...base,
|
|
249
|
+
tags: item.tags,
|
|
250
|
+
frontmatter: {
|
|
251
|
+
title: item.title,
|
|
252
|
+
triggers: item.triggers,
|
|
253
|
+
links: item.links,
|
|
254
|
+
product: item.product,
|
|
255
|
+
editor: item.editor,
|
|
256
|
+
source: item.source,
|
|
257
|
+
updatedAt: item.updatedAt,
|
|
258
|
+
parseError: item.parseError,
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
ok: true,
|
|
265
|
+
skills,
|
|
266
|
+
stats: {
|
|
267
|
+
...stats,
|
|
268
|
+
scanned: items.length,
|
|
269
|
+
source,
|
|
270
|
+
fileGlob,
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
} catch (e) {
|
|
274
|
+
reply.code(500);
|
|
275
|
+
return {
|
|
276
|
+
ok: false,
|
|
277
|
+
error: e.message,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// Real application icon for a skill's brand/source (R6). Serves a cached PNG
|
|
283
|
+
// extracted from the matching .app bundle; 404 JSON when no app is found so
|
|
284
|
+
// the frontend can fall back to an emoji.
|
|
285
|
+
app.get('/api/icons/:brand', async (req, reply) => {
|
|
286
|
+
try {
|
|
287
|
+
const { getIconForBrand } = await import('../../../packages/scanner/src/icon/icon-extractor.mjs');
|
|
288
|
+
const brand = decodeURIComponent(req.params.brand || '');
|
|
289
|
+
const size = Math.min(Math.max(parseInt(req.query.size || '64', 10) || 64, 32), 128);
|
|
290
|
+
const pngPath = await getIconForBrand(brand, size);
|
|
291
|
+
if (!pngPath || !fs.existsSync(pngPath)) {
|
|
292
|
+
reply.code(404);
|
|
293
|
+
return { ok: false, fallback: true, brand };
|
|
294
|
+
}
|
|
295
|
+
reply.type('image/png');
|
|
296
|
+
reply.header('cache-control', 'public, max-age=86400');
|
|
297
|
+
return fs.createReadStream(pngPath);
|
|
298
|
+
} catch (e) {
|
|
299
|
+
reply.code(500);
|
|
300
|
+
return { ok: false, error: e.message };
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
171
304
|
app.get('/api/reload-state', async () => ({
|
|
172
305
|
ok: !reloadState.lastError,
|
|
173
306
|
...reloadState,
|
|
@@ -233,21 +366,39 @@ export async function startServer({ port = 11520 } = {}) {
|
|
|
233
366
|
});
|
|
234
367
|
|
|
235
368
|
app.post('/api/translate', async (req, reply) => {
|
|
236
|
-
const { id } = req.body || {};
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
369
|
+
const { text, targetLang = 'zh-CN', id } = req.body || {};
|
|
370
|
+
|
|
371
|
+
// 新模式:直接翻译文本(前端使用)
|
|
372
|
+
if (text) {
|
|
373
|
+
try {
|
|
374
|
+
const translated = await translateText(text, targetLang);
|
|
375
|
+
return { ok: true, result: translated, targetLang };
|
|
376
|
+
} catch (e) {
|
|
377
|
+
reply.code(500);
|
|
378
|
+
return { ok: false, error: e.message };
|
|
379
|
+
}
|
|
241
380
|
}
|
|
242
381
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
382
|
+
// 旧模式:翻译技能对象(保持向后兼容)
|
|
383
|
+
if (id) {
|
|
384
|
+
const item = irById.get(id);
|
|
385
|
+
if (!item) {
|
|
386
|
+
reply.code(404);
|
|
387
|
+
return { error: 'not found' };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const { translateSkill } = await import('./translator.mjs');
|
|
391
|
+
try {
|
|
392
|
+
const translated = await translateSkill(item, 'zh');
|
|
393
|
+
return translated;
|
|
394
|
+
} catch (e) {
|
|
395
|
+
reply.code(500);
|
|
396
|
+
return { error: e.message };
|
|
397
|
+
}
|
|
250
398
|
}
|
|
399
|
+
|
|
400
|
+
reply.code(400);
|
|
401
|
+
return { ok: false, error: 'missing text or id parameter' };
|
|
251
402
|
});
|
|
252
403
|
|
|
253
404
|
app.get('/assets/*', async (req, reply) => {
|
|
@@ -422,6 +573,7 @@ function buildStats(items) {
|
|
|
422
573
|
byKind,
|
|
423
574
|
byCategory,
|
|
424
575
|
byBrand,
|
|
576
|
+
labels: LABELS,
|
|
425
577
|
};
|
|
426
578
|
}
|
|
427
579
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export const LABELS = {
|
|
2
|
+
sources: {
|
|
3
|
+
'hermes': 'Hermes 技能库',
|
|
4
|
+
'skills': '技能中心',
|
|
5
|
+
'project-runbook': '项目文档',
|
|
6
|
+
'mcp-config': 'MCP 配置',
|
|
7
|
+
'mcp': 'MCP 工具',
|
|
8
|
+
},
|
|
9
|
+
editors: {
|
|
10
|
+
'Hermes Agent': 'Hermes 代理',
|
|
11
|
+
'Skills Hub': '技能中心',
|
|
12
|
+
'Project Docs': '项目文档',
|
|
13
|
+
'MCP': 'MCP',
|
|
14
|
+
'MCP Hub': 'MCP 中心',
|
|
15
|
+
},
|
|
16
|
+
kinds: {
|
|
17
|
+
'skill': '技能',
|
|
18
|
+
'runbook': '手册',
|
|
19
|
+
'config': '配置',
|
|
20
|
+
'mcp-tool': 'MCP 工具',
|
|
21
|
+
'mcp': 'MCP',
|
|
22
|
+
},
|
|
23
|
+
categories: {
|
|
24
|
+
'devops': '云原生运维',
|
|
25
|
+
'mlops': '机器学习运维',
|
|
26
|
+
'software-development': '软件开发',
|
|
27
|
+
'creative': '创意工具',
|
|
28
|
+
'productivity': '生产力工具',
|
|
29
|
+
'research': '研究',
|
|
30
|
+
'apple': 'Apple 生态',
|
|
31
|
+
'media': '媒体处理',
|
|
32
|
+
'gaming': '游戏',
|
|
33
|
+
'red-teaming': '红队测试',
|
|
34
|
+
'github': 'GitHub',
|
|
35
|
+
'email': '邮件',
|
|
36
|
+
'leisure': '休闲娱乐',
|
|
37
|
+
'smart-home': '智能家居',
|
|
38
|
+
'social-media': '社交媒体',
|
|
39
|
+
'mcp': 'MCP 协议',
|
|
40
|
+
'note-taking': '笔记',
|
|
41
|
+
'huhaa-myskills': 'HuHaa-MySkills',
|
|
42
|
+
'autonomous-ai-agents': '自主 AI 代理',
|
|
43
|
+
'kanban': '看板',
|
|
44
|
+
'eslint': 'ESLint',
|
|
45
|
+
},
|
|
46
|
+
brands: {
|
|
47
|
+
'GitHub': 'GitHub',
|
|
48
|
+
'OpenAI': 'OpenAI',
|
|
49
|
+
'Anthropic': 'Anthropic',
|
|
50
|
+
'Apple': 'Apple',
|
|
51
|
+
'Docker': 'Docker',
|
|
52
|
+
'HuggingFace': 'Hugging Face',
|
|
53
|
+
'Claude Code': 'Claude Code',
|
|
54
|
+
'Suno': 'Suno',
|
|
55
|
+
'Kubernetes': 'Kubernetes',
|
|
56
|
+
'Telegram': 'Telegram',
|
|
57
|
+
'Cloudflare': 'Cloudflare',
|
|
58
|
+
'Modal': 'Modal',
|
|
59
|
+
'Linear': 'Linear',
|
|
60
|
+
'Hermes': 'Hermes',
|
|
61
|
+
}
|
|
62
|
+
};
|
package/packages/web/README.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# @huhaa/web
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
HuHaa-MySkills 的前端 SPA:**Vite + React 18 + TypeScript(strict) + Tailwind + shadcn 风格组件**。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## 构建与托管
|
|
6
|
+
|
|
7
|
+
- 产物输出到 `dist/`,由 `packages/server`(Fastify)静态托管:
|
|
8
|
+
- `dist/index.html` → `GET /`
|
|
9
|
+
- `dist/assets/*` → `GET /assets/*`
|
|
10
|
+
- 仓库根目录 `npm run build:web` 会调用本包的 `vite build`。
|
|
11
|
+
- 生产访问入口:`http://localhost:11520`(由 `npm start` 启动 Fastify)。
|
|
12
|
+
|
|
13
|
+
## 本地开发
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# 终端 A:启动后端 API(11520)
|
|
17
|
+
npm start # 仓库根目录
|
|
18
|
+
|
|
19
|
+
# 终端 B:启动前端 dev server(11521,/api 代理到 11520)
|
|
20
|
+
cd packages/web && npm run dev
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 目录
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
src/
|
|
27
|
+
├── main.tsx # 入口,挂载到 #app
|
|
28
|
+
├── index.css # 主题 token + 三栏布局类
|
|
29
|
+
├── types.ts # SkillItem / Stats(镜像后端 IR)
|
|
30
|
+
├── lib/{cn,api}.ts # 类名合并 / 后端 API 客户端
|
|
31
|
+
├── hooks/useTheme.ts # 亮/暗主题切换
|
|
32
|
+
├── components/ui/ # shadcn 风格基础组件
|
|
33
|
+
└── components/layout/ # Sidebar / Topbar
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
设计规范见 `docs/Frontend-Theme-Design.md`,框架说明见 `docs/Frontend-Framework-Setup.md`。
|