skillhelper 0.4.2
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/LICENSE +21 -0
- package/README.md +105 -0
- package/bin/lib/atomic-write.mjs +72 -0
- package/bin/lib/paths.mjs +65 -0
- package/bin/lib/port.mjs +23 -0
- package/bin/skillhelper.mjs +720 -0
- package/build/build-site.mjs +35 -0
- package/build/build-web.mjs +29 -0
- package/build/pack-smoke.mjs +195 -0
- package/build/prepare-publish.mjs +3 -0
- package/build/verify.mjs +157 -0
- package/config/sources.example.yaml +126 -0
- package/docs/Frontend-Spec.md +1207 -0
- package/docs/scan_skills_rules.md +432 -0
- package/package.json +95 -0
- package/packages/scanner/package.json +15 -0
- package/packages/scanner/src/adapters/claude-agents.mjs +134 -0
- package/packages/scanner/src/adapters/claude-plugin.mjs +260 -0
- package/packages/scanner/src/adapters/codex-plugin.mjs +330 -0
- package/packages/scanner/src/adapters/directory-skill.mjs +239 -0
- package/packages/scanner/src/adapters/file-docs.mjs +158 -0
- package/packages/scanner/src/adapters/hermes-plugin.mjs +149 -0
- package/packages/scanner/src/adapters/markdown-skill.mjs +229 -0
- package/packages/scanner/src/adapters/mcp-config.mjs +190 -0
- package/packages/scanner/src/adapters/scan-tier.mjs +99 -0
- package/packages/scanner/src/adapters/skill-adapter.mjs +345 -0
- package/packages/scanner/src/adapters/tier1-editor-skills.mjs +278 -0
- package/packages/scanner/src/adapters/tier2-user-skills.mjs +218 -0
- package/packages/scanner/src/adapters/tier3-other-skills.mjs +225 -0
- package/packages/scanner/src/config/editor-tiers.mjs +191 -0
- package/packages/scanner/src/core/atomic-write.mjs +11 -0
- package/packages/scanner/src/core/descriptor.mjs +143 -0
- package/packages/scanner/src/core/path-hash.mjs +89 -0
- package/packages/scanner/src/core/registry.mjs +128 -0
- package/packages/scanner/src/hash/path-hash.mjs +8 -0
- package/packages/scanner/src/icon/brand-map.mjs +268 -0
- package/packages/scanner/src/icon/icon-extractor.mjs +466 -0
- package/packages/scanner/src/index.mjs +450 -0
- package/packages/scanner/src/types.d.ts +144 -0
- package/packages/scanner/src/utils.mjs +194 -0
- package/packages/server/package.json +15 -0
- package/packages/server/src/index.mjs +1071 -0
- package/packages/server/src/labels.mjs +64 -0
- 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 +53 -0
- package/packages/web/README.md +36 -0
- package/packages/web/dist/assets/index-CGt04vt0.js +304 -0
- package/packages/web/dist/assets/index-CLSDoa3H.css +1 -0
- package/packages/web/dist/brand-icon.png +0 -0
- package/packages/web/dist/brand-logo.png +0 -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/favicon.ico +0 -0
- package/packages/web/dist/favicon.svg +5 -0
- package/packages/web/dist/icons/hermes-128.png +0 -0
- package/packages/web/dist/icons/hermes-192.png +0 -0
- package/packages/web/dist/icons/hermes-256.png +0 -0
- package/packages/web/dist/icons/hermes-32.png +0 -0
- package/packages/web/dist/icons/hermes-512.png +0 -0
- package/packages/web/dist/icons/hermes-64.png +0 -0
- package/packages/web/dist/icons/hermes.png +0 -0
- package/packages/web/dist/index.html +37 -0
- package/packages/web/dist/robots.txt +3 -0
- package/packages/web/dist/site.webmanifest +24 -0
- package/packages/web/public/brand-icon.png +0 -0
- package/packages/web/public/brand-logo.png +0 -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/public/favicon.ico +0 -0
- package/packages/web/public/favicon.svg +5 -0
- package/packages/web/public/icons/hermes-128.png +0 -0
- package/packages/web/public/icons/hermes-192.png +0 -0
- package/packages/web/public/icons/hermes-256.png +0 -0
- package/packages/web/public/icons/hermes-32.png +0 -0
- package/packages/web/public/icons/hermes-512.png +0 -0
- package/packages/web/public/icons/hermes-64.png +0 -0
- package/packages/web/public/icons/hermes.png +0 -0
- package/packages/web/public/robots.txt +3 -0
- package/packages/web/public/site.webmanifest +24 -0
- package/scripts/install-and-sync.sh +209 -0
- package/scripts/sync-skills.sh +605 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export const LABELS = {
|
|
2
|
+
sources: {
|
|
3
|
+
'hermes': 'Hermes 技能库',
|
|
4
|
+
'skills': '技能中心',
|
|
5
|
+
'project-runbook': '项目文档',
|
|
6
|
+
'mcp-config': 'MCP 配置',
|
|
7
|
+
'mcp': 'MCP 工具',
|
|
8
|
+
'codex-plugin': 'Codex 插件',
|
|
9
|
+
},
|
|
10
|
+
editors: {
|
|
11
|
+
'Hermes Agent': 'Hermes 代理',
|
|
12
|
+
'Skills Hub': '技能中心',
|
|
13
|
+
'Project Docs': '项目文档',
|
|
14
|
+
'MCP': 'MCP',
|
|
15
|
+
'MCP Hub': 'MCP 中心',
|
|
16
|
+
},
|
|
17
|
+
kinds: {
|
|
18
|
+
'skill': '技能',
|
|
19
|
+
'runbook': '手册',
|
|
20
|
+
'config': '配置',
|
|
21
|
+
'mcp-tool': 'MCP 工具',
|
|
22
|
+
'mcp': 'MCP',
|
|
23
|
+
'plugin': '插件',
|
|
24
|
+
},
|
|
25
|
+
categories: {
|
|
26
|
+
'devops': '云原生运维',
|
|
27
|
+
'mlops': '机器学习运维',
|
|
28
|
+
'software-development': '软件开发',
|
|
29
|
+
'creative': '创意工具',
|
|
30
|
+
'productivity': '生产力工具',
|
|
31
|
+
'research': '研究',
|
|
32
|
+
'apple': 'Apple 生态',
|
|
33
|
+
'media': '媒体处理',
|
|
34
|
+
'gaming': '游戏',
|
|
35
|
+
'red-teaming': '红队测试',
|
|
36
|
+
'github': 'GitHub',
|
|
37
|
+
'email': '邮件',
|
|
38
|
+
'leisure': '休闲娱乐',
|
|
39
|
+
'smart-home': '智能家居',
|
|
40
|
+
'social-media': '社交媒体',
|
|
41
|
+
'mcp': 'MCP 协议',
|
|
42
|
+
'note-taking': '笔记',
|
|
43
|
+
'skillhelper': '呼哈哈-技能助手',
|
|
44
|
+
'autonomous-ai-agents': '自主 AI 代理',
|
|
45
|
+
'kanban': '看板',
|
|
46
|
+
'eslint': 'ESLint',
|
|
47
|
+
},
|
|
48
|
+
brands: {
|
|
49
|
+
'GitHub': 'GitHub',
|
|
50
|
+
'OpenAI': 'OpenAI',
|
|
51
|
+
'Anthropic': 'Anthropic',
|
|
52
|
+
'Apple': 'Apple',
|
|
53
|
+
'Docker': 'Docker',
|
|
54
|
+
'HuggingFace': 'Hugging Face',
|
|
55
|
+
'Claude Code': 'Claude Code',
|
|
56
|
+
'Suno': 'Suno',
|
|
57
|
+
'Kubernetes': 'Kubernetes',
|
|
58
|
+
'Telegram': 'Telegram',
|
|
59
|
+
'Cloudflare': 'Cloudflare',
|
|
60
|
+
'Modal': 'Modal',
|
|
61
|
+
'Linear': 'Linear',
|
|
62
|
+
'Hermes': 'Hermes',
|
|
63
|
+
}
|
|
64
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// 语言判定 —— 本地零依赖,不走外网。
|
|
2
|
+
// 目的:在调用翻译 API 前短路掉已是中文的文本,避免无意义的外网请求。
|
|
3
|
+
//
|
|
4
|
+
// 判定规则:统计 CJK 统一表意文字(U+4E00–U+9FFF)在文本中的占比。
|
|
5
|
+
// 占比 >= 30% 视为中文,跳过翻译。技能描述多为英文夹中文术语
|
|
6
|
+
// (如"使用 MCP 协议"),纯中文描述远超 30%,纯英文描述为 0%,
|
|
7
|
+
// 阈值不敏感。
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 判定文本是否为中文
|
|
11
|
+
* @param {string} text - 待判定文本
|
|
12
|
+
* @returns {boolean} true 表示已是中文,无需翻译
|
|
13
|
+
*/
|
|
14
|
+
export function isChinese(text) {
|
|
15
|
+
if (!text || !text.trim()) return true; // 空文本视为无需翻译
|
|
16
|
+
const cjk = (text.match(/[一-鿿]/g) || []).length;
|
|
17
|
+
return cjk / text.length >= 0.3;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 检测文本源语言代码(用于翻译缓存的 from 字段)。
|
|
22
|
+
* google-translate-api-x 偶发不返回 from.language.code(部分文本 Google 无法判定源语言),
|
|
23
|
+
* 用本地检测兜底,避免 from 字段落盘为 'unknown'。
|
|
24
|
+
*
|
|
25
|
+
* 判定逻辑与 isChinese 一致:CJK 占比 >= 30% 视为中文,否则视为英文
|
|
26
|
+
* (技能描述以英文为主,其他语言罕见;中文已由 isChinese 判定)。
|
|
27
|
+
*
|
|
28
|
+
* @param {string} text - 待判定文本
|
|
29
|
+
* @returns {'zh'|'en'|'unknown'} 语言代码
|
|
30
|
+
*/
|
|
31
|
+
export function detectLanguage(text) {
|
|
32
|
+
if (!text || !text.trim()) return 'unknown';
|
|
33
|
+
return isChinese(text) ? 'zh' : 'en';
|
|
34
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export const ANTHROPIC_MODEL = 'claude-sonnet-4-6';
|
|
2
|
+
|
|
3
|
+
export async function callAnthropic(prompt, { max_tokens } = {}) {
|
|
4
|
+
if (!process.env.ANTHROPIC_API_KEY) {
|
|
5
|
+
throw new Error('ANTHROPIC_API_KEY not set');
|
|
6
|
+
}
|
|
7
|
+
if (!prompt || typeof prompt !== 'string') {
|
|
8
|
+
throw new Error('prompt must be a non-empty string');
|
|
9
|
+
}
|
|
10
|
+
if (!Number.isInteger(max_tokens) || max_tokens <= 0) {
|
|
11
|
+
throw new Error('max_tokens must be a positive integer');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
headers: {
|
|
17
|
+
'Content-Type': 'application/json',
|
|
18
|
+
'x-api-key': process.env.ANTHROPIC_API_KEY,
|
|
19
|
+
'anthropic-version': '2023-06-01',
|
|
20
|
+
},
|
|
21
|
+
body: JSON.stringify({
|
|
22
|
+
model: ANTHROPIC_MODEL,
|
|
23
|
+
max_tokens,
|
|
24
|
+
messages: [{
|
|
25
|
+
role: 'user',
|
|
26
|
+
content: prompt,
|
|
27
|
+
}],
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
const error = await response.text();
|
|
33
|
+
throw new Error(`Anthropic API: ${response.status} ${error}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const data = await response.json();
|
|
37
|
+
const text = data?.content?.[0]?.text;
|
|
38
|
+
if (typeof text !== 'string' || !text.trim()) {
|
|
39
|
+
throw new Error('Anthropic API returned empty text');
|
|
40
|
+
}
|
|
41
|
+
return text;
|
|
42
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// 翻译缓存 —— 双层结构:进程内 Map(热路径)+ 磁盘 JSON(跨重启)。
|
|
2
|
+
// key = md5(text):不同技能若描述相同则共享缓存条目;文件更新导致
|
|
3
|
+
// text 变化时 md5 自然不同,自动重译,无需 mtime 比对。
|
|
4
|
+
//
|
|
5
|
+
// 风格对齐 icon-extractor.mjs:内存 Map 命中即返回,磁盘异步落盘。
|
|
6
|
+
// purge 命令删除整个 SKILLHELPER_HOME 目录,本缓存文件随之清理。
|
|
7
|
+
|
|
8
|
+
import crypto from 'node:crypto';
|
|
9
|
+
import { translateCacheFile, ensureHomeDir, readJson, writeJson } from '../../../bin/lib/paths.mjs';
|
|
10
|
+
|
|
11
|
+
const CACHE_VERSION = 1;
|
|
12
|
+
const FLUSH_DELAY = 500; // 防抖落盘,避免高频写
|
|
13
|
+
|
|
14
|
+
let memoryCache = new Map();
|
|
15
|
+
let dirty = false;
|
|
16
|
+
let flushTimer = null;
|
|
17
|
+
let loaded = false;
|
|
18
|
+
|
|
19
|
+
function md5(text) {
|
|
20
|
+
return crypto.createHash('md5').update(text, 'utf8').digest('hex');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// LRU 上限:避免 translate-cache.json 无限膨胀。动态读取环境变量(测试用小值)。
|
|
24
|
+
function getMaxEntries() {
|
|
25
|
+
return parseInt(process.env.SKILLHELPER_TRANSLATE_CACHE_MAX || '5000', 10);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function load() {
|
|
29
|
+
if (loaded) return;
|
|
30
|
+
loaded = true;
|
|
31
|
+
const data = readJson(translateCacheFile(), null);
|
|
32
|
+
if (data && data.version === CACHE_VERSION && data.entries) {
|
|
33
|
+
memoryCache = new Map(Object.entries(data.entries));
|
|
34
|
+
// 加载时若超限,按 LRU 淘汰最旧条目
|
|
35
|
+
evictIfNeeded();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** LRU 淘汰:Map 迭代顺序 = 插入顺序,首个即最久未使用 */
|
|
40
|
+
function evictIfNeeded() {
|
|
41
|
+
const max = getMaxEntries();
|
|
42
|
+
while (memoryCache.size > max) {
|
|
43
|
+
const oldest = memoryCache.keys().next().value;
|
|
44
|
+
if (oldest === undefined) break;
|
|
45
|
+
memoryCache.delete(oldest);
|
|
46
|
+
dirty = true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 查询缓存(LRU 命中时移到末尾,标记最近使用)
|
|
52
|
+
* @param {string} text - 原文
|
|
53
|
+
* @returns {{from:string,to:string,result:string,translatedAt:string}|null} 缓存条目或 null
|
|
54
|
+
*/
|
|
55
|
+
export function getCached(text) {
|
|
56
|
+
load();
|
|
57
|
+
const key = md5(text);
|
|
58
|
+
const entry = memoryCache.get(key);
|
|
59
|
+
if (entry) {
|
|
60
|
+
// LRU: 命中时移到末尾(最近使用)
|
|
61
|
+
memoryCache.delete(key);
|
|
62
|
+
memoryCache.set(key, entry);
|
|
63
|
+
}
|
|
64
|
+
return entry || null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 写入缓存(内存立即生效,磁盘防抖落盘)。LRU 超限时淘汰最旧条目。
|
|
69
|
+
* @param {string} text - 原文
|
|
70
|
+
* @param {string} result - 译文
|
|
71
|
+
* @param {string} from - 源语言代码(如 'en')
|
|
72
|
+
*/
|
|
73
|
+
export function setCached(text, result, from) {
|
|
74
|
+
load();
|
|
75
|
+
const key = md5(text);
|
|
76
|
+
// LRU: 命中时先 delete 再 set,移到末尾(最近使用)
|
|
77
|
+
if (memoryCache.has(key)) memoryCache.delete(key);
|
|
78
|
+
memoryCache.set(key, {
|
|
79
|
+
from: from || 'unknown',
|
|
80
|
+
to: 'zh-CN',
|
|
81
|
+
text,
|
|
82
|
+
result,
|
|
83
|
+
translatedAt: new Date().toISOString(),
|
|
84
|
+
});
|
|
85
|
+
evictIfNeeded();
|
|
86
|
+
dirty = true;
|
|
87
|
+
scheduleFlush();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function scheduleFlush() {
|
|
91
|
+
if (flushTimer) return;
|
|
92
|
+
flushTimer = setTimeout(() => {
|
|
93
|
+
flushTimer = null;
|
|
94
|
+
flush();
|
|
95
|
+
}, FLUSH_DELAY);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 立即落盘。进程退出前应调用一次(onClose 钩子)。
|
|
100
|
+
*/
|
|
101
|
+
export function flush() {
|
|
102
|
+
if (!dirty) return;
|
|
103
|
+
ensureHomeDir();
|
|
104
|
+
const data = { version: CACHE_VERSION, entries: Object.fromEntries(memoryCache) };
|
|
105
|
+
try {
|
|
106
|
+
writeJson(translateCacheFile(), data);
|
|
107
|
+
dirty = false;
|
|
108
|
+
} catch (e) {
|
|
109
|
+
console.warn(`[translate-cache] flush failed: ${e.message}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 获取缓存条目数(测试 / 诊断用)
|
|
115
|
+
*/
|
|
116
|
+
export function size() {
|
|
117
|
+
load();
|
|
118
|
+
return memoryCache.size;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 重置缓存(仅测试用:清空内存并标记未加载,以便切换 SKILLHELPER_HOME 后重新加载)
|
|
123
|
+
*/
|
|
124
|
+
export function _resetForTest() {
|
|
125
|
+
memoryCache = new Map();
|
|
126
|
+
dirty = false;
|
|
127
|
+
if (flushTimer) {
|
|
128
|
+
clearTimeout(flushTimer);
|
|
129
|
+
flushTimer = null;
|
|
130
|
+
}
|
|
131
|
+
loaded = false;
|
|
132
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// 旧模式:整技能对象翻译,依赖 ANTHROPIC_API_KEY(需密钥,按全局规则使用前需询问)。
|
|
2
|
+
// 推荐使用 packages/server/src/index.mjs 的 translateText(Google 免费 + md5 缓存)。
|
|
3
|
+
// 本模块保留以兼容 /api/translate 的 id 旧模式调用,新代码勿直接依赖。
|
|
4
|
+
import { ANTHROPIC_MODEL, callAnthropic } from './llm-client.mjs';
|
|
5
|
+
|
|
6
|
+
export async function translateSkill(skill, targetLang = 'zh-CN') {
|
|
7
|
+
if (targetLang !== 'zh-CN' || skill.i18n?.name_zh) return skill;
|
|
8
|
+
|
|
9
|
+
if (!process.env.ANTHROPIC_API_KEY) {
|
|
10
|
+
console.warn('[translator] ANTHROPIC_API_KEY not set, skipping translation');
|
|
11
|
+
return skill;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const prompt = `Translate these skill metadata to Chinese (简体中文). Return JSON only, no markdown.
|
|
15
|
+
|
|
16
|
+
Current metadata:
|
|
17
|
+
- name: ${skill.name}
|
|
18
|
+
- description: ${skill.description || '(none)'}
|
|
19
|
+
- category: ${skill.category || '(none)'}
|
|
20
|
+
|
|
21
|
+
Return JSON format (translate only the values):
|
|
22
|
+
{
|
|
23
|
+
"name_zh": "中文名称",
|
|
24
|
+
"description_zh": "中文描述",
|
|
25
|
+
"category_zh": "中文分类"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
Guidelines:
|
|
29
|
+
- Keep technical terms like "API", "MCP", "LLM" in English when appropriate
|
|
30
|
+
- Use common Chinese equivalents for concepts (e.g., "skill" → "技能", "rule" → "规则")
|
|
31
|
+
- Keep descriptions concise (under 100 chars)
|
|
32
|
+
- Preserve the technical meaning
|
|
33
|
+
- Return valid JSON only`;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const text = await callAnthropic(prompt, { max_tokens: 500 });
|
|
37
|
+
const translated = JSON.parse(text);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
...skill,
|
|
41
|
+
i18n: {
|
|
42
|
+
name_zh: translated.name_zh,
|
|
43
|
+
description_zh: translated.description_zh,
|
|
44
|
+
category_zh: translated.category_zh,
|
|
45
|
+
translatedAt: new Date().toISOString(),
|
|
46
|
+
translationModel: ANTHROPIC_MODEL,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
} catch (e) {
|
|
50
|
+
console.error(`[translator] error: ${e.message}`);
|
|
51
|
+
return skill;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# @skillhelper/web
|
|
2
|
+
|
|
3
|
+
SkillHelper 的前端 SPA:**Vite + React 18 + TypeScript(strict) + Tailwind + shadcn 风格组件**。
|
|
4
|
+
|
|
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`。
|