aicodeswitch 2.0.4 → 2.0.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/CHANGELOG.md +4 -0
- package/CLAUDE.md +9 -2
- package/README.md +2 -1
- package/TECH.md +193 -0
- package/dist/server/config.js +4 -0
- package/dist/server/main.js +727 -0
- package/dist/server/proxy-server.js +31 -5
- package/dist/ui/assets/index-BOY_bl12.js +441 -0
- package/dist/ui/assets/{index-CO1PHif-.css → index-BOlCGnMv.css} +1 -1
- package/dist/ui/index.html +2 -2
- package/package.json +3 -3
- package/dist/ui/assets/index-kGCJkqIq.js +0 -423
package/dist/server/main.js
CHANGED
|
@@ -18,6 +18,7 @@ const dotenv_1 = __importDefault(require("dotenv"));
|
|
|
18
18
|
const fs_1 = __importDefault(require("fs"));
|
|
19
19
|
const path_1 = __importDefault(require("path"));
|
|
20
20
|
const crypto_1 = require("crypto");
|
|
21
|
+
const https_proxy_agent_1 = require("https-proxy-agent");
|
|
21
22
|
const database_1 = require("./database");
|
|
22
23
|
const proxy_server_1 = require("./proxy-server");
|
|
23
24
|
const os_1 = __importDefault(require("os"));
|
|
@@ -25,6 +26,7 @@ const auth_1 = require("./auth");
|
|
|
25
26
|
const version_check_1 = require("./version-check");
|
|
26
27
|
const utils_1 = require("./utils");
|
|
27
28
|
const config_metadata_1 = require("./config-metadata");
|
|
29
|
+
const config_1 = require("./config");
|
|
28
30
|
const dotenvPath = path_1.default.resolve(os_1.default.homedir(), '.aicodeswitch/aicodeswitch.conf');
|
|
29
31
|
if (fs_1.default.existsSync(dotenvPath)) {
|
|
30
32
|
dotenv_1.default.config({ path: dotenvPath });
|
|
@@ -32,6 +34,37 @@ if (fs_1.default.existsSync(dotenvPath)) {
|
|
|
32
34
|
const host = process.env.HOST || '127.0.0.1';
|
|
33
35
|
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 4567;
|
|
34
36
|
const dataDir = process.env.DATA_DIR ? path_1.default.resolve(process.cwd(), process.env.DATA_DIR) : path_1.default.join(os_1.default.homedir(), '.aicodeswitch/data');
|
|
37
|
+
let globalProxyConfig = null;
|
|
38
|
+
function updateProxyConfig(config) {
|
|
39
|
+
if (config.proxyEnabled && config.proxyUrl) {
|
|
40
|
+
globalProxyConfig = {
|
|
41
|
+
enabled: true,
|
|
42
|
+
url: config.proxyUrl,
|
|
43
|
+
username: config.proxyUsername,
|
|
44
|
+
password: config.proxyPassword,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
globalProxyConfig = null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function getProxyAgent() {
|
|
52
|
+
if (!(globalProxyConfig === null || globalProxyConfig === void 0 ? void 0 : globalProxyConfig.enabled) || !globalProxyConfig.url) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const url = globalProxyConfig.url;
|
|
57
|
+
const proxyUrl = url.startsWith('http') ? url : `http://${url}`;
|
|
58
|
+
if (globalProxyConfig.username && globalProxyConfig.password) {
|
|
59
|
+
const proxyUrlWithAuth = proxyUrl.replace('://', `://${encodeURIComponent(globalProxyConfig.username)}:${encodeURIComponent(globalProxyConfig.password)}@`);
|
|
60
|
+
return proxyUrlWithAuth;
|
|
61
|
+
}
|
|
62
|
+
return proxyUrl;
|
|
63
|
+
}
|
|
64
|
+
catch (_a) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
35
68
|
const app = (0, express_1.default)();
|
|
36
69
|
app.use((0, cors_1.default)());
|
|
37
70
|
app.use(express_1.default.json({ limit: '10mb' }));
|
|
@@ -298,7 +331,364 @@ const checkCodexBackupExists = () => {
|
|
|
298
331
|
return false;
|
|
299
332
|
}
|
|
300
333
|
};
|
|
334
|
+
const getCentralSkillsDir = () => {
|
|
335
|
+
return path_1.default.join(os_1.default.homedir(), '.aicodeswitch', 'skills');
|
|
336
|
+
};
|
|
337
|
+
function sanitizeDirName(name) {
|
|
338
|
+
return name
|
|
339
|
+
.trim()
|
|
340
|
+
.toLowerCase()
|
|
341
|
+
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-')
|
|
342
|
+
.replace(/^-+|-+$/g, '')
|
|
343
|
+
.substring(0, 100);
|
|
344
|
+
}
|
|
345
|
+
function getSkillDirByName(name) {
|
|
346
|
+
const sanitizedName = sanitizeDirName(name);
|
|
347
|
+
const centralDir = getCentralSkillsDir();
|
|
348
|
+
return path_1.default.join(centralDir, sanitizedName);
|
|
349
|
+
}
|
|
350
|
+
const getSkillSymlinkPath = (skillId, targetType) => {
|
|
351
|
+
const baseDir = targetType === 'claude-code' ? '.claude' : '.codex';
|
|
352
|
+
return path_1.default.join(os_1.default.homedir(), baseDir, 'skills', skillId);
|
|
353
|
+
};
|
|
354
|
+
function isSkillSymlinkExists(skillId, targetType) {
|
|
355
|
+
const symlinkPath = getSkillSymlinkPath(skillId, targetType);
|
|
356
|
+
try {
|
|
357
|
+
const stats = fs_1.default.lstatSync(symlinkPath);
|
|
358
|
+
return stats.isSymbolicLink();
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function createSkillSymlink(skillId, targetType) {
|
|
365
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
366
|
+
try {
|
|
367
|
+
const centralDir = getCentralSkillsDir();
|
|
368
|
+
const skillDir = path_1.default.join(centralDir, skillId);
|
|
369
|
+
const symlinkPath = getSkillSymlinkPath(skillId, targetType);
|
|
370
|
+
if (!fs_1.default.existsSync(skillDir)) {
|
|
371
|
+
return { success: false, error: 'Skill目录不存在' };
|
|
372
|
+
}
|
|
373
|
+
const targetBaseDir = path_1.default.dirname(symlinkPath);
|
|
374
|
+
if (!fs_1.default.existsSync(targetBaseDir)) {
|
|
375
|
+
fs_1.default.mkdirSync(targetBaseDir, { recursive: true });
|
|
376
|
+
}
|
|
377
|
+
if (fs_1.default.existsSync(symlinkPath)) {
|
|
378
|
+
if (fs_1.default.lstatSync(symlinkPath).isSymbolicLink()) {
|
|
379
|
+
fs_1.default.unlinkSync(symlinkPath);
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
return { success: false, error: '目标路径已存在非软链接文件' };
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
const relativePath = path_1.default.relative(targetBaseDir, skillDir);
|
|
386
|
+
if (process.platform === 'win32') {
|
|
387
|
+
fs_1.default.symlinkSync(skillDir, symlinkPath, 'junction');
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
fs_1.default.symlinkSync(relativePath, symlinkPath);
|
|
391
|
+
}
|
|
392
|
+
return { success: true };
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
return { success: false, error: error.message };
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
function removeSkillSymlink(skillId, targetType) {
|
|
400
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
401
|
+
try {
|
|
402
|
+
const symlinkPath = getSkillSymlinkPath(skillId, targetType);
|
|
403
|
+
if (!fs_1.default.existsSync(symlinkPath)) {
|
|
404
|
+
return { success: true };
|
|
405
|
+
}
|
|
406
|
+
const stats = fs_1.default.lstatSync(symlinkPath);
|
|
407
|
+
if (stats.isSymbolicLink()) {
|
|
408
|
+
fs_1.default.unlinkSync(symlinkPath);
|
|
409
|
+
}
|
|
410
|
+
return { success: true };
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
return { success: false, error: error.message };
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
function parseGitHubUrl(githubUrl, subPath) {
|
|
418
|
+
// 解析各种GitHub URL格式
|
|
419
|
+
const patterns = [
|
|
420
|
+
// https://github.com/owner/repo/tree/{ref}/path/to/dir
|
|
421
|
+
/github\.com\/([^\/]+)\/([^\/]+)(?:\/tree\/[^\/]+)?\/(.*)/,
|
|
422
|
+
// git@github.com:owner/repo.git
|
|
423
|
+
/git@github\.com:([^\/]+)\/([^\/]+)(?:\.git)?$/,
|
|
424
|
+
];
|
|
425
|
+
for (const pattern of patterns) {
|
|
426
|
+
const match = githubUrl.match(pattern);
|
|
427
|
+
if (match) {
|
|
428
|
+
let owner = match[1];
|
|
429
|
+
let repo = match[2];
|
|
430
|
+
let repoPath = match[3] || '';
|
|
431
|
+
// 移除.git后缀(如果存在)
|
|
432
|
+
if (repo.endsWith('.git')) {
|
|
433
|
+
repo = repo.slice(0, -4);
|
|
434
|
+
}
|
|
435
|
+
// 拼接完整路径
|
|
436
|
+
const fullPath = subPath ? path_1.default.join(repoPath, subPath) : repoPath;
|
|
437
|
+
return { owner, repo, path: fullPath };
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
throw new Error(`无法解析GitHub URL: ${githubUrl}`);
|
|
441
|
+
}
|
|
442
|
+
// 重试配置
|
|
443
|
+
const MAX_RETRY_COUNT = 3;
|
|
444
|
+
const RETRY_DELAY_MS = 1000;
|
|
445
|
+
// 带重试的fetch包装函数,支持代理
|
|
446
|
+
function fetchWithRetry(url_1) {
|
|
447
|
+
return __awaiter(this, arguments, void 0, function* (url, options = {}, retryCount = MAX_RETRY_COUNT) {
|
|
448
|
+
let lastError = null;
|
|
449
|
+
// 获取代理配置
|
|
450
|
+
const proxyUrl = getProxyAgent();
|
|
451
|
+
for (let i = 0; i < retryCount; i++) {
|
|
452
|
+
try {
|
|
453
|
+
const fetchOptions = Object.assign({}, options);
|
|
454
|
+
// 如果启用了代理,添加 agent
|
|
455
|
+
if (proxyUrl) {
|
|
456
|
+
try {
|
|
457
|
+
fetchOptions.agent = new https_proxy_agent_1.HttpsProxyAgent(proxyUrl);
|
|
458
|
+
console.log(`使用代理请求: ${url}`);
|
|
459
|
+
}
|
|
460
|
+
catch (agentError) {
|
|
461
|
+
console.warn('创建代理 agent 失败,将跳过代理:', agentError);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const response = yield fetch(url, fetchOptions);
|
|
465
|
+
// 如果是403限流错误,等待后重试
|
|
466
|
+
if (response.status === 403) {
|
|
467
|
+
const resetTime = response.headers.get('X-RateLimit-Reset');
|
|
468
|
+
const waitTime = resetTime
|
|
469
|
+
? Math.max(parseInt(resetTime) * 1000 - Date.now(), RETRY_DELAY_MS)
|
|
470
|
+
: RETRY_DELAY_MS * (i + 1);
|
|
471
|
+
console.warn(`GitHub API限流,等待 ${Math.ceil(waitTime / 1000)} 秒后重试...`);
|
|
472
|
+
yield new Promise(resolve => setTimeout(resolve, Math.min(waitTime, 30000)));
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
return response;
|
|
476
|
+
}
|
|
477
|
+
catch (error) {
|
|
478
|
+
lastError = error;
|
|
479
|
+
console.warn(`请求失败 (${i + 1}/${retryCount}):`, error);
|
|
480
|
+
if (i < retryCount - 1) {
|
|
481
|
+
yield new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS * (i + 1)));
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
throw lastError || new Error('请求失败');
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
// 使用GitHub Contents API获取目录内容
|
|
489
|
+
function getGitHubContents(owner, repo, filePath, ref) {
|
|
490
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
491
|
+
let apiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${filePath}`;
|
|
492
|
+
if (ref) {
|
|
493
|
+
apiUrl += `?ref=${ref}`;
|
|
494
|
+
}
|
|
495
|
+
const response = yield fetchWithRetry(apiUrl, {
|
|
496
|
+
headers: {
|
|
497
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
498
|
+
'User-Agent': 'AICodeSwitch-SkillsManager',
|
|
499
|
+
},
|
|
500
|
+
});
|
|
501
|
+
if (!response.ok) {
|
|
502
|
+
if (response.status === 403) {
|
|
503
|
+
// API限流
|
|
504
|
+
const resetTime = response.headers.get('X-RateLimit-Reset');
|
|
505
|
+
throw new Error(`GitHub API限流,请稍后再试${resetTime ? `(${new Date(parseInt(resetTime) * 1000).toLocaleTimeString()})` : ''}`);
|
|
506
|
+
}
|
|
507
|
+
if (response.status === 404) {
|
|
508
|
+
throw new Error(`路径不存在: ${filePath}`);
|
|
509
|
+
}
|
|
510
|
+
throw new Error(`GitHub API错误: ${response.status} ${response.statusText}`);
|
|
511
|
+
}
|
|
512
|
+
const data = yield response.json();
|
|
513
|
+
// Contents API对单个文件返回对象,对目录返回数组
|
|
514
|
+
return Array.isArray(data) ? data : [data];
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
// 下载单个文件(带重试)
|
|
518
|
+
function downloadFile(downloadUrl_1, targetPath_1) {
|
|
519
|
+
return __awaiter(this, arguments, void 0, function* (downloadUrl, targetPath, retryCount = MAX_RETRY_COUNT) {
|
|
520
|
+
let lastError = null;
|
|
521
|
+
for (let i = 0; i < retryCount; i++) {
|
|
522
|
+
try {
|
|
523
|
+
const response = yield fetchWithRetry(downloadUrl);
|
|
524
|
+
if (!response.ok) {
|
|
525
|
+
throw new Error(`下载失败: ${response.status} ${response.statusText}`);
|
|
526
|
+
}
|
|
527
|
+
const content = yield response.text();
|
|
528
|
+
// 确保目标目录存在
|
|
529
|
+
const dir = path_1.default.dirname(targetPath);
|
|
530
|
+
if (!fs_1.default.existsSync(dir)) {
|
|
531
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
532
|
+
}
|
|
533
|
+
fs_1.default.writeFileSync(targetPath, content, 'utf-8');
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
catch (error) {
|
|
537
|
+
lastError = error;
|
|
538
|
+
console.warn(`文件下载失败 (${i + 1}/${retryCount}):`, downloadUrl);
|
|
539
|
+
if (i < retryCount - 1) {
|
|
540
|
+
yield new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS * (i + 1)));
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
throw lastError || new Error('文件下载失败');
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
// 递归下载目录内容
|
|
548
|
+
function downloadDirectory(owner, repo, contents, basePath, ref) {
|
|
549
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
550
|
+
let downloadedCount = 0;
|
|
551
|
+
for (const item of contents) {
|
|
552
|
+
const relativePath = item.path ? item.path.split('/').slice(-1)[0] : item.name;
|
|
553
|
+
const targetPath = path_1.default.join(basePath, relativePath);
|
|
554
|
+
if (item.type === 'file') {
|
|
555
|
+
// 下载文件
|
|
556
|
+
if (item.download_url) {
|
|
557
|
+
yield downloadFile(item.download_url, targetPath);
|
|
558
|
+
downloadedCount++;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
else if (item.type === 'dir') {
|
|
562
|
+
// 递归下载子目录
|
|
563
|
+
const subContents = yield getGitHubContents(owner, repo, item.path, ref);
|
|
564
|
+
const subCount = yield downloadDirectory(owner, repo, subContents, targetPath, ref);
|
|
565
|
+
downloadedCount += subCount;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return downloadedCount;
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
// 验证下载完整性
|
|
572
|
+
function verifyDownload(targetDir) {
|
|
573
|
+
if (!fs_1.default.existsSync(targetDir)) {
|
|
574
|
+
return { valid: false, filesCount: 0, error: '目标目录不存在' };
|
|
575
|
+
}
|
|
576
|
+
const hasSkillJson = fs_1.default.existsSync(path_1.default.join(targetDir, 'skill.json'));
|
|
577
|
+
const hasSkillMd = fs_1.default.existsSync(path_1.default.join(targetDir, 'SKILL.md'));
|
|
578
|
+
if (!hasSkillJson && !hasSkillMd) {
|
|
579
|
+
return { valid: false, filesCount: 0, error: '缺少必要文件: skill.json 或 SKILL.md' };
|
|
580
|
+
}
|
|
581
|
+
// 统计下载的文件数量
|
|
582
|
+
let filesCount = 0;
|
|
583
|
+
try {
|
|
584
|
+
const countFiles = (dir) => {
|
|
585
|
+
const items = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
586
|
+
for (const item of items) {
|
|
587
|
+
if (item.isFile()) {
|
|
588
|
+
filesCount++;
|
|
589
|
+
}
|
|
590
|
+
else if (item.isDirectory()) {
|
|
591
|
+
countFiles(path_1.default.join(dir, item.name));
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
countFiles(targetDir);
|
|
596
|
+
}
|
|
597
|
+
catch (error) {
|
|
598
|
+
return { valid: false, filesCount: 0, error: '无法读取目录' };
|
|
599
|
+
}
|
|
600
|
+
if (filesCount === 0) {
|
|
601
|
+
return { valid: false, filesCount: 0, error: '下载的文件为空' };
|
|
602
|
+
}
|
|
603
|
+
return { valid: true, filesCount };
|
|
604
|
+
}
|
|
605
|
+
// 从GitHub下载指定路径的skill
|
|
606
|
+
function downloadSkillFromGitHub(githubUrl, skillPath, targetDir) {
|
|
607
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
608
|
+
try {
|
|
609
|
+
// 解析GitHub URL
|
|
610
|
+
const { owner, repo, path: repoPath } = parseGitHubUrl(githubUrl, skillPath);
|
|
611
|
+
// 构造完整路径
|
|
612
|
+
const fullPath = repoPath || skillPath;
|
|
613
|
+
if (!fullPath) {
|
|
614
|
+
throw new Error('无效的skill路径');
|
|
615
|
+
}
|
|
616
|
+
// 获取目录内容
|
|
617
|
+
const contents = yield getGitHubContents(owner, repo, fullPath);
|
|
618
|
+
if (contents.length === 0) {
|
|
619
|
+
throw new Error('目录为空');
|
|
620
|
+
}
|
|
621
|
+
// 确保目标目录存在
|
|
622
|
+
if (!fs_1.default.existsSync(targetDir)) {
|
|
623
|
+
fs_1.default.mkdirSync(targetDir, { recursive: true });
|
|
624
|
+
}
|
|
625
|
+
// 递归下载所有文件
|
|
626
|
+
const filesDownloaded = yield downloadDirectory(owner, repo, contents, targetDir);
|
|
627
|
+
if (filesDownloaded === 0) {
|
|
628
|
+
throw new Error('未能下载任何文件');
|
|
629
|
+
}
|
|
630
|
+
// 验证下载完整性
|
|
631
|
+
const verification = verifyDownload(targetDir);
|
|
632
|
+
if (!verification.valid) {
|
|
633
|
+
// 清理不完整的下载
|
|
634
|
+
fs_1.default.rmSync(targetDir, { recursive: true, force: true });
|
|
635
|
+
throw new Error(`下载验证失败: ${verification.error}`);
|
|
636
|
+
}
|
|
637
|
+
return { success: true, filesDownloaded };
|
|
638
|
+
}
|
|
639
|
+
catch (error) {
|
|
640
|
+
return { success: false, filesDownloaded: 0, error: error.message };
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
const listInstalledSkills = () => {
|
|
645
|
+
const result = new Map();
|
|
646
|
+
const centralDir = getCentralSkillsDir();
|
|
647
|
+
if (!fs_1.default.existsSync(centralDir)) {
|
|
648
|
+
return [];
|
|
649
|
+
}
|
|
650
|
+
const entries = fs_1.default.readdirSync(centralDir, { withFileTypes: true });
|
|
651
|
+
entries.filter(entry => entry.isDirectory()).forEach(entry => {
|
|
652
|
+
const skillId = entry.name;
|
|
653
|
+
const skillDir = path_1.default.join(centralDir, skillId);
|
|
654
|
+
const metadataPath = path_1.default.join(skillDir, 'skill.json');
|
|
655
|
+
if (!fs_1.default.existsSync(metadataPath)) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
try {
|
|
659
|
+
const metadata = JSON.parse(fs_1.default.readFileSync(metadataPath, 'utf-8'));
|
|
660
|
+
const enabledTargets = [];
|
|
661
|
+
['claude-code', 'codex'].forEach((targetType) => {
|
|
662
|
+
if (isSkillSymlinkExists(skillId, targetType)) {
|
|
663
|
+
enabledTargets.push(targetType);
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
const existing = result.get(skillId);
|
|
667
|
+
if (existing) {
|
|
668
|
+
existing.targets = [...new Set([...existing.targets, ...(metadata.targets || [])])];
|
|
669
|
+
existing.enabledTargets = enabledTargets;
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
result.set(skillId, {
|
|
673
|
+
id: skillId,
|
|
674
|
+
name: metadata.name || skillId,
|
|
675
|
+
description: metadata.description,
|
|
676
|
+
targets: metadata.targets || [],
|
|
677
|
+
enabledTargets: enabledTargets,
|
|
678
|
+
githubUrl: metadata.githubUrl,
|
|
679
|
+
skillPath: metadata.skillPath,
|
|
680
|
+
installedAt: metadata.installedAt || Date.now(),
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
catch (error) {
|
|
685
|
+
console.error(`Failed to parse skill metadata for ${skillId}:`, error);
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
return Array.from(result.values()).sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'));
|
|
689
|
+
};
|
|
301
690
|
const registerRoutes = (dbManager, proxyServer) => {
|
|
691
|
+
updateProxyConfig(dbManager.getConfig());
|
|
302
692
|
app.get('/health', (_req, res) => res.json({ status: 'ok' }));
|
|
303
693
|
// 鉴权相关路由 - 公开访问
|
|
304
694
|
app.get('/api/auth/status', (_req, res) => {
|
|
@@ -452,9 +842,346 @@ const registerRoutes = (dbManager, proxyServer) => {
|
|
|
452
842
|
const result = dbManager.updateConfig(config);
|
|
453
843
|
if (result) {
|
|
454
844
|
yield proxyServer.updateConfig(config);
|
|
845
|
+
updateProxyConfig(config);
|
|
455
846
|
}
|
|
456
847
|
res.json(result);
|
|
457
848
|
})));
|
|
849
|
+
// Skills 管理相关
|
|
850
|
+
app.get('/api/skills/installed', (_req, res) => {
|
|
851
|
+
const skills = listInstalledSkills();
|
|
852
|
+
res.json(skills);
|
|
853
|
+
});
|
|
854
|
+
app.post('/api/skills/search', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
855
|
+
var _a, _b;
|
|
856
|
+
const { query } = req.body;
|
|
857
|
+
if (!query || !query.trim()) {
|
|
858
|
+
res.status(400).json({ error: 'Query is required' });
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
if (!config_1.SKILLSMP_API_KEY) {
|
|
862
|
+
res.status(500).json({ error: 'SKILLSMP_API_KEY 未配置' });
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
const url = `https://skillsmp.com/api/v1/skills/ai-search?q=${encodeURIComponent(query.trim())}`;
|
|
866
|
+
const response = yield fetch(url, {
|
|
867
|
+
headers: {
|
|
868
|
+
Authorization: `Bearer ${config_1.SKILLSMP_API_KEY}`,
|
|
869
|
+
},
|
|
870
|
+
});
|
|
871
|
+
if (!response.ok) {
|
|
872
|
+
let errorMessage = 'Skills 搜索失败';
|
|
873
|
+
try {
|
|
874
|
+
const errorBody = yield response.json();
|
|
875
|
+
errorMessage = ((_a = errorBody === null || errorBody === void 0 ? void 0 : errorBody.error) === null || _a === void 0 ? void 0 : _a.message) || errorMessage;
|
|
876
|
+
}
|
|
877
|
+
catch (error) {
|
|
878
|
+
errorMessage = yield response.text();
|
|
879
|
+
}
|
|
880
|
+
res.status(response.status).json({ error: errorMessage });
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
const data = yield response.json();
|
|
884
|
+
const results = (((_b = data === null || data === void 0 ? void 0 : data.data) === null || _b === void 0 ? void 0 : _b.data) || [])
|
|
885
|
+
.map((item) => item === null || item === void 0 ? void 0 : item.skill)
|
|
886
|
+
.filter(Boolean)
|
|
887
|
+
.map((skill) => {
|
|
888
|
+
const tags = [
|
|
889
|
+
skill.author ? `作者: ${skill.author}` : null,
|
|
890
|
+
typeof skill.stars === 'number' ? `⭐ ${skill.stars}` : null,
|
|
891
|
+
].filter(Boolean);
|
|
892
|
+
const result = {
|
|
893
|
+
id: skill.id,
|
|
894
|
+
name: skill.name || skill.id,
|
|
895
|
+
description: skill.description,
|
|
896
|
+
tags: tags.length > 0 ? tags : [],
|
|
897
|
+
url: skill.githubUrl || skill.skillUrl,
|
|
898
|
+
};
|
|
899
|
+
return result;
|
|
900
|
+
});
|
|
901
|
+
res.json(results);
|
|
902
|
+
})));
|
|
903
|
+
app.post('/api/skills/install', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
904
|
+
const { skillId, name, description, tags, githubUrl, skillPath } = req.body;
|
|
905
|
+
if (!skillId) {
|
|
906
|
+
const response = {
|
|
907
|
+
success: false,
|
|
908
|
+
message: '缺少 Skill ID',
|
|
909
|
+
};
|
|
910
|
+
res.status(400).json(response);
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
const skillName = name || skillId;
|
|
914
|
+
const skillDir = getSkillDirByName(skillName);
|
|
915
|
+
const sanitizedDirName = path_1.default.basename(skillDir);
|
|
916
|
+
if (fs_1.default.existsSync(skillDir)) {
|
|
917
|
+
const existingSkillJson = path_1.default.join(skillDir, 'skill.json');
|
|
918
|
+
let existingMetadata = null;
|
|
919
|
+
if (fs_1.default.existsSync(existingSkillJson)) {
|
|
920
|
+
try {
|
|
921
|
+
existingMetadata = JSON.parse(fs_1.default.readFileSync(existingSkillJson, 'utf-8'));
|
|
922
|
+
}
|
|
923
|
+
catch (e) {
|
|
924
|
+
// 忽略解析错误
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
if (githubUrl) {
|
|
928
|
+
const downloadResult = yield downloadSkillFromGitHub(githubUrl, skillPath || '', skillDir);
|
|
929
|
+
if (!downloadResult.success) {
|
|
930
|
+
const response = {
|
|
931
|
+
success: false,
|
|
932
|
+
message: `下载失败: ${downloadResult.error}`,
|
|
933
|
+
};
|
|
934
|
+
res.status(500).json(response);
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
const metadata = {
|
|
938
|
+
id: sanitizedDirName,
|
|
939
|
+
name: name || (existingMetadata === null || existingMetadata === void 0 ? void 0 : existingMetadata.name) || skillId,
|
|
940
|
+
description: description || (existingMetadata === null || existingMetadata === void 0 ? void 0 : existingMetadata.description),
|
|
941
|
+
tags: tags || (existingMetadata === null || existingMetadata === void 0 ? void 0 : existingMetadata.tags),
|
|
942
|
+
githubUrl,
|
|
943
|
+
skillPath,
|
|
944
|
+
targets: [...((existingMetadata === null || existingMetadata === void 0 ? void 0 : existingMetadata.targets) || [])],
|
|
945
|
+
enabledTargets: [...((existingMetadata === null || existingMetadata === void 0 ? void 0 : existingMetadata.enabledTargets) || [])],
|
|
946
|
+
installedAt: (existingMetadata === null || existingMetadata === void 0 ? void 0 : existingMetadata.installedAt) || Date.now(),
|
|
947
|
+
updatedAt: Date.now(),
|
|
948
|
+
filesDownloaded: downloadResult.filesDownloaded,
|
|
949
|
+
};
|
|
950
|
+
fs_1.default.writeFileSync(path_1.default.join(skillDir, 'skill.json'), JSON.stringify(metadata, null, 2));
|
|
951
|
+
const response = {
|
|
952
|
+
success: true,
|
|
953
|
+
installedSkill: {
|
|
954
|
+
id: sanitizedDirName,
|
|
955
|
+
name: metadata.name,
|
|
956
|
+
description: metadata.description,
|
|
957
|
+
targets: metadata.targets,
|
|
958
|
+
enabledTargets: metadata.enabledTargets,
|
|
959
|
+
githubUrl,
|
|
960
|
+
skillPath,
|
|
961
|
+
installedAt: metadata.installedAt,
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
res.json(response);
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
else {
|
|
969
|
+
fs_1.default.mkdirSync(skillDir, { recursive: true });
|
|
970
|
+
}
|
|
971
|
+
if (githubUrl) {
|
|
972
|
+
const downloadResult = yield downloadSkillFromGitHub(githubUrl, skillPath || '', skillDir);
|
|
973
|
+
if (!downloadResult.success) {
|
|
974
|
+
const response = {
|
|
975
|
+
success: false,
|
|
976
|
+
message: `下载失败: ${downloadResult.error}`,
|
|
977
|
+
};
|
|
978
|
+
res.status(500).json(response);
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
const metadata = {
|
|
983
|
+
id: sanitizedDirName,
|
|
984
|
+
name: name || skillId,
|
|
985
|
+
description,
|
|
986
|
+
tags,
|
|
987
|
+
targets: [],
|
|
988
|
+
enabledTargets: [],
|
|
989
|
+
githubUrl,
|
|
990
|
+
skillPath,
|
|
991
|
+
installedAt: Date.now(),
|
|
992
|
+
};
|
|
993
|
+
fs_1.default.writeFileSync(path_1.default.join(skillDir, 'skill.json'), JSON.stringify(metadata, null, 2));
|
|
994
|
+
const response = {
|
|
995
|
+
success: true,
|
|
996
|
+
installedSkill: {
|
|
997
|
+
id: sanitizedDirName,
|
|
998
|
+
name: metadata.name,
|
|
999
|
+
description: metadata.description,
|
|
1000
|
+
targets: [],
|
|
1001
|
+
enabledTargets: [],
|
|
1002
|
+
githubUrl,
|
|
1003
|
+
skillPath,
|
|
1004
|
+
installedAt: metadata.installedAt,
|
|
1005
|
+
}
|
|
1006
|
+
};
|
|
1007
|
+
res.json(response);
|
|
1008
|
+
})));
|
|
1009
|
+
app.post('/api/skills/create-local', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1010
|
+
const { name, description, instruction, link, targets } = req.body;
|
|
1011
|
+
if (!(name === null || name === void 0 ? void 0 : name.trim())) {
|
|
1012
|
+
res.status(400).json({ success: false, message: '请填写 Skill 名称' });
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
if (!(description === null || description === void 0 ? void 0 : description.trim())) {
|
|
1016
|
+
res.status(400).json({ success: false, message: '请填写描述' });
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
if (!(instruction === null || instruction === void 0 ? void 0 : instruction.trim())) {
|
|
1020
|
+
res.status(400).json({ success: false, message: '请填写指令' });
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
if (!targets || targets.length === 0) {
|
|
1024
|
+
res.status(400).json({ success: false, message: '请至少选择一个安装目标' });
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
const skillDir = getSkillDirByName(name);
|
|
1028
|
+
const sanitizedDirName = path_1.default.basename(skillDir);
|
|
1029
|
+
if (fs_1.default.existsSync(skillDir)) {
|
|
1030
|
+
res.status(200).json({ success: false, message: `Skill "${name}" 已存在` });
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
fs_1.default.mkdirSync(skillDir, { recursive: true });
|
|
1034
|
+
const skillMdContent = `---
|
|
1035
|
+
name: ${sanitizedDirName}
|
|
1036
|
+
description: ${description.trim()}
|
|
1037
|
+
---
|
|
1038
|
+
|
|
1039
|
+
# ${name.trim()}
|
|
1040
|
+
|
|
1041
|
+
${description.trim()}
|
|
1042
|
+
|
|
1043
|
+
## 指令
|
|
1044
|
+
|
|
1045
|
+
${instruction}
|
|
1046
|
+
`;
|
|
1047
|
+
fs_1.default.writeFileSync(path_1.default.join(skillDir, 'SKILL.md'), skillMdContent, 'utf-8');
|
|
1048
|
+
const metadata = {
|
|
1049
|
+
id: sanitizedDirName,
|
|
1050
|
+
name: name.trim(),
|
|
1051
|
+
description: description.trim(),
|
|
1052
|
+
tags: [],
|
|
1053
|
+
targets: targets,
|
|
1054
|
+
enabledTargets: [],
|
|
1055
|
+
githubUrl: link || '',
|
|
1056
|
+
skillPath: '',
|
|
1057
|
+
installedAt: Date.now(),
|
|
1058
|
+
};
|
|
1059
|
+
fs_1.default.writeFileSync(path_1.default.join(skillDir, 'skill.json'), JSON.stringify(metadata, null, 2));
|
|
1060
|
+
const response = {
|
|
1061
|
+
success: true,
|
|
1062
|
+
installedSkill: {
|
|
1063
|
+
id: sanitizedDirName,
|
|
1064
|
+
name: metadata.name,
|
|
1065
|
+
description: metadata.description,
|
|
1066
|
+
targets: metadata.targets,
|
|
1067
|
+
enabledTargets: [],
|
|
1068
|
+
githubUrl: link || '',
|
|
1069
|
+
skillPath: '',
|
|
1070
|
+
installedAt: metadata.installedAt,
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
res.json(response);
|
|
1074
|
+
})));
|
|
1075
|
+
// 获取skill详细信息(用于显示和安装)
|
|
1076
|
+
app.get('/api/skills/:skillId/details', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1077
|
+
const { skillId } = req.params;
|
|
1078
|
+
if (!config_1.SKILLSMP_API_KEY) {
|
|
1079
|
+
res.status(500).json({ error: 'SKILLSMP_API_KEY 未配置' });
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
try {
|
|
1083
|
+
const url = `https://skillsmp.com/api/v1/skills/${skillId}`;
|
|
1084
|
+
const response = yield fetch(url, {
|
|
1085
|
+
headers: {
|
|
1086
|
+
Authorization: `Bearer ${config_1.SKILLSMP_API_KEY}`,
|
|
1087
|
+
},
|
|
1088
|
+
});
|
|
1089
|
+
if (!response.ok) {
|
|
1090
|
+
res.status(response.status).json({ error: '获取skill详情失败' });
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
const data = yield response.json();
|
|
1094
|
+
const skill = data === null || data === void 0 ? void 0 : data.data;
|
|
1095
|
+
if (!skill) {
|
|
1096
|
+
res.status(404).json({ error: 'Skill不存在' });
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
res.json({
|
|
1100
|
+
id: skill.id,
|
|
1101
|
+
name: skill.name,
|
|
1102
|
+
description: skill.description,
|
|
1103
|
+
author: skill.author,
|
|
1104
|
+
stars: skill.stars,
|
|
1105
|
+
githubUrl: skill.githubUrl || skill.skillUrl,
|
|
1106
|
+
skillPath: skill.skillPath || '',
|
|
1107
|
+
readme: skill.readme,
|
|
1108
|
+
tags: skill.tags || [],
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
catch (error) {
|
|
1112
|
+
console.error('获取skill详情失败:', error);
|
|
1113
|
+
res.status(500).json({ error: error.message });
|
|
1114
|
+
}
|
|
1115
|
+
})));
|
|
1116
|
+
app.post('/api/skills/:skillId/enable', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1117
|
+
const { skillId } = req.params;
|
|
1118
|
+
const { targetType } = req.body;
|
|
1119
|
+
if (!targetType || (targetType !== 'claude-code' && targetType !== 'codex')) {
|
|
1120
|
+
res.status(400).json({ success: false, error: '无效的目标类型' });
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
const centralDir = getCentralSkillsDir();
|
|
1124
|
+
const skillDir = path_1.default.join(centralDir, skillId);
|
|
1125
|
+
if (!fs_1.default.existsSync(skillDir)) {
|
|
1126
|
+
res.status(404).json({ success: false, error: 'Skill不存在' });
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
const symlinkResult = yield createSkillSymlink(skillId, targetType);
|
|
1130
|
+
if (!symlinkResult.success) {
|
|
1131
|
+
res.status(500).json({ success: false, error: symlinkResult.error });
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
const metadataPath = path_1.default.join(skillDir, 'skill.json');
|
|
1135
|
+
if (fs_1.default.existsSync(metadataPath)) {
|
|
1136
|
+
const metadata = JSON.parse(fs_1.default.readFileSync(metadataPath, 'utf-8'));
|
|
1137
|
+
if (!metadata.enabledTargets) {
|
|
1138
|
+
metadata.enabledTargets = [];
|
|
1139
|
+
}
|
|
1140
|
+
if (!metadata.enabledTargets.includes(targetType)) {
|
|
1141
|
+
metadata.enabledTargets.push(targetType);
|
|
1142
|
+
}
|
|
1143
|
+
fs_1.default.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
|
|
1144
|
+
}
|
|
1145
|
+
res.json({ success: true });
|
|
1146
|
+
})));
|
|
1147
|
+
app.post('/api/skills/:skillId/disable', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1148
|
+
const { skillId } = req.params;
|
|
1149
|
+
const { targetType } = req.body;
|
|
1150
|
+
if (!targetType || (targetType !== 'claude-code' && targetType !== 'codex')) {
|
|
1151
|
+
res.status(400).json({ success: false, error: '无效的目标类型' });
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
const symlinkResult = yield removeSkillSymlink(skillId, targetType);
|
|
1155
|
+
if (!symlinkResult.success) {
|
|
1156
|
+
res.status(500).json({ success: false, error: symlinkResult.error });
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
const centralDir = getCentralSkillsDir();
|
|
1160
|
+
const skillDir = path_1.default.join(centralDir, skillId);
|
|
1161
|
+
const metadataPath = path_1.default.join(skillDir, 'skill.json');
|
|
1162
|
+
if (fs_1.default.existsSync(metadataPath)) {
|
|
1163
|
+
const metadata = JSON.parse(fs_1.default.readFileSync(metadataPath, 'utf-8'));
|
|
1164
|
+
if (metadata.enabledTargets) {
|
|
1165
|
+
metadata.enabledTargets = metadata.enabledTargets.filter((t) => t !== targetType);
|
|
1166
|
+
fs_1.default.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
res.json({ success: true });
|
|
1170
|
+
})));
|
|
1171
|
+
app.delete('/api/skills/:skillId', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1172
|
+
const { skillId } = req.params;
|
|
1173
|
+
const centralDir = getCentralSkillsDir();
|
|
1174
|
+
const skillDir = path_1.default.join(centralDir, skillId);
|
|
1175
|
+
if (!fs_1.default.existsSync(skillDir)) {
|
|
1176
|
+
res.status(404).json({ success: false, error: 'Skill不存在' });
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
['claude-code', 'codex'].forEach((targetType) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1180
|
+
yield removeSkillSymlink(skillId, targetType);
|
|
1181
|
+
}));
|
|
1182
|
+
fs_1.default.rmSync(skillDir, { recursive: true, force: true });
|
|
1183
|
+
res.json({ success: true });
|
|
1184
|
+
})));
|
|
458
1185
|
app.post('/api/write-config/claude', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
459
1186
|
const result = yield writeClaudeConfig(dbManager);
|
|
460
1187
|
res.json(result);
|